nightly build: Add repository skill for kernel updates

Keep the kernel update procedure in .agents/skills so it travels with the
repository and is available to contributors without a personal install.
Document the canonical nightly list, derived CI matrices, official version
sources and the boundary between updates and new distribution support.

Include a read-only upstream helper that compares configured series with
both the kernel.org release feed and archives, including final EOL updates.
Use a repository-relative command and shared English UI metadata.
This commit is contained in:
Gleb Chesnokov
2026-10-19 13:13:40 +03:00
parent b5702037d3
commit ecec432722
3 changed files with 247 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
---
name: scst-kernel-update
description: Refresh SCST nightly kernel versions and derived CI matrices from official upstream and distribution archives, and prepare the nightly/update branch. Use for kernel support-list maintenance; adding distribution releases or UEK generations and fixing compatibility regressions are separate work.
---
# SCST kernel updates
Follow the repository's current `AGENTS.md` and `CONTRIBUTING.md`. This skill
captures update policy; it does not authorize builds, live-host operations,
publication, or history rewriting beyond the user's request.
## Branch and source of truth
- Inspect status, worktrees, remotes, and `nightly/update` before switching.
Resolve spelling from Git rather than creating a second `nighly/update`.
- Fetch the requested base and update branch. Record both SHAs. Rebase on
current `origin/master` when requested, preserving unrelated work and any
branch-only compatibility fixes. Already applied commits may be skipped;
verify patch equivalence rather than replaying them blindly.
- `ABT_KERNELS` in `nightly/conf/nightly.conf` is canonical. Exact versions
propagate through `scripts/kernel/kernel-matrix` to regression and RPM CI.
`.github/kernel-matrix.conf` contains selectors and RPM target policy.
Inspect these consumers each time in case paths or behavior have changed.
- Updating an existing series normally changes only `nightly.conf`. A newly
requested upstream release also needs its CI selector. Do not duplicate
exact versions in workflow YAML or regenerate unrelated artifacts.
## Find current versions
Compare the checked-in inventory directly with published latest versions.
Reading every intervening announcement is unnecessary; the mailing list is
useful for release context, EOL notices, and investigating a regression.
Record the retrieval date, old/new versions, and supporting URLs.
For upstream kernels, run the bundled read-only helper from the SCST root:
```sh
python3 .agents/skills/scst-kernel-update/scripts/upstream_candidates.py \
nightly/conf/nightly.conf
```
It combines <https://www.kernel.org/releases.json> with the official
`https://cdn.kernel.org/pub/linux/kernel/v<major>.x/` indexes. The feed alone
misses final updates of series that have since become EOL. The helper checks
existing upstream series, reports missing current non-RC releases separately,
and never edits files. Network/parse failures are errors, not "up to date".
Verify proposed tarball/patch URLs before applying candidates, especially
when the feed and mirror index temporarily disagree. Exclude RC and linux-next.
For distribution entries, retain the exact distribution/release pair, upstream
baseline, and UEK generation already present. Read
`scripts/kernel/rhel-rpm-functions` for URLs the runner can actually consume.
- Rocky: start at <https://download.rockylinux.org/pub/rocky/>. Inspect the
exact minor release's `BaseOS/source/tree/Packages/k/` (or `Packages/`).
Retired minors move to `https://dl.rockylinux.org/vault/rocky/`; a missing
active directory does not authorize advancing to another minor.
- UEK: start at <https://yum.oracle.com/oracle-linux-10.html> and the analogous
page for the existing OL major. Select that entry's UEK generation, then
inspect `index_src.html` for `kernel-uek` source RPMs. The same OL major can
offer several UEK generations. Keep OL major, UEK generation, and upstream
baseline distinct; do not select the largest version across repositories.
- Existing AlmaLinux/CentOS entries: inspect the exact release's source vault
during a full inventory refresh. They can contain a missed final update.
Keep the baseline and package variant: a CentOS vault can also contain a
different kernel baseline or `centos.plus`. Do not replace old entries with
another distribution or delete them because of EOL.
Compare RPM **version and release**, preferably with RPM's comparator or repo
metadata. Alphabetic sorting and blindly taking the last `sort -V` filename
can rank `553.el8_10` above `553.174.1.el8_10`. Keep only the intended package
(`kernel` or `kernel-uek`), not debug, realtime, tools, or development variants.
Verify the exact source RPM exists at a URL supported by the runner. For RPM
CI targets, also verify the exact binary kernel/devel package version is
published in the configured repositories (Rocky kernel-devel may be in
AppStream rather than BaseOS). Indexes/repodata and successful HEAD
requests suffice; do not download large packages just to check availability.
## Apply and validate
- Update each existing series to its newest confirmed published version.
Keep options (`-nc`, `-ns`, etc.) and ordering. When advancing the upstream
head, preserve the repository's convention: the new head has full nightly
checks and the previous head becomes a normal `-nc` entry.
- Add a new upstream series when the request covers that release. Newly
discovered distribution minors, OL majors, and UEK generations are reported
separately; adding them requires a separate support change.
- Run `bash -n` and Bash ShellCheck on changed sourced configs, then
`bash scripts/kernel/kernel-matrix regression` and
`bash scripts/kernel/kernel-matrix rpm`. Confirm unique resolutions, the
intended upstream addition, unchanged distro identities/RPM targets, and
updated exact versions in both outputs. Standalone config lint may report
existing SC2034 assignments consumed by their sourcing scripts; establish
the base result and also lint through `kernel-matrix`, without changing the
repository's ignore policy.
- Inspect the diff and run `git diff --check <base>`. Use the registered
`nightly build:` commit prefix and required per-commit checks. Keep the
version refresh separate from compatibility/backport fixes.
- Report base/HEAD, changed versions, incomplete source checks, and which
validation ran. Matrix generation and lint are not compilation or runtime
evidence. Investigate CI failures at the exact published HEAD before adding
backports; no speculative compatibility changes belong in the refresh.
When publishing is authorized, push only the intended branch. If its rebase
requires rewriting remote history, respect the user's authorization and use
`--force-with-lease=refs/heads/nightly/update:<recorded-remote-sha>` rather than
unconditional force. A lease rejection requires inspecting the new remote
state, not retrying with a weaker guard. Do not claim CI ran merely because a
branch was pushed: inspect workflow triggers and any existing PR.
@@ -0,0 +1,4 @@
interface:
display_name: "SCST kernel update"
short_description: "Refresh the SCST nightly and CI kernel matrices"
default_prompt: "Use $scst-kernel-update to refresh existing SCST kernel series from official sources and prepare the nightly/update branch."
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Report upstream SCST kernel candidates without sourcing or editing config."""
import argparse
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from html.parser import HTMLParser
import json
from pathlib import Path
import re
import sys
from urllib.parse import urlparse
from urllib.request import urlopen
FEED = "https://www.kernel.org/releases.json"
ARCHIVE = "https://cdn.kernel.org/pub/linux/kernel/v{}.x/"
VERSION = re.compile(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?")
OPTIONS = re.compile(r"(?:-(?:nc|ns|nm|4|f|i|u|p))*")
def version_key(version):
parts = tuple(map(int, version.split(".")))
return parts + (0,) * (3 - len(parts))
def series(version):
return ".".join(version.split(".")[:2])
def read_config(path):
text = path.read_text()
match = re.search(r'^ABT_KERNELS="([^"$`]+)"', text, re.MULTILINE)
if not match:
raise ValueError("expected a literal double-quoted ABT_KERNELS assignment")
entries = match[1].replace("\\\n", " ").split()
upstream = {}
for entry in entries:
if "^" in entry:
continue
match = re.fullmatch(r"(" + VERSION.pattern + r")(" + OPTIONS.pattern + r")", entry)
if not match:
raise ValueError(f"unrecognized upstream entry: {entry}")
version = match[1]
name = series(version)
if name in upstream:
raise ValueError(f"duplicate upstream series: {name}")
upstream[name] = version
if not upstream:
raise ValueError("no upstream versions found")
return upstream
def fetch(url):
for attempt in range(2):
try:
with urlopen(url, timeout=25) as response:
return response.read().decode("utf-8")
except OSError:
if attempt:
raise
class Tarballs(HTMLParser):
def __init__(self):
super().__init__()
self.versions = set()
def handle_starttag(self, tag, attrs):
if tag != "a":
return
name = Path(urlparse(dict(attrs).get("href") or "").path).name
match = re.fullmatch(r"linux-(" + VERSION.pattern + r")\.tar\.xz", name)
if match:
self.versions.add(match[1])
def read_archive(major):
url = ARCHIVE.format(major)
parser = Tarballs()
parser.feed(fetch(url))
if not parser.versions:
raise ValueError(f"no release tarballs found: {url}")
return url, parser.versions
def inventory(current):
releases = json.loads(fetch(FEED))["releases"]
published = [r["version"] for r in releases if VERSION.fullmatch(r["version"])]
if not published:
raise ValueError("release feed contains no final releases")
majors = sorted({int(v.split(".")[0]) for v in [*current.values(), *published]})
latest = {}
for version in published:
name = series(version)
if name not in latest or version_key(version) > version_key(latest[name][0]):
latest[name] = (version, FEED)
with ThreadPoolExecutor(max_workers=4) as pool:
for url, versions in pool.map(read_archive, majors):
for version in versions:
name = series(version)
if name not in latest or version_key(version) >= version_key(latest[name][0]):
latest[name] = (version, url)
updates = []
for name, old in current.items():
if name not in latest:
raise ValueError(f"no published release found for existing series {name}")
new, source = latest[name]
if version_key(new) < version_key(old):
raise ValueError(f"sources are older than configured {old}: {new}")
if version_key(new) > version_key(old):
updates.append({"old": old, "new": new, "source": source})
additions = [{"new": latest[name][0], "source": latest[name][1]}
for name in sorted({series(v) for v in published} - current.keys(),
key=version_key, reverse=True)]
return {"checked_at": datetime.now(timezone.utc).isoformat(),
"checked_upstream_series": len(current),
"updates": updates, "new_series_for_review": additions}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("config", type=Path)
args = parser.parse_args()
try:
result = inventory(read_config(args.config))
except (OSError, ValueError, KeyError, TypeError) as error:
print(f"error: {error}", file=sys.stderr)
return 1
print(json.dumps(result, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())