Repository navigation
[backport] Auto-create backport PRs, fix changelog links, and sync changelogs to main #20197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e3aa4fe
3127352
af9b2aa
523f228
2e027ed
3c8f645
8bc24ec
02a1998
9d376f8
ad8e1cf
97f7cbd
9a4ebb7
f3dd5fd
e5e9a65
ce26b1c
c116d22
88375f1
98690cb
260b002
65f609e
a681dbd
12cd41e
193aacd
f1ccbd7
8692372
f41dce8
9b3c252
12bdefe
64eb91e
eb7cdc9
d5489ce
fc1253a
d4a54c3
9974f1b
3dc0c59
24b1355
9428899
01ff583
f3b5a1e
47285b3
dbdb669
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| #!/bin/bash | ||
|
|
||
| source .buildkite/scripts/common.sh | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| add_bin_path | ||
| with_mage | ||
|
|
||
| echo "--- Check changelog versions are not already in main" | ||
| mage -d "${WORKSPACE}" -v checkChangelogVersionsInMain "${BUILDKITE_PULL_REQUEST_BASE_BRANCH}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| #!/usr/bin/env bash | ||
| # Process checked branches from a backport checklist comment. | ||
| # | ||
| # For each checklist item that is checked but not yet processed, this script: | ||
| # 1. Skips branches that already have an open or merged PR (dedup guard). | ||
| # 2. Runs backport_apply.sh to cherry-pick the merge commit and open a PR. | ||
| # 3. Recovers gracefully when a concurrent run (e.g. a simultaneous push | ||
| # event) already opened the PR — posts ✅ instead of a false ⚠️. | ||
| # 4. Updates the checklist comment with the outcome (✅ #PR or ⚠️ conflict). | ||
| # | ||
| # Required environment variables (set by the workflow step's env: block): | ||
| # GH_TOKEN GitHub token with contents:write and pull-requests:write. | ||
| # REPOSITORY GitHub repository in org/repo form. | ||
| # MERGE_SHA Full SHA of the merge commit to cherry-pick. | ||
| # PR_AUTHOR Login of the PR author (used in conflict messages). | ||
| # COMMENT_ID ID of the checklist comment to patch. | ||
| # BODY_FILE Path to a file containing the current checklist comment body. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| ITEMS=$(mage -d "$GITHUB_WORKSPACE" parseBackportChecklist "$BODY_FILE") | ||
|
mallendem marked this conversation as resolved.
Resolved
|
||
| SHORT_SHA="${MERGE_SHA:0:8}" | ||
|
|
||
| echo "$ITEMS" | jq -c '.[]' | while IFS= read -r item; do | ||
| PKG=$(jq -r '.Package' <<< "$item") | ||
| BRANCH=$(jq -r '.Branch' <<< "$item") | ||
| CHECKED=$(jq -r '.Checked' <<< "$item") | ||
| PROCESSED=$(jq -r '.Processed' <<< "$item") | ||
|
|
||
| if [[ "$CHECKED" != "true" || "$PROCESSED" != "false" ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| # Derive the version suffix from the branch name, mirroring workingBranchName in apply.go: | ||
| # strings.TrimPrefix(branchName, "backport-"+pkg+"-") | ||
| # e.g. backport-aws-6.14 → "6.14", backport-kubernetes-1.x → "1.x" | ||
| VERSION_SUFFIX="${BRANCH#backport-${PKG}-}" | ||
|
|
||
| # Dedup: skip if a working branch for this exact (package, version, sha) already has a PR | ||
| WORKING_BRANCH="auto-backport/${PKG}-${VERSION_SUFFIX}-${SHORT_SHA}" | ||
| EXISTING=$(gh pr list --repo "$REPOSITORY" --head "$WORKING_BRANCH" --state all --json number --jq 'length') | ||
| if [[ "${EXISTING:-0}" -gt 0 ]]; then | ||
| echo "PR already exists for $WORKING_BRANCH — skipping" | ||
| continue | ||
| fi | ||
|
|
||
| # Run the backport; --json ensures structured output even on conflict | ||
| RESULT=$("$GITHUB_WORKSPACE/dev/scripts/backport_apply.sh" \ | ||
| --sha "$MERGE_SHA" \ | ||
| --package "$PKG" \ | ||
| --target "$BRANCH" \ | ||
| --open-pr \ | ||
| --json \ | ||
| --repository "$REPOSITORY" || true) | ||
|
|
||
| STATUS_VAL=$(jq -r '.status // empty' <<< "$RESULT" 2>/dev/null || true) | ||
| if [[ "$STATUS_VAL" == "success" ]]; then | ||
| PR_URL=$(jq -r '.pr_url' <<< "$RESULT") | ||
| PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$') | ||
| STATUS="✅ #${PR_NUM}" | ||
| else | ||
| # Before reporting a conflict, check if a concurrent run (e.g. a push | ||
| # event racing with this issue_comment event) already opened a PR for | ||
| # this working branch. A push rejection is not a real cherry-pick conflict. | ||
| CONCURRENT_PR=$(gh pr list --repo "$REPOSITORY" --head "$WORKING_BRANCH" \ | ||
| --state all --json number --jq 'first.number // empty') | ||
| if [[ -n "$CONCURRENT_PR" ]]; then | ||
| STATUS="✅ #${CONCURRENT_PR}" | ||
| else | ||
| SUGGESTED=$(jq -r '.suggested_command // empty' <<< "$RESULT" 2>/dev/null || true) | ||
| STATUS="⚠️ conflict: @${PR_AUTHOR} please resolve manually${SUGGESTED:+: \`${SUGGESTED}\`}" | ||
| fi | ||
| fi | ||
|
|
||
| # Update the in-memory checklist body and patch the GitHub comment | ||
| NEW_BODY=$(mage -d "$GITHUB_WORKSPACE" updateChecklistBranchStatus "$BODY_FILE" "$BRANCH" "$STATUS") | ||
| printf '%s' "$NEW_BODY" > "$BODY_FILE" | ||
|
|
||
| echo "Updating checklist comment for $BRANCH → $STATUS" | ||
| gh api --method PATCH \ | ||
| "repos/$REPOSITORY/issues/comments/$COMMENT_ID" \ | ||
| --field body=@"$BODY_FILE" \ | ||
| --silent | ||
|
|
||
| echo "Processed $BRANCH → $STATUS" | ||
| done | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| --- | ||
| name: Backport — auto create PRs | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| issue_comment: | ||
| types: [edited] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| auto-backport: | ||
| runs-on: ubuntu-latest | ||
| # Skip bot-authored edits to break the infinite self-trigger loop: | ||
| # patching the checklist comment (✅ / ⚠️ suffix) fires another | ||
| # issue_comment edited event — filtering on actor stops it here. | ||
| # For issue_comment events also require: | ||
| # - the event is on a pull request (not a plain issue) | ||
| # - the edited comment contains the checklist marker (zero-cost body check, | ||
| # no API call needed — the full comment body is in the event payload) | ||
| if: >- | ||
| ${{ | ||
| github.actor != 'github-actions[bot]' && | ||
| ( | ||
| github.event_name == 'push' || | ||
| ( | ||
| github.event.issue.pull_request != null && | ||
| contains(github.event.comment.body, '<!-- backport-checklist -->') | ||
| ) | ||
| ) | ||
| }} | ||
| concurrency: | ||
| # Serialise runs per PR: | ||
| # - push to main: keyed on merge SHA (one run per merge) | ||
| # - issue_comment edited: keyed on PR number (serialises re-checklist runs) | ||
| group: auto-backport-${{ github.event_name == 'push' && github.sha || github.event.issue.number }} | ||
| cancel-in-progress: true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. claude flagged this as a potential issue and i think is worth to check this out
EXISTING=$(gh pr list --repo "$REPOSITORY" --head "$WORKING_BRANCH" --state all --json number --jq 'length')A branch-pushed-no-PR state passes this check (EXISTING=0), so the next
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, and after reviewing it, the issue is wider than Fixed in d4a54c3: instead of immediately posting |
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| steps: | ||
| - name: Check PR is merged | ||
| id: pr-check | ||
| if: github.event_name == 'issue_comment' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| ISSUE_NUMBER: ${{ github.event.issue.number }} | ||
| run: | | ||
| STATE=$(gh pr view "$ISSUE_NUMBER" --repo "$REPOSITORY" --json state --jq '.state') | ||
| if [[ "$STATE" == "MERGED" ]]; then | ||
| echo "merged=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "PR #$ISSUE_NUMBER is not merged (state: $STATE) — skipping" | ||
| fi | ||
|
|
||
| - name: Check actor permission | ||
| id: actor-check | ||
| if: github.event_name == 'issue_comment' && steps.pr-check.outputs.merged == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| ACTOR: ${{ github.actor }} | ||
| run: | | ||
| PERMISSION=$(gh api "repos/$REPOSITORY/collaborators/$ACTOR/permission" \ | ||
| --jq '.permission' 2>/dev/null || echo "none") | ||
| if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" ]]; then | ||
| echo "permitted=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Actor $ACTOR lacks write/admin permission ($PERMISSION) — skipping" | ||
| fi | ||
|
|
||
| - name: Resolve merge SHA and PR metadata | ||
| id: resolve | ||
| if: >- | ||
| ${{ | ||
| github.event_name == 'push' || | ||
| steps.actor-check.outputs.permitted == 'true' | ||
| }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| PUSH_SHA: ${{ github.sha }} | ||
| ISSUE_NUMBER: ${{ github.event.issue.number }} | ||
| run: | | ||
| if [[ "$EVENT_NAME" == "push" ]]; then | ||
| MERGE_SHA="$PUSH_SHA" | ||
| PR_DATA=$(gh api "repos/$REPOSITORY/commits/$MERGE_SHA/pulls" \ | ||
| --jq 'map(select(.base.ref == "main")) | first // empty') | ||
| if [[ -z "$PR_DATA" ]]; then | ||
| echo "No PR found for commit $MERGE_SHA — skipping" | ||
| exit 0 | ||
| fi | ||
| PR_NUMBER=$(jq -r '.number' <<< "$PR_DATA") | ||
| LABELS=$(jq -r '[.labels[].name] | join(",")' <<< "$PR_DATA") | ||
| PR_AUTHOR=$(jq -r '.user.login' <<< "$PR_DATA") | ||
| else | ||
| PR_NUMBER="$ISSUE_NUMBER" | ||
| PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPOSITORY" \ | ||
| --json mergeCommit,labels,author) | ||
| MERGE_SHA=$(jq -r '.mergeCommit.oid' <<< "$PR_DATA") | ||
| LABELS=$(jq -r '[.labels[].name] | join(",")' <<< "$PR_DATA") | ||
| PR_AUTHOR=$(jq -r '.author.login' <<< "$PR_DATA") | ||
| fi | ||
|
|
||
| if [[ "$LABELS" == *"backport:sync-changelog"* ]]; then | ||
| echo "PR carries backport:sync-changelog label — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "merge_sha=$MERGE_SHA" >> "$GITHUB_OUTPUT" | ||
| echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | ||
| echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - uses: actions/checkout@v7 | ||
| if: steps.resolve.outputs.pr_number != '' | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up Go | ||
| if: steps.resolve.outputs.pr_number != '' | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: .go-version | ||
|
|
||
| - name: Install mage | ||
| if: steps.resolve.outputs.pr_number != '' | ||
| run: go install github.com/magefile/mage | ||
|
|
||
| - name: Find checklist comment | ||
| id: find-comment | ||
| if: steps.resolve.outputs.pr_number != '' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| PR_NUMBER: ${{ steps.resolve.outputs.pr_number }} | ||
| run: | | ||
| MATCHING=$(gh api "repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ | ||
| --paginate \ | ||
| --jq '.[] | select(.body | contains("<!-- backport-checklist -->"))' \ | ||
| | jq -s '.') | ||
|
|
||
| COUNT=$(jq 'length' <<< "$MATCHING") | ||
| if [[ "$COUNT" -eq 0 ]]; then | ||
| echo "No checklist comment found on PR #$PR_NUMBER — skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| BODY_FILE="$RUNNER_TEMP/checklist-body.txt" | ||
| jq -r '.[0].body' <<< "$MATCHING" > "$BODY_FILE" | ||
|
|
||
| echo "comment_id=$(jq -r '.[0].id' <<< "$MATCHING")" >> "$GITHUB_OUTPUT" | ||
| echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Configure git | ||
| if: steps.find-comment.outputs.comment_id != '' | ||
| uses: elastic/oblt-actions/git/setup@v1 | ||
| with: | ||
| username: 'github-actions[bot]' | ||
| email: '41898282+github-actions[bot]@users.noreply.github.com' | ||
|
|
||
| - name: Process checked branches | ||
| if: steps.find-comment.outputs.comment_id != '' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| MERGE_SHA: ${{ steps.resolve.outputs.merge_sha }} | ||
| PR_AUTHOR: ${{ steps.resolve.outputs.pr_author }} | ||
| COMMENT_ID: ${{ steps.find-comment.outputs.comment_id }} | ||
| BODY_FILE: ${{ steps.find-comment.outputs.body_file }} | ||
| run: .github/scripts/backport/process-checked-branches.sh | ||
Uh oh!
There was an error while loading. Please reload this page.