Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions scripts/canary-rollout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ _gh_move_tag() {
# real rejection to surface, not to paper over with a create.
local low="${out,,}"
if [[ "$low" != *"not found"* && "$low" != *"http 404"* ]]; then
# #749: on a 403 "Resource not accessible by integration" — the failure #745/#746 did NOT
# fix — dump the write token's EFFECTIVE permissions + scope so the next step is decided
# from data, not another guess. Only runs on the 403 branch (a non-404 that is also a
# permission 403), before the ::error:: so the diagnostic and the surfaced rejection are
# co-located in the run log. Best-effort: never alters rc.
if [[ "$low" == *"403"* || "$low" == *"not accessible by integration"* ]]; then
_gh_403_diag "$repo" >&2 || true
fi
echo "::error::_gh_move_tag: could not move refs/tags/$tag -> ${sha:0:12} on $repo (HTTP): ${out//$'\n'/ }" >&2
return "$rc"
fi
Expand All @@ -158,6 +166,48 @@ _gh_move_tag() {
return "$rc"
}

# _gh_403_diag <repo> — one-shot effective-permission diagnostic (#749), called only from the
# _gh_move_tag 403 failure path. #745/#746 (repo-scoped write token) did NOT resolve the 403
# "Resource not accessible by integration" on a protected channel-tag PATCH, so before guessing
# again we DUMP DATA, using the SAME token the PATCH used (_gh_write → CANARY_WRITE_TOKEN when
# set). This distinguishes the two possible root causes:
# (a) the token LACKS effective contents:write → a minting bug (code-fixable), or
# (b) the token HAS contents:write but the release-channel-tags ruleset bypass lapsed → NOT a
# code bug (a maintainer must re-save bypass_actors / open GitHub support).
# Emits, to stderr, inside a foldable ::group:::
# - X-Accepted-GitHub-Permissions from `gh api -i repos/<repo>` (the perms the API advertises),
# - total_count from /installation/repositories (how many repos the installation token covers).
# Best-effort: every probe is guarded, and the function always returns 0 — a diagnostic must
# never mask the 403 or change the mover's exit status.
_gh_403_diag() {
local repo="$1" hdr inst count=""
echo "::group::_gh_move_tag 403 effective-permission diagnostic (#749)"
if [ -n "${CANARY_WRITE_TOKEN:-}" ]; then
echo "diag: introspecting with CANARY_WRITE_TOKEN (repo-scoped write token)"
else
echo "diag: introspecting with ambient GH_TOKEN (no CANARY_WRITE_TOKEN set)"
fi
# (1) What permission does the API advertise for this repo endpoint?
hdr="$(_gh_write api -i "repos/$repo" 2>/dev/null | grep -i '^x-accepted-github-permissions:' || true)"
if [ -n "$hdr" ]; then
printf 'diag: %s\n' "${hdr%$'\r'}"
else
echo "diag: X-Accepted-GitHub-Permissions header not present on repos/$repo response"
fi
# (2) How many repositories does the installation token cover?
inst="$(_gh_write api /installation/repositories 2>/dev/null || true)"
if [ -n "$inst" ]; then
count="$(jq -r '.total_count? // empty' <<<"$inst" 2>/dev/null || true)"
fi
if [ -n "$count" ]; then
echo "diag: installation/repositories total_count=${count}"
else
echo "diag: installation/repositories total_count=<unreadable>"
fi
echo "::endgroup::"
return 0
}

# _gh_create_annotated_tag <repo> <tag> <sha> <message> — create the immutable annotated
# tag object <tag> pointing at commit <sha> on <repo> and publish its ref, via the GitHub
# API with the release-manager App token (mirrors cut-release.sh's gh_create_annotated_tag).
Expand Down
131 changes: 131 additions & 0 deletions tests/canary_rollout.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,137 @@ GHEOF
! grep -q 'owner-wide' "$TOKEN_LOG"
}

# ── #749: one-shot effective-permission diagnostic on a 403 tag-move ────────────
# #745/#746 (repo-scoped write token) did NOT resolve the 403 "Resource not accessible by
# integration" on a protected channel-tag PATCH. To decide the next step from DATA rather than
# guessing again, the _gh_move_tag 403 failure path (behind #744's un-suppressed error) dumps
# the write token's EFFECTIVE permissions + scope — using the SAME write token the PATCH used —
# so we can tell (a) a token that LACKS effective contents:write (a minting bug, code-fixable)
# from (b) a token that HAS it but is still blocked because the ruleset bypass lapsed (NOT a
# code bug). The diagnostic must run ONLY on a 403 (not on the #743 non-404 422 rejection),
# must still surface the ::error:: + return non-zero, and must not fall back to POST.
_diag_403_stub() {
local scope="${1:-selected}" # selected (repo-scoped, low count) | all (owner-wide, high count)
local install_count
if [ "$scope" = all ]; then install_count=50; else install_count=2; fi
STUB_BIN="$(mktemp -d "$BATS_TEST_TMPDIR/stub.XXXXXX")"; export PATH="$STUB_BIN:$PATH"
export CALL_LOG="$STUB_BIN/calls.log"; : > "$CALL_LOG"
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:-<unset>}" >> "$TOKEN_LOG"
case "\$*" in
*"-X PATCH"*"git/refs/tags/"*)
echo "PATCH" >> "$CALL_LOG"
echo '{"message":"Resource not accessible by integration","status":"403"}' >&2
exit 1 ;;
*"-X POST"*"git/refs"*)
echo "POST" >> "$CALL_LOG"; echo '{"ref":"refs/tags/x"}'; exit 0 ;;
*"-i "*"repos/"*)
echo "DIAG_HEADERS" >> "$CALL_LOG"
printf 'HTTP/2.0 403 Forbidden\r\n'
printf 'X-Accepted-GitHub-Permissions: contents=write; contents=read\r\n'
printf '\r\n'
echo '{"full_name":"petry-projects/.github"}'
exit 0 ;;
*"installation/repositories"*)
echo "DIAG_INSTALL" >> "$CALL_LOG"
echo '{"total_count":$install_count}'
exit 0 ;;
*) echo "{}" ;;
esac
GHEOF
chmod +x "$STUB_BIN/gh"
}

