From a9f68757fc01bbac450692a60016880cb4750437 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:02:56 +1000 Subject: [PATCH 1/6] ci: add crate skeleton, cargo-deny and admin API gates --- .github/workflows/ci.yml | 79 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + Cargo.lock | 7 ++++ Cargo.toml | 11 ++++++ deny.toml | 14 +++++++ src/main.rs | 3 ++ tests/cli.rs | 14 +++++++ 7 files changed, 129 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 deny.toml create mode 100644 src/main.rs create mode 100644 tests/cli.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3af7c7b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + # This CLI calls user-scoped endpoints only. Any reference to the + # platform-admin API in the source or the built binary fails the build. + ADMIN_API_PATTERN: "/api/admin|platform-admins|flagship" + +jobs: + checks: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: No admin API references in the source + run: | + # git grep exits 0 on a match, 1 on none, and anything else on error. + # This file holds the pattern, so it is the one path excluded. + rc=0 + git grep --text -nE "$ADMIN_API_PATTERN" -- ':!.github/workflows/ci.yml' || rc=$? + if [ "$rc" -eq 0 ]; then + echo "::error::admin API reference found in the source" + exit 1 + elif [ "$rc" -ne 1 ]; then + echo "::error::git grep failed with exit code $rc" + exit "$rc" + fi + + - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 + + - name: Format + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --locked --all-targets -- -D warnings + + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Test + run: cargo test --locked + + - name: Build release binary + run: cargo build --locked --release + + - name: No admin API references in the binary + run: | + # grep exits 0 on a match, 1 on none, and 2 on error (a missing binary included). + rc=0 + grep -aoE "$ADMIN_API_PATTERN" target/release/cachekit-cli || rc=$? + if [ "$rc" -eq 0 ]; then + echo "::error::admin API reference found in the release binary" + exit 1 + elif [ "$rc" -ne 1 ]; then + echo "::error::grep failed with exit code $rc" + exit "$rc" + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f1343ed --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cachekit-cli" +version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1351785 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cachekit-cli" +version = "0.0.0" +edition = "2021" +rust-version = "1.85" +description = "Command-line client for CacheKit" +license = "MIT" +repository = "https://github.com/cachekit-io/cachekit-cli" + +[lints.rust] +unsafe_code = "forbid" diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..f10551b --- /dev/null +++ b/deny.toml @@ -0,0 +1,14 @@ +# Dependencies come from crates.io and nowhere else. A path dependency has no +# version, so `wildcards = "deny"` rejects one that points outside this crate. + +[licenses] +allow = ["MIT", "Apache-2.0"] + +[bans] +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f0f2d5b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cachekit-cli {}", env!("CARGO_PKG_VERSION")); +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..9983161 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,14 @@ +use std::process::Command; + +#[test] +fn binary_runs_and_prints_its_version() { + let out = Command::new(env!("CARGO_BIN_EXE_cachekit-cli")) + .output() + .expect("failed to spawn the binary"); + assert!(out.status.success()); + let stdout = String::from_utf8(out.stdout).expect("stdout is not UTF-8"); + assert_eq!( + stdout.trim(), + format!("cachekit-cli {}", env!("CARGO_PKG_VERSION")) + ); +} From 3384ed1de5b53f7a316905182f9706cb7797f2ae Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:04:19 +1000 Subject: [PATCH 2/6] test: deliberate admin API reference to prove the CI gate fails --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f0f2d5b..d0d5d8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,3 @@ fn main() { - println!("cachekit-cli {}", env!("CARGO_PKG_VERSION")); + println!("cachekit-cli {} {}", env!("CARGO_PKG_VERSION"), "/api/admin"); } From bc494323c96959a4f750a47898423d16d442c000 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:05:17 +1000 Subject: [PATCH 3/6] test: keep the deliberate hit in the binary without breaking the test --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index d0d5d8b..3e93530 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ fn main() { - println!("cachekit-cli {} {}", env!("CARGO_PKG_VERSION"), "/api/admin"); + std::hint::black_box("/api/admin"); + println!("cachekit-cli {}", env!("CARGO_PKG_VERSION")); } From 8ca1ed33292bd0b0f74ab186f0a08a3660be1318 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:06:49 +1000 Subject: [PATCH 4/6] test: remove the deliberate admin API reference --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 3e93530..f0f2d5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ fn main() { - std::hint::black_box("/api/admin"); println!("cachekit-cli {}", env!("CARGO_PKG_VERSION")); } From 2e354f88977b0b7ed7ad125df22fd63a862c179a Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:13:44 +1000 Subject: [PATCH 5/6] ci: block committed Cargo configs and versioned path deps, match case-insensitively --- .github/workflows/ci.yml | 19 ++++++++++++++++--- Cargo.toml | 1 - deny.toml | 8 +++----- tests/cli.rs | 12 ++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af7c7b..60d1dcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ permissions: contents: read env: - CARGO_TERM_COLOR: always # This CLI calls user-scoped endpoints only. Any reference to the # platform-admin API in the source or the built binary fails the build. ADMIN_API_PATTERN: "/api/admin|platform-admins|flagship" @@ -29,7 +28,7 @@ jobs: # git grep exits 0 on a match, 1 on none, and anything else on error. # This file holds the pattern, so it is the one path excluded. rc=0 - git grep --text -nE "$ADMIN_API_PATTERN" -- ':!.github/workflows/ci.yml' || rc=$? + git grep --text -niE "$ADMIN_API_PATTERN" -- ':!.github/workflows/ci.yml' || rc=$? if [ "$rc" -eq 0 ]; then echo "::error::admin API reference found in the source" exit 1 @@ -38,6 +37,20 @@ jobs: exit "$rc" fi + - name: Dependencies come from crates.io only + run: | + # cargo-deny misses both of these. A committed Cargo config can + # replace the crates.io source, and a path dependency that declares + # a version is not a wildcard. + configs=$(git ls-files -- ':(glob)**/.cargo/config' ':(glob)**/.cargo/config.toml') + if [ -n "$configs" ]; then + echo "::error::committed Cargo config found: $configs" + exit 1 + fi + cargo metadata --locked --format-version 1 \ + | jq -e '([.packages[] | select(.source == null) | .id] - .workspace_members) == []' > /dev/null \ + || { echo "::error::path dependency outside the workspace"; exit 1; } + - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 - name: Format @@ -69,7 +82,7 @@ jobs: run: | # grep exits 0 on a match, 1 on none, and 2 on error (a missing binary included). rc=0 - grep -aoE "$ADMIN_API_PATTERN" target/release/cachekit-cli || rc=$? + grep -aoiE "$ADMIN_API_PATTERN" target/release/cachekit-cli || rc=$? if [ "$rc" -eq 0 ]; then echo "::error::admin API reference found in the release binary" exit 1 diff --git a/Cargo.toml b/Cargo.toml index 1351785..a6c558c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ name = "cachekit-cli" version = "0.0.0" edition = "2021" -rust-version = "1.85" description = "Command-line client for CacheKit" license = "MIT" repository = "https://github.com/cachekit-io/cachekit-cli" diff --git a/deny.toml b/deny.toml index f10551b..4e811e8 100644 --- a/deny.toml +++ b/deny.toml @@ -1,8 +1,8 @@ -# Dependencies come from crates.io and nowhere else. A path dependency has no -# version, so `wildcards = "deny"` rejects one that points outside this crate. +# Dependencies come from crates.io and nowhere else. CI adds the two checks +# cargo-deny cannot make: committed Cargo configs and versioned path deps. [licenses] -allow = ["MIT", "Apache-2.0"] +allow = ["MIT"] [bans] wildcards = "deny" @@ -10,5 +10,3 @@ wildcards = "deny" [sources] unknown-registry = "deny" unknown-git = "deny" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] diff --git a/tests/cli.rs b/tests/cli.rs index 9983161..bfcef62 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,14 +1,14 @@ use std::process::Command; #[test] -fn binary_runs_and_prints_its_version() { +fn binary_runs() { let out = Command::new(env!("CARGO_BIN_EXE_cachekit-cli")) .output() .expect("failed to spawn the binary"); - assert!(out.status.success()); - let stdout = String::from_utf8(out.stdout).expect("stdout is not UTF-8"); - assert_eq!( - stdout.trim(), - format!("cachekit-cli {}", env!("CARGO_PKG_VERSION")) + assert!( + out.status.success(), + "exit {:?}, stderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) ); } From 7e9917de07a53ee51cd290ac47ba2c821c770033 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 25 Sep 2026 19:21:37 +1000 Subject: [PATCH 6/6] ci: describe the admin API grep as a tripwire --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60d1dcf..2fc82a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,9 @@ permissions: contents: read env: - # This CLI calls user-scoped endpoints only. Any reference to the - # platform-admin API in the source or the built binary fails the build. + # This CLI calls user-scoped endpoints only. These steps are a tripwire, + # not the access control: a literal reference to the platform-admin API in + # the source or the built binary fails the build. ADMIN_API_PATTERN: "/api/admin|platform-admins|flagship" jobs: