diff --git a/.github/workflows/alfred-workflow.yml b/.github/workflows/alfred-workflow.yml index e426516..27cfcad 100644 --- a/.github/workflows/alfred-workflow.yml +++ b/.github/workflows/alfred-workflow.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95d60e4..ce02ae7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' @@ -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: @@ -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 @@ -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 }} @@ -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 diff --git a/docs/plugins/alfred_remote_upload.md b/docs/plugins/alfred_remote_upload.md index 48815fc..5b6e80b 100644 --- a/docs/plugins/alfred_remote_upload.md +++ b/docs/plugins/alfred_remote_upload.md @@ -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 中的用户值独立保存,不会被仓库中的默认值覆盖。 ## 配置 diff --git a/docs/release.md b/docs/release.md index 50b582c..8222a18 100644 --- a/docs/release.md +++ b/docs/release.md @@ -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 验证。 ## 触发方式 @@ -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 ``` 触发条件: @@ -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' @@ -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_.zip`,解压后把 `InterviewTimer.app` 放到 `~/Applications` 或 `/Applications`。 @@ -82,6 +89,7 @@ life_tools_darwin_arm64_.zip life_tools_video_subtitle_source_.zip life_tools_emby_video_subtitle_plugin_.zip life_tools_alfred_remote_upload_.alfredworkflow +life_tools_ulanzi_d200x_command_executor_.zip life_tools_interview_timer_macos_.zip life_tools_interview_timer_macos_.sha256 checksums.txt @@ -123,6 +131,8 @@ InterviewTimer.app Alfred 产物是可直接导入 Alfred 5 的 `life_tools_alfred_remote_upload_.alfredworkflow`。用户的 `hosts_json` 和 MRU 状态由 Alfred 及其 workflow data 目录管理,不进入发布包。 +Ulanzi 产物 `life_tools_ulanzi_d200x_command_executor_.zip` 的最外层是 `com.ulanzi.commandexecutor.ulanziPlugin`,可按 [Ulanzi D200X 命令执行器安装说明](../plugins/unlanzi_d200x/docs/command-executor-installation.md) 解压到 Studio 插件目录。历史 Release 不会因 workflow 更新自动重建;只有包含该发布逻辑的新 tag 会自动附带此资产。 + ## CI 验证 发布前 release workflow 会运行: @@ -131,6 +141,7 @@ Alfred 产物是可直接导入 Alfred 5 的 `life_tools_alfred_remote_upload_ "$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")" diff --git a/plugins/alfred_remote_upload/tests/run.sh b/plugins/alfred_remote_upload/tests/run.sh index d6b979a..57900df 100755 --- a/plugins/alfred_remote_upload/tests/run.sh +++ b/plugins/alfred_remote_upload/tests/run.sh @@ -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" @@ -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 diff --git a/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-design.md b/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-design.md new file mode 100644 index 0000000..4800230 --- /dev/null +++ b/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-design.md @@ -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
Go / Python / Emby / Alfred"] + TAG --> ULANZI["macOS Ulanzi job
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_.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 不包含构建产物、用户路径或本机凭据。 diff --git a/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-plan.md b/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-plan.md new file mode 100644 index 0000000..b42e412 --- /dev/null +++ b/plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-plan.md @@ -0,0 +1,182 @@ +# Command Executor Release Asset Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make every future `v*` GitHub Release contain a versioned, checksummed Ulanzi D200X command executor installation zip. + +**Architecture:** Keep the existing Ubuntu release builder and add one macOS Ulanzi builder in the same workflow. The existing publish job remains the only Release writer; it downloads both artifacts, regenerates `checksums.txt`, and uploads the combined set. + +**Tech Stack:** GitHub Actions YAML, Bash, Node.js 20, npm, macOS `ditto`, Python standard-library contract test. + +--- + +### Task 1: Add a failing Release contract test + +**Files:** +- Create: `tests/release_workflow_test.py` + +- [ ] **Step 1: Write the failing test** + +Create a standard-library Python test that reads `.github/workflows/release.yml` and asserts: + +```python +required_fragments = [ + "ulanzi-release:", + "runs-on: macos-latest", + "name: Build Ulanzi release asset", + "plugins/unlanzi_d200x/command_executor", + "life_tools_ulanzi_d200x_command_executor_${tag}.zip", + "name: ulanzi-release-asset", + "needs: [release, ulanzi-release]", + "sha256sum *.zip *.alfredworkflow > checksums.txt", +] +``` + +Also assert that the Ulanzi artifact download occurs before final checksum generation and before `gh release create/upload`. + +- [ ] **Step 2: Verify RED** + +Run: + +```bash +python3 -m unittest tests/release_workflow_test.py -v +``` + +Expected: failure reporting the missing `ulanzi-release` job. + +### Task 2: Add the macOS Ulanzi build job + +**Files:** +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Add `ulanzi-release`** + +The job must: + +```yaml +ulanzi-release: + name: Build Ulanzi release asset + runs-on: macos-latest +``` + +Use `actions/setup-node@v4` with Node.js 20 and the command executor lockfile cache. Run `npm ci`, `npm test`, `bash -n build.sh`, and `./build.sh` in `plugins/unlanzi_d200x/command_executor`. + +- [ ] **Step 2: Create the versioned asset** + +Resolve the tag exactly like the existing release job: + +```bash +tag="${GITHUB_REF_NAME}" +if [ "${GITHUB_REF_TYPE:-}" != "tag" ]; then + tag="v0.0.0-ci" +fi +``` + +Copy the validated build output to: + +```text +$RUNNER_TEMP/ulanzi-release/life_tools_ulanzi_d200x_command_executor_${tag}.zip +``` + +- [ ] **Step 3: Upload the job artifact** + +Use `actions/upload-artifact@v4`, artifact name `ulanzi-release-asset`, and `if-no-files-found: error`. + +### Task 3: Aggregate assets in the single publish job + +**Files:** +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Require both build jobs** + +Set: + +```yaml +needs: [release, ulanzi-release] +``` + +- [ ] **Step 2: Download the Ulanzi artifact** + +Download `ulanzi-release-asset` into `dist` after downloading `release-assets`. + +- [ ] **Step 3: Regenerate final checksums** + +Before calling `gh release`, run: + +```bash +cd dist +sha256sum *.zip *.alfredworkflow > checksums.txt +``` + +The existing create-or-upload behavior remains unchanged. + +- [ ] **Step 4: Verify GREEN** + +Run: + +```bash +python3 -m unittest tests/release_workflow_test.py -v +``` + +Expected: all tests pass. + +### Task 4: Update release documentation + +**Files:** +- Modify: `docs/release.md` +- Modify: `plugins/unlanzi_d200x/docs/README.md` + +- [ ] **Step 1: Document the new asset** + +Add: + +```text +life_tools_ulanzi_d200x_command_executor_.zip +``` + +Explain that it is built on macOS, is directly installable in Ulanzi Studio, and is included in `checksums.txt`. + +- [ ] **Step 2: Document historical-version behavior** + +State that an existing Release is not automatically rebuilt after workflow changes; backfills require an explicit upload or a new tag. + +### Task 5: Run full relevant verification + +**Files:** +- No source changes. + +- [ ] **Step 1: Run Release contract tests** + +```bash +python3 -m unittest tests/release_workflow_test.py -v +``` + +- [ ] **Step 2: Parse workflow YAML** + +```bash +ruby -e 'require "yaml"; YAML.load_file(".github/workflows/release.yml", aliases: true); puts "workflow yaml ok"' +``` + +- [ ] **Step 3: Install and test Ulanzi dependencies** + +```bash +cd plugins/unlanzi_d200x/command_executor +npm ci +npm test +./build.sh +``` + +Expected: 75 tests pass, package validation passes, and zip integrity passes. + +- [ ] **Step 4: Validate documentation diagram** + +Render the Mermaid diagram from the design document with `mmdc`. + +- [ ] **Step 5: Review repository state** + +```bash +git diff --check +git status --short +``` + +Confirm no generated output, `node_modules`, user-specific path, or credential is tracked. diff --git a/plugins/unlanzi_d200x/docs/README.md b/plugins/unlanzi_d200x/docs/README.md index aaac6e1..cf889aa 100644 --- a/plugins/unlanzi_d200x/docs/README.md +++ b/plugins/unlanzi_d200x/docs/README.md @@ -8,6 +8,8 @@ |---|---|---| | [命令执行器设计](2026-07-25-command-executor-design.md) | 已有 | 定义 `command_executor` 的范围、配置模型、运行行为和验收标准 | | [命令执行器实现计划](2026-07-25-command-executor-implementation-plan.md) | 已有 | 按 TDD 顺序拆解源码、构建、文档和 Studio/D200X 实机验证 | +| [命令执行器 Release 资产设计](2026-07-26-command-executor-release-design.md) | 已有 | 定义 macOS 构建、统一发布、资产命名和校验和边界 | +| [命令执行器 Release 资产实现计划](2026-07-26-command-executor-release-plan.md) | 已有 | 按契约测试拆解 workflow、文档和发布验证 | | [Ulanzi 插件开发参考](ulanzi-plugin-development-reference.md) | 已有 | 提炼官方开发指南、本地安装指南和 SDK 的核心接入知识 | | [命令执行器安装说明](command-executor-installation.md) | 已实机验证 | 说明 macOS 构建、安装、升级、回滚、验收、日志和卸载 | | [命令执行器开发指南](command-executor-development-guide.md) | 已完成 | 记录目录职责、SDK 快照、运行模型、测试、构建、安装和调试 | diff --git a/plugins/unlanzi_d200x/docs/command-executor-installation.md b/plugins/unlanzi_d200x/docs/command-executor-installation.md index db83674..5134504 100644 --- a/plugins/unlanzi_d200x/docs/command-executor-installation.md +++ b/plugins/unlanzi_d200x/docs/command-executor-installation.md @@ -14,7 +14,33 @@ 本机实测环境是 macOS 14.7.8、Ulanzi Studio 3.1.9 和 D200X。插件已经安装并由实体按键执行成功。 -## 2. 从源码构建 +## 2. 获取安装包 + +### 2.1 从 GitHub Release 下载 + +优先从 [GitHub Releases](https://github.com/mcoder2014/life_tools/releases) 下载与版本 tag 同名的安装包: + +```text +life_tools_ulanzi_d200x_command_executor_.zip +``` + +例如,发布版本是 `v0.0.8` 时,文件名是: + +```text +life_tools_ulanzi_d200x_command_executor_v0.0.8.zip +``` + +也可以使用 GitHub CLI 下载: + +```bash +tag="v0.0.8" +gh release download "$tag" \ + --pattern "life_tools_ulanzi_d200x_command_executor_${tag}.zip" +``` + +已经发布的历史版本不会因发布流程更新而自动补齐资产。如果对应 Release 没有该 zip,请使用包含此发布逻辑的新版本,或按下一节从源码构建。 + +### 2.2 从源码构建 在仓库根目录执行: @@ -69,12 +95,15 @@ zip 的最外层已经是 `.ulanziPlugin` 目录,可直接解压到插件根 ```bash plugin_root="$HOME/Library/Application Support/Ulanzi/UlanziDeck/Plugins" +archive="life_tools_ulanzi_d200x_command_executor_v0.0.8.zip" mkdir -p "$plugin_root" /usr/bin/ditto -x -k \ - output/life_tools_ulanzi_d200x_command_executor.zip \ + "$archive" \ "$plugin_root" ``` +从源码构建时,把 `archive` 改为 `output/life_tools_ulanzi_d200x_command_executor.zip`。 + 安装后应存在: ```text diff --git a/tests/release_workflow_test.py b/tests/release_workflow_test.py new file mode 100644 index 0000000..2f8c314 --- /dev/null +++ b/tests/release_workflow_test.py @@ -0,0 +1,53 @@ +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +RELEASE_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "release.yml" + + +class ReleaseWorkflowTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") + + def test_builds_versioned_ulanzi_asset_on_macos(self): + self.assertIn("- 'tests/release_workflow_test.py'", self.workflow) + self.assertIn( + "python3 -m unittest tests/release_workflow_test.py -v", self.workflow + ) + + job_start = self.workflow.index(" ulanzi-release:") + publish_start = self.workflow.index(" publish:", job_start) + ulanzi_job = self.workflow[job_start:publish_start] + required_fragments = [ + "name: Build Ulanzi release asset", + "runs-on: macos-latest", + "plugins/unlanzi_d200x/command_executor", + "run: ./build.sh", + "life_tools_ulanzi_d200x_command_executor_${tag}.zip", + "name: ulanzi-release-asset", + "path: ${{ runner.temp }}/ulanzi-release/", + "if-no-files-found: error", + ] + + for fragment in required_fragments: + with self.subTest(fragment=fragment): + self.assertIn(fragment, ulanzi_job) + + def test_publish_combines_ulanzi_before_checksums_and_release_write(self): + publish_start = self.workflow.index(" publish:") + publish_job = self.workflow[publish_start:] + self.assertIn("needs: [release, ulanzi-release]", publish_job) + download_index = publish_job.index("name: Download Ulanzi release asset") + checksum_index = publish_job.index( + "sha256sum *.zip *.alfredworkflow > checksums.txt", download_index + ) + release_index = publish_job.index('if gh release view "$tag"') + + self.assertLess(download_index, checksum_index) + self.assertLess(checksum_index, release_index) + + +if __name__ == "__main__": + unittest.main()