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
28 changes: 4 additions & 24 deletions .github/workflows/bootstrap-copilot-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
Expand Down
29 changes: 22 additions & 7 deletions .github/workflows/cleanup-copilot-sync-branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down