Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .automation/backport-skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Edera commits on edera/mainline that must never be backported to
# edera/6.18-lts, one subject per line, exactly as the commit has it.
#
# Each pull request merged into edera/mainline is backported on its own
# (.github/workflows/lts-backport.yml); label it no-backport to opt out. This
# file is for commits that did not arrive through such a pull request, or
# were ported to 6.18 in a form the subject match cannot see. A commit that
# is listed here, or belongs to a no-backport pull request, does not hold up
# later backports that touch the same files.
#
# Blank lines and lines starting with # are ignored.
206 changes: 206 additions & 0 deletions .automation/skills/mainline-backport/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
name: mainline-backport
description: Backport of one merged edera/mainline pull request to edera/6.18-lts, run unattended in CI by .github/workflows/lts-backport.yml. Covers the cherry-picks, adapting a commit to the older kernel, spotting a dependency on another backport, build checks, and the report that becomes the backport pull request.
---

# Mainline pull request backport

Edera kernel work lands on `edera/mainline` first, one pull request at a time,
and every merged pull request is proposed for `edera/6.18-lts` as its own
backport pull request. You are preparing one of those. The workflow has
already worked out which of the pull request's commits the LTS tree still
lacks and hands you that list.

The goal is that each backport changes **exactly what its mainline commit
changes**. Where 6.18 differs from mainline enough that the same change does
not apply, you adapt it the way a careful stable maintainer would and you say
exactly what you did.

This runs unattended. Nobody is watching while you work, and nobody can answer
a question. Everything a reviewer needs has to end up in the report.

## What happens to your result

You cannot push, and nothing you write lands on `edera/6.18-lts`: a maintainer
merges the backport pull request, or does not. When you finish, the workflow
independently:

1. runs `verify-backport.sh`, which checks from git alone that your branch
fast-forwards the LTS tip, is linear, and holds one commit per requested
commit, in order, each naming its source with a `(cherry picked from
commit <sha>)` trailer, keeping its source's subject, and changing the
same files by the same +/- lines;
2. builds x86_64 and arm64 `vmlinux` with `kernel-build.sh`.

Both results go into the pull request next to your report. A commit you had
to adapt **will** show up as adapted there. That is expected, not a failure;
your report explains it. Never reword a subject, merge two commits into one,
or split one into two: backports are recognised later by subject, and a
renamed one would be offered again.

## Inputs

The workflow gives you, in the prompt:

- `MAINLINE_PR`: the number and title of the merged pull request.
- `OLD_TIP`: the `edera/6.18-lts` commit to build on (already checked out).
- `REQUEST`: a file listing the pull request's commits to backport, one per
line as `<sha> TAB <subject>`, oldest first. Their objects are already
fetched. Commits the LTS tree already has, and automation-only commits, are
not in it.
- `PENDING`: a file listing every Edera commit on `edera/mainline` that the
LTS tree lacks, in the same format, oldest first. The request's own commits
are among them. The workflow has already checked that nothing earlier in
this list touches the request's files; you look for the dependencies a file
match cannot see.
- `RESULT`: the local branch name your result must end up on.
- `REPORT`: the path your report must be written to.

The automation's own scripts are in `.rebase-tools/scripts/`, copied from the
commit this run started from. Use those, never the copy inside the tree.

### How to run commands here

Your shell commands are checked against an allowlist that matches the
command's first word. Shell variables, `$(...)`, `<(...)` and `VAR=x cmd`
prefixes will be refused, so `OLD_TIP`, `RESULT`, `<sha>` and `<...>` below
are placeholders: substitute the literal value. Put scratch files, worktrees
and build output under `.rebase-scratch/` in the repository root; the
kernel's `.gitignore` ignores it.

This is a blobless clone: file contents are fetched from GitHub the first time
a command needs them, so the first checkout or `git show` can be slow. That is
not a hang.

## 1. Read the request

```sh
cat REQUEST
cat PENDING
git show --stat <sha> # for each requested commit
```

For each commit, read its message and diff, and check what it depends on:
does it call a helper, use a struct field or a Kconfig symbol that 6.18
lacks? If so, where does that come from?

