Skip to content

fix: use PR-based sync instead of direct push to protected main - #44

Merged
mateodelnorte merged 5 commits into
mainfrom
fix/sync-workflow-branch-protection
Mar 5, 2026
Merged

mateodelnorte merged 5 commits into
mainfrom
fix/sync-workflow-branch-protection

Conversation

@mateodelnorte

@mateodelnorte mateodelnorte commented Mar 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Branch protection on meta main requires PRs — no PAT can bypass this
  • Changed on-child-update.yml to create a sync/<repo>/<sha> branch, open a PR, and enable auto-merge (squash) instead of pushing directly
  • This fixes the recurring workflow failures from GH013: Repository rule violations

Test plan

  • Trigger a workflow_dispatch with test payload and verify it creates a branch + PR + auto-merges

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated repository sync workflow to use environment-based authentication.
    • Creates and manages a dedicated sync branch and pull request, enabling auto-squash merge and branch cleanup.
    • Made sync idempotent: detects existing branches/PRs to avoid duplication and handles concurrent runs gracefully.
    • Improves commit/PR formatting with cleaned messages and co-author attribution; exposes the resulting PR URL.

@coderabbitai

coderabbitai Bot commented Mar 5, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8412c896-1dc1-456a-a979-2c189a40aa70

📥 Commits

Reviewing files that changed from the base of the PR and between ca2bac0 and 13e0d56.

📒 Files selected for processing (1)
  • .github/workflows/on-child-update.yml

Walkthrough

Implements an idempotent branch-and-PR sync: uses GH_TOKEN (PARENT_REPO_PAT) to create or reuse branch sync/${REPO_NAME}/${SHORT_SHA}, cleans commit message with Co‑authored‑by, creates or finds a PR (${TYPE}(${REPO_NAME}): ${CLEAN_MSG}), and enables auto‑merge with squash and branch deletion.

Changes

Cohort / File(s) Summary
Workflow: on-child-update
.github/workflows/on-child-update.yml
Renames step to "Create sync branch and PR"; replaces PAT usage with PARENT_REPO_PAT/GH_TOKEN; derives branch sync/${REPO_NAME}/${SHORT_SHA}, PR title/body and cleaned message; adds idempotent branch handling (detect existing branch/PR, race‑condition resilience), creates branch/empty commit when needed, uses gh pr create with fallback to find existing PR, and enables auto‑merge (squash + delete branch).

Sequence Diagram(s)

sequenceDiagram
  participant Runner as Action Runner
  participant GHCLI as GitHub CLI / API
  participant ParentRepo as Parent Repository

  Runner->>GHCLI: authenticate with GH_TOKEN (PARENT_REPO_PAT)
  Runner->>ParentRepo: check for branch sync/${REPO_NAME}/${SHORT_SHA}
  alt branch exists
    Runner->>ParentRepo: find existing PR for branch
    alt PR exists
      Runner->>GHCLI: enable auto-merge on PR
      GHCLI-->>Runner: PR URL / status
    else no PR
      Runner->>ParentRepo: fetch & switch to branch (proceed to commit if needed)
    end
  else branch missing
    Runner->>ParentRepo: create branch, commit cleaned message + Co‑authored-by, push
    alt push race/failure
      Runner->>ParentRepo: fetch & re-check branch/PR
    end
  end
  Runner->>GHCLI: create PR (title/body) or find existing
  Runner->>GHCLI: enable auto-merge (squash + delete branch)
  GHCLI-->>Runner: return PR URL / status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I found a branch beneath the moon,

I trimmed the title, hummed a gentle tune,
Pushed a tiny commit, opened a neat PR,
Squash‑merge whispered — now the tree's less scarred.
Hoppity hop, the sync went far and wide.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: replacing direct protected branch pushes with a PR-based workflow that uses branch protection-compliant mechanisms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sync-workflow-branch-protection

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 5, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR reworks the on-child-update.yml sync workflow to comply with branch protection on main by replacing a direct git push with a sync/<repo>/<sha> branch + auto-merge PR flow, and swaps the checkout token from REPO_WRITE_PACKAGES_PAT to PARENT_REPO_PAT.

