Skip to content

Commit d6d975c

Browse files
bhcopelandnuclearcat
authored andcommitted
tools: add script and CI workflow to sync stable-rc branches
Add sync_stable_branches.py which fetches kernel.org/releases.json and updates config/trees/stable-rc.yaml with current stable and longterm branches, removing any that have reached EOL. Add GitHub Actions workflow that runs weekly to check for updates and automatically creates a PR when changes are needed. Signed-off-by: Ben Copeland <ben.copeland@linaro.org>
1 parent 8dfdd95 commit d6d975c

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Sync stable-rc branches
2+
3+
on:
4+
schedule:
5+
# Run weekly on Monday at 08:00 UTC
6+
- cron: '0 8 * * 1'
7+
workflow_dispatch:
8+
9+
jobs:
10+
sync:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
16+
steps:
17+
- name: Check out source code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.12'
24+
25+
- name: Check for updates
26+
id: check
27+
run: |
28+
python tools/sync_stable_branches.py --dry-run 2>&1 | tee output.txt
29+
if grep -q "Config is already up to date" output.txt; then
30+
echo "needs_update=false" >> $GITHUB_OUTPUT
31+
else
32+
echo "needs_update=true" >> $GITHUB_OUTPUT
33+
fi
34+
35+
- name: Apply updates
36+
if: steps.check.outputs.needs_update == 'true'
37+
run: python tools/sync_stable_branches.py
38+
39+
- name: Create Pull Request
40+
if: steps.check.outputs.needs_update == 'true'
41+
uses: peter-evans/create-pull-request@v7
42+
with:
43+
commit-message: "config/trees: sync stable-rc with kernel.org"
44+
title: "config/trees: sync stable-rc with kernel.org"
45+
body: |
46+
Automated sync of stable-rc.yaml with current kernel.org releases.
47+
48+
This PR was created automatically by the sync-stable-branches workflow.
49+
branch: sync-stable-branches
50+
delete-branch: true

tools/sync_stable_branches.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
#
3+
# SPDX-License-Identifier: LGPL-2.1-or-later
4+
#
5+
# Copyright (C) 2026 Linaro Limited
6+
# Author: Ben Copeland <ben.copeland@linaro.org>
7+
#
8+
# Sync stable-rc.yaml with current kernel.org stable/longterm branches
9+
10+
"""
11+
Fetch current stable and longterm kernel versions from kernel.org
12+
and update config/trees/stable-rc.yaml accordingly.
13+
"""
14+
15+
import argparse
16+
import json
17+
import re
18+
import sys
19+
import urllib.request
20+
from pathlib import Path
21+
22+
RELEASES_URL = "https://www.kernel.org/releases.json"
23+
CONFIG_PATH = Path(__file__).parent.parent / "config" / "trees" / "stable-rc.yaml"
24+
25+
26+
def fetch_releases():
27+
"""Fetch releases.json from kernel.org"""
28+
with urllib.request.urlopen(RELEASES_URL, timeout=30) as response:
29+
return json.loads(response.read().decode())
30+
31+
32+
def get_active_branches(releases_data):
33+
"""
34+
Extract active (non-EOL) stable and longterm branch versions.
35+
Returns sorted list of (major, minor) tuples.
36+
"""
37+
branches = set()
38+
39+
for release in releases_data.get("releases", []):
40+
moniker = release.get("moniker", "")
41+
if moniker not in ("stable", "longterm"):
42+
continue
43+
44+
if release.get("iseol", False):
45+
continue
46+
47+
version = release.get("version", "")
48+
match = re.match(r"^(\d+)\.(\d+)\.", version)
49+
if match:
50+
major, minor = int(match.group(1)), int(match.group(2))
51+
branches.add((major, minor))
52+
53+
return sorted(branches)
54+
55+
56+
def generate_yaml(branches):
57+
"""Generate the stable-rc.yaml content"""
58+
lines = ["build_configs:"]
59+
60+
for i, (major, minor) in enumerate(branches):
61+
version_str = f"{major}.{minor}"
62+
config_name = f"stable-rc_{version_str}"
63+
branch_name = f"linux-{version_str}.y"
64+
65+
if i == 0:
66+
# First entry defines the anchor
67+
lines.append(f" {config_name}: &stable-rc")
68+
lines.append(" tree: stable-rc")
69+
lines.append(f" branch: '{branch_name}'")
70+
else:
71+
lines.append("")
72+
lines.append(f" {config_name}:")
73+
lines.append(" <<: *stable-rc")
74+
lines.append(f" branch: '{branch_name}'")
75+
76+
lines.append("")
77+
return "\n".join(lines)
78+
79+
80+
def parse_current_branches(config_path):
81+
"""Parse current branches from existing config file"""
82+
if not config_path.exists():
83+
return set()
84+
85+
branches = set()
86+
content = config_path.read_text()
87+
88+
for match in re.finditer(r"branch:\s*'linux-(\d+)\.(\d+)\.y'", content):
89+
major, minor = int(match.group(1)), int(match.group(2))
90+
branches.add((major, minor))
91+
92+
return branches
93+
94+
95+
def main():
96+
parser = argparse.ArgumentParser(
97+
description="Sync stable-rc.yaml with kernel.org releases"
98+
)
99+
parser.add_argument(
100+
"--dry-run", "-n",
101+
action="store_true",
102+
help="Show what would change without modifying the file"
103+
)
104+
parser.add_argument(
105+
"--config",
106+
type=Path,
107+
default=CONFIG_PATH,
108+
help=f"Path to stable-rc.yaml (default: {CONFIG_PATH})"
109+
)
110+
args = parser.parse_args()
111+
112+
print(f"Fetching releases from {RELEASES_URL}...")
113+
try:
114+
releases_data = fetch_releases()
115+
except Exception as e:
116+
print(f"Error fetching releases: {e}", file=sys.stderr)
117+
return 1
118+
119+
active_branches = get_active_branches(releases_data)
120+
if not active_branches:
121+
print("Error: No active branches found", file=sys.stderr)
122+
return 1
123+
124+
print(f"Active kernel.org branches: {', '.join(f'{m}.{n}' for m, n in active_branches)}")
125+
126+
current_branches = parse_current_branches(args.config)
127+
print(f"Current config branches: {', '.join(f'{m}.{n}' for m, n in sorted(current_branches))}")
128+
129+
# Calculate differences
130+
to_add = set(active_branches) - current_branches
131+
to_remove = current_branches - set(active_branches)
132+
133+
if not to_add and not to_remove:
134+
print("Config is already up to date.")
135+
return 0
136+
137+
if to_add:
138+
print(f"Branches to add: {', '.join(f'{m}.{n}' for m, n in sorted(to_add))}")
139+
if to_remove:
140+
print(f"Branches to remove (EOL): {', '.join(f'{m}.{n}' for m, n in sorted(to_remove))}")
141+
142+
new_content = generate_yaml(active_branches)
143+
144+
if args.dry_run:
145+
print("\n--- New config (dry-run) ---")
146+
print(new_content)
147+
return 0
148+
149+
args.config.write_text(new_content)
150+
print(f"Updated {args.config}")
151+
return 0
152+
153+
154+
if __name__ == "__main__":
155+
sys.exit(main())

0 commit comments

Comments
 (0)