@test "_gh_move_tag: on a 403 dumps the effective-permission diagnostic then surfaces the error (#749)" {
_diag_403_stub selected
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" -ne 0 ]
[[ "$output" == *"effective-permission diagnostic"* ]]
[[ "$output" == *"X-Accepted-GitHub-Permissions: contents=write"* ]]
[[ "$output" == *"total_count=2"* ]]
[[ "$output" == *"::error::"* ]]
grep -q '^PATCH$' "$CALL_LOG"
grep -q '^DIAG_HEADERS$' "$CALL_LOG"
grep -q '^DIAG_INSTALL$' "$CALL_LOG"
! grep -q '^POST$' "$CALL_LOG"
}

@test "_gh_move_tag: the 403 diagnostic introspects through CANARY_WRITE_TOKEN (#749)" {
_diag_403_stub selected
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"
grep -q 'installation/repositories.*repo-scoped' "$TOKEN_LOG"
! grep -q 'installation/repositories.*owner-wide' "$TOKEN_LOG"
}

@test "_gh_move_tag: the 403 diagnostic reports the higher total_count for an owner-wide installation token (#749)" {
_diag_403_stub all
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"
[[ "$output" == *"total_count=50"* ]]
[[ "$output" != *"total_count=2"* ]]
}

@test "_gh_move_tag: a non-403 (422 ruleset) failure does NOT trigger the 403 diagnostic (#749)" {
_move_tag_stub reject
run bash -c "source '$ORCH' && _gh_move_tag petry-projects/.github agent-shield/v2-ring0 12b0075a9c48000000000000000000000000000"
[ "$status" -ne 0 ]
[[ "$output" != *"effective-permission diagnostic"* ]]
[[ "$output" == *"::error::"* ]]
}

@test "_gh_403_diag: empty installation/repositories response does not cause a jq parse error (#749)" {
# Stub: PATCH → 403; installation/repositories → empty (API failure)
STUB_BIN="$(mktemp -d "$BATS_TEST_TMPDIR/stub.XXXXXX")"; export PATH="$STUB_BIN:$PATH"
cat > "$STUB_BIN/gh" <<'GHEOF'
#!/usr/bin/env bash
case "$*" in
*"-X PATCH"*"git/refs/tags/"*)
echo '{"message":"Resource not accessible by integration","status":"403"}' >&2; exit 1 ;;
*"-i "*"repos/"*)
printf 'HTTP/2.0 403 Forbidden\r\nX-Accepted-GitHub-Permissions: contents=write\r\n\r\n{}'
exit 0 ;;
*"installation/repositories"*) exit 1 ;; # API failure → empty $inst
*) echo "{}" ;;
esac
GHEOF
chmod +x "$STUB_BIN/gh"
run env GH_TOKEN=tok bash -c \
"source '$ORCH' && _gh_move_tag petry-projects/.github agent-shield/v2-ring0 12b0075a9c48000000000000000000000000000"
[ "$status" -ne 0 ]
[[ "$output" == *"effective-permission diagnostic"* ]]
# Must not print a jq error about parse failure
[[ "$output" != *"parse error"* ]]
[[ "$output" == *"total_count=<unreadable>"* ]]
}

@test "_gh_403_diag: installation/repositories response missing keys does not error (#749)" {
# Stub: installation/repositories returns valid JSON but without the expected keys
STUB_BIN="$(mktemp -d "$BATS_TEST_TMPDIR/stub.XXXXXX")"; export PATH="$STUB_BIN:$PATH"
cat > "$STUB_BIN/gh" <<'GHEOF'
#!/usr/bin/env bash
case "$*" in
*"-X PATCH"*"git/refs/tags/"*)
echo '{"message":"Resource not accessible by integration","status":"403"}' >&2; exit 1 ;;
*"-i "*"repos/"*)
printf 'HTTP/2.0 403 Forbidden\r\nX-Accepted-GitHub-Permissions: contents=write\r\n\r\n{}'
exit 0 ;;
*"installation/repositories"*) echo '{}' ;; # valid JSON but no total_count
*) echo "{}" ;;
esac
GHEOF
chmod +x "$STUB_BIN/gh"
run env GH_TOKEN=tok bash -c \
"source '$ORCH' && _gh_move_tag petry-projects/.github agent-shield/v2-ring0 12b0075a9c48000000000000000000000000000"
[ "$status" -ne 0 ]
[[ "$output" == *"effective-permission diagnostic"* ]]
[[ "$output" != *"parse error"* ]]
[[ "$output" == *"total_count=<unreadable>"* ]]
}

# ── 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
Expand Down
Loading