Skip to content

Commit f8a4ad6

Browse files
Merge sync PRs to prerelease right away and retry failed backports (#2293) (#2295)
* Merge sync PRs to prerelease right away and retry failed backports Since the v1.10 release merge, every auto-backport to prerelease has failed, and the fixes reached prerelease only through manual backports weeks later. Most failures were an ordering race rather than real divergence: sync PRs stay open for minutes to hours, and a second PR to main touching the same file is cherry-picked onto a prerelease that does not have the first change yet. #2141 conflicted on a freeze file because the sync PR of #2138 merged 19 seconds after it. Failed backports are never retried, so each one then breaks every later PR on the same files (#2227 after #2141, #2258 and #2257 after #2235). The workflow now merges a clean sync PR immediately and publishes prerelease itself, since a merge made with GITHUB_TOKEN does not fire the push event publish.yml listens to. A failed backport labels the source PR backport-failed, so the missing changes are listable instead of buried in bot comments. After each sync merge, and after any manual merge into prerelease, the workflow comments /sync-prerelease on the oldest labeled PR, so a backport that only waited on an earlier change applies on its own. No concurrency group: GitHub cancels a pending run when another queues, which would drop backports silently when several PRs merge within minutes. The cost of merging without review is that a main-only PR now needs the no-sync-prerelease label before it merges, where it used to be enough to leave the sync PR unmerged. * Label sync PRs that fail to merge A sync PR can also fail to merge when prerelease moves between the cherry-pick and the merge. That backport now gets the backport-failed label too, instead of being missed by the list and the retry. The open sync PR is left for a manual merge. Publishing no longer depends on the label cleanup succeeding after the merge, since nothing else publishes a GITHUB_TOKEN merge. * Retry the oldest failed backport even past 30 labeled PRs gh pr list returns at most 30 results by default, newest first, so with more labeled PRs the sort only saw the newest 30 and the oldest backport, often the one the others wait on, would never be retried. (cherry picked from commit 35c61f4) Co-authored-by: Christophe Dervieux <christophe.dervieux@gmail.com>
1 parent aff04d7 commit f8a4ad6

2 files changed

Lines changed: 98 additions & 18 deletions

File tree

‎.github/workflows/README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ Note that technically, <prerelease.quarto.org> is also a deploy preview on Netli
3333
- `port-to-prerelease.yml` - This workflow is used to sync changes made to main for quarto.org to prerelease branch for prerelease.quarto.org.
3434
- It is triggered when a PR is merged in to `main`. It can also be triggered manually by adding a comment `/sync-prerelease` on a merged PR.
3535
- This workflow uses [`korthout/backport-action`](https://github.com/korthout/backport-action) to create a PR with the changes merged into `main` branch to be synced to `prerelease` branch.
36-
- It will also write a new `/deploy-preview` comment in the new PR to trigger the preview deployment from `preview.yml`.
37-
- This is possible because it uses a fine-grained PAT token which allows a workflow to trigger another using usual event (GITHUB_TOKEN does not allow that usually). This is configured in repo secrets.
36+
- When the cherry-pick applies cleanly, the workflow merges the sync PR right away and publishes prerelease by calling `publish.yml` (a merge made with GITHUB_TOKEN does not trigger the usual push event). Merging immediately keeps prerelease current, so the next PR touching the same files does not conflict against a sync PR still waiting for review.
37+
- A PR that must not reach prerelease (e.g. docs for behavior that differs between versions) needs the `no-sync-prerelease` label **before** it is merged into `main`.
38+
- When the cherry-pick fails, the source PR gets the `backport-failed` label: `gh pr list --label backport-failed --state merged` lists every change still missing from prerelease. After backporting one by hand, remove its label.
39+
- After each sync merge, and after any PR is merged into `prerelease` by hand, the workflow retries the oldest `backport-failed` PR by commenting `/sync-prerelease` on it. A successful retry triggers the next one; the chain stops at the first PR that fails again.
40+
- The retry comment uses a fine-grained PAT token, which allows a workflow to trigger another using usual event (GITHUB_TOKEN does not allow that usually). This is configured in repo secrets.
3841

3942
## Reusable Composite Actions
4043

‎.github/workflows/port-to-prerelease.yml‎

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ on:
55
issue_comment:
66
types: [created]
77

8-
permissions:
9-
contents: write # so it can comment
10-
pull-requests: write # so it can create pull requests
11-
checks: write # so it can create a check run
8+
permissions: {} # each job opts in below
129

1310
jobs:
1411
sync-to-prerelease:
1512
name: Backport pull request
1613
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write # so it can comment
16+
pull-requests: write # so it can create, merge and label pull requests
17+
checks: write # so it can create a check run
1718
# Don't run on closed unmerged pull requests and only PRs to main,
1819
# skipping PRs labeled 'no-sync-prerelease' (e.g. release merges that
1920
# would otherwise try to cherry-pick the whole prerelease history back).
@@ -29,6 +30,8 @@ jobs:
2930
github.event.comment.user.id != 41898282 &&
3031
startsWith(github.event.comment.body, '/sync-prerelease')
3132
)
33+
outputs:
34+
merged: ${{ steps.merge.outputs.merged }}
3235
steps:
3336
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3437
with:
@@ -55,18 +58,92 @@ jobs:
5558
label_pattern: ''
5659
target_branches: prerelease
5760

58-
- name: Trigger deploy using a comment
61+
# Merging right away keeps prerelease up to date before the next PR to
62+
# main is cherry-picked; a sync PR left open makes the next backport
63+
# touching the same file conflict.
64+
- name: Merge the sync pull request
65+
id: merge
5966
if: steps.synced-pr.outputs.was_successful == 'true'
60-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
6167
env:
62-
CREATED_PULL_NUMBERS: ${{ steps.synced-pr.outputs.created_pull_numbers }}
63-
with:
68+
GH_TOKEN: ${{ github.token }}
69+
SYNC_PR: ${{ steps.synced-pr.outputs.created_pull_numbers }}
70+
run: |
71+
gh pr merge "$SYNC_PR" --repo "$GITHUB_REPOSITORY" --squash
72+
echo "merged=true" >> "$GITHUB_OUTPUT"
73+
74+
- name: Remove the backport-failed label
75+
if: steps.synced-pr.outputs.was_successful == 'true'
76+
env:
77+
GH_TOKEN: ${{ github.token }}
78+
SOURCE_PR: ${{ github.event.pull_request.number || github.event.issue.number }}
79+
run: |
80+
if gh pr view "$SOURCE_PR" --repo "$GITHUB_REPOSITORY" --json labels --jq '.labels[].name' | grep -qx backport-failed; then
81+
gh pr edit "$SOURCE_PR" --repo "$GITHUB_REPOSITORY" --remove-label backport-failed
82+
fi
83+
84+
# Also when the sync PR could not be merged, e.g. prerelease moved and
85+
# it now conflicts; it stays open for a manual merge.
86+
- name: Label the failed backport
87+
if: >
88+
!cancelled() && (
89+
steps.synced-pr.outputs.was_successful == 'false' ||
90+
steps.merge.outcome == 'failure'
91+
)
92+
env:
93+
GH_TOKEN: ${{ github.token }}
94+
SOURCE_PR: ${{ github.event.pull_request.number || github.event.issue.number }}
95+
run: gh pr edit "$SOURCE_PR" --repo "$GITHUB_REPOSITORY" --add-label backport-failed
96+
97+
# Merging with GITHUB_TOKEN does not trigger the push event that publishes
98+
# prerelease, so publish it explicitly. Publishing the branch tip rather than
99+
# the merge commit keeps the latest content when two syncs land together.
100+
publish-prerelease:
101+
needs: [ sync-to-prerelease ]
102+
# Publish even if a later step of the sync job failed after the merge.
103+
if: ${{ !cancelled() && needs.sync-to-prerelease.outputs.merged == 'true' }}
104+
uses: ./.github/workflows/publish.yml
105+
# A called workflow cannot exceed its caller's grant.
106+
permissions:
107+
contents: read
108+
statuses: write
109+
with:
110+
ref: prerelease
111+
prerelease: true
112+
secrets:
113+
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
114+
115+
# A backport that failed because an earlier change was missing from
116+
# prerelease can apply once that change lands. After a sync merge, or a
117+
# manual merge into prerelease, retry the oldest failed backport. Each
118+
# successful retry triggers the next one; the chain stops at the first
119+
# backport that fails again.
120+
retry-failed-backport:
121+
name: Retry the oldest failed backport
122+
needs: [ sync-to-prerelease ]
123+
runs-on: ubuntu-latest
124+
permissions:
125+
pull-requests: read # so it can list labeled pull requests
126+
if: >
127+
!cancelled() && (
128+
needs.sync-to-prerelease.outputs.merged == 'true' || (
129+
github.event_name == 'pull_request_target' &&
130+
github.event.pull_request.merged &&
131+
github.event.pull_request.base.ref == 'prerelease'
132+
)
133+
)
134+
steps:
135+
- name: Find the oldest PR labeled backport-failed
136+
id: oldest
137+
env:
138+
GH_TOKEN: ${{ github.token }}
139+
run: |
140+
pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --base main --state merged --label backport-failed --limit 1000 --json number,mergedAt --jq 'sort_by(.mergedAt) | .[0].number // empty')
141+
echo "pr=$pr" >> "$GITHUB_OUTPUT"
142+
143+
- name: Re-run its backport with a comment
144+
if: steps.oldest.outputs.pr != ''
145+
env:
64146
# special fine-grained token for issue comment trigger
65-
github-token: ${{ secrets.ISSUE_COMMENT_TRIGGER_PAT }}
66-
script: |
67-
await github.rest.issues.createComment({
68-
owner: context.repo.owner,
69-
repo: context.repo.repo,
70-
issue_number: process.env.CREATED_PULL_NUMBERS,
71-
body: "/deploy-preview",
72-
});
147+
GH_TOKEN: ${{ secrets.ISSUE_COMMENT_TRIGGER_PAT }}
148+
RETRY_PR: ${{ steps.oldest.outputs.pr }}
149+
run: gh api "repos/$GITHUB_REPOSITORY/issues/$RETRY_PR/comments" -f body="/sync-prerelease"

0 commit comments

Comments
 (0)