ci(matrix): Dynamically discover all Rust-supported OpenWrt SDK targets with fail-fast: false and ensure local image cleanup

This commit is contained in:
Nils Andreas Svee committed 2026-10-13 13:19:15 +07:00
1 parent 3c6bab64c4
commit 8b0a7f3f4c
5 files changed
+254 -26

No files matched your search

+50
View File
@@ -1,3 +1,53 @@
# All-target matrix validation — 2026-10-13
The Gitea workflow now defaults to `targets=all`. Target discovery reads every
page of the public GHCR SDK tag list and selects each release's target tags,
using release directory names to exclude architecture aliases. Architecture
metadata comes from the release's `.targets.json`, with the matching OpenWrt
source tag supplying `ARCH` for SDK-only targets absent from that JSON.
Live generation and registry manifest checks produced:
| OpenWrt release | Available target SDK images | Rust-supported matrix targets |
| --- | ---: | ---: |
| 25.12.5 (stable) | 94 | 93 |
| 24.10.8 (oldstable) | 90 | 89 |
| Total | 184 | 182 |
Both releases exclude `ixp4xx/generic` (`armeb`). The architecture allowlist
matches `RUST_ARCH_DEPENDS` at the released packages feed revisions:
[25.12.5](https://github.com/openwrt/packages/blob/5caa62e0bc9f7fb9b0c12a23267bceb7724214dd/lang/rust/rust-values.mk)
and [24.10.8](https://github.com/openwrt/packages/blob/23abaa6f3b0fdfd76b570031107e5718476ff0c8/lang/rust/rust-values.mk).
All four Malta subtargets remain included for both releases. The older release
also retains SDK-only `lantiq/ase`, `lantiq/xway_legacy`, `ramips/rt288x` and
`realtek/rtl931x`.
Validation passed:
- All 182 generated base-image tags returned HTTP 200 and a manifest digest
from GHCR. Matrix entries are unique; `GITHUB_OUTPUT` matches JSON stdout.
- The explicit `x86/64,mediatek/filogic,octeon/generic` subset produces six
jobs. Selecting `siflower/sf21` produces only its 25.12.5 job.
- Six offline regression tests cover per-release availability, unsupported
architectures, explicit selections and duplicates, invalid/empty selections,
architecture aliases, SDK-only architecture fallback, missing metadata and
registry pagination. The matrix job runs these tests before live discovery.
- YAML parsing, actionlint 1.7.12 (excluding only the existing Gitea
`builtin:checkout` syntax), ShellCheck and `git diff --check` pass.
- Workflow assertions confirm `fail-fast: false`, unchanged `max-parallel: 1`,
timeout, runner, build/verification commands and cache environment. Each job
pushes after its own successful verification when `publish=true`.
- Mock Docker execution checks publication and cleanup after a successful push,
a failed push and an absent image. Cleanup runs with `always()` and removes
only the job's SDK tag; it does not prune caches, volumes or other images.
The Redis/S3 infrastructure, sccache configuration, eight-job compilation limit,
eight-CPU/20 GiB builder limits and 50 GiB BuildKit GC budget are unchanged.
This validation did not compile all SDKs or dispatch/publish images. Architecture
eligibility and available base images do not guarantee every Rust bootstrap will
succeed. Live workflow/publication acceptance remains pending; the legacy
Jenkinsfile is retained unchanged until cutover acceptance.
# Migration validation — 2026-10-10
Inspected the original Jenkinsfile, Dockerfile, downstream `sqm-autorate-rust`
+84 -16
View File
@@ -1,40 +1,108 @@
#!/usr/bin/env python3
"""Resolve the same stable/oldstable x target matrix as Jenkins."""
"""Resolve Rust-supported SDK tags for each stable/oldstable release."""
import json
import os
import re
import subprocess
import tempfile
import sys
import urllib.parse
import urllib.request
def resolve(versions, targets):
# OpenWrt packages/lang/rust/rust-values.mk: RUST_ARCH_DEPENDS.
RUST_ARCHES = {
"aarch64", "arm", "i386", "loongarch64", "mips", "mips64",
"mips64el", "mipsel", "powerpc", "powerpc64", "riscv64", "x86_64",
}
def fetch(url):
with urllib.request.urlopen(url, timeout=60) as response:
return response.read().decode()
def sdk_tags():
token = json.loads(fetch("https://ghcr.io/token?service=ghcr.io&scope=repository:openwrt/sdk:pull"))["token"]
url = "https://ghcr.io/v2/openwrt/sdk/tags/list?n=1000"
tags = set()
while url:
request = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(request, timeout=60) as response:
tags.update(json.load(response)["tags"])
link = response.headers.get("Link", "")
next_page = re.search(r'<([^>]+)>;\s*rel="next"', link)
url = urllib.parse.urljoin(url, next_page[1]) if next_page else None
return tags
def release_targets(version, tags):
base = f"https://downloads.openwrt.org/releases/{version}"
# Directory names distinguish target/subtarget tags from architecture aliases.
boards = re.findall(r'href="([a-z0-9_+-]+)/"', fetch(f"{base}/targets/"))
suffix = f"-{version}"
targets = sorted({f"{board}/{tag[len(board) + 1:-len(suffix)]}"
for board in boards for tag in tags
if tag.startswith(f"{board}-") and tag.endswith(suffix)})
if not targets:
raise ValueError(f"No SDK images available for OpenWrt {version}")
architectures = json.loads(fetch(f"{base}/.targets.json"))
result = {}
for target in targets:
if not re.fullmatch(r"[a-z0-9_+-]+/[a-z0-9_+-]+", target):
raise ValueError(f"Invalid SDK target: {target!r}")
if target in architectures:
arch = architectures[target]
arch = arch if arch in RUST_ARCHES else arch.split("_", 1)[0]
else:
# SDK-only targets (e.g. Malta) have no profiles in .targets.json.
board, subtarget = target.split("/")
source = f"https://raw.githubusercontent.com/openwrt/openwrt/v{version}/target/linux"
definition = fetch(f"{source}/{board}/{subtarget}/target.mk")
match = re.search(r"^ARCH\s*:?=\s*([a-z0-9_]+)\s*$", definition, re.MULTILINE)
if not match:
definition = fetch(f"{source}/{board}/Makefile")
match = re.search(r"^ARCH\s*:?=\s*([a-z0-9_]+)\s*$", definition, re.MULTILINE)
if not match:
raise ValueError(f"Cannot determine architecture for {target} on {version}")
arch = match[1]
result[target] = arch
return result
def resolve(versions, targets, tags):
versions = [versions[k] for k in ("stable_version", "oldstable_version") if versions.get(k)]
if not versions:
raise ValueError("OpenWrt returned no stable or oldstable version")
targets = [t.strip() for t in targets.split(",") if t.strip()]
if not targets:
raise ValueError("No targets selected")
if "all" in targets and targets != ["all"]:
raise ValueError("Select all or a comma-separated target list")
for target in targets:
if not re.fullmatch(r"[a-z0-9_+-]+/[a-z0-9_+-]+", target):
if target != "all" and not re.fullmatch(r"[a-z0-9_+-]+/[a-z0-9_+-]+", target):
raise ValueError(f"Invalid target: {target!r}")
for version in versions:
if not re.fullmatch(r"[a-zA-Z0-9_.-]+", version):
raise ValueError(f"Invalid version: {version!r}")
return {"include": [{"target": t.replace("/", "-"), "version": v}
for v in versions for t in dict.fromkeys(targets)]}
matrix = []
for version in dict.fromkeys(versions):
available = release_targets(version, tags)
selected = available if targets == ["all"] else dict.fromkeys(targets)
supported = [t for t in selected if available.get(t) in RUST_ARCHES]
skipped = [t for t in selected if t not in supported]
print(f"OpenWrt {version}: {len(supported)} targets selected; "
f"{len(available)} SDK images available", file=sys.stderr)
if skipped:
print(f"Skipped unavailable or Rust-unsupported targets: {', '.join(skipped)}", file=sys.stderr)
matrix.extend({"target": t.replace("/", "-"), "version": version} for t in supported)
if not matrix:
raise ValueError("No available Rust-supported SDK targets selected")
return {"include": matrix}
if __name__ == "__main__":
with urllib.request.urlopen("https://downloads.openwrt.org/.versions.json", timeout=60) as response:
versions = json.load(response)
targets = os.environ.get("TARGETS", "x86/64,mediatek/filogic,octeon/generic")
if targets.strip() == "all":
with tempfile.TemporaryDirectory(prefix="openwrt-targets-") as source:
subprocess.run(["git", "clone", "--depth", "1", "https://github.com/openwrt/openwrt.git", source], check=True)
output = subprocess.check_output(["perl", "scripts/dump-target-info.pl", "targets"], cwd=source, text=True)
targets = ",".join(line.split()[0] for line in output.splitlines() if line.strip())
matrix = json.dumps(resolve(versions, targets), separators=(",", ":"))
versions = json.loads(fetch("https://downloads.openwrt.org/.versions.json"))
targets = os.environ.get("TARGETS", "all")
matrix = json.dumps(resolve(versions, targets, sdk_tags()), separators=(",", ":"))
print(matrix)
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"matrix={matrix}\n")
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Offline checks for release-specific SDK selection and registry pagination."""
import contextlib
import io
import json
import unittest
from unittest.mock import patch
import matrix
class MatrixTests(unittest.TestCase):
versions = {"stable_version": "25.12.5", "oldstable_version": "24.10.8"}
def resolve(self, targets):
releases = {
"25.12.5": {"x86/64": "x86_64", "siflower/sf21": "riscv64", "ixp4xx/generic": "armeb"},
"24.10.8": {"x86/64": "x86_64", "malta/be64": "mips64", "arc/generic": "arc"},
}
with patch.object(matrix, "release_targets", side_effect=lambda v, tags: releases[v]):
with contextlib.redirect_stderr(io.StringIO()):
return matrix.resolve(self.versions, targets, set())["include"]
def test_all_uses_each_releases_available_supported_sdks(self):
self.assertEqual(self.resolve("all"), [
{"target": "x86-64", "version": "25.12.5"},
{"target": "siflower-sf21", "version": "25.12.5"},
{"target": "x86-64", "version": "24.10.8"},
{"target": "malta-be64", "version": "24.10.8"},
])
def test_explicit_targets_deduplicate_and_skip_unavailable_combinations(self):
self.assertEqual(self.resolve("siflower/sf21,x86/64,x86/64,ixp4xx/generic"), [
{"target": "siflower-sf21", "version": "25.12.5"},
{"target": "x86-64", "version": "25.12.5"},
{"target": "x86-64", "version": "24.10.8"},
])
def test_invalid_or_empty_selection_fails(self):
for targets in ("", "../bad", "x86/64;echo", "all,x86/64", "ixp4xx/generic", "missing/generic"):
with self.subTest(targets=targets), self.assertRaises(ValueError):
self.resolve(targets)
for versions in ({}, {"stable_version": "../bad"}):
with self.assertRaises(ValueError):
matrix.resolve(versions, "all", set())
def test_discovery_excludes_aliases_and_other_releases_and_keeps_sdk_only_targets(self):
base = "https://downloads.openwrt.org/releases/25.12.5"
source = "https://raw.githubusercontent.com/openwrt/openwrt/v25.12.5/target/linux"
responses = {
f"{base}/targets/": '<a href="x86/">x86</a><a href="ixp4xx/">ixp4xx</a>'
'<a href="malta/">malta</a><a href="ramips/">ramips</a>',
f"{base}/.targets.json": json.dumps({"x86/64": "x86_64", "ixp4xx/generic": "armeb_xscale"}),
f"{source}/malta/be64/target.mk": "ARCH:=mips64\n",
f"{source}/ramips/rt288x/target.mk": "SUBTARGET:=rt288x\n",
f"{source}/ramips/Makefile": "ARCH:=mipsel\n",
}
tags = {"x86-64-25.12.5", "ixp4xx-generic-25.12.5", "malta-be64-25.12.5",
"ramips-rt288x-25.12.5", "x86_64-25.12.5", "arm_cortex-a7-25.12.5",
"x86-generic-24.10.8", "x86-64-v25.12.5"}
with patch.object(matrix, "fetch", side_effect=lambda url: responses[url]):
self.assertEqual(matrix.release_targets("25.12.5", tags), {
"ixp4xx/generic": "armeb", "malta/be64": "mips64",
"ramips/rt288x": "mipsel", "x86/64": "x86_64",
})
def test_missing_images_or_unknown_architecture_fails(self):
with patch.object(matrix, "fetch", return_value='<a href="malta/">malta</a>'):
with self.assertRaisesRegex(ValueError, "No SDK images"):
matrix.release_targets("25.12.5", set())
def response(url):
if url.endswith("/targets/"):
return '<a href="malta/">malta</a>'
return "{}" if url.endswith(".json") else "SUBTARGET:=be\n"
with patch.object(matrix, "fetch", side_effect=response):
with self.assertRaisesRegex(ValueError, "Cannot determine architecture"):
matrix.release_targets("25.12.5", {"malta-be-25.12.5"})
def test_registry_pagination(self):
first = io.BytesIO(b'{"tags":["x86-64-25.12.5"]}')
first.headers = {"Link": '</v2/openwrt/sdk/tags/list?last=x86-64-25.12.5&n=1000>; rel="next"'}
second = io.BytesIO(b'{"tags":["malta-be-24.10.8"]}')
second.headers = {}
with patch.object(matrix, "fetch", return_value='{"token":"public-pull-token"}'):
with patch.object(matrix.urllib.request, "urlopen", side_effect=[first, second]) as opened:
self.assertEqual(matrix.sdk_tags(), {"x86-64-25.12.5", "malta-be-24.10.8"})
self.assertEqual(opened.call_args_list[1].args[0].full_url,
"https://ghcr.io/v2/openwrt/sdk/tags/list?last=x86-64-25.12.5&n=1000")
if __name__ == "__main__":
unittest.main()