diff --git a/.github/workflows/ores-cli-zpkg-kind-canary.yml b/.github/workflows/ores-cli-zpkg-kind-canary.yml new file mode 100644 index 0000000..cb7a178 --- /dev/null +++ b/.github/workflows/ores-cli-zpkg-kind-canary.yml @@ -0,0 +1,108 @@ +name: ores-cli Zed package.kind canary + +on: + pull_request: + paths: + - '.github/workflows/ores-cli-zpkg-kind-canary.yml' + - 'certification/ores-cli-zpkg-kind/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ores-zpkg-kind-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + certify: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Check out exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + fetch-depth: 1 + + - name: Verify exact candidate and product lint blob + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" + test "$(git hash-object certification/ores-cli-zpkg-kind/src/audit/zpkg_manifest.rs)" = e78a7636df785fbf4746436325bbbc8b38d46184 + + - name: Install Rust 1.95 + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.95.0 + components: rustfmt,clippy + + - name: Certify exact ores-cli lint + working-directory: certification/ores-cli-zpkg-kind + shell: bash + run: | + set -euo pipefail + cargo +1.95.0 generate-lockfile + cargo +1.95.0 fmt --all -- --check + cargo +1.95.0 check --locked --all-targets + cargo +1.95.0 clippy --locked --all-targets -- -D warnings + cargo +1.95.0 test --locked --all-targets -- --nocapture + cargo +1.95.0 test --release --locked --all-targets + + - name: Install checksum-pinned Zed v0.3.0 + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/zed.tar.gz" + curl --fail --silent --show-error --location --retry 3 \ + https://github.com/zed-pkg/zed-cli/releases/download/v0.3.0/zed-x86_64-unknown-linux-musl.tar.gz \ + -o "$archive" + echo "e380240c96242ab3b29e0e9d14c95ba946d086f20f0732e34d0e5cf47513efc7 $archive" | sha256sum --check + mkdir -p "$RUNNER_TEMP/zed-bin" + tar -xzf "$archive" -C "$RUNNER_TEMP/zed-bin" + echo "$RUNNER_TEMP/zed-bin" >> "$GITHUB_PATH" + + - name: Require Zed to accept canonical shape and reject package.kind + shell: bash + run: | + set -euo pipefail + good="$RUNNER_TEMP/zpkg-good" + bad="$RUNNER_TEMP/zpkg-bad" + mkdir -p "$good" "$bad" + cat > "$good/.zpkg.toml" <<'TOML' + [package] + org = "zed-pkg" + name = "zed-e2e" + version = "0.1.0" + description = "Zed validation fixture" + license = "MIT" + language = "typescript" + + [package.repository] + vcs = "git" + url = "https://github.com/zed-pkg/zed-e2e" + + [install] + adapter = "none" + dir = ".vendor/.zed" + + [targets.repository] + dir = "." + + [scripts] + test = "true" + TOML + cp "$good/.zpkg.toml" "$bad/.zpkg.toml" + sed -i '/version = "0.1.0"/a kind = "pub-lib-core"' "$bad/.zpkg.toml" + (cd "$good" && zed validate) + set +e + (cd "$bad" && zed validate) >"$RUNNER_TEMP/bad.out" 2>&1 + status=$? + set -e + test "$status" -ne 0 + grep -Ei 'kind|unknown|invalid|manifest' "$RUNNER_TEMP/bad.out" + + - name: Prove certificate sources remained immutable + run: git diff --exit-code -- certification/ores-cli-zpkg-kind .github/workflows/ores-cli-zpkg-kind-canary.yml diff --git a/certification/ores-cli-zpkg-kind/Cargo.toml b/certification/ores-cli-zpkg-kind/Cargo.toml new file mode 100644 index 0000000..dfec890 --- /dev/null +++ b/certification/ores-cli-zpkg-kind/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ores-cli-zpkg-kind-certification" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +serde_json = "1" +toml = "0.8" + +[dev-dependencies] +tempfile = "3" diff --git a/certification/ores-cli-zpkg-kind/src/audit/mod.rs b/certification/ores-cli-zpkg-kind/src/audit/mod.rs new file mode 100644 index 0000000..6f99251 --- /dev/null +++ b/certification/ores-cli-zpkg-kind/src/audit/mod.rs @@ -0,0 +1,24 @@ +use std::path::{Path, PathBuf}; + +use crate::model::CommandReport; + +mod zpkg_manifest; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RepositoryAuditOptions { + pub path: PathBuf, + pub profile: String, + pub additional_required_paths: Vec, +} + +#[must_use] +pub fn run(root: &Path) -> CommandReport { + zpkg_manifest::augment_zpkg_manifest_audit( + &RepositoryAuditOptions { + path: root.to_path_buf(), + profile: "baseline".to_owned(), + additional_required_paths: Vec::new(), + }, + CommandReport::new("audit repo"), + ) +} diff --git a/certification/ores-cli-zpkg-kind/src/audit/zpkg_manifest.rs b/certification/ores-cli-zpkg-kind/src/audit/zpkg_manifest.rs new file mode 100644 index 0000000..e78a763 --- /dev/null +++ b/certification/ores-cli-zpkg-kind/src/audit/zpkg_manifest.rs @@ -0,0 +1,154 @@ +use std::fs; + +use serde_json::json; +use toml::Value; + +use super::RepositoryAuditOptions; +use crate::model::{CommandReport, Finding}; + +const MANIFEST: &str = ".zpkg.toml"; + +/// Add conservative Zed manifest checks to repository audit. +/// +/// `zed validate` remains the canonical executable validator. This static pass +/// only rejects fleet mistakes that have already proven harmful and can be +/// identified without reimplementing Zed's schema. In particular, +/// `package.kind` is not part of the canonical Zed package model; package role +/// is expressed by repository naming, targets and other supported metadata. +pub(super) fn augment_zpkg_manifest_audit( + options: &RepositoryAuditOptions, + mut report: CommandReport, +) -> CommandReport { + let path = options.path.join(MANIFEST); + let metadata = match fs::symlink_metadata(&path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return report.finalize(), + Err(error) => { + report.push( + Finding::warning( + "zpkg-manifest-unreadable", + format!("could not inspect {MANIFEST}: {error}"), + ) + .with_target(MANIFEST), + ); + return report.finalize(); + } + }; + + if metadata.file_type().is_symlink() || !metadata.is_file() { + report.push( + Finding::error( + "zpkg-manifest-not-regular", + ".zpkg.toml must be a repository-owned regular file", + ) + .with_target(MANIFEST), + ); + return report.finalize(); + } + + let source = match fs::read_to_string(&path) { + Ok(source) => source, + Err(error) => { + report.push( + Finding::error( + "zpkg-manifest-unreadable", + format!("could not read {MANIFEST} as UTF-8: {error}"), + ) + .with_target(MANIFEST), + ); + return report.finalize(); + } + }; + let document = match source.parse::() { + Ok(document) => document, + Err(error) => { + report.push( + Finding::error( + "zpkg-manifest-invalid", + format!("could not parse {MANIFEST}: {error}"), + ) + .with_target(MANIFEST), + ); + return report.finalize(); + } + }; + + report.insert_metadata("zpkgManifestInspected", json!(true)); + + if document + .get("package") + .and_then(Value::as_table) + .is_some_and(|package| package.contains_key("kind")) + { + report.push( + Finding::error( + "zpkg-package-kind-noncanonical", + "`package.kind` is not canonical Zed manifest metadata; express package role through supported targets/naming and require `zed validate` for full admission", + ) + .with_target(MANIFEST) + .with_detail("field", json!("package.kind")), + ); + } + + report.finalize() +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::augment_zpkg_manifest_audit; + use crate::audit::RepositoryAuditOptions; + use crate::model::CommandReport; + + fn audit(source: &str) -> CommandReport { + let root = tempdir().expect("temporary repository"); + fs::write(root.path().join(".zpkg.toml"), source).expect("manifest"); + augment_zpkg_manifest_audit( + &RepositoryAuditOptions { + path: root.path().to_path_buf(), + profile: "baseline".to_owned(), + additional_required_paths: Vec::new(), + }, + CommandReport::new("audit repo"), + ) + } + + #[test] + fn rejects_noncanonical_package_kind() { + let report = audit( + r#"[package] +org = "oresoftware" +name = "ores-dnd" +version = "0.1.0" +kind = "pub-lib-core" +"#, + ); + assert!(report + .findings + .iter() + .any(|finding| finding.code == "zpkg-package-kind-noncanonical")); + } + + #[test] + fn accepts_supported_package_shape_without_kind() { + let report = audit( + r#"[package] +org = "oresoftware" +name = "ores-dnd" +version = "0.1.0" +description = "cross-runtime drag and drop" + +[targets.rust] +dir = "src/rust" +adapter = "rust" +"#, + ); + assert!(!report + .findings + .iter() + .any(|finding| finding.code == "zpkg-package-kind-noncanonical")); + } +} diff --git a/certification/ores-cli-zpkg-kind/src/lib.rs b/certification/ores-cli-zpkg-kind/src/lib.rs new file mode 100644 index 0000000..cb9a880 --- /dev/null +++ b/certification/ores-cli-zpkg-kind/src/lib.rs @@ -0,0 +1,2 @@ +pub mod audit; +pub mod model; diff --git a/certification/ores-cli-zpkg-kind/src/model.rs b/certification/ores-cli-zpkg-kind/src/model.rs new file mode 100644 index 0000000..d012892 --- /dev/null +++ b/certification/ores-cli-zpkg-kind/src/model.rs @@ -0,0 +1,86 @@ +use std::collections::BTreeMap; + +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct Finding { + pub code: String, + pub message: String, + pub severity: Severity, + pub target: Option, + pub details: BTreeMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Error, + Warning, +} + +impl Finding { + pub fn error(code: impl Into, message: impl Into) -> Self { + Self::new(code, message, Severity::Error) + } + + pub fn warning(code: impl Into, message: impl Into) -> Self { + Self::new(code, message, Severity::Warning) + } + + fn new(code: impl Into, message: impl Into, severity: Severity) -> Self { + Self { + code: code.into(), + message: message.into(), + severity, + target: None, + details: BTreeMap::new(), + } + } + + pub fn with_target(mut self, target: impl Into) -> Self { + self.target = Some(target.into()); + self + } + + pub fn with_detail(mut self, key: impl Into, value: Value) -> Self { + self.details.insert(key.into(), value); + self + } +} + +#[derive(Debug, Clone)] +pub struct CommandReport { + pub command: String, + pub findings: Vec, + pub metadata: BTreeMap, +} + +impl CommandReport { + pub fn new(command: impl Into) -> Self { + Self { + command: command.into(), + findings: Vec::new(), + metadata: BTreeMap::new(), + } + } + + pub fn push(&mut self, finding: Finding) { + self.findings.push(finding); + } + + pub fn insert_metadata(&mut self, key: impl Into, value: Value) { + self.metadata.insert(key.into(), value); + } + + #[must_use] + pub fn finalize(self) -> Self { + self + } + + #[must_use] + pub fn issue_count(&self) -> usize { + self.findings + .iter() + .filter(|finding| finding.severity == Severity::Error) + .count() + } +} diff --git a/certification/ores-cli-zpkg-kind/tests/external.rs b/certification/ores-cli-zpkg-kind/tests/external.rs new file mode 100644 index 0000000..c66bf7b --- /dev/null +++ b/certification/ores-cli-zpkg-kind/tests/external.rs @@ -0,0 +1,46 @@ +use std::fs; +use std::path::Path; + +use ores_cli_zpkg_kind_certification::audit::run; +use tempfile::tempdir; + +const GOOD: &str = r#"[package] +org = "zed-pkg" +name = "zed-e2e" +version = "0.1.0" +description = "Zed validation fixture" +license = "MIT" +language = "typescript" + +[package.repository] +vcs = "git" +url = "https://github.com/zed-pkg/zed-e2e" + +[install] +adapter = "none" +dir = ".vendor/.zed" + +[targets.repository] +dir = "." + +[scripts] +test = "true" +"#; + +fn write(root: &Path, body: &str) { + fs::write(root.join(".zpkg.toml"), body).expect("manifest"); +} + +#[test] +fn exact_lint_accepts_good_and_rejects_package_kind() { + let good = tempdir().expect("good repo"); + write(good.path(), GOOD); + let report = run(good.path()); + assert_eq!(report.issue_count(), 0, "{:#?}", report.findings); + + let bad = tempdir().expect("bad repo"); + let bad_manifest = GOOD.replace("version = \"0.1.0\"", "version = \"0.1.0\"\nkind = \"pub-lib-core\""); + write(bad.path(), &bad_manifest); + let report = run(bad.path()); + assert!(report.findings.iter().any(|finding| finding.code == "zpkg-package-kind-noncanonical")); +}