diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a673446..0388f28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,25 @@ -# CI — 语法/结构冒烟 + 完整打包验证(零依赖,无 pnpm install) +# CI — 统一流程(条件分流,避免发版重复打包) # -# 触发: PR + push master +# 触发事件按场景分流(jobs 用 if 条件,单 workflow 覆盖全部): +# - pull_request → check + pack(打包验证,PR 期保证打包链健康) +# - push master(无 tag) → check only(打包已由 PR 期 pack 验证,不发版不重复打) +# - push tag v* → check + release(完整发版链,打包唯一发生点) +# - workflow_dispatch → check + pack(手动出包,产 artifact) # -# jobs: -# check: node --check 全部 js/mjs + manifest/skills 结构断言(快,秒级) -# pack-smoke: 完整打包链冒烟——fetch-vendor(cache 命中秒过)+ pack(产 zip/sha256) -# vendor 下载走 actions/cache(key 含 fetch-vendor.mjs hash:脚本变才重拉), -# PR 期即验证打包不坏,避免 tag 发版时才暴露 +# 为什么合一:此前 ci.yml + release.yml 双 workflow,发版时 push --atomic 同时推 +# master(触发 CI 的 pack-smoke)与 tag(触发 Release 的 build)→ 同一次发版打包两遍。 +# 合一后 tag 场景 release job 是唯一打包点,branch push run 只跑 check(~6s)。 name: CI on: pull_request: push: branches: [master] + tags: ['v*'] + workflow_dispatch: jobs: + # ---- 全场景共用:语法/结构冒烟(秒级) ---- check: runs-on: ubuntu-latest steps: @@ -36,16 +41,18 @@ jobs: node -e "const m=require('./manifest.json'); if(!m.id||!m.version) process.exit(1); console.log('manifest ok:', m.id, m.version)" test -f skills/gh/SKILL.md && echo "skills/gh ok" - pack-smoke: - runs-on: ubuntu-latest + # ---- PR / 手动:打包验证(产 artifact,3 天窗口) ---- + pack: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} needs: check + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 - # vendor 缓存:key 随 fetch-vendor.mjs(VERSIONS 表)变化,命中则免下载免解压 + # vendor 缓存:key 随 fetch-vendor.mjs(VERSIONS 表)变化,命中免下载免解压 - name: Cache vendor uses: actions/cache@v4 with: @@ -61,7 +68,7 @@ jobs: - name: Fetch vendor (git + gh + gnupg) run: node scripts/fetch-vendor.mjs - - name: Pack smoke + - name: Pack run: node scripts/pack.mjs - name: Verify package contents @@ -72,3 +79,130 @@ jobs: [ -f "$ZIP.sha256" ] || { echo "::error::no sha256 produced"; exit 1; } unzip -l "$ZIP" | grep -q "skills/gh/SKILL.md" && echo "skills/gh/SKILL.md in package" unzip -l "$ZIP" | grep -q "vendor/gnupg/bin/gpg.exe" && echo "vendor gnupg in package" + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: github-hanako-package + retention-days: 3 + path: releases/github-hanako-*.zip* + if-no-files-found: error + + # ---- tag:完整发版链(打包唯一发生点,不再与 CI 重复) ---- + release: + # 仅真实 tag push 触发(dispatch 可指 tag ref,会误覆盖 release 资产 → 排除) + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + needs: check + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: actions/setup-node@v5 + with: + node-version: 22 + + # tag 与 manifest version 一致性校验:pack 产物名来自 manifest,verify 按 tag 期待 + # 资产名——tag/manifest 不一致会在 verify 误删 release,这里 fail-fast + - name: Validate tag matches manifest version + env: + TAG: ${{ github.ref_name }} + run: | + set -e + VER=$(node -p "require('./manifest.json').version") + EXPECT="v$VER" + if [ "$TAG" != "$EXPECT" ]; then + echo "::error::tag $TAG 与 manifest version $VER 不一致(应为 $EXPECT)" + exit 1 + fi + echo "tag $TAG matches manifest version $VER" + + - name: Cache vendor + uses: actions/cache@v4 + with: + path: | + vendor + _tmp/vendor-dl + key: vendor-${{ hashFiles('scripts/fetch-vendor.mjs') }} + + - name: Install 7z + run: sudo apt-get update && sudo apt-get install -y p7zip-full + + - name: Fetch vendor (git + gh + gnupg) + run: node scripts/fetch-vendor.mjs + + - name: Package + run: node scripts/pack.mjs + + # 打包成功才建 release(避免打包失败留下无资产的空 release);已存在则跳过。 + # TAG 经 env 传入(github.ref_name 可含引号/分号等字符,直接拼 shell 有注入面) + - name: Create release + id: create + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists, skipping create" + else + gh release create "$TAG" \ + --prerelease \ + --title "$TAG" \ + --generate-notes + fi + + - name: Upload release assets + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -e + cd releases + ZIP=$(ls github-hanako-*.zip | head -1) + [ -n "$ZIP" ] || { echo "::error::no package zip found in releases/"; exit 1; } + gh release upload "$TAG" "$ZIP" "$ZIP.sha256" --clobber + + # 单点收尾:资产齐全才留 release,缺则删并 fail(公开 release 不留残缺)。 + # !cancelled() 兜底:upload 失败(set -e 中断)也会执行清理;release 未建 + # (package/fetch 失败发生在 create 前)→ 无残缺,正常退出不误报 + - name: Verify release assets complete + if: ${{ !cancelled() }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -e + # 仅 create-release 被跳过(fetch/package 失败在 create 前)→ 确定无 release + if [ "${{ steps.create.outcome }}" == "skipped" ]; then + echo "create-release 未执行(此前步骤失败),无残缺 release" + exit 0 + fi + # create success / failure 均查证:success 后必存在;failure 可能半建 + # (gh release create 是 draft→upload→publish 多步,中途失败留孤儿) + # 用 gh api 404 精确区分「不存在」与「API 错误」,错误 propagate + if ! gh api "repos/{owner}/{repo}/releases/tags/$TAG" >/dev/null 2>&1; then + MSG=$(gh api "repos/{owner}/{repo}/releases/tags/$TAG" 2>&1 >/dev/null || true) + if echo "$MSG" | grep -qi 'not found'; then + echo "release $TAG 不存在(create 未留下半成品),无需清理" + exit 0 + fi + echo "::error::release $TAG 查询失败(outcome=${{ steps.create.outcome }}):$MSG" + exit 1 + fi + # 资产名 = pkgId + "-v" + version(pack.mjs),version 段带 v 前缀与 tag 一致 + # 如 tag v0.1.1 → github-hanako-v0.1.1.zip(勿用 ${TAG#v} 去 v——曾致误判删 release) + ZIP="github-hanako-$TAG.zip" + if ! gh release view "$TAG" --json assets -q '.assets[].name' | grep -qx "$ZIP"; then + echo "::error::release $TAG missing asset $ZIP" + gh release delete "$TAG" --yes + exit 1 + fi + if ! gh release view "$TAG" --json assets -q '.assets[].name' | grep -qx "$ZIP.sha256"; then + echo "::error::release $TAG missing asset $ZIP.sha256" + gh release delete "$TAG" --yes + exit 1 + fi + echo "release $TAG assets complete: $ZIP + $ZIP.sha256" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1dd52b8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,130 +0,0 @@ -# Release — Linux 单平台打包 + 直传 release 资产 -# 骨架对齐 dsh-hanako release.yml(三段式:create-release → build → 资产直传), -# 按 github-hanako 交付物本质裁剪(纯 JS + 内嵌 win 运行时,零 npm 依赖)。 -# -# 触发: push tag v*(发版打 tag 自动触发)或手动 workflow_dispatch -# -# 三段式: -# - create-release(仅 tag 场景): 先建 pre-release(已存在则跳过,--generate-notes) -# - build(needs create-release): ubuntu 单平台 setup-node + fetch-vendor(git/gh/gnupg, -# vendor 走 cache)+ pack(产 zip + sha256);tag 场景直传 release 资产,dispatch 走 artifact -# - verify(仅 tag 场景): 查 release 资产齐全,缺则删 release 并 fail(不留残缺) -# -# 与 dsh-hanako 的差异: -# - 零 npm 依赖(package.json 无 dependencies)→ 无 pnpm install、无 lockfile -# - vendor/ 内嵌 win-x64 运行时(git/gh/gnupg,fetch-vendor.mjs 下载,sha256 校验), -# 交付物平台相关(win 资产)但构建在 ubuntu 单平台完成(纯文件打包,不执行 win 二进制) -# - gnupg 条目解压依赖 7z → build 前 apt 装 p7zip-full -# - 产物目录 releases/(.gitignore 忽略),资产名 github-hanako-v.zip 与 tag 对齐 -name: Release - -on: - push: - tags: ['v*'] - workflow_dispatch: - -jobs: - create-release: - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Create pre-release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if gh release view "${{ github.ref_name }}" >/dev/null 2>&1; then - echo "Release ${{ github.ref_name }} already exists, skipping create" - else - gh release create "${{ github.ref_name }}" \ - --prerelease \ - --title "${{ github.ref_name }}" \ - --generate-notes - fi - - build: - needs: create-release - # create-release 仅在 tag 场景运行,dispatch 场景会被 skip;被 needs 的 job skip 会 - # 让下游默认 if(success()) 一起 skip → 用 !cancelled() 显式放行 skipped 依赖链 - # (success(tag) / skipped(dispatch) 都继续构建),保留 failure/cancelled 门禁 - if: ${{ !cancelled() && needs.create-release.result != 'failure' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - with: - node-version: 22 - - # vendor 缓存:key 随 fetch-vendor.mjs(VERSIONS 表)变化,命中则免下载免解压 - - name: Cache vendor - uses: actions/cache@v4 - with: - path: | - vendor - _tmp/vendor-dl - key: vendor-${{ hashFiles('scripts/fetch-vendor.mjs') }} - - - name: Install 7z - run: sudo apt-get update && sudo apt-get install -y p7zip-full - - - name: Fetch vendor (git + gh + gnupg) - run: node scripts/fetch-vendor.mjs - - - name: Package - run: node scripts/pack.mjs - - - name: Upload release assets (tag) - if: startsWith(github.ref, 'refs/tags/v') - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - cd releases - ZIP=$(ls github-hanako-*.zip | head -1) - [ -n "$ZIP" ] || { echo "::error::no package zip found in releases/"; exit 1; } - gh release upload "${{ github.ref_name }}" "$ZIP" "$ZIP.sha256" --clobber - - - name: Upload artifacts - uses: actions/upload-artifact@v6 - with: - name: github-hanako-package - retention-days: 3 - path: releases/github-hanako-*.zip* - if-no-files-found: error - - verify: - if: startsWith(github.ref, 'refs/tags/v') - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - - name: Verify release assets complete - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - TAG="${{ github.ref_name }}" - # 资产名 = pkgId + "-v" + version(pack.mjs),version 段带 v 前缀与 tag 一致 - # 如 tag v0.1.1 → github-hanako-v0.1.1.zip(勿用 ${TAG#v} 去 v——曾致误判删 release) - ZIP="github-hanako-$TAG.zip" - if ! gh release view "$TAG" --json assets -q '.assets[].name' | grep -qx "$ZIP"; then - echo "::error::release $TAG missing asset $ZIP" - gh release delete "$TAG" --yes - exit 1 - fi - if ! gh release view "$TAG" --json assets -q '.assets[].name' | grep -qx "$ZIP.sha256"; then - echo "::error::release $TAG missing asset $ZIP.sha256" - gh release delete "$TAG" --yes - exit 1 - fi - echo "release $TAG assets complete: $ZIP + $ZIP.sha256"