#!/usr/bin/env rust-script
use std::fs;
use std::process::exit;
fn main() {
let workflow = fs::read_to_string(".github/workflows/release.yml")
.expect("failed to read .github/workflows/release.yml");
let reconciliation = fs::read_to_string(".github/workflows/verify-releases.yml")
.expect("failed to read .github/workflows/verify-releases.yml");
let required_snippets = [
"auto-release:",
"manual-release:",
"publish-docker-images:",
"publish-docker-manifests:",
"packages: write",
"CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}",
"DOCKERHUB_IMAGE: konard/link-assistant-router",
"rust-script scripts/wait-for-crate.rs",
"cargo install rust-script --version 0.36.0 --locked",
"cargo install cargo-audit --version 0.22.2 --locked --force",
"cargo install cargo-cyclonedx --version 0.5.9 --locked",
"cargo cyclonedx --format json --all-features --all --spec-version 1.5",
"--override-filename link-assistant-router.cdx",
"artifact-metadata: write",
"actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131",
"cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
"npm run build 2>&1 | tee /tmp/admin-ui-build.log",
"git diff --exit-code -- ui/dist",
"toolchain: 1.97.1",
"docker/login-action@",
"docker/setup-buildx-action@",
"docker/metadata-action@",
"docker/build-push-action@",
"actions/attest-build-provenance@",
"publish-release-artifacts:",
"subject-path: dist/*",
"provenance: mode=max",
"sbom: true",
"gh release upload",
"gh attestation verify",
"Verify tag and package version",
"gh release view \"v${RELEASE_VERSION}\"",
"platform: linux/amd64",
"platform: linux/arm64",
"runner: ubuntu-24.04-arm",
"runs-on: ${{ matrix.runner }}",
"platforms: ${{ matrix.platform }}",
"push-by-digest=true",
"cache-from: type=gha",
"cache-to: type=gha,mode=max",
"docker buildx imagetools create",
"rust-script scripts/check-docker-platforms.rs",
"bash scripts/verify-ghcr-visibility.sh",
"GHCR_IMAGE: ghcr.io/link-assistant/router",
"org.opencontainers.image.source=https://github.com/link-assistant/router",
"username: konard",
"password: ${{ secrets.DOCKERHUB_TOKEN }}",
"ghcr.io/${{ github.repository }}",
"${{ env.DOCKERHUB_IMAGE }}",
"labels: ${{ steps.image-labels.outputs.labels }}",
"grep -v '^org.opencontainers.image.revision='",
"needs: [create-github-release]",
"ref: refs/tags/v${{ env.RELEASE_VERSION }}",
"release-commit: ${{ steps.tag-commit.outputs.commit }}",
"ref: ${{ needs.create-github-release.outputs.release-commit }}",
"org.opencontainers.image.revision=${RELEASE_COMMIT}",
"rust-script scripts/check-release-provenance.rs",
"verify-release-provenance:",
"--crates-io-url \"https://crates.io/crates/link-assistant-router\"",
"--docker-hub-url \"https://hub.docker.com/r/konard/link-assistant-router\"",
"rust-script scripts/check-github-releases.rs --repository \"${{ github.repository }}\" --default-branch main",
];
let mut failures = Vec::new();
for snippet in required_snippets {
if !workflow.contains(snippet) {
failures.push(format!(
"missing required release workflow snippet: {snippet}"
));
}
}
if count_occurrences(&workflow, "packages: write") < 2 {
failures.push("Docker build and manifest jobs must grant packages: write".to_string());
}
if count_occurrences(&workflow, "docker/login-action@") < 4 {
failures.push(
"Docker build and manifest jobs must both log in to GHCR and Docker Hub".to_string(),
);
}
if count_occurrences(&workflow, "docker/build-push-action@") != 1 {
failures.push("the variant-and-architecture matrix must use one shared build step".to_string());
}
if workflow.contains("docker/setup-qemu-action") {
failures.push("release images must not use QEMU emulation".to_string());
}
for line in workflow
.lines()
.chain(reconciliation.lines())
.filter(|line| line.trim_start().starts_with("uses:"))
{
let revision = line
.split_once('@')
.map(|(_, revision)| revision.split_whitespace().next().unwrap_or_default())
.unwrap_or_default();
if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) {
failures.push(format!("workflow action is not pinned to an immutable commit: {line}"));
}
}
let toolchain_actions = count_occurrences(&workflow, "dtolnay/rust-toolchain@")
+ count_occurrences(&reconciliation, "dtolnay/rust-toolchain@");
let pinned_toolchains = count_occurrences(&workflow, "toolchain: 1.97.1")
+ count_occurrences(&reconciliation, "toolchain: 1.97.1");
if toolchain_actions != pinned_toolchains {
failures.push("every Rust action must select the reviewed numeric toolchain".to_string());
}
if count_occurrences(&workflow, "ref: refs/tags/v${{ env.RELEASE_VERSION }}") != 1 {
failures.push(
"packaging jobs must check out the resolved release commit, not the tag ref"
.to_string(),
);
}
if count_occurrences(&workflow, "ref: ${{ needs.create-github-release.outputs.release-commit }}") < 4 {
failures.push(
"every image, binary, and verification job must check out the release tag commit"
.to_string(),
);
}
if workflow.contains("sha256sum dist/*") || workflow.contains("shasum -a 256 dist/*") {
failures.push(
"checksum files must be generated from inside dist/ so they list flat names"
.to_string(),
);
}
if count_occurrences(&workflow, "sha256sum -c") < 2 {
failures.push(
"packaging and the post-publication guard must both verify checksums the way a consumer does"
.to_string(),
);
}
if !reconciliation.contains("rust-script scripts/check-release-provenance.rs") {
failures.push(
"the scheduled reconciliation must re-verify published release provenance".to_string(),
);
}
if count_occurrences(&workflow, "push-by-digest=true") != 1 {
failures.push(
"every matrix leg must publish its native image by digest".to_string(),
);
}
if count_occurrences(&workflow, "docker buildx imagetools create") != 2 {
failures.push(
"both registries must receive one merged runtime manifest".to_string(),
);
}
if count_occurrences(
&workflow,
"rust-script scripts/check-docker-platforms.rs",
) != 1
{
failures.push(
"the shared manifest job must verify every published multi-platform image".to_string(),
);
}
if count_occurrences(&workflow, "bash scripts/verify-ghcr-visibility.sh") != 1 {
failures.push(
"the shared manifest job must verify anonymous GHCR visibility once".to_string(),
);
}
if workflow.contains("docker-build:")
|| workflow.contains("needs: [lint, test, build, docker-build]")
{
failures.push("release publication must not wait for a disposable Docker build".to_string());
}
if !workflow.contains("create-github-release:\n name: Create GitHub Release\n needs: [auto-release, manual-release]") {
failures.push("GitHub releases must be created immediately after crate publication".to_string());
}
if !workflow.contains("rust-script --test scripts/check-github-releases.rs") {
failures.push("CI must test GitHub release reconciliation logic".to_string());
}
if count_occurrences(
&workflow,
"CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}",
) < 3
{
failures.push(
"workflow must map both CARGO_REGISTRY_TOKEN and CARGO_TOKEN secrets to Cargo's native CARGO_REGISTRY_TOKEN env var"
.to_string(),
);
}
if count_occurrences(
&workflow,
"--crates-io-url \"https://crates.io/crates/link-assistant-router\"",
) != 1
{
failures.push(
"the shared GitHub release must include the crates.io release badge/link".to_string(),
);
}
if count_occurrences(
&workflow,
"--docker-hub-url \"https://hub.docker.com/r/konard/link-assistant-router\"",
) != 1
{
failures.push(
"the shared GitHub release must include the Docker Hub release badge/link".to_string(),
);
}
if failures.is_empty() {
println!("release workflow builds native images and publishes public, verified multi-platform manifests");
} else {
for failure in failures {
eprintln!("Error: {failure}");
}
exit(1);
}
}
fn count_occurrences(haystack: &str, needle: &str) -> usize {
haystack.match_indices(needle).count()
}