From e590103d703b758e6795d46c422c2281cfb94435 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 5 Aug 2026 10:24:35 +0800 Subject: [PATCH] fix(ci): authenticate skill updater pull requests (#3404) Punchcard-Session: amber-orchard-valley-s4 --- .github/workflows/update-skills.yml | 40 +++++++++++- CHANGELOG.md | 1 + scripts/update-skills-workflow.test.ts | 85 ++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 scripts/update-skills-workflow.test.ts diff --git a/.github/workflows/update-skills.yml b/.github/workflows/update-skills.yml index 7b0669a3..78a65f4c 100644 --- a/.github/workflows/update-skills.yml +++ b/.github/workflows/update-skills.yml @@ -7,7 +7,6 @@ on: permissions: contents: write - pull-requests: write concurrency: group: update-skills @@ -99,11 +98,13 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Open or update pull request + - name: Commit and push update branch if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ github.token }} run: | + set -euo pipefail + branch="automation/update-skills" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" @@ -112,6 +113,41 @@ jobs: git commit -m "chore: update skills" git push --force-with-lease origin "$branch" + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + continue-on-error: true + if: steps.changes.outputs.changed == 'true' + with: + app-id: "2729701" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-pull-requests: write + + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token-fallback + continue-on-error: true + if: steps.changes.outputs.changed == 'true' && steps.app-token.outcome == 'failure' + with: + app-id: "2971289" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-pull-requests: write + + - name: Open or update pull request + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} + run: | + set -euo pipefail + + if [[ -z "${GH_TOKEN:-}" ]]; then + echo "::error::Unable to create a Barnacle GitHub App token. Check the primary GH_APP_PRIVATE_KEY and fallback GH_APP_PRIVATE_KEY_FALLBACK credentials and their pull-request permissions." >&2 + exit 1 + fi + + branch="automation/update-skills" body="$RUNNER_TEMP/update-skills-pr.md" { echo "## Summary" diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a3832d..b9f3955d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- CI: authenticate the scheduled project-skill updater's pull request mutations with the Barnacle GitHub App while retaining the workflow token for provenance lookup and branch publication. - Workers: materialize zero-byte directory markers without colliding with descendant files, while retaining real empty files and verifying every downloaded artifact digest. - Deploy: allow an explicitly confirmed backend-only deploy to pause and reliably restore active external-skill rollouts instead of requiring a manual dashboard toggle. - GitHub Actions/CLI: trigger exact pre-publication checks immediately and wait for package publication to finish, so a pending staged upload no longer reports a successful release. diff --git a/scripts/update-skills-workflow.test.ts b/scripts/update-skills-workflow.test.ts new file mode 100644 index 00000000..aaf12150 --- /dev/null +++ b/scripts/update-skills-workflow.test.ts @@ -0,0 +1,85 @@ +/* @vitest-environment node */ + +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; +import { parse as parseYaml } from "yaml"; + +type WorkflowStep = { + "continue-on-error"?: boolean; + env?: Record; + id?: string; + if?: string; + name?: string; + run?: string; + uses?: string; + with?: Record; +}; + +describe("update skills workflow", () => { + it("uses GitHub App auth only for pull request mutations", async () => { + const workflow = parseYaml(await readFile(".github/workflows/update-skills.yml", "utf8")) as { + jobs: { + update: { + steps: WorkflowStep[]; + }; + }; + permissions?: Record; + }; + + expect(workflow.permissions).toEqual({ contents: "write" }); + + const steps = workflow.jobs.update.steps; + const provenanceStep = steps.find((step) => step.name === "Refresh changed skill provenance"); + expect(provenanceStep?.env).toEqual({ GH_TOKEN: "${{ github.token }}" }); + + const pushStep = steps.find((step) => step.name === "Commit and push update branch"); + expect(pushStep?.env).toEqual({ GH_TOKEN: "${{ github.token }}" }); + expect(pushStep?.run).toContain('git push --force-with-lease origin "$branch"'); + expect(pushStep?.run).not.toContain("gh pr "); + + const primaryTokenStep = steps.find((step) => step.id === "app-token"); + expect(primaryTokenStep).toMatchObject({ + uses: "actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3", + "continue-on-error": true, + if: "steps.changes.outputs.changed == 'true'", + with: { + "app-id": "2729701", + "private-key": "${{ secrets.GH_APP_PRIVATE_KEY }}", + owner: "${{ github.repository_owner }}", + repositories: "${{ github.event.repository.name }}", + "permission-pull-requests": "write", + }, + }); + + const fallbackTokenStep = steps.find((step) => step.id === "app-token-fallback"); + expect(fallbackTokenStep).toMatchObject({ + uses: "actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3", + "continue-on-error": true, + if: "steps.changes.outputs.changed == 'true' && steps.app-token.outcome == 'failure'", + with: { + "app-id": "2971289", + "private-key": "${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}", + owner: "${{ github.repository_owner }}", + repositories: "${{ github.event.repository.name }}", + "permission-pull-requests": "write", + }, + }); + + const pullRequestStep = steps.find((step) => step.name === "Open or update pull request"); + expect(pullRequestStep?.env).toEqual({ + GH_TOKEN: "${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}", + }); + const pullRequestRun = pullRequestStep?.run ?? ""; + expect(pullRequestRun).toContain("gh pr list"); + expect(pullRequestRun).toContain("gh pr edit"); + expect(pullRequestRun).toContain("gh pr create"); + expect(pullRequestRun).not.toContain("git push"); + expect(JSON.stringify(pullRequestStep)).not.toContain("github.token"); + + const tokenGuard = 'if [[ -z "${GH_TOKEN:-}" ]]'; + expect(pullRequestRun).toContain(tokenGuard); + expect(pullRequestRun).toContain("primary GH_APP_PRIVATE_KEY"); + expect(pullRequestRun).toContain("fallback GH_APP_PRIVATE_KEY_FALLBACK"); + expect(pullRequestRun.indexOf(tokenGuard)).toBeLessThan(pullRequestRun.indexOf("gh pr list")); + }); +});