- From an earlier commit in the request: fine, it is backported first.
- From a commit in `PENDING` that is not in the request: this backport has to
**wait** for that one. Stop here; see "If it has to wait".
- From upstream (Linus' tree) only: see the next section.

## 2. Cherry-pick, in order

```sh
git switch -c RESULT OLD_TIP
git cherry-pick -x <sha> # one at a time, oldest first
```

`-x` is required: the trailer it adds is how the checker pairs your commit
with its source. If you have to amend, keep the trailer and the subject.

When a pick conflicts, or applies but would not build or behave the same on
6.18:

- Read what differs between the two kernels around the change, and adapt the
change so it does on 6.18 what it does on mainline. Prefer the smallest
adaptation; do not backport an unrelated upstream refactor to make the
patch apply.
- If the conflict is with Edera code that only a pending commit brings,
that is a dependency: **wait**, as above.
- If the commit depends on an upstream change 6.18 lacks and cannot be
adapted without it, do not pull that upstream change in. Leave the commit
out (`git cherry-pick --abort` for that one, then continue with the next)
and make it a **Needs a decision** item saying what it needs.
- If the commit does not apply to 6.18 at all (it fixes code only mainline
has), leave it out the same way and say so.
- If you cannot tell what the right adaptation is, make the most conservative
one you can defend, mark it **UNSURE** in the report, and explain both
readings.
- Never use `-X ours`, `-X theirs`, or `git checkout --ours/--theirs` on a
whole file to make a conflict go away.
- Never commit anything that is not a backport of a requested commit. If a
backport needs a 6.18-only fix to build, fold it into that backport and
report it as an adaptation.

## 3. Build

```sh
bash .rebase-tools/scripts/kernel-build.sh x86_64 .rebase-scratch/obj-x86 4
bash .rebase-tools/scripts/kernel-build.sh arm64 .rebase-scratch/obj-arm64 4
```

If a build fails because of a backport, fix it **in that backport**: drive the
todo list with sed, `git -c sequence.editor="sed -i 's/^pick <sha>/edit
<sha>/'" rebase -i OLD_TIP`, amend (keeping subject and trailer), then `git
rebase --continue`. Never rebase below OLD_TIP.

If a build fails on OLD_TIP too, it is not yours: build OLD_TIP the same way
in a `.rebase-scratch/` worktree to confirm, and report it as pre-existing.

## 4. Check yourself

```sh
bash .rebase-tools/scripts/verify-backport.sh OLD_TIP RESULT REQUEST
```

Every commit it lists as adapted or left out must be accounted for in your
report. If one is not, you changed something you did not mean to: find it and
undo it.

## 5. Report

Write the report to the `REPORT` path, in Markdown. Its **first line** is the
status, exactly one of:

- `Status: proposed` when the RESULT branch holds a backport for review;
- `Status: waiting` when it has to wait for another backport (no RESULT
branch);
- `Status: not-applicable` when none of the request applies to 6.18 (no
RESULT branch).

The rest becomes the pull request body (or the comment on the mainline pull
request when there is no branch), so lead with what a reviewer must decide.
No preamble, no sign-off.

```markdown
Status: proposed

## Summary
One paragraph: commits requested, backported unchanged, adapted, left out,
and whether you expect the checker to pass.

## Needs a decision
One bullet per question, each starting **UNSURE:**, with both readings and
what you committed. Every left-out commit is one. "None." if there is
nothing.

## Adapted commits
Per commit: subject, what differs on 6.18, what you changed and why. "None."
if none.

## Left out
Per commit: subject and why. "None." if none.

## Builds
x86_64, arm64: pass, fail (with the first error), or pre-existing failure
(confirmed on OLD_TIP).
```

Then stop. The workflow reads the branch, not the working tree.

## If it has to wait

Do not create the RESULT branch. Write the report with `Status: waiting`,
and under `## Waiting on` list each pending commit it needs, as `` `<sha>`
<subject> ``, with one line on what it needs from it. The workflow labels the
mainline pull request `backport-waiting` and tries again once other backports
land.

## If nothing applies, or you cannot finish

If none of the request applies to 6.18, do not create the RESULT branch and
write the report with `Status: not-applicable`, explaining why. If you could
not produce a backport for any other reason, do not create the branch either
and explain what went wrong; the first line is then whatever fits best, and
a missing branch is treated as needing a human either way.
92 changes: 92 additions & 0 deletions .github/scripts/backport-blockers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Works out what a merged edera/mainline pull request still needs on
# edera/6.18-lts, and what it has to wait for first.
#
# Backports to edera/6.18-lts land in the order their commits landed on
# edera/mainline. A pull request is backported only once every Edera commit
# that came before it on mainline, is still missing from the LTS tree, and
# touches a file it touches has been backported (or skipped). Otherwise its
# commits could be applied on top of code they were never written against.
#
# Usage:
# backport-blockers.sh <pending-file> <request-file> <lts-tip> <lts-upstream> <todo-out>
#
# <pending-file> is what pending-backports.sh prints for the whole of
# edera/mainline: the mainline commits the LTS tree lacks, oldest first.
# <request-file> lists the pull request's own commits the same way ("<sha>
# TAB <subject>", oldest first). A request commit still needs backporting when
# it matches a pending commit, and those are written to <todo-out>:
#
# - by patch-id first, which is how a rebase-merged commit normally matches
# its copy on mainline;
# - otherwise, if the LTS series does not already have its patch-id, by
# subject, ignoring a trailing version tag. The patch-id check keeps a
# commit that is already on the LTS tree from claiming a different
# pending commit that happens to share its subject.
#
# Prints the pending commits the pull request has to wait for, oldest first,
# and nothing when it can go ahead.

set -euo pipefail

if [ $# -ne 5 ] || [ ! -r "$1" ] || [ ! -r "$2" ]; then
echo "usage: $0 <pending-file> <request-file> <lts-tip> <lts-upstream> <todo-out>" >&2
exit 2
fi
pending=$1
request=$2
l_tip=$(git rev-parse --verify "$3^{commit}")
l_base=$(git merge-base "$l_tip" "$(git rev-parse --verify "$4^{commit}")")
todo=$5

work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

norm() { sed -E 's/[[:space:]]*\((v?[0-9]+\.[0-9]+(\.[0-9y]+)?(-lts)?)\)$//'; }
pid() { git show "$1" | git patch-id --stable | cut -d' ' -f1; }
files() { git show --format= --name-only "$1"; }

git log --no-merges --format='commit %H' --patch "$l_base..$l_tip" |
git patch-id --stable | cut -d' ' -f1 | sort -u >"$work/lts-pids"

# Pending commits, numbered, with patch-ids.
n=0
while IFS=$'\t' read -r sha subject; do
n=$((n + 1))
printf '%s\t%s\t%s\t%s\n' "$n" "$sha" "$subject" "$(pid "$sha")"
done <"$pending" >"$work/pending"

# Each request commit claims the first pending commit it matches that nothing
# else has claimed.
: >"$todo"
: >"$work/claimed"
while IFS=$'\t' read -r sha subject; do
s=$(printf '%s' "$subject" | norm)
p=$(pid "$sha")
by=subject
grep -qxF -- "$p" "$work/lts-pids" && by=none
hit=$(awk -F'\t' -v s="$s" -v p="$p" -v by="$by" '
FILENAME == ARGV[1] { claimed[$0] = 1; next }
($1 in claimed) { next }
$4 == p { print $1; found = 1; exit }
by == "subject" && $3 == s && !first { first = $1 }
END { if (!found && first) print first }
' "$work/claimed" "$work/pending")
if [ -n "$hit" ]; then
echo "$hit" >>"$work/claimed"
printf '%s\t%s\n' "$sha" "$s" >>"$todo"
files "$sha" >>"$work/files"
fi
done <"$request"
[ -s "$todo" ] || exit 0
sort -u -o "$work/files" "$work/files"
first=$(sort -n "$work/claimed" | head -n1)

awk -F'\t' -v first="$first" '
FILENAME == ARGV[1] { claimed[$0] = 1; next }
$1 < first && !($1 in claimed) { print $2 "\t" $3 }
' "$work/claimed" "$work/pending" | while IFS=$'\t' read -r sha subject; do
if files "$sha" | grep -qxFf "$work/files"; then
printf '%s\t%s\n' "$sha" "$subject"
fi
done
80 changes: 80 additions & 0 deletions .github/scripts/pending-backports.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Lists the Edera commits on edera/mainline that edera/6.18-lts does not have.
#
# Edera kernel work lands on edera/mainline first and flows down to
# edera/6.18-lts. Both trees are rebased onto their upstream nightly, so commit
# hashes are no use for telling which commits already made it across. A commit
# counts as present on the LTS tree when its series there has:
#
# - a commit with the same subject, ignoring a trailing version tag such as
# " (6.18)" that a port may add (the k-th mainline commit with a subject
# pairs with the k-th LTS one, so repeated subjects are counted, not
# merged); or
# - a commit with the same patch-id, which catches a port whose subject was
# reworded but whose change was not.
#
# A commit that must never reach the LTS tree (it fixes code only mainline
# has, say) is listed by subject in the skip file, one per line (the
# backport workflow adds the commits of mainline pull requests labelled
# no-backport); blank lines
# and lines starting with # are ignored. Commits that change only .github/ or
# .automation/ are never offered: both trees carry the automation, it is
# landed on each by hand, and the backport is not allowed to touch it.
#
# Usage:
# pending-backports.sh <mainline-tip> <mainline-upstream> <lts-tip> <lts-upstream> [<skip-file>]
#
# Prints "<sha> TAB <subject>" for each pending commit, oldest first, and
# nothing when the trees agree. Merge commits are ignored on both sides: their
# content is in the commits they merged.

set -euo pipefail

if [ $# -lt 4 ] || [ $# -gt 5 ]; then
echo "usage: $0 <mainline-tip> <mainline-upstream> <lts-tip> <lts-upstream> [<skip-file>]" >&2
exit 2
fi

m_tip=$(git rev-parse --verify "$1^{commit}")
m_base=$(git merge-base "$m_tip" "$(git rev-parse --verify "$2^{commit}")")
l_tip=$(git rev-parse --verify "$3^{commit}")
l_base=$(git merge-base "$l_tip" "$(git rev-parse --verify "$4^{commit}")")
skip=${5:-/dev/null}

work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

# <sha> TAB <normalized subject> TAB <patch-id>, oldest first.
series() {
git log --no-merges --reverse --format='commit %H' --patch "$1" |
git patch-id --stable | awk '{ print $2 "\t" $1 }' >"$work/pid.map"
git log --no-merges --reverse --format='%H%x09%s' "$1" |
awk -F'\t' 'NR == FNR { pid[$1] = $2; next }
{
s = $2
sub(/[ \t]*\((v?[0-9]+\.[0-9]+(\.[0-9y]+)?(-lts)?)\)$/, "", s)
print $1 "\t" s "\t" ($1 in pid ? pid[$1] : "-")
}' "$work/pid.map" -
}
series "$m_base..$m_tip" >"$work/mainline"
series "$l_base..$l_tip" >"$work/lts"
# Mainline commits whose every changed path is automation.
git log --no-merges --format='commit %H' --name-only "$m_base..$m_tip" |
awk '/^commit / { if (c != "" && only) print c; c = $2; only = 0; seen = 0; next }
NF { if (!seen) only = 1; seen = 1; if ($0 !~ /^\.(github|automation)\//) only = 0 }
END { if (c != "" && only) print c }' >"$work/automation"
grep -vE '^[[:space:]]*(#|$)' "$skip" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' >"$work/skip" || true

awk -F'\t' '
FILENAME == ARGV[1] { auto[$0] = 1; next }
FILENAME == ARGV[2] { skip[$0] = 1; next }
FILENAME == ARGV[3] { have[$2]++; if ($3 != "-") bypid[$3] = $2; next }
{
if (($1 in auto) || ($2 in skip)) next
# A patch-id match uses up the LTS commit it matched, under the
# subject that commit has there, so it cannot also pair with another.
if ($3 in bypid) { have[bypid[$3]]--; delete bypid[$3]; next }
if (have[$2] > 0) { have[$2]--; next }
print $1 "\t" $2
}
' "$work/automation" "$work/skip" "$work/lts" "$work/mainline"
Loading
Loading