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
29 changes: 21 additions & 8 deletions .github/scripts/bootstrap-copilot-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
181 changes: 181 additions & 0 deletions .github/workflows/test-bootstrap-fix.yml
Original file line number Diff line number Diff line change
@@ -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."
Loading