Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1daf66f
[backport] add ListActiveBackportBranches to inventory
mrodm Jun 30, 2026
55ea883
[backport] add DetectPackages to dev/backports/packages
mrodm Jun 30, 2026
72f85d8
[backport] add DetectBackportPackages and ListActiveBackportBranches …
mrodm Jun 30, 2026
0b0afb3
[backport] add backport-packages-detect workflow (#19213)
mrodm Jun 30, 2026
6932972
[backport] add post-backport-checklist workflow (#19213)
mrodm Jun 30, 2026
74dad4e
[backport] make checklist comment logic testable (#19213)
mrodm Jun 30, 2026
2fb2348
[backport] remove unused listActiveBackportBranches mage target (#19213)
mrodm Jun 30, 2026
f6e76d5
[backport] unexport internal-only checklist symbols (#19213)
mrodm Jun 30, 2026
81774d4
[backport] parse .backports.yml once per checklist render and drop ni…
mrodm Jul 1, 2026
1dfe680
[backport] eliminate double manifest reads in package enumeration (#1…
mrodm Jul 1, 2026
1e223d4
[backport] guarantee temp file cleanup and inline single-use helper (…
mrodm Jul 1, 2026
bb65ba1
[backport] address high-effort code review findings (#19213)
mrodm Jul 1, 2026
678fa3c
[backport] split post-checklist step into two (#19213)
mrodm Jul 1, 2026
816d049
[backport] skip detect job for sync-changelog PRs (#19213)
mrodm Jul 2, 2026
8e6c280
Add temporarily other target branch
mrodm Jul 2, 2026
552e752
[backport] join split lines in BuildComment (#19213)
mrodm Jul 2, 2026
75dadd7
fix second code-review findings in backport checklist automation
mrodm Jul 3, 2026
6abd326
remove unimplemented automation promise from checklist comment
mrodm Jul 3, 2026
7810771
Avoid triggering all packages - to be reverted
mrodm Jul 3, 2026
94a1b7d
Add reopened event to the backport-packages-detect workflow
mrodm Jul 6, 2026
ac73375
Merge remote-tracking branch 'upstream/main' into backport-checklist-…
mrodm Jul 6, 2026
adcd05b
use mage parameter instead of env var in renderBackportChecklist
mrodm Jul 6, 2026
79ec40c
add job-level concurrency to post-checklist workflow
mrodm Jul 8, 2026
9bb7bec
render branches as plain list until auto-backport PR creation lands (…
mrodm Jul 8, 2026
366f20b
add contents: read to job-level permissions in post-checklist workflow
mrodm Jul 8, 2026
ae8e854
Remove changes to debug workflows
mrodm Jul 8, 2026
95edbd5
Merge remote-tracking branch 'upstream/main' into backport-checklist-…
mrodm Jul 8, 2026
49bb7a6
note in checklist comment that manual edits will be overwritten on push
mrodm Jul 8, 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
59 changes: 59 additions & 0 deletions .github/workflows/backport-packages-detect.yml

@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 post-backport-checklist too) and the events used if they make sense to avoid any security concerns?

TIA!! 😊

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: Backport — detect packages

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main

jobs:
detect-packages:
runs-on: ubuntu-latest
# Skip PRs created by the sync-changelog workflow — those backport changelog
# entries back to main and do not need a backport checklist of their own.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'backport:sync-changelog') }}
# No permissions — this job runs in the restricted pull_request context
# and must be fork-safe. Write permissions live in post-backport-checklist.yml
# which is triggered via workflow_run and runs in the base repository context.
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Full history is required so that git diff can resolve both the PR
# base SHA and the head SHA, which may not be present in a shallow clone.
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: .go-version

- name: Install mage
run: go install github.com/magefile/mage
Comment thread
fr4nc1sc0-r4m0n marked this conversation as resolved.
Resolved

- name: Detect packages touched by this PR
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
PACKAGES_JSON=$(mage -d "$GITHUB_WORKSPACE" detectBackportPackages "$BASE_SHA" "$HEAD_SHA" -asJSON)

jq -n \
--argjson pkgs "$PACKAGES_JSON" \
--argjson pr "$PR_NUMBER" \
'{"pr_number": $pr, "packages": $pkgs}' > pr-packages.json

echo "Detected packages: $PACKAGES_JSON"

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: pr-packages-${{ github.run_id }}
path: pr-packages.json
if-no-files-found: ignore
retention-days: 1
124 changes: 124 additions & 0 deletions .github/workflows/post-backport-checklist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
Comment thread
fr4nc1sc0-r4m0n marked this conversation as resolved.
Resolved
name: Backport — post checklist

on:
workflow_run:
workflows: ["Backport — detect packages"]
types: [completed]

Comment thread
fr4nc1sc0-r4m0n marked this conversation as resolved.
Resolved
permissions:
contents: read

jobs:
post-checklist:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
# Cancel any in-progress run for the same branch when rapid pushes trigger
# overlapping workflow_run jobs — the newest run wins.
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
Comment thread
mrodm marked this conversation as resolved.
Resolved
# Required by actions/download-artifact when fetching from another workflow run.
actions: read

steps:
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: .go-version

- name: Install mage
run: go install github.com/magefile/mage

- name: Download artifact
# continue-on-error so a missing artifact (e.g. detect job was skipped due to
# backport:sync-changelog label) does not fail the job — the next step checks
# for the file and exits cleanly when it is absent.
continue-on-error: true
uses: actions/download-artifact@v7
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
name: pr-packages-${{ github.event.workflow_run.id }}
path: artifacts

- name: Validate artifact and find checklist comment
id: find-comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
ARTIFACT: artifacts/pr-packages.json
run: |
Comment thread
fr4nc1sc0-r4m0n marked this conversation as resolved.
Resolved
if [[ ! -f "$ARTIFACT" ]]; then
echo "Artifact not found — exiting"
exit 0
fi

PR_NUMBER=$(jq -r '.pr_number' "$ARTIFACT")
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"

# Collect all comments that contain the checklist marker across all pages.
MATCHING=$(gh api "repos/$REPOSITORY/issues/$PR_NUMBER/comments" \
--paginate \
--jq '.[] | select(.body | contains("<!-- backport-checklist -->"))' \
| jq -s '.')

COUNT=$(jq 'length' <<< "$MATCHING")
if [[ "$COUNT" -gt 1 ]]; then
IDS=$(jq -r '.[].id' <<< "$MATCHING" | paste -sd ', ')
gh api --method POST \
"repos/$REPOSITORY/issues/$PR_NUMBER/comments" \
--field body=":warning: **Backport checklist error**: found ${COUNT} duplicate checklist comments (IDs: ${IDS}). Please delete the extras manually so the checklist can be updated automatically."
echo "Duplicate checklist comments found — posted notice to PR #$PR_NUMBER" >&2
exit 1
fi

echo "comment_id=$(jq -r 'first.id // empty' <<< "$MATCHING")" >> "$GITHUB_OUTPUT"

- name: Render and post checklist comment
if: steps.find-comment.outputs.pr_number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
ARTIFACT: artifacts/pr-packages.json
PR_NUMBER: ${{ steps.find-comment.outputs.pr_number }}
COMMENT_ID: ${{ steps.find-comment.outputs.comment_id }}
run: |
EXISTING_BODY=""
Comment thread
fr4nc1sc0-r4m0n marked this conversation as resolved.
Resolved
if [[ -n "$COMMENT_ID" ]]; then
EXISTING_BODY=$(gh api "repos/$REPOSITORY/issues/comments/$COMMENT_ID" \
--jq '.body // empty')
fi

# Render the new comment body. The existing body is piped into stdin so
# the mage target can parse previously-checked branches and preserve their
# state. Prints nothing when no package has any active branch.
NEW_BODY=$(printf '%s' "$EXISTING_BODY" | \
mage -d "$GITHUB_WORKSPACE" renderBackportChecklist "$ARTIFACT")

if [[ -z "$NEW_BODY" ]]; then
echo "No active backport branches — nothing to post"
exit 0
fi

# Write to a file to avoid quoting issues when passing to gh api.
BODY_FILE=$(mktemp)
trap 'rm -f "$BODY_FILE"' EXIT
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"
fi
13 changes: 4 additions & 9 deletions dev/backports/changelog/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@ package changelog

import (
"fmt"
"path/filepath"

"github.com/elastic/integrations/dev/citools"
)

// BuildPackageIndex returns a map of package name → directory for every
// package found under packagesDir.
func BuildPackageIndex(packagesDir string) (map[string]string, error) {
paths, err := citools.ListPackages(packagesDir)
pkgs, err := citools.ListPackagesWithNames(packagesDir)
if err != nil {
return nil, fmt.Errorf("listing packages under %s: %w", packagesDir, err)
}

index := make(map[string]string, len(paths))
for _, p := range paths {
manifest, err := citools.ReadPackageManifest(filepath.Join(p, citools.ManifestFileName))
if err != nil {
return nil, fmt.Errorf("reading manifest at %s: %w", p, err)
}
index[manifest.Name] = p
index := make(map[string]string, len(pkgs))
for _, p := range pkgs {
index[p.Name] = p.Path
}

return index, nil
Expand Down
100 changes: 100 additions & 0 deletions dev/backports/checklist/checklist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

// Package checklist provides pure functions for building and parsing the
// backport-checklist comment posted on pull requests targeting main.
// No I/O is performed here — callers supply any dynamic data (active branches,
// existing comment body) and receive back a rendered string.
package checklist

import (
"fmt"
"regexp"
"slices"
"strings"

"github.com/elastic/integrations/dev/backports"
)

// marker is the HTML comment embedded at the start of every checklist comment.
// It is used to find and update an existing checklist rather than posting a new one.
const marker = "<!-- backport-checklist -->"

// PackageBranches pairs a package name with its active backport branches.
// Branches are in inventory order.
type PackageBranches struct {
Package string
Branches []backports.ActiveResult
}

// checkedLineRe matches a checked checkbox line: "- [x] `branch-name`..."
var checkedLineRe = regexp.MustCompile("^- \\[[xX]\\] `([^`]+)`")

// ParseCheckedBranches scans body for "- [x] `<branch>`" lines and returns the
// set of branch names that are currently ticked. An empty or marker-free body
// returns an empty (non-nil) map so callers never need a nil check.
func ParseCheckedBranches(body string) map[string]bool {
checked := make(map[string]bool)
if !strings.Contains(body, marker) {
return checked
}
for line := range strings.SplitSeq(body, "\n") {
if m := checkedLineRe.FindStringSubmatch(strings.TrimRight(line, "\r")); m != nil {
checked[m[1]] = true
}
}
return checked
}

// BuildComment renders the full comment body starting with marker.
// Packages that have no active branches are omitted, so stale sections disappear
// automatically on recompute without any special removal logic.
// The checked parameter is accepted for forward-compatibility but currently has no
// effect on the output — branches are rendered as plain list items until #19214
// (auto-backport PR creation) is implemented, at which point checkboxes will be restored.
//
// Returns "" when no package has any active branch; callers should skip posting.
func BuildComment(pkgs []PackageBranches, checked map[string]bool) string {
if !slices.ContainsFunc(pkgs, func(p PackageBranches) bool { return len(p.Branches) > 0 }) {
return ""
}

var b strings.Builder

fmt.Fprintln(&b, marker)
fmt.Fprintln(&b, "## Backport branches")
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, "Consider backporting this change to the branches listed below.")
fmt.Fprintln(&b)
fmt.Fprintln(&b, "This comment is updated automatically on each push — manual edits will be overwritten.")

for _, p := range pkgs {
if len(p.Branches) == 0 {
continue
}

// One blank line before every package header (between intro and first
// package, and between consecutive packages).
fmt.Fprintln(&b)
fmt.Fprintf(&b, "**%s**\n", p.Package)
for _, r := range p.Branches {
line := fmt.Sprintf("- `%s`", r.Branch)
if r.MaintainedUntil != nil {
line += fmt.Sprintf(" (maintained until %s)", *r.MaintainedUntil)
}
fmt.Fprintln(&b, line)
}
}

fmt.Fprintln(&b)
fmt.Fprintln(&b, "---")
fmt.Fprintln(&b)
fmt.Fprintln(&b, "> [!TIP]")
fmt.Fprintln(&b, "> If a branch above is no longer required, set `archived: true` in its entry in `.backports.yml` to stop it appearing here.")
fmt.Fprintln(&b, `> If the branch has a known end-of-life date, prefer `+"`"+`maintained_until: "YYYY-MM-DD"`+"`"+` — it will be excluded automatically once that date passes.`)

return b.String()
}
Loading
Loading