Skip to content

[CI][Backport] Automatic backport checklist on PRs targeting main#19997

Open
mrodm wants to merge 22 commits into
elastic:mainfrom
mrodm:backport-checklist-automation
Open

[CI][Backport] Automatic backport checklist on PRs targeting main#19997
mrodm wants to merge 22 commits into
elastic:mainfrom
mrodm:backport-checklist-automation

Conversation

@mrodm

@mrodm mrodm commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Proposed commit message

Implement automatic backport checklist for PRs targeting main (#19213).

When a PR is opened or updated, a workflow detects which packages it touches and posts
a checklist comment listing the active backport branches for those packages. Authors tick
the boxes for the branches they want to backport to. The checklist is updated automatically
on each new push, preserving previously checked boxes.

Architecture — two-workflow fork-safety pattern:

  • backport-packages-detect.yml — triggered by pull_request (no secrets, fork-safe).
    Runs mage detectBackportPackages, uploads { pr_number, packages } as a workflow artifact.
  • post-backport-checklist.yml — triggered by workflow_run on the above. Has
    pull-requests: write. Downloads the artifact, renders the checklist via
    mage renderBackportChecklist, and posts or updates the comment via gh api.

New Go packages:

  • dev/backports/packages — pure DetectPackages function: maps git-diff file paths to
    package names by prefix-matching against citools.ListPackagesWithNames output. Package
    name is read from manifest.yml, not inferred from the directory name.
  • dev/backports/checklist — pure BuildComment / ParseCheckedBranches functions:
    renders the full comment body and parses previously-checked branch names, preserving
    checkbox state across re-renders.

New mage targets:

  • detectBackportPackages — runs git diff --name-only and maps files to packages; emits
    JSON when -asJSON is passed.
  • renderBackportChecklist — reads the artifact via $ARTIFACT env var, parses existing
    checked branches from stdin, and prints the new comment body (empty = no active branches).

backport:sync-changelog label guard:
PRs created by the sync-changelog workflow (#19215) carry this label; the detect job is
skipped via a job-level if: so they do not receive a checklist of their own. The label
rename from automation to backport:sync-changelog is handled in a separate PR.

Notable behaviour:

  • Duplicate checklist comments: posts a warning to the PR and exits 1 so maintainers can
    clean up manually.
  • MaintainedUntil parse error: fails open (branch stays active) — ValidateInventory
    enforces the date format so this path is only reachable via an unvalidated direct edit.
  • No checklist posted when no touched package has any active backport branch.
  • Checked boxes are preserved on each re-render; [X] (uppercase) is treated identically
    to [x].

Author's Checklist

  • Remove backport-checklist-automation from the branches filter in
    backport-packages-detect.yml (line 9) — kept during development for local testing.
  • Revert or clean up temporary testing commits (8e6c280916, 7810771711) before
    opening the PR.
  • Validate that checkboxes ticket are kept in each update.
  • Validate that checkboxes related to packages that are not part of the Pull Request changes are removed too.
  • Validate that packages with non-active branches are ignored.

How to test this PR locally

Tip: This feature is best tested from a fork. The two-workflow fork-safety pattern
behaves differently for same-repo PRs (where the pull_request context has write access)
vs. fork PRs (where it is restricted). Testing from a fork validates the full intended flow.

  1. Open a PR targeting main that touches at least one package under packages/ with an
    active backport branch in .backports.yml.
  2. The Backport — detect packages workflow runs automatically. Check its artifact contains
    { "pr_number": N, "packages": [...] }.
  3. The Backport — post checklist workflow runs next. Verify the checklist comment appears
    on the PR.
  4. Tick a checkbox, push a new commit — verify the comment updates and the ticked box is
    preserved.
  5. Open a PR with the backport:sync-changelog label — verify no checklist is posted.

To run unit tests locally:

go test ./dev/backports/... ./dev/backports/packages/... ./dev/backports/checklist/...

Related issues

Related Pull Requests testing this feature:

Screenshots

Example of comment:
image


This PR was generated with the assistance of Claude (claude-sonnet-4-6).

mrodm and others added 19 commits June 30, 2026 16:12
Adds ListActiveBackportBranches(path, packageName string, now time.Time) to
dev/backports/inventory.go. Returns all active backport branches for a given
package in inventory order, filtering out archived entries and entries whose
maintained_until date has passed. Returns an empty slice (no error) when the
package is not found.

Part of elastic#19213.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces dev/backports/packages/detect.go with DetectPackages(files []string,
packagesDir string) ([]string, error). Uses citools.ListPackages to discover all
known package roots upfront, then maps each changed file path to its package by
prefix match. Handles both flat (packages/<name>/) and nested
(packages/<technology>/<name>/) structures without custom tree-walking.
Returns a deduplicated list of package names in encounter order.

Includes unit tests covering flat, nested, deduplication, name-from-manifest,
non-package files, empty input, and error cases.

Part of elastic#19213.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mage targets

Adds two mage targets to magefile.go:

- DetectBackportPackages(before, after string, asJSON *bool): runs
  git diff --name-only before..after and maps changed files to package
  names via bppackages.DetectPackages. Outputs one name per line or a
  JSON array with -json.

- ListActiveBackportBranches(packageName string, asJSON *bool): returns
  all active branches for a package from .backports.yml. Outputs one
  branch name per line or a JSON array of full entries with -json.

Both targets are used by the backport checklist GHA workflows
introduced in elastic#19213.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds .github/workflows/backport-packages-detect.yml, the fork-safe
read-only half of the backport checklist two-workflow pair.

Triggers on pull_request (opened/synchronize) targeting main with no
write permissions. Checks out with full history (fetch-depth: 0) so
that git diff can resolve both base and head SHAs, installs mage, and
calls mage detectBackportPackages to map changed files to package names.
Uploads the result as a short-lived artifact (1 day retention):
  {"pr_number": 1234, "packages": ["aws", "kubernetes"]}

Event context values are passed through env: to avoid script injection.
The write-permission half (post-backport-checklist.yml) consumes this
artifact via workflow_run and posts the checklist comment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds .github/workflows/post-backport-checklist.yml: triggers via
workflow_run when backport-packages-detect succeeds, downloads the
pr-packages artifact from the detecting run, finds/creates/updates a
PR comment containing the backport checklist (<!-- backport-checklist
--> marker, branch lines per package, preserved checkbox states).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract rendering and checkbox-state-preservation logic from the
post-backport-checklist workflow into a new, unit-tested Go package
(dev/backports/checklist), following the existing pattern set by
dev/backports/changelog/comment.go.

- BuildComment renders the full markdown checklist body (marker,
  per-package branch lines, maintained_until dates, tip footer);
  returns "" when no package has active branches.
- ParseCheckedBranches extracts previously-ticked branch names from
  an existing comment body so their state is preserved on update.
- RenderBackportChecklist mage target wires the two together: reads
  ARTIFACT env (path to pr-packages.json), existing body from stdin,
  and prints the rendered body.

The workflow step now calls mage renderBackportChecklist instead of
duplicating the rendering logic in bash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tic#19213)

The mage target was superseded by renderBackportChecklist, which calls
backports.ListActiveBackportBranches internally. No workflow or script
calls the standalone target, so the wrapper is removed. The underlying
Go function and its tests are kept — they are still used by
renderBackportChecklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marker and hasActiveBranches are only used within the checklist package
itself — no external caller references them — so they are unexported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-slice return (elastic#19213)

RenderBackportChecklist called ListActiveBackportBranches once per
package, reading and parsing .backports.yml N times per invocation.
Replace with ListAllActiveBackportBranches, which parses the inventory
exactly once and returns results for all requested packages in a single
call. ListActiveBackportBranches is removed as it had no remaining
callers.

Initialize DetectPackages result as make([]string, 0) instead of a var
declaration so callers receive a non-nil empty slice, allowing the
nil-to-empty guard in DetectBackportPackages to be dropped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…astic#19213)

Add citools.PackageInfo and ListPackagesWithNames, which return path and
manifest name together from a single directory walk. Refactor ListPackages
to delegate to it so the walk logic is not duplicated.

Update DetectPackages, BuildPackageIndex, and buildKnownPackages to use
ListPackagesWithNames. Each manifest is now read exactly once per
invocation — previously ListPackages read every manifest for IsValid()
and each caller re-read them all again to obtain the package name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lastic#19213)

Add trap 'rm -f "$BODY_FILE"' EXIT in post-backport-checklist.yml so
the temp file is always removed on any exit path, not just the happy
path. Remove the now-redundant explicit rm -f at the end of the script.

Replace the single-use hasActiveBranches helper in checklist.go with an
inline slices.ContainsFunc call, removing an unnecessary indirection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extract loadInventory helper to consolidate ReadFile+Unmarshal
  duplicated across ListAllActiveBackportBranches, CheckActive, and
  ValidateInventory
- document fail-open behaviour for malformed maintained_until dates
- pre-append filepath.Separator to package paths before the inner loop
  to avoid O(N×M) string allocations in DetectPackages
- fix stale doc comment in DetectPackages (ListPackages → ListPackagesWithNames)
- handle json.Marshal error in DetectBackportPackages instead of
  discarding with _
- detect duplicate checklist comments in post-backport-checklist workflow
  and post a warning to the PR instead of silently picking one

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- post-backport-checklist: add continue-on-error to download-artifact so
  a skipped detect job (backport:sync-changelog label) does not fail the
  post workflow before the missing-artifact guard is reached
- checklist: match [X] uppercase checkbox in ParseCheckedBranches regex;
  add test case for uppercase state preservation
- magefile: correct doc comment flag name -json → -asJSON
- inventory: add missing doc comment to loadInventory
- detect: document why make([]string, 0) is used over var declaration;
  assert non-nil in empty-result test to catch future regressions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mrodm mrodm self-assigned this Jul 6, 2026
@mrodm mrodm changed the title [Enhancement] Automatic backport checklist on PRs targeting main [CI][Backport] Automatic backport checklist on PRs targeting main Jul 6, 2026
fmt.Fprintln(&b)
fmt.Fprintln(&b, "Only branches for packages touched by this PR's current diff are shown.")
fmt.Fprintln(&b)
fmt.Fprintln(&b, "Check the branches to backport this change to.")

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.

This message could be updated as part of #19214 . Right now, this message is just informative and therefore no pull requests are going to be created.

As part of #19214, as pull requests are going to be created, tihs message could be updated too to something like:

PRs will be created automatically on merge, or when you update this checklist after merge.

Comment thread .buildkite/pipeline.yml
Comment on lines +168 to +169
if: |
build.branch == "nonexisting"

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.

To be reverted before merging

types: [opened, synchronize, reopened]
branches:
- main
- backport-checklist-automation

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.

To be reverted before merging

@mrodm mrodm marked this pull request as ready for review July 6, 2026 16:33
@mrodm mrodm requested a review from a team as a code owner July 6, 2026 16:33
@mrodm mrodm requested a review from v1v July 6, 2026 16:33

@mrodm mrodm Jul 6, 2026

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.

@v1v could you help us to review the permissions of this workflow (and port-backport-checklist too) and the events used if they make sense to avoid any security concerns?

TIA!! 😊

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
if checked[r.Branch] {
state = "x"
}
line := fmt.Sprintf("- [%s] `%s`", state, r.Branch)

@mrodm mrodm Jul 6, 2026

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.

Do you prefer that the comment just post a list of branches without the checkbox ? Thinking that, since it does not create Pull Requests yet the automation (to be done in #19214). WDYT ?

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

cc @mrodm

@v1v v1v requested a review from a team July 6, 2026 17:38
@mergify

mergify Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

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.

backport: post a backport checklist automatically on pull requests targeting main

2 participants