use serde::Serialize;
use crate::embedded;
#[derive(Debug, Clone, Serialize)]
pub struct InvariantFailure {
pub code: &'static str,
pub destination: String,
pub reason: String,
pub remediation: &'static str,
}
impl InvariantFailure {
fn new(
code: &'static str,
destination: &str,
reason: impl Into<String>,
remediation: &'static str,
) -> Self {
Self {
code,
destination: destination.to_owned(),
reason: reason.into(),
remediation,
}
}
}
#[must_use]
pub fn failures(tech: &str, forge: &str, destination: &str, bytes: &[u8]) -> Vec<InvariantFailure> {
match (tech, forge, destination) {
("rust", "github", "dist-workspace.toml") => dist_workspace(destination, bytes),
_ => Vec::new(),
}
}
fn dist_workspace(destination: &str, bytes: &[u8]) -> Vec<InvariantFailure> {
let Ok(text) = std::str::from_utf8(bytes) else {
return vec![InvariantFailure::new(
"unparsable-configuration",
destination,
"the file is not UTF-8, so its configuration cannot be judged",
"repair the file so it parses as TOML",
)];
};
let table: toml::Table = match text.parse() {
Ok(table) => table,
Err(error) => {
return vec![InvariantFailure::new(
"unparsable-configuration",
destination,
format!("the file does not parse as TOML: {error}"),
"repair the file so it parses as TOML",
)];
}
};
let dist = table.get("dist").and_then(toml::Value::as_table);
let mut failures = Vec::new();
let value = |key: &str| dist.and_then(|dist| dist.get(key));
if value("github-attestations").and_then(toml::Value::as_bool) != Some(true) {
failures.push(InvariantFailure::new(
"attestations-disabled",
destination,
"github-attestations is not effectively true, so no release artifact is attested",
"set github-attestations = true in [dist]",
));
}
let phase = value("github-attestations-phase").and_then(toml::Value::as_str);
if phase != Some("host") {
failures.push(InvariantFailure::new(
"attestation-phase-not-host",
destination,
phase.map_or_else(
|| "github-attestations-phase is unset, so the default phase attests only the per-platform archives and the curled installers ship unattested".to_owned(),
|other| format!(
"github-attestations-phase is \"{other}\"; only the host phase attests every asset before the release page exists"
),
),
"set github-attestations-phase = \"host\" in [dist]",
));
}
if value("github-release").and_then(toml::Value::as_str) != Some("host") {
failures.push(InvariantFailure::new(
"release-phase-unpaired",
destination,
"github-release is not \"host\", leaving the release creation unpaired with the attest phase",
"set github-release = \"host\" in [dist], pairing the release creation with the phase that attests",
));
}
if value("github-attestations-filters").is_some() {
failures.push(InvariantFailure::new(
"attestation-filters-narrowed",
destination,
"github-attestations-filters narrows what is attested below the whole release payload",
"remove github-attestations-filters from [dist]; the default [\"*\"] attests every hosted file",
));
}
let expected = seed_action_commits();
let found = value("github-action-commits").and_then(toml::Value::as_table);
for (action, commit) in &expected {
let remediation = "bring the [dist.github-action-commits] table to the payload seed's (rk snippet rust/github/dist-workspace.toml) and regenerate with dist generate --mode ci";
match found.and_then(|table| table.get(action)) {
Some(value) => match value.as_str() {
Some(pinned) if pinned == commit.as_str() => {}
Some(pinned) => failures.push(InvariantFailure::new(
"action-commit-stale",
destination,
format!(
"[dist.github-action-commits] pins {action} at {pinned}, where the payload pins {commit}"
),
remediation,
)),
None => failures.push(InvariantFailure::new(
"action-commit-invalid",
destination,
format!(
"[dist.github-action-commits] pins {action} with a non-string value; a pin is a full commit SHA string"
),
remediation,
)),
},
None => failures.push(InvariantFailure::new(
"action-commit-missing",
destination,
format!(
"[dist.github-action-commits] does not pin {action}, so the workflow runs whatever the movable tag names"
),
remediation,
)),
}
}
failures
}
fn seed_action_commits() -> Vec<(String, String)> {
let Some(text) = embedded::SNIPPETS
.get_file("rust/github/dist-workspace.toml")
.and_then(|file| file.contents_utf8())
else {
return Vec::new();
};
let Ok(table) = text.parse::<toml::Table>() else {
return Vec::new();
};
table
.get("dist")
.and_then(toml::Value::as_table)
.and_then(|dist| dist.get("github-action-commits"))
.and_then(toml::Value::as_table)
.map(|commits| {
commits
.iter()
.filter_map(|(action, commit)| {
commit
.as_str()
.map(|commit| (action.clone(), commit.to_owned()))
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::failures;
const CLEAN: &str = r#"
[dist]
github-attestations = true
github-attestations-phase = "host"
github-release = "host"
[dist.github-action-commits]
"actions/checkout" = "d23441a48e516b6c34aea4fa41551a30e30af803"
"actions/download-artifact" = "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"
"actions/upload-artifact" = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"
"actions/attest" = "1e69f48acb82d1966a394da916b4c1698aa569d6"
"#;
#[test]
fn the_seeded_configuration_is_judged_effectively() {
assert!(failures("rust", "github", "dist-workspace.toml", CLEAN.as_bytes()).is_empty());
let seed = crate::embedded::SNIPPETS
.get_file("rust/github/dist-workspace.toml")
.and_then(|file| file.contents_utf8())
.expect("the seed is embedded");
assert!(
failures("rust", "github", "dist-workspace.toml", seed.as_bytes()).is_empty(),
"the payload's own seed satisfies the invariants it seeds"
);
}
#[test]
fn a_missing_or_stale_action_commit_table_fails() {
let missing = "[dist]\ngithub-attestations=true\ngithub-attestations-phase='host'\ngithub-release='host'\n";
let found = failures("rust", "github", "dist-workspace.toml", missing.as_bytes());
assert!(
found
.iter()
.any(|failure| failure.code == "action-commit-missing"),
"a missing entry falls back to the movable tag: {found:?}"
);
let stale = CLEAN.replace(
"d23441a48e516b6c34aea4fa41551a30e30af803",
"0000000000000000000000000000000000000000",
);
let found = failures("rust", "github", "dist-workspace.toml", stale.as_bytes());
assert!(
found
.iter()
.any(|failure| failure.code == "action-commit-stale"
&& failure.reason.contains("actions/checkout")
&& failure
.reason
.contains("0000000000000000000000000000000000000000")),
"a mismatch names the found and expected commits: {found:?}"
);
let invalid = CLEAN.replace("\"d23441a48e516b6c34aea4fa41551a30e30af803\"", "123");
let found_invalid = failures("rust", "github", "dist-workspace.toml", invalid.as_bytes());
assert!(
found_invalid
.iter()
.any(|failure| failure.code == "action-commit-invalid"
&& failure.reason.contains("actions/checkout")),
"a non-string value is invalid configuration, not an absent pin: {found_invalid:?}"
);
assert!(
!found
.iter()
.any(|failure| failure.reason.contains("actions/attest")),
"only the stale action is named: {found:?}"
);
}
#[test]
fn each_degraded_form_fails_with_its_code() {
let cases: &[(&str, &str)] = &[
(
"[dist]\n# github-attestations = true\ngithub-attestations-phase='host'\ngithub-release='host'\n",
"attestations-disabled",
),
(
"[dist]\ngithub-attestations = false\ngithub-attestations-phase='host'\ngithub-release='host'\n",
"attestations-disabled",
),
(
"[dist]\ngithub-attestations = true\ngithub-release='host'\n",
"attestation-phase-not-host",
),
(
"[dist]\ngithub-attestations = true\ngithub-attestations-phase='build-local-artifacts'\ngithub-release='host'\n",
"attestation-phase-not-host",
),
(
"[dist]\ngithub-attestations = true\ngithub-attestations-phase='host'\ngithub-release='announce'\n",
"release-phase-unpaired",
),
(
"[dist]\ngithub-attestations = true\ngithub-attestations-phase='host'\ngithub-release='host'\ngithub-attestations-filters=['*.tar.gz']\n",
"attestation-filters-narrowed",
),
("not toml at [all", "unparsable-configuration"),
];
for (text, code) in cases {
let found = failures("rust", "github", "dist-workspace.toml", text.as_bytes());
assert!(
found.iter().any(|failure| failure.code == *code),
"{text:?} must fail with {code}, got {found:?}"
);
}
}
#[test]
fn the_rule_is_keyed_by_pair_and_destination() {
let broken = b"[dist]\ngithub-attestations = false\n";
assert!(failures("rust", "gitlab", "dist-workspace.toml", broken).is_empty());
assert!(failures("bash", "github", "dist-workspace.toml", broken).is_empty());
assert!(failures("rust", "github", "release-plz.toml", broken).is_empty());
}
}