archlinux/archlinux only publishes linux/amd64, so the native aarch64 Arch lane failed with 'exec format error' before provisioning. Both Arch Dockerfiles now take ARCH_BASE_IMAGE; the aarch64 matrix entry pins ghcr.io/menci/archlinuxarm by digest and x86_64 keeps the official image. package-validate.sh forwards ARCH_BASE_IMAGE to the systemd substrate.
260 lines
10 KiB
Python
Executable file
260 lines
10 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Static and scenario checks for immutable experimental release sources."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Scenario:
|
|
event: str
|
|
channel: str
|
|
event_sha: str
|
|
experimental_sha: str
|
|
expected_sha: str
|
|
expected_channel: str
|
|
|
|
|
|
def resolve_source(scenario: Scenario) -> tuple[str, str]:
|
|
channel = scenario.channel or "nightly"
|
|
use_experimental = scenario.event == "schedule" or (
|
|
scenario.event == "workflow_dispatch" and channel != "stable"
|
|
)
|
|
source_sha = scenario.experimental_sha if use_experimental else scenario.event_sha
|
|
effective_channel = (
|
|
"stable"
|
|
if scenario.event == "push"
|
|
else channel
|
|
if channel in {"stable", "weekly", "nightly"}
|
|
else "nightly"
|
|
)
|
|
return source_sha, effective_channel
|
|
|
|
|
|
def nightly_tag(date: str, source_sha: str) -> str:
|
|
if re.fullmatch(r"[0-9a-f]{40}", source_sha) is None:
|
|
raise ValueError("nightly tags require a full source SHA")
|
|
if re.fullmatch(r"[0-9]{8}", date) is None:
|
|
raise ValueError("nightly tags require a compact UTC date")
|
|
return f"experimental-nightly-{date}-{source_sha}"
|
|
|
|
|
|
def scenario_failures() -> list[str]:
|
|
cases = (
|
|
Scenario("schedule", "", "main-sha", "exp-sha", "exp-sha", "nightly"),
|
|
Scenario(
|
|
"workflow_dispatch",
|
|
"nightly",
|
|
"selected-sha",
|
|
"exp-sha",
|
|
"exp-sha",
|
|
"nightly",
|
|
),
|
|
Scenario(
|
|
"workflow_dispatch",
|
|
"",
|
|
"selected-sha",
|
|
"exp-sha",
|
|
"exp-sha",
|
|
"nightly",
|
|
),
|
|
Scenario(
|
|
"workflow_dispatch",
|
|
"weekly",
|
|
"selected-sha",
|
|
"exp-sha",
|
|
"exp-sha",
|
|
"weekly",
|
|
),
|
|
Scenario(
|
|
"workflow_dispatch", "stable", "tag-sha", "exp-sha", "tag-sha", "stable"
|
|
),
|
|
Scenario("push", "", "tag-sha", "exp-sha", "tag-sha", "stable"),
|
|
# Divergent history is intentionally reduced to exact SHA inequality; ancestry is irrelevant.
|
|
Scenario(
|
|
"schedule",
|
|
"",
|
|
"divergent-main",
|
|
"divergent-exp",
|
|
"divergent-exp",
|
|
"nightly",
|
|
),
|
|
)
|
|
failures = []
|
|
for case in cases:
|
|
actual = resolve_source(case)
|
|
expected = (case.expected_sha, case.expected_channel)
|
|
if actual != expected:
|
|
failures.append(
|
|
f"scenario failed: {case}: expected {expected}, got {actual}"
|
|
)
|
|
|
|
def should_release(
|
|
last_sha: str, current_sha: str, compare_available: bool
|
|
) -> bool:
|
|
del (
|
|
compare_available
|
|
) # Comparison enriches logs; SHA inequality is authoritative.
|
|
return last_sha != current_sha
|
|
|
|
if not should_release("divergent-last", "divergent-exp", compare_available=False):
|
|
failures.append("divergent histories must release when exact SHAs differ")
|
|
if should_release("same", "same", compare_available=False):
|
|
failures.append("equal release SHAs must not publish again")
|
|
|
|
source_sha = "a" * 40
|
|
if nightly_tag("20260101", source_sha) != (
|
|
"experimental-nightly-20260101-" + source_sha
|
|
):
|
|
failures.append("nightly tag is not immutable")
|
|
try:
|
|
nightly_tag("20260101", "aaaaaaa")
|
|
failures.append("short SHA was accepted for an immutable nightly tag")
|
|
except ValueError:
|
|
pass
|
|
return failures
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, default=Path.cwd())
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
|
|
warm = (root / ".github/workflows/matrix-warm.yml").read_text(encoding="utf-8")
|
|
|
|
failures = scenario_failures()
|
|
required_release = (
|
|
"name: Resolve immutable release source",
|
|
"ref: 'heads/experimental'",
|
|
"needs: [resolve-release, check-commits]",
|
|
"needs: [resolve-release, warm]",
|
|
"needs: [resolve-release, check-commits, build-release]",
|
|
"ref: ${{ needs.resolve-release.outputs.source_sha }}",
|
|
"target_commitish: ${{ needs.resolve-release.outputs.source_sha }}",
|
|
"group: release-publish-${{ needs.resolve-release.outputs.channel }}-${{ needs.resolve-release.outputs.source_sha }}",
|
|
"cancel-in-progress: false",
|
|
"if (lastSha !== currentSha)",
|
|
"comparison was unavailable",
|
|
'TAG_OUT="experimental-nightly-${DATE_COMPACT}-${SOURCE_SHA}"',
|
|
"immutableNightly = /^experimental-nightly-[0-9]{8}-[0-9a-f]{40}$/",
|
|
"Nightly tag ${tagName} did not resolve safely to a commit",
|
|
"name: Require successful Tests run for exact experimental source",
|
|
"workflow_id: 'tests.yml'",
|
|
"run.status === 'completed'",
|
|
"run.conclusion === 'success'",
|
|
"run.head_sha === sourceSha",
|
|
"tests_run_id: ${{ steps.tests.outputs.tests_run_id }}",
|
|
"tests_run_attempt: ${{ steps.tests.outputs.tests_run_attempt }}",
|
|
"tests_run_url: ${{ steps.tests.outputs.tests_run_url }}",
|
|
"runs_on: ubuntu-24.04-arm",
|
|
"arch_name: x86_64",
|
|
"arch_name: aarch64",
|
|
"docker_platform: linux/amd64",
|
|
"docker_platform: linux/arm64",
|
|
"arch_base_image: archlinux/archlinux:latest",
|
|
"arch_base_image: ghcr.io/menci/archlinuxarm@sha256:",
|
|
'--build-arg "ARCH_BASE_IMAGE=${{ matrix.arch_base_image }}"',
|
|
"ARCH_BASE_IMAGE: ${{ matrix.arch_base_image }}",
|
|
"name: Inspect and install-validate Arch package natively",
|
|
'bash scripts/local-ci/package-validate.sh --only arch --arch "$ARCH_PKG_PATH"',
|
|
"name: Generate and validate release manifest (latest.json)",
|
|
"scripts/ci/generate_release_manifest.py",
|
|
'--source-sha "${{ needs.resolve-release.outputs.source_sha }}"',
|
|
'--tests-run-id "${{ needs.resolve-release.outputs.tests_run_id }}"',
|
|
'--tests-run-attempt "${{ needs.resolve-release.outputs.tests_run_attempt }}"',
|
|
"subject-checksums: assets/SHA256SUMS",
|
|
"uses: actions/attest@11bbd243972067817e9ed160cb123cab3601f436",
|
|
"predicate-type: https://yamsmemory.ai/attestations/release-source/v1",
|
|
"predicate-path: release-provenance.json",
|
|
"id-token: write",
|
|
"attestations: write",
|
|
)
|
|
required_warm = (
|
|
"checkout_ref:",
|
|
"ref: ${{ inputs.checkout_ref || github.sha }}",
|
|
)
|
|
failures.extend(
|
|
f"release workflow missing control: {value}"
|
|
for value in required_release
|
|
if value not in release
|
|
)
|
|
failures.extend(
|
|
f"warm workflow missing control: {value}"
|
|
for value in required_warm
|
|
if value not in warm
|
|
)
|
|
|
|
if release.count("ref: 'heads/experimental'") != 1:
|
|
failures.append("experimental branch must be resolved exactly once")
|
|
if "ref: 'heads/main'" in release:
|
|
failures.append("nightly freshness must not resolve main")
|
|
if release.count("DATE_COMPACT=$(date -u +%Y%m%d)") != 1:
|
|
failures.append("nightly metadata must be derived exactly once")
|
|
if (
|
|
release.count('TAG_OUT="experimental-nightly-${DATE_COMPACT}-${SOURCE_SHA}"')
|
|
!= 1
|
|
):
|
|
failures.append("nightly tag must be derived exactly once from the full SHA")
|
|
if release.count("YEAR=$(date -u +%G)") != 1:
|
|
failures.append("weekly metadata must be derived exactly once")
|
|
if release.count('VERSION="${TAG#yams-v}"') != 1:
|
|
failures.append("stable metadata must be derived exactly once")
|
|
if release.count('BASE_NUMERIC_VERSION="$(git describe') != 1:
|
|
failures.append("base version must be derived exactly once")
|
|
if "github.event.workflow_run.head_sha || github.ref" in release:
|
|
failures.append("release checkout still permits mutable ref drift")
|
|
|
|
arch_x86 = release.find("- os: arch-linux-hosted-x86_64")
|
|
arch_arm = release.find("- os: arch-linux-hosted-aarch64")
|
|
upload = release.find("- name: Upload validated release artifacts")
|
|
arch_validation = release.find(
|
|
"- name: Inspect and install-validate Arch package natively"
|
|
)
|
|
linux_validation = release.find("- name: Validate Linux package install")
|
|
if min(arch_x86, arch_arm, arch_validation, linux_validation, upload) < 0:
|
|
failures.append("release package lane ordering controls are incomplete")
|
|
elif not (arch_x86 < arch_arm < arch_validation < linux_validation < upload):
|
|
failures.append("package validation must complete before artifact upload")
|
|
|
|
manifest = release.find("- name: Generate and validate release manifest")
|
|
attestation = release.find(
|
|
"- name: Attest validated release distributables to exact source"
|
|
)
|
|
draft_release = release.find("- name: Create or update GitHub Release")
|
|
publish_release = release.find("- name: Publish GitHub Release")
|
|
if min(manifest, attestation, draft_release, publish_release) < 0:
|
|
failures.append("manifest, attestation, or release publication step missing")
|
|
elif not (manifest < attestation < draft_release < publish_release):
|
|
failures.append(
|
|
"attestation must follow manifest and precede release publication"
|
|
)
|
|
|
|
create_release = release[release.find(" create-release:") :]
|
|
if create_release.count("id-token: write") != 1:
|
|
failures.append("create-release must own the only id-token:write grant")
|
|
if release[: release.find(" create-release:")].count("id-token: write") != 0:
|
|
failures.append("id-token:write must not be granted outside create-release")
|
|
if re.search(r"actions/attest(?:-build-provenance)?@(?:v|main|master)", release):
|
|
failures.append("provenance action must be pinned to a full commit SHA")
|
|
if "actions/attest-build-provenance@" in release:
|
|
failures.append(
|
|
"default event-SHA provenance must not describe experimental assets"
|
|
)
|
|
if re.search(r"\bqemu\b|emulat", release, re.IGNORECASE):
|
|
failures.append("Arch lanes must not claim or configure emulation")
|
|
|
|
if failures:
|
|
print("\n".join(failures))
|
|
return 1
|
|
|
|
print("experimental release source policy: clean")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|