Key changes:

  • Creates a deterministic sync/${REPO_NAME}/${SHORT_SHA} branch, opens a PR against main, and enables auto-squash-merge with branch cleanup via gh pr merge --auto --squash --delete-branch.
  • Adds an idempotency guard: if the branch already exists (previous run partially completed), it checks for an open PR and re-enables auto-merge instead of failing; if the branch exists but has no PR, it falls through to gh pr create.
  • Moves the Co-authored-by trailer into the PR body so it survives the squash merge commit (previously only in the individual commit message, which is discarded on squash).

Issue: The gh pr create call at line 126 is not guarded against a TOCTOU race condition — if a concurrent run creates the PR between the idempotency check and the create call, the step will hard-fail with -e active, even though the desired state (a PR exists) has been achieved.

Confidence Score: 2/5

  • The PR is safe for single workflow execution, but has a narrow race condition that will cause failures under concurrent execution.
  • The core logic correctly addresses the protected-branch constraint, and idempotency handling covers the most common retry scenario. However, there is a genuine TOCTOU race between the idempotency check (line 106) and the gh pr create call (line 126) — if two workflows execute concurrently, one could create the PR while the other is between the check and create, causing the step to hard-fail even though the desired state (PR exists) is achieved. This is a realistic scenario with workflow retries or multiple concurrent dispatches.
  • .github/workflows/on-child-update.yml — specifically the gh pr create call at line 126 needs protection against concurrent PR creation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([workflow_dispatch / repository_dispatch]) --> B[Extract payload<br/>REPO_NAME, SHORT_SHA, MESSAGE, TYPE, ACTOR]
    B --> C{Branch<br/>sync/REPO/SHA<br/>exists remotely?}

    C -- Yes --> D{Open PR exists<br/>for branch + base main?}
    D -- Yes --> E[gh pr merge --auto --squash<br/>--delete-branch]
    E --> F([exit 0])

    D -- No --> G[git fetch + checkout<br/>existing branch]
    G --> H[gh pr create<br/>--title --body --head --base main]

    C -- No --> I[git checkout -b BRANCH<br/>git commit --allow-empty<br/>git push -u origin BRANCH]
    I --> H

    H --> J[gh pr merge PR_URL<br/>--auto --squash --delete-branch]
    J --> K[echo Created PR: PR_URL]
    K --> L([Log sync step])
Loading

Last reviewed commit: ca2bac0

Comment thread .github/workflows/on-child-update.yml Outdated
Comment on lines +103 to +105
git checkout -b "$BRANCH"
git commit --allow-empty -m "$COMMIT_MSG"
git push -u origin "$BRANCH"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branch push fails on workflow retry

If this workflow fails after the git push (e.g. during gh pr create) and is retried, the second run will fail at git push -u origin "$BRANCH" with "already exists" because the branch sync/${REPO_NAME}/${SHORT_SHA} was already created. Since the branch name is deterministic from REPO_NAME + SHORT_SHA, it never changes on retries.

