[ka:cli-build-out] ka-promote: implement resolver + cumulative + manifest.lock (closes #22) #23
@@ -0,0 +1,7 @@
|
|||||||
|
# ka-promote / ka-build output
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# transient
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
@@ -106,10 +106,12 @@ persistent, audit trail per item).
|
|||||||
## Verbs (explicit, parameterized, audit-issue auto-filed)
|
## Verbs (explicit, parameterized, audit-issue auto-filed)
|
||||||
|
|
||||||
```
|
```
|
||||||
ka-promote <campaign> <patch-or-glob> --to <scope>
|
ka-promote <host> # resolve fleet/<host>.yaml → cumulative.patch + manifest.lock [bin/ka-promote — implemented Phase 6, issue #22]
|
||||||
|
ka-import <campaign> <patch-or-glob> --to <scope> # patches from campaign → scope-tagged tree (today: manual git workflow)
|
||||||
ka-close <campaign> --status success
|
ka-close <campaign> --status success
|
||||||
ka-abandon <campaign> --keep-as-archive | --purge-from-fleet
|
ka-abandon <campaign> --keep-as-archive | --purge-from-fleet
|
||||||
ka-install <host>
|
ka-build <host> # render PKGBUILD template with cumulative b2sum, run makepkg [next verb, issue TBD]
|
||||||
|
ka-install <host> # scp + pacman -U + extlinux/mkinitcpio + heartbeat [last verb, issue TBD]
|
||||||
ka-keep <job-id> [--for <duration>]
|
ka-keep <job-id> [--for <duration>]
|
||||||
ka-pause-prune / ka-resume-prune
|
ka-pause-prune / ka-resume-prune
|
||||||
ka-restore-archive <job-id>
|
ka-restore-archive <job-id>
|
||||||
@@ -120,6 +122,13 @@ ka-migrate-tree --from <p> --to <p>
|
|||||||
ka-wake-data # wraps wake-host data through His
|
ka-wake-data # wraps wake-host data through His
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: the original spec had `ka-promote <campaign> <patch-or-glob> --to <scope>`
|
||||||
|
("promote patches from a campaign into the canonical tree"). That semantic
|
||||||
|
moved to `ka-import` to free `ka-promote` for the manifest-resolution role
|
||||||
|
its issue (#22) and the implemented `bin/ka-promote` actually fulfil. `ka-import`
|
||||||
|
remains unimplemented — patches still land in `patches/` via the regular git
|
||||||
|
+ PR workflow.
|
||||||
|
|
||||||
Conversational invocation triggers a y/n confirmation enumerating what will
|
Conversational invocation triggers a y/n confirmation enumerating what will
|
||||||
happen. Direct CLI invocation executes immediately.
|
happen. Direct CLI invocation executes immediately.
|
||||||
|
|
||||||
@@ -226,15 +235,17 @@ via `ka-snooze <issue-id> [--for <duration>]`.
|
|||||||
|
|
||||||
## Bootstrap reference build (2026-05-09 — fresnel)
|
## Bootstrap reference build (2026-05-09 — fresnel)
|
||||||
|
|
||||||
First end-to-end run, before any `ka-*` CLI exists. Documented here as the
|
First end-to-end run, before `ka-promote` / `ka-build` / `ka-install` existed.
|
||||||
canonical worked example so future ka-* implementations have a concrete
|
Documented here as the canonical worked example; the substrate that the ka-*
|
||||||
substrate to replay. Issue #3 (fresnel DTS persistence) closed by this
|
verbs are/will-be implemented against. Issue #3 (fresnel DTS persistence) closed by this
|
||||||
build.
|
build. `ka-promote` (issue #22) replaced the manual step #1 below as of 2026-05-18.
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
- **Baseline:** mmind/linux-rockchip @ `v7.0` (Heiko Stübner / Collabora,
|
- **Baseline:** torvalds/linux @ `v7.0` (verified during ka-promote Phase 3,
|
||||||
via kernel.org).
|
issue #22 — mmind/linux-rockchip does not ship a plain `v7.0` tag despite
|
||||||
|
earlier docs; mmind kept in fresnel.yaml as informational
|
||||||
|
`patch_authoring_context`).
|
||||||
- **Patches** (scope `board/pinebook-pro`):
|
- **Patches** (scope `board/pinebook-pro`):
|
||||||
- `0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch`
|
- `0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch`
|
||||||
- `0002-arm64-dts-rk3399-pinebook-pro-enable-hdmi-sound.patch`
|
- `0002-arm64-dts-rk3399-pinebook-pro-enable-hdmi-sound.patch`
|
||||||
@@ -249,13 +260,14 @@ build.
|
|||||||
|
|
||||||
### Manual substitute for each ka-* verb
|
### Manual substitute for each ka-* verb
|
||||||
|
|
||||||
| Designed verb | What we did manually |
|
| Designed verb | What we did manually | Status |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `ka-promote fresnel-fourier <patches> --to board/pinebook-pro` | Authored 3 patches with proper headers/scope tags, pushed to `marfrit/kernel-agent/patches/board/pinebook-pro/` via Gitea contents API as `claude-noether`. |
|
| `ka-import fresnel-fourier <patches> --to board/pinebook-pro` (originally named `ka-promote` in this row) | Authored 3 patches with proper headers/scope tags, pushed to `marfrit/kernel-agent/patches/board/pinebook-pro/` via Gitea contents API as `claude-noether`. | still manual — `ka-import` unimplemented |
|
||||||
| `ka-build fresnel` | On boltzmann: cloned linux v7.0 from kernel.org, ran `makepkg -s --skipchecksums --skippgpcheck` against `marfrit-packages/arch/linux-fresnel-fourier/PKGBUILD`. Native aarch64 (boltzmann is RK3588). One headers-pkg bug discovered (`ln -sr` on missing parent dir) and fixed mid-flight. Repackaged. |
|
| `ka-promote fresnel` (new — manifest → cumulative.patch + manifest.lock) | n/a (didn't exist) | **automated 2026-05-18, issue #22** |
|
||||||
| `ka-sign + push` | scp pkgs hertz → `sudo /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>` per pkg. Script signs with key `92D5E96D8F63C75E4116AA1FF5C8C4603D0D250C`, runs repo-add, rsyncs to nc. |
|
| `ka-build fresnel` | On boltzmann: cloned linux v7.0 from kernel.org, ran `makepkg -s --skipchecksums --skippgpcheck` against `marfrit-packages/arch/linux-fresnel-fourier/PKGBUILD`. Native aarch64 (boltzmann is RK3588). One headers-pkg bug discovered (`ln -sr` on missing parent dir) and fixed mid-flight. Repackaged. | still manual — next verb to implement |
|
||||||
| `ka-install fresnel` (consent-via-action) | `sudo pacman -U /tmp/<pkg>` over LAN scp (HTTPS to nc was throttled by fresnel's wifi). pacman post-transaction hook updated extlinux. mkinitcpio run manually because the standard hook trigger watches `vmlinuz` not `Image`. |
|
| `ka-sign + push` | scp pkgs hertz → `sudo /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>` per pkg. Script signs with key `92D5E96D8F63C75E4116AA1FF5C8C4603D0D250C`, runs repo-add, rsyncs to nc. | still manual — folded into `ka-build` |
|
||||||
| Bar 1..3 verification | SSH heartbeat OK, `pacman -Q linux-fresnel-fourier` = `7.0-1`, post-reboot cluster0 1.704 GHz / cluster1 2.184 GHz confirmed. |
|
| `ka-install fresnel` (consent-via-action) | `sudo pacman -U /tmp/<pkg>` over LAN scp (HTTPS to nc was throttled by fresnel's wifi). pacman post-transaction hook updated extlinux. mkinitcpio run manually because the standard hook trigger watches `vmlinuz` not `Image`. | still manual — last verb to implement |
|
||||||
|
| Bar 1..3 verification | SSH heartbeat OK, `pacman -Q linux-fresnel-fourier` = `7.0-1`, post-reboot cluster0 1.704 GHz / cluster1 2.184 GHz confirmed. | folded into `ka-install` |
|
||||||
|
|
||||||
### Files / locations involved
|
### Files / locations involved
|
||||||
|
|
||||||
|
|||||||
Executable
+264
@@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ka-promote — resolve fleet/<host>.yaml + emit cumulative.patch + manifest.lock.
|
||||||
|
|
||||||
|
First of the three writing verbs (ka-promote → ka-build → ka-install).
|
||||||
|
Read-mostly: only writes to ${KA_BUILD_DIR:-./build}/<host>/<baseline_ref>/.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ka-promote <host>
|
||||||
|
ka-promote <host> --output-dir <path>
|
||||||
|
ka-promote <host> --validate-against <linux-checkout>
|
||||||
|
ka-promote --list-hosts
|
||||||
|
ka-promote --version
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 success
|
||||||
|
2 missing input (manifest, patch file, series-dir)
|
||||||
|
3 --validate-against failed (ref mismatch or apply-check failure)
|
||||||
|
4 manifest parse / schema error
|
||||||
|
|
||||||
|
Language note: pure python3 (not bash like ka-status). The data shape
|
||||||
|
here — YAML in, YAML out, dict construction, per-file hashing, glob
|
||||||
|
resolution — fits python naturally; bash + python -c heredocs would be
|
||||||
|
quoting hell for no readability gain. See issue #22 comment 1132.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
SCHEMA_VERSION = 1
|
||||||
|
COVER_LETTER = "0000-cover-letter.patch"
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg, code=1):
|
||||||
|
print(f"ka-promote: error: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def find_repo_root():
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
root = os.path.dirname(here)
|
||||||
|
if not os.path.isdir(os.path.join(root, "fleet")):
|
||||||
|
die(f"fleet/ not found relative to {here}", 4)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def list_hosts(fleet_dir):
|
||||||
|
for path in sorted(glob.glob(os.path.join(fleet_dir, "*.yaml"))):
|
||||||
|
print(os.path.basename(path)[:-5])
|
||||||
|
|
||||||
|
|
||||||
|
def load_manifest(path):
|
||||||
|
try:
|
||||||
|
raw = open(path, "rb").read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
die(f"manifest not found: {path}", 2)
|
||||||
|
sha = hashlib.sha256(raw).hexdigest()
|
||||||
|
try:
|
||||||
|
m = yaml.safe_load(raw)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
die(f"manifest parse error: {e}", 4)
|
||||||
|
if not isinstance(m, dict):
|
||||||
|
die(f"manifest root must be a mapping: {path}", 4)
|
||||||
|
for key in ("host", "baseline", "includes"):
|
||||||
|
if key not in m:
|
||||||
|
die(f"manifest missing required key '{key}': {path}", 4)
|
||||||
|
if not isinstance(m["includes"], list) or not m["includes"]:
|
||||||
|
die(f"manifest.includes must be a non-empty list: {path}", 4)
|
||||||
|
return m, sha
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_includes(includes, patches_root):
|
||||||
|
"""Walk manifest.includes, expand series-dirs, dedupe-check, hash."""
|
||||||
|
seen = set()
|
||||||
|
resolved = []
|
||||||
|
order = 0
|
||||||
|
for entry in includes:
|
||||||
|
if not isinstance(entry, str):
|
||||||
|
die(f"includes entry must be a string, got {type(entry).__name__}: {entry!r}", 4)
|
||||||
|
if entry in seen:
|
||||||
|
die(f"duplicate include: {entry}", 4)
|
||||||
|
seen.add(entry)
|
||||||
|
src_path = os.path.join(patches_root, entry)
|
||||||
|
if entry.endswith(".patch"):
|
||||||
|
if not os.path.isfile(src_path):
|
||||||
|
die(f"missing patch: {src_path}", 2)
|
||||||
|
order += 1
|
||||||
|
resolved.append({
|
||||||
|
"apply_order": order,
|
||||||
|
"include": entry,
|
||||||
|
"src": src_path,
|
||||||
|
"from_series": False,
|
||||||
|
})
|
||||||
|
elif entry.endswith("/"):
|
||||||
|
dir_path = src_path.rstrip("/")
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
die(f"missing series-dir: {dir_path}", 2)
|
||||||
|
files = sorted(glob.glob(os.path.join(dir_path, "*.patch")))
|
||||||
|
files = [f for f in files if os.path.basename(f) != COVER_LETTER]
|
||||||
|
if not files:
|
||||||
|
die(f"series-dir has no applied patches (only cover-letter or empty): {dir_path}", 2)
|
||||||
|
for f in files:
|
||||||
|
order += 1
|
||||||
|
resolved.append({
|
||||||
|
"apply_order": order,
|
||||||
|
"include": entry + os.path.basename(f),
|
||||||
|
"src": f,
|
||||||
|
"from_series": True,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
die(f"include must end in '.patch' or '/': {entry}", 4)
|
||||||
|
for r in resolved:
|
||||||
|
with open(r["src"], "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
r["sha256"] = hashlib.sha256(data).hexdigest()
|
||||||
|
r["size"] = len(data)
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def write_cumulative(resolved, out_path):
|
||||||
|
with open(out_path, "wb") as out:
|
||||||
|
for r in resolved:
|
||||||
|
with open(r["src"], "rb") as src:
|
||||||
|
out.write(src.read())
|
||||||
|
with open(out_path, "rb") as f:
|
||||||
|
b2 = hashlib.blake2b(f.read()).hexdigest()
|
||||||
|
size = os.path.getsize(out_path)
|
||||||
|
return size, b2
|
||||||
|
|
||||||
|
|
||||||
|
def write_lock(lock_path, *, host, manifest_rel, manifest_sha, baseline,
|
||||||
|
resolved, cumulative_size, cumulative_b2sum):
|
||||||
|
epoch = os.environ.get("SOURCE_DATE_EPOCH")
|
||||||
|
if epoch:
|
||||||
|
generated_at = datetime.fromtimestamp(int(epoch), tz=timezone.utc).isoformat()
|
||||||
|
else:
|
||||||
|
generated_at = datetime.now(tz=timezone.utc).isoformat()
|
||||||
|
lock = {
|
||||||
|
"ka_promote_version": VERSION,
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"generated_at": generated_at,
|
||||||
|
"host": host,
|
||||||
|
"manifest": {"path": manifest_rel, "sha256": manifest_sha},
|
||||||
|
"baseline": baseline,
|
||||||
|
"resolved_patches": [
|
||||||
|
{
|
||||||
|
"apply_order": r["apply_order"],
|
||||||
|
"include": r["include"],
|
||||||
|
"sha256": r["sha256"],
|
||||||
|
"size": r["size"],
|
||||||
|
"from_series": r["from_series"],
|
||||||
|
}
|
||||||
|
for r in resolved
|
||||||
|
],
|
||||||
|
"cumulative": {
|
||||||
|
"path": "cumulative.patch",
|
||||||
|
"size": cumulative_size,
|
||||||
|
"b2sum": cumulative_b2sum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with open(lock_path, "w") as f:
|
||||||
|
yaml.dump(lock, f, sort_keys=True, default_flow_style=False)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_against(checkout, baseline_ref, cumulative_path):
|
||||||
|
# `.git` is a directory in a plain checkout, a file (gitdir pointer)
|
||||||
|
# in a worktree. `os.path.exists` covers both.
|
||||||
|
if not os.path.exists(os.path.join(checkout, ".git")):
|
||||||
|
die(f"--validate-against: not a git checkout: {checkout}", 3)
|
||||||
|
def git(*args):
|
||||||
|
return subprocess.run(
|
||||||
|
["git", *args], cwd=checkout, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
r = git("rev-parse", f"{baseline_ref}^{{tree}}")
|
||||||
|
if r.returncode != 0:
|
||||||
|
die(f"baseline ref '{baseline_ref}' not found in checkout {checkout}", 3)
|
||||||
|
baseline_tree = r.stdout.strip()
|
||||||
|
head_tree = git("rev-parse", "HEAD^{tree}").stdout.strip()
|
||||||
|
if head_tree != baseline_tree:
|
||||||
|
die(f"checkout HEAD tree {head_tree} != baseline.ref {baseline_ref} tree {baseline_tree}. "
|
||||||
|
"Refusing apply-check on diverged tree.", 3)
|
||||||
|
# Working tree must match HEAD too — `git apply --check` runs against
|
||||||
|
# the working tree, not HEAD, so a dirty tree gives false negatives.
|
||||||
|
r = git("status", "--porcelain")
|
||||||
|
if r.stdout.strip():
|
||||||
|
die(f"checkout {checkout} has uncommitted changes. "
|
||||||
|
"`git reset --hard {0} && git clean -fdx` first.".format(baseline_ref), 3)
|
||||||
|
r = git("apply", "--check", cumulative_path)
|
||||||
|
if r.returncode != 0:
|
||||||
|
die(f"git apply --check failed:\n{r.stderr}", 3)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(prog="ka-promote", add_help=True)
|
||||||
|
p.add_argument("host", nargs="?", help="fleet host name (omit with --list-hosts/--version)")
|
||||||
|
p.add_argument("--output-dir", help="override ${KA_BUILD_DIR:-<repo>/build}")
|
||||||
|
p.add_argument("--validate-against", metavar="CHECKOUT",
|
||||||
|
help="run git apply --check against a clean baseline.ref checkout")
|
||||||
|
p.add_argument("--list-hosts", action="store_true", help="list available fleet/<host>.yaml manifests")
|
||||||
|
p.add_argument("--version", action="store_true", help="print ka-promote schema version + exit")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
repo_root = find_repo_root()
|
||||||
|
fleet_dir = os.path.join(repo_root, "fleet")
|
||||||
|
patches_root = os.path.join(repo_root, "patches")
|
||||||
|
|
||||||
|
if args.version:
|
||||||
|
print(f"ka-promote version {VERSION} (schema {SCHEMA_VERSION})")
|
||||||
|
return 0
|
||||||
|
if args.list_hosts:
|
||||||
|
list_hosts(fleet_dir)
|
||||||
|
return 0
|
||||||
|
if not args.host:
|
||||||
|
p.error("host is required (or use --list-hosts / --version)")
|
||||||
|
|
||||||
|
manifest_path = os.path.join(fleet_dir, f"{args.host}.yaml")
|
||||||
|
manifest, manifest_sha = load_manifest(manifest_path)
|
||||||
|
|
||||||
|
if manifest.get("host") != args.host:
|
||||||
|
die(f"manifest.host {manifest.get('host')!r} does not match filename {args.host!r}", 4)
|
||||||
|
|
||||||
|
baseline = manifest["baseline"]
|
||||||
|
if "ref" not in baseline:
|
||||||
|
die("manifest.baseline.ref is required", 4)
|
||||||
|
baseline_ref = baseline["ref"]
|
||||||
|
|
||||||
|
resolved = resolve_includes(manifest["includes"], patches_root)
|
||||||
|
|
||||||
|
out_root = args.output_dir or os.environ.get("KA_BUILD_DIR") or os.path.join(repo_root, "build")
|
||||||
|
out_dir = os.path.join(out_root, args.host, baseline_ref)
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
cumulative_path = os.path.join(out_dir, "cumulative.patch")
|
||||||
|
|
||||||
|
size, b2sum = write_cumulative(resolved, cumulative_path)
|
||||||
|
write_lock(
|
||||||
|
os.path.join(out_dir, "manifest.lock"),
|
||||||
|
host=args.host,
|
||||||
|
manifest_rel=os.path.relpath(manifest_path, repo_root),
|
||||||
|
manifest_sha=manifest_sha,
|
||||||
|
baseline=baseline,
|
||||||
|
resolved=resolved,
|
||||||
|
cumulative_size=size,
|
||||||
|
cumulative_b2sum=b2sum,
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.validate_against:
|
||||||
|
validate_against(args.validate_against, baseline_ref, cumulative_path)
|
||||||
|
|
||||||
|
print(f"ka-promote: {args.host} -> {out_dir}")
|
||||||
|
print(f" cumulative: cumulative.patch ({size} bytes)")
|
||||||
|
print(f" b2sum: {b2sum}")
|
||||||
|
print(f" patches: {len(resolved)} resolved ({sum(1 for r in resolved if r['from_series'])} from series-dirs)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+9
-6
@@ -1,9 +1,11 @@
|
|||||||
# kernel-agent manifest for fresnel (Pinebook Pro / Rockchip RK3399)
|
# kernel-agent manifest for fresnel (Pinebook Pro / Rockchip RK3399)
|
||||||
#
|
#
|
||||||
# Status: bootstrap, manually-driven. ka-* CLI not yet implemented.
|
# Status: ka-promote-consumable. Used as Phase-3 parity reference for ka-promote
|
||||||
# This manifest is the input ka-promote / ka-build will consume once landed.
|
# bring-up (issue #22). ka-build / ka-install CLI still pending.
|
||||||
# Until then it documents the canonical patch set + baseline for the manual
|
#
|
||||||
# build that produces linux-fresnel-fourier.
|
# baseline.tree corrected 2026-05-18: mmind/linux-rockchip does not ship a
|
||||||
|
# plain v7.0 tag; baseline is torvalds/linux v7.0. mmind kept as informational
|
||||||
|
# patch_authoring_context.
|
||||||
|
|
||||||
host: fresnel
|
host: fresnel
|
||||||
arch: arm64
|
arch: arm64
|
||||||
@@ -12,10 +14,11 @@ board: pinebook-pro
|
|||||||
distro: archlinux-arm # EndeavourOS pacman base
|
distro: archlinux-arm # EndeavourOS pacman base
|
||||||
|
|
||||||
baseline:
|
baseline:
|
||||||
tree: mmind/linux-rockchip
|
tree: torvalds/linux
|
||||||
url: https://git.kernel.org/pub/scm/linux/kernel/git/mmind/linux-rockchip.git
|
url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
|
||||||
ref: v7.0
|
ref: v7.0
|
||||||
upstream_compat: linux-7.0 # what the patches target
|
upstream_compat: linux-7.0 # what the patches target
|
||||||
|
patch_authoring_context: mmind/linux-rockchip # informational — patches authored against Rockchip rebase
|
||||||
|
|
||||||
# Scope-tagged patch includes. Each entry resolves to
|
# Scope-tagged patch includes. Each entry resolves to
|
||||||
# patches/<scope>/.../<file>.patch in marfrit/kernel-agent.
|
# patches/<scope>/.../<file>.patch in marfrit/kernel-agent.
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Bad-include fixture: an entry that does not end in .patch or /.
|
||||||
|
# Expected: ka-promote exits 4 with a clear schema error.
|
||||||
|
|
||||||
|
host: fixture-bad-include
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3566
|
||||||
|
board: fixture
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
tree: torvalds/linux
|
||||||
|
url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
|
||||||
|
ref: v7.0
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- board/pinebook-pro/0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch
|
||||||
|
- this-is-not-a-patch-or-dir # neither .patch nor /
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: fixture-bad-include
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Duplicate-include fixture: same include twice.
|
||||||
|
# Expected: ka-promote exits 4 with a "duplicate include" error.
|
||||||
|
|
||||||
|
host: fixture-duplicate-include
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3566
|
||||||
|
board: fixture
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
tree: torvalds/linux
|
||||||
|
url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
|
||||||
|
ref: v7.0
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- board/pinebook-pro/0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch
|
||||||
|
- board/pinebook-pro/0002-arm64-dts-rk3399-pinebook-pro-enable-hdmi-sound.patch
|
||||||
|
- board/pinebook-pro/0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch # dup
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: fixture-duplicate-include
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Missing-patch fixture: a .patch include pointing at a file that doesn't exist.
|
||||||
|
# Expected: ka-promote exits 2 with a "missing patch" error.
|
||||||
|
|
||||||
|
host: fixture-missing-patch
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3566
|
||||||
|
board: fixture
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
tree: torvalds/linux
|
||||||
|
url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
|
||||||
|
ref: v7.0
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- board/pinebook-pro/0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch
|
||||||
|
- board/pinebook-pro/9999-this-patch-does-not-exist.patch
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: fixture-missing-patch
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Synthetic fixture: single series-dir include.
|
||||||
|
# Used by tests/ka-promote/run-tests.sh to verify the series-dir resolver
|
||||||
|
# expands a directory entry to its .patch files in filename order, with
|
||||||
|
# 0000-cover-letter.patch excluded.
|
||||||
|
|
||||||
|
host: fixture-series-dir
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3566
|
||||||
|
board: pinetab2
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
tree: torvalds/linux
|
||||||
|
url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
|
||||||
|
ref: v7.0
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- driver/bes2600/staging-prep-series-danctnix/
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: fixture-series-dir
|
||||||
Executable
+138
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ka-promote test suite.
|
||||||
|
#
|
||||||
|
# Each test runs ka-promote against a fixture from fixtures/ in a temporary
|
||||||
|
# sandboxed kernel-agent tree (bin/, fleet/, patches/ — patches/ symlinked
|
||||||
|
# from the real repo so we exercise the real scope-tagged patch files).
|
||||||
|
#
|
||||||
|
# Exit 0 iff every test passes. Non-zero on first failure.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
fixtures="${repo_root}/tests/ka-promote/fixtures"
|
||||||
|
|
||||||
|
# Phase-3 ground truth — recorded 2026-05-18, fresnel cumulative b2sum.
|
||||||
|
FRESNEL_EXPECTED_B2SUM=4d9d93c655ea701b587bf1383c794f41b1aeb3bc32bca69ce3488852ec2c1474a2f47585608598b39ac05671490b8df63c5bc7d093f87e1afd5a92f908891b67
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
results=()
|
||||||
|
|
||||||
|
note() { printf ' %s\n' "$*"; }
|
||||||
|
ok() { results+=("PASS $1"); pass=$((pass+1)); note "PASS"; }
|
||||||
|
ko() { results+=("FAIL $1: $2"); fail=$((fail+1)); note "FAIL: $2"; }
|
||||||
|
|
||||||
|
make_sandbox() {
|
||||||
|
# $1 = fixture yaml path. Builds a scratch tree with bin/+fleet/+patches/
|
||||||
|
# and copies the fixture into fleet/<host>.yaml (extracting host from fixture).
|
||||||
|
local fixture="$1"
|
||||||
|
local scratch
|
||||||
|
scratch=$(mktemp -d -t ka-promote-test.XXXXXX)
|
||||||
|
mkdir -p "$scratch/bin" "$scratch/fleet"
|
||||||
|
cp "$repo_root/bin/ka-promote" "$scratch/bin/ka-promote"
|
||||||
|
ln -s "$repo_root/patches" "$scratch/patches"
|
||||||
|
local host
|
||||||
|
host=$(python3 -c "import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))['host'])" "$fixture")
|
||||||
|
cp "$fixture" "$scratch/fleet/${host}.yaml"
|
||||||
|
echo "$scratch|$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local name="$1"
|
||||||
|
local fixture="$2"
|
||||||
|
local expected_exit="$3"
|
||||||
|
local check_fn="${4:-}"
|
||||||
|
|
||||||
|
echo "::: $name"
|
||||||
|
local pair scratch host
|
||||||
|
pair=$(make_sandbox "$fixture")
|
||||||
|
scratch="${pair%|*}"
|
||||||
|
host="${pair#*|}"
|
||||||
|
set +e
|
||||||
|
out=$("$scratch/bin/ka-promote" "$host" --output-dir "$scratch/build" 2>&1)
|
||||||
|
actual_exit=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$actual_exit" -ne "$expected_exit" ]; then
|
||||||
|
ko "$name" "expected exit $expected_exit, got $actual_exit. Output: $out"
|
||||||
|
rm -rf "$scratch"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ -n "$check_fn" ]; then
|
||||||
|
if ! "$check_fn" "$scratch" "$host" "$out"; then
|
||||||
|
ko "$name" "check function reported failure (see notes above)"
|
||||||
|
rm -rf "$scratch"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
ok "$name"
|
||||||
|
rm -rf "$scratch"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_series_dir() {
|
||||||
|
# 7 patches resolved, all from_series:true, in filename order.
|
||||||
|
local scratch="$1" host="$2"
|
||||||
|
local lock="$scratch/build/$host/v7.0/manifest.lock"
|
||||||
|
[ -f "$lock" ] || { note "manifest.lock missing"; return 1; }
|
||||||
|
local n
|
||||||
|
n=$(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))")
|
||||||
|
if [ "$n" -ne 7 ]; then
|
||||||
|
note "expected 7 resolved patches, got $n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
python3 - "$lock" <<'PY' || { note "from_series check failed"; exit 1; }
|
||||||
|
import yaml, sys
|
||||||
|
lk = yaml.safe_load(open(sys.argv[1]))
|
||||||
|
for r in lk['resolved_patches']:
|
||||||
|
assert r['from_series'] is True, f"{r['include']} not flagged from_series"
|
||||||
|
expected_basenames = [f'0{i:03d}'[1:] for i in range(1,8)] # 0001..0007
|
||||||
|
got = [r['include'].split('/')[-1][:4] for r in lk['resolved_patches']]
|
||||||
|
assert got == ['000'+str(i) for i in range(1,8)], f'apply order mismatch: {got}'
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
check_fresnel_parity() {
|
||||||
|
local scratch="$1" host="$2"
|
||||||
|
local lock="$scratch/build/$host/v7.0/manifest.lock"
|
||||||
|
[ -f "$lock" ] || { note "manifest.lock missing"; return 1; }
|
||||||
|
local b2
|
||||||
|
b2=$(python3 -c "import yaml; print(yaml.safe_load(open('$lock'))['cumulative']['b2sum'])")
|
||||||
|
if [ "$b2" != "$FRESNEL_EXPECTED_B2SUM" ]; then
|
||||||
|
note "b2sum mismatch"
|
||||||
|
note " expected: $FRESNEL_EXPECTED_B2SUM"
|
||||||
|
note " got: $b2"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Running ka-promote test suite from $repo_root"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic.
|
||||||
|
mkdir -p /tmp/ka-promote-parity-fixture
|
||||||
|
cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml
|
||||||
|
run_test "fresnel parity (= Phase-3 ground truth b2sum)" \
|
||||||
|
/tmp/ka-promote-parity-fixture/fresnel.yaml 0 check_fresnel_parity
|
||||||
|
rm -rf /tmp/ka-promote-parity-fixture
|
||||||
|
|
||||||
|
run_test "series-dir resolver (bes2600/staging-prep-series-danctnix → 7 patches)" \
|
||||||
|
"$fixtures/series-dir.yaml" 0 check_series_dir
|
||||||
|
|
||||||
|
run_test "bad-include rejection (exit 4)" \
|
||||||
|
"$fixtures/bad-include.yaml" 4
|
||||||
|
|
||||||
|
run_test "missing-patch rejection (exit 2)" \
|
||||||
|
"$fixtures/missing-patch.yaml" 2
|
||||||
|
|
||||||
|
run_test "duplicate-include rejection (exit 4)" \
|
||||||
|
"$fixtures/duplicate-include.yaml" 4
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===================="
|
||||||
|
printf '%s\n' "${results[@]}"
|
||||||
|
echo "===================="
|
||||||
|
echo "passed: $pass"
|
||||||
|
echo "failed: $fail"
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
Reference in New Issue
Block a user