diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 0000000..4d6f10d --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,56 @@ +name: Go Test Reminder + +on: + pull_request: + paths: + - '.github/workflows/go-test.yml' + - 'go.mod' + - 'go.sum' + - '**/*.go' + push: + branches: + - master + paths: + - '.github/workflows/go-test.yml' + - 'go.mod' + - 'go.sum' + - '**/*.go' + +permissions: + contents: read + +jobs: + go-test: + name: Go test reminder + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + cache: true + + - name: Run Go tests as reminder + id: go_test + shell: bash + run: | + set +e + go test ./... 2>&1 | tee go-test.log + status="${PIPESTATUS[0]}" + if [ "$status" -ne 0 ]; then + echo "failed=true" >> "$GITHUB_OUTPUT" + echo "::warning::go test ./... failed with exit code $status. This workflow is advisory and does not block release packaging." + else + echo "failed=false" >> "$GITHUB_OUTPUT" + fi + exit 0 + + - name: Write reminder summary + if: steps.go_test.outputs.failed == 'true' + run: | + echo '## Go test reminder' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo '`go test ./...` failed. This workflow is advisory and does not block release packaging.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..f2d85a5 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,62 @@ +name: Python Test Reminder + +on: + pull_request: + paths: + - '.github/workflows/python-test.yml' + - 'video_subtitle/**' + push: + branches: + - master + paths: + - '.github/workflows/python-test.yml' + - 'video_subtitle/**' + +permissions: + contents: read + +jobs: + python-test: + name: Python test reminder + runs-on: ubuntu-latest + env: + PYTHON_VERSION: '3.12.x' + PYTHONDONTWRITEBYTECODE: '1' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run video_subtitle tests as reminder + id: python_test + shell: bash + run: | + set +e + python3 -m pip install -r video_subtitle/requirements.txt + install_status="$?" + if [ "$install_status" -ne 0 ]; then + echo "failed=true" >> "$GITHUB_OUTPUT" + echo "::warning::python3 -m pip install -r video_subtitle/requirements.txt failed with exit code $install_status. Python tests were skipped. This workflow is advisory and does not block release packaging." + exit 0 + fi + + python3 -m unittest video_subtitle/video_subtitle_test.py 2>&1 | tee python-test.log + status="${PIPESTATUS[0]}" + if [ "$status" -ne 0 ]; then + echo "failed=true" >> "$GITHUB_OUTPUT" + echo "::warning::python3 -m unittest video_subtitle/video_subtitle_test.py failed with exit code $status. This workflow is advisory and does not block release packaging." + else + echo "failed=false" >> "$GITHUB_OUTPUT" + fi + exit 0 + + - name: Write reminder summary + if: steps.python_test.outputs.failed == 'true' + run: | + echo '## Python test reminder' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo '`python3 -m unittest video_subtitle/video_subtitle_test.py` failed. This workflow is advisory and does not block release packaging.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..71eb868 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,162 @@ +name: Release + +on: + pull_request: + paths: + - '.github/workflows/release.yml' + - '.github/workflows/go-test.yml' + - '.github/workflows/python-test.yml' + - 'docs/release.md' + - 'README.MD' + - 'AGENTS.md' + - 'go.mod' + - 'go.sum' + - '**/*.go' + - 'video_subtitle/**' + - 'emby_plugins/video_subtitle/**' + - 'sample/life_tools/**' + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Build release assets + runs-on: ubuntu-latest + env: + GO_VERSION: '1.21.x' + PYTHON_VERSION: '3.12.x' + DOTNET_VERSION: '8.0.x' + PYTHONDONTWRITEBYTECODE: '1' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Compile video_subtitle Python files + run: | + python3 -m py_compile \ + video_subtitle/video_subtitle.py \ + video_subtitle/video_subtitle_test.py \ + video_subtitle/lib/*.py + + - name: Run Emby plugin tests + run: dotnet test emby_plugins/video_subtitle/LifeTools.Emby.VideoSubtitle.sln --configuration Release + + - name: Build release assets + shell: bash + run: | + set -euo pipefail + + tag="${GITHUB_REF_NAME}" + if [ "${GITHUB_REF_TYPE:-}" != "tag" ]; then + tag="v0.0.0-ci" + fi + root="$PWD" + dist="$root/dist" + tmp="$root/.release-tmp" + platforms=(linux/amd64 linux/arm64 darwin/amd64 darwin/arm64) + + rm -rf "$dist" "$tmp" + mkdir -p "$dist" "$tmp" + + for platform in "${platforms[@]}"; do + os="${platform%/*}" + arch="${platform#*/}" + pkg="life_tools_${os}_${arch}_${tag}" + out="$tmp/$pkg" + mkdir -p "$out/bin" "$out/sample/life_tools" "$out/docs" + + GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "-s -w" -o "$out/bin/renameV1" ./renameV1/... + GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "-s -w" -o "$out/bin/check_keywords" ./save_work/... + GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "-s -w" -o "$out/bin/retry_exec" ./retry_exec/... + GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "-s -w" -o "$out/bin/codex_hook_notify" ./codex_hook_notify/... + GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags "-s -w" -o "$out/bin/file_share" ./file_share/... + + cp README.MD install.sh "$out/" + cp sample/life_tools/*.json "$out/sample/life_tools/" + cp docs/install.md docs/codex_hook_notify.md docs/file_share.md "$out/docs/" + cp docs/release.md "$out/docs/" + + ( + cd "$tmp" + zip -qr "$dist/$pkg.zip" "$pkg" + ) + done + + video_pkg="life_tools_video_subtitle_source_${tag}" + video_out="$tmp/$video_pkg" + mkdir -p "$video_out/video_subtitle" "$video_out/sample/life_tools" "$video_out/docs" + cp -R video_subtitle/*.py video_subtitle/lib video_subtitle/prompts video_subtitle/requirements.txt "$video_out/video_subtitle/" + cp sample/life_tools/video_subtitle.json "$video_out/sample/life_tools/" + cp README.MD install.sh "$video_out/" + cp docs/video_subtitle.md docs/release.md "$video_out/docs/" + ( + cd "$tmp" + zip -qr "$dist/$video_pkg.zip" "$video_pkg" + ) + + dotnet build emby_plugins/video_subtitle/LifeTools.Emby.VideoSubtitle.sln --configuration Release + plugin_pkg="life_tools_emby_video_subtitle_plugin_${tag}" + plugin_out="$tmp/$plugin_pkg" + mkdir -p "$plugin_out" "$plugin_out/docs" "$plugin_out/emby_plugins/video_subtitle" + cp emby_plugins/video_subtitle/src/LifeTools.Emby.VideoSubtitle.Emby/bin/Release/netstandard2.0/LifeTools.Emby.VideoSubtitle.Emby.dll "$plugin_out/" + cp emby_plugins/video_subtitle/install.sh emby_plugins/video_subtitle/build.sh "$plugin_out/emby_plugins/video_subtitle/" + cp docs/emby_video_subtitle_plugin.md docs/video_subtitle.md docs/release.md "$plugin_out/docs/" + { + echo '# Life Tools Emby Video Subtitle Plugin' + echo + echo 'Copy only `LifeTools.Emby.VideoSubtitle.Emby.dll` into the Emby plugins directory, then restart Emby.' + echo + echo 'Default local install:' + echo + echo '```bash' + echo 'sudo install -m 0644 LifeTools.Emby.VideoSubtitle.Emby.dll /var/lib/emby/plugins/LifeTools.Emby.VideoSubtitle.Emby.dll' + echo 'sudo systemctl restart emby-server' + echo '```' + echo + echo 'The plugin calls `/usr/local/bin/video_subtitle` by default. Install and configure `video_subtitle` before submitting jobs.' + } > "$plugin_out/README.md" + ( + cd "$tmp" + zip -qr "$dist/$plugin_pkg.zip" "$plugin_pkg" + ) + + ( + cd "$dist" + sha256sum *.zip > checksums.txt + ) + + ls -lh "$dist" + + - name: Create GitHub release + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME}" + title="life_tools ${tag}" + if gh release view "$tag" >/dev/null 2>&1; then + gh release upload "$tag" dist/*.zip dist/checksums.txt --clobber + else + gh release create "$tag" dist/*.zip dist/checksums.txt --title "$title" --generate-notes + fi diff --git a/AGENTS.md b/AGENTS.md index 82320d2..4cd55bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,6 +139,19 @@ emby_plugins/video_subtitle/install.sh --help - 只改和任务直接相关的文件。 - 不使用 `git reset --hard`、`git checkout --`、批量删除或其他破坏性命令,除非用户明确要求。 + +## Release Workflow 规则 + +`.github/workflows/release.yml` 负责 tag 发布,不是普通 CI。修改发布流程时要同时关注 Go、Python `video_subtitle` 和 Emby 插件三类产物。 + +- tag 触发规则保持 `v*`,避免普通分支 push 意外创建 Release;`pull_request` 只能做 dry-run,不能创建 Release。 +- Go 测试放在 `.github/workflows/go-test.yml`,Python 单元测试放在 `.github/workflows/python-test.yml`,测试失败只能写 GitHub warning 和 summary,不能让 reminder workflow 或 release workflow 失败。 +- Go 二进制包只放稳定 CLI 工具:`renameV1`、`check_keywords`、`retry_exec`、`codex_hook_notify`、`file_share`。 +- `video_subtitle` 只能按源码包发布,不能宣传成免依赖二进制;它仍依赖 Python、ffmpeg、TOS、ASR 和 LLM 配置。 +- Emby 插件包只放 `LifeTools.Emby.VideoSubtitle.Emby.dll` 和文档,不要把 `MediaBrowser.*`、`Emby.*` 或核心库 DLL 打进插件发布包。 +- 修改发布包内容时同步更新 `docs/release.md` 和 README 的发布入口。 +- 发布 workflow 需要 `permissions: contents: write`,不要扩大到无关权限。 + ## 文档规则 - README 已经是中文,新增仓库文档优先中文。 diff --git a/README.MD b/README.MD index ce06c30..8da8b7f 100644 --- a/README.MD +++ b/README.MD @@ -33,6 +33,10 @@ 完整安装说明和 AI Agent 快速安装步骤见:[docs/install.md](docs/install.md)。 +## 发布包 + +推送 `v*` tag 会通过 GitHub Actions 自动构建 GitHub Release 资产,包括 Linux/macOS Go 二进制包、`video_subtitle` 源码包和 Emby 插件包。发布流程和资产说明见:[docs/release.md](docs/release.md)。 + ## 哪些工具? ## retry_exec diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..fe8211b --- /dev/null +++ b/docs/release.md @@ -0,0 +1,130 @@ +# 发布说明 + +本仓库通过 GitHub Actions 在推送 `v*` tag 时自动构建发布包,并把 zip 上传到 GitHub Release。PR 会执行发布包打包 dry-run,但不会创建 Release。Go 和 Python 单元测试在单独 workflow 里运行,失败只作为提醒,不阻塞发布包流程。 + +## 触发方式 + +从最新 `master` 创建 tag 并推送。只有推送匹配 `v*` 的 tag 才会创建或更新 GitHub Release,PR 上的 release workflow 只做 dry-run: + +```bash +git switch master +git pull --ff-only origin master +git tag -a v0.0.3 -m "life_tools v0.0.3" +git push origin v0.0.3 +``` + +workflow 文件: + +```text +.github/workflows/release.yml +``` + +触发条件: + +```yaml +on: + pull_request: + paths: + - '.github/workflows/release.yml' + - '.github/workflows/go-test.yml' + - '.github/workflows/python-test.yml' + - 'docs/release.md' + - 'README.MD' + - 'AGENTS.md' + - 'go.mod' + - 'go.sum' + - '**/*.go' + - 'video_subtitle/**' + - 'emby_plugins/video_subtitle/**' + - 'sample/life_tools/**' + push: + tags: + - 'v*' +``` + +`pull_request` 在 release workflow 中只做 Python 编译检查、Emby 插件测试和打包 dry-run;只有 tag push 才执行 `gh release create` 或 `gh release upload`。Go 测试由 `.github/workflows/go-test.yml` 单独执行,Python 单元测试由 `.github/workflows/python-test.yml` 单独执行。测试失败时 reminder workflow 只写 GitHub warning 和 summary,本身仍返回成功,不阻塞发布包流程。 + +## 发布操作流程 + +1. 先把发布相关 PR 合并到 `master`。 +2. 在本地同步最新 `master`,创建新的 `v*` tag,并推送到远端。不要复用已经发布过的 tag;新版本用新 tag。 +3. 打开 GitHub Actions 的 `Release` workflow,确认 tag 触发的 `Build release assets` job 成功。 +4. 打开 GitHub 仓库的 Releases 页面,进入对应 tag,例如 `v0.0.3`,下载需要的 zip。 + +也可以用 GitHub CLI 下载产物: + +```bash +gh release view v0.0.3 --web +gh release download v0.0.3 --dir /tmp/life_tools_v0.0.3 +cd /tmp/life_tools_v0.0.3 +sha256sum -c checksums.txt +``` + +如果 tag push 后没有出现 Release,先检查 tag 名是否以 `v` 开头,再检查 Actions 里的 `Release` workflow 日志。 + +## 发布包 + +每次 tag 发布会生成这些资产: + +```text +life_tools_linux_amd64_.zip +life_tools_linux_arm64_.zip +life_tools_darwin_amd64_.zip +life_tools_darwin_arm64_.zip +life_tools_video_subtitle_source_.zip +life_tools_emby_video_subtitle_plugin_.zip +checksums.txt +``` + +Go 二进制包包含: + +```text +bin/renameV1 +bin/check_keywords +bin/retry_exec +bin/codex_hook_notify +bin/file_share +install.sh +sample/life_tools/*.json +docs/*.md +``` + +`video_subtitle` 发布包包含 Python 源码、prompts、`requirements.txt`、示例配置和文档。它不是纯二进制工具,使用前仍需要 Python 依赖、ffmpeg、TOS、ASR、LLM 配置。 + +Emby 插件发布包只包含部署需要的插件 DLL 和文档。安装到 Emby 插件目录时只复制: + +```text +LifeTools.Emby.VideoSubtitle.Emby.dll +``` + +不要把 `MediaBrowser.*`、`Emby.*` 或核心库 DLL 放进 Emby 插件目录。 + +## CI 验证 + +发布前 release workflow 会运行: + +```bash +python3 -m py_compile video_subtitle/video_subtitle.py video_subtitle/video_subtitle_test.py 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 +``` + +测试提示 workflow 会运行: + +```bash +go test ./... +python3 -m unittest video_subtitle/video_subtitle_test.py +``` + +这些 workflow 失败时只写 GitHub warning 和 summary,不阻塞 release workflow,也不阻止 tag 发布资产。 + +## 权限 + +Release workflow 需要: + +```yaml +permissions: + contents: write +``` + +这是 `gh release create` 和 `gh release upload` 上传资产所需的最小仓库权限。