A safe fix is to check whether the branch/PR already exists before pushing, or use --force on the push (though that's heavier). A lightweight approach:

# Check if branch already exists remotely
if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
  echo "Branch $BRANCH already exists, checking for open PR..."
  PR_URL=$(gh pr list --head "$BRANCH" --base main --json url -q '.[0].url')
else
  git checkout -b "$BRANCH"
  git commit --allow-empty -m "$COMMIT_MSG"
  git push -u origin "$BRANCH"
  PR_URL=$(gh pr create \
    --title "${TYPE}(${REPO_NAME}): ${CLEAN_MSG}" \
    --body "Synced from \`${REPO_NAME}@${SHORT_SHA}\`

Co-authored-by: ${ACTOR} <${ACTOR}@users.noreply.github.com>" \
    --head "$BRANCH" \
    --base main)
fi

gh pr merge "$PR_URL" --auto --squash

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47cace0 and 13e0d56 — the script now handles concurrent push failures by fetching the remote branch and recovering, and PR creation failures by looking up the existing PR. All three code paths (branch pre-exists, push race, PR create race) converge on the same idempotent outcome.

Comment thread .github/workflows/on-child-update.yml Outdated
Comment on lines +107 to +114
# Create PR and enable auto-merge
PR_URL=$(gh pr create \
--title "${TYPE}(${REPO_NAME}): ${CLEAN_MSG}" \
--body "Synced from \`${REPO_NAME}@${SHORT_SHA}\`" \
--head "$BRANCH" \
--base main)

gh pr merge "$PR_URL" --auto --squash

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Co-authored-by attribution lost in squash merge

When GitHub performs a squash merge, it generates the merge commit message from the PR title + PR body — not from the individual commit messages. The Co-authored-by trailer embedded in $COMMIT_MSG (on the empty commit) will be silently discarded in the squash commit.

To preserve attribution, include the trailer in the PR --body instead:

Suggested change
# Create PR and enable auto-merge
PR_URL=$(gh pr create \
--title "${TYPE}(${REPO_NAME}): ${CLEAN_MSG}" \
--body "Synced from \`${REPO_NAME}@${SHORT_SHA}\`" \
--head "$BRANCH" \
--base main)
gh pr merge "$PR_URL" --auto --squash
# Create PR and enable auto-merge
PR_URL=$(gh pr create \
--title "${TYPE}(${REPO_NAME}): ${CLEAN_MSG}" \
--body "Synced from \`${REPO_NAME}@${SHORT_SHA}\`
Co-authored-by: ${ACTOR} <${ACTOR}@users.noreply.github.com>" \
--head "$BRANCH" \
--base main)
gh pr merge "$PR_URL" --auto --squash --delete-branch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in 0bfb64a — Co-authored-by is now included in the PR body, which survives squash merge (GitHub copies PR body into squash commit message).

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

@greptile, please review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/on-child-update.yml:
- Line 40: The workflow currently uses an org-level secret token reference
`secrets.PARENT_REPO_PAT` for checkout/gh auth which broadens blast radius;
replace this with a repository-scoped credential (preferably a GitHub App
installation token or a fine-grained PAT) that only has the minimal permissions
required (`contents:write` and `pull_requests:write`) and a rotation policy, and
update all occurrences of `token: ${{ secrets.PARENT_REPO_PAT }}` (including the
other instance flagged) to reference the new secret (e.g.,
`secrets.REPO_SCOPED_TOKEN`) and ensure the workflow steps that call
`actions/checkout` and `gh` use that limited-scope token.
- Around line 96-113: The current flow assumes branch and PR don't already exist
and will fail on retries; update the script to detect and reuse existing branch
and PR instead of always running git checkout -b and gh pr create. Specifically:
check whether BRANCH already exists locally or remotely and only run git
checkout -b / git push -u origin "$BRANCH" when it does not (otherwise checkout
the existing branch and update it), and when creating the PR replace the blind
PR_URL=$(gh pr create ...) step with an idempotent sequence that attempts to
create the PR and, on failure due to "already exists", queries for the existing
PR (e.g., via gh pr list/filter by --head "$BRANCH" --base main) and sets PR_URL
to that existing PR result so retries reuse the same PR. Ensure
commit/allow-empty behavior still runs only when needed and that error paths
return the existing branch/PR instead of failing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 933795c9-eca9-485a-b124-83de5082a01b

📥 Commits

Reviewing files that changed from the base of the PR and between 37cda44 and 3eb5d0a.

📒 Files selected for processing (1)
  • .github/workflows/on-child-update.yml

Comment thread .github/workflows/on-child-update.yml
Comment thread .github/workflows/on-child-update.yml Outdated
@mateodelnorte

Copy link
Copy Markdown
Contributor Author

🤖 Agent starting (run-pr-comment-1772674300): pr-comment run dispatched.

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

🤖 Agent starting (run-pr-comment-1772674332): pr-comment run dispatched.

Comment thread .github/workflows/on-child-update.yml Outdated
# Check for an existing PR and just ensure auto-merge is enabled.
if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
echo "Branch $BRANCH already exists — checking for existing PR"
EXISTING_PR=$(gh pr list --head "$BRANCH" --json url --jq '.[0].url // empty')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gh pr list missing --base main filter in idempotency check

Without --base main, this query returns any open PR from $BRANCH regardless of its target. If, for any reason, a PR was manually opened from this branch to a different base, the idempotency path would treat it as the sync PR, incorrectly enable auto-merge on it, and silently exit — leaving the actual sync PR never created.

Since gh pr create below always targets main, the idempotency lookup should match the same base:

Suggested change
EXISTING_PR=$(gh pr list --head "$BRANCH" --json url --jq '.[0].url // empty')
EXISTING_PR=$(gh pr list --head "$BRANCH" --base main --json url --jq '.[0].url // empty')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ca2bac0 — added --base main to the gh pr list query so the idempotency check only matches PRs targeting main.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
.github/workflows/on-child-update.yml (1)

40-40: ⚠️ Potential issue | 🟠 Major

Use a repo-scoped credential instead of org-level PAT.

secrets.PARENT_REPO_PAT is still used in both checkout and gh auth, which keeps the secret blast radius broader than necessary.

🔐 Suggested tightening
       - name: Checkout
         uses: actions/checkout@v4
         with:
-          token: ${{ secrets.PARENT_REPO_PAT }}
+          token: ${{ secrets.REPO_SCOPED_TOKEN }}
@@
         env:
@@
-          GH_TOKEN: ${{ secrets.PARENT_REPO_PAT }}
+          GH_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}

Also applies to: 89-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/on-child-update.yml at line 40, Replace the org-scoped
secret referenced as secrets.PARENT_REPO_PAT with a repo-scoped secret and
update all usages; create a repo-scoped secret (e.g., PARENT_REPO_TOKEN) in this
repository and change the token: ${{ secrets.PARENT_REPO_PAT }} occurrences used
by the checkout step and by the gh auth invocation to token: ${{
secrets.PARENT_REPO_TOKEN }} so both the checkout action and the gh
authentication use the new repo-scoped credential instead of the org-level PAT.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/on-child-update.yml:
- Around line 109-111: The workflow currently masks failures from the gh pr
merge command by appending "|| true" to the gh pr merge "$EXISTING_PR" --auto
--squash --delete-branch invocation which causes subsequent exit 0 to always
succeed; remove the "|| true" (or otherwise capture and handle the exit status)
so that a failed gh pr merge causes the job to fail or to branch into explicit
error-handling logic instead of falling through to the unconditional exit 0;
update the block containing the gh pr merge "$EXISTING_PR" --auto --squash
--delete-branch call and the following exit 0 to propagate non-zero exit codes
or add explicit retry/error reporting logic.

---

Duplicate comments:
In @.github/workflows/on-child-update.yml:
- Line 40: Replace the org-scoped secret referenced as secrets.PARENT_REPO_PAT
with a repo-scoped secret and update all usages; create a repo-scoped secret
(e.g., PARENT_REPO_TOKEN) in this repository and change the token: ${{
secrets.PARENT_REPO_PAT }} occurrences used by the checkout step and by the gh
auth invocation to token: ${{ secrets.PARENT_REPO_TOKEN }} so both the checkout
action and the gh authentication use the new repo-scoped credential instead of
the org-level PAT.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1418a37c-ecaa-4091-84ef-1a2c4e743c35

📥 Commits

Reviewing files that changed from the base of the PR and between 3eb5d0a and 0bfb64a.

📒 Files selected for processing (1)
  • .github/workflows/on-child-update.yml

Comment thread .github/workflows/on-child-update.yml Outdated
@mateodelnorte

Copy link
Copy Markdown
Contributor Author

🤖 Agent starting (run-pr-comment-1772675464): pr-comment run dispatched.

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

Pushed ca2bac0 addressing remaining review comments:

  • Greptile (missing --base main): Added --base main filter to gh pr list in the idempotency check so it only matches PRs targeting main
  • CodeRabbit (|| true masking failures): Removed || true from gh pr merge so auto-merge failures are properly surfaced

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

@greptile, @CodeRabbit. please review.

@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown

@mateodelnorte: Sure! I'll review the latest changes, including the fixes for the --base main filter and the || true removal.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/on-child-update.yml:
- Around line 115-131: The workflow can race when two runs create the same
branch/PR; add a recovery path around the git push and gh pr create steps by
detecting failures and reusing any existing remote branch/PR: if git push -u
origin "$BRANCH" fails, run a recovery sequence that fetches origin, checks out
or resets to origin/"$BRANCH" (preserving the intended commit/empty commit
semantics) and sets the upstream instead of failing; similarly, if gh pr create
fails because a PR already exists, query GitHub for an existing PR for --head
"$BRANCH" (e.g., via gh pr list/gh pr view) and populate PR_URL/PR_BODY/PR_TITLE
from that existing PR instead of erroring. Ensure these recovery checks
reference the same BRANCH/PR_URL/PR_BODY/PR_TITLE variables so the script
becomes idempotent under concurrent runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c33ef9c0-6136-47e6-b7bf-50741033fb7c

📥 Commits

Reviewing files that changed from the base of the PR and between 0bfb64a and ca2bac0.

📒 Files selected for processing (1)
  • .github/workflows/on-child-update.yml

Comment thread .github/workflows/on-child-update.yml Outdated
Comment thread .github/workflows/on-child-update.yml Outdated
Comment on lines +126 to +130
PR_URL=$(gh pr create \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--head "$BRANCH" \
--base main)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gh pr create unguarded against concurrent PR creation

There is a race condition between the idempotency check at line 106 and the gh pr create call here. If a concurrent workflow run creates the PR while this run is executing the code between the two calls, gh pr create will exit non-zero with a "PR already exists" error, causing the step to fail even though the desired state (a PR exists) has been achieved.

A safe fix is to use error suppression and fall back to looking up the existing PR:

Suggested change
PR_URL=$(gh pr create \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--head "$BRANCH" \
--base main)
PR_URL=$(gh pr create \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--head "$BRANCH" \
--base main) \
|| PR_URL=$(gh pr list --head "$BRANCH" --base main --json url --jq '.[0].url')
gh pr merge "$PR_URL" --auto --squash --delete-branch
echo "Created PR: $PR_URL"

This ensures the step succeeds whether the PR was created by this run or a concurrent one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47cace0 — gh pr create is now wrapped with failure recovery that queries for an existing PR on the same branch, making the entire workflow idempotent under concurrent execution.

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

🤖 Agent starting (run-pr-review-1772676044): pr-review run dispatched.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.github/workflows/on-child-update.yml (1)

102-130: ⚠️ Potential issue | 🟠 Major

Race window still present in branch push and PR creation path.

Line 104 checks existence, but Line 122 and Line 126 still assume no concurrent run won the race in between. This can fail otherwise-valid retries with deterministic branch names.

Suggested race-safe idempotency patch
           if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
             echo "Branch $BRANCH already exists — checking for existing PR"
             EXISTING_PR=$(gh pr list --head "$BRANCH" --base main --json url --jq '.[0].url // empty')
             if [[ -n "$EXISTING_PR" ]]; then
               echo "PR already exists: $EXISTING_PR"
               gh pr merge "$EXISTING_PR" --auto --squash --delete-branch
               exit 0
             fi
             # Branch exists but no PR — fetch it and create PR below
             git fetch origin "$BRANCH"
             git checkout "$BRANCH"
           else
             git checkout -b "$BRANCH"
             git commit --allow-empty -m "$PR_TITLE
@@
           Co-authored-by: ${ACTOR} <${ACTOR}@users.noreply.github.com>"
-            git push -u origin "$BRANCH"
+            if ! git push -u origin "$BRANCH"; then
+              echo "Branch push raced; reusing remote branch."
+              git fetch origin "$BRANCH"
+              git checkout -B "$BRANCH" "origin/$BRANCH"
+            fi
           fi

           # Create PR with Co-authored-by in body (survives squash merge)
-          PR_URL=$(gh pr create \
-            --title "$PR_TITLE" \
-            --body "$PR_BODY" \
-            --head "$BRANCH" \
-            --base main)
+          PR_URL="$(gh pr list --head "$BRANCH" --base main --state open --json url --jq '.[0].url // empty')"
+          if [[ -z "$PR_URL" ]]; then
+            if ! PR_URL=$(gh pr create \
+              --title "$PR_TITLE" \
+              --body "$PR_BODY" \
+              --head "$BRANCH" \
+              --base main); then
+              PR_URL="$(gh pr list --head "$BRANCH" --base main --state open --json url --jq '.[0].url // empty')"
+              [[ -n "$PR_URL" ]] || exit 1
+            fi
+          fi
#!/bin/bash
set -euo pipefail

FILE=".github/workflows/on-child-update.yml"

echo "== Relevant section =="
sed -n '96,136p' "$FILE"

echo
echo "== Guard checks =="
rg -n 'git ls-remote|git push -u origin "\$BRANCH"|gh pr create|gh pr list --head "\$BRANCH" --base main' "$FILE"

echo
echo "Expected race-safe indicators:"
echo "- guarded push fallback: if ! git push -u origin \"\$BRANCH\"; then ..."
echo "- pre-create open PR lookup: --state open"
echo "- post-create recovery lookup when create fails"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/on-child-update.yml around lines 102 - 130, The current
flow has a race between checking branch existence and later git push/gh pr
create calls; make the sequence robust by turning the optimistic branch push
into a guarded push (attempt git push -u origin "$BRANCH" and if it fails
because the remote branch already exists, fetch and checkout the remote branch
instead), ensure pre-PR lookup uses gh pr list --head "$BRANCH" --base main
--state open to only consider open PRs, and after gh pr create fails (or returns
a non-zero) re-query gh pr list for an open PR and enable auto-merge/merge that
PR (same behavior as the existing block that calls gh pr merge "$EXISTING_PR"
--auto --squash --delete-branch); update logic around BRANCH, git push -u origin
"$BRANCH", gh pr create, and gh pr list --head "$BRANCH" to implement these
guarded/fallback checks so retrying workflows are idempotent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/on-child-update.yml:
- Around line 102-130: The current flow has a race between checking branch
existence and later git push/gh pr create calls; make the sequence robust by
turning the optimistic branch push into a guarded push (attempt git push -u
origin "$BRANCH" and if it fails because the remote branch already exists, fetch
and checkout the remote branch instead), ensure pre-PR lookup uses gh pr list
--head "$BRANCH" --base main --state open to only consider open PRs, and after
gh pr create fails (or returns a non-zero) re-query gh pr list for an open PR
and enable auto-merge/merge that PR (same behavior as the existing block that
calls gh pr merge "$EXISTING_PR" --auto --squash --delete-branch); update logic
around BRANCH, git push -u origin "$BRANCH", gh pr create, and gh pr list --head
"$BRANCH" to implement these guarded/fallback checks so retrying workflows are
idempotent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 088a9ca6-4aa6-4c83-88a6-a30ea25b7428

📥 Commits

Reviewing files that changed from the base of the PR and between 0bfb64a and ca2bac0.

📒 Files selected for processing (1)
  • .github/workflows/on-child-update.yml

mateodelnorte and others added 5 commits March 4, 2026 20:01
Branch protection on meta main requires PRs — no PAT can push directly.
Changed the sync workflow to create a branch, open a PR, and enable
auto-merge (squash) instead of pushing directly to main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Retry safety: check if sync branch already exists before creating,
  reuse existing PR if found
- Co-authored-by in PR body so it survives squash merge
- --delete-branch on gh pr merge to prevent branch accumulation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…heck

- Add --base main to gh pr list so idempotency check only matches PRs
  targeting main, not PRs from the same branch to other bases
- Remove || true from gh pr merge so auto-merge failures are surfaced
  instead of silently swallowed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add recovery paths for when two workflow runs execute simultaneously:
- git push failure: fetch the remote branch and recover gracefully
- gh pr create failure: look up the existing PR instead of failing
- Extract shared helpers (find_existing_pr, enable_auto_merge) to
  reduce duplication across idempotency paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 2>&1 on git push and gh pr create could mix stderr warnings
into PR_URL, breaking downstream enable_auto_merge calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mateodelnorte
mateodelnorte force-pushed the fix/sync-workflow-branch-protection branch from ca2bac0 to 13e0d56 Compare March 5, 2026 02:03
@mateodelnorte

Copy link
Copy Markdown
Contributor Author

Push: Race condition handling + self-review fixes

New commits

  • 47cace0 — fix: handle race conditions in concurrent sync workflow runs
    • Added recovery for git push failure (concurrent run pushed same branch)
    • Added recovery for gh pr create failure (PR already exists)
    • Extracted find_existing_pr() and enable_auto_merge() helpers to reduce duplication
  • 13e0d56 — fix: remove stderr redirects that could corrupt captured variables
    • Removed 2>&1 from git push and gh pr create that could mix stderr into PR_URL

Comments addressed

  • Race condition on concurrent branch/PR creation (CodeRabbit #2887116129, Greptile #2887122016) — 3 recovery paths now handle all timing scenarios
  • Branch push retry failure (Greptile #2887045655) — push failure triggers fetch + recovery
  • Idempotent PR creation (CodeRabbit #2887050433) — enhanced with concurrent-safe fallbacks
  • Co-authored-by in squash (Greptile #2887045689) — confirmed already fixed in 0bfb64a
  • PARENT_REPO_PAT scope (CodeRabbit #2887050432) — intentional per @mateodelnorte
  • --base main filter (Greptile #2887062047) — already fixed in ca2bac0
  • || true masking (CodeRabbit #2887063099) — already fixed in ca2bac0

@mateodelnorte

Copy link
Copy Markdown
Contributor Author

Final Summary

All CI checks green. All review comments addressed.

Commits on this branch (5)

Commit Description
2c8be3d Use PR-based sync instead of direct push to protected main
eefb622 Add retry idempotency, squash attribution, and branch cleanup
9b130e3 Add --base main filter and remove || true error masking
47cace0 Handle race conditions in concurrent sync workflow runs
13e0d56 Remove stderr redirects that could corrupt captured variables

Review comments resolved

  • Race condition on concurrent runs (CodeRabbit, Greptile) — added recovery paths for git push and gh pr create failures when two runs execute simultaneously
  • Branch push retry failure (Greptile) — push failure triggers fetch + branch recovery
  • Idempotent PR creation (CodeRabbit) — workflow now converges to same outcome regardless of timing
  • Co-authored-by in squash merge (Greptile) — Co-authored-by placed in PR body (survives squash)
  • --base main filter missing (Greptile) — added to gh pr list query
  • || true masking errors (CodeRabbit) — removed so failures propagate
  • PARENT_REPO_PAT scope (CodeRabbit) — intentional per @mateodelnorte (org-level needed for meta-repo pattern)

Self-review fixes

  • Removed 2>&1 stderr redirects on git push and gh pr create that could corrupt PR_URL with error text
  • Extracted find_existing_pr() and enable_auto_merge() helpers to reduce duplication across 3 idempotency paths

No issues flagged for human review

@mateodelnorte
mateodelnorte merged commit e65ce03 into main Mar 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant