Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e3aa4fe
[backport] Replace source PR link with sentinel URL in changelog entries
mrodm Jul 15, 2026
3127352
[backport] Add changelog.UpdateEntryLinks to replace link fields by v…
mrodm Jul 15, 2026
af9b2aa
[backport] Fix changelog sentinel link after PR creation
mrodm Jul 15, 2026
523f228
[backport] Restore BuildComment checkboxes and update intro text
mrodm Jul 15, 2026
2e027ed
[backport] Add ParseChecklistItems and ChecklistItem
mrodm Jul 15, 2026
3c8f645
[backport] Add UpdateBranchStatus
mrodm Jul 15, 2026
8bc24ec
[backport] Add ParseBackportChecklist and UpdateChecklistBranchStatus…
mrodm Jul 15, 2026
02a1998
[backport] Add auto-backport.yml workflow
mrodm Jul 15, 2026
9d376f8
Add backport entry
mrodm Jul 14, 2026
ad8e1cf
Tests for debugging in fork
mrodm Jul 16, 2026
97f7cbd
[backport] Skip checkout early when issue_comment PR is not merged
mrodm Jul 16, 2026
9a4ebb7
[backport] Move resolve step before checkout to skip clone on early exit
mrodm Jul 16, 2026
f3dd5fd
Make backport_apply.sh executable
mrodm Jul 16, 2026
e5e9a65
[backport] Skip job early when edited comment is not the checklist
mrodm Jul 16, 2026
ce26b1c
[backport] Restore original branch after Apply() succeeds
mrodm Jul 16, 2026
c116d22
[backport] Redirect git stdout to stderr in Apply() to keep JSON outp…
mrodm Jul 16, 2026
88375f1
[backport] Log before PATCH and silence gh api response
mrodm Jul 16, 2026
98690cb
Update sync to avoid showing git output in stdout
mrodm Jul 16, 2026
260b002
[backport] Fix *string params passed as positional args to mage Apply…
mrodm Jul 16, 2026
65f609e
[backport] Recreate checklist comment instead of editing in place
mrodm Jul 17, 2026
a681dbd
Create step for git credentials
mrodm Jul 17, 2026
12cd41e
[backport] Retry GitHub API call to resolve backport PR number
mrodm Jul 17, 2026
193aacd
Add error messages in sync process
mrodm Jul 17, 2026
f1ccbd7
Update error message
mrodm Jul 17, 2026
8692372
[backport] Add Buildkite check for changelog versions already in main
mrodm Jul 17, 2026
f41dce8
Update if condition in workflow
mrodm Jul 17, 2026
9b3c252
Skip trigger integration tests - to be removed
mrodm Jul 17, 2026
12bdefe
Merge upstream/main into add_workflows_backport_prs
mrodm Jul 17, 2026
64eb91e
Revert "Add backport entry"
mrodm Jul 17, 2026
eb7cdc9
Ensure that os.ReadFile does not have errors in tests
mrodm Jul 21, 2026
d5489ce
Replaced exec.Command by run helper
mrodm Jul 21, 2026
fc1253a
Detect REPLACE_ME sentinel links as hard failures in changelog check
mrodm Jul 21, 2026
d4a54c3
Recover gracefully when a concurrent run already opened the backport PR
mrodm Jul 21, 2026
9974f1b
Extract process-checked-branches logic into a standalone script
mrodm Jul 22, 2026
3dc0c59
Fix working branch name derivation for non-numeric version suffixes
mrodm Jul 22, 2026
24b1355
Reorder early-exit checks: merged before actor permission
mrodm Jul 22, 2026
9428899
Remove debug branch references for add_workflows_backport_prs-test
mrodm Jul 22, 2026
01ff583
Use three-dot diff in changedChangelogs and gitDiff to scope to PR-on…
mrodm Jul 22, 2026
f3b5a1e
Add check_changelog_versions_in_main.sh to non_package_patterns.txt
mrodm Jul 27, 2026
47285b3
Add error message before sentinel exit in check_changelog_entries.sh
mrodm Jul 27, 2026
dbdb669
Revert skip triggering integration tests
mrodm Jul 28, 2026
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
16 changes: 15 additions & 1 deletion .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,24 @@ steps:
run-in-docker: false
context: junit-sources

- label: ":scroll: Check changelog versions not in main"
key: "check-changelog-versions-in-main"
command: ".buildkite/scripts/check_changelog_versions_in_main.sh"
agents:
image: "${LINUX_AGENT_IMAGE}"
cpu: "2"
memory: "1G"
if: |
build.env('BUILDKITE_PULL_REQUEST') != "false" &&
Comment thread
mallendem marked this conversation as resolved.
Resolved
build.env('BUILDKITE_PULL_REQUEST_BASE_BRANCH') =~ /^backport-/
if_changed:
- "**/changelog.yml"

- label: ":scroll: Check changelog PR links"
key: "check-changelog-pr-links"
command: ".buildkite/scripts/check_changelog_entries.sh"
soft_fail: true
soft_fail:
- exit_status: 2
agents:
image: "${LINUX_AGENT_IMAGE}"
cpu: "2"
Expand Down
14 changes: 13 additions & 1 deletion .buildkite/scripts/check_changelog_entries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ source "${SCRIPT_DIR}/common.sh"

CHANGELOG_SKIP_LABEL="${CHANGELOG_SKIP_LABEL:-"changelog-link-check:skip"}"

# Global counter for sentinel (REPLACE_ME) links — set inside check_changelog_file,
Comment thread
mallendem marked this conversation as resolved.
Resolved
# read in main to distinguish hard (exit 1) vs soft (exit 2) failures.
_sentinel_errors=0

# Extracts the GitHub org/repo path from a remote URL.
# Handles SSH (git@github.com:org/repo.git) and HTTPS formats.
github_repo_path() {
Expand Down Expand Up @@ -52,6 +56,10 @@ check_changelog_file() {
[[ -z "${link}" ]] && continue
if [[ "${link}" =~ /issues/[0-9]+$ ]]; then
echo "SKIP: '${link}' (issue link, not required to match PR)"
elif [[ "${link}" == *"REPLACE_ME"* ]]; then
echo "SENTINEL: '${link}' (placeholder not replaced — auto-fix may have failed)"
_sentinel_errors=$((_sentinel_errors + 1))
errors=$((errors + 1))
elif [[ "${link}" != "${expected_pr_link}" ]]; then
echo "ERROR: unexpected link: '${link}'"
echo " expected: '${expected_pr_link}'"
Expand Down Expand Up @@ -180,7 +188,11 @@ main() {
message+=$'\n\n'"[View Buildkite build](${BUILDKITE_BUILD_URL})"
notify_changelog_mismatch "${message}" "${BUILDKITE_PULL_REQUEST}"
fi
exit 1
if [[ "${_sentinel_errors}" -gt 0 ]]; then
Comment thread
mallendem marked this conversation as resolved.
Resolved
echo "FATAL: ${_sentinel_errors} changelog link(s) still contain the REPLACE_ME sentinel — the auto-fix step likely failed to push the link-fix commit."
exit 1
fi
exit 2
fi

echo ""
Expand Down
11 changes: 11 additions & 0 deletions .buildkite/scripts/check_changelog_versions_in_main.sh
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}"
2 changes: 2 additions & 0 deletions .buildkite/scripts/non_package_patterns.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
^\.buildkite/scripts/build_packages\.sh
^\.buildkite/scripts/check_backport_owners\.sh
^\.buildkite/scripts/check_changelog_entries\.sh
^\.buildkite/scripts/check_changelog_versions_in_main\.sh
^\.buildkite/scripts/packages/.+\.sh
^\.buildkite/scripts/requirements-ci-python-scripts\.txt
^\.buildkite/scripts/run_buildkite_scripts_tests\.sh
Expand All @@ -32,6 +33,7 @@
^\.buildkite/scripts/test_trigger_backport\.sh
^\.github/dependabot\.yml
^\.github/stale\.yml
^\.github/scripts/
Comment thread
mallendem marked this conversation as resolved.
Resolved
^\.github/workflows/
^\.github/CODEOWNERS
^\.github/ISSUE_TEMPLATE/
Expand Down
86 changes: 86 additions & 0 deletions .github/scripts/backport/process-checked-branches.sh
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")
Comment thread
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
176 changes: 176 additions & 0 deletions .github/workflows/auto-backport.yml
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

cancel-in-progress: true on the issue_comment concurrency group can
cancel a run mid-loop — for example after a working branch has been pushed
but before its PR was opened. The dedup check at the top of the branch loop
only looks for an existing PR:

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
run attempts backport_apply.sh again. That run fails when it tries to push
(branch already exists on remote with different history), surfaces as a
⚠️ conflict on the checklist, and requires manual intervention.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, and after reviewing it, the issue is wider than cancel-in-progress. A push event (PR merged) and an issue_comment event (checklist edited just before merge) land in different concurrency groups — auto-backport-<sha> vs auto-backport-<pr_number> — so they can run in parallel. Both pass the dedup check simultaneously (both see EXISTING=0), both cherry-pick and push, and the losing run gets a push rejection that was being reported as a ⚠️ conflict.

Fixed in d4a54c3: instead of immediately posting ⚠️ on any failure, the workflow now checks whether a PR was already opened for the working branch by a concurrent run. If that is true, it posts ✅ with that PR number. The genuine conflict path (real cherry-pick conflict, no concurrent PR) is unchanged.

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
14 changes: 5 additions & 9 deletions .github/workflows/post-backport-checklist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,9 @@ jobs:
printf '%s' "$NEW_BODY" > "$BODY_FILE"

if [[ -n "$COMMENT_ID" ]]; then
gh api --method PATCH \
"repos/$REPOSITORY/issues/comments/$COMMENT_ID" \
--field body=@"$BODY_FILE"
echo "Updated checklist comment #$COMMENT_ID on PR #$PR_NUMBER"
else
gh api --method POST \
"repos/$REPOSITORY/issues/$PR_NUMBER/comments" \
--field body=@"$BODY_FILE"
echo "Created checklist comment on PR #$PR_NUMBER"
gh api --method DELETE "repos/$REPOSITORY/issues/comments/$COMMENT_ID"
fi
gh api --method POST \
"repos/$REPOSITORY/issues/$PR_NUMBER/comments" \
--field body=@"$BODY_FILE"
echo "Recreated checklist comment on PR #$PR_NUMBER"
9 changes: 7 additions & 2 deletions .github/workflows/sync-backport-changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ jobs:
if: steps.guard.outputs.proceed == 'true'
run: go install github.com/magefile/mage

- name: Configure git
if: steps.guard.outputs.proceed == 'true'
uses: elastic/oblt-actions/git/setup@v1
with:
username: 'github-actions[bot]'
email: '41898282+github-actions[bot]@users.noreply.github.com'

- name: Sync backport changelog
id: sync
if: steps.guard.outputs.proceed == 'true'
Expand All @@ -57,8 +64,6 @@ jobs:
BACKPORT_BRANCH: ${{ github.ref_name }}
PACKAGES_DIR: ${{ github.workspace }}/packages
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
mage -d "$GITHUB_WORKSPACE" syncBackportChangelog

- name: Comment on backport PR
Expand Down
Loading
Loading