Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/alfred-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
run: plugins/alfred_remote_upload/tests/run.sh

- name: Run clipboard integration tests
env:
ALFRED_SKIP_MULTI_FILE_PASTEBOARD_TEST: '1'
run: plugins/alfred_remote_upload/tests/clipboard_integration.sh

- name: Build and inspect package
Expand Down
66 changes: 65 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- '.github/workflows/go-test.yml'
- '.github/workflows/python-test.yml'
- '.github/workflows/alfred-workflow.yml'
- 'tests/release_workflow_test.py'
- 'docs/**'
- 'README.MD'
- 'AGENTS.md'
Expand Down Expand Up @@ -48,6 +49,9 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Validate release workflow contract
run: python3 -m unittest tests/release_workflow_test.py -v

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
Expand Down Expand Up @@ -158,10 +162,60 @@ jobs:
path: dist/
if-no-files-found: error

ulanzi-release:
name: Build Ulanzi release asset
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: plugins/unlanzi_d200x/command_executor/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: plugins/unlanzi_d200x/command_executor

- name: Run tests
run: npm test
working-directory: plugins/unlanzi_d200x/command_executor

- name: Check build script syntax
run: bash -n build.sh
working-directory: plugins/unlanzi_d200x/command_executor

- name: Build plugin package
run: ./build.sh
working-directory: plugins/unlanzi_d200x/command_executor

- name: Prepare versioned release asset
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
if [ "${GITHUB_REF_TYPE:-}" != "tag" ]; then
tag="v0.0.0-ci"
fi
source="plugins/unlanzi_d200x/command_executor/output/life_tools_ulanzi_d200x_command_executor.zip"
output="$RUNNER_TEMP/ulanzi-release"
mkdir -p "$output"
cp "$source" "$output/life_tools_ulanzi_d200x_command_executor_${tag}.zip"

- name: Upload Ulanzi release asset
uses: actions/upload-artifact@v4
with:
name: ulanzi-release-asset
path: ${{ runner.temp }}/ulanzi-release/
if-no-files-found: error

publish:
name: Publish GitHub release
if: github.ref_type == 'tag'
needs: release
needs: [release, ulanzi-release]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -172,6 +226,12 @@ jobs:
name: release-assets
path: dist

