From 86e13a23646cf19de2b09313e3dff18bcc397510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:20:50 +0000 Subject: [PATCH 1/3] Initial plan From 68c7112bc56e937d67625f39a151bdb704ba06a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:35:07 +0000 Subject: [PATCH 2/3] Fix bootstrap: use Git Data API for branch check, add sleep 5 before PR creation Co-authored-by: einari <134365+einari@users.noreply.github.com> --- .github/scripts/bootstrap-copilot-sync.sh | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/scripts/bootstrap-copilot-sync.sh b/.github/scripts/bootstrap-copilot-sync.sh index 3987993..515ae3f 100755 --- a/.github/scripts/bootstrap-copilot-sync.sh +++ b/.github/scripts/bootstrap-copilot-sync.sh @@ -404,24 +404,30 @@ echo "$repos" | jq -r '.[]' | while read -r repo; do # ---------------------------------------------------------------- # 11. Create PR (skip if one already exists for this branch) # ---------------------------------------------------------------- - # Poll GET /repos/{owner}/{repo}/branches/{branch} until the branch - # is visible to the regular REST API (which shares the same replica - # as the PR-creation endpoint). The Git Data API writes land - # immediately but the branches/pulls service may lag a few seconds. + # Poll GET /repos/{owner}/{repo}/git/ref/heads/{branch} until the + # branch ref is visible to the Git Data API. This uses the same + # endpoint that the rest of the script uses, avoiding permission + # asymmetries where fine-grained PATs can write git refs but cannot + # read via the higher-level /branches/{branch} endpoint. branch_accessible=false for wait_i in $(seq 1 6); do - branch_check=$(gh api "repos/Cratis/$repo/branches/$branch" \ - --jq '.name' 2>/dev/null || true) - if [ "$branch_check" = "$branch" ]; then + branch_check_err=$(mktemp) + branch_check=$(gh api "repos/Cratis/$repo/git/ref/heads/$branch" \ + --jq '.object.sha' 2>"$branch_check_err" || true) + if [ -n "$branch_check" ]; then + rm -f "$branch_check_err" branch_accessible=true break fi + branch_check_api_err=$(cat "$branch_check_err" 2>/dev/null || true) + rm -f "$branch_check_err" + [ -n "$branch_check_api_err" ] && echo " ℹ Branch check error: $branch_check_api_err" echo " ℹ Waiting for branch $branch to propagate (attempt $wait_i/6)..." sleep 10 done if [ "$branch_accessible" = "false" ]; then - echo " ⚠ Branch $branch not accessible via branches API after 60s; skipping PR creation for $repo" + echo " ⚠ Branch $branch not accessible via Git Data API after 60s; skipping PR creation for $repo" echo "$repo" >> "$pr_failures_file" continue fi @@ -445,6 +451,13 @@ echo "$repos" | jq -r '.[]' | while read -r repo; do # where it does not exist, causing "Head sha can't be blank" GraphQL # errors. The REST endpoint resolves the head branch name in the # context of the target repository without any local git lookup. + # + # Brief pause before the first attempt: the Git Data API write and the + # Pulls API read can race, causing a 422 "head: invalid" error even + # when the branch ref was confirmed above. 3 s was not enough in + # practice; 5 s covers typical propagation. The retry loop below + # provides an additional safety net. + sleep 5 pr_error=$(mktemp) pr_created=false max_attempts=3 From 20d8ae568121bd169d5fb8dcc2ece0b3f11fd33b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:45:42 +0000 Subject: [PATCH 3/3] Add test workflow to verify branch check endpoint and PR creation Co-authored-by: einari <134365+einari@users.noreply.github.com> --- .github/workflows/test-bootstrap-fix.yml | 181 +++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 .github/workflows/test-bootstrap-fix.yml diff --git a/.github/workflows/test-bootstrap-fix.yml b/.github/workflows/test-bootstrap-fix.yml new file mode 100644 index 0000000..91f623f --- /dev/null +++ b/.github/workflows/test-bootstrap-fix.yml @@ -0,0 +1,181 @@ +name: Test Bootstrap Fix +# Smoke-test for the branch-check and PR-creation fixes in bootstrap-copilot-sync.sh. +# Runs on push to copilot/test-bootstrap-workflow to give live evidence that: +# 1. GET /git/ref/heads/{branch} succeeds with the fine-grained PAT. +# 2. POST /repos/{owner}/{repo}/pulls succeeds (with sleep 5 propagation guard). +# +# This workflow will be deleted once the PR is merged and the bootstrap has been +# validated in production. + +on: + push: + branches: + - copilot/test-bootstrap-workflow + +permissions: + contents: read + +jobs: + verify-branch-check-endpoint: + name: Verify Git Data API branch check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify PAT authentication + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + login=$(gh api /user --jq '.login' 2>/dev/null || true) + if [ -z "$login" ]; then + echo "::error::PAT_WORKFLOWS is not set or cannot authenticate" + exit 1 + fi + echo "✓ Authenticated as $login" + + - name: Test /git/ref/heads/{branch} endpoint (the NEW branch check) + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + # This test exercises the EXACT endpoint pattern that was changed. + # We use Cratis/Chronicle (a known repo) and check its default branch. + # If the fine-grained PAT can read this, the propagation poll will work. + + echo "--- Testing /git/ref/heads/main on Cratis/Chronicle ---" + result=$(gh api "repos/Cratis/Chronicle/git/ref/heads/main" \ + --jq '.object.sha' 2>/dev/null || true) + + if [ -n "$result" ]; then + echo "✓ GET /git/ref/heads/main → sha=$result" + echo " The new branch check endpoint works with the fine-grained PAT." + else + echo "::error::GET /git/ref/heads/main returned empty — PAT cannot access this endpoint" + exit 1 + fi + + echo "" + echo "--- Testing /git/ref/heads/main on Cratis/Fundamentals ---" + result2=$(gh api "repos/Cratis/Fundamentals/git/ref/heads/main" \ + --jq '.object.sha' 2>/dev/null || true) + if [ -n "$result2" ]; then + echo "✓ GET /git/ref/heads/main → sha=$result2" + else + echo "::warning::GET /git/ref/heads/main on Fundamentals returned empty" + fi + + - name: Test OLD /branches/{branch} endpoint (should fail or be inaccessible) + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + # This step shows WHY the old endpoint was broken. + # A fine-grained PAT with only Contents:write may return empty here, + # causing the 60-second polling loop to always time out. + + echo "--- Testing /branches/main on Cratis/Chronicle ---" + result=$(gh api "repos/Cratis/Chronicle/branches/main" \ + --jq '.name' 2>/dev/null || true) + + if [ -n "$result" ]; then + echo "ℹ /branches/main returned: name=$result" + echo " This endpoint works for this PAT too." + echo " The issue was likely something else in those runs." + else + echo "✓ /branches/main returned EMPTY — confirmed the old endpoint is broken" + echo " This is why every run timed out: the PAT can read git refs" + echo " but not the higher-level /branches endpoint." + fi + # This step always succeeds — it is diagnostic only. + + verify-pr-creation: + name: Verify PR creation works + runs-on: ubuntu-latest + needs: verify-branch-check-endpoint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if add-copilot-sync-workflows branch exists on Chronicle + id: check-branch + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + # Previous bootstrap runs created the branch but no PR. + # Check if it still exists via the new endpoint pattern. + branch="add-copilot-sync-workflows" + sha=$(gh api "repos/Cratis/Chronicle/git/ref/heads/$branch" \ + --jq '.object.sha' 2>/dev/null || true) + + if [ -n "$sha" ]; then + echo "✓ Branch $branch exists on Chronicle (sha=$sha)" + echo "branch_exists=true" >> "$GITHUB_OUTPUT" + echo "branch_sha=$sha" >> "$GITHUB_OUTPUT" + else + echo "branch_exists=false" >> "$GITHUB_OUTPUT" + echo "ℹ Branch $branch does not exist on Chronicle (may have been cleaned up)" + fi + + - name: Check for existing PR on Chronicle + id: check-pr + if: steps.check-branch.outputs.branch_exists == 'true' + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + branch="add-copilot-sync-workflows" + pr_num=$(gh api "repos/Cratis/Chronicle/pulls?state=open&head=Cratis:$branch" \ + --jq '.[0].number // empty' 2>/dev/null || true) + + if [ -n "$pr_num" ]; then + echo "ℹ PR already exists: #$pr_num" + echo "pr_exists=true" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_num" >> "$GITHUB_OUTPUT" + else + echo "No open PR found for branch $branch" + echo "pr_exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create PR on Chronicle (proves PR creation works with sleep 5) + if: >- + steps.check-branch.outputs.branch_exists == 'true' && + steps.check-pr.outputs.pr_exists == 'false' + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + branch="add-copilot-sync-workflows" + + default_branch=$(gh api "repos/Cratis/Chronicle" \ + --jq '.default_branch' 2>/dev/null || true) + echo "Default branch: $default_branch" + + echo "Sleeping 5s (simulating post-branch-creation propagation guard)..." + sleep 5 + + pr_response=$(gh api -X POST "repos/Cratis/Chronicle/pulls" \ + -f title="Bootstrap Copilot sync workflows" \ + -f body="Test PR from bootstrap fix verification." \ + -f head="$branch" \ + -f base="$default_branch" \ + 2>/tmp/pr_err || true) + pr_url=$(echo "$pr_response" | jq -r '.html_url // empty' 2>/dev/null || true) + + if [ -n "$pr_url" ] && [ "$pr_url" != "null" ]; then + echo "✓ PR created: $pr_url" + echo " PR creation with plain branch name + sleep 5 works." + else + pr_err=$(cat /tmp/pr_err 2>/dev/null || true) + pr_msg=$(echo "$pr_response" | jq -r '.message // empty' 2>/dev/null || true) + pr_errs=$(echo "$pr_response" | jq -r '(.errors // []) | map(.message // .code // "unknown") | join("; ")' 2>/dev/null || true) + echo "::error::PR creation failed" + [ -n "$pr_err" ] && echo " API error: $pr_err" + [ -n "$pr_msg" ] && echo " GitHub message: $pr_msg" + [ -n "$pr_errs" ] && echo " GitHub errors: $pr_errs" + exit 1 + fi + + - name: Summary when branch does not exist + if: steps.check-branch.outputs.branch_exists != 'true' + run: | + echo "Branch add-copilot-sync-workflows not found on Chronicle." + echo "This likely means it was cleaned up after previous runs." + echo "Run the full bootstrap workflow on main to do a complete end-to-end test." + echo "The branch-check endpoint is verified by the first job."