Files
cozystack/.github/workflows/backport.yaml
Myasnikov Daniil 6ff5b98d56 ci(backport): target the newest existing release line
Determine the backport targets by enumerating real release-X.Y branches
instead of deriving them from getLatestRelease.

getLatestRelease returns the newest published stable. Now that cutting
an rc creates release-X.Y before vX.Y.0 exists, that name lagged by a
full line for the whole freeze window: a `backport` on a fix for the
release being stabilised would have landed on the previous line and
missed the release it was written for. The freeze window is when
backports matter most, since the frozen branch is the only way in.

Also drops the contiguous-minor assumption — `previous` was computed as
min-1, naming a branch that does not exist whenever a minor is skipped.
Sorting is numeric, so release-1.10 correctly outranks release-1.9.

Outside a freeze window the result is unchanged: with release-1.0..1.5
present it still resolves to release-1.5 / release-1.4.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-03 21:31:59 +05:00

159 lines
7.0 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Automatic Backport
on:
pull_request_target:
types: [closed, labeled] # fires when PR is closed (merged) or labeled
concurrency:
group: backport-${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
# Top-level token defaults to read-only; jobs request the minimum extra scopes.
permissions:
contents: read
jobs:
# Determine which backports are needed
prepare:
if: |
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
(
contains(github.event.pull_request.labels.*.name, 'backport') ||
contains(github.event.pull_request.labels.*.name, 'backport-previous') ||
(github.event.action == 'labeled' && (github.event.label.name == 'backport' || github.event.label.name == 'backport-previous'))
)
# GitHub API calls only — no docker, no repo toolchain: GitHub-hosted.
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
backport_current: ${{ steps.labels.outputs.backport }}
backport_previous: ${{ steps.labels.outputs.backport_previous }}
current_branch: ${{ steps.branches.outputs.current_branch }}
previous_branch: ${{ steps.branches.outputs.previous_branch }}
steps:
- name: Check which labels are present
id: labels
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const pr = context.payload.pull_request;
const labels = pr.labels.map(l => l.name);
const isBackport = labels.includes('backport');
const isBackportPrevious = labels.includes('backport-previous');
core.setOutput('backport', isBackport ? 'true' : 'false');
core.setOutput('backport_previous', isBackportPrevious ? 'true' : 'false');
console.log(`backport label: ${isBackport}, backport-previous label: ${isBackportPrevious}`);
- name: Determine target branches
id: branches
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
// Enumerate the real release-X.Y branches rather than deriving them
// from the latest published release.
//
// Since cutting an rc freezes the line (cut-prerelease.yaml),
// release-X.Y is created when the first vX.Y.0-rc.N is cut — before
// vX.Y.0 is ever published. getLatestRelease returns the newest
// published STABLE, so throughout the freeze window it still named
// the previous line: a `backport` on a fix for the release being
// stabilised would have landed on release-X.(Y-1) and missed the
// release it was written for. That window is exactly when backports
// matter most, since the frozen branch is the only way in.
//
// Enumerating branches also drops the contiguous-minor assumption:
// `previous` used to be computed as min-1, which named a
// non-existent branch whenever a minor was skipped.
const branches = await github.paginate(github.rest.repos.listBranches, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
const lines = branches
.map(b => /^release-(\d+)\.(\d+)$/.exec(b.name))
.filter(Boolean)
.map(m => ({ name: m[0], maj: Number(m[1]), min: Number(m[2]) }))
.sort((a, b) => (b.maj - a.maj) || (b.min - a.min));
if (lines.length === 0) {
core.setFailed('No release-X.Y branches found; cannot determine backport target.');
return;
}
const currentBranch = lines[0].name;
const previousBranch = lines.length > 1 ? lines[1].name : '';
core.setOutput('current_branch', currentBranch);
core.setOutput('previous_branch', previousBranch);
console.log(`Release lines found: ${lines.map(l => l.name).join(', ')}`);
console.log(`Current branch: ${currentBranch}, Previous branch: ${previousBranch || 'N/A'}`);
// Both targets come from the enumeration above, so they exist by
// construction — no getBranch probe needed. What CAN still fail is
// asking for a previous line when only one exists.
if (!previousBranch && '${{ steps.labels.outputs.backport_previous }}' === 'true') {
core.setFailed('backport-previous requested, but only one release line exists.');
return;
}
backport:
needs: prepare
if: |
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
(needs.prepare.outputs.backport_current == 'true' || needs.prepare.outputs.backport_previous == 'true')
# git cherry-picks via backport-action — no docker, no repo toolchain: GitHub-hosted.
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
strategy:
matrix:
backport_type: [current, previous]
steps:
# 1. Determine target branch based on matrix
- name: Set target branch
id: target
if: |
(matrix.backport_type == 'current' && needs.prepare.outputs.backport_current == 'true') ||
(matrix.backport_type == 'previous' && needs.prepare.outputs.backport_previous == 'true')
run: |
if [ "${{ matrix.backport_type }}" == "current" ]; then
echo "branch=${{ needs.prepare.outputs.current_branch }}" >> $GITHUB_OUTPUT
echo "Target branch: ${{ needs.prepare.outputs.current_branch }}"
else
echo "branch=${{ needs.prepare.outputs.previous_branch }}" >> $GITHUB_OUTPUT
echo "Target branch: ${{ needs.prepare.outputs.previous_branch }}"
fi
# 2. Checkout (required by backport‑action)
- name: Checkout repository
if: steps.target.outcome == 'success'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# 3. Create the back‑port pull request
- name: Create back‑port PR
id: backport
if: steps.target.outcome == 'success'
uses: korthout/backport-action@0193454f0c5947491d348f33a275c119f30eb736 # v3.2.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
label_pattern: '' # don't read labels for targets
target_branches: ${{ steps.target.outputs.branch }}
merge_commits: skip
# conflict_resolution lives inside the experimental JSON input, not as a
# top-level key. Passed top-level it is silently ignored and the action
# falls back to the experimental default ("fail"), which on a conflict
# creates no PR and only comments. Nesting it here makes a conflicting
# backport open a draft PR with the conflict committed for manual fixup.
experimental: |
{
"conflict_resolution": "draft_commit_conflicts"
}