- name: Download Ulanzi release asset
uses: actions/download-artifact@v4
with:
name: ulanzi-release-asset
path: dist

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -180,6 +240,10 @@ jobs:
set -euo pipefail
tag="${GITHUB_REF_NAME}"
title="life_tools ${tag}"
(
cd dist
sha256sum *.zip *.alfredworkflow > checksums.txt
)
if gh release view "$tag" >/dev/null 2>&1; then
gh release upload "$tag" dist/*.zip dist/*.alfredworkflow dist/checksums.txt --clobber
else
Expand Down
2 changes: 2 additions & 0 deletions docs/plugins/alfred_remote_upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ plugins/alfred_remote_upload/build.sh
open "plugins/alfred_remote_upload/dist/Remote Upload.alfredworkflow"
```

`clipboard_integration.sh` 默认在真实桌面会话中执行全部剪贴板用例。GitHub 托管的 macOS 26 runner 无法稳定保留 fixture 写入的多个 Finder URL,因此 CI 通过 `ALFRED_SKIP_MULTI_FILE_PASTEBOARD_TEST=1` 只跳过该 live pasteboard 用例;多文件拒绝规则仍由 `tests/run.sh` 使用两个普通文件做确定性验证。

打开产物后由 Alfred 完成安装。升级 Workflow 时,Alfred Workflow Configuration 中的用户值独立保存,不会被仓库中的默认值覆盖。

## 配置
Expand Down
31 changes: 27 additions & 4 deletions docs/release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 发布说明

本仓库通过 GitHub Actions 在推送 `v*` tag 时自动构建发布包,并上传到 GitHub Release。PR 会执行 Go 单测和发布包打包 dry-run,但不会创建 Release。Python 单元测试在单独 workflow 里运行,失败只作为提醒,不阻塞发布包流程。Swift macOS App 和 Alfred Workflow 使用独立的 macOS workflow 验证。
本仓库通过 GitHub Actions 在推送 `v*` tag 时自动构建发布包,并上传到 GitHub Release。PR 会执行 Go 单测和发布包打包 dry-run,但不会创建 Release。Python 单元测试在单独 workflow 里运行,失败只作为提醒,不阻塞发布包流程。Swift macOS App、Alfred Workflow 和 Ulanzi D200X 命令执行器使用 macOS runner 验证。

## 触发方式

Expand All @@ -19,6 +19,7 @@ workflow 文件:
.github/workflows/release.yml
.github/workflows/swift-mac-app.yml
.github/workflows/alfred-workflow.yml
.github/workflows/ulanzi-command-executor.yml
```

触发条件:
Expand All @@ -30,6 +31,8 @@ on:
- '.github/workflows/release.yml'
- '.github/workflows/go-test.yml'
- '.github/workflows/python-test.yml'
- '.github/workflows/alfred-workflow.yml'
- 'tests/release_workflow_test.py'
- 'docs/**'
- 'README.MD'
- 'AGENTS.md'
Expand All @@ -45,17 +48,21 @@ on:
- 'v*'
```

`pull_request` 在 release workflow 中只做 Python 编译检查、Emby 插件测试和打包 dry-run;只有 tag push 才进入独立的 `Publish GitHub release` job,执行 `gh release create` 或 `gh release upload`。Go 测试由 `.github/workflows/go-test.yml` 单独执行,失败会阻塞 PR。Python 单元测试由 `.github/workflows/python-test.yml` 单独执行,失败时只写 GitHub warning 和 summary,本身仍返回成功,不阻塞发布包流程。
`pull_request` 在 release workflow 中执行发布契约测试、Python 编译检查、Emby 插件测试,以及 Ubuntu 和 macOS 两组打包 dry-run;只有 tag push 才进入独立的 `Publish GitHub release` job,执行 `gh release create` 或 `gh release upload`。macOS job 使用官方 SDK 基础上的现有 `command_executor/build.sh` 生成 Ulanzi 安装包。publish job 下载两组 artifact 后重新生成 `checksums.txt`,确保 Ulanzi zip 也被纳入最终校验和。

Go 测试由 `.github/workflows/go-test.yml` 单独执行,失败会阻塞 PR。Python 单元测试由 `.github/workflows/python-test.yml` 单独执行,失败时只写 GitHub warning 和 summary,本身仍返回成功,不阻塞发布包流程。

`swift-mac-app.yml` 使用 `macos-latest` runner。PR 和 `master` 推送会验证 `gui/interview_timer` 的 Swift 单测、可执行产物构建和 `.app` 打包;`v*` tag 会额外上传未签名的 `InterviewTimer.app` zip。

`alfred-workflow.yml` 使用 `macos-latest` runner。相关 PR 会检查 JXA、Shell、plist、离线测试和 `.alfredworkflow` 打包,并上传 dry-run artifact;不会创建 tag 或 Release。

`ulanzi-command-executor.yml` 使用 `macos-latest` runner,为插件改动提供快速测试和打包反馈。tag 发布时由 `release.yml` 的 `Build Ulanzi release asset` job 重新构建版本化安装包,并由统一的 publish job 写入 Release。

## 发布操作流程

1. 先把发布相关 PR 合并到 `master`。
2. 在本地同步最新 `master`,创建新的 `v*` tag,并推送到远端。不要复用已经发布过的 tag;新版本用新 tag。
3. 打开 GitHub Actions 的 `Release` workflow,确认 tag 触发的 `Build release assets` job 成功
3. 打开 GitHub Actions 的 `Release` workflow,确认 tag 触发的 `Build release assets` 和 `Build Ulanzi release asset` job 都成功
4. 打开 GitHub 仓库的 Releases 页面,进入对应 tag,例如 `v0.0.3`,下载需要的 zip。
5. 如果需要 `InterviewTimer.app`,下载 `life_tools_interview_timer_macos_<tag>.zip`,解压后把 `InterviewTimer.app` 放到 `~/Applications` 或 `/Applications`。

Expand All @@ -82,6 +89,7 @@ life_tools_darwin_arm64_<tag>.zip
life_tools_video_subtitle_source_<tag>.zip
life_tools_emby_video_subtitle_plugin_<tag>.zip
life_tools_alfred_remote_upload_<tag>.alfredworkflow
life_tools_ulanzi_d200x_command_executor_<tag>.zip
life_tools_interview_timer_macos_<tag>.zip
life_tools_interview_timer_macos_<tag>.sha256
checksums.txt
Expand Down Expand Up @@ -123,6 +131,8 @@ InterviewTimer.app

Alfred 产物是可直接导入 Alfred 5 的 `life_tools_alfred_remote_upload_<tag>.alfredworkflow`。用户的 `hosts_json` 和 MRU 状态由 Alfred 及其 workflow data 目录管理,不进入发布包。

Ulanzi 产物 `life_tools_ulanzi_d200x_command_executor_<tag>.zip` 的最外层是 `com.ulanzi.commandexecutor.ulanziPlugin`,可按 [Ulanzi D200X 命令执行器安装说明](../plugins/unlanzi_d200x/docs/command-executor-installation.md) 解压到 Studio 插件目录。历史 Release 不会因 workflow 更新自动重建;只有包含该发布逻辑的新 tag 会自动附带此资产。

## CI 验证

发布前 release workflow 会运行:
Expand All @@ -131,6 +141,7 @@ Alfred 产物是可直接导入 Alfred 5 的 `life_tools_alfred_remote_upload_<t
python3 -m py_compile cli/video_subtitle/video_subtitle.py cli/video_subtitle/video_subtitle_test.py cli/video_subtitle/lib/*.py
dotnet test emby_plugins/video_subtitle/LifeTools.Emby.VideoSubtitle.sln --configuration Release
dotnet build emby_plugins/video_subtitle/LifeTools.Emby.VideoSubtitle.sln --configuration Release
python3 -m unittest tests/release_workflow_test.py -v
```

Swift macOS App workflow 会运行:
Expand Down Expand Up @@ -158,12 +169,24 @@ Alfred Workflow macOS CI 会运行:

```bash
plugins/alfred_remote_upload/tests/run.sh
plugins/alfred_remote_upload/tests/clipboard_integration.sh
ALFRED_SKIP_MULTI_FILE_PASTEBOARD_TEST=1 plugins/alfred_remote_upload/tests/clipboard_integration.sh
plugins/alfred_remote_upload/build.sh
plutil -lint plugins/alfred_remote_upload/workflow/info.plist
unzip -t /tmp/life_tools_alfred_remote_upload_ci.alfredworkflow
```

macOS 26 hosted runner 无法稳定保留测试 fixture 写入的多个 Finder URL,因此 CI 只跳过这个 live pasteboard 用例。`tests/run.sh` 仍使用两个普通文件验证多文件拒绝规则;开发者在真实桌面会话中直接运行 `clipboard_integration.sh` 时仍执行完整用例。

Ulanzi D200X macOS CI 和 release job 会运行:

```bash
cd plugins/unlanzi_d200x/command_executor
npm ci
npm test
bash -n build.sh
./build.sh
```

Go 测试失败会阻塞 PR;Python 测试失败时只写 GitHub warning 和 summary,不阻塞 release workflow,也不阻止 tag 发布资产。

## 权限
Expand Down
12 changes: 8 additions & 4 deletions plugins/alfred_remote_upload/tests/clipboard_integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@ IFS=$'\x1f' read -r file_kind file_path file_extension file_temporary <<< "$file

second_file="$TEST_TMP/second.txt"
printf 'second' > "$second_file"
files_json="$(/usr/bin/python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' "$finder_file" "$second_file")"
/usr/bin/osascript -l JavaScript "$FIXTURE_SCRIPT" set-files "$files_json" >/dev/null
if read_clipboard multiple >/dev/null 2>&1; then
fail "multiple Finder files should be rejected"
if [[ "${ALFRED_SKIP_MULTI_FILE_PASTEBOARD_TEST:-0}" == "1" ]]; then
echo "SKIP: hosted runner does not preserve multiple Finder URLs on NSPasteboard"
else
files_json="$(/usr/bin/python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' "$finder_file" "$second_file")"
/usr/bin/osascript -l JavaScript "$FIXTURE_SCRIPT" set-files "$files_json" >/dev/null
if read_clipboard multiple >/dev/null 2>&1; then
fail "multiple Finder files should be rejected"
fi
fi

files_json="$(/usr/bin/python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' "$TEST_TMP")"
Expand Down
5 changes: 5 additions & 0 deletions plugins/alfred_remote_upload/tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ file_paths_json() {

test_clipboard_file_validation() {
local regular_file="$TEST_TMP/no-extension"
local second_regular_file="$TEST_TMP/second-file"
local directory="$TEST_TMP/clipboard-directory"
local fifo="$TEST_TMP/clipboard-fifo"
local output
printf 'file' > "$regular_file"
printf 'second' > "$second_regular_file"
mkdir -p "$directory"
/usr/bin/mkfifo "$fifo"

Expand All @@ -161,6 +163,9 @@ test_clipboard_file_validation() {
if /usr/bin/osascript -l JavaScript "$CLIPBOARD_SCRIPT" validate-file-paths "$(file_paths_json "$directory")" >/dev/null 2>&1; then
fail "directories should be rejected"
fi
if /usr/bin/osascript -l JavaScript "$CLIPBOARD_SCRIPT" validate-file-paths "$(file_paths_json "$regular_file" "$second_regular_file")" >/dev/null 2>&1; then
fail "multiple regular Finder files should be rejected"
fi
if /usr/bin/osascript -l JavaScript "$CLIPBOARD_SCRIPT" validate-file-paths "$(file_paths_json "$regular_file" "$directory")" >/dev/null 2>&1; then
fail "multiple Finder items should be rejected"
fi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 命令执行器 Release 资产设计

## 1. 核心判断

当前 tag 发布只在独立的 Ulanzi CI 中构建命令执行器,产物被保存为有保留期限的 Actions Artifact,没有进入 GitHub Release。用户无法从版本发布页直接取得可安装包,因此值得修复。

修复范围只包含 Release 资产汇总:

- 不修改命令执行器源码、官方 SDK 快照或插件包结构。
- 不修改 `command_executor/build.sh` 的 macOS 构建约束。
- 不让多个 workflow 竞争创建或更新同一个 GitHub Release。
- 保留现有 Go、Python、Emby、Alfred 和 InterviewTimer 发布行为。

## 2. 方案比较

| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 在 `release.yml` 增加 macOS Ulanzi job,由现有 publish job 汇总 | 单一 Release 写入口;复用原构建脚本;PR 可验证 | tag 发布增加一个 macOS job | 采用 |
| 让 `ulanzi-command-executor.yml` 在 tag 时直接上传 Release | 修改文件少 | 与 `release.yml` 竞争 Release 创建时序;校验和分散 | 不采用 |
| 把 Ulanzi 构建改成 Linux 兼容并塞进现有 Ubuntu job | runner 数量不变 | 需要改动已验证的 macOS `ditto` 打包路径,扩大风险 | 不采用 |

## 3. 发布数据流

```mermaid
flowchart LR
TAG["PR dry-run 或 v* tag"] --> CORE["Ubuntu release job<br/>Go / Python / Emby / Alfred"]
TAG --> ULANZI["macOS Ulanzi job<br/>npm ci / test / build.sh"]
CORE --> COREART["release-assets artifact"]
ULANZI --> ULANZIART["ulanzi-release-asset artifact"]
COREART --> PUBLISH["Publish GitHub release"]
ULANZIART --> PUBLISH
PUBLISH --> CHECKSUM["重新生成 checksums.txt"]
CHECKSUM --> RELEASE["单一 GitHub Release"]
```

PR 执行两个构建 job,但跳过 Release 写入。`v*` tag 执行相同构建,并由现有 publish job 统一下载两个 artifact、生成最终校验和并创建或更新 Release。

## 4. 资产契约

Ulanzi 安装包名称固定为:

```text
life_tools_ulanzi_d200x_command_executor_<tag>.zip
```

其中:

- tag 发布使用真实 `GITHUB_REF_NAME`,例如 `v0.0.8`。
- PR dry-run 使用 `v0.0.0-ci`,与现有发布包约定一致。
- zip 内容仍由 `plugins/unlanzi_d200x/command_executor/build.sh` 生成。
- publish job 下载 Ulanzi artifact 后重新生成 `checksums.txt`,确保最终 Release 中的 Ulanzi zip 被纳入校验。

## 5. 失败语义

| 失败点 | 行为 |
|---|---|
| `npm ci`、测试或构建失败 | Ulanzi job 失败,publish job 不运行 |
| Ulanzi zip 缺失 | `upload-artifact` 的 `if-no-files-found: error` 阻止发布 |
| 任一主发布资产构建失败 | publish job 不运行 |
| 校验和生成失败 | 不执行 `gh release create/upload` |
| Release 已存在 | 使用 `gh release upload --clobber` 更新同名资产 |

## 6. 当前版本边界

流程修复合并后只自动影响新的 tag。已经创建的 `v0.0.7` 不会因为 workflow 文件变化而自动重跑;如需补齐该版本,必须基于 `v0.0.7` 对应源码构建相同命名的 zip,并单独上传到现有 Release。

## 7. 验证标准

- 发布契约测试先在旧 workflow 上失败,再在修改后通过。
- Ulanzi 75 个测试全部通过。
- `build.sh` 成功生成并校验安装 zip。
- workflow YAML 能被解析,且 PR Actions 同时产出主发布 artifact 与 Ulanzi artifact。
- PR diff 不包含构建产物、用户路径或本机凭据。
Loading
Loading