Skip to content

Commit d5aeab7

Browse files
authored
fix(ci): push the release-cut baked manifest as a ruleset bypass bot (#5358)
* fix(ci): push the release-cut baked manifest as a ruleset bypass bot bake-plugin-previews-release.yml renders every preview on a release/** cut and pushes the authoritative manifest straight onto the release branch. But release/** is guarded by the 'Protected branches (preview/*, release/v*)' ruleset whose pull_request rule rejects direct pushes — and the workflow pushed with PREVIEW_BAKE_TOKEN||github.token, i.e. github-actions[bot], which is NOT a bypass actor. So the writeback failed with 'GH013: Repository rule violations found for refs/heads/release/vX.Y.Z' (observed on release/v0.14.2), and the release branch's bundled data/plugin-previews/manifest.json was never refreshed — the packaged app then falls back to rendering each preview as a live iframe (pathological for the WebGL example templates). Mint an open-design-bot token (app id 3640364 — already a bypass actor on that ruleset, same App the metrics/blog/contributors workflows use) after the render and push with it via an explicit token URL. persist-credentials: false on checkout so the leftover github.token extraheader can't override the inline bot token on push (same failure mode fixed in #5357). No ruleset change required — the bypass actor already exists; the workflow just wasn't using it. * test(ci): lock the release-cut bake's ruleset-bypass push contract Per .github/AGENTS.md, cross-workflow auth changes must update topology coverage in e2e/tests/packaged-smoke-workflow.test.ts rather than rely on YAML review. Add a companion assertion (alongside the existing bake-plugin-previews.yml one) that locks bake-plugin-previews-release.yml's three co-dependent auth invariants: persist-credentials: false on checkout, the open-design-bot token mint (BOT_APP_CLIENT_ID/BOT_APP_PRIVATE_KEY — the App that is a ruleset bypass actor, not RELEASE_BOT_APP_ID), and the explicit x-access-token:${BOT_TOKEN} push. A refactor dropping any one silently reintroduces the GH013 rejection that stranded release/v0.14.2's manifest.
1 parent b335680 commit d5aeab7

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

.github/workflows/bake-plugin-previews-release.yml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ jobs:
4040
with:
4141
ref: ${{ github.ref }}
4242
fetch-depth: 0
43-
token: ${{ secrets.PREVIEW_BAKE_TOKEN || github.token }}
43+
# Read-only clone with the default token, but do NOT persist it as a
44+
# github.com credential. release/** is guarded by the "Protected
45+
# branches (preview/*, release/v*)" ruleset (deletion / non_fast_forward
46+
# / pull_request), so a direct push is rejected (GH013) unless the pusher
47+
# is a bypass actor. github-actions[bot] is NOT one — so the manifest
48+
# writeback below force-authenticates as open-design-bot, which IS a
49+
# bypass actor, via an explicit token URL. A persisted github.token
50+
# extraheader would override that inline token on push (see #5357), so
51+
# drop it; nothing between here and the push needs the persisted cred.
52+
persist-credentials: false
4453

4554
- name: Loop guard — skip the bake bot's own manifest commit
4655
id: guard
@@ -71,8 +80,26 @@ jobs:
7180
r2-bucket: ${{ secrets.CLOUDFLARE_R2_REPOSITORY_ASSETS_BUCKET }}
7281
r2-endpoint: ${{ secrets.CLOUDFLARE_R2_REPOSITORY_ASSETS_URL }}
7382

83+
- name: Mint the open-design-bot token for the release-branch push
84+
if: ${{ steps.guard.outputs.skip != 'true' }}
85+
id: bot
86+
# open-design-bot (app id 3640364) is a bypass actor on the
87+
# "Protected branches (preview/*, release/v*)" ruleset, so its push clears
88+
# the pull_request rule that rejects github-actions[bot] (GH013). The
89+
# release bake pushes the manifest directly (it then rides the non-squash
90+
# release back-merge to main). Minted here — after the long render, not at
91+
# job start — because an App token lives ~1h and the render can run long.
92+
uses: actions/create-github-app-token@v3.2.0
93+
with:
94+
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
95+
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
96+
owner: nexu-io
97+
repositories: open-design
98+
7499
- name: Commit the authoritative manifest onto the release branch
75100
if: ${{ steps.guard.outputs.skip != 'true' }}
101+
env:
102+
BOT_TOKEN: ${{ steps.bot.outputs.token }}
76103
run: |
77104
OLD=data/plugin-previews/manifest.json
78105
NEW=.tmp/plugin-previews/manifest.json
@@ -89,4 +116,7 @@ jobs:
89116
git config user.email "bot@open-design.ai"
90117
git add "$OLD"
91118
git commit -m "chore(plugin-previews): refresh baked preview manifest (release cut)"
92-
git push origin "HEAD:${GITHUB_REF#refs/heads/}"
119+
# Push as open-design-bot (a ruleset bypass actor) via an explicit token
120+
# URL. A plain `git push origin` would authenticate as github-actions[bot]
121+
# and be rejected by the release/** pull_request rule (GH013).
122+
git push "https://x-access-token:${BOT_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF#refs/heads/}"

e2e/tests/packaged-smoke-workflow.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ const bakePreviewsAutomergeWorkflowPath = join(
2929
"bake-plugin-previews-automerge.yml",
3030
);
3131
const bakePreviewsWorkflowPath = join(workspaceRoot, ".github", "workflows", "bake-plugin-previews.yml");
32+
const bakePreviewsReleaseWorkflowPath = join(
33+
workspaceRoot,
34+
".github",
35+
"workflows",
36+
"bake-plugin-previews-release.yml",
37+
);
3238
const finalizeReleaseWorkflowPath = join(workspaceRoot, ".github", "workflows", "finalize-release.yml");
3339
const handoffScriptPath = join(workspaceRoot, ".github", "scripts", "handoff.py");
3440
const releaseBetaWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-beta.yml");
@@ -814,6 +820,32 @@ process.stdin.on("end", () => {
814820
expect(workflow).not.toContain("git/ref/heads/main");
815821
});
816822

823+
it("[P2] keeps the release-cut bake pushing its manifest as a ruleset bypass bot", async () => {
824+
// release/** is guarded by the "Protected branches (preview/*, release/v*)" ruleset whose
825+
// pull_request rule rejects a direct push (GH013) unless the pusher is a bypass actor. The
826+
// release-cut bake writes the authoritative manifest straight onto the release branch, so it
827+
// must push as open-design-bot — a bypass actor on that ruleset — not github-actions[bot],
828+
// which is NOT and gets rejected (this stranded release/v0.14.2's manifest). These three auth
829+
// invariants only work together; a refactor that drops any one silently reintroduces the
830+
// GH013 regression, so lock them here rather than rely on YAML review.
831+
const workflow = await readFile(bakePreviewsReleaseWorkflowPath, "utf8");
832+
833+
// 1. Checkout must NOT persist GITHUB_TOKEN: its http.extraheader would override the inline bot
834+
// token on push and re-authenticate as github-actions[bot] (the same override fixed in the
835+
// post-merge bake — #5357).
836+
expect(workflow).toContain("persist-credentials: false");
837+
// 2. The run mints an open-design-bot token via the BOT_APP_* creds — the App that IS a bypass
838+
// actor on the release ruleset. RELEASE_BOT_APP_ID (used by the post-merge bake) is a
839+
// different App and is NOT a bypass actor, so pin the correct credentials, not just the
840+
// generic token action.
841+
expect(workflow).toContain("actions/create-github-app-token");
842+
expect(workflow).toContain("secrets.BOT_APP_CLIENT_ID");
843+
expect(workflow).toContain("secrets.BOT_APP_PRIVATE_KEY");
844+
// 3. The manifest push goes through the explicit tokenized URL with that bot token, so it
845+
// authenticates as the bypass actor rather than the checkout's default credential.
846+
expect(workflow).toContain("x-access-token:${BOT_TOKEN}");
847+
});
848+
817849
it("[P2] keeps PR and merge queue CI separated by hot/full validation mode", async () => {
818850
const workflow = await readFile(ciWorkflowPath, "utf8");
819851
const scopes = sectionBetween(workflow, " scopes:", " static_gate:");

0 commit comments

Comments
 (0)