From f2aaed5c5d0b973557d62a7a77c1c0825ff8dadc Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:59:59 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20implement=20issue=20#745=20?= =?UTF-8?q?=E2=80=94=20Canary=20tag-move=20403=20'Resource=20not=20accessi?= =?UTF-8?q?ble=20by=20integration'=20=E2=80=94=20release-manager=20App=20t?= =?UTF-8?q?oken=20can't=20PATCH=20protected=20channel=20tags=20(follow-up?= =?UTF-8?q?=20to=20#743/#744)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/canary-rollout.yml | 32 +++++++++++++++++++ scripts/canary-rollout.sh | 26 ++++++++++++--- tests/canary_rollout.bats | 48 ++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/.github/workflows/canary-rollout.yml b/.github/workflows/canary-rollout.yml index 8be095b0d..3b85b948f 100644 --- a/.github/workflows/canary-rollout.yml +++ b/.github/workflows/canary-rollout.yml @@ -122,6 +122,32 @@ jobs: exit 1 fi + # Mint a SECOND, repo-scoped installation token for the tag WRITES only (#745). The + # owner-wide token above is refused the release-channel-tags ruleset bypass on a + # protected channel-tag PATCH — 403 "Resource not accessible by integration": GitHub + # evaluates an App's bypass/effective-permissions differently for an owner-level token + # than for a repo-scoped installation token. This token is scoped to just the two + # tag-host repos (every agent's registry host is .github or .github-private) with an + # explicit Contents:write, and is passed to the engine as CANARY_WRITE_TOKEN so ONLY + # _gh_move_tag / _gh_create_annotated_tag use it — the fleet gate's cross-repo + # run-history reads keep the owner-wide token (don't regress the '*' stable-tier + # enumeration). Not a hard requirement to verify separately: if the write token is + # empty the engine falls back to the owner-wide token and the move re-fails loudly. + - name: Mint repo-scoped write token (tag moves) + id: write-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_MANAGER_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_MANAGER_APP_KEY }} + owner: ${{ github.repository_owner }} + # Scope to exactly the tag-host repos (NOT owner-wide) and request Contents:write + # explicitly — this is the combination that carries the ruleset bypass for a + # protected channel-tag UPDATE. + repositories: | + .github + .github-private + permission-contents: write + - name: Checkout (with tags) uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -142,6 +168,9 @@ jobs: if: github.event_name == 'schedule' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + # Repo-scoped write token for the inline cut's tag writes (_gh_create_annotated_tag + # + _gh_move_tag). The owner-wide GH_TOKEN above stays the credential for reads (#745). + CANARY_WRITE_TOKEN: ${{ steps.write-token.outputs.token }} CANARY_AUTO_CUT: ${{ vars.CANARY_AUTO_CUT }} run: | set -uo pipefail @@ -171,6 +200,9 @@ jobs: # The inline autocut cut (create+move tags on each agent's host) and the gate's # cross-repo run-history reads both authenticate with the App token. GH_TOKEN: ${{ steps.app-token.outputs.token }} + # Repo-scoped write token used ONLY for the channel-tag PATCH/POST in _gh_move_tag / + # _gh_create_annotated_tag (promote-all's real moves). Reads/gate keep GH_TOKEN (#745). + CANARY_WRITE_TOKEN: ${{ steps.write-token.outputs.token }} # One TSV line per real promotion (agent, ring, sha, owning-repo) so the deployment # step records a deployment for EVERY move in a promote-all run, on the right host. CANARY_PROMOTIONS_LOG: ${{ runner.temp }}/promotions.tsv diff --git a/scripts/canary-rollout.sh b/scripts/canary-rollout.sh index ac325e1f7..7b31427eb 100755 --- a/scripts/canary-rollout.sh +++ b/scripts/canary-rollout.sh @@ -107,6 +107,24 @@ _gh_tag_commit() { fi } +# _gh_write — run a `gh` mutation with the repo-scoped WRITE token when one is +# provided (CANARY_WRITE_TOKEN), else the ambient GH_TOKEN. The owner-wide token the workflow +# mints for the fleet gate's cross-repo run-history reads is refused a release-channel-tags +# ruleset bypass on a protected channel-tag PATCH — 403 "Resource not accessible by +# integration" (#745): GitHub evaluates an App's bypass/effective-permissions differently for +# an owner-level token than for a repo-scoped installation token. So tag writes are routed +# through a SECOND token minted repo-scoped (repositories: .github + .github-private) with an +# explicit Contents:write, while reads keep the owner-wide token (don't regress the '*' +# stable-tier enumeration). Absent CANARY_WRITE_TOKEN (e.g. unit tests / local runs) it is a +# transparent pass-through, so every existing GH_TOKEN-only path is unchanged. +_gh_write() { + if [ -n "${CANARY_WRITE_TOKEN:-}" ]; then + GH_TOKEN="$CANARY_WRITE_TOKEN" gh "$@" + else + gh "$@" + fi +} + # _gh_move_tag — force-move (or create) the lightweight ref # refs/tags/ on to via the GitHub API. THE channel-tag mover for every # agent (promote + rollback), this-repo and cross-repo alike (#1076): the API path is @@ -123,7 +141,7 @@ _gh_tag_commit() { _gh_move_tag() { [ $# -lt 3 ] && return 1 local repo="$1" tag="$2" sha="$3" out rc - out="$(gh api -X PATCH "repos/$repo/git/refs/tags/$tag" \ + out="$(_gh_write api -X PATCH "repos/$repo/git/refs/tags/$tag" \ -f sha="$sha" -F force=true 2>&1)" && return 0 rc=$? # Only a genuinely-absent ref (404) warrants creating it; every other PATCH failure is a @@ -133,7 +151,7 @@ _gh_move_tag() { echo "::error::_gh_move_tag: could not move refs/tags/$tag -> ${sha:0:12} on $repo (HTTP): ${out//$'\n'/ }" >&2 return "$rc" fi - out="$(gh api -X POST "repos/$repo/git/refs" \ + out="$(_gh_write api -X POST "repos/$repo/git/refs" \ -f ref="refs/tags/$tag" -f sha="$sha" 2>&1)" && return 0 rc=$? echo "::error::_gh_move_tag: could not create refs/tags/$tag -> ${sha:0:12} on $repo (HTTP): ${out//$'\n'/ }" >&2 @@ -152,14 +170,14 @@ _gh_move_tag() { _gh_create_annotated_tag() { [ $# -lt 4 ] && return 1 local repo="$1" tag="$2" sha="$3" message="$4" obj - obj="$(gh api -X POST "repos/$repo/git/tags" \ + obj="$(_gh_write api -X POST "repos/$repo/git/tags" \ -f tag="$tag" -f message="$message" -f object="$sha" -f type=commit \ --jq '.sha // empty')" if [ $? -ne 0 ] || [ -z "$obj" ]; then echo "::error::_gh_create_annotated_tag: could not create the annotated release tag on $repo or read back its object SHA" >&2 return 1 fi - gh api -X POST "repos/$repo/git/refs" \ + _gh_write api -X POST "repos/$repo/git/refs" \ -f ref="refs/tags/$tag" -f sha="$obj" >/dev/null 2>&1 || { echo "::error::_gh_create_annotated_tag: created tag object $obj on $repo but could not publish ref refs/tags/$tag" >&2 return 1 diff --git a/tests/canary_rollout.bats b/tests/canary_rollout.bats index da2b2600a..70395c50a 100644 --- a/tests/canary_rollout.bats +++ b/tests/canary_rollout.bats @@ -1136,6 +1136,54 @@ GHEOF [[ "$output" == *"Validation Failed"* ]] } +# ── #745: protected channel-tag WRITES go through a repo-scoped write token ────── +# The owner-wide App token is refused a release-channel-tags ruleset bypass on a PATCH +# of a protected channel tag (403 "Resource not accessible by integration") — GitHub +# evaluates an App's bypass/effective-perms differently for an owner-level token than a +# repo-scoped installation token. The fix mints a SECOND, repo-scoped token +# (Contents:write on the tag hosts) and routes ONLY the tag writes through it via +# CANARY_WRITE_TOKEN — the read-only fleet gate keeps the owner-wide GH_TOKEN so the +# '*' stable-tier enumeration is not regressed. Each stub line records which token was +# in effect (the GH_TOKEN visible to the `gh` child) for the mutation. +_token_capture_stub() { + STUB_BIN="$(mktemp -d "$BATS_TEST_TMPDIR/stub.XXXXXX")"; export PATH="$STUB_BIN:$PATH" + export TOKEN_LOG="$STUB_BIN/tokens.log"; : > "$TOKEN_LOG" + cat > "$STUB_BIN/gh" <<'GHEOF' +#!/usr/bin/env bash +printf '%s\t%s\n' "$*" "${GH_TOKEN:-}" >> "$TOKEN_LOG" +echo "1111111111111111111111111111111111111111" +exit 0 +GHEOF + chmod +x "$STUB_BIN/gh" +} + +@test "_gh_move_tag: routes the PATCH through CANARY_WRITE_TOKEN when set (#745)" { + _token_capture_stub + run env GH_TOKEN=owner-wide CANARY_WRITE_TOKEN=repo-scoped bash -c \ + "source '$ORCH' && _gh_move_tag petry-projects/.github agent-shield/v2-ring0 12b0075a9c48000000000000000000000000000" + [ "$status" -eq 0 ] + grep -q 'PATCH.*repo-scoped' "$TOKEN_LOG" + ! grep -q 'PATCH.*owner-wide' "$TOKEN_LOG" +} + +@test "_gh_move_tag: falls back to the ambient GH_TOKEN when CANARY_WRITE_TOKEN is unset (#745)" { + _token_capture_stub + run env -u CANARY_WRITE_TOKEN GH_TOKEN=owner-wide bash -c \ + "source '$ORCH' && _gh_move_tag petry-projects/.github agent-shield/v2-ring0 12b0075a9c48000000000000000000000000000" + [ "$status" -eq 0 ] + grep -q 'PATCH.*owner-wide' "$TOKEN_LOG" +} + +@test "_gh_create_annotated_tag: routes the object create + ref publish through CANARY_WRITE_TOKEN when set (#745)" { + _token_capture_stub + run env GH_TOKEN=owner-wide CANARY_WRITE_TOKEN=repo-scoped bash -c \ + "source '$ORCH' && _gh_create_annotated_tag petry-projects/.github agent-shield/v2.0.0 12b0075a9c48000000000000000000000000000 'agent-shield release v2.0.0'" + [ "$status" -eq 0 ] + # both the git/tags object create and the git/refs publish must carry the write token + [ "$(grep -c 'repo-scoped' "$TOKEN_LOG")" -ge 2 ] + ! grep -q 'owner-wide' "$TOKEN_LOG" +} + # ── orchestrator: sync-issues — auto-triage held promotions into tracked issues ── # A held (BLOCKED) promotion files/updates ONE idempotent issue per agent with the failing-run # evidence; a cleared agent's issue auto-closes; the fleet-status table is rendered to the job From 15219dadb565650f3a6e0eeff223e81442c568fc Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Thu, 16 Jul 2026 01:48:37 -0500 Subject: [PATCH 2/2] docs(canary): breadcrumb the confirmed 403 (run 29435711407) on the write-token step [#745] Ties the repo-scoped write-token fix to the diagnostic evidence from #744's un-suppressed error, for future debuggers. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01J4z5uYVjwdyQjh2wFZxkut --- .github/workflows/canary-rollout.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/canary-rollout.yml b/.github/workflows/canary-rollout.yml index 3b85b948f..97d7c8793 100644 --- a/.github/workflows/canary-rollout.yml +++ b/.github/workflows/canary-rollout.yml @@ -133,6 +133,9 @@ jobs: # run-history reads keep the owner-wide token (don't regress the '*' stable-tier # enumeration). Not a hard requirement to verify separately: if the write token is # empty the engine falls back to the owner-wide token and the move re-fails loudly. + # Confirmed by #744's un-suppressed error (run 29435711407): the owner-wide-token + # PATCH .../git/refs/tags/ returned 403 "Resource not accessible by + # integration" while an admin-token PATCH of the same ref succeeded. - name: Mint repo-scoped write token (tag moves) id: write-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0