From 16cd094f3ac2a23ff520c25c1ba443267220030e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 09:48:45 +0000 Subject: [PATCH 1/2] Initial plan From b16aa5cd8f7c8c2048e749672f75018a325f312f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 09:54:55 +0000 Subject: [PATCH 2/2] Fix bootstrap pre-flight check and cleanup null-SHA / 403-PR / 422-DELETE bugs Co-authored-by: einari <134365+einari@users.noreply.github.com> --- .github/workflows/bootstrap-copilot-sync.yml | 28 +++--------------- .../cleanup-copilot-sync-branches.yml | 29 ++++++++++++++----- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.github/workflows/bootstrap-copilot-sync.yml b/.github/workflows/bootstrap-copilot-sync.yml index 2250dfb..6cbcc7b 100644 --- a/.github/workflows/bootstrap-copilot-sync.yml +++ b/.github/workflows/bootstrap-copilot-sync.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Verify PAT can authenticate and has required permissions + - name: Verify PAT can authenticate env: GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }} run: | @@ -38,29 +38,9 @@ jobs: fi login=$(gh api /user --jq '.login') echo "✓ Authenticated as $login" - - # Pre-flight check: verify that the PAT can create pull requests. - # We probe the PR creation endpoint with deliberately invalid data so the - # API returns 422 (Unprocessable Entity) when authorized, vs 403 (Forbidden) - # when the token lacks pull-request write permission. - # curl -w "%{http_code}" is used for reliable numeric status capture. - http_code=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/Cratis/Workflows/pulls" \ - -d '{"title":"__preflight__","head":"__preflight__","base":"__preflight__"}') - - if [ "$http_code" = "403" ] || [ "$http_code" = "401" ]; then - echo "::error::PAT_DOCUMENTATION cannot create pull requests (HTTP $http_code)." - echo "::error::For a fine-grained PAT add 'Pull requests: Read and write' permission." - echo "::error::For a classic PAT ensure the 'repo' scope is selected." - echo "::error::Update the secret and re-run this workflow." - exit 1 - fi - # Any other code (404, 422, etc.) means the token IS authorized for PR creation. - echo "✓ PAT has pull-request creation permission (probe returned HTTP $http_code)" + # NOTE: PR-creation permission is validated implicitly per-repository during + # the main loop below. A pre-flight probe against Cratis/Workflows would + # always fail for fine-grained PATs that (correctly) exclude this repo. - name: Get all Cratis repositories id: get-repos diff --git a/.github/workflows/cleanup-copilot-sync-branches.yml b/.github/workflows/cleanup-copilot-sync-branches.yml index 9f6b8de..9b1f51a 100644 --- a/.github/workflows/cleanup-copilot-sync-branches.yml +++ b/.github/workflows/cleanup-copilot-sync-branches.yml @@ -59,26 +59,41 @@ jobs: echo "$repos" | jq -r '.[]' | while read -r repo; do echo "Checking Cratis/$repo..." - # Check whether the branch exists + # Check whether the branch exists. + # gh api writes the error response body to stdout on 4xx errors; the + # --jq filter '.object.sha' applied to a non-branch response (e.g. + # {"message":"Not Found",...}) yields the literal string "null", so we + # must treat both an empty value and "null" as "branch not found". branch_sha=$(gh api "repos/Cratis/$repo/git/ref/heads/$branch" \ --jq '.object.sha' 2>/dev/null || true) - if [ -z "$branch_sha" ]; then + if [ -z "$branch_sha" ] || [ "$branch_sha" = "null" ]; then echo " ℹ Branch not found in $repo, skipping" continue fi - # Check whether an open PR references this branch (skip if so) - open_pr=$(gh api "repos/Cratis/$repo/pulls?state=open&head=Cratis:$branch" \ - --jq '.[0].number // empty' 2>/dev/null || true) + # Check whether an open PR references this branch (skip if so). + # Pipe through a separate jq invocation so that error responses (e.g. + # 403 "Resource not accessible by personal access token") are always + # parsed as JSON and never leaked as a raw string into $open_pr. + pr_response=$(gh api "repos/Cratis/$repo/pulls?state=open&head=Cratis:$branch" \ + 2>/dev/null || true) + open_pr=$(echo "$pr_response" | jq -r '.[0].number // empty' 2>/dev/null || true) if [ -n "$open_pr" ]; then echo " ⚠ Open PR #$open_pr references $branch in $repo — skipping (close or merge the PR first)" echo "$repo" >> "$skipped_file" continue fi - # Delete the branch - if gh api -X DELETE "repos/Cratis/$repo/git/refs/heads/$branch" 2>/dev/null; then + # Delete the branch. + # A 422 ("Reference does not exist") means the branch disappeared between + # the existence check and the delete — treat as already gone (success). + # A 409 ("Git Repository is empty") means there is nothing to delete. + # Both are non-error conditions for the cleanup workflow. + delete_output=$(gh api -X DELETE "repos/Cratis/$repo/git/refs/heads/$branch" \ + 2>&1 || true) + delete_status=$(echo "$delete_output" | jq -r '.status // empty' 2>/dev/null || true) + if [ -z "$delete_output" ] || [ "$delete_status" = "422" ] || [ "$delete_status" = "409" ]; then echo " ✓ Deleted branch $branch from $repo" echo "$repo" >> "$deleted_file" else