fix: handle existing sync PRs in upstream-sync workflow
Add idempotency to the merge-upstream job: 1. Check for existing open PR from chore/upstream-sync-* branch 2. If PR exists and branch is up to date with upstream → skip 3. If PR exists but upstream has new commits → checkout existing branch, merge new upstream commits, run Claude to review/test 4. If no PR exists → create new branch (existing behavior) Also update Claude's prompt to handle both create and update PR cases, so it knows to push to the existing PR branch instead of creating a duplicate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
88e7747d2f
commit
b1524d7857
2 files changed
+108
-14
No files matched your search
+56
-8
@@ -233,6 +233,8 @@ _merge_branch_ref = h.gha_steps("prepare", "branch")
|
|||||||
_merge_behind_ref = h.gha_needs("check-upstream", "behind")
|
_merge_behind_ref = h.gha_needs("check-upstream", "behind")
|
||||||
_merge_ahead_ref = h.gha_needs("check-upstream", "ahead")
|
_merge_ahead_ref = h.gha_needs("check-upstream", "ahead")
|
||||||
_merge_issue_ref = h.gha_needs("check-upstream", "issue_number")
|
_merge_issue_ref = h.gha_needs("check-upstream", "issue_number")
|
||||||
|
_merge_existing_pr_ref = h.gha_steps("existing-pr", "pr_exists")
|
||||||
|
_merge_existing_pr_number_ref = h.gha_steps("existing-pr", "pr_number")
|
||||||
_merge_clean_ref = h.gha_steps("prepare", "merge_clean")
|
_merge_clean_ref = h.gha_steps("prepare", "merge_clean")
|
||||||
_merge_affected_ref = h.gha_steps("prepare", "affected_files")
|
_merge_affected_ref = h.gha_steps("prepare", "affected_files")
|
||||||
|
|
||||||
@@ -243,6 +245,7 @@ You are automating an upstream sync for our Forgejo fork of anthropics/claude-co
|
|||||||
- Branch: ${_merge_branch_ref}
|
- Branch: ${_merge_branch_ref}
|
||||||
- ${_merge_behind_ref} commits behind upstream, ${_merge_ahead_ref} commits ahead (our Forgejo changes)
|
- ${_merge_behind_ref} commits behind upstream, ${_merge_ahead_ref} commits ahead (our Forgejo changes)
|
||||||
- Tracking issue: #${_merge_issue_ref}
|
- Tracking issue: #${_merge_issue_ref}
|
||||||
|
- Existing PR: ${_merge_existing_pr_ref} (if true, PR #${_merge_existing_pr_number_ref} already exists — update it instead of creating a new one)
|
||||||
- Merge was attempted. Status: ${_merge_clean_ref} (true = clean, false = has conflicts)
|
- Merge was attempted. Status: ${_merge_clean_ref} (true = clean, false = has conflicts)
|
||||||
|
|
||||||
## Impact analysis
|
## Impact analysis
|
||||||
@@ -273,8 +276,10 @@ The fewer upstream files we modify, the fewer conflicts in the next sync.
|
|||||||
2. Run typecheck: bun run typecheck
|
2. Run typecheck: bun run typecheck
|
||||||
3. Check formatting: bun run format:check — if issues, fix with bun run format
|
3. Check formatting: bun run format:check — if issues, fix with bun run format
|
||||||
4. Commit any remaining changes and push the branch
|
4. Commit any remaining changes and push the branch
|
||||||
5. Create a PR targeting main via the Forgejo API:
|
5. Push the branch, then create or update the PR:
|
||||||
POST ${"$"}GITHUB_API_URL/repos/${"$"}GITHUB_REPOSITORY/pulls
|
- If existing PR is "true": PR #${_merge_existing_pr_number_ref} already exists. Just push the branch — the PR updates automatically.
|
||||||
|
Optionally update the PR body via PATCH ${"$"}GITHUB_API_URL/repos/${"$"}GITHUB_REPOSITORY/pulls/${_merge_existing_pr_number_ref}
|
||||||
|
- If existing PR is "false": Create a new PR via POST ${"$"}GITHUB_API_URL/repos/${"$"}GITHUB_REPOSITORY/pulls
|
||||||
Auth: token ${"$"}GITHUB_TOKEN
|
Auth: token ${"$"}GITHUB_TOKEN
|
||||||
Title: "chore: merge ${_merge_behind_ref} upstream commits from anthropics/claude-code-action"
|
Title: "chore: merge ${_merge_behind_ref} upstream commits from anthropics/claude-code-action"
|
||||||
Body should include:
|
Body should include:
|
||||||
@@ -491,6 +496,29 @@ fi"""
|
|||||||
token = h.gha_secret("CLAUDE_BOT_TOKEN")
|
token = h.gha_secret("CLAUDE_BOT_TOKEN")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "Check for existing sync PR"
|
||||||
|
id = "existing-pr"
|
||||||
|
run = """\
|
||||||
|
# Search for open PRs from chore/upstream-sync-* branches
|
||||||
|
RESPONSE=${"$"}(curl -s \\
|
||||||
|
-H "Authorization: token ${h.gha_secret("CLAUDE_BOT_TOKEN")}" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
"${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}/pulls?state=open&limit=50")
|
||||||
|
|
||||||
|
PR_BRANCH=${"$"}(echo "${"$"}RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].head.ref // ""')
|
||||||
|
|
||||||
|
if [ -n "${"$"}PR_BRANCH" ]; then
|
||||||
|
PR_NUMBER=${"$"}(echo "${"$"}RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].number')
|
||||||
|
echo "Found existing sync PR #${"$"}PR_NUMBER on branch ${"$"}PR_BRANCH"
|
||||||
|
echo "pr_exists=true" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
echo "pr_branch=${"$"}PR_BRANCH" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
echo "pr_number=${"$"}PR_NUMBER" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "No existing sync PR found"
|
||||||
|
echo "pr_exists=false" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
fi"""
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "Prepare upstream merge"
|
name = "Prepare upstream merge"
|
||||||
id = "prepare"
|
id = "prepare"
|
||||||
@@ -501,6 +529,29 @@ git config user.email "claude@wxs.ro"
|
|||||||
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
||||||
git fetch upstream main --quiet
|
git fetch upstream main --quiet
|
||||||
|
|
||||||
|
# Check if existing PR branch is already up to date with upstream
|
||||||
|
if [ "${h.gha_steps("existing-pr", "pr_exists")}" = "true" ]; then
|
||||||
|
PR_BRANCH="${h.gha_steps("existing-pr", "pr_branch")}"
|
||||||
|
git checkout "${"$"}PR_BRANCH"
|
||||||
|
|
||||||
|
# Check if upstream/main is already merged into the PR branch
|
||||||
|
BEHIND=${"$"}(git rev-list "${"$"}PR_BRANCH"..upstream/main --count)
|
||||||
|
if [ "${"$"}BEHIND" -eq 0 ]; then
|
||||||
|
echo "Existing PR branch ${"$"}PR_BRANCH is already up to date with upstream"
|
||||||
|
echo "skip=true" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Existing PR branch ${"$"}PR_BRANCH is ${"$"}BEHIND commits behind upstream — updating"
|
||||||
|
BRANCH="${"$"}PR_BRANCH"
|
||||||
|
else
|
||||||
|
BRANCH="chore/upstream-sync-${"$"}(date +%Y%m%d)"
|
||||||
|
git checkout -b "${"$"}BRANCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "skip=false" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
echo "branch=${"$"}BRANCH" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Files our fork has modified vs upstream
|
# Files our fork has modified vs upstream
|
||||||
FORK_FILES=${"$"}(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^${"$"}')
|
FORK_FILES=${"$"}(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^${"$"}')
|
||||||
echo "=== Fork-modified files ==="
|
echo "=== Fork-modified files ==="
|
||||||
@@ -531,10 +582,6 @@ echo "affected_files<<EOF" >> "${"$"}GITHUB_OUTPUT"
|
|||||||
echo "${"$"}{AFFECTED:-(none)}" >> "${"$"}GITHUB_OUTPUT"
|
echo "${"$"}{AFFECTED:-(none)}" >> "${"$"}GITHUB_OUTPUT"
|
||||||
echo "EOF" >> "${"$"}GITHUB_OUTPUT"
|
echo "EOF" >> "${"$"}GITHUB_OUTPUT"
|
||||||
|
|
||||||
BRANCH="chore/upstream-sync-${"$"}(date +%Y%m%d)"
|
|
||||||
git checkout -b "${"$"}BRANCH"
|
|
||||||
echo "branch=${"$"}BRANCH" >> "${"$"}GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
if git merge upstream/main --no-edit; then
|
if git merge upstream/main --no-edit; then
|
||||||
echo "merge_clean=true" >> "${"$"}GITHUB_OUTPUT"
|
echo "merge_clean=true" >> "${"$"}GITHUB_OUTPUT"
|
||||||
echo "Merge completed cleanly"
|
echo "Merge completed cleanly"
|
||||||
@@ -543,10 +590,11 @@ else
|
|||||||
echo "Merge has conflicts — Claude will resolve them"
|
echo "Merge has conflicts — Claude will resolve them"
|
||||||
fi"""
|
fi"""
|
||||||
}
|
}
|
||||||
_bun_install()
|
_bun_install() | {"if" = "steps.prepare.outputs.skip != 'true'"}
|
||||||
_bun_deps()
|
_bun_deps() | {"if" = "steps.prepare.outputs.skip != 'true'"}
|
||||||
{
|
{
|
||||||
name = "Claude: review merge and create PR"
|
name = "Claude: review merge and create PR"
|
||||||
|
"if" = "steps.prepare.outputs.skip != 'true'"
|
||||||
uses = _claude_action_url
|
uses = _claude_action_url
|
||||||
"with" = {
|
"with" = {
|
||||||
prompt = _merge_prompt
|
prompt = _merge_prompt
|
||||||
|
|||||||
@@ -187,6 +187,27 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
token: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
token: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
||||||
|
- name: Check for existing sync PR
|
||||||
|
id: existing-pr
|
||||||
|
run: |-
|
||||||
|
# Search for open PRs from chore/upstream-sync-* branches
|
||||||
|
RESPONSE=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/pulls?state=open&limit=50")
|
||||||
|
|
||||||
|
PR_BRANCH=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].head.ref // ""')
|
||||||
|
|
||||||
|
if [ -n "$PR_BRANCH" ]; then
|
||||||
|
PR_NUMBER=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].number')
|
||||||
|
echo "Found existing sync PR #$PR_NUMBER on branch $PR_BRANCH"
|
||||||
|
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "pr_branch=$PR_BRANCH" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "No existing sync PR found"
|
||||||
|
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
- name: Prepare upstream merge
|
- name: Prepare upstream merge
|
||||||
id: prepare
|
id: prepare
|
||||||
run: |-
|
run: |-
|
||||||
@@ -196,6 +217,29 @@ jobs:
|
|||||||
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
git remote add upstream https://github.com/anthropics/claude-code-action.git
|
||||||
git fetch upstream main --quiet
|
git fetch upstream main --quiet
|
||||||
|
|
||||||
|
# Check if existing PR branch is already up to date with upstream
|
||||||
|
if [ "${{ steps.existing-pr.outputs.pr_exists }}" = "true" ]; then
|
||||||
|
PR_BRANCH="${{ steps.existing-pr.outputs.pr_branch }}"
|
||||||
|
git checkout "$PR_BRANCH"
|
||||||
|
|
||||||
|
# Check if upstream/main is already merged into the PR branch
|
||||||
|
BEHIND=$(git rev-list "$PR_BRANCH"..upstream/main --count)
|
||||||
|
if [ "$BEHIND" -eq 0 ]; then
|
||||||
|
echo "Existing PR branch $PR_BRANCH is already up to date with upstream"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Existing PR branch $PR_BRANCH is $BEHIND commits behind upstream — updating"
|
||||||
|
BRANCH="$PR_BRANCH"
|
||||||
|
else
|
||||||
|
BRANCH="chore/upstream-sync-$(date +%Y%m%d)"
|
||||||
|
git checkout -b "$BRANCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Files our fork has modified vs upstream
|
# Files our fork has modified vs upstream
|
||||||
FORK_FILES=$(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^$')
|
FORK_FILES=$(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^$')
|
||||||
echo "=== Fork-modified files ==="
|
echo "=== Fork-modified files ==="
|
||||||
@@ -226,10 +270,6 @@ jobs:
|
|||||||
echo "${AFFECTED:-(none)}" >> "$GITHUB_OUTPUT"
|
echo "${AFFECTED:-(none)}" >> "$GITHUB_OUTPUT"
|
||||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
BRANCH="chore/upstream-sync-$(date +%Y%m%d)"
|
|
||||||
git checkout -b "$BRANCH"
|
|
||||||
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
if git merge upstream/main --no-edit; then
|
if git merge upstream/main --no-edit; then
|
||||||
echo "merge_clean=true" >> "$GITHUB_OUTPUT"
|
echo "merge_clean=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "Merge completed cleanly"
|
echo "Merge completed cleanly"
|
||||||
@@ -252,9 +292,12 @@ jobs:
|
|||||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
|
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
|
||||||
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
|
if: steps.prepare.outputs.skip != 'true'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
if: steps.prepare.outputs.skip != 'true'
|
||||||
- name: 'Claude: review merge and create PR'
|
- name: 'Claude: review merge and create PR'
|
||||||
|
if: steps.prepare.outputs.skip != 'true'
|
||||||
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/claude-code-action@main
|
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/claude-code-action@main
|
||||||
with:
|
with:
|
||||||
prompt: |-
|
prompt: |-
|
||||||
@@ -264,6 +307,7 @@ jobs:
|
|||||||
- Branch: ${{ steps.prepare.outputs.branch }}
|
- Branch: ${{ steps.prepare.outputs.branch }}
|
||||||
- ${{ needs.check-upstream.outputs.behind }} commits behind upstream, ${{ needs.check-upstream.outputs.ahead }} commits ahead (our Forgejo changes)
|
- ${{ needs.check-upstream.outputs.behind }} commits behind upstream, ${{ needs.check-upstream.outputs.ahead }} commits ahead (our Forgejo changes)
|
||||||
- Tracking issue: #${{ needs.check-upstream.outputs.issue_number }}
|
- Tracking issue: #${{ needs.check-upstream.outputs.issue_number }}
|
||||||
|
- Existing PR: ${{ steps.existing-pr.outputs.pr_exists }} (if true, PR #${{ steps.existing-pr.outputs.pr_number }} already exists — update it instead of creating a new one)
|
||||||
- Merge was attempted. Status: ${{ steps.prepare.outputs.merge_clean }} (true = clean, false = has conflicts)
|
- Merge was attempted. Status: ${{ steps.prepare.outputs.merge_clean }} (true = clean, false = has conflicts)
|
||||||
|
|
||||||
## Impact analysis
|
## Impact analysis
|
||||||
@@ -294,8 +338,10 @@ jobs:
|
|||||||
2. Run typecheck: bun run typecheck
|
2. Run typecheck: bun run typecheck
|
||||||
3. Check formatting: bun run format:check — if issues, fix with bun run format
|
3. Check formatting: bun run format:check — if issues, fix with bun run format
|
||||||
4. Commit any remaining changes and push the branch
|
4. Commit any remaining changes and push the branch
|
||||||
5. Create a PR targeting main via the Forgejo API:
|
5. Push the branch, then create or update the PR:
|
||||||
POST $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls
|
- If existing PR is "true": PR #${{ steps.existing-pr.outputs.pr_number }} already exists. Just push the branch — the PR updates automatically.
|
||||||
|
Optionally update the PR body via PATCH $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls/${{ steps.existing-pr.outputs.pr_number }}
|
||||||
|
- If existing PR is "false": Create a new PR via POST $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls
|
||||||
Auth: token $GITHUB_TOKEN
|
Auth: token $GITHUB_TOKEN
|
||||||
Title: "chore: merge ${{ needs.check-upstream.outputs.behind }} upstream commits from anthropics/claude-code-action"
|
Title: "chore: merge ${{ needs.check-upstream.outputs.behind }} upstream commits from anthropics/claude-code-action"
|
||||||
Body should include:
|
Body should include:
|
||||||
|
|||||||
Reference in new issue
Block a user