Skip to content
Open
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
55 changes: 55 additions & 0 deletions .github/workflows/kernel-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Kernel auto bump

on:
workflow_dispatch:
schedule:
# Run at 2am every day. renovate presnetly doesnt have a scheudle restricting it - so this is its schedule
- cron: "0 2 * * *"

jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: "Checkout"
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Run kernel bump script
run: |
# bump_kernel.py will create a changes.json only if the changes exist - write it to the repo root
uv run --python 3.13 --directory scripts/kernel-bumper bump_kernel.py --changes-to-file ../../changes.json

- name: Check for changes.json
id: check_changes
run: |
if [ -f changes.json ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Update linuxkit YAMLs
if: steps.check_changes.outputs.exists == 'true'
run: |
make -C kernel update-kernel-yamls

- name: Authenticate gh CLI
if: steps.check_changes.outputs.exists == 'true'
env:
KERNEL_BUMP_TOKEN: ${{ secrets.KERNEL_BUMP_TOKEN }}
run: |
echo "$KERNEL_BUMP_TOKEN" | gh auth login --with-token
gh auth setup-git

- name: Create or update bump PR
if: steps.check_changes.outputs.exists == 'true'
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
uv run --python 3.13 --directory scripts/kernel-bumper post_pr.py --changes-data-file ../../changes.json
1 change: 1 addition & 0 deletions MAINTAINERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# This file is compiled into the MAINTAINERS file in docker/opensource.
#

barfoo
[Rules]

[Rules.maintainers]
Expand Down
17 changes: 17 additions & 0 deletions docs/kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,23 @@ docker run --rm -ti -v $(pwd):/src linuxkit/kconfig
1. Commit the new config files.
1. Test that you can build the kernel with that config as `make build-<version>`, e.g. `make build-7.0.5`.


### Automated kernel version bumps

The repository includes automation to keep `KERNEL_VERSION` in `kernel/*/build-args` up to date with the latest patch releases for each series.

- Script: `scripts/bump_kernel.py` — queries kernel.org for the latest patch in each `X.Y` series, updates the `KERNEL_VERSION` lines in the appropriate `build-args` files, and runs `make -C kernel update-kernel-yamls` to propagate changes to examples, tests and docs.
- Renovate: `.github/renovate.json` config uses the `regex` manager restricted to `kernel/*/build-args` and invokes the script as a `postUpgradeTasks` command so Renovate will open a branch/PR when a bump is needed.
- Nightly workflow: `.github/workflows/kernel-bump.yml` runs nightly (and on-demand via `workflow_dispatch`) and executes the bump script; it creates a pull request only when changes are produced.

Behavior notes:

- The process is idempotent and no-ops when there is no version change — no PR or commit is created in that case.
- For standalone runs (outside Renovate), the script supports `--dry-run` and `--git-commit` modes; the latter stages and commits changes (useful for the nightly workflow).
- The workflow and Renovate use `GITHUB_TOKEN` (or Renovate credentials) to create branches/PRs; no external credentials are required for reading kernel.org.

If you need to exclude a particular series (for example `-rt` variants or EOL series) edit the `kernel/<series>/build-args` file manually — the automation is intended to cover the standard stable series only.

In addition, there are tests that are applied to a specific kernel version, notably the tests in
[020_kernel](../test/cases/020_kernel/). You will need to add a new test case for the new series,
copying an existing one and modifying it as needed.
Expand Down
17 changes: 17 additions & 0 deletions scripts/kernel-bumper/PR_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Kernel versions bump

This PR was generated automatically by the kernel bump workflow.

Please review the changes and merge when ready.

This PR bumps the following kernel versions:

{{HEADLINES}}

Further kernel releases will update this PR.

---

The following files will be changed by this PR:

{{CHANGED_FILES}}
17 changes: 17 additions & 0 deletions scripts/kernel-bumper/README-bump_kernel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
bump_kernel.py

This script updates `KERNEL_VERSION` in `kernel/*/build-args` to the latest
patch release for each series per kernel.org, and then runs
`make -C kernel update-kernel-yamls` to update references across the repo.

Usage:

```sh
python3 scripts/bump_kernel.py --dry-run
python3 scripts/bump_kernel.py --git-commit
```

Notes:
- The script is idempotent and will no-op when there are no changes.
- When integrating with Renovate, prefer running the script as a `postUpgradeTasks`
command so Renovate manages branch creation and PRs.
195 changes: 195 additions & 0 deletions scripts/kernel-bumper/bump_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""bump_kernel.py

Scan kernel/*/build-args for KERNEL_VERSION entries, find latest
patch versions from kernel.org and update the files.

Usage:
python3 scripts/bump_kernel.py [--dry-run]

The script is idempotent and will no-op if no changes are necessary.
"""

from __future__ import annotations

import argparse
import json
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Final, List, Optional

import requests
from semver import Version

KERNEL_JSON_URL = "https://www.kernel.org/releases.json"

logger = logging.getLogger(__name__)

DEFAULT_REPO_ROOT: Final = Path(__file__).parent.parent.parent


@dataclass
class Change:
path: Path
old: Version
new: Version

def to_dict(self) -> Dict[str, str]:
return {"path": str(self.path), "old": str(self.old), "new": str(self.new)}


@dataclass
class BuildArgsFile:
path: Path
prefix: str = "KERNEL_VERSION"
version: Optional[Version] = None
kernel_version_line_re: Final = re.compile(rf"^{prefix}\s*=\s*([\d.]+)\s*$", re.MULTILINE)

def get_current_version(self) -> Optional[Version]:
# just to stop us reading the file every damn time
if not self.version:
for ln in self.path.read_text().splitlines():
kv = self.parse_line(ln)
if kv:
self.version = kv
break
return self.version

def parse_line(self, s: str) -> Optional[Version]:
m = self.kernel_version_line_re.match(s)
if not m:
return None
version_str = m.group(1)
version = Version.parse(version_str)
if not version:
return None
return version

def replace_kernel_version(self, new_version: Version) -> Optional[Change]:
text = self.path.read_text()
new_text = re.sub(self.kernel_version_line_re, f"{self.prefix}={new_version}", text)
if new_text != text:
self.path.write_text(new_text)
return Change(path=self.path, old=self.version, new=new_version)
else:
return None


def fetch_releases_json(timeout: int = 10) -> Dict:
"""Fetch the kernel.org releases JSON.

Raises requests.HTTPError on non-2xx responses.
"""
resp = requests.get(KERNEL_JSON_URL, timeout=timeout)
resp.raise_for_status()
return resp.json()


def parse_versions_from_releases(data: Dict) -> List[Version]:
"""Extract a list of version strings from the releases JSON.

Returns a list like ["6.1.12", "6.1.13", ...].
"""
releases = data.get("releases") or []
versions = []
for r in releases:
vstr = r.get("version", "")
if not Version.is_valid(vstr):
# kernel release candidates follow the form "6.2-rc1" which semver doesn't like, so just skip invalid versions
logger.debug(f"Skipping invalid version in releases.json: {vstr}")
continue
versions.append(Version.parse(vstr))
return versions


def latest_patch_for_series(versions: List[Version], major: int, minor: int) -> Optional[Version]:
"""Return the latest patch-level version for a given major.minor series.

Example: given versions containing "6.1.12" and "6.1.13",
calling with major=6, minor=1 returns "6.1.13".
"""
candidates = [v for v in versions if v.major == major and v.minor == minor]
return max(candidates) if candidates else None


def find_build_args_files(root: Path = DEFAULT_REPO_ROOT) -> List[BuildArgsFile]:
"""Returns a list of kernel/*/build-args files in the workspace."""
return [BuildArgsFile(path=p) for p in root.glob("kernel/*/build-args")]


def process_build_args_file(build_args: BuildArgsFile, versions: List[str], dry_run: bool) -> Optional[Change]:
"""Check a single `build-args` file and update it if a newer patch exists.

Returns a `Change` if the file was modified, otherwise None.
"""
current_version = build_args.get_current_version()
if not current_version:
# No KERNEL_VERSION line found
return None

latest = latest_patch_for_series(versions, current_version.major, current_version.minor)
if not latest:
logger.info(f"No matching release found for series {current_version.major}.{current_version.minor}.x")
return None

if latest == current_version:
logger.info(f"{build_args.path} is up-to-date ({current_version})")
return None

logger.info(f"Updating {build_args.path}: {current_version} -> {latest}")
if dry_run:
logger.debug(f"Dry run: would update {build_args.path} from {current_version} to {latest}")
return Change(path=build_args.path, old=current_version, new=latest)

return build_args.replace_kernel_version(latest)


def main(argv: Optional[List[str]] = None) -> int:

p = argparse.ArgumentParser(description="Bump kernel KERNEL_VERSION in kernel/*/build-args")
p.add_argument("--dry-run", action="store_true", help="Show what would be changed but do not modify files")
p.add_argument("--repo-root", type=Path, default=DEFAULT_REPO_ROOT, help="Path to the root of the repository")
p.add_argument("--log-level", default="INFO", help="Logging level (e.g. DEBUG, INFO, WARNING)")
p.add_argument("--verbose", "-v", action="store_const", const="DEBUG", dest="log_level", help="Set log level to DEBUG")
p.add_argument("--changes-to-file", type=Path, help="Dump a JSON list of changes to a file")
args = p.parse_args(argv)

logging.basicConfig(level=args.log_level.upper())

files = find_build_args_files(args.repo_root)
if not files:
logger.info("No build-args files found.")
return 0

try:
releases = fetch_releases_json()
except Exception as e:
logger.error(f"Failed to fetch releases.json: {e}")
return 1

versions = parse_versions_from_releases(releases)
changes: List[Change] = []

# Process each build-args file and collect changes
for f in files:
ch = process_build_args_file(f, versions, args.dry_run)
if ch and (not args.dry_run):
changes.append(ch)

if not changes:
logger.info("No changes necessary.")
else:
logger.info(f"Changed {len(changes)} build-args files.")
if args.changes_to_file:
fpath = Path(args.changes_to_file)
logger.info(f"Writing changes to {fpath}")
content = json.dumps([c.to_dict() for c in changes], indent=2)
fpath.write_text(content)

return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading