From b80643a185f2e824b5f26540e0dffd7b7c64a83b Mon Sep 17 00:00:00 2001
From: mcoder2014 <17683497+mcoder2014@users.noreply.github.com>
Date: Sun, 26 Jul 2026 20:38:23 +0800
Subject: [PATCH 1/3] docs: design Ulanzi release asset flow
---
...6-07-26-command-executor-release-design.md | 73 +++++++
...026-07-26-command-executor-release-plan.md | 182 ++++++++++++++++++
plugins/unlanzi_d200x/docs/README.md | 2 +
3 files changed, 257 insertions(+)
create mode 100644 plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-design.md
create mode 100644 plugins/unlanzi_d200x/docs/2026-07-26-command-executor-release-plan.md
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 快照、运行模型、测试、构建、安装和调试 |
From 4ede97cc12ece032869f09273f738cb992124c33 Mon Sep 17 00:00:00 2001
From: mcoder2014 <17683497+mcoder2014@users.noreply.github.com>
Date: Sun, 26 Jul 2026 20:52:26 +0800
Subject: [PATCH 2/3] fix: publish Ulanzi command executor release asset
---
.github/workflows/release.yml | 66 ++++++++++++++++++-
docs/release.md | 27 +++++++-
.../docs/command-executor-installation.md | 33 +++++++++-
tests/release_workflow_test.py | 53 +++++++++++++++
4 files changed, 173 insertions(+), 6 deletions(-)
create mode 100644 tests/release_workflow_test.py
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/release.md b/docs/release.md
index 50b582c..2e2d231 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_.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()
From a3171a5a5513a5af308c6cbddc8c81103fd73503 Mon Sep 17 00:00:00 2001
From: mcoder2014 <17683497+mcoder2014@users.noreply.github.com>
Date: Sun, 26 Jul 2026 21:56:16 +0800
Subject: [PATCH 3/3] test: skip flaky Alfred pasteboard case in CI
---
.github/workflows/alfred-workflow.yml | 2 ++
docs/plugins/alfred_remote_upload.md | 2 ++
docs/release.md | 4 +++-
.../tests/clipboard_integration.sh | 12 ++++++++----
plugins/alfred_remote_upload/tests/run.sh | 5 +++++
5 files changed, 20 insertions(+), 5 deletions(-)
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/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 2e2d231..8222a18 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -169,12 +169,14 @@ 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
diff --git a/plugins/alfred_remote_upload/tests/clipboard_integration.sh b/plugins/alfred_remote_upload/tests/clipboard_integration.sh
index 840c206..e3f0e61 100755
--- a/plugins/alfred_remote_upload/tests/clipboard_integration.sh
+++ b/plugins/alfred_remote_upload/tests/clipboard_integration.sh
@@ -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")"
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