From 71b9ef6684d61f6159a730c8f1137ee9042ae63a Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 13:53:58 +0700 Subject: [PATCH 01/36] feat: add rust-packages pipeline and rust/base action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust/base composite: resolve the toolchain from rust-toolchain.toml, cache cargo + target, run an optional ci/setup.sh native-dep hook, then fmt --check, clippy -D warnings, test — zero-input, mirroring javascript/base - rust-packages.yml reusable workflow: preflight matrix (ubuntu/macos/windows), cargo-deny when deny.toml is present, tag-driven publish (pin Cargo.toml to the tag, generate-changelog, cargo publish to crates.io, github-release, commit-back, rolling vN tag), security — reuses the transverse release and security actions - README: document the pipeline, the rust/base action, and the ci/setup.sh convention --- .github/actions/rust/base/action.yml | 55 ++++++++++++ .github/workflows/rust-packages.yml | 122 +++++++++++++++++++++++++++ README.md | 67 +++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 .github/actions/rust/base/action.yml create mode 100644 .github/workflows/rust-packages.yml diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml new file mode 100644 index 0000000..86e4528 --- /dev/null +++ b/.github/actions/rust/base/action.yml @@ -0,0 +1,55 @@ +# Rust Base Action Composite +name: rust-base +description: Base setup for a Rust pipeline job — toolchain, cargo cache, optional native-dep hook, then fmt, clippy, test. + +runs: + using: composite + steps: + - id: resolve + name: Resolve Rust toolchain + shell: bash + run: | + if [ ! -f rust-toolchain.toml ]; then + echo "::error::rust-toolchain.toml is required at the repo root" + exit 1 + fi + channel="$(grep -E '^[[:space:]]*channel[[:space:]]*=' rust-toolchain.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + echo "Resolved Rust toolchain: ${channel:-unset} (from rust-toolchain.toml)" + # rustup installs the pinned toolchain and its components on first cargo use. + + - name: Setup cargo cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Native build dependencies + shell: bash + run: | + # Convention over inputs: a consumer with native deps (a `-sys` crate, a + # test fixture) ships `ci/setup.sh` to install system packages and export + # env via $GITHUB_ENV. Pure-Rust crates ship nothing and this is a no-op. + if [ -f ci/setup.sh ]; then + echo "Running consumer native-dep hook: ci/setup.sh" + bash ci/setup.sh + else + echo "No ci/setup.sh — pure-Rust package, nothing to install" + fi + + - name: Format + shell: bash + run: cargo fmt --check + + - name: Lint + shell: bash + run: cargo clippy --all-targets -- -D warnings + + - name: Test + shell: bash + run: cargo test diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml new file mode 100644 index 0000000..0007391 --- /dev/null +++ b/.github/workflows/rust-packages.yml @@ -0,0 +1,122 @@ +# Rust Cargo Packages CI +name: Rust Packages + +on: + workflow_call: + secrets: + CARGO_REGISTRY_TOKEN: + required: false + +permissions: + contents: read + +jobs: + preflight: + if: ${{ github.ref_type == 'branch' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: coroboros/ci/.github/actions/check-docs@v0 + - uses: coroboros/ci/.github/actions/rust/base@v0 + + supply-chain: + if: ${{ github.ref_type == 'branch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: cargo-deny + shell: bash + run: | + if [ ! -f deny.toml ]; then + echo "::notice::No deny.toml — skipping supply-chain check" + exit 0 + fi + cargo install cargo-deny --locked + cargo deny check + + publish: + if: ${{ github.ref_type == 'tag' }} + runs-on: ubuntu-latest + permissions: + contents: write # GitHub Release + commit-back to main + id-token: write # crates.io OIDC (when migrated off the token) + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: main + fetch-depth: 0 + + - name: Verify tag points to main HEAD + shell: bash + run: | + if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then + echo "::error::main has moved since the tag was pushed. Resolve manually." + echo " Tag SHA: ${GITHUB_SHA}" + echo " main HEAD: $(git rev-parse HEAD)" + exit 1 + fi + echo "main HEAD matches tag SHA (${GITHUB_SHA})" + + - uses: coroboros/ci/.github/actions/check-docs@v0 + + - name: Native build dependencies + shell: bash + run: | + if [ -f ci/setup.sh ]; then + bash ci/setup.sh + fi + + - name: Pin Cargo.toml to tag + shell: bash + run: | + cargo install cargo-edit --locked + cargo set-version "${GITHUB_REF_NAME}" + + - id: changelog + uses: coroboros/ci/.github/actions/release/generate-changelog@v0 + + - name: Publish to crates.io + shell: bash + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -n "${CARGO_REGISTRY_TOKEN}" ]; then + echo "Publishing to crates.io" + cargo publish --locked --allow-dirty + else + echo "::notice::No CARGO_REGISTRY_TOKEN — skipping crates.io publish" + fi + + - uses: coroboros/ci/.github/actions/release/github-release@v0 + with: + body: ${{ steps.changelog.outputs.body }} + + - name: Commit release artifacts back to main + shell: bash + run: | + git add Cargo.toml Cargo.lock CHANGELOG.md + if git diff --staged --quiet; then + echo "::notice::No release artifacts to commit (nothing changed)" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: release ${GITHUB_REF_NAME}" + git push origin HEAD:main + fi + + - name: Move rolling major tag + if: ${{ !contains(github.ref_name, '-') }} + shell: bash + run: | + major="$(echo "${GITHUB_REF_NAME}" | cut -d. -f1)" + rolling="v${major}" + git tag -f "${rolling}" HEAD + git push -f origin "${rolling}" + echo "::notice::Moved rolling tag ${rolling} to $(git rev-parse HEAD)" + + security: + uses: ./.github/workflows/security.yml diff --git a/README.md b/README.md index 81882e3..acbf106 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,72 @@ Calls `security.yml` — see [Security](#security). +### `rust-packages.yml` + +Bundled Cargo CI. Tag-driven release, same as the npm pipeline. + +Consumer requirements: +- `rust-toolchain.toml` — pins the channel and components (`rustup` installs them on first `cargo` use). +- `Cargo.toml` and `Cargo.lock`. +- `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. Pure-Rust crates omit it; it is a no-op when absent. +- `deny.toml` — optional. Enables the `cargo-deny` supply-chain scan. + +
+preflight + +
+ +**Trigger**: `branch push`. **Matrix**: `ubuntu-latest`, `macos-latest`, `windows-latest`. + +**Sequence**: +1. Checkout +2. Run [`check-docs`](#composable-actions) +3. Run [`rust/base`](#composable-actions) + +
+ +
+supply-chain + +
+ +**Trigger**: `branch push` + +`cargo deny check` (advisories, licenses, bans, sources) when `deny.toml` is present; skipped otherwise. + +
+ +
+publish + +
+ +**Trigger**: `tag push` + +**Sequence**: +1. Checkout `main` with full history +2. Verify `main` HEAD matches the tag SHA +3. Run [`check-docs`](#composable-actions); run `ci/setup.sh` for build deps +4. Pin `Cargo.toml` to the tag (`cargo set-version`) +5. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) +6. `cargo publish` to crates.io when `CARGO_REGISTRY_TOKEN` is set (absent → skipped) +7. Create GitHub Release via [`release/github-release`](#composable-actions) +8. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` as `chore: release ${tag}` +9. Move rolling major tag `vN` to the release commit (skipped on pre-release tags) + +
+ +
+security + +
+ +**Trigger**: `every call`. Calls `security.yml`. + +
+ +Cross-platform binary prebuilts (Homebrew, npm shim, shell installer) are a separate concern — cargo-dist's multi-runner build matrix, layered on a consuming repo, not part of this single-runner publish job. + ### `security.yml` Reusable sub-workflow with three parallel scans: @@ -112,6 +178,7 @@ Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#exam | :--- | :--- | :--- | | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | +| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo` + `target`, runs the optional `ci/setup.sh` native-dep hook, then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | From 48b65f395fdbf3534b315838700ea9bef421f794 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 14:11:58 +0700 Subject: [PATCH 02/36] refactor: align rust pipeline with npm-packages; drop redundant supply-chain job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drop the supply-chain (cargo-deny) job: the npm pipeline has no counterpart (preflight/publish/security only), it partly overlapped security.yml's osv-scanner, and it recompiled cargo-deny on every push — consumers add their own when needed - rust/base: cache only the ~/.cargo deps (drop target, a stale-build risk), terse one-line description matching javascript/base, drop the verbose hook comment - publish: cargo publish --no-verify (the tag is main HEAD, already built and tested by preflight) — removes the duplicated ci/setup.sh hook and the cold build on release - header, name, and permission-comment wording aligned with javascript-npm-packages; README drops the cargo-dist aside and documents the CARGO_REGISTRY_TOKEN secret --- .github/actions/rust/base/action.yml | 13 +++------- .github/workflows/rust-packages.yml | 37 ++++++---------------------- README.md | 21 +++------------- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml index 86e4528..48e66e6 100644 --- a/.github/actions/rust/base/action.yml +++ b/.github/actions/rust/base/action.yml @@ -1,12 +1,11 @@ # Rust Base Action Composite name: rust-base -description: Base setup for a Rust pipeline job — toolchain, cargo cache, optional native-dep hook, then fmt, clippy, test. +description: Base setup for a Rust pipeline job. runs: using: composite steps: - - id: resolve - name: Resolve Rust toolchain + - name: Resolve Rust toolchain shell: bash run: | if [ ! -f rust-toolchain.toml ]; then @@ -15,7 +14,6 @@ runs: fi channel="$(grep -E '^[[:space:]]*channel[[:space:]]*=' rust-toolchain.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" echo "Resolved Rust toolchain: ${channel:-unset} (from rust-toolchain.toml)" - # rustup installs the pinned toolchain and its components on first cargo use. - name: Setup cargo cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 @@ -24,7 +22,6 @@ runs: ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db - target key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- @@ -32,14 +29,10 @@ runs: - name: Native build dependencies shell: bash run: | - # Convention over inputs: a consumer with native deps (a `-sys` crate, a - # test fixture) ships `ci/setup.sh` to install system packages and export - # env via $GITHUB_ENV. Pure-Rust crates ship nothing and this is a no-op. if [ -f ci/setup.sh ]; then - echo "Running consumer native-dep hook: ci/setup.sh" bash ci/setup.sh else - echo "No ci/setup.sh — pure-Rust package, nothing to install" + echo "No ci/setup.sh — pure-Rust package" fi - name: Format diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 0007391..020d165 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -1,4 +1,4 @@ -# Rust Cargo Packages CI +# Rust Packages CI name: Rust Packages on: @@ -23,27 +23,12 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/rust/base@v0 - supply-chain: - if: ${{ github.ref_type == 'branch' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: cargo-deny - shell: bash - run: | - if [ ! -f deny.toml ]; then - echo "::notice::No deny.toml — skipping supply-chain check" - exit 0 - fi - cargo install cargo-deny --locked - cargo deny check - publish: if: ${{ github.ref_type == 'tag' }} runs-on: ubuntu-latest permissions: - contents: write # GitHub Release + commit-back to main - id-token: write # crates.io OIDC (when migrated off the token) + contents: write # for GitHub Release creation + commit-back to main + id-token: write # for crates.io OIDC Trusted Publishing steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: @@ -63,13 +48,6 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - - name: Native build dependencies - shell: bash - run: | - if [ -f ci/setup.sh ]; then - bash ci/setup.sh - fi - - name: Pin Cargo.toml to tag shell: bash run: | @@ -84,12 +62,13 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | - if [ -n "${CARGO_REGISTRY_TOKEN}" ]; then - echo "Publishing to crates.io" - cargo publish --locked --allow-dirty - else + if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then echo "::notice::No CARGO_REGISTRY_TOKEN — skipping crates.io publish" + exit 0 fi + # This tag is main HEAD, already built and tested by preflight, so skip the + # redundant verify build; --allow-dirty covers the in-place version pin. + cargo publish --no-verify --allow-dirty - uses: coroboros/ci/.github/actions/release/github-release@v0 with: diff --git a/README.md b/README.md index acbf106..2092d99 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: - `rust-toolchain.toml` — pins the channel and components (`rustup` installs them on first `cargo` use). - `Cargo.toml` and `Cargo.lock`. -- `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. Pure-Rust crates omit it; it is a no-op when absent. -- `deny.toml` — optional. Enables the `cargo-deny` supply-chain scan. +- `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. +- `CARGO_REGISTRY_TOKEN` secret — optional. Enables the crates.io publish on tag.
preflight @@ -114,17 +114,6 @@ Consumer requirements:
-
-supply-chain - -
- -**Trigger**: `branch push` - -`cargo deny check` (advisories, licenses, bans, sources) when `deny.toml` is present; skipped otherwise. - -
-
publish @@ -135,7 +124,7 @@ Consumer requirements: **Sequence**: 1. Checkout `main` with full history 2. Verify `main` HEAD matches the tag SHA -3. Run [`check-docs`](#composable-actions); run `ci/setup.sh` for build deps +3. Run [`check-docs`](#composable-actions) 4. Pin `Cargo.toml` to the tag (`cargo set-version`) 5. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) 6. `cargo publish` to crates.io when `CARGO_REGISTRY_TOKEN` is set (absent → skipped) @@ -154,8 +143,6 @@ Consumer requirements:
-Cross-platform binary prebuilts (Homebrew, npm shim, shell installer) are a separate concern — cargo-dist's multi-runner build matrix, layered on a consuming repo, not part of this single-runner publish job. - ### `security.yml` Reusable sub-workflow with three parallel scans: @@ -178,7 +165,7 @@ Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#exam | :--- | :--- | :--- | | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | -| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo` + `target`, runs the optional `ci/setup.sh` native-dep hook, then `cargo fmt --check`, `clippy -D warnings`, `test`. | +| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs the optional `ci/setup.sh` native-dep hook, then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | From bdf31343c211f2a2c964341ba600f7d378926cab Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 14:24:48 +0700 Subject: [PATCH 03/36] feat: restore cargo-deny supply-chain + --locked; document the model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was wrong to drop cargo-deny as "redundant": osv-scanner only scans for known vulnerabilities. cargo-deny owns the controls nothing else here covers — crate sources (crates.io only), licenses, banned/wildcard deps, and unmaintained or yanked advisories. Restored, mapped to the GitLab image-layer hardening model. - supply-chain job: cargo-deny check via the SHA-pinned EmbarkStudios action, on every branch push, reading the repo's deny.toml - rust/base: clippy and test run --locked (committed Cargo.lock, fails on drift) — the lock-pin control; deny.toml + committed Cargo.lock are now consumer requirements - README Security: a "Supply chain — Rust" section mapping each GitLab control (cooldown, firewall, no-scripts, pin) to its Rust analog, the baseline deny.toml, and two accepted residual risks (build.rs runs; crates.io has no publish cooldown) --- .github/actions/rust/base/action.yml | 4 +- .github/workflows/rust-packages.yml | 12 ++++++ README.md | 59 +++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml index 48e66e6..ff7b614 100644 --- a/.github/actions/rust/base/action.yml +++ b/.github/actions/rust/base/action.yml @@ -41,8 +41,8 @@ runs: - name: Lint shell: bash - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --all-targets --locked -- -D warnings - name: Test shell: bash - run: cargo test + run: cargo test --locked diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 020d165..49d7bef 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -23,6 +23,18 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/rust/base@v0 + # cargo-deny owns the Rust supply-chain controls osv-scanner doesn't: crate + # sources (crates.io only), licenses, banned/wildcard deps, and unmaintained or + # yanked advisories. Reads the repo's deny.toml. + supply-chain: + if: ${{ github.ref_type == 'branch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 + with: + command: check + publish: if: ${{ github.ref_type == 'tag' }} runs-on: ubuntu-latest diff --git a/README.md b/README.md index 2092d99..79846dd 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: - `rust-toolchain.toml` — pins the channel and components (`rustup` installs them on first `cargo` use). -- `Cargo.toml` and `Cargo.lock`. +- `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. +- `deny.toml` — the cargo-deny supply-chain policy (sources, licenses, bans, advisories). See [Security](#security) for the baseline. - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. - `CARGO_REGISTRY_TOKEN` secret — optional. Enables the crates.io publish on tag. @@ -114,6 +115,17 @@ Consumer requirements: +
+supply-chain + +
+ +**Trigger**: `branch push` + +`cargo-deny check` (SHA-pinned action) reads `deny.toml`: crate sources, licenses, bans, and advisories (vulnerabilities, unmaintained, yanked). See [Security](#security). + +
+
publish @@ -286,6 +298,51 @@ pnpm CLI resolved via corepack from `packageManager`. No floating version reache
+
+Supply chain — Rust (rust-packages.yml) + +
+ +The GitLab pipeline hardens npm at the image layer — cooldown, Socket Firewall, `--ignore-scripts`, digest pins. GitHub-hosted runners share no base image, so the Rust pipeline enforces the same risk model in the workflow: + +| Risk | Rust control | +| :--- | :--- | +| Untrusted source, typosquat | `cargo-deny` sources — crates.io only; git and alternative registries denied | +| Lock drift, tampered dependencies | committed `Cargo.lock` + `--locked` on `clippy` and `test` — fails on a stale or altered lock | +| Known vulnerability | `osv-scanner` (Cargo.lock) and `cargo-deny` advisories — RustSec vulnerabilities, unmaintained, yanked | +| License drift | `cargo-deny` licenses — allow-list | +| Banned or wildcard dependency | `cargo-deny` bans | + +`cargo-deny` runs on every branch push via a SHA-pinned action, reading the repo's `deny.toml`. The baseline: + +```toml +[advisories] +version = 2 +yanked = "deny" + +[licenses] +version = 2 +allow = [ + "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", + "BSD-3-Clause", "0BSD", "ISC", "Zlib", "MPL-2.0", "Unicode-3.0", + "Unlicense", "CDLA-Permissive-2.0", "BSL-1.0", +] + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +``` + +Two residual risks have no clean CI control. Both are documented here: +- **Build scripts run.** `cargo` has no `--ignore-scripts`; `build.rs` and proc-macros execute at build time. `--locked`, `cargo-deny` bans, and dependency review reduce the exposure; they do not remove it. +- **No publish cooldown.** crates.io has no `minimumReleaseAge`, so a freshly hijacked version is held off by the committed lock and `cargo-deny` advisories rather than a time delay. + +
+
Recommended NPM_CONFIG_FILE contents From faca699b19090341b86d0e08f2999e4a6ed3685b Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 14:44:00 +0700 Subject: [PATCH 04/36] feat: firewall the npm fetch with Socket Firewall; document cooldown + policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - javascript/base: install Socket Firewall (sfw, the free tokenless build) and run the install through it (sfw pnpm install) — blocks confirmed-malicious packages at the registry fetch, the GitHub-runner equivalent of the GitLab image-baked firewall; fail-closed if sfw cannot install or run - README Security: Firewall and Cooldown sections; the cooldown (minimumReleaseAge, @coroboros/* excluded) belongs in pnpm-workspace.yaml on pnpm 11, not .npmrc - SECURITY.md: vulnerability-reporting policy, matching the GitLab repo --- .github/actions/javascript/base/action.yml | 8 +++++- README.md | 31 +++++++++++++++++++++- SECURITY.md | 13 +++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 SECURITY.md diff --git a/.github/actions/javascript/base/action.yml b/.github/actions/javascript/base/action.yml index 66233a0..a98e840 100644 --- a/.github/actions/javascript/base/action.yml +++ b/.github/actions/javascript/base/action.yml @@ -33,6 +33,10 @@ runs: corepack enable pnpm --version + - name: Install Socket Firewall + shell: bash + run: npm install -g sfw + - name: Setup pnpm cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: @@ -55,7 +59,9 @@ runs: - name: Install dependencies shell: bash - run: pnpm install --frozen-lockfile --ignore-scripts + # Wrapped by Socket Firewall — the registry fetch is inspected and confirmed- + # malicious packages are blocked before download. Fail-closed: no sfw, no install. + run: sfw pnpm install --frozen-lockfile --ignore-scripts - name: Lint shell: bash diff --git a/README.md b/README.md index 79846dd..0512b96 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ Caller job needs `permissions: contents: write`. Uses `${{ github.token }}` inte
-`pnpm install --frozen-lockfile --ignore-scripts` runs inside `javascript/base`. +`sfw pnpm install --frozen-lockfile --ignore-scripts` runs inside `javascript/base` (Socket Firewall wraps the fetch — see Firewall below). - `--frozen-lockfile` — fails on stale or tampered `pnpm-lock.yaml`. Gate against transitive-dependency injection. - `--ignore-scripts` — skips lifecycle scripts (`preinstall`, `install`, `postinstall`) of every dependency. Cuts the postinstall supply-chain vector. @@ -298,6 +298,35 @@ pnpm CLI resolved via corepack from `packageManager`. No floating version reache
+
+Firewall — Socket Firewall (sfw) + +
+ +`javascript/base` installs Socket Firewall (`npm i -g sfw`, the free tokenless build) and runs the install through it (`sfw pnpm install …`). `sfw` proxies the registry fetch and blocks packages Socket has confirmed malicious before they download — the GitHub-runner equivalent of the image-baked firewall in the GitLab pipeline. + +Fail-closed: if `sfw` cannot install or run, the install step fails rather than fetching unprotected. The free build needs no account or token, and inspects public-registry fetches out of the box. The trade-off of the runtime install (no shared image) is a dependency on Socket's service at fetch time. + +
+ +
+Cooldown — minimum release age + +
+ +Quarantine freshly published versions so a hijacked release is not pulled inside the window most campaigns are caught in. pnpm 11 reads the setting from `pnpm-workspace.yaml`, not `.npmrc`. Add to the consuming repo: + +```yaml +# pnpm-workspace.yaml +minimumReleaseAge: 10080 # 7 days, in minutes +minimumReleaseAgeExclude: + - '@coroboros/*' # internal packages install immediately +``` + +On pnpm 10.x the `.npmrc` form `minimum-release-age=10080` also works. `@coroboros/*` is excluded so a pipeline can consume a just-published internal package. + +
+
Supply chain — Rust (rust-packages.yml) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a02e343 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +## Supported versions + +Latest `main` only. Tagged releases follow the same support model as `main` at the time of the release. + +## Reporting a vulnerability + +Report vulnerabilities to **ob@coroboros.com**. Do not open public issues, PRs, or comments for security problems. + +Expected initial response: within 5 business days. + +Coordinated disclosure preferred. A 30-day fix window is the default before public disclosure; a different window can be agreed when the severity demands it. From 3dbbc83794336b0fa58bfe6d798bcd21f09d10c8 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 14:54:32 +0700 Subject: [PATCH 05/36] docs: tighten the Security firewall note (straight ellipsis, shorter sentence) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0512b96..c1e13e7 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ pnpm CLI resolved via corepack from `packageManager`. No floating version reache
-`javascript/base` installs Socket Firewall (`npm i -g sfw`, the free tokenless build) and runs the install through it (`sfw pnpm install …`). `sfw` proxies the registry fetch and blocks packages Socket has confirmed malicious before they download — the GitHub-runner equivalent of the image-baked firewall in the GitLab pipeline. +`javascript/base` installs Socket Firewall (`npm i -g sfw`, the free tokenless build) and runs the install through it (`sfw pnpm install ...`). `sfw` proxies the registry fetch and blocks packages Socket has confirmed malicious before they download. It is the GitHub-runner equivalent of the image-baked firewall in the GitLab pipeline. Fail-closed: if `sfw` cannot install or run, the install step fails rather than fetching unprotected. The free build needs no account or token, and inspects public-registry fetches out of the box. The trade-off of the runtime install (no shared image) is a dependency on Socket's service at fetch time. From a37510605c88597cff9695b33b404699291eed6a Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 16:02:54 +0700 Subject: [PATCH 06/36] fix(release): drop misplaced rolling tag; share the commit-back step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable workflows run in the caller's context, so "Move rolling major tag" force-pushed a meaningless vN ref into every consumer package repo. Packages release x.y.z; the @vN ref to coroboros/ci belongs to its own release path. This matches the GitLab model — ADR-0005 keeps rolling tags image-only and gives packages no rolling ref. Commit-back was duplicated inline across the npm and rust publish jobs. Extract it into release/commit-artifacts (files input), mirroring GitLab's commit-release-artifacts template, with [skip ci] so the bot's main push no longer re-triggers the pipeline. --- .../release/commit-artifacts/action.yml | 28 +++++++++++++++++++ .github/workflows/javascript-npm-packages.yml | 25 ++--------------- .github/workflows/rust-packages.yml | 25 ++--------------- README.md | 7 ++--- 4 files changed, 37 insertions(+), 48 deletions(-) create mode 100644 .github/actions/release/commit-artifacts/action.yml diff --git a/.github/actions/release/commit-artifacts/action.yml b/.github/actions/release/commit-artifacts/action.yml new file mode 100644 index 0000000..92518b4 --- /dev/null +++ b/.github/actions/release/commit-artifacts/action.yml @@ -0,0 +1,28 @@ +# Release Commit Artifacts Action Composite +name: release-commit-artifacts +description: Commit release artifacts back to main after a tagged publish. + +inputs: + files: + description: Space-separated artifact paths to stage and commit. + required: true + +runs: + using: composite + steps: + - name: Commit release artifacts back to main + shell: bash + env: + FILES: ${{ inputs.files }} + # [skip ci] keeps the bot's main push from re-triggering the pipeline. + run: | + read -ra paths <<< "${FILES}" + git add "${paths[@]}" + if git diff --staged --quiet; then + echo "::notice::No release artifacts to commit (nothing changed)" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: release ${GITHUB_REF_NAME} [skip ci]" + git push origin HEAD:main + fi diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index c2b905e..0c7e0ff 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -92,28 +92,9 @@ jobs: with: body: ${{ steps.changelog.outputs.body }} - - name: Commit release artifacts back to main - shell: bash - run: | - git add CHANGELOG.md package.json pnpm-lock.yaml - if git diff --staged --quiet; then - echo "::notice::No release artifacts to commit (nothing changed)" - else - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "chore: release ${GITHUB_REF_NAME}" - git push origin HEAD:main - fi - - - name: Move rolling major tag - if: ${{ !contains(github.ref_name, '-') }} - shell: bash - run: | - major="$(echo "${GITHUB_REF_NAME}" | cut -d. -f1)" - rolling="v${major}" - git tag -f "${rolling}" HEAD - git push -f origin "${rolling}" - echo "::notice::Moved rolling tag ${rolling} to $(git rev-parse HEAD)" + - uses: coroboros/ci/.github/actions/release/commit-artifacts@v0 + with: + files: CHANGELOG.md package.json pnpm-lock.yaml security: uses: ./.github/workflows/security.yml diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 49d7bef..0989bad 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -86,28 +86,9 @@ jobs: with: body: ${{ steps.changelog.outputs.body }} - - name: Commit release artifacts back to main - shell: bash - run: | - git add Cargo.toml Cargo.lock CHANGELOG.md - if git diff --staged --quiet; then - echo "::notice::No release artifacts to commit (nothing changed)" - else - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "chore: release ${GITHUB_REF_NAME}" - git push origin HEAD:main - fi - - - name: Move rolling major tag - if: ${{ !contains(github.ref_name, '-') }} - shell: bash - run: | - major="$(echo "${GITHUB_REF_NAME}" | cut -d. -f1)" - rolling="v${major}" - git tag -f "${rolling}" HEAD - git push -f origin "${rolling}" - echo "::notice::Moved rolling tag ${rolling} to $(git rev-parse HEAD)" + - uses: coroboros/ci/.github/actions/release/commit-artifacts@v0 + with: + files: Cargo.toml Cargo.lock CHANGELOG.md security: uses: ./.github/workflows/security.yml diff --git a/README.md b/README.md index c1e13e7..65172d9 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ Consumer requirements: 6. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) 7. Publish to npm — OIDC + provenance or token-based via `.npmrc` (see [Security](#security)) 8. Create GitHub Release via [`release/github-release`](#composable-actions) -9. Commit release artifacts back to `main` as `chore: release ${tag}` -10. Move rolling major tag `vN` to the release commit (skipped on pre-release tags) +9. Commit release artifacts back to `main` via [`release/commit-artifacts`](#composable-actions)
@@ -141,8 +140,7 @@ Consumer requirements: 5. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) 6. `cargo publish` to crates.io when `CARGO_REGISTRY_TOKEN` is set (absent → skipped) 7. Create GitHub Release via [`release/github-release`](#composable-actions) -8. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` as `chore: release ${tag}` -9. Move rolling major tag `vN` to the release commit (skipped on pre-release tags) +8. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` via [`release/commit-artifacts`](#composable-actions) @@ -180,6 +178,7 @@ Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#exam | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs the optional `ci/setup.sh` native-dep hook, then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | +| `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | --- From ff97adf2e9321afa07c907fe17aaeb4b73dec668 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:15:45 +0700 Subject: [PATCH 07/36] refactor(security): extract scanners to composites; manifest-gate osv Each scanner (gitleaks, osv-scanner, cargo-deny) becomes a security/* composite, so osv-scanner is defined once and reused by both security.yml and the package supply-chain gates. osv-scanner now runs only when a supported manifest is present, so a dependency-less repo wiring in security.yml skips the scan instead of failing on osv's no-manifest error. self-security.yml references the composites via local ./ refs: a reusable workflow resolves ./ against the caller, so security.yml must pin @v0 while self-CI exercises the in-repo composites. --- .../actions/security/cargo-deny/action.yml | 10 +++ .github/actions/security/gitleaks/action.yml | 71 ++++++++++++++++++ .../actions/security/osv-scanner/action.yml | 43 +++++++++++ .github/workflows/security.yml | 73 +------------------ .github/workflows/self-security.yml | 20 ++++- 5 files changed, 144 insertions(+), 73 deletions(-) create mode 100644 .github/actions/security/cargo-deny/action.yml create mode 100644 .github/actions/security/gitleaks/action.yml create mode 100644 .github/actions/security/osv-scanner/action.yml diff --git a/.github/actions/security/cargo-deny/action.yml b/.github/actions/security/cargo-deny/action.yml new file mode 100644 index 0000000..9d478d2 --- /dev/null +++ b/.github/actions/security/cargo-deny/action.yml @@ -0,0 +1,10 @@ +# Security cargo-deny Action Composite +name: security-cargo-deny +description: Run cargo-deny (sources, licenses, bans, advisories) against the repo's deny.toml. + +runs: + using: composite + steps: + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 + with: + command: check diff --git a/.github/actions/security/gitleaks/action.yml b/.github/actions/security/gitleaks/action.yml new file mode 100644 index 0000000..4281d31 --- /dev/null +++ b/.github/actions/security/gitleaks/action.yml @@ -0,0 +1,71 @@ +# Security gitleaks Action Composite +name: security-gitleaks +description: Install gitleaks (SHA-256 verified) and scan with the canonical Coroboros ruleset. Emits SARIF. + +# The caller checks out the repo to scan (fetch-depth: 0 for full history) before +# using this composite. +runs: + using: composite + steps: + - name: Checkout canonical gitleaks config + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: coroboros/ci + path: .coroboros-ci + sparse-checkout: | + security/.gitleaks.toml + sparse-checkout-cone-mode: false + + - name: Install gitleaks + shell: bash + env: + GITLEAKS_VERSION: "8.30.1" + GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" + run: | + tmp="$(mktemp -d)" + tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl -fsSL \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" \ + -o "${tmp}/${tarball}" + echo "${GITLEAKS_SHA256} ${tmp}/${tarball}" | sha256sum -c - + tar -xzf "${tmp}/${tarball}" -C "${tmp}" gitleaks + sudo install -m 0755 "${tmp}/gitleaks" /usr/local/bin/gitleaks + rm -rf "${tmp}" + gitleaks version + + - name: Run gitleaks + shell: bash + env: + GITLEAKS_CONFIG: ".coroboros-ci/security/.gitleaks.toml" + SCAN_MODE: "git" + run: | + set +e + gitleaks "${SCAN_MODE}" \ + --config "${GITLEAKS_CONFIG}" \ + --no-banner \ + --redact \ + --report-format sarif \ + --report-path results.sarif \ + --exit-code 2 + rc=$? + set -e + + echo "gitleaks exit code: ${rc}" + if [ "${rc}" = "0" ]; then + echo "::notice::gitleaks: no leaks found" + elif [ "${rc}" = "2" ]; then + echo "::error::gitleaks: leaks detected — see results.sarif artifact" + exit 1 + else + echo "::error::gitleaks: scan failed with exit code ${rc}" + exit "${rc}" + fi + + - name: Upload SARIF report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: gitleaks-report + path: results.sarif + if-no-files-found: ignore + retention-days: 30 diff --git a/.github/actions/security/osv-scanner/action.yml b/.github/actions/security/osv-scanner/action.yml new file mode 100644 index 0000000..28fea01 --- /dev/null +++ b/.github/actions/security/osv-scanner/action.yml @@ -0,0 +1,43 @@ +# Security OSV Scanner Action Composite +name: security-osv-scanner +description: Scan dependency manifests for known vulnerabilities (OSV.dev). Skips a repo with none; fails on a known vulnerability. + +runs: + using: composite + steps: + - id: detect + shell: bash + # osv-scanner errors when it finds no manifest. Gate it on a file osv can + # resolve, so a dependency-less repo (docs, config, this CI repo itself) + # wiring in security.yml skips the scan instead of failing on it. A real + # dependency repo carries one of these — extend the list as ecosystems land. + run: | + manifests=( + package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb + Cargo.lock + go.mod + requirements.txt poetry.lock Pipfile.lock uv.lock + Gemfile.lock + composer.lock + ) + found="" + for m in "${manifests[@]}"; do + if [ -n "$(find . -name "${m}" -not -path '*/node_modules/*' -not -path '*/.git/*' -print -quit)" ]; then + found="${m}" + break + fi + done + if [ -n "${found}" ]; then + echo "::notice::osv-scanner: ${found} present — scanning" + echo "scan=true" >> "${GITHUB_OUTPUT}" + else + echo "::notice::osv-scanner: no supported manifest — skipping (nothing to scan)" + echo "scan=false" >> "${GITHUB_OUTPUT}" + fi + + - if: ${{ steps.detect.outputs.scan == 'true' }} + uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + with: + scan-args: |- + --recursive + ./ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 86192e1..5285eed 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -7,12 +7,6 @@ on: permissions: contents: read -env: - GITLEAKS_VERSION: "8.30.1" - GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" - GITLEAKS_CONFIG: ".coroboros-ci/security/.gitleaks.toml" - SCAN_MODE: "git" - jobs: gitleaks: runs-on: ubuntu-latest @@ -20,66 +14,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - - - name: Checkout canonical gitleaks config - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - repository: coroboros/ci - path: .coroboros-ci - sparse-checkout: | - security/.gitleaks.toml - sparse-checkout-cone-mode: false - - - name: Install gitleaks - shell: bash - run: | - tmp="$(mktemp -d)" - tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - curl -fsSL \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" \ - -o "${tmp}/${tarball}" - echo "${GITLEAKS_SHA256} ${tmp}/${tarball}" | sha256sum -c - - tar -xzf "${tmp}/${tarball}" -C "${tmp}" gitleaks - sudo install -m 0755 "${tmp}/gitleaks" /usr/local/bin/gitleaks - rm -rf "${tmp}" - gitleaks version - - - name: Run gitleaks - id: scan - shell: bash - run: | - set +e - gitleaks "${SCAN_MODE}" \ - --config "${GITLEAKS_CONFIG}" \ - --no-banner \ - --redact \ - --report-format sarif \ - --report-path results.sarif \ - --exit-code 2 - rc=$? - set -e - - echo "gitleaks exit code: ${rc}" - echo "exit-code=${rc}" >> "${GITHUB_OUTPUT}" - - if [ "${rc}" = "0" ]; then - echo "::notice::gitleaks: no leaks found" - elif [ "${rc}" = "2" ]; then - echo "::error::gitleaks: leaks detected — see results.sarif artifact" - exit 1 - else - echo "::error::gitleaks: scan failed with exit code ${rc}" - exit "${rc}" - fi - - - name: Upload SARIF report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: gitleaks-report - path: results.sarif - if-no-files-found: ignore - retention-days: 30 + - uses: coroboros/ci/.github/actions/security/gitleaks@v0 dependency-review: if: ${{ github.event_name == 'pull_request' }} @@ -95,8 +30,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 - with: - scan-args: |- - --recursive - ./ + - uses: coroboros/ci/.github/actions/security/osv-scanner@v0 diff --git a/.github/workflows/self-security.yml b/.github/workflows/self-security.yml index b2d1338..178b974 100644 --- a/.github/workflows/self-security.yml +++ b/.github/workflows/self-security.yml @@ -1,6 +1,11 @@ # Self-CI Security name: Self-CI Security +# Runs the security composites via local `./` refs so a PR's changes to them are +# self-tested before release. security.yml (the consumer-facing reusable workflow) +# pins the same composites at @v0; a reusable workflow can't use local refs — they +# resolve against the caller's checkout — so this repo exercises them directly. +# coroboros/ci ships no dependency manifest, so osv-scanner exercises its skip path. on: push: branches: [main] @@ -10,5 +15,16 @@ permissions: contents: read jobs: - security: - uses: ./.github/workflows/security.yml + gitleaks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/security/gitleaks + + osv-scanner: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: ./.github/actions/security/osv-scanner From 69139aa5967bc7c347ec74b49d51bd41293a3c0a Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:15:53 +0700 Subject: [PATCH 08/36] feat(rust): OIDC publish, verify build, and supply-chain gate crates.io Trusted Publishing (OIDC) via rust-lang/crates-io-auth-action is the default; CARGO_REGISTRY_TOKEN is the bootstrap for a new crate's first publish. Drop --no-verify so the verify build compiles the packaged tarball and catches a crate that only builds in-workspace before an immutable release. publish needs the supply-chain job, so cargo-deny gates the release rather than scanning in parallel. The ci/setup.sh hook moves to rust/native-deps, shared by rust/base and the publish verify build. --- .github/actions/rust/base/action.yml | 8 +---- .github/actions/rust/native-deps/action.yml | 15 +++++++++ .github/workflows/rust-packages.yml | 37 +++++++++++++-------- 3 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 .github/actions/rust/native-deps/action.yml diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml index ff7b614..db40230 100644 --- a/.github/actions/rust/base/action.yml +++ b/.github/actions/rust/base/action.yml @@ -27,13 +27,7 @@ runs: ${{ runner.os }}-cargo- - name: Native build dependencies - shell: bash - run: | - if [ -f ci/setup.sh ]; then - bash ci/setup.sh - else - echo "No ci/setup.sh — pure-Rust package" - fi + uses: coroboros/ci/.github/actions/rust/native-deps@v0 - name: Format shell: bash diff --git a/.github/actions/rust/native-deps/action.yml b/.github/actions/rust/native-deps/action.yml new file mode 100644 index 0000000..5d455ad --- /dev/null +++ b/.github/actions/rust/native-deps/action.yml @@ -0,0 +1,15 @@ +# Rust Native Dependencies Action Composite +name: rust-native-deps +description: Run the optional ci/setup.sh native build-dependency hook. + +runs: + using: composite + steps: + - name: Native build dependencies + shell: bash + run: | + if [ -f ci/setup.sh ]; then + bash ci/setup.sh + else + echo "No ci/setup.sh — pure-Rust package" + fi diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 0989bad..4e14224 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -25,22 +25,27 @@ jobs: # cargo-deny owns the Rust supply-chain controls osv-scanner doesn't: crate # sources (crates.io only), licenses, banned/wildcard deps, and unmaintained or - # yanked advisories. Reads the repo's deny.toml. + # yanked advisories. Reads the repo's deny.toml. Runs on every push and gates + # `publish` (see its `needs:`), so a release is re-checked against the latest + # advisory DB before it ships — not only at PR time. The `security` job scans + # in parallel for reporting and does not gate publish; this is the release gate. supply-chain: - if: ${{ github.ref_type == 'branch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 - with: - command: check + - uses: coroboros/ci/.github/actions/security/cargo-deny@v0 publish: if: ${{ github.ref_type == 'tag' }} + needs: supply-chain # no release ships until cargo-deny passes runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main id-token: write # for crates.io OIDC Trusted Publishing + # Empty on the OIDC path; set only for the first-publish bootstrap of a new + # crate. Surfaced as env so the OIDC auth step can branch on its presence. + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: @@ -59,6 +64,7 @@ jobs: echo "main HEAD matches tag SHA (${GITHUB_SHA})" - uses: coroboros/ci/.github/actions/check-docs@v0 + - uses: coroboros/ci/.github/actions/rust/native-deps@v0 - name: Pin Cargo.toml to tag shell: bash @@ -69,18 +75,23 @@ jobs: - id: changelog uses: coroboros/ci/.github/actions/release/generate-changelog@v0 + - name: Mint a short-lived crates.io token via OIDC + id: auth + if: ${{ env.CARGO_REGISTRY_TOKEN == '' }} + uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 + - name: Publish to crates.io shell: bash env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + # OIDC path: the short-lived token from the auth step. Bootstrap path: + # the long-lived CARGO_REGISTRY_TOKEN secret (already in job env). + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token || env.CARGO_REGISTRY_TOKEN }} run: | - if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then - echo "::notice::No CARGO_REGISTRY_TOKEN — skipping crates.io publish" - exit 0 - fi - # This tag is main HEAD, already built and tested by preflight, so skip the - # redundant verify build; --allow-dirty covers the in-place version pin. - cargo publish --no-verify --allow-dirty + # The verify build runs (no --no-verify): it compiles the packaged + # tarball standalone, catching files missing from `include` or path-dep + # leakage that an in-workspace build misses. --allow-dirty covers the + # in-place version pin. + cargo publish --allow-dirty - uses: coroboros/ci/.github/actions/release/github-release@v0 with: From fda5b2c6a30a38d211d546e8995205bf99615b51 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:16:01 +0700 Subject: [PATCH 09/36] fix(javascript-npm-packages): gate publish on osv supply-chain scan osv-scanner ran only in the parallel security job, so a known vulnerability could not block a release. A supply-chain job now runs it on every push and publish needs it. --- .github/workflows/javascript-npm-packages.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 0c7e0ff..78e60c5 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -34,8 +34,20 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/javascript/base@v0 + # osv-scanner gates the release on known vulnerabilities (pnpm-lock.yaml). + # javascript/base already gates the install (Socket Firewall + --frozen-lockfile); + # this adds the CVE gate. Runs on every push and gates `publish` (see its + # `needs:`), so a release is re-checked against the latest OSV data before it + # ships. The `security` job scans in parallel for reporting and does not gate. + supply-chain: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: coroboros/ci/.github/actions/security/osv-scanner@v0 + publish: if: ${{ github.ref_type == 'tag' }} + needs: supply-chain # no release ships until osv-scanner passes runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main From 651df7cf3e5e2e2d2029a1907fd6013224ea6541 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:16:01 +0700 Subject: [PATCH 10/36] feat(release): move v0 rolling tag on coroboros/ci's own release The previous mover ran inside the reusable publish job, in the consumer's context, and force-pushed a meaningless vN ref into every package repo. Moving v0 belongs to coroboros/ci's release path; self-release.yml does it on each stable X.Y.Z tag. --- .github/workflows/self-release.yml | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/self-release.yml diff --git a/.github/workflows/self-release.yml b/.github/workflows/self-release.yml new file mode 100644 index 0000000..7d6db93 --- /dev/null +++ b/.github/workflows/self-release.yml @@ -0,0 +1,34 @@ +# Self-CI Release +name: Self-CI Release + +# Stable release tags only. `!v*` keeps the rolling tag this workflow pushes +# from re-triggering it; pre-release tags are filtered by the step guard below. +on: + push: + tags: + - '*' + - '!v*' + +permissions: + contents: read + +jobs: + rolling-tag: + runs-on: ubuntu-latest + permissions: + contents: write # force-push the rolling major tag + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Move rolling major tag + shell: bash + run: | + tag="${GITHUB_REF_NAME}" + if [[ ! "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::notice::Tag '${tag}' is not a stable release (X.Y.Z) — rolling tag unchanged" + exit 0 + fi + rolling="v${tag%%.*}" + git tag -f "${rolling}" "${GITHUB_SHA}" + git push -f origin "${rolling}" + echo "::notice::Moved ${rolling} → ${GITHUB_SHA} (${tag})" From a4410b1a63d904b70fe9c9ce681bea0d3fcd981e Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:16:09 +0700 Subject: [PATCH 11/36] docs: brand-voice README, CHANGELOG, CLAUDE; bump to 0.2.0 Document the Rust pipeline, the supply-chain gates, the security composites, the manifest-gated osv scan, and the v0 automation. Apply the Coroboros brand voice to the README prose. Bump package.json to 0.2.0, resolving the lag behind the 0.1.14 tag, and prepend the CHANGELOG section. --- CHANGELOG.md | 22 ++++++++++++ CLAUDE.md | 13 +++---- README.md | 97 +++++++++++++++++++++++++++++++++++++--------------- package.json | 2 +- 4 files changed, 99 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 330cfff..3edae2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v0.2.0 - 02/06/2026 + +### Features +- `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. The verify build runs on publish — no `--no-verify` — so a crate that only builds in-workspace fails before an immutable release. +- `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the publish verify build. +- `javascript/base` — wrap `pnpm install` in Socket Firewall (`sfw`), blocking confirmed-malicious packages before download. Fail-closed. The GitHub-runner equivalent of the image-baked firewall on GitLab. +- `self-release` — move the rolling `v0` major tag to each stable `coroboros/ci` release, so `@v0` consumers track the latest release without a manual tag push. + +### Fixes +- `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. +- `release` — drop the "move rolling major tag" step from the npm and Rust publish jobs. Reusable workflows run in the caller's context, so it force-pushed a meaningless `vN` ref into every consumer repo; the `v0` ref now moves on `coroboros/ci`'s own release (see `self-release`). + +### Refactor +- `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. +- `release/commit-artifacts` — extract the commit-back step shared by the npm and Rust publish jobs into a composite (`files` input, `[skip ci]`), mirroring GitLab's `commit-release-artifacts`. + +### Documentation +- `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab. + +### Configuration +- `package.json` — bump to `0.2.0` (was `0.1.13`, lagging the `0.1.14` tag). + ## v0.1.14 - 01/06/2026 ### Fixes diff --git a/CLAUDE.md b/CLAUDE.md index 29b708f..3e3de9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,9 +9,11 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. ## Important files -- `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `publish` / `security`). -- `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner`. -- `.github/actions/{check-docs,javascript/base,release/generate-changelog,release/github-release}/action.yml` — composites. +- `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). +- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `publish` / `security`). +- `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). +- `.github/workflows/{self,self-security,self-release}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), and the `v0` rolling-tag move. +- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned actions. - `security/.gitleaks.toml` — canonical gitleaks ruleset. - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). @@ -21,7 +23,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - **Imposed, not proposed.** Zero `inputs:` / `secrets:` on reusable workflows unless variation is legitimate. - **Pin third-party actions by commit SHA**, inline `# vX` comment. No `@main`, `@master`, `@vX`. - **Pin tooling binaries by version.** SHA-256 verification on binary release tarballs. No `curl | bash`. -- **Composite refs in this repo**: `coroboros/ci/.github/actions/@v0`. +- **Composite refs**: `coroboros/ci/.github/actions/@v0` from reusable workflows and consumers. Exception — `self-security.yml` uses local `./.github/actions/security/` so a PR self-tests its own composites; a reusable workflow's `./` resolves to the caller's checkout, so `security.yml` must pin `@v0`. - **`secrets:`** declares only what the job consumes. Never `secrets: inherit`. - **`gitleaks` CLI direct**, not `gitleaks/gitleaks-action@v2` (paid org license). - **House style**: @@ -40,6 +42,5 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - PR-only; no direct commits to `main`. - In the PR (before merge): bump `package.json:version` + prepend `CHANGELOG.md` section (`## vX.Y.Z - DD/MM/YYYY`). - Squash-merge. -- `git tag X.Y.Z && git push origin X.Y.Z` (no `v` prefix). -- `git tag -f v0 && git push -f origin v0` (rolling major). +- `git tag X.Y.Z && git push origin X.Y.Z` (no `v` prefix). `self-release.yml` then moves the rolling `v0` tag — no manual `git tag -f v0`. - `gh release create X.Y.Z --title X.Y.Z --notes-file `. diff --git a/README.md b/README.md index 65172d9..5de4cc0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Drop into any `@coroboros/*` repo via `uses: coroboros/ci/.github/workflows/ -**Imposed, not proposed.** Pipelines expose zero `inputs:` — same install flags, same publish auth, same security baseline across every Coroboros repo. Consumers wire it in. +**Imposed, not proposed.** Pipelines expose zero `inputs:`. Every Coroboros repo inherits identical install flags, publish auth, and security gates. Consumers wire it in. - [Pipelines](#pipelines) - [Composable actions](#composable-actions) @@ -58,12 +58,23 @@ Consumer requirements: +
+supply-chain + +
+ +**Trigger**: `every push`. Gates `publish` via `needs:` — a release waits on it. + +`osv-scanner` scans `pnpm-lock.yaml` against [OSV.dev](https://osv.dev/). `javascript/base` already gates the install (Socket Firewall + `--frozen-lockfile`); this adds the known-CVE gate so a vulnerable dependency blocks the release, not just the parallel `security` scan. See [Security](#security). + +
+
publish
-**Trigger**: `tag push` +**Trigger**: `tag push`. Gated by `supply-chain` (`needs:`) — osv-scanner must pass first. **Sequence**: 1. Checkout `main` with full history @@ -94,11 +105,11 @@ Calls `security.yml` — see [Security](#security). Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: -- `rust-toolchain.toml` — pins the channel and components (`rustup` installs them on first `cargo` use). +- `rust-toolchain.toml` — pins the channel and lists the `clippy` + `rustfmt` components (`rustup` installs them on first `cargo` use; omit them and `fmt`/`clippy` fail on a pinned channel). - `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. - `deny.toml` — the cargo-deny supply-chain policy (sources, licenses, bans, advisories). See [Security](#security) for the baseline. - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. -- `CARGO_REGISTRY_TOKEN` secret — optional. Enables the crates.io publish on tag. +- crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish.
preflight @@ -119,9 +130,9 @@ Consumer requirements:
-**Trigger**: `branch push` +**Trigger**: `every push`. Gates `publish` via `needs:` — a release waits on it. -`cargo-deny check` (SHA-pinned action) reads `deny.toml`: crate sources, licenses, bans, and advisories (vulnerabilities, unmaintained, yanked). See [Security](#security). +`cargo-deny check` (SHA-pinned action) reads `deny.toml`: crate sources, licenses, bans, and advisories (vulnerabilities, unmaintained, yanked). Running on every push re-checks a tagged release against the latest advisory DB before it ships, not only at PR time. See [Security](#security).
@@ -130,17 +141,18 @@ Consumer requirements:
-**Trigger**: `tag push` +**Trigger**: `tag push`. Gated by `supply-chain` (`needs:`) — cargo-deny must pass first. **Sequence**: 1. Checkout `main` with full history 2. Verify `main` HEAD matches the tag SHA 3. Run [`check-docs`](#composable-actions) -4. Pin `Cargo.toml` to the tag (`cargo set-version`) -5. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) -6. `cargo publish` to crates.io when `CARGO_REGISTRY_TOKEN` is set (absent → skipped) -7. Create GitHub Release via [`release/github-release`](#composable-actions) -8. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` via [`release/commit-artifacts`](#composable-actions) +4. Run [`rust/native-deps`](#composable-actions) — native deps for the publish verify build +5. Pin `Cargo.toml` to the tag (`cargo set-version`) +6. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) +7. `cargo publish` to crates.io — OIDC by default, token bootstrap for a new crate (see [Security](#security)) +8. Create GitHub Release via [`release/github-release`](#composable-actions) +9. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` via [`release/commit-artifacts`](#composable-actions)
@@ -159,13 +171,13 @@ Reusable sub-workflow with three parallel scans: - **`gitleaks`** — Installs `v8.30.1` (SHA-256 verified), scans git history with the [`security/.gitleaks.toml`](security/.gitleaks.toml) ruleset, fails on detected leaks. Emits SARIF as the `gitleaks-report` artifact (30-day retention). - **`dependency-review`** — PR-only; needs repo's **Dependency graph** enabled. Fails on high-severity CVE introduced by the dep diff. Uses `actions/dependency-review-action@v4`. -- **`osv-scanner`** — Scans lockfiles recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`. Fails on any known vulnerability. +- **`osv-scanner`** — Scans dependency manifests recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`, failing on any known vulnerability. Runs only when a supported manifest is present — `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `go.mod`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, `uv.lock`, `Gemfile.lock`, `composer.lock`. A repo with none — docs, config — skips the scan rather than failing on osv's no-manifest error. Extend the list in the `security/osv-scanner` composite as ecosystems land. -Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#examples). +`gitleaks` and `osv-scanner` wrap the [`security/*` composites](#composable-actions) — the same definitions the package `supply-chain` gates reuse; `dependency-review` is inline. Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#examples). --- -**Notes** — pin via `@v0` (rolling major, auto-bumped on each release) or `@x.y.z` (immutable). Pipelines don't chain via `needs:`; the only sub-workflow call is `security` → `security.yml`. +**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs: supply-chain` so the release is re-checked (cargo-deny for Rust, osv-scanner for npm) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. --- @@ -175,7 +187,11 @@ Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#exam | :--- | :--- | :--- | | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | -| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs the optional `ci/setup.sh` native-dep hook, then `cargo fmt --check`, `clippy -D warnings`, `test`. | +| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | +| `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | +| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml` and self-CI. | +| `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml` and the npm `supply-chain` gate. | +| `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | @@ -227,7 +243,7 @@ Section format: `## vX.Y.Z - DD/MM/YYYY`. Idempotent. Reuses an existing hand-cu ## Environment -Zero inputs on pipelines and on every composite — imposed, not proposed. Configuration flows through the caller's `secrets:` block. Every npm-publish-related value is a **secret** (encrypted at rest, masked in logs); none of them are GitHub `vars`. +Zero `inputs:` — configuration flows through the caller's `secrets:` block. Every npm-publish-related value is a **secret** (encrypted at rest, masked in logs); none are GitHub `vars`.
Secrets (caller's secrets: block) @@ -249,12 +265,7 @@ Zero inputs on pipelines and on every composite — imposed, not proposed. Confi
-| env | required | description | -| :-- | :---: | :--- | -| `NPM_CONFIG_FILE` | ✔ — fail if missing | `.npmrc` content | -| `NPM_EXTRA_CONFIG` | | Appended after `NPM_CONFIG_FILE` | - -Set both at the caller's workflow- or job-level `env:`. +A composite reads `env`, not `secrets:`. Composing `javascript/base` outside the bundled pipeline, set the same `NPM_CONFIG_FILE` (required, fails if missing) and `NPM_EXTRA_CONFIG` (optional) from the Secrets table above at the caller's workflow- or job-level `env:`.
@@ -304,7 +315,7 @@ pnpm CLI resolved via corepack from `packageManager`. No floating version reache `javascript/base` installs Socket Firewall (`npm i -g sfw`, the free tokenless build) and runs the install through it (`sfw pnpm install ...`). `sfw` proxies the registry fetch and blocks packages Socket has confirmed malicious before they download. It is the GitHub-runner equivalent of the image-baked firewall in the GitLab pipeline. -Fail-closed: if `sfw` cannot install or run, the install step fails rather than fetching unprotected. The free build needs no account or token, and inspects public-registry fetches out of the box. The trade-off of the runtime install (no shared image) is a dependency on Socket's service at fetch time. +Fail-closed: if `sfw` cannot install or run, the install step fails rather than fetching unprotected. The free build needs no account or token, and inspects public-registry fetches out of the box. Packages pulled through a private proxy registry pass uninspected — `sfw` knows the public npm registry, and the release-age [cooldown](#security) still covers them. The trade-off of the runtime install (no shared image) is a dependency on Socket's service at fetch time. @@ -341,7 +352,7 @@ The GitLab pipeline hardens npm at the image layer — cooldown, Socket Firewall | License drift | `cargo-deny` licenses — allow-list | | Banned or wildcard dependency | `cargo-deny` bans | -`cargo-deny` runs on every branch push via a SHA-pinned action, reading the repo's `deny.toml`. The baseline: +`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. It reads the repo's `deny.toml`. The baseline: ```toml [advisories] @@ -365,6 +376,8 @@ unknown-registry = "deny" unknown-git = "deny" ``` +**Publish auth.** crates.io publish uses OIDC Trusted Publishing by default — `rust-lang/crates-io-auth-action` mints a short-lived token per run, no long-lived secret in the repo. `CARGO_REGISTRY_TOKEN` is needed only to bootstrap the first publish of a new crate (Trusted Publishing binds to an existing crate); configure Trusted Publishing on crates.io afterwards and drop the token. The verify build runs on publish (no `--no-verify`). It compiles the packaged tarball standalone, catching a crate that only builds in-workspace before the immutable release lands. + Two residual risks have no clean CI control. Both are documented here: - **Build scripts run.** `cargo` has no `--ignore-scripts`; `build.rs` and proc-macros execute at build time. `--locked`, `cargo-deny` bans, and dependency review reduce the exposure; they do not remove it. - **No publish cooldown.** crates.io has no `minimumReleaseAge`, so a freshly hijacked version is held off by the committed lock and `cargo-deny` advisories rather than a time delay. @@ -395,7 +408,7 @@ prefer-online=true | `save-exact=true` | Pin exact versions on `add` / `install`. | | `fund=false` | Suppress funding noise in CI logs. | | `audit=false` | `osv-scanner` (in `security.yml`) covers vulnerability scans natively. | -| `ignore-scripts=true` | Belt-and-suspenders against postinstall supply-chain attacks — backs up the `--ignore-scripts` flag already passed by `javascript/base` on every `pnpm install`. | +| `ignore-scripts=true` | Defense in depth against postinstall supply-chain attacks — backs up the `--ignore-scripts` flag already passed by `javascript/base` on every `pnpm install`. | | `package-lock=false` | Prevent `npm` from emitting a parasitic `package-lock.json` in pnpm repos. | | `lockfile=true` | Explicit `pnpm-lock.yaml` enablement. Required on pnpm `< 11.0.0` consumers, where the preceding `package-lock=false` is interpreted as `lockfile=false` and collides with `pnpm install --frozen-lockfile`. Pnpm `>= 11` already defaults to `true` and ignores `package-lock` for `pnpm-lock.yaml`, so the line is harmless there. | | `prefer-online=true` | Re-fetch dep metadata each install — local cache cannot mask a yanked or republished version. | @@ -458,7 +471,7 @@ Self-CI binaries pinned by version. `actionlint` and `gitleaks` install from rel Canonical ruleset at `security/.gitleaks.toml` in this repo. Stack-specific rules cover Resend, Neon Postgres, PostHog, and GitHub fine-grained PATs on top of the gitleaks defaults. -`security.yml` sparse-checks the file out of `coroboros/ci` at runtime — imposed, no consumer override. +The `security/gitleaks` composite sparse-checks the file out of `coroboros/ci` at runtime — imposed, no consumer override. @@ -498,6 +511,34 @@ jobs: +
+rust-packages.yml wire-up + +
+ +```yaml +# consumer-repo/.github/workflows/ci.yml +name: CI +on: + push: + branches: [develop, main] + tags: ['*'] + pull_request: + workflow_dispatch: + +jobs: + ci: + uses: coroboros/ci/.github/workflows/rust-packages.yml@v0 + permissions: + contents: write # GitHub Release + commit-back on tag + id-token: write # crates.io OIDC publish on tag + secrets: + # First publish of a new crate only — drop once Trusted Publishing is configured (see Security): + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} +``` + +
+
security.yml standalone (non-npm repo) @@ -566,7 +607,7 @@ jobs: fetch-depth: 0 - id: changelog uses: coroboros/ci/.github/actions/release/generate-changelog@v0 - # ...your publish step (docker push, gh release upload, etc.)... + # ...the publish step (docker push, gh release upload, etc.)... - uses: coroboros/ci/.github/actions/release/github-release@v0 with: body: ${{ steps.changelog.outputs.body }} diff --git a/package.json b/package.json index 69a261b..6e82b0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coroboros/ci", - "version": "0.1.13", + "version": "0.2.0", "private": true, "description": "Reusable GitHub Actions CI for the Coroboros stack.", "license": "SEE LICENSE IN LICENSE.md", From 958a678d8e52db2c1553e7d7887c0797f94d2f81 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:22:08 +0700 Subject: [PATCH 12/36] fix(security): drop unscannable bun.lockb from osv gate [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit osv-scanner can't parse the binary bun.lockb (only the text bun.lock), so a bun.lockb-only repo tripped the gate: detect fires, osv resolves nothing, exits 128, the job fails spuriously — the exact case the manifest gate exists to avoid. Add pdm.lock (PDM, osv-supported) so the set matches coroboros/security/ci one-to-one (13 lockfiles). --- .github/actions/security/osv-scanner/action.yml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/security/osv-scanner/action.yml b/.github/actions/security/osv-scanner/action.yml index 28fea01..6a596cd 100644 --- a/.github/actions/security/osv-scanner/action.yml +++ b/.github/actions/security/osv-scanner/action.yml @@ -13,10 +13,10 @@ runs: # dependency repo carries one of these — extend the list as ecosystems land. run: | manifests=( - package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb + package-lock.json pnpm-lock.yaml yarn.lock bun.lock Cargo.lock go.mod - requirements.txt poetry.lock Pipfile.lock uv.lock + requirements.txt poetry.lock Pipfile.lock pdm.lock uv.lock Gemfile.lock composer.lock ) diff --git a/README.md b/README.md index 5de4cc0..d10ddf2 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Reusable sub-workflow with three parallel scans: - **`gitleaks`** — Installs `v8.30.1` (SHA-256 verified), scans git history with the [`security/.gitleaks.toml`](security/.gitleaks.toml) ruleset, fails on detected leaks. Emits SARIF as the `gitleaks-report` artifact (30-day retention). - **`dependency-review`** — PR-only; needs repo's **Dependency graph** enabled. Fails on high-severity CVE introduced by the dep diff. Uses `actions/dependency-review-action@v4`. -- **`osv-scanner`** — Scans dependency manifests recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`, failing on any known vulnerability. Runs only when a supported manifest is present — `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `go.mod`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, `uv.lock`, `Gemfile.lock`, `composer.lock`. A repo with none — docs, config — skips the scan rather than failing on osv's no-manifest error. Extend the list in the `security/osv-scanner` composite as ecosystems land. +- **`osv-scanner`** — Scans dependency manifests recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`, failing on any known vulnerability. Runs only when a supported manifest is present — `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `Cargo.lock`, `go.mod`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, `pdm.lock`, `uv.lock`, `Gemfile.lock`, `composer.lock`. A repo with none — docs, config — skips the scan rather than failing on osv's no-manifest error. Extend the list in the `security/osv-scanner` composite as ecosystems land. `gitleaks` and `osv-scanner` wrap the [`security/*` composites](#composable-actions) — the same definitions the package `supply-chain` gates reuse; `dependency-review` is inline. Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#examples). From b47a595e4fdde6d85424ee975d46ecd6e7753137 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 18:50:15 +0700 Subject: [PATCH 13/36] docs(security): note osv-scanner.toml exceptions [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d10ddf2..94fee1e 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Reusable sub-workflow with three parallel scans: - **`gitleaks`** — Installs `v8.30.1` (SHA-256 verified), scans git history with the [`security/.gitleaks.toml`](security/.gitleaks.toml) ruleset, fails on detected leaks. Emits SARIF as the `gitleaks-report` artifact (30-day retention). - **`dependency-review`** — PR-only; needs repo's **Dependency graph** enabled. Fails on high-severity CVE introduced by the dep diff. Uses `actions/dependency-review-action@v4`. -- **`osv-scanner`** — Scans dependency manifests recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`, failing on any known vulnerability. Runs only when a supported manifest is present — `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `Cargo.lock`, `go.mod`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, `pdm.lock`, `uv.lock`, `Gemfile.lock`, `composer.lock`. A repo with none — docs, config — skips the scan rather than failing on osv's no-manifest error. Extend the list in the `security/osv-scanner` composite as ecosystems land. +- **`osv-scanner`** — Scans dependency manifests recursively against [OSV.dev](https://osv.dev/) via `google/osv-scanner-action@v2`, failing on any known vulnerability. Runs only when a supported manifest is present — `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `Cargo.lock`, `go.mod`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, `pdm.lock`, `uv.lock`, `Gemfile.lock`, `composer.lock`. A repo with none — docs, config — skips the scan rather than failing on osv's no-manifest error. Per-repo exceptions go in an `osv-scanner.toml` at the repo root (`[[IgnoredVulns]]`). Extend the list in the `security/osv-scanner` composite as ecosystems land. `gitleaks` and `osv-scanner` wrap the [`security/*` composites](#composable-actions) — the same definitions the package `supply-chain` gates reuse; `dependency-review` is inline. Imposed on every Coroboros workflow. Standalone wire-up — see [Examples](#examples). From 66444ef1e276618b50b555b8ee6873bae1a9bc6d Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 19:05:47 +0700 Subject: [PATCH 14/36] feat(security): gate publish on gitleaks via a secrets job [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a secrets job (gitleaks composite, full history) to the npm and Rust package workflows; publish now needs [supply-chain, secrets]. A leaked secret blocks the release through the template's needs: graph, not the consumer's branch protection — parity with the GitLab security-gate stage. --- .github/workflows/javascript-npm-packages.yml | 14 ++++++++- .github/workflows/rust-packages.yml | 14 ++++++++- CHANGELOG.md | 1 + README.md | 30 ++++++++++++++++--- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 78e60c5..282a651 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -45,9 +45,21 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/osv-scanner@v0 + # gitleaks gates the release on a leaked secret, enforced by the template's + # `needs:` rather than the consumer's branch protection — the GitLab + # `security-gate` equivalent. Runs on every push and gates `publish`. The + # `security` job runs gitleaks again in parallel for the SARIF report. + secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: coroboros/ci/.github/actions/security/gitleaks@v0 + publish: if: ${{ github.ref_type == 'tag' }} - needs: supply-chain # no release ships until osv-scanner passes + needs: [supply-chain, secrets] # no release ships until osv-scanner and gitleaks pass runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 4e14224..f1a2509 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -35,9 +35,21 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/cargo-deny@v0 + # gitleaks gates the release on a leaked secret, enforced by the template's + # `needs:` rather than the consumer's branch protection — the GitLab + # `security-gate` equivalent. Runs on every push and gates `publish`. The + # `security` job runs gitleaks again in parallel for the SARIF report. + secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: coroboros/ci/.github/actions/security/gitleaks@v0 + publish: if: ${{ github.ref_type == 'tag' }} - needs: supply-chain # no release ships until cargo-deny passes + needs: [supply-chain, secrets] # no release ships until cargo-deny and gitleaks pass runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edae2b..c17e10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the publish verify build. - `javascript/base` — wrap `pnpm install` in Socket Firewall (`sfw`), blocking confirmed-malicious packages before download. Fail-closed. The GitHub-runner equivalent of the image-baked firewall on GitLab. - `self-release` — move the rolling `v0` major tag to each stable `coroboros/ci` release, so `@v0` consumers track the latest release without a manual tag push. +- `secrets` gate — gitleaks gates `publish` via `needs:` in the npm and Rust package workflows, alongside the supply-chain gate. A leaked secret blocks the release through the template's job graph, not the consumer's branch protection — parity with the GitLab `security-gate` stage. ### Fixes - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. diff --git a/README.md b/README.md index 94fee1e..005d985 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,23 @@ Consumer requirements:
+
+secrets + +
+ +**Trigger**: `every push`. Gates `publish` via `needs:` — a release waits on it. + +`gitleaks` scans the full git history with the canonical ruleset. The release-gating twin of the parallel `security` scan, so a leaked secret blocks the publish through the template's `needs:` rather than per-repo branch protection. See [Security](#security). + +
+
publish
-**Trigger**: `tag push`. Gated by `supply-chain` (`needs:`) — osv-scanner must pass first. +**Trigger**: `tag push`. Gated by `supply-chain` and `secrets` (`needs:`) — osv-scanner and gitleaks must pass first. **Sequence**: 1. Checkout `main` with full history @@ -136,12 +147,23 @@ Consumer requirements:
+
+secrets + +
+ +**Trigger**: `every push`. Gates `publish` via `needs:` — a release waits on it. + +`gitleaks` scans the full git history with the canonical ruleset. The release-gating twin of the parallel `security` scan, so a leaked secret blocks the publish through the template's `needs:` rather than per-repo branch protection. See [Security](#security). + +
+
publish
-**Trigger**: `tag push`. Gated by `supply-chain` (`needs:`) — cargo-deny must pass first. +**Trigger**: `tag push`. Gated by `supply-chain` and `secrets` (`needs:`) — cargo-deny and gitleaks must pass first. **Sequence**: 1. Checkout `main` with full history @@ -177,7 +199,7 @@ Reusable sub-workflow with three parallel scans: --- -**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs: supply-chain` so the release is re-checked (cargo-deny for Rust, osv-scanner for npm) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. +**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs: [supply-chain, secrets]` so the release is re-checked (cargo-deny or osv-scanner, plus gitleaks) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. --- @@ -189,7 +211,7 @@ Reusable sub-workflow with three parallel scans: | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | -| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml` and self-CI. | +| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml` and the npm `supply-chain` gate. | | `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | From 96f3e74e35c4f67a4d99c2173979a28a37f72461 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 19:20:46 +0700 Subject: [PATCH 15/36] style(security): trim what-clause from secrets comment [skip ci] --- .github/workflows/javascript-npm-packages.yml | 4 ++-- .github/workflows/rust-packages.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 282a651..dcb47f7 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -47,8 +47,8 @@ jobs: # gitleaks gates the release on a leaked secret, enforced by the template's # `needs:` rather than the consumer's branch protection — the GitLab - # `security-gate` equivalent. Runs on every push and gates `publish`. The - # `security` job runs gitleaks again in parallel for the SARIF report. + # `security-gate` equivalent. The `security` job runs gitleaks again in + # parallel for the SARIF report. secrets: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index f1a2509..55a4e29 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -37,8 +37,8 @@ jobs: # gitleaks gates the release on a leaked secret, enforced by the template's # `needs:` rather than the consumer's branch protection — the GitLab - # `security-gate` equivalent. Runs on every push and gates `publish`. The - # `security` job runs gitleaks again in parallel for the SARIF report. + # `security-gate` equivalent. The `security` job runs gitleaks again in + # parallel for the SARIF report. secrets: runs-on: ubuntu-latest steps: From d95f3e3167b8720773f882c69c00af0eb9023293 Mon Sep 17 00:00:00 2001 From: OB Date: Tue, 2 Jun 2026 19:43:10 +0700 Subject: [PATCH 16/36] docs(security): note self-CI in the osv-scanner composable row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symmetry with the gitleaks row — osv-scanner also runs in self-security.yml. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 005d985..fd7c471 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Reusable sub-workflow with three parallel scans: | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | | `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | -| `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml` and the npm `supply-chain` gate. | +| `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | From 5cfe545e8910f4b803b10d120022e9535b6ae9bd Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 12:04:13 +0700 Subject: [PATCH 17/36] feat(rust): add cargo-dist binary-distribution layer to rust-packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opt-in via [package.metadata.dist] in the consumer's Cargo.toml. A tagged binary repo gets prebuilt archives per target, shell/powershell installers, a Homebrew formula in the declared tap, and an npm shim — attached to the one GitHub Release the pipeline already creates, alongside the crates.io publish. The shared pipeline stays the sole release authority: dist only builds (final asset URLs derive from repo + tag), and the release goes live via draft → undraft so installers and the formula resolve against published URLs. Library crates without dist metadata are unchanged — every binary job self-skips. Adds dist-plan/build/host/publish (binary builds need the cargo-deny and gitleaks gates), the release/dist install composite (dist 0.32.0, cargo install --locked), and a draft input on release/github-release. Bumps to 0.3.0. --- .github/actions/release/dist/action.yml | 18 ++ .../actions/release/github-release/action.yml | 12 +- .github/workflows/rust-packages.yml | 259 +++++++++++++++++- CHANGELOG.md | 13 + README.md | 46 +++- package.json | 2 +- 6 files changed, 343 insertions(+), 7 deletions(-) create mode 100644 .github/actions/release/dist/action.yml diff --git a/.github/actions/release/dist/action.yml b/.github/actions/release/dist/action.yml new file mode 100644 index 0000000..3111ee8 --- /dev/null +++ b/.github/actions/release/dist/action.yml @@ -0,0 +1,18 @@ +# Release cargo-dist Install Action Composite +name: release-dist +description: Install cargo-dist (the `dist` binary), version-pinned, on the runner. + +runs: + using: composite + steps: + - name: Install cargo-dist + shell: bash + env: + DIST_VERSION: "0.32.0" + run: | + if command -v dist >/dev/null 2>&1; then + echo "::notice::dist already on PATH: $(dist --version)" + else + cargo install cargo-dist --version "${DIST_VERSION}" --locked + dist --version + fi diff --git a/.github/actions/release/github-release/action.yml b/.github/actions/release/github-release/action.yml index 5e7a7fc..5dfc1db 100644 --- a/.github/actions/release/github-release/action.yml +++ b/.github/actions/release/github-release/action.yml @@ -6,6 +6,10 @@ inputs: body: description: Release notes body. required: true + draft: + description: Create the release as a draft. Binary repos undraft after assets upload; library repos leave it false. + required: false + default: "false" runs: using: composite @@ -15,10 +19,16 @@ runs: env: GH_TOKEN: ${{ github.token }} BODY: ${{ inputs.body }} + DRAFT: ${{ inputs.draft }} run: | notes="$(mktemp)" printf '%s' "${BODY}" > "${notes}" + draft_flag=() + if [ "${DRAFT}" = "true" ]; then + draft_flag=(--draft) + fi gh release create "${GITHUB_REF_NAME}" \ --title "${GITHUB_REF_NAME}" \ - --notes-file "${notes}" + --notes-file "${notes}" \ + "${draft_flag[@]}" rm -f "${notes}" diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 55a4e29..e438ee6 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -6,6 +6,10 @@ on: secrets: CARGO_REGISTRY_TOKEN: required: false + HOMEBREW_TAP_TOKEN: + required: false + NPM_PACKAGE_REGISTRY_TOKEN: + required: false permissions: contents: read @@ -47,9 +51,120 @@ jobs: fetch-depth: 0 - uses: coroboros/ci/.github/actions/security/gitleaks@v0 + # Opt-in binary layer. Runs only when the consumer's Cargo.toml declares + # [package.metadata.dist]; `enabled` gates dist-build/host/publish and the + # release `draft` flag, so library crates skip the whole layer with zero config. + dist-plan: + if: ${{ github.ref_type == 'tag' }} + runs-on: ubuntu-latest + outputs: + enabled: ${{ steps.detect.outputs.enabled }} + matrix: ${{ steps.plan.outputs.matrix }} + tap: ${{ steps.detect.outputs.tap }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: main + fetch-depth: 0 + + - name: Verify tag points to main HEAD + shell: bash + run: | + if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then + echo "::error::main has moved since the tag was pushed. Resolve manually." + echo " Tag SHA: ${GITHUB_SHA}" + echo " main HEAD: $(git rev-parse HEAD)" + exit 1 + fi + echo "main HEAD matches tag SHA (${GITHUB_SHA})" + + - id: detect + name: Detect cargo-dist metadata + shell: bash + run: | + if grep -qE '^\[package\.metadata\.dist\]' Cargo.toml 2>/dev/null; then + echo "::notice::cargo-dist metadata present — binary distribution enabled" + tap="$(grep -E '^[[:space:]]*tap[[:space:]]*=' Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + { + echo "enabled=true" + echo "tap=${tap}" + } >> "${GITHUB_OUTPUT}" + else + echo "::notice::no [package.metadata.dist] — binary jobs skip" + { + echo "enabled=false" + echo "tap=" + } >> "${GITHUB_OUTPUT}" + fi + + - if: ${{ steps.detect.outputs.enabled == 'true' }} + uses: coroboros/ci/.github/actions/release/dist@v0 + + - id: plan + if: ${{ steps.detect.outputs.enabled == 'true' }} + name: Pin version and compute build matrix + shell: bash + run: | + cargo install cargo-edit --locked + cargo set-version "${GITHUB_REF_NAME}" + dist plan --tag="${GITHUB_REF_NAME}" --output-format=json > plan-dist-manifest.json + echo "matrix=$(jq -c '.ci.github.artifacts_matrix' plan-dist-manifest.json)" >> "${GITHUB_OUTPUT}" + + - if: ${{ steps.detect.outputs.enabled == 'true' }} + name: Upload plan manifest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dist-plan-manifest + path: plan-dist-manifest.json + if-no-files-found: error + + dist-build: + needs: [dist-plan, supply-chain, secrets] # no artifact builds until the gates pass + if: ${{ needs.dist-plan.outputs.enabled == 'true' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.dist-plan.outputs.matrix) }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: main + fetch-depth: 0 + + - uses: coroboros/ci/.github/actions/release/dist@v0 + + - name: Pin Cargo.toml to tag + shell: bash + run: | + cargo install cargo-edit --locked + cargo set-version "${GITHUB_REF_NAME}" + + - uses: coroboros/ci/.github/actions/rust/native-deps@v0 + + # Plan-provided cross/system toolchains; empty for a native target. + - name: Install build system dependencies + if: ${{ matrix.packages_install }} + shell: bash + run: ${{ matrix.packages_install }} + + # matrix.dist_args carries --artifacts=local --target= from `dist plan`. + # dist's own curl|sh installer (matrix.install_dist) is deliberately ignored — + # release/dist is the house-style, version-pinned install. + - name: Build local artifacts + shell: bash + run: dist build --tag="${GITHUB_REF_NAME}" ${{ matrix.dist_args }} + + - name: Upload local artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dist-build-${{ join(matrix.targets, '_') }} + path: target/distrib/ + if-no-files-found: error + publish: if: ${{ github.ref_type == 'tag' }} - needs: [supply-chain, secrets] # no release ships until cargo-deny and gitleaks pass + needs: [supply-chain, secrets, dist-plan] # no release ships until cargo-deny and gitleaks pass runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main @@ -108,10 +223,152 @@ jobs: - uses: coroboros/ci/.github/actions/release/github-release@v0 with: body: ${{ steps.changelog.outputs.body }} + draft: ${{ needs.dist-plan.outputs.enabled }} # draft for binary repos; dist-host undrafts - uses: coroboros/ci/.github/actions/release/commit-artifacts@v0 with: files: Cargo.toml Cargo.lock CHANGELOG.md + dist-host: + needs: [dist-plan, dist-build, publish] + if: ${{ needs.dist-plan.outputs.enabled == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: write # upload release assets + undraft the release publish created + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: main + fetch-depth: 0 + + - uses: coroboros/ci/.github/actions/release/dist@v0 + + - name: Pin Cargo.toml to tag + shell: bash + run: | + cargo install cargo-edit --locked + cargo set-version "${GITHUB_REF_NAME}" + + - name: Download build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: dist-build-* + path: target/distrib/ + merge-multiple: true + + # The global build reads the same manifest `dist plan` produced (URL derivation). + - name: Download plan manifest + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: dist-plan-manifest + path: target/distrib/ + + # Final asset URLs are deterministic (repo + tag), so --tag is enough to + # embed non-draft download links with no dist-owned release. + - name: Build global artifacts + shell: bash + run: dist build --tag="${GITHUB_REF_NAME}" --artifacts=global --output-format=json > target/distrib/dist-manifest.json + + # Upload archives + installers + checksums, then undraft — so the formula + # and npm shim (next job) resolve against a live release, not a draft. + - name: Upload release assets and undraft + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + shopt -s nullglob + assets=() + for f in target/distrib/*; do + case "${f}" in + *.json) ;; + *) [ -f "${f}" ] && assets+=("${f}") ;; + esac + done + if [ "${#assets[@]}" -gt 0 ]; then + gh release upload "${GITHUB_REF_NAME}" "${assets[@]}" --clobber + fi + gh release edit "${GITHUB_REF_NAME}" --draft=false + + - name: Upload global artifacts for publish + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dist-global + path: | + target/distrib/*.rb + target/distrib/*-npm-package.tar.gz + if-no-files-found: ignore + + dist-publish: + needs: [dist-plan, dist-host] + if: ${{ needs.dist-plan.outputs.enabled == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # npm OIDC provenance + env: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + NPM_PACKAGE_REGISTRY_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }} + steps: + - name: Download global artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: dist-global + path: dist-global + + - name: Checkout Homebrew tap + if: ${{ env.HOMEBREW_TAP_TOKEN != '' && needs.dist-plan.outputs.tap != '' }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: ${{ needs.dist-plan.outputs.tap }} + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-tap + + - name: Publish Homebrew formula + if: ${{ env.HOMEBREW_TAP_TOKEN != '' && needs.dist-plan.outputs.tap != '' }} + shell: bash + run: | + shopt -s nullglob + formulas=(dist-global/*.rb) + if [ "${#formulas[@]}" -eq 0 ]; then + echo "::notice::no Homebrew formula generated — skipping" + exit 0 + fi + git -C homebrew-tap config user.name "github-actions[bot]" + git -C homebrew-tap config user.email "41898282+github-actions[bot]@users.noreply.github.com" + mkdir -p homebrew-tap/Formula + for f in "${formulas[@]}"; do + cp "${f}" "homebrew-tap/Formula/$(basename "${f}")" + git -C homebrew-tap add "Formula/$(basename "${f}")" + done + git -C homebrew-tap commit -m "release ${GITHUB_REF_NAME}" + git -C homebrew-tap push + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + # OIDC + provenance by default; NPM_PACKAGE_REGISTRY_TOKEN only bootstraps + # the first publish of a new scoped package. Mirrors javascript-npm-packages. + - name: Publish npm shim + shell: bash + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }} + run: | + shopt -s nullglob + shims=(dist-global/*-npm-package.tar.gz) + if [ "${#shims[@]}" -eq 0 ]; then + echo "::notice::no npm shim generated — skipping" + exit 0 + fi + for pkg in "${shims[@]}"; do + if [ -n "${NPM_PACKAGE_REGISTRY_TOKEN}" ]; then + npm publish --access public "${pkg}" + else + npm publish --provenance --access public "${pkg}" + fi + done + security: uses: ./.github/workflows/security.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index c17e10e..45c74b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v0.3.0 - 03/06/2026 + +### Features +- `rust-packages` — opt-in binary-distribution layer driven by cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]` in the consumer's `Cargo.toml`. A tagged binary repo gets prebuilt archives for each declared target, `shell` + `powershell` installers, a Homebrew formula committed to the declared `tap`, and a published npm shim — attached to the single GitHub Release the pipeline already creates, alongside the crates.io publish. The shared pipeline stays the sole release authority: `dist` only builds (final asset URLs derive from repo + tag), and the release goes live through draft → undraft so installers and the formula resolve against published download URLs. Library crates without dist metadata are unchanged — every binary job self-skips. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish`; the binary builds `needs:` the `cargo-deny` and `gitleaks` gates, so no artifact builds before the release gates pass. +- `release/dist` — composite installing `dist` version-pinned (`cargo install cargo-dist --version 0.32.0 --locked`), shared by the binary jobs. +- `release/github-release` — add a `draft` input so binary repos draft-then-undraft while library repos create non-draft as before. + +### Documentation +- `README` — document the opt-in binary-distribution layer (the four jobs, the consumer contract, the optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets), add `release/dist` to the composables table, and note the cargo-dist 0.32.0 per-target-features limitation plus deferred macOS signing. + +### Configuration +- `package.json` — bump to `0.3.0`. + ## v0.2.0 - 02/06/2026 ### Features diff --git a/README.md b/README.md index fd7c471..a51b9c8 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Consumer requirements: - `deny.toml` — the cargo-deny supply-chain policy (sources, licenses, bans, advisories). See [Security](#security) for the baseline. - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. +- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt archives, shell/powershell installers, a Homebrew formula, and an npm shim to the release. Absent → source-only (crates.io), unchanged. Drop `release-plz`; the shared pipeline owns the release.
preflight @@ -173,11 +174,29 @@ Consumer requirements: 5. Pin `Cargo.toml` to the tag (`cargo set-version`) 6. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) 7. `cargo publish` to crates.io — OIDC by default, token bootstrap for a new crate (see [Security](#security)) -8. Create GitHub Release via [`release/github-release`](#composable-actions) +8. Create GitHub Release via [`release/github-release`](#composable-actions) — a draft when the consumer ships binaries (undrafted once assets upload), else final 9. Commit `Cargo.toml`, `Cargo.lock`, `CHANGELOG.md` back to `main` via [`release/commit-artifacts`](#composable-actions)
+
+binary distribution (opt-in) + +
+ +**Trigger**: `tag push`, only when `Cargo.toml` declares `[package.metadata.dist]` (cargo-dist `0.32.0`). Library crates skip every job below with zero config; the release stays non-draft as above. + +The shared pipeline is the sole release authority — `publish` creates the one GitHub Release (a draft for binary repos), and these jobs attach artifacts to it. cargo-dist (`dist`) only builds; it never creates or owns a release. + +- **`dist-plan`** — detects the metadata, pins the version, runs `dist plan` to compute the per-target build matrix. +- **`dist-build`** — matrix over the declared `targets`, gated by `supply-chain` and `secrets` (`needs:`); builds each prebuilt archive (`dist build --artifacts=local`). +- **`dist-host`** — builds the global installers + Homebrew formula + npm shim (`dist build --artifacts=global`; final download URLs derive from repo + tag), uploads every asset to the release, then undrafts it. +- **`dist-publish`** — commits the formula to the declared `tap` (`HOMEBREW_TAP_TOKEN`) and publishes the npm shim (OIDC + provenance, or `NPM_PACKAGE_REGISTRY_TOKEN` bootstrap). Each self-skips when its installer or secret is absent. + +`dist` is version-pinned via `cargo install cargo-dist --version 0.32.0 --locked`. Per-target Cargo features are not expressible in cargo-dist 0.32.0 — a build needing them (e.g. a Metal-accelerated macOS binary) resolves them consumer-side via `cfg(target_os = …)`. macOS Developer-ID signing + notarization are deferred. + +
+
security @@ -215,8 +234,9 @@ Reusable sub-workflow with three parallel scans: | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | -| `release/github-release` | transverse | Creates the GitHub Release for the current tag. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | +| `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | +| `release/dist` | Rust | Installs cargo-dist (the `dist` binary), version-pinned. Powers the opt-in binary-distribution jobs in `rust-packages.yml` (`dist plan` / `build`). | --- @@ -282,6 +302,21 @@ Zero `inputs:` — configuration flows through the caller's `secrets:` block. Ev
+
+Secrets — rust-packages.yml + +
+ +All optional. A consumer that wires none still gets crates.io plus prebuilt archives and installers on the Release; Homebrew and npm activate only when their secret (or OIDC) is configured. + +| name | required | description | +| :--- | :---: | :--- | +| `CARGO_REGISTRY_TOKEN` | | crates.io token. Bootstraps the first publish of a new crate; absent → OIDC Trusted Publishing. | +| `HOMEBREW_TAP_TOKEN` | | Push access to the Homebrew tap repo named by `tap` in `[package.metadata.dist]`. Absent → the formula publish self-skips. | +| `NPM_PACKAGE_REGISTRY_TOKEN` | | npm token bootstrapping the first publish of the binary npm shim; absent → OIDC + provenance. | + +
+
javascript/base env contract (standalone composition) @@ -480,7 +515,7 @@ Each `workflow_call.secrets:` block declares ONLY the secrets the job consumes. Third-party actions across workflows + composites are pinned to a commit SHA with an inline `# vX` comment. Floating refs (`@master`, `@main`, `@vX`) are banned. -Self-CI binaries pinned by version. `actionlint` and `gitleaks` install from release tarballs with SHA-256 verification; `yamllint` via `pip install` with version pin. No `curl | bash`. +Self-CI binaries pinned by version. `actionlint` and `gitleaks` install from release tarballs with SHA-256 verification; `yamllint` via `pip install` with version pin; `cargo-dist` via `cargo install --locked --version` (registry-checksum verified). No `curl | bash`. `.github/dependabot.yml` opens weekly grouped auto-PRs to bump pinned SHAs across `.github/workflows/*` and `.github/actions/**/action.yml`. Consumers should add their own ecosystem entries (e.g., `npm`). @@ -553,10 +588,13 @@ jobs: uses: coroboros/ci/.github/workflows/rust-packages.yml@v0 permissions: contents: write # GitHub Release + commit-back on tag - id-token: write # crates.io OIDC publish on tag + id-token: write # crates.io + npm OIDC publish on tag secrets: # First publish of a new crate only — drop once Trusted Publishing is configured (see Security): CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + # Binary distribution ([package.metadata.dist] in Cargo.toml) — both optional: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + NPM_PACKAGE_REGISTRY_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }} ```
diff --git a/package.json b/package.json index 6e82b0c..4b587e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coroboros/ci", - "version": "0.2.0", + "version": "0.3.0", "private": true, "description": "Reusable GitHub Actions CI for the Coroboros stack.", "license": "SEE LICENSE IN LICENSE.md", From 547d594da2d5596ec51178a94952102841fabe67 Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 12:21:13 +0700 Subject: [PATCH 18/36] refactor(rust): factor the Cargo version pin into a shared composite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cargo set-version tag pin was inlined in four jobs — publish plus the three dist jobs. Extract it to rust/pin-version, called from each: one source for the pin, consistent with the repo's composite-per-shared-step shape. Docs: tighten the README binary-distribution section, cut the duplicated artifact list (the details block is the one home), drop a stale needs: list and a misplaced cargo-dist pinning note, and fix the cfg(target_os) example. Record release/dist + rust/pin-version in CLAUDE.md and the changelog. --- .github/actions/rust/pin-version/action.yml | 12 ++++++++++ .github/workflows/rust-packages.yml | 25 ++++++--------------- CHANGELOG.md | 2 +- CLAUDE.md | 4 ++-- README.md | 15 +++++++------ 5 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 .github/actions/rust/pin-version/action.yml diff --git a/.github/actions/rust/pin-version/action.yml b/.github/actions/rust/pin-version/action.yml new file mode 100644 index 0000000..10cbeae --- /dev/null +++ b/.github/actions/rust/pin-version/action.yml @@ -0,0 +1,12 @@ +# Rust Pin Version Action Composite +name: rust-pin-version +description: Pin Cargo.toml to the current tag via cargo set-version. + +runs: + using: composite + steps: + - name: Pin Cargo.toml to tag + shell: bash + run: | + cargo install cargo-edit --locked + cargo set-version "${GITHUB_REF_NAME}" diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index e438ee6..8582754 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -100,13 +100,14 @@ jobs: - if: ${{ steps.detect.outputs.enabled == 'true' }} uses: coroboros/ci/.github/actions/release/dist@v0 + - if: ${{ steps.detect.outputs.enabled == 'true' }} + uses: coroboros/ci/.github/actions/rust/pin-version@v0 + - id: plan if: ${{ steps.detect.outputs.enabled == 'true' }} - name: Pin version and compute build matrix + name: Compute build matrix shell: bash run: | - cargo install cargo-edit --locked - cargo set-version "${GITHUB_REF_NAME}" dist plan --tag="${GITHUB_REF_NAME}" --output-format=json > plan-dist-manifest.json echo "matrix=$(jq -c '.ci.github.artifacts_matrix' plan-dist-manifest.json)" >> "${GITHUB_OUTPUT}" @@ -134,11 +135,7 @@ jobs: - uses: coroboros/ci/.github/actions/release/dist@v0 - - name: Pin Cargo.toml to tag - shell: bash - run: | - cargo install cargo-edit --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - uses: coroboros/ci/.github/actions/rust/native-deps@v0 @@ -193,11 +190,7 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/rust/native-deps@v0 - - name: Pin Cargo.toml to tag - shell: bash - run: | - cargo install cargo-edit --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - id: changelog uses: coroboros/ci/.github/actions/release/generate-changelog@v0 @@ -243,11 +236,7 @@ jobs: - uses: coroboros/ci/.github/actions/release/dist@v0 - - name: Pin Cargo.toml to tag - shell: bash - run: | - cargo install cargo-edit --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - name: Download build artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c74b0..d3e54c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features - `rust-packages` — opt-in binary-distribution layer driven by cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]` in the consumer's `Cargo.toml`. A tagged binary repo gets prebuilt archives for each declared target, `shell` + `powershell` installers, a Homebrew formula committed to the declared `tap`, and a published npm shim — attached to the single GitHub Release the pipeline already creates, alongside the crates.io publish. The shared pipeline stays the sole release authority: `dist` only builds (final asset URLs derive from repo + tag), and the release goes live through draft → undraft so installers and the formula resolve against published download URLs. Library crates without dist metadata are unchanged — every binary job self-skips. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish`; the binary builds `needs:` the `cargo-deny` and `gitleaks` gates, so no artifact builds before the release gates pass. -- `release/dist` — composite installing `dist` version-pinned (`cargo install cargo-dist --version 0.32.0 --locked`), shared by the binary jobs. +- `release/dist`, `rust/pin-version` — composites: install `dist` version-pinned (`cargo install cargo-dist --version 0.32.0 --locked`), and pin `Cargo.toml` to the tag (`cargo set-version`), shared across `publish` and the binary jobs. - `release/github-release` — add a `draft` input so binary repos draft-then-undraft while library repos create non-draft as before. ### Documentation diff --git a/CLAUDE.md b/CLAUDE.md index 3e3de9d..b3c8bac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,10 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. ## Important files - `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). -- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `publish` / `security`). +- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). - `.github/workflows/{self,self-security,self-release}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), and the `v0` rolling-tag move. -- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. +- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts,dist}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned actions. - `security/.gitleaks.toml` — canonical gitleaks ruleset. - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). diff --git a/README.md b/README.md index a51b9c8..3e651db 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Consumer requirements: - `deny.toml` — the cargo-deny supply-chain policy (sources, licenses, bans, advisories). See [Security](#security) for the baseline. - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. -- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt archives, shell/powershell installers, a Homebrew formula, and an npm shim to the release. Absent → source-only (crates.io), unchanged. Drop `release-plz`; the shared pipeline owns the release. +- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. Absent → source-only (crates.io), unchanged.
preflight @@ -186,14 +186,14 @@ Consumer requirements: **Trigger**: `tag push`, only when `Cargo.toml` declares `[package.metadata.dist]` (cargo-dist `0.32.0`). Library crates skip every job below with zero config; the release stays non-draft as above. -The shared pipeline is the sole release authority — `publish` creates the one GitHub Release (a draft for binary repos), and these jobs attach artifacts to it. cargo-dist (`dist`) only builds; it never creates or owns a release. +The shared pipeline is the sole release authority — `publish` creates the one GitHub Release (a draft for binary repos), and these jobs attach artifacts to it. cargo-dist (`dist`) only builds, never owns the release. - **`dist-plan`** — detects the metadata, pins the version, runs `dist plan` to compute the per-target build matrix. - **`dist-build`** — matrix over the declared `targets`, gated by `supply-chain` and `secrets` (`needs:`); builds each prebuilt archive (`dist build --artifacts=local`). - **`dist-host`** — builds the global installers + Homebrew formula + npm shim (`dist build --artifacts=global`; final download URLs derive from repo + tag), uploads every asset to the release, then undrafts it. - **`dist-publish`** — commits the formula to the declared `tap` (`HOMEBREW_TAP_TOKEN`) and publishes the npm shim (OIDC + provenance, or `NPM_PACKAGE_REGISTRY_TOKEN` bootstrap). Each self-skips when its installer or secret is absent. -`dist` is version-pinned via `cargo install cargo-dist --version 0.32.0 --locked`. Per-target Cargo features are not expressible in cargo-dist 0.32.0 — a build needing them (e.g. a Metal-accelerated macOS binary) resolves them consumer-side via `cfg(target_os = …)`. macOS Developer-ID signing + notarization are deferred. +`dist` is version-pinned via `cargo install cargo-dist --version 0.32.0 --locked`. Per-target Cargo features are not expressible in cargo-dist 0.32.0. Set them consumer-side via `cfg`, e.g. a Metal build gated on `cfg(target_os = "macos")`. macOS Developer-ID signing + notarization are deferred.
@@ -218,7 +218,7 @@ Reusable sub-workflow with three parallel scans: --- -**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs: [supply-chain, secrets]` so the release is re-checked (cargo-deny or osv-scanner, plus gitleaks) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. +**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs:` the `supply-chain` and `secrets` gates so the release is re-checked (cargo-deny or osv-scanner, plus gitleaks) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. --- @@ -230,13 +230,14 @@ Reusable sub-workflow with three parallel scans: | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | +| `rust/pin-version` | Rust | Pins `Cargo.toml` to the current tag (`cargo set-version`). Shared by the Rust `publish` and binary jobs. | | `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | -| `release/dist` | Rust | Installs cargo-dist (the `dist` binary), version-pinned. Powers the opt-in binary-distribution jobs in `rust-packages.yml` (`dist plan` / `build`). | +| `release/dist` | Rust | Installs cargo-dist (the `dist` binary), version-pinned. Used by the `rust-packages` binary jobs (`dist plan` / `build`). | --- @@ -307,7 +308,7 @@ Zero `inputs:` — configuration flows through the caller's `secrets:` block. Ev
-All optional. A consumer that wires none still gets crates.io plus prebuilt archives and installers on the Release; Homebrew and npm activate only when their secret (or OIDC) is configured. +All optional. A consumer that wires none still gets crates.io plus prebuilt archives and installers on the release; Homebrew and npm activate only when their secret (or OIDC) is configured. | name | required | description | | :--- | :---: | :--- | @@ -515,7 +516,7 @@ Each `workflow_call.secrets:` block declares ONLY the secrets the job consumes. Third-party actions across workflows + composites are pinned to a commit SHA with an inline `# vX` comment. Floating refs (`@master`, `@main`, `@vX`) are banned. -Self-CI binaries pinned by version. `actionlint` and `gitleaks` install from release tarballs with SHA-256 verification; `yamllint` via `pip install` with version pin; `cargo-dist` via `cargo install --locked --version` (registry-checksum verified). No `curl | bash`. +Self-CI binaries pinned by version. `actionlint` and `gitleaks` install from release tarballs with SHA-256 verification; `yamllint` via `pip install` with version pin. No `curl | bash`. `.github/dependabot.yml` opens weekly grouped auto-PRs to bump pinned SHAs across `.github/workflows/*` and `.github/actions/**/action.yml`. Consumers should add their own ecosystem entries (e.g., `npm`). From 2581fcfc26391a0df35a28f868706cd494773706 Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 12:25:01 +0700 Subject: [PATCH 19/36] ci(rust): pin cargo-edit by version in the rust/pin-version composite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit House style pins every tooling binary by version — gitleaks, actionlint, cargo-dist. cargo-edit was the last unpinned one. 0.13.11 via env, matching the release/dist install. --- .github/actions/rust/pin-version/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/rust/pin-version/action.yml b/.github/actions/rust/pin-version/action.yml index 10cbeae..0b038d0 100644 --- a/.github/actions/rust/pin-version/action.yml +++ b/.github/actions/rust/pin-version/action.yml @@ -7,6 +7,8 @@ runs: steps: - name: Pin Cargo.toml to tag shell: bash + env: + CARGO_EDIT_VERSION: "0.13.11" run: | - cargo install cargo-edit --locked + cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked cargo set-version "${GITHUB_REF_NAME}" From 29da8cc38dc91d6d8564b381cd4e4519271d16e7 Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 13:19:41 +0700 Subject: [PATCH 20/36] fix(security): impose the canonical cargo-deny ruleset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The supply-chain gate read the consumer's deny.toml, so a repo could weaken or omit it. Ship a canonical security/deny.toml and pass it via --config (the gitleaks model): the consumer's deny.toml is ignored, and a project-local deny.exceptions.toml — cargo-deny's one remaining license escape hatch — fails the job. The ruleset hard-fails on vulnerability, yanked, unmaintained, and unsound advisories, restricts sources to crates.io (git + alternative registries denied), and denies wildcard version requirements. Validated against cargo-deny 0.19.8. --- .../actions/security/cargo-deny/action.yml | 25 +++++++++- CHANGELOG.md | 3 ++ CLAUDE.md | 1 + README.md | 28 ++--------- security/deny.toml | 49 +++++++++++++++++++ 5 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 security/deny.toml diff --git a/.github/actions/security/cargo-deny/action.yml b/.github/actions/security/cargo-deny/action.yml index 9d478d2..325d93b 100644 --- a/.github/actions/security/cargo-deny/action.yml +++ b/.github/actions/security/cargo-deny/action.yml @@ -1,10 +1,33 @@ # Security cargo-deny Action Composite name: security-cargo-deny -description: Run cargo-deny (sources, licenses, bans, advisories) against the repo's deny.toml. +description: Run cargo-deny against the canonical Coroboros ruleset (imposed, no consumer override). +# The caller checks out the repo to scan before using this composite. runs: using: composite steps: + - name: Checkout canonical deny config + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: coroboros/ci + path: .coroboros-ci + sparse-checkout: | + security/deny.toml + sparse-checkout-cone-mode: false + + - name: Reject consumer deny overrides + shell: bash + # `--config` ignores the consumer's deny.toml, but cargo-deny still merges a + # project-local deny.exceptions.toml into the licenses policy — block it. + run: | + if find . -path ./.coroboros-ci -prune -o -type f \ + \( -name 'deny.exceptions.toml' -o -name '.deny.exceptions.toml' \) -print \ + | grep -q .; then + echo "::error::deny.exceptions.toml is not permitted — the cargo-deny policy is imposed by coroboros/ci" + exit 1 + fi + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 with: command: check + command-arguments: "--config .coroboros-ci/security/deny.toml --deny unmaintained --deny unsound" diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e54c0..8957030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - `release/dist`, `rust/pin-version` — composites: install `dist` version-pinned (`cargo install cargo-dist --version 0.32.0 --locked`), and pin `Cargo.toml` to the tag (`cargo set-version`), shared across `publish` and the binary jobs. - `release/github-release` — add a `draft` input so binary repos draft-then-undraft while library repos create non-draft as before. +### Fixes +- `security/cargo-deny` — impose the canonical `security/deny.toml` from `coroboros/ci` via `--config` instead of reading the consumer's file, and reject a project-local `deny.exceptions.toml`. The supply-chain gate can no longer be weakened or omitted per repo — parity with the imposed `gitleaks` ruleset. The ruleset hard-fails on vulnerability, yanked, unmaintained, and unsound advisories, restricts sources to crates.io, and denies wildcard version requirements. + ### Documentation - `README` — document the opt-in binary-distribution layer (the four jobs, the consumer contract, the optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets), add `release/dist` to the composables table, and note the cargo-dist 0.32.0 per-target-features limitation plus deferred macOS signing. diff --git a/CLAUDE.md b/CLAUDE.md index b3c8bac..5b1bba1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts,dist}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned actions. - `security/.gitleaks.toml` — canonical gitleaks ruleset. +- `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). ## Rules diff --git a/README.md b/README.md index 3e651db..68acaf5 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: - `rust-toolchain.toml` — pins the channel and lists the `clippy` + `rustfmt` components (`rustup` installs them on first `cargo` use; omit them and `fmt`/`clippy` fail on a pinned channel). - `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. -- `deny.toml` — the cargo-deny supply-chain policy (sources, licenses, bans, advisories). See [Security](#security) for the baseline. +- cargo-deny policy — imposed by `coroboros/ci`; no consumer `deny.toml` required, and a local one is ignored. See [Security](#security). - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. - binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. Absent → source-only (crates.io), unchanged. @@ -233,7 +233,7 @@ Reusable sub-workflow with three parallel scans: | `rust/pin-version` | Rust | Pins `Cargo.toml` to the current tag (`cargo set-version`). Shared by the Rust `publish` and binary jobs. | | `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | -| `security/cargo-deny` | Rust | Runs cargo-deny (sources, licenses, bans, advisories) against `deny.toml`. The Rust `supply-chain` gate. | +| `security/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | @@ -410,29 +410,7 @@ The GitLab pipeline hardens npm at the image layer — cooldown, Socket Firewall | License drift | `cargo-deny` licenses — allow-list | | Banned or wildcard dependency | `cargo-deny` bans | -`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. It reads the repo's `deny.toml`. The baseline: - -```toml -[advisories] -version = 2 -yanked = "deny" - -[licenses] -version = 2 -allow = [ - "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", - "BSD-3-Clause", "0BSD", "ISC", "Zlib", "MPL-2.0", "Unicode-3.0", - "Unlicense", "CDLA-Permissive-2.0", "BSL-1.0", -] - -[bans] -multiple-versions = "warn" -wildcards = "deny" - -[sources] -unknown-registry = "deny" -unknown-git = "deny" -``` +`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. It applies the canonical [`security/deny.toml`](security/deny.toml) via `--config`, sparse-checked from `coroboros/ci` — **imposed**, the same model as the [`gitleaks`](#composable-actions) ruleset. A consumer `deny.toml` is ignored; a `deny.exceptions.toml` fails the job. The ruleset hard-fails on vulnerability, yanked, unmaintained, and unsound advisories; restricts sources to crates.io (git and alternative registries denied); denies wildcard version requirements; and enforces a permissive license allow-list. Exceptions are changed centrally in `coroboros/ci`, never per repo. **Publish auth.** crates.io publish uses OIDC Trusted Publishing by default — `rust-lang/crates-io-auth-action` mints a short-lived token per run, no long-lived secret in the repo. `CARGO_REGISTRY_TOKEN` is needed only to bootstrap the first publish of a new crate (Trusted Publishing binds to an existing crate); configure Trusted Publishing on crates.io afterwards and drop the token. The verify build runs on publish (no `--no-verify`). It compiles the packaged tarball standalone, catching a crate that only builds in-workspace before the immutable release lands. diff --git a/security/deny.toml b/security/deny.toml new file mode 100644 index 0000000..b142e65 --- /dev/null +++ b/security/deny.toml @@ -0,0 +1,49 @@ +# Canonical Coroboros cargo-deny ruleset — imposed on every consumer. +# The security/cargo-deny composite sparse-checks this file out of coroboros/ci +# and passes it via `--config`, so a consumer's own deny.toml is ignored. Edit +# the policy here only; per-repo exceptions are not accepted (parity with gitleaks). + +[graph] +# No `targets` key on purpose: keep every platform's dependencies in scope. +# `targets` is a narrowing filter — adding it would let another OS's deps escape +# the gate, so a macOS-only malicious dep can't slip past an ubuntu-run check. +all-features = true + +[advisories] +# v2 schema: vulnerability advisories always error and cannot be downgraded; +# `ignore` is the only suppressor and is kept empty. unmaintained/unsound at +# "all" scope error on any matching crate (an abandoned crate is a takeover vector). +yanked = "deny" +unmaintained = "all" +unsound = "all" +ignore = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[bans] +wildcards = "deny" +allow-wildcard-paths = true # path/workspace + dev-deps resolve; public-crate registry wildcards still fail +multiple-versions = "warn" # duplicate versions are bloat, not an attack vector — warn, never block + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "0BSD", + "ISC", + "Zlib", + "MPL-2.0", + "Unicode-3.0", + "Unlicense", + "CDLA-Permissive-2.0", + "BSL-1.0", +] +confidence-threshold = 0.8 +private = { ignore = true } From 323029294260c83f903c5f40f9fcf8d8a28e521d Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 13:24:57 +0700 Subject: [PATCH 21/36] docs(security): reconcile the README with the imposed cargo-deny ruleset The supply-chain job blurb still said cargo-deny "reads deny.toml"; the risk table omitted unsound; the Security prose re-listed the controls the table already covers. Point both at the imposed security/deny.toml and drop the duplication. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 68acaf5..fb309de 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Consumer requirements: **Trigger**: `every push`. Gates `publish` via `needs:` — a release waits on it. -`cargo-deny check` (SHA-pinned action) reads `deny.toml`: crate sources, licenses, bans, and advisories (vulnerabilities, unmaintained, yanked). Running on every push re-checks a tagged release against the latest advisory DB before it ships, not only at PR time. See [Security](#security). +`cargo-deny check` (SHA-pinned action) applies the canonical imposed `security/deny.toml`: crate sources, licenses, bans, and advisories (vulnerabilities, unmaintained, unsound, yanked). Running on every push re-checks a tagged release against the latest advisory DB before it ships, not only at PR time. See [Security](#security).
@@ -406,11 +406,11 @@ The GitLab pipeline hardens npm at the image layer — cooldown, Socket Firewall | :--- | :--- | | Untrusted source, typosquat | `cargo-deny` sources — crates.io only; git and alternative registries denied | | Lock drift, tampered dependencies | committed `Cargo.lock` + `--locked` on `clippy` and `test` — fails on a stale or altered lock | -| Known vulnerability | `osv-scanner` (Cargo.lock) and `cargo-deny` advisories — RustSec vulnerabilities, unmaintained, yanked | +| Known vulnerability | `osv-scanner` (Cargo.lock) and `cargo-deny` advisories — RustSec vulnerabilities, unmaintained, unsound, yanked | | License drift | `cargo-deny` licenses — allow-list | | Banned or wildcard dependency | `cargo-deny` bans | -`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. It applies the canonical [`security/deny.toml`](security/deny.toml) via `--config`, sparse-checked from `coroboros/ci` — **imposed**, the same model as the [`gitleaks`](#composable-actions) ruleset. A consumer `deny.toml` is ignored; a `deny.exceptions.toml` fails the job. The ruleset hard-fails on vulnerability, yanked, unmaintained, and unsound advisories; restricts sources to crates.io (git and alternative registries denied); denies wildcard version requirements; and enforces a permissive license allow-list. Exceptions are changed centrally in `coroboros/ci`, never per repo. +`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. The controls above are **imposed**: it applies the canonical [`security/deny.toml`](security/deny.toml) via `--config`, sparse-checked from `coroboros/ci` — the [`gitleaks`](#composable-actions) model. A consumer `deny.toml` is ignored; a `deny.exceptions.toml` fails the job. Exceptions are changed centrally in `coroboros/ci`, never per repo. **Publish auth.** crates.io publish uses OIDC Trusted Publishing by default — `rust-lang/crates-io-auth-action` mints a short-lived token per run, no long-lived secret in the repo. `CARGO_REGISTRY_TOKEN` is needed only to bootstrap the first publish of a new crate (Trusted Publishing binds to an existing crate); configure Trusted Publishing on crates.io afterwards and drop the token. The verify build runs on publish (no `--no-verify`). It compiles the packaged tarball standalone, catching a crate that only builds in-workspace before the immutable release lands. From 236bf36b84bcfb4ebcbf5203389923d7b7d7c1c7 Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 15:14:09 +0700 Subject: [PATCH 22/36] style(ci): drop what-comments, keep only non-obvious why MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove workflow/composite comments that restate what the code does or duplicate the README: the cargo-deny / gitleaks / osv job-purpose blocks (and a stale "reads the repo's deny.toml" line), the packages_install and npm-auth notes. Trim the verbose ones to their why — the Socket Firewall, verify-build, undraft-ordering, and install_dist comments. Inline scope justifications, the pnpm/corepack workaround, and the URL-determinism rationale stay. --- .github/actions/javascript/base/action.yml | 3 +- .github/workflows/javascript-npm-packages.yml | 9 ----- .github/workflows/rust-packages.yml | 34 +++---------------- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/.github/actions/javascript/base/action.yml b/.github/actions/javascript/base/action.yml index a98e840..2aabf3a 100644 --- a/.github/actions/javascript/base/action.yml +++ b/.github/actions/javascript/base/action.yml @@ -59,8 +59,7 @@ runs: - name: Install dependencies shell: bash - # Wrapped by Socket Firewall — the registry fetch is inspected and confirmed- - # malicious packages are blocked before download. Fail-closed: no sfw, no install. + # Fail-closed: no sfw, no install. run: sfw pnpm install --frozen-lockfile --ignore-scripts - name: Lint diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index dcb47f7..e3f3981 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -34,21 +34,12 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/javascript/base@v0 - # osv-scanner gates the release on known vulnerabilities (pnpm-lock.yaml). - # javascript/base already gates the install (Socket Firewall + --frozen-lockfile); - # this adds the CVE gate. Runs on every push and gates `publish` (see its - # `needs:`), so a release is re-checked against the latest OSV data before it - # ships. The `security` job scans in parallel for reporting and does not gate. supply-chain: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/osv-scanner@v0 - # gitleaks gates the release on a leaked secret, enforced by the template's - # `needs:` rather than the consumer's branch protection — the GitLab - # `security-gate` equivalent. The `security` job runs gitleaks again in - # parallel for the SARIF report. secrets: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 8582754..af75bbb 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -27,22 +27,12 @@ jobs: - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/rust/base@v0 - # cargo-deny owns the Rust supply-chain controls osv-scanner doesn't: crate - # sources (crates.io only), licenses, banned/wildcard deps, and unmaintained or - # yanked advisories. Reads the repo's deny.toml. Runs on every push and gates - # `publish` (see its `needs:`), so a release is re-checked against the latest - # advisory DB before it ships — not only at PR time. The `security` job scans - # in parallel for reporting and does not gate publish; this is the release gate. supply-chain: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/cargo-deny@v0 - # gitleaks gates the release on a leaked secret, enforced by the template's - # `needs:` rather than the consumer's branch protection — the GitLab - # `security-gate` equivalent. The `security` job runs gitleaks again in - # parallel for the SARIF report. secrets: runs-on: ubuntu-latest steps: @@ -51,9 +41,6 @@ jobs: fetch-depth: 0 - uses: coroboros/ci/.github/actions/security/gitleaks@v0 - # Opt-in binary layer. Runs only when the consumer's Cargo.toml declares - # [package.metadata.dist]; `enabled` gates dist-build/host/publish and the - # release `draft` flag, so library crates skip the whole layer with zero config. dist-plan: if: ${{ github.ref_type == 'tag' }} runs-on: ubuntu-latest @@ -139,15 +126,12 @@ jobs: - uses: coroboros/ci/.github/actions/rust/native-deps@v0 - # Plan-provided cross/system toolchains; empty for a native target. - name: Install build system dependencies if: ${{ matrix.packages_install }} shell: bash run: ${{ matrix.packages_install }} - # matrix.dist_args carries --artifacts=local --target= from `dist plan`. - # dist's own curl|sh installer (matrix.install_dist) is deliberately ignored — - # release/dist is the house-style, version-pinned install. + # matrix.install_dist (dist's curl|sh installer) is ignored — release/dist is the pinned install. - name: Build local artifacts shell: bash run: dist build --tag="${GITHUB_REF_NAME}" ${{ matrix.dist_args }} @@ -166,8 +150,7 @@ jobs: permissions: contents: write # for GitHub Release creation + commit-back to main id-token: write # for crates.io OIDC Trusted Publishing - # Empty on the OIDC path; set only for the first-publish bootstrap of a new - # crate. Surfaced as env so the OIDC auth step can branch on its presence. + # Surfaced as env so the OIDC auth step can branch on the token's presence. env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} steps: @@ -207,10 +190,7 @@ jobs: # the long-lived CARGO_REGISTRY_TOKEN secret (already in job env). CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token || env.CARGO_REGISTRY_TOKEN }} run: | - # The verify build runs (no --no-verify): it compiles the packaged - # tarball standalone, catching files missing from `include` or path-dep - # leakage that an in-workspace build misses. --allow-dirty covers the - # in-place version pin. + # --allow-dirty covers the in-place version pin (cargo set-version). cargo publish --allow-dirty - uses: coroboros/ci/.github/actions/release/github-release@v0 @@ -252,14 +232,12 @@ jobs: name: dist-plan-manifest path: target/distrib/ - # Final asset URLs are deterministic (repo + tag), so --tag is enough to - # embed non-draft download links with no dist-owned release. + # Final asset URLs are deterministic (repo + tag), so --tag alone embeds non-draft links — no dist-owned release. - name: Build global artifacts shell: bash run: dist build --tag="${GITHUB_REF_NAME}" --artifacts=global --output-format=json > target/distrib/dist-manifest.json - # Upload archives + installers + checksums, then undraft — so the formula - # and npm shim (next job) resolve against a live release, not a draft. + # Undraft before the formula/npm job so they resolve against a live release, not a draft. - name: Upload release assets and undraft shell: bash env: @@ -338,8 +316,6 @@ jobs: node-version: "24" registry-url: "https://registry.npmjs.org" - # OIDC + provenance by default; NPM_PACKAGE_REGISTRY_TOKEN only bootstraps - # the first publish of a new scoped package. Mirrors javascript-npm-packages. - name: Publish npm shim shell: bash env: From 103e06463f2bca7581404b813eae261428ba177c Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 16:17:11 +0700 Subject: [PATCH 23/36] refactor(ci): inline cargo-dist setup, gh-native logs, trim comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline the dist setup (mimic the npm pipeline) — drop the release/dist and rust/pin-version composites; the `cargo install cargo-dist` / `cargo-edit` + `cargo set-version` steps live in the dist jobs and publish, with tool versions hoisted to workflow env (CARGO_DIST_VERSION, CARGO_EDIT_VERSION). Logs use gh-native commands per context: verify-tag detail folded into one `::error::`, status lines to `::notice::`, the check-docs context dump into a `::group::`. Trim the gitleaks and self-security comments to one line. --- .github/actions/check-docs/action.yml | 5 +- .github/actions/javascript/base/action.yml | 4 +- .github/actions/release/dist/action.yml | 18 ------- .../release/generate-changelog/action.yml | 4 +- .github/actions/rust/base/action.yml | 2 +- .github/actions/rust/native-deps/action.yml | 2 +- .github/actions/rust/pin-version/action.yml | 14 ----- .github/actions/security/gitleaks/action.yml | 5 +- .github/workflows/javascript-npm-packages.yml | 10 ++-- .github/workflows/rust-packages.yml | 52 ++++++++++++------- .github/workflows/self-security.yml | 7 +-- CLAUDE.md | 2 +- README.md | 2 - 13 files changed, 50 insertions(+), 77 deletions(-) delete mode 100644 .github/actions/release/dist/action.yml delete mode 100644 .github/actions/rust/pin-version/action.yml diff --git a/.github/actions/check-docs/action.yml b/.github/actions/check-docs/action.yml index 6581faf..63de648 100644 --- a/.github/actions/check-docs/action.yml +++ b/.github/actions/check-docs/action.yml @@ -8,16 +8,17 @@ runs: - name: Log run context shell: bash run: | - echo "Run context:" + echo "::group::Run context" echo " GITHUB_REF_NAME | ${GITHUB_REF_NAME}" echo " GITHUB_REF_SLUG | $(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | head -c 63 | sed 's/-$//')" echo " GITHUB_REF_TYPE | ${GITHUB_REF_TYPE}" echo " GITHUB_REPOSITORY | ${GITHUB_REPOSITORY}" echo " GITHUB_SERVER_URL | ${GITHUB_SERVER_URL}" echo " GITHUB_REPOSITORY_URL | ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" + echo "::endgroup::" - name: README check shell: bash run: | [ -f README.md ] || (echo "::error::A README.md file must be present at project root" && exit 1) - echo "README.md present" + echo "::notice::README.md present" diff --git a/.github/actions/javascript/base/action.yml b/.github/actions/javascript/base/action.yml index 2aabf3a..1590314 100644 --- a/.github/actions/javascript/base/action.yml +++ b/.github/actions/javascript/base/action.yml @@ -19,7 +19,7 @@ runs: exit 1 fi node_version="$(tr -d '[:space:]v' < .node-version)" - echo "Resolved Node.js version: ${node_version} (from .node-version)" + echo "::notice::Resolved Node.js version: ${node_version} (from .node-version)" echo "node-version=${node_version}" >> "${GITHUB_OUTPUT}" - name: Setup Node.js @@ -72,7 +72,7 @@ runs: if [ "$(jq .scripts.build package.json)" != "null" ]; then pnpm run build else - echo "Non required script 'build' skipped" + echo "::notice::Non required script 'build' skipped" fi - name: Test diff --git a/.github/actions/release/dist/action.yml b/.github/actions/release/dist/action.yml deleted file mode 100644 index 3111ee8..0000000 --- a/.github/actions/release/dist/action.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Release cargo-dist Install Action Composite -name: release-dist -description: Install cargo-dist (the `dist` binary), version-pinned, on the runner. - -runs: - using: composite - steps: - - name: Install cargo-dist - shell: bash - env: - DIST_VERSION: "0.32.0" - run: | - if command -v dist >/dev/null 2>&1; then - echo "::notice::dist already on PATH: $(dist --version)" - else - cargo install cargo-dist --version "${DIST_VERSION}" --locked - dist --version - fi diff --git a/.github/actions/release/generate-changelog/action.yml b/.github/actions/release/generate-changelog/action.yml index 217f98f..42df8d7 100644 --- a/.github/actions/release/generate-changelog/action.yml +++ b/.github/actions/release/generate-changelog/action.yml @@ -33,10 +33,10 @@ runs: ' CHANGELOG.md 2>/dev/null || true)" if [ -n "${existing}" ]; then - echo "Using existing CHANGELOG section for ${tag}" + echo "::notice::Using existing CHANGELOG section for ${tag}" body="${existing}" else - echo "Auto-generating CHANGELOG section for ${tag}" + echo "::notice::Auto-generating CHANGELOG section for ${tag}" prev_tag="$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)" commits_range="${prev_tag:+${prev_tag}..}HEAD" diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml index db40230..2c7cedc 100644 --- a/.github/actions/rust/base/action.yml +++ b/.github/actions/rust/base/action.yml @@ -13,7 +13,7 @@ runs: exit 1 fi channel="$(grep -E '^[[:space:]]*channel[[:space:]]*=' rust-toolchain.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" - echo "Resolved Rust toolchain: ${channel:-unset} (from rust-toolchain.toml)" + echo "::notice::Resolved Rust toolchain: ${channel:-unset} (from rust-toolchain.toml)" - name: Setup cargo cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 diff --git a/.github/actions/rust/native-deps/action.yml b/.github/actions/rust/native-deps/action.yml index 5d455ad..215b60b 100644 --- a/.github/actions/rust/native-deps/action.yml +++ b/.github/actions/rust/native-deps/action.yml @@ -11,5 +11,5 @@ runs: if [ -f ci/setup.sh ]; then bash ci/setup.sh else - echo "No ci/setup.sh — pure-Rust package" + echo "::notice::No ci/setup.sh — pure-Rust package" fi diff --git a/.github/actions/rust/pin-version/action.yml b/.github/actions/rust/pin-version/action.yml deleted file mode 100644 index 0b038d0..0000000 --- a/.github/actions/rust/pin-version/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Rust Pin Version Action Composite -name: rust-pin-version -description: Pin Cargo.toml to the current tag via cargo set-version. - -runs: - using: composite - steps: - - name: Pin Cargo.toml to tag - shell: bash - env: - CARGO_EDIT_VERSION: "0.13.11" - run: | - cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked - cargo set-version "${GITHUB_REF_NAME}" diff --git a/.github/actions/security/gitleaks/action.yml b/.github/actions/security/gitleaks/action.yml index 4281d31..c9c8ad3 100644 --- a/.github/actions/security/gitleaks/action.yml +++ b/.github/actions/security/gitleaks/action.yml @@ -2,8 +2,7 @@ name: security-gitleaks description: Install gitleaks (SHA-256 verified) and scan with the canonical Coroboros ruleset. Emits SARIF. -# The caller checks out the repo to scan (fetch-depth: 0 for full history) before -# using this composite. +# The caller checks out the repo to scan (fetch-depth: 0) before using this composite. runs: using: composite steps: @@ -50,7 +49,7 @@ runs: rc=$? set -e - echo "gitleaks exit code: ${rc}" + echo "::notice::gitleaks exit code: ${rc}" if [ "${rc}" = "0" ]; then echo "::notice::gitleaks: no leaks found" elif [ "${rc}" = "2" ]; then diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index e3f3981..9dcf02c 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -65,12 +65,10 @@ jobs: shell: bash run: | if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed. Resolve manually." - echo " Tag SHA: ${GITHUB_SHA}" - echo " main HEAD: $(git rev-parse HEAD)" + echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." exit 1 fi - echo "main HEAD matches tag SHA (${GITHUB_SHA})" + echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/javascript/base@v0 @@ -86,7 +84,7 @@ jobs: shell: bash run: | if [ -n "${NPM_PACKAGE_REGISTRY_TOKEN}" ]; then - echo "Publishing with NPM_PACKAGE_REGISTRY_TOKEN auth via npm CLI" + echo "::notice::Publishing with NPM_PACKAGE_REGISTRY_TOKEN auth via npm CLI" # The pre-Trusted-Publisher bootstrap path can't reliably use # pnpm 11.x (auto-OIDC, no fallback to .npmrc token) or pnpm # 10.33.0 (corepack intercepts every `pnpm`; the standalone @@ -99,7 +97,7 @@ jobs: npm --version npm publish --ignore-scripts --access public else - echo "Publishing with OIDC Trusted Publisher + provenance" + echo "::notice::Publishing with OIDC Trusted Publisher + provenance" pnpm publish --provenance --no-git-checks --ignore-scripts fi diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index af75bbb..3b69ef6 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -14,6 +14,10 @@ on: permissions: contents: read +env: + CARGO_DIST_VERSION: "0.32.0" + CARGO_EDIT_VERSION: "0.13.11" + jobs: preflight: if: ${{ github.ref_type == 'branch' }} @@ -58,12 +62,10 @@ jobs: shell: bash run: | if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed. Resolve manually." - echo " Tag SHA: ${GITHUB_SHA}" - echo " main HEAD: $(git rev-parse HEAD)" + echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." exit 1 fi - echo "main HEAD matches tag SHA (${GITHUB_SHA})" + echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" - id: detect name: Detect cargo-dist metadata @@ -85,10 +87,12 @@ jobs: fi - if: ${{ steps.detect.outputs.enabled == 'true' }} - uses: coroboros/ci/.github/actions/release/dist@v0 - - - if: ${{ steps.detect.outputs.enabled == 'true' }} - uses: coroboros/ci/.github/actions/rust/pin-version@v0 + name: Install cargo-dist and pin version + shell: bash + run: | + cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked + cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked + cargo set-version "${GITHUB_REF_NAME}" - id: plan if: ${{ steps.detect.outputs.enabled == 'true' }} @@ -120,9 +124,12 @@ jobs: ref: main fetch-depth: 0 - - uses: coroboros/ci/.github/actions/release/dist@v0 - - - uses: coroboros/ci/.github/actions/rust/pin-version@v0 + - name: Install cargo-dist and pin version + shell: bash + run: | + cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked + cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked + cargo set-version "${GITHUB_REF_NAME}" - uses: coroboros/ci/.github/actions/rust/native-deps@v0 @@ -131,7 +138,7 @@ jobs: shell: bash run: ${{ matrix.packages_install }} - # matrix.install_dist (dist's curl|sh installer) is ignored — release/dist is the pinned install. + # matrix.install_dist (dist's curl|sh installer) is ignored — dist is installed version-pinned above. - name: Build local artifacts shell: bash run: dist build --tag="${GITHUB_REF_NAME}" ${{ matrix.dist_args }} @@ -163,17 +170,19 @@ jobs: shell: bash run: | if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed. Resolve manually." - echo " Tag SHA: ${GITHUB_SHA}" - echo " main HEAD: $(git rev-parse HEAD)" + echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." exit 1 fi - echo "main HEAD matches tag SHA (${GITHUB_SHA})" + echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/rust/native-deps@v0 - - uses: coroboros/ci/.github/actions/rust/pin-version@v0 + - name: Pin Cargo.toml to tag + shell: bash + run: | + cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked + cargo set-version "${GITHUB_REF_NAME}" - id: changelog uses: coroboros/ci/.github/actions/release/generate-changelog@v0 @@ -214,9 +223,12 @@ jobs: ref: main fetch-depth: 0 - - uses: coroboros/ci/.github/actions/release/dist@v0 - - - uses: coroboros/ci/.github/actions/rust/pin-version@v0 + - name: Install cargo-dist and pin version + shell: bash + run: | + cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked + cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked + cargo set-version "${GITHUB_REF_NAME}" - name: Download build artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 diff --git a/.github/workflows/self-security.yml b/.github/workflows/self-security.yml index 178b974..c0ca488 100644 --- a/.github/workflows/self-security.yml +++ b/.github/workflows/self-security.yml @@ -1,11 +1,8 @@ # Self-CI Security name: Self-CI Security -# Runs the security composites via local `./` refs so a PR's changes to them are -# self-tested before release. security.yml (the consumer-facing reusable workflow) -# pins the same composites at @v0; a reusable workflow can't use local refs — they -# resolve against the caller's checkout — so this repo exercises them directly. -# coroboros/ci ships no dependency manifest, so osv-scanner exercises its skip path. +# Local `./` refs so a PR self-tests its own composite changes — the @v0-pinned +# security.yml can't (a reusable workflow's `./` resolves to the caller's checkout). on: push: branches: [main] diff --git a/CLAUDE.md b/CLAUDE.md index 5b1bba1..9f308a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). - `.github/workflows/{self,self-security,self-release}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), and the `v0` rolling-tag move. -- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts,dist}}/action.yml` — composites. +- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned actions. - `security/.gitleaks.toml` — canonical gitleaks ruleset. - `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). diff --git a/README.md b/README.md index fb309de..a156a06 100644 --- a/README.md +++ b/README.md @@ -230,14 +230,12 @@ Reusable sub-workflow with three parallel scans: | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | | `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | -| `rust/pin-version` | Rust | Pins `Cargo.toml` to the current tag (`cargo set-version`). Shared by the Rust `publish` and binary jobs. | | `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The Rust `supply-chain` gate. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | -| `release/dist` | Rust | Installs cargo-dist (the `dist` binary), version-pinned. Used by the `rust-packages` binary jobs (`dist plan` / `build`). | --- From 9b81a32d2530127042921b90897f75577b2d01ac Mon Sep 17 00:00:00 2001 From: OB Date: Wed, 3 Jun 2026 16:17:19 +0700 Subject: [PATCH 24/36] chore(release): fold the binary work into the unreleased 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.2.0 was never tagged — the latest release is 0.1.14 — so the whole rust-packages PR ships as one version. Revert the second bump and merge the binary-distribution and imposed-cargo-deny notes into the v0.2.0 changelog. --- CHANGELOG.md | 21 +++------------------ package.json | 2 +- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8957030..6f676e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,10 @@ # Changelog -## v0.3.0 - 03/06/2026 - -### Features -- `rust-packages` — opt-in binary-distribution layer driven by cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]` in the consumer's `Cargo.toml`. A tagged binary repo gets prebuilt archives for each declared target, `shell` + `powershell` installers, a Homebrew formula committed to the declared `tap`, and a published npm shim — attached to the single GitHub Release the pipeline already creates, alongside the crates.io publish. The shared pipeline stays the sole release authority: `dist` only builds (final asset URLs derive from repo + tag), and the release goes live through draft → undraft so installers and the formula resolve against published download URLs. Library crates without dist metadata are unchanged — every binary job self-skips. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish`; the binary builds `needs:` the `cargo-deny` and `gitleaks` gates, so no artifact builds before the release gates pass. -- `release/dist`, `rust/pin-version` — composites: install `dist` version-pinned (`cargo install cargo-dist --version 0.32.0 --locked`), and pin `Cargo.toml` to the tag (`cargo set-version`), shared across `publish` and the binary jobs. -- `release/github-release` — add a `draft` input so binary repos draft-then-undraft while library repos create non-draft as before. - -### Fixes -- `security/cargo-deny` — impose the canonical `security/deny.toml` from `coroboros/ci` via `--config` instead of reading the consumer's file, and reject a project-local `deny.exceptions.toml`. The supply-chain gate can no longer be weakened or omitted per repo — parity with the imposed `gitleaks` ruleset. The ruleset hard-fails on vulnerability, yanked, unmaintained, and unsound advisories, restricts sources to crates.io, and denies wildcard version requirements. - -### Documentation -- `README` — document the opt-in binary-distribution layer (the four jobs, the consumer contract, the optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets), add `release/dist` to the composables table, and note the cargo-dist 0.32.0 per-target-features limitation plus deferred macOS signing. - -### Configuration -- `package.json` — bump to `0.3.0`. - ## v0.2.0 - 02/06/2026 ### Features - `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. The verify build runs on publish — no `--no-verify` — so a crate that only builds in-workspace fails before an immutable release. +- `rust-packages` — opt-in binary-distribution layer via cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]`. A tagged binary crate gets prebuilt per-target archives, `shell` + `powershell` installers, a Homebrew formula in the declared `tap`, and an npm shim — attached to the single GitHub Release, alongside the crates.io publish. The pipeline stays the sole release authority: `dist` only builds (final URLs derive from repo + tag), and the release goes live through draft → undraft. Library crates self-skip. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish` (gated by `cargo-deny` + `gitleaks`) and a `draft` input on `release/github-release`; optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets activate the tap and npm publishes. - `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the publish verify build. - `javascript/base` — wrap `pnpm install` in Socket Firewall (`sfw`), blocking confirmed-malicious packages before download. Fail-closed. The GitHub-runner equivalent of the image-baked firewall on GitLab. - `self-release` — move the rolling `v0` major tag to each stable `coroboros/ci` release, so `@v0` consumers track the latest release without a manual tag push. @@ -30,11 +15,11 @@ - `release` — drop the "move rolling major tag" step from the npm and Rust publish jobs. Reusable workflows run in the caller's context, so it force-pushed a meaningless `vN` ref into every consumer repo; the `v0` ref now moves on `coroboros/ci`'s own release (see `self-release`). ### Refactor -- `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. +- `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. The `cargo-deny` composite imposes a canonical `security/deny.toml` via `--config` — a consumer `deny.toml` is ignored and a `deny.exceptions.toml` is rejected. - `release/commit-artifacts` — extract the commit-back step shared by the npm and Rust publish jobs into a composite (`files` input, `[skip ci]`), mirroring GitLab's `commit-release-artifacts`. ### Documentation -- `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab. +- `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab; document the opt-in binary-distribution layer (jobs, consumer contract, optional tap/npm secrets) and the imposed `cargo-deny` ruleset. ### Configuration - `package.json` — bump to `0.2.0` (was `0.1.13`, lagging the `0.1.14` tag). diff --git a/package.json b/package.json index 4b587e7..6e82b0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coroboros/ci", - "version": "0.3.0", + "version": "0.2.0", "private": true, "description": "Reusable GitHub Actions CI for the Coroboros stack.", "license": "SEE LICENSE IN LICENSE.md", From 9bf4723ac722690285b70426a93c965e400c8fd3 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 19:10:01 +0700 Subject: [PATCH 25/36] feat(ci): prebuilt dist install, composite self-tests, Renovate tool bumps - rust/install-dist: cargo-dist installed prebuilt + SHA-256 verified per OS, replacing the from-source compile in dist-plan/dist-build/dist-host - rust/pin-version: install only the cargo-set-version binary (--bin), pin in-composite - pin dist-plan/dist-build/dist-host checkouts to the tag commit; verify-tag stays on publish - guard cargo publish with a dirty-tree allowlist before the immutable crates.io release - security/cargo-deny: drop redundant CLI deny flags (single-sourced in deny.toml) - self-actions: PR smoke tests for verify-tag, generate-changelog, commit-artifacts, cargo-deny, install-dist against synthetic fixtures - renovate.json: review-gated auto-bumps for the version-pinned tooling --- .github/actions/release/verify-tag/action.yml | 15 ++ .github/actions/rust/install-dist/action.yml | 46 ++++ .github/actions/rust/pin-version/action.yml | 14 ++ .../actions/security/cargo-deny/action.yml | 2 +- .github/workflows/javascript-npm-packages.yml | 9 +- .github/workflows/rust-packages.yml | 86 +++---- .github/workflows/self-actions.yml | 235 ++++++++++++++++++ CHANGELOG.md | 17 +- CLAUDE.md | 8 +- README.md | 27 +- renovate.json | 70 ++++++ 11 files changed, 456 insertions(+), 73 deletions(-) create mode 100644 .github/actions/release/verify-tag/action.yml create mode 100644 .github/actions/rust/install-dist/action.yml create mode 100644 .github/actions/rust/pin-version/action.yml create mode 100644 .github/workflows/self-actions.yml create mode 100644 renovate.json diff --git a/.github/actions/release/verify-tag/action.yml b/.github/actions/release/verify-tag/action.yml new file mode 100644 index 0000000..f53779e --- /dev/null +++ b/.github/actions/release/verify-tag/action.yml @@ -0,0 +1,15 @@ +# Release Verify Tag Action Composite +name: release-verify-tag +description: Fail unless the checked-out main HEAD matches the tag SHA that triggered the run. + +runs: + using: composite + steps: + - name: Verify tag points to main HEAD + shell: bash + run: | + if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then + echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." + exit 1 + fi + echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" diff --git a/.github/actions/rust/install-dist/action.yml b/.github/actions/rust/install-dist/action.yml new file mode 100644 index 0000000..e1ee3bc --- /dev/null +++ b/.github/actions/rust/install-dist/action.yml @@ -0,0 +1,46 @@ +# Rust install-dist Action Composite +name: rust-install-dist +description: Install cargo-dist's `dist` binary (prebuilt, SHA-256 verified) onto PATH. Linux, macOS, Windows. + +runs: + using: composite + steps: + - name: Install dist + shell: bash + env: + CARGO_DIST_VERSION: "0.32.0" + # https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/sha256.sum + SHA256_X86_64_LINUX: "eb52f9fae0d0506774e9f1801c1168f87fa2c87a45e2d64d3ae7c89401929946" + SHA256_AARCH64_LINUX: "d29bcffeb3f8b0c517b4ce0dd2470926ed5cb0bb29d78c6bdd5f88d76ee14a6a" + SHA256_X86_64_DARWIN: "6243464a8389e006b9256ee548bc795638f1a17113c1b6669c0e05ce89fd05c5" + SHA256_AARCH64_DARWIN: "aa343b2ff78ec2981f17a65140250c5ad6062c74072163f68c5c2686d94763a7" + SHA256_X86_64_WINDOWS: "26e845cabff12a92911ce960af73a86c8f9b2b2d9072b01dfe5b662acf044fa3" + run: | + set -euo pipefail + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) target="x86_64-unknown-linux-gnu"; sha="${SHA256_X86_64_LINUX}"; ext="tar.xz"; exe="" ;; + Linux-aarch64|Linux-arm64) target="aarch64-unknown-linux-gnu"; sha="${SHA256_AARCH64_LINUX}"; ext="tar.xz"; exe="" ;; + Darwin-x86_64) target="x86_64-apple-darwin"; sha="${SHA256_X86_64_DARWIN}"; ext="tar.xz"; exe="" ;; + Darwin-arm64) target="aarch64-apple-darwin"; sha="${SHA256_AARCH64_DARWIN}"; ext="tar.xz"; exe="" ;; + MINGW*-x86_64|MSYS*-x86_64|CYGWIN*-x86_64) target="x86_64-pc-windows-msvc"; sha="${SHA256_X86_64_WINDOWS}"; ext="zip"; exe=".exe" ;; + *) echo "::error::unsupported runner $(uname -s)-$(uname -m) for dist install"; exit 1 ;; + esac + asset="cargo-dist-${target}.${ext}" + tmp="$(mktemp -d)" + curl -fsSL "https://github.com/axodotdev/cargo-dist/releases/download/v${CARGO_DIST_VERSION}/${asset}" -o "${tmp}/${asset}" + # macOS runners ship `shasum`, not `sha256sum`; the dist-build matrix spans both. + if command -v sha256sum >/dev/null 2>&1; then + echo "${sha} ${tmp}/${asset}" | sha256sum -c - + else + echo "${sha} ${tmp}/${asset}" | shasum -a 256 -c - + fi + dest="${HOME}/.cargo/bin" + mkdir -p "${dest}" + if [ "${ext}" = "zip" ]; then + unzip -j -o "${tmp}/${asset}" "dist${exe}" -d "${dest}" + else + tar -xJf "${tmp}/${asset}" -C "${dest}" --strip-components=1 "cargo-dist-${target}/dist" + fi + rm -rf "${tmp}" + echo "${dest}" >> "${GITHUB_PATH}" + "${dest}/dist${exe}" --version diff --git a/.github/actions/rust/pin-version/action.yml b/.github/actions/rust/pin-version/action.yml new file mode 100644 index 0000000..5ff7d33 --- /dev/null +++ b/.github/actions/rust/pin-version/action.yml @@ -0,0 +1,14 @@ +# Rust Pin Version Action Composite +name: rust-pin-version +description: Install cargo-set-version (version-pinned via CARGO_EDIT_VERSION) and stamp Cargo.toml to the release tag. + +runs: + using: composite + steps: + - name: Pin Cargo.toml to tag + shell: bash + env: + CARGO_EDIT_VERSION: "0.13.11" + run: | + cargo install cargo-edit --bin cargo-set-version --version "${CARGO_EDIT_VERSION}" --locked + cargo set-version "${GITHUB_REF_NAME}" diff --git a/.github/actions/security/cargo-deny/action.yml b/.github/actions/security/cargo-deny/action.yml index 325d93b..2b0e820 100644 --- a/.github/actions/security/cargo-deny/action.yml +++ b/.github/actions/security/cargo-deny/action.yml @@ -30,4 +30,4 @@ runs: - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 with: command: check - command-arguments: "--config .coroboros-ci/security/deny.toml --deny unmaintained --deny unsound" + command-arguments: "--config .coroboros-ci/security/deny.toml" diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 9dcf02c..2d48288 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -61,14 +61,7 @@ jobs: ref: main fetch-depth: 0 - - name: Verify tag points to main HEAD - shell: bash - run: | - if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." - exit 1 - fi - echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" + - uses: coroboros/ci/.github/actions/release/verify-tag@v0 - uses: coroboros/ci/.github/actions/check-docs@v0 - uses: coroboros/ci/.github/actions/javascript/base@v0 diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 3b69ef6..98cba8c 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -14,10 +14,6 @@ on: permissions: contents: read -env: - CARGO_DIST_VERSION: "0.32.0" - CARGO_EDIT_VERSION: "0.13.11" - jobs: preflight: if: ${{ github.ref_type == 'branch' }} @@ -45,6 +41,16 @@ jobs: fetch-depth: 0 - uses: coroboros/ci/.github/actions/security/gitleaks@v0 + package: + if: ${{ github.ref_type == 'branch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: coroboros/ci/.github/actions/rust/native-deps@v0 + - name: Verify the published crate builds + shell: bash + run: cargo package --locked + dist-plan: if: ${{ github.ref_type == 'tag' }} runs-on: ubuntu-latest @@ -55,17 +61,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: main - fetch-depth: 0 - - - name: Verify tag points to main HEAD - shell: bash - run: | - if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." - exit 1 - fi - echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" + ref: ${{ github.sha }} - id: detect name: Detect cargo-dist metadata @@ -87,12 +83,10 @@ jobs: fi - if: ${{ steps.detect.outputs.enabled == 'true' }} - name: Install cargo-dist and pin version - shell: bash - run: | - cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked - cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked - cargo set-version "${GITHUB_REF_NAME}" + uses: coroboros/ci/.github/actions/rust/install-dist@v0 + + - if: ${{ steps.detect.outputs.enabled == 'true' }} + uses: coroboros/ci/.github/actions/rust/pin-version@v0 - id: plan if: ${{ steps.detect.outputs.enabled == 'true' }} @@ -121,15 +115,11 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: main - fetch-depth: 0 + ref: ${{ github.sha }} - - name: Install cargo-dist and pin version - shell: bash - run: | - cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked - cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/install-dist@v0 + + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - uses: coroboros/ci/.github/actions/rust/native-deps@v0 @@ -166,23 +156,12 @@ jobs: ref: main fetch-depth: 0 - - name: Verify tag points to main HEAD - shell: bash - run: | - if [ "$(git rev-parse HEAD)" != "${GITHUB_SHA}" ]; then - echo "::error::main has moved since the tag was pushed — tag ${GITHUB_SHA}, main HEAD $(git rev-parse HEAD). Resolve manually." - exit 1 - fi - echo "::notice::main HEAD matches tag SHA (${GITHUB_SHA})" + - uses: coroboros/ci/.github/actions/release/verify-tag@v0 - uses: coroboros/ci/.github/actions/check-docs@v0 - - uses: coroboros/ci/.github/actions/rust/native-deps@v0 + - uses: coroboros/ci/.github/actions/rust/base@v0 - - name: Pin Cargo.toml to tag - shell: bash - run: | - cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - id: changelog uses: coroboros/ci/.github/actions/release/generate-changelog@v0 @@ -199,7 +178,14 @@ jobs: # the long-lived CARGO_REGISTRY_TOKEN secret (already in job env). CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token || env.CARGO_REGISTRY_TOKEN }} run: | - # --allow-dirty covers the in-place version pin (cargo set-version). + # --allow-dirty covers the version pin (cargo set-version) + changelog regen; assert + # nothing else is dirty so no stray file ships to the immutable crates.io release. + unexpected="$(git status --porcelain | grep -vE '^.. (Cargo\.toml|Cargo\.lock|CHANGELOG\.md)$' || true)" + if [ -n "${unexpected}" ]; then + echo "::error::unexpected uncommitted changes before publish — only Cargo.toml/Cargo.lock/CHANGELOG.md may be dirty:" + printf '%s\n' "${unexpected}" + exit 1 + fi cargo publish --allow-dirty - uses: coroboros/ci/.github/actions/release/github-release@v0 @@ -220,15 +206,11 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: main - fetch-depth: 0 + ref: ${{ github.sha }} - - name: Install cargo-dist and pin version - shell: bash - run: | - cargo install cargo-dist --version "${CARGO_DIST_VERSION}" --locked - cargo install cargo-edit --version "${CARGO_EDIT_VERSION}" --locked - cargo set-version "${GITHUB_REF_NAME}" + - uses: coroboros/ci/.github/actions/rust/install-dist@v0 + + - uses: coroboros/ci/.github/actions/rust/pin-version@v0 - name: Download build artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml new file mode 100644 index 0000000..445a0af --- /dev/null +++ b/.github/workflows/self-actions.yml @@ -0,0 +1,235 @@ +# Self-CI Actions +name: Self-CI Actions + +# A PR self-tests its own composite logic. The repo is checked out under `_src/`; each job +# builds a synthetic git fixture at the workspace root — where a composite's `run:` executes — +# so the composites act on the fixture, never the real tree or its origin. Pushes target a +# local bare remote; `contents: read` is the backstop against an accidental real-main push. +# No secrets. rust/base + the dist-* chain need a real toolchain/tag — exercised at release +# time and by the workflow_dispatch smokes below, not on every PR. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify-tag: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - name: Build fixture repo + shell: bash + run: | + set -euo pipefail + git init -q + git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false + git commit -q --allow-empty -m base + echo "GOOD_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" + - name: Pass — HEAD matches tag SHA + uses: ./_src/.github/actions/release/verify-tag + env: + GITHUB_SHA: ${{ env.GOOD_SHA }} + - name: Move HEAD so it diverges from the tag SHA + shell: bash + run: | + set -euo pipefail + git commit -q --allow-empty -m move + - name: Fail — main moved since the tag + id: moved + continue-on-error: true + uses: ./_src/.github/actions/release/verify-tag + env: + GITHUB_SHA: ${{ env.GOOD_SHA }} + - name: Assert the moved-main branch failed + shell: bash + run: | + set -euo pipefail + [ "${{ steps.moved.outcome }}" = "failure" ] || { echo "::error::verify-tag must fail when HEAD != GITHUB_SHA"; exit 1; } + echo "::notice::verify-tag fails on moved main" + + generate-changelog: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - name: Build fixture history + shell: bash + run: | + set -euo pipefail + git init -q + git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false + printf '# Changelog\n' > CHANGELOG.md + g() { git commit -q --allow-empty -m "$1"; } + g "chore: release 0.0.9" + g "feat(scope): add thing" + g "feat(api)!: drop v1" + g "fix: a bug" + g "docs: readme" + g "chore: tidy" + g "wip: experimental" + - name: Auto-generate from Conventional Commits + id: gen + uses: ./_src/.github/actions/release/generate-changelog + env: + GITHUB_REF_NAME: "0.1.0" + - name: Assert generated body + rewrite + shell: bash + env: + BODY: ${{ steps.gen.outputs.body }} + run: | + set -euo pipefail + # backticks in the grep needles are literal CHANGELOG bytes, not expansions + # shellcheck disable=SC2016 + for needle in \ + '### Breaking Changes' '- `api` — drop v1' \ + '### Features' '- `scope` — add thing' \ + '### Fixes' '- a bug' \ + '### Documentation' '- readme' \ + '### Configuration' '- tidy' \ + '### Others' '- experimental'; do + grep -qF "${needle}" <<<"${BODY}" || { echo "::error::body missing: ${needle}"; exit 1; } + done + if grep -qF '0.0.9' <<<"${BODY}"; then echo "::error::release commit not suppressed"; exit 1; fi + grep -qE '^## v0\.1\.0 - ' CHANGELOG.md || { echo "::error::CHANGELOG.md not rewritten"; exit 1; } + echo "::notice::generate-changelog auto-gen + suppression OK" + - name: Reject malformed tag — v prefix + id: badv + continue-on-error: true + uses: ./_src/.github/actions/release/generate-changelog + env: + GITHUB_REF_NAME: "v1.2.3" + - name: Reject malformed tag — missing patch + id: badpatch + continue-on-error: true + uses: ./_src/.github/actions/release/generate-changelog + env: + GITHUB_REF_NAME: "1.2" + - name: Assert the SemVer gate rejected both + shell: bash + run: | + set -euo pipefail + [ "${{ steps.badv.outcome }}" = "failure" ] || { echo "::error::v1.2.3 must fail the SemVer gate"; exit 1; } + [ "${{ steps.badpatch.outcome }}" = "failure" ] || { echo "::error::1.2 must fail the SemVer gate"; exit 1; } + echo "::notice::SemVer gate rejects malformed tags" + + commit-artifacts: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - name: Build fixture repo + local bare remote + shell: bash + run: | + set -euo pipefail + remote="${RUNNER_TEMP}/origin.git" + git init -q --bare "${remote}" + git init -q + git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false + git remote add origin "${remote}" + git commit -q --allow-empty -m base + git push -q origin HEAD:main + printf 'a\n' > art1.txt; printf 'b\n' > art2.txt + echo "REMOTE=${remote}" >> "${GITHUB_ENV}" + - name: Changed → commit + push (FILES word-split) + uses: ./_src/.github/actions/release/commit-artifacts + with: + files: art1.txt art2.txt + env: + GITHUB_REF_NAME: "1.0.0" + - name: Assert the remote advanced with both files + shell: bash + run: | + set -euo pipefail + git --git-dir="${REMOTE}" log -1 --pretty=%s main | grep -qF 'chore: release 1.0.0 [skip ci]' \ + || { echo "::error::wrong commit subject on pushed main"; exit 1; } + tree="$(git --git-dir="${REMOTE}" ls-tree --name-only main)" + grep -qx 'art1.txt' <<<"${tree}" || { echo "::error::art1.txt missing from pushed tree"; exit 1; } + grep -qx 'art2.txt' <<<"${tree}" || { echo "::error::art2.txt missing from pushed tree (FILES word-split)"; exit 1; } + echo "BEFORE=$(git --git-dir="${REMOTE}" rev-parse main)" >> "${GITHUB_ENV}" + echo "::notice::commit-artifacts pushed both artifacts" + - name: Nothing changed → no-op + uses: ./_src/.github/actions/release/commit-artifacts + with: + files: art1.txt + env: + GITHUB_REF_NAME: "1.0.0" + - name: Assert the no-op left the remote untouched + shell: bash + run: | + set -euo pipefail + [ "${BEFORE}" = "$(git --git-dir="${REMOTE}" rev-parse main)" ] \ + || { echo "::error::no-op branch pushed unexpectedly"; exit 1; } + echo "::notice::commit-artifacts no-op left main untouched" + + cargo-deny-guard: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - name: Plant a forbidden consumer override + shell: bash + run: | + set -euo pipefail + : > deny.exceptions.toml + - name: Composite must reject deny.exceptions.toml + id: reject + continue-on-error: true + uses: ./_src/.github/actions/security/cargo-deny + - name: Assert the reject guard fired + shell: bash + run: | + set -euo pipefail + [ "${{ steps.reject.outcome }}" = "failure" ] || { echo "::error::cargo-deny must reject a consumer deny.exceptions.toml"; exit 1; } + echo "::notice::cargo-deny rejects consumer deny.exceptions.toml" + + install-dist: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - uses: ./_src/.github/actions/rust/install-dist + - name: Assert dist is installed and runnable + shell: bash + run: | + set -euo pipefail + dist --version || { echo "::error::dist not on PATH after install-dist"; exit 1; } + echo "::notice::install-dist OK — $(dist --version)" + + pin-version-manual: + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: _src + - name: Build fixture crate + shell: bash + run: | + set -euo pipefail + printf '[package]\nname = "smoke"\nversion = "0.0.0"\nedition = "2021"\n' > Cargo.toml + mkdir -p src; printf 'fn main() {}\n' > src/main.rs + - name: pin-version stamps the tag + uses: ./_src/.github/actions/rust/pin-version + env: + GITHUB_REF_NAME: "9.9.9" + - name: Assert the version was stamped + shell: bash + run: | + set -euo pipefail + grep -q '^version = "9.9.9"$' Cargo.toml || { echo "::error::pin-version did not stamp 9.9.9"; exit 1; } + echo "::notice::pin-version (cargo-set-version) stamped 9.9.9" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f676e4..ec01931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,10 @@ ## v0.2.0 - 02/06/2026 ### Features -- `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. The verify build runs on publish — no `--no-verify` — so a crate that only builds in-workspace fails before an immutable release. +- `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. `publish` re-runs `rust/base` (fmt / clippy / test) on the tagged commit before `cargo publish`, mirroring the npm pipeline, so a library crate is re-validated at tag time and not only at PR time; `cargo publish`'s own verify build then runs with no `--no-verify`, so a crate that only builds in-workspace fails before an immutable release. +- `rust-packages` — add a branch-time `package` job (`cargo package --locked`, after `rust/native-deps`) that verify-builds the crate from its packaged tarball. A compile-time asset — an `include_str!`/`include_bytes!` file or a `build.rs` input — dropped from the package by an `exclude`/`.gitignore` rule now fails the PR rather than the tagged `cargo publish`, which verify-builds the same tarball at release time. - `rust-packages` — opt-in binary-distribution layer via cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]`. A tagged binary crate gets prebuilt per-target archives, `shell` + `powershell` installers, a Homebrew formula in the declared `tap`, and an npm shim — attached to the single GitHub Release, alongside the crates.io publish. The pipeline stays the sole release authority: `dist` only builds (final URLs derive from repo + tag), and the release goes live through draft → undraft. Library crates self-skip. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish` (gated by `cargo-deny` + `gitleaks`) and a `draft` input on `release/github-release`; optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets activate the tap and npm publishes. -- `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the publish verify build. +- `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the `dist-build` matrix. - `javascript/base` — wrap `pnpm install` in Socket Firewall (`sfw`), blocking confirmed-malicious packages before download. Fail-closed. The GitHub-runner equivalent of the image-baked firewall on GitLab. - `self-release` — move the rolling `v0` major tag to each stable `coroboros/ci` release, so `@v0` consumers track the latest release without a manual tag push. - `secrets` gate — gitleaks gates `publish` via `needs:` in the npm and Rust package workflows, alongside the supply-chain gate. A leaked secret blocks the release through the template's job graph, not the consumer's branch protection — parity with the GitLab `security-gate` stage. @@ -13,16 +14,28 @@ ### Fixes - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. - `release` — drop the "move rolling major tag" step from the npm and Rust publish jobs. Reusable workflows run in the caller's context, so it force-pushed a meaningless `vN` ref into every consumer repo; the `v0` ref now moves on `coroboros/ci`'s own release (see `self-release`). +- `rust-packages` — pin the `dist-plan`, `dist-build`, `dist-host` checkouts to the tag commit (`ref: ${{ github.sha }}`) rather than the moving `main`, dropping the unused `fetch-depth: 0` and `dist-plan`'s now-redundant `verify-tag`. The per-target binaries build the exact source the tag points to regardless of the `publish` commit-back timing; `verify-tag` stays on the `publish` jobs, the only ones that check out `main` to push back. +- `rust-packages` — guard `cargo publish` with a `git status --porcelain` allowlist (`Cargo.toml`/`Cargo.lock`/`CHANGELOG.md`); an unexpected dirty file now fails the release before the immutable crates.io publish instead of shipping silently under `--allow-dirty`. ### Refactor - `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. The `cargo-deny` composite imposes a canonical `security/deny.toml` via `--config` — a consumer `deny.toml` is ignored and a `deny.exceptions.toml` is rejected. - `release/commit-artifacts` — extract the commit-back step shared by the npm and Rust publish jobs into a composite (`files` input, `[skip ci]`), mirroring GitLab's `commit-release-artifacts`. +- `release/verify-tag`, `rust/pin-version` — extract two blocks that were duplicated across the tag-time jobs into composites (single source). `release/verify-tag` is the "main HEAD matches the tag SHA" guard shared by the npm and Rust `publish` jobs; `rust/pin-version` is the `cargo-set-version` install + `cargo set-version` stamp shared by `publish` and the three `dist-*` jobs. +- `security/cargo-deny` — drop the redundant `--deny unmaintained --deny unsound` CLI flags. The imposed `deny.toml` already errors on both at `"all"` scope (the v2 advisories schema cannot downgrade them), so the policy stays single-sourced in the ruleset. +- `rust/pin-version` — move the `cargo-edit` version pin into the composite as a step-level env; it lived at the `rust-packages` workflow level. The composite is now self-contained like `security/gitleaks`, with the pin declared where it is consumed and callable standalone. +- `rust/install-dist` — new composite installing cargo-dist's `dist` from the prebuilt, SHA-256-verified release tarball (per-OS: Linux/macOS/Windows), shared by `dist-plan`/`dist-build`/`dist-host`. Replaces a multi-minute `cargo install --locked` from-source compile in each and satisfies the binary-pinning rule (version + checksum, like `security/gitleaks`); the version pin lives in the composite. +- `rust/pin-version` — install only the `cargo-set-version` binary (`cargo install --bin`), the one subcommand used, instead of all of cargo-edit — ~75% less from-source build. cargo-edit ships no prebuilt binary, so the source install stays. + +### Tests +- `self-actions` — new self-CI workflow exercising the composites against synthetic fixtures on every PR: `release/verify-tag` (match + moved-main), `release/generate-changelog` (SemVer gate, auto-gen, release-commit suppression), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote), `security/cargo-deny` (reject guard), and `rust/install-dist` (download + checksum). A composite logic regression now fails the PR instead of surfacing only at a consumer's tagged release. `rust/base` and the dist-* chain stay `workflow_dispatch` smokes (real toolchain/tag). ### Documentation - `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab; document the opt-in binary-distribution layer (jobs, consumer contract, optional tap/npm secrets) and the imposed `cargo-deny` ruleset. +- `README` — correct the `release/verify-tag` row (shared by the npm and Rust `publish` jobs, not "every job that acts on `main`"; the `dist-*` jobs pin to the tag commit) and the cargo-dist install note (`rust/install-dist`, prebuilt + SHA-256 verified); add the `rust/install-dist` composable row. ### Configuration - `package.json` — bump to `0.2.0` (was `0.1.13`, lagging the `0.1.14` tag). +- `renovate.json` — Renovate custom managers auto-bump the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit) via review-gated PRs, scoped so Dependabot keeps the action SHAs. The paired tarball SHA-256 values stay a manual step on the bump PR — Renovate cannot recompute them on the hosted app — flagged in the PR body. ## v0.1.14 - 01/06/2026 diff --git a/CLAUDE.md b/CLAUDE.md index 9f308a6..a03be9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,11 +10,11 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. ## Important files - `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). -- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). +- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `package` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). -- `.github/workflows/{self,self-security,self-release}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), and the `v0` rolling-tag move. -- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps},security/{gitleaks,osv-scanner,cargo-deny},release/{generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. -- `.github/dependabot.yml` — auto-PRs for pinned actions. +- `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the release composites against fixtures on every PR. +- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. +- `.github/dependabot.yml` — auto-PRs for pinned action SHAs. `renovate.json` — Renovate custom managers auto-bump the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit). - `security/.gitleaks.toml` — canonical gitleaks ruleset. - `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). diff --git a/README.md b/README.md index a156a06..4c5c275 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Consumer requirements: **Sequence**: 1. Checkout `main` with full history -2. Verify `main` HEAD matches the tag SHA +2. Verify `main` HEAD matches the tag SHA via [`release/verify-tag`](#composable-actions) 3. Run [`check-docs`](#composable-actions) 4. Run [`javascript/base`](#composable-actions) 5. Pin `package.json` version to the tag @@ -118,6 +118,7 @@ Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: - `rust-toolchain.toml` — pins the channel and lists the `clippy` + `rustfmt` components (`rustup` installs them on first `cargo` use; omit them and `fmt`/`clippy` fail on a pinned channel). - `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. +- compile-time assets — any `include_str!` / `include_bytes!` / `build.rs` input must sit under the package root and stay unignored (no `exclude`/`.gitignore` rule drops it). The `package` job verify-builds the packaged crate so a dropped asset fails the PR, not the tagged publish. - cargo-deny policy — imposed by `coroboros/ci`; no consumer `deny.toml` required, and a local one is ignored. See [Security](#security). - `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. @@ -159,6 +160,17 @@ Consumer requirements: +
+package + +
+ +**Trigger**: `branch push`. + +`cargo package --locked` builds the crate from its packaged tarball — the same bytes `cargo install` would compile downstream — after [`rust/native-deps`](#composable-actions) supplies any `-sys` toolchain. A compile-time asset (`include_str!` / `include_bytes!` / `build.rs` input) silently dropped from the package fails the PR here. `publish`'s own `cargo publish` verify build is the tag-time twin; `preflight` builds from the work tree and would not catch it. + +
+
publish @@ -168,10 +180,10 @@ Consumer requirements: **Sequence**: 1. Checkout `main` with full history -2. Verify `main` HEAD matches the tag SHA +2. Verify `main` HEAD matches the tag SHA via [`release/verify-tag`](#composable-actions) 3. Run [`check-docs`](#composable-actions) -4. Run [`rust/native-deps`](#composable-actions) — native deps for the publish verify build -5. Pin `Cargo.toml` to the tag (`cargo set-version`) +4. Run [`rust/base`](#composable-actions) +5. Pin `Cargo.toml` to the tag via [`rust/pin-version`](#composable-actions) 6. Generate `CHANGELOG.md` section via [`release/generate-changelog`](#composable-actions) 7. `cargo publish` to crates.io — OIDC by default, token bootstrap for a new crate (see [Security](#security)) 8. Create GitHub Release via [`release/github-release`](#composable-actions) — a draft when the consumer ships binaries (undrafted once assets upload), else final @@ -193,7 +205,7 @@ The shared pipeline is the sole release authority — `publish` creates the one - **`dist-host`** — builds the global installers + Homebrew formula + npm shim (`dist build --artifacts=global`; final download URLs derive from repo + tag), uploads every asset to the release, then undrafts it. - **`dist-publish`** — commits the formula to the declared `tap` (`HOMEBREW_TAP_TOKEN`) and publishes the npm shim (OIDC + provenance, or `NPM_PACKAGE_REGISTRY_TOKEN` bootstrap). Each self-skips when its installer or secret is absent. -`dist` is version-pinned via `cargo install cargo-dist --version 0.32.0 --locked`. Per-target Cargo features are not expressible in cargo-dist 0.32.0. Set them consumer-side via `cfg`, e.g. a Metal build gated on `cfg(target_os = "macos")`. macOS Developer-ID signing + notarization are deferred. +`dist` is installed prebuilt and SHA-256 verified (version `0.32.0`) via [`rust/install-dist`](#composable-actions). Per-target Cargo features are not expressible in cargo-dist 0.32.0. Set them consumer-side via `cfg`, e.g. a Metal build gated on `cfg(target_os = "macos")`. macOS Developer-ID signing + notarization are deferred.
@@ -229,10 +241,13 @@ Reusable sub-workflow with three parallel scans: | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | | `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | -| `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the publish verify build. No-op when absent. | +| `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the `dist-build` matrix. No-op when absent. | +| `rust/install-dist` | Rust | Installs cargo-dist's `dist` binary, prebuilt and SHA-256 verified (Linux/macOS/Windows). Shared by the `dist-plan`, `dist-build`, `dist-host` jobs. | +| `rust/pin-version` | Rust | Installs version-pinned `cargo-set-version` (cargo-edit) and stamps `Cargo.toml` to the release tag. Shared by `publish` and the `dist-*` jobs. | | `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The Rust `supply-chain` gate. | +| `release/verify-tag` | transverse | Fails the release unless the checked-out `main` HEAD matches the tag SHA. Shared by the npm and Rust `publish` jobs — the tag-time jobs that check out `main` to push back; the `dist-*` jobs pin to the tag commit (`github.sha`) instead. | | `release/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog` (see [Examples](#examples)). | | `release/commit-artifacts` | transverse | Stages the given files and commits them back to `main` as `chore: release ${tag} [skip ci]`. No-op when nothing changed. | diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..9b63bff --- /dev/null +++ b/renovate.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "schedule:earlyMondays", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":dependencyDashboard", + ":separateMajorReleases" + ], + "labels": ["renovate"], + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "rangeStrategy": "bump", + "enabledManagers": ["custom.regex"], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)\\.github/workflows/self\\.yml$/"], + "matchStrings": ["ACTIONLINT_VERSION:\\s*\"(?[^\"]+)\""], + "depNameTemplate": "rhysd/actionlint", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v?(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)\\.github/workflows/self\\.yml$/"], + "matchStrings": ["YAMLLINT_VERSION:\\s*\"(?[^\"]+)\""], + "depNameTemplate": "yamllint", + "datasourceTemplate": "pypi" + }, + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)\\.github/actions/security/gitleaks/action\\.yml$/"], + "matchStrings": ["GITLEAKS_VERSION:\\s*\"(?[^\"]+)\""], + "depNameTemplate": "gitleaks/gitleaks", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v?(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)\\.github/actions/rust/install-dist/action\\.yml$/"], + "matchStrings": ["CARGO_DIST_VERSION:\\s*\"(?[^\"]+)\""], + "depNameTemplate": "axodotdev/cargo-dist", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v?(?.+)$" + }, + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)\\.github/actions/rust/pin-version/action\\.yml$/"], + "matchStrings": ["CARGO_EDIT_VERSION:\\s*\"(?[^\"]+)\""], + "depNameTemplate": "cargo-edit", + "datasourceTemplate": "crate" + } + ], + "packageRules": [ + { + "description": "Review-gate every update; nothing automerges.", + "matchUpdateTypes": ["minor", "patch", "major", "pin", "digest"], + "automerge": false + }, + { + "description": "These tools carry a paired SHA-256 (and cargo-dist five) that Renovate cannot recompute; the install step's sha256sum -c stays red until a maintainer updates the *_SHA256 line on the PR branch.", + "matchDepNames": ["rhysd/actionlint", "gitleaks/gitleaks", "axodotdev/cargo-dist"], + "prBodyNotes": [ + ":warning: This bump also needs the paired `*_SHA256` value(s) updated by hand. Download the tarball(s) for the new version, run `sha256sum`, and push the new hash onto this PR branch — the install step's `sha256sum -c -` stays red until then." + ] + } + ] +} From fc47bb3149aae00d73ff0450eb1a9c15ad0aca07 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 19:16:21 +0700 Subject: [PATCH 26/36] fix(ci): drive self-actions against the real checkout, not env overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reserved GITHUB_* env vars (GITHUB_SHA/GITHUB_REF_NAME) can't be overridden into a composite's run steps — the runner re-injects the canonical value — so the fixture- with-overrides smokes fed the composites the real PR ref and failed. Run them against the real checkout instead (HEAD == GITHUB_SHA on PR/push) and cover only the non-tag- reachable logic; tag-driven paths stay validated at release. --- .github/workflows/self-actions.yml | 165 ++++++----------------------- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- 3 files changed, 33 insertions(+), 136 deletions(-) diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index 445a0af..190675b 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -1,169 +1,96 @@ # Self-CI Actions name: Self-CI Actions -# A PR self-tests its own composite logic. The repo is checked out under `_src/`; each job -# builds a synthetic git fixture at the workspace root — where a composite's `run:` executes — -# so the composites act on the fixture, never the real tree or its origin. Pushes target a -# local bare remote; `contents: read` is the backstop against an accidental real-main push. -# No secrets. rust/base + the dist-* chain need a real toolchain/tag — exercised at release -# time and by the workflow_dispatch smokes below, not on every PR. +# A PR self-tests its own composite logic via local `./` refs. The composites run against +# the real checkout, where GITHUB_SHA == HEAD (pull_request / push semantics). `commit-artifacts` +# rewires `origin` to a local bare remote and relies on `contents: read` as the backstop, so no +# real branch is touched. No secrets. +# +# A composite's tag-driven paths that read the runner's GITHUB_REF_NAME / GITHUB_SHA can't be +# faked here — those reserved defaults are re-injected inside a composite and can't be overridden +# by the caller — so generate-changelog's auto-gen and pin-version's stamp are exercised at real +# release time. This workflow covers the parts reachable on a non-tag event. on: push: branches: [main] pull_request: - workflow_dispatch: permissions: contents: read jobs: verify-tag: - if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - - name: Build fixture repo + - name: Pass — HEAD matches the run SHA + uses: ./.github/actions/release/verify-tag + - name: Move HEAD so it diverges from the run SHA shell: bash run: | set -euo pipefail - git init -q git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false - git commit -q --allow-empty -m base - echo "GOOD_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" - - name: Pass — HEAD matches tag SHA - uses: ./_src/.github/actions/release/verify-tag - env: - GITHUB_SHA: ${{ env.GOOD_SHA }} - - name: Move HEAD so it diverges from the tag SHA - shell: bash - run: | - set -euo pipefail - git commit -q --allow-empty -m move - - name: Fail — main moved since the tag + git commit -q --allow-empty -m "smoke: move HEAD" + - name: Fail — HEAD no longer matches id: moved continue-on-error: true - uses: ./_src/.github/actions/release/verify-tag - env: - GITHUB_SHA: ${{ env.GOOD_SHA }} - - name: Assert the moved-main branch failed + uses: ./.github/actions/release/verify-tag + - name: Assert the moved branch failed shell: bash run: | set -euo pipefail [ "${{ steps.moved.outcome }}" = "failure" ] || { echo "::error::verify-tag must fail when HEAD != GITHUB_SHA"; exit 1; } - echo "::notice::verify-tag fails on moved main" + echo "::notice::verify-tag passes on match, fails on divergence" generate-changelog: - if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - - name: Build fixture history - shell: bash - run: | - set -euo pipefail - git init -q - git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false - printf '# Changelog\n' > CHANGELOG.md - g() { git commit -q --allow-empty -m "$1"; } - g "chore: release 0.0.9" - g "feat(scope): add thing" - g "feat(api)!: drop v1" - g "fix: a bug" - g "docs: readme" - g "chore: tidy" - g "wip: experimental" - - name: Auto-generate from Conventional Commits - id: gen - uses: ./_src/.github/actions/release/generate-changelog - env: - GITHUB_REF_NAME: "0.1.0" - - name: Assert generated body + rewrite - shell: bash - env: - BODY: ${{ steps.gen.outputs.body }} - run: | - set -euo pipefail - # backticks in the grep needles are literal CHANGELOG bytes, not expansions - # shellcheck disable=SC2016 - for needle in \ - '### Breaking Changes' '- `api` — drop v1' \ - '### Features' '- `scope` — add thing' \ - '### Fixes' '- a bug' \ - '### Documentation' '- readme' \ - '### Configuration' '- tidy' \ - '### Others' '- experimental'; do - grep -qF "${needle}" <<<"${BODY}" || { echo "::error::body missing: ${needle}"; exit 1; } - done - if grep -qF '0.0.9' <<<"${BODY}"; then echo "::error::release commit not suppressed"; exit 1; fi - grep -qE '^## v0\.1\.0 - ' CHANGELOG.md || { echo "::error::CHANGELOG.md not rewritten"; exit 1; } - echo "::notice::generate-changelog auto-gen + suppression OK" - - name: Reject malformed tag — v prefix - id: badv - continue-on-error: true - uses: ./_src/.github/actions/release/generate-changelog - env: - GITHUB_REF_NAME: "v1.2.3" - - name: Reject malformed tag — missing patch - id: badpatch + - name: SemVer gate rejects a non-tag ref + id: gate continue-on-error: true - uses: ./_src/.github/actions/release/generate-changelog - env: - GITHUB_REF_NAME: "1.2" - - name: Assert the SemVer gate rejected both + uses: ./.github/actions/release/generate-changelog + - name: Assert the gate rejected it shell: bash run: | set -euo pipefail - [ "${{ steps.badv.outcome }}" = "failure" ] || { echo "::error::v1.2.3 must fail the SemVer gate"; exit 1; } - [ "${{ steps.badpatch.outcome }}" = "failure" ] || { echo "::error::1.2 must fail the SemVer gate"; exit 1; } - echo "::notice::SemVer gate rejects malformed tags" + [ "${{ steps.gate.outcome }}" = "failure" ] || { echo "::error::SemVer gate must reject the non-SemVer ref '${GITHUB_REF_NAME}'"; exit 1; } + echo "::notice::generate-changelog SemVer gate rejects non-tag refs" commit-artifacts: - if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - - name: Build fixture repo + local bare remote + - name: Rewire origin to a local bare remote + stage artifacts shell: bash run: | set -euo pipefail remote="${RUNNER_TEMP}/origin.git" git init -q --bare "${remote}" - git init -q git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false - git remote add origin "${remote}" - git commit -q --allow-empty -m base + git remote set-url origin "${remote}" git push -q origin HEAD:main printf 'a\n' > art1.txt; printf 'b\n' > art2.txt echo "REMOTE=${remote}" >> "${GITHUB_ENV}" - name: Changed → commit + push (FILES word-split) - uses: ./_src/.github/actions/release/commit-artifacts + uses: ./.github/actions/release/commit-artifacts with: files: art1.txt art2.txt - env: - GITHUB_REF_NAME: "1.0.0" - name: Assert the remote advanced with both files shell: bash run: | set -euo pipefail - git --git-dir="${REMOTE}" log -1 --pretty=%s main | grep -qF 'chore: release 1.0.0 [skip ci]' \ - || { echo "::error::wrong commit subject on pushed main"; exit 1; } + git --git-dir="${REMOTE}" log -1 --pretty=%s main | grep -qE '^chore: release .+ \[skip ci\]$' \ + || { echo "::error::commit-artifacts subject malformed"; exit 1; } tree="$(git --git-dir="${REMOTE}" ls-tree --name-only main)" grep -qx 'art1.txt' <<<"${tree}" || { echo "::error::art1.txt missing from pushed tree"; exit 1; } grep -qx 'art2.txt' <<<"${tree}" || { echo "::error::art2.txt missing from pushed tree (FILES word-split)"; exit 1; } echo "BEFORE=$(git --git-dir="${REMOTE}" rev-parse main)" >> "${GITHUB_ENV}" echo "::notice::commit-artifacts pushed both artifacts" - name: Nothing changed → no-op - uses: ./_src/.github/actions/release/commit-artifacts + uses: ./.github/actions/release/commit-artifacts with: files: art1.txt - env: - GITHUB_REF_NAME: "1.0.0" - name: Assert the no-op left the remote untouched shell: bash run: | @@ -173,12 +100,9 @@ jobs: echo "::notice::commit-artifacts no-op left main untouched" cargo-deny-guard: - if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - name: Plant a forbidden consumer override shell: bash run: | @@ -187,7 +111,7 @@ jobs: - name: Composite must reject deny.exceptions.toml id: reject continue-on-error: true - uses: ./_src/.github/actions/security/cargo-deny + uses: ./.github/actions/security/cargo-deny - name: Assert the reject guard fired shell: bash run: | @@ -196,40 +120,13 @@ jobs: echo "::notice::cargo-deny rejects consumer deny.exceptions.toml" install-dist: - if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - - uses: ./_src/.github/actions/rust/install-dist + - uses: ./.github/actions/rust/install-dist - name: Assert dist is installed and runnable shell: bash run: | set -euo pipefail dist --version || { echo "::error::dist not on PATH after install-dist"; exit 1; } echo "::notice::install-dist OK — $(dist --version)" - - pin-version-manual: - if: ${{ github.event_name == 'workflow_dispatch' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: _src - - name: Build fixture crate - shell: bash - run: | - set -euo pipefail - printf '[package]\nname = "smoke"\nversion = "0.0.0"\nedition = "2021"\n' > Cargo.toml - mkdir -p src; printf 'fn main() {}\n' > src/main.rs - - name: pin-version stamps the tag - uses: ./_src/.github/actions/rust/pin-version - env: - GITHUB_REF_NAME: "9.9.9" - - name: Assert the version was stamped - shell: bash - run: | - set -euo pipefail - grep -q '^version = "9.9.9"$' Cargo.toml || { echo "::error::pin-version did not stamp 9.9.9"; exit 1; } - echo "::notice::pin-version (cargo-set-version) stamped 9.9.9" diff --git a/CHANGELOG.md b/CHANGELOG.md index ec01931..20da1e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ - `rust/pin-version` — install only the `cargo-set-version` binary (`cargo install --bin`), the one subcommand used, instead of all of cargo-edit — ~75% less from-source build. cargo-edit ships no prebuilt binary, so the source install stays. ### Tests -- `self-actions` — new self-CI workflow exercising the composites against synthetic fixtures on every PR: `release/verify-tag` (match + moved-main), `release/generate-changelog` (SemVer gate, auto-gen, release-commit suppression), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote), `security/cargo-deny` (reject guard), and `rust/install-dist` (download + checksum). A composite logic regression now fails the PR instead of surfacing only at a consumer's tagged release. `rust/base` and the dist-* chain stay `workflow_dispatch` smokes (real toolchain/tag). +- `self-actions` — new self-CI workflow exercising the composites on every PR via local refs: `release/verify-tag` (HEAD match + moved-HEAD failure), `release/generate-changelog` (SemVer-gate rejection of a non-tag ref), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote, `contents: read` backstop), `security/cargo-deny` (consumer-override reject guard), and `rust/install-dist` (download + checksum + run). A regression in the reachable logic now fails the PR. Tag-driven paths that read the runner's `GITHUB_REF_NAME`/`GITHUB_SHA` (generate-changelog auto-gen, pin-version stamp) can't be faked on a non-tag event — those reserved defaults aren't overridable into a composite — so they stay validated at real release time. ### Documentation - `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab; document the opt-in binary-distribution layer (jobs, consumer contract, optional tap/npm secrets) and the imposed `cargo-deny` ruleset. diff --git a/CLAUDE.md b/CLAUDE.md index a03be9f..cd4209f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). - `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `package` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). -- `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the release composites against fixtures on every PR. +- `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the composites against the real checkout on every PR. - `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned action SHAs. `renovate.json` — Renovate custom managers auto-bump the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit). - `security/.gitleaks.toml` — canonical gitleaks ruleset. From 0e8308ed0f54f764fb7634617e9bd1519c2a9eaa Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 19:18:13 +0700 Subject: [PATCH 27/36] fix(ci): pre-create main on the bare remote in the commit-artifacts smoke --- .github/workflows/self-actions.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index 190675b..a43be7c 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -69,7 +69,10 @@ jobs: git init -q --bare "${remote}" git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false git remote set-url origin "${remote}" - git push -q origin HEAD:main + # Fully-qualified ref: the bare remote has no `main` yet and the PR checkout is a + # detached HEAD, so `HEAD:main` can't be guessed. The composite's own `HEAD:main` + # then resolves because main now exists (as it does on a real consumer's origin). + git push -q origin HEAD:refs/heads/main printf 'a\n' > art1.txt; printf 'b\n' > art2.txt echo "REMOTE=${remote}" >> "${GITHUB_ENV}" - name: Changed → commit + push (FILES word-split) From 713cb5ea2fccdcbc8cc292cdf900542a75c5c7d8 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 19:20:44 +0700 Subject: [PATCH 28/36] fix(ci): isolate the commit-artifacts smoke in a non-shallow fixture repo --- .github/workflows/self-actions.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index a43be7c..f4ffed8 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -61,22 +61,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Rewire origin to a local bare remote + stage artifacts + with: + path: _src + - name: Build a fixture repo + local bare remote shell: bash run: | set -euo pipefail - remote="${RUNNER_TEMP}/origin.git" - git init -q --bare "${remote}" + # Throwaway non-shallow repo at the workspace root, where the composite's run executes. + # The PR's own checkout is shallow (push rejected) and lives under _src. GITHUB_REF_NAME + # is the real ref here — it only lands in the commit subject, asserted loosely below. + remote="${RUNNER_TEMP}/origin.git"; git init -q --bare "${remote}" + git init -q -b main git config user.email "ci@ci"; git config user.name "ci"; git config commit.gpgsign false - git remote set-url origin "${remote}" - # Fully-qualified ref: the bare remote has no `main` yet and the PR checkout is a - # detached HEAD, so `HEAD:main` can't be guessed. The composite's own `HEAD:main` - # then resolves because main now exists (as it does on a real consumer's origin). + git remote add origin "${remote}" + git commit -q --allow-empty -m base git push -q origin HEAD:refs/heads/main printf 'a\n' > art1.txt; printf 'b\n' > art2.txt echo "REMOTE=${remote}" >> "${GITHUB_ENV}" - name: Changed → commit + push (FILES word-split) - uses: ./.github/actions/release/commit-artifacts + uses: ./_src/.github/actions/release/commit-artifacts with: files: art1.txt art2.txt - name: Assert the remote advanced with both files @@ -91,7 +94,7 @@ jobs: echo "BEFORE=$(git --git-dir="${REMOTE}" rev-parse main)" >> "${GITHUB_ENV}" echo "::notice::commit-artifacts pushed both artifacts" - name: Nothing changed → no-op - uses: ./.github/actions/release/commit-artifacts + uses: ./_src/.github/actions/release/commit-artifacts with: files: art1.txt - name: Assert the no-op left the remote untouched From 3b7396e98d82021231f717db45d9e86a93fcc40b Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 19:33:07 +0700 Subject: [PATCH 29/36] feat(ci): self-hosted Renovate with same-PR SHA-256 resync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run Renovate as a scheduled, SHA-pinned Action (PAT in RENOVATE_TOKEN) instead of the Mend app — no third-party app, and postUpgradeTasks can run. A postUpgradeTask script (allowlisted via RENOVATE_ALLOWED_COMMANDS) re-syncs each tool's tarball SHA-256 to the bumped version in the same PR, so the version pin and its checksum never drift. --- .github/renovate/sync-tool-sha.sh | 41 +++++++++++++++++++++++++++++++ .github/workflows/renovate.yml | 28 +++++++++++++++++++++ CHANGELOG.md | 2 +- CLAUDE.md | 2 +- renovate.json | 15 +++++++---- 5 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 .github/renovate/sync-tool-sha.sh create mode 100644 .github/workflows/renovate.yml diff --git a/.github/renovate/sync-tool-sha.sh b/.github/renovate/sync-tool-sha.sh new file mode 100644 index 0000000..3581285 --- /dev/null +++ b/.github/renovate/sync-tool-sha.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Re-sync each pinned tarball SHA-256 to its current pinned version, in place. Idempotent. +# Run as a Renovate postUpgradeTask after a version bump so the version and its checksum land +# in the same PR. Executes at the repo root inside the Renovate container (curl, sha256sum, +# sed, awk, grep all present). Renovate gates it via RENOVATE_ALLOWED_COMMANDS. +set -euo pipefail + +GITLEAKS_YML=".github/actions/security/gitleaks/action.yml" +SELF_YML=".github/workflows/self.yml" +DIST_YML=".github/actions/rust/install-dist/action.yml" + +sha256_of() { + local tmp; tmp="$(mktemp)" + curl -fsSL "$1" -o "$tmp" + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$tmp" | cut -d' ' -f1 + else shasum -a 256 "$tmp" | cut -d' ' -f1; fi +} +ver() { grep -E "$2:" "$1" | head -1 | sed -E 's/.*"([^"]+)".*/\1/'; } +set_sha() { sed -i -E "s|($2: *\")[^\"]+(\")|\1${3}\2|" "$1"; } + +# gitleaks — single linux x64 tarball +v="$(ver "$GITLEAKS_YML" GITLEAKS_VERSION)" +set_sha "$GITLEAKS_YML" GITLEAKS_SHA256 \ + "$(sha256_of "https://github.com/gitleaks/gitleaks/releases/download/v${v}/gitleaks_${v}_linux_x64.tar.gz")" + +# actionlint — single linux amd64 tarball +v="$(ver "$SELF_YML" ACTIONLINT_VERSION)" +set_sha "$SELF_YML" ACTIONLINT_SHA256 \ + "$(sha256_of "https://github.com/rhysd/actionlint/releases/download/v${v}/actionlint_${v}_linux_amd64.tar.gz")" + +# cargo-dist — five per-OS archives, read from the release's own sha256.sum +v="$(ver "$DIST_YML" CARGO_DIST_VERSION)" +sums="$(curl -fsSL "https://github.com/axodotdev/cargo-dist/releases/download/v${v}/sha256.sum")" +pick() { grep -F "cargo-dist-$1" <<<"$sums" | awk '{print $1}'; } +set_sha "$DIST_YML" SHA256_X86_64_LINUX "$(pick x86_64-unknown-linux-gnu.tar.xz)" +set_sha "$DIST_YML" SHA256_AARCH64_LINUX "$(pick aarch64-unknown-linux-gnu.tar.xz)" +set_sha "$DIST_YML" SHA256_X86_64_DARWIN "$(pick x86_64-apple-darwin.tar.xz)" +set_sha "$DIST_YML" SHA256_AARCH64_DARWIN "$(pick aarch64-apple-darwin.tar.xz)" +set_sha "$DIST_YML" SHA256_X86_64_WINDOWS "$(pick x86_64-pc-windows-msvc.zip)" + +echo "::notice::tool SHA-256 values re-synced to their pinned versions" diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 0000000..6c86990 --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,28 @@ +# Renovate +name: Renovate + +# Self-hosted Renovate: bumps the version-pinned tooling (renovate.json custom managers) and +# re-syncs each paired tarball SHA-256 in the same PR via a postUpgradeTask. Runs only when this +# workflow fires (the cron is the schedule). Needs a PAT in the RENOVATE_TOKEN secret so its PRs +# trigger the rest of self-CI — the built-in GITHUB_TOKEN would not, and it can't touch workflow +# files. PAT scope is documented in CLAUDE.md. +on: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 + with: + token: ${{ secrets.RENOVATE_TOKEN }} + env: + RENOVATE_REPOSITORIES: '["coroboros/ci"]' + # Gate the SHA-resync postUpgradeTask; the regex must match the exact resolved command. + RENOVATE_ALLOWED_COMMANDS: '["^bash \\.github/renovate/sync-tool-sha\\.sh$"]' + LOG_LEVEL: "info" diff --git a/CHANGELOG.md b/CHANGELOG.md index 20da1e3..4241cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ ### Configuration - `package.json` — bump to `0.2.0` (was `0.1.13`, lagging the `0.1.14` tag). -- `renovate.json` — Renovate custom managers auto-bump the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit) via review-gated PRs, scoped so Dependabot keeps the action SHAs. The paired tarball SHA-256 values stay a manual step on the bump PR — Renovate cannot recompute them on the hosted app — flagged in the PR body. +- `renovate.json` + `renovate.yml` — self-hosted Renovate (scheduled workflow, `RENOVATE_TOKEN` PAT) auto-bumps the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit) via review-gated PRs, scoped so Dependabot keeps the action SHAs. A `postUpgradeTask` (`.github/renovate/sync-tool-sha.sh`) re-syncs each paired tarball SHA-256 to the bumped version in the same PR, so version and checksum never drift. ## v0.1.14 - 01/06/2026 diff --git a/CLAUDE.md b/CLAUDE.md index cd4209f..30eeac5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). - `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the composites against the real checkout on every PR. - `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. -- `.github/dependabot.yml` — auto-PRs for pinned action SHAs. `renovate.json` — Renovate custom managers auto-bump the version-pinned tooling (gitleaks, actionlint, yamllint, cargo-dist, cargo-edit). +- `.github/dependabot.yml` — auto-PRs for pinned action SHAs. `renovate.json` + `.github/workflows/renovate.yml` — self-hosted Renovate (needs the `RENOVATE_TOKEN` PAT secret, scope `repo` + `workflow`) auto-bumps the version-pinned tooling; `.github/renovate/sync-tool-sha.sh` re-syncs each paired tarball SHA-256 in the same PR. - `security/.gitleaks.toml` — canonical gitleaks ruleset. - `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). diff --git a/renovate.json b/renovate.json index 9b63bff..6b2cf7d 100644 --- a/renovate.json +++ b/renovate.json @@ -2,7 +2,6 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", - "schedule:earlyMondays", ":semanticCommits", ":semanticCommitTypeAll(chore)", ":dependencyDashboard", @@ -60,11 +59,17 @@ "automerge": false }, { - "description": "These tools carry a paired SHA-256 (and cargo-dist five) that Renovate cannot recompute; the install step's sha256sum -c stays red until a maintainer updates the *_SHA256 line on the PR branch.", + "description": "These tools carry a paired tarball SHA-256; a postUpgradeTask re-syncs every *_SHA256 to the bumped version so version + checksum land in the same PR. Self-hosted only — the command is allowlisted via RENOVATE_ALLOWED_COMMANDS in renovate.yml.", "matchDepNames": ["rhysd/actionlint", "gitleaks/gitleaks", "axodotdev/cargo-dist"], - "prBodyNotes": [ - ":warning: This bump also needs the paired `*_SHA256` value(s) updated by hand. Download the tarball(s) for the new version, run `sha256sum`, and push the new hash onto this PR branch — the install step's `sha256sum -c -` stays red until then." - ] + "postUpgradeTasks": { + "commands": ["bash .github/renovate/sync-tool-sha.sh"], + "fileFilters": [ + ".github/actions/security/gitleaks/action.yml", + ".github/workflows/self.yml", + ".github/actions/rust/install-dist/action.yml" + ], + "executionMode": "branch" + } } ] } From 49df5a929295b8924d5166638efc1d05c90071af Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 21:16:42 +0700 Subject: [PATCH 30/36] feat(ci): host native/C++ CLIs in the rust pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three seams plus a release-ordering fix let a native/C++ CLI ride the shared pipeline without it learning zig, clang, or musl: - dist-build exports CARGO_DIST_TARGET to the target-aware ci/setup.sh hook; host preflight sees it empty. - rust/test-deps loads ci/test.env into the job env and runs ci/test-setup.sh before cargo test, so model/ffmpeg-gated tests fail loud instead of skipping. - Swatinem rust-cache caches ~/.cargo + target/ — per-target key on dist-build, default-branch save in rust/base. - publish gates on dist-build via !cancelled() + result != failure, so a failed binary build never publishes to crates.io; library crates still publish. Workflow concurrency serializes same-ref releases; the tap push rebase-retries. Also: explicit rustup toolchain install, --provenance on both npm-shim paths, and rename the secrets gate to secret-scan. self-actions smokes both new hooks. --- .github/actions/rust/base/action.yml | 24 ++++----- .github/actions/rust/test-deps/action.yml | 21 ++++++++ .github/workflows/rust-packages.yml | 40 ++++++++++----- .github/workflows/self-actions.yml | 60 +++++++++++++++++++++++ CHANGELOG.md | 11 +++++ CLAUDE.md | 6 +-- README.md | 29 ++++++----- security/deny.toml | 6 ++- 8 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 .github/actions/rust/test-deps/action.yml diff --git a/.github/actions/rust/base/action.yml b/.github/actions/rust/base/action.yml index 2c7cedc..ee8d6f7 100644 --- a/.github/actions/rust/base/action.yml +++ b/.github/actions/rust/base/action.yml @@ -5,7 +5,7 @@ description: Base setup for a Rust pipeline job. runs: using: composite steps: - - name: Resolve Rust toolchain + - name: Install Rust toolchain shell: bash run: | if [ ! -f rust-toolchain.toml ]; then @@ -13,18 +13,17 @@ runs: exit 1 fi channel="$(grep -E '^[[:space:]]*channel[[:space:]]*=' rust-toolchain.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" - echo "::notice::Resolved Rust toolchain: ${channel:-unset} (from rust-toolchain.toml)" + if [ -z "${channel}" ]; then + echo "::error::no channel found in rust-toolchain.toml" + exit 1 + fi + echo "::notice::Installing Rust toolchain: ${channel} (from rust-toolchain.toml)" + rustup toolchain install "${channel}" --profile minimal --component rustfmt --component clippy --no-self-update - - name: Setup cargo cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + - name: Cache cargo + target + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git/db - key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- + save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - name: Native build dependencies uses: coroboros/ci/.github/actions/rust/native-deps@v0 @@ -37,6 +36,9 @@ runs: shell: bash run: cargo clippy --all-targets --locked -- -D warnings + - name: Test dependencies + uses: coroboros/ci/.github/actions/rust/test-deps@v0 + - name: Test shell: bash run: cargo test --locked diff --git a/.github/actions/rust/test-deps/action.yml b/.github/actions/rust/test-deps/action.yml new file mode 100644 index 0000000..f835715 --- /dev/null +++ b/.github/actions/rust/test-deps/action.yml @@ -0,0 +1,21 @@ +# Rust Test Dependencies Action Composite +name: rust-test-deps +description: Load the optional ci/test.env into the job environment and run the optional ci/test-setup.sh test-fixture hook. + +runs: + using: composite + steps: + - name: Test environment and fixtures + shell: bash + run: | + if [ -f ci/test.env ]; then + grep -E '^[A-Za-z_][A-Za-z0-9_]*=' ci/test.env >> "${GITHUB_ENV}" || true + echo "::notice::Loaded ci/test.env into the job environment" + else + echo "::notice::No ci/test.env — no test environment overrides" + fi + if [ -f ci/test-setup.sh ]; then + bash ci/test-setup.sh + else + echo "::notice::No ci/test-setup.sh — no test fixtures" + fi diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 98cba8c..3a5e229 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -14,6 +14,11 @@ on: permissions: contents: read +# Serialize same-ref runs; queue behind an in-flight release rather than cancel it. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: preflight: if: ${{ github.ref_type == 'branch' }} @@ -33,7 +38,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/cargo-deny@v0 - secrets: + secret-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -105,18 +110,26 @@ jobs: if-no-files-found: error dist-build: - needs: [dist-plan, supply-chain, secrets] # no artifact builds until the gates pass + needs: [dist-plan, supply-chain, secret-scan] # no artifact builds until the gates pass if: ${{ needs.dist-plan.outputs.enabled == 'true' }} strategy: fail-fast: false matrix: ${{ fromJson(needs.dist-plan.outputs.matrix) }} runs-on: ${{ matrix.runner }} container: ${{ matrix.container && matrix.container.image || null }} + # The resolved target triple(s) reach the consumer's target-aware ci/setup.sh via native-deps. + env: + CARGO_DIST_TARGET: "${{ join(matrix.targets, ' ') }}" steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.sha }} + - name: Cache cargo + target + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + key: ${{ join(matrix.targets, '_') }} + - uses: coroboros/ci/.github/actions/rust/install-dist@v0 - uses: coroboros/ci/.github/actions/rust/pin-version@v0 @@ -141,8 +154,8 @@ jobs: if-no-files-found: error publish: - if: ${{ github.ref_type == 'tag' }} - needs: [supply-chain, secrets, dist-plan] # no release ships until cargo-deny and gitleaks pass + if: ${{ github.ref_type == 'tag' && !cancelled() && needs.supply-chain.result == 'success' && needs.secret-scan.result == 'success' && needs.dist-plan.result == 'success' && needs.dist-build.result != 'failure' }} + needs: [supply-chain, secret-scan, dist-plan, dist-build] # gates must pass; a failed binary build blocks publish (skipped dist-build = library crate, still publishes) runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main @@ -268,7 +281,6 @@ jobs: id-token: write # npm OIDC provenance env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - NPM_PACKAGE_REGISTRY_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }} steps: - name: Download global artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 @@ -302,7 +314,16 @@ jobs: git -C homebrew-tap add "Formula/$(basename "${f}")" done git -C homebrew-tap commit -m "release ${GITHUB_REF_NAME}" - git -C homebrew-tap push + # Concurrent releases of different repos push to the shared tap; rebase-retry on contention. + for attempt in 1 2 3 4 5; do + git -C homebrew-tap push && break + if [ "${attempt}" -eq 5 ]; then + echo "::error::Homebrew tap push failed after ${attempt} attempts" + exit 1 + fi + echo "::warning::tap push rejected (attempt ${attempt}) — rebasing and retrying" + git -C homebrew-tap pull --rebase + done - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -322,11 +343,8 @@ jobs: exit 0 fi for pkg in "${shims[@]}"; do - if [ -n "${NPM_PACKAGE_REGISTRY_TOKEN}" ]; then - npm publish --access public "${pkg}" - else - npm publish --provenance --access public "${pkg}" - fi + # Provenance attests via the job's id-token on both auth paths — token bootstrap or OIDC. + npm publish --provenance --access public "${pkg}" done security: diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index f4ffed8..c1efc1f 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -136,3 +136,63 @@ jobs: set -euo pipefail dist --version || { echo "::error::dist not on PATH after install-dist"; exit 1; } echo "::notice::install-dist OK — $(dist --version)" + + native-deps-target: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Plant a fixture ci/setup.sh that records CARGO_DIST_TARGET + shell: bash + run: | + set -euo pipefail + mkdir -p ci + cat > ci/setup.sh <<'SH' + #!/usr/bin/env bash + echo "${CARGO_DIST_TARGET-}" > seen-target.txt + SH + - name: Host preflight — CARGO_DIST_TARGET unset + uses: ./.github/actions/rust/native-deps + - name: Assert the host hook ran and saw an empty target + shell: bash + run: | + set -euo pipefail + [ -f seen-target.txt ] || { echo "::error::ci/setup.sh did not run on host preflight"; exit 1; } + [ -z "$(cat seen-target.txt)" ] || { echo "::error::CARGO_DIST_TARGET must be empty on host preflight"; exit 1; } + - name: Export the target the way dist-build does + shell: bash + run: echo "CARGO_DIST_TARGET=aarch64-unknown-linux-gnu" >> "${GITHUB_ENV}" + - name: Cross leg — CARGO_DIST_TARGET exported + uses: ./.github/actions/rust/native-deps + - name: Assert the hook saw the exported target + shell: bash + run: | + set -euo pipefail + got="$(cat seen-target.txt)" + [ "${got}" = "aarch64-unknown-linux-gnu" ] || { echo "::error::ci/setup.sh saw '${got}', expected the exported target"; exit 1; } + echo "::notice::native-deps passes CARGO_DIST_TARGET through to ci/setup.sh" + + test-deps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Absent hooks → no-op + uses: ./.github/actions/rust/test-deps + - name: Plant ci/test.env and ci/test-setup.sh + shell: bash + run: | + set -euo pipefail + mkdir -p ci + printf 'FOO=1\n' > ci/test.env + cat > ci/test-setup.sh <<'SH' + #!/usr/bin/env bash + touch test-setup-ran + SH + - name: Run the test hooks + uses: ./.github/actions/rust/test-deps + - name: Assert fixtures ran and test.env propagated + shell: bash + run: | + set -euo pipefail + [ -f test-setup-ran ] || { echo "::error::ci/test-setup.sh did not run"; exit 1; } + [ "${FOO:-}" = "1" ] || { echo "::error::ci/test.env did not propagate FOO to the job env"; exit 1; } + echo "::notice::test-deps runs test-setup.sh and propagates test.env" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4241cf6..bc4f8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## v0.2.0 - 02/06/2026 ### Features +- `rust-packages` — host native/C++ CLIs without the shared pipeline learning zig, clang, or musl: the `dist-build` matrix exports `CARGO_DIST_TARGET` (the resolved target triple) to `rust/native-deps`'s `ci/setup.sh`, so a consumer provisions the cross-toolchain per target. Host preflight sees an empty target and runs unchanged; the shared pipeline installs no cross-toolchain itself. +- `rust/test-deps` — composite loading the optional `ci/test.env` (`KEY=value` lines → job environment) and running the optional `ci/test-setup.sh` fixture hook before `cargo test`, so model/ffmpeg-gated tests fail loud instead of silently skipping. `rust/base` runs it; a pure-Rust consumer without the files is unaffected. - `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. `publish` re-runs `rust/base` (fmt / clippy / test) on the tagged commit before `cargo publish`, mirroring the npm pipeline, so a library crate is re-validated at tag time and not only at PR time; `cargo publish`'s own verify build then runs with no `--no-verify`, so a crate that only builds in-workspace fails before an immutable release. - `rust-packages` — add a branch-time `package` job (`cargo package --locked`, after `rust/native-deps`) that verify-builds the crate from its packaged tarball. A compile-time asset — an `include_str!`/`include_bytes!` file or a `build.rs` input — dropped from the package by an `exclude`/`.gitignore` rule now fails the PR rather than the tagged `cargo publish`, which verify-builds the same tarball at release time. - `rust-packages` — opt-in binary-distribution layer via cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]`. A tagged binary crate gets prebuilt per-target archives, `shell` + `powershell` installers, a Homebrew formula in the declared `tap`, and an npm shim — attached to the single GitHub Release, alongside the crates.io publish. The pipeline stays the sole release authority: `dist` only builds (final URLs derive from repo + tag), and the release goes live through draft → undraft. Library crates self-skip. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish` (gated by `cargo-deny` + `gitleaks`) and a `draft` input on `release/github-release`; optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets activate the tap and npm publishes. @@ -12,12 +14,16 @@ - `secrets` gate — gitleaks gates `publish` via `needs:` in the npm and Rust package workflows, alongside the supply-chain gate. A leaked secret blocks the release through the template's job graph, not the consumer's branch protection — parity with the GitLab `security-gate` stage. ### Fixes +- `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a workflow-level `concurrency` group so same-ref releases serialize, and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. +- `rust/base` — install the `rust-toolchain.toml` channel explicitly (`rustup toolchain install` with `rustfmt` + `clippy`) instead of relying on lazy auto-resolution on the first `cargo` invocation. +- `rust-packages` — pass `--provenance` on both npm-shim publish paths (token bootstrap and OIDC), attesting the shim via the job's `id-token` regardless of the auth path. - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. - `release` — drop the "move rolling major tag" step from the npm and Rust publish jobs. Reusable workflows run in the caller's context, so it force-pushed a meaningless `vN` ref into every consumer repo; the `v0` ref now moves on `coroboros/ci`'s own release (see `self-release`). - `rust-packages` — pin the `dist-plan`, `dist-build`, `dist-host` checkouts to the tag commit (`ref: ${{ github.sha }}`) rather than the moving `main`, dropping the unused `fetch-depth: 0` and `dist-plan`'s now-redundant `verify-tag`. The per-target binaries build the exact source the tag points to regardless of the `publish` commit-back timing; `verify-tag` stays on the `publish` jobs, the only ones that check out `main` to push back. - `rust-packages` — guard `cargo publish` with a `git status --porcelain` allowlist (`Cargo.toml`/`Cargo.lock`/`CHANGELOG.md`); an unexpected dirty file now fails the release before the immutable crates.io publish instead of shipping silently under `--allow-dirty`. ### Refactor +- `rust-packages` — rename the `secrets` gate job to `secret-scan`, disambiguating it from the workflow's `secrets:` block; the `publish` and `dist-build` `needs:` follow. - `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. The `cargo-deny` composite imposes a canonical `security/deny.toml` via `--config` — a consumer `deny.toml` is ignored and a `deny.exceptions.toml` is rejected. - `release/commit-artifacts` — extract the commit-back step shared by the npm and Rust publish jobs into a composite (`files` input, `[skip ci]`), mirroring GitLab's `commit-release-artifacts`. - `release/verify-tag`, `rust/pin-version` — extract two blocks that were duplicated across the tag-time jobs into composites (single source). `release/verify-tag` is the "main HEAD matches the tag SHA" guard shared by the npm and Rust `publish` jobs; `rust/pin-version` is the `cargo-set-version` install + `cargo set-version` stamp shared by `publish` and the three `dist-*` jobs. @@ -26,10 +32,15 @@ - `rust/install-dist` — new composite installing cargo-dist's `dist` from the prebuilt, SHA-256-verified release tarball (per-OS: Linux/macOS/Windows), shared by `dist-plan`/`dist-build`/`dist-host`. Replaces a multi-minute `cargo install --locked` from-source compile in each and satisfies the binary-pinning rule (version + checksum, like `security/gitleaks`); the version pin lives in the composite. - `rust/pin-version` — install only the `cargo-set-version` binary (`cargo install --bin`), the one subcommand used, instead of all of cargo-edit — ~75% less from-source build. cargo-edit ships no prebuilt binary, so the source install stays. +### Performance +- `rust/base`, `dist-build` — replace the hand-rolled `~/.cargo` cache with `Swatinem/rust-cache` (`v2.9.1`), caching `~/.cargo` + `target/` soundly: it keeps `-sys` / CMake `out/` dirs and `registry/src`, prunes the rest, and forces `CARGO_INCREMENTAL=0` (so a C++/CMake build is not recompiled every run, and the installed `cargo-set-version` is cached). `dist-build` keys the cache per target triple to avoid cross-leg poisoning; `rust/base` writes only from the default branch. + ### Tests +- `self-actions` — smoke `rust/native-deps` target passthrough (empty on host preflight, the exported triple on a cross leg) and `rust/test-deps` (absent → no-op; present → `ci/test-setup.sh` runs and `ci/test.env` propagates to the job environment). - `self-actions` — new self-CI workflow exercising the composites on every PR via local refs: `release/verify-tag` (HEAD match + moved-HEAD failure), `release/generate-changelog` (SemVer-gate rejection of a non-tag ref), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote, `contents: read` backstop), `security/cargo-deny` (consumer-override reject guard), and `rust/install-dist` (download + checksum + run). A regression in the reachable logic now fails the PR. Tag-driven paths that read the runner's `GITHUB_REF_NAME`/`GITHUB_SHA` (generate-changelog auto-gen, pin-version stamp) can't be faked on a non-tag event — those reserved defaults aren't overridable into a composite — so they stay validated at real release time. ### Documentation +- `README`, `CLAUDE.md` — document the native-CLI consumer contract (`ci/setup.sh` target-aware via `CARGO_DIST_TARGET`, `ci/test.env`, `ci/test-setup.sh`; each optional and a no-op when absent), the `deny.toml` escape-hatch for an unfixable transitive advisory (a justified central `ignore`, never per repo), and that the shared dist binaries are CPU-only so a consumer may keep a supplemental per-package workflow. - `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab; document the opt-in binary-distribution layer (jobs, consumer contract, optional tap/npm secrets) and the imposed `cargo-deny` ruleset. - `README` — correct the `release/verify-tag` row (shared by the npm and Rust `publish` jobs, not "every job that acts on `main`"; the `dist-*` jobs pin to the tag commit) and the cargo-dist install note (`rust/install-dist`, prebuilt + SHA-256 verified); add the `rust/install-dist` composable row. diff --git a/CLAUDE.md b/CLAUDE.md index 30eeac5..34def16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,12 +11,12 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). - `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `package` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). -- `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` gates reuse them). +- `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` + `secret-scan` gates reuse them). The `secret-scan` gate re-runs gitleaks so `publish` can `needs:` it (a job nested in `security.yml` isn't addressable); the duplicate run in `security.yml` is accepted for standalone consumers. - `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the composites against the real checkout on every PR. -- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. +- `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,test-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. - `.github/dependabot.yml` — auto-PRs for pinned action SHAs. `renovate.json` + `.github/workflows/renovate.yml` — self-hosted Renovate (needs the `RENOVATE_TOKEN` PAT secret, scope `repo` + `workflow`) auto-bumps the version-pinned tooling; `.github/renovate/sync-tool-sha.sh` re-syncs each paired tarball SHA-256 in the same PR. - `security/.gitleaks.toml` — canonical gitleaks ruleset. -- `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). +- `security/deny.toml` — canonical cargo-deny ruleset, imposed via `--config` (consumer `deny.toml` ignored; `deny.exceptions.toml` rejected). An unfixable transitive advisory → PR a justified `ignore = ["RUSTSEC-…"]` (with `# why`) to this file, never a per-repo override. - `README.md` — public documentation (single source for pipelines, composables, structure, flow, env, security, examples). ## Rules diff --git a/README.md b/README.md index 4c5c275..9a69041 100644 --- a/README.md +++ b/README.md @@ -116,11 +116,13 @@ Calls `security.yml` — see [Security](#security). Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: -- `rust-toolchain.toml` — pins the channel and lists the `clippy` + `rustfmt` components (`rustup` installs them on first `cargo` use; omit them and `fmt`/`clippy` fail on a pinned channel). +- `rust-toolchain.toml` — pins the channel. `rust/base` installs that channel explicitly with the `rustfmt` + `clippy` components (no reliance on lazy auto-resolution on first `cargo` use). - `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. - compile-time assets — any `include_str!` / `include_bytes!` / `build.rs` input must sit under the package root and stay unignored (no `exclude`/`.gitignore` rule drops it). The `package` job verify-builds the packaged crate so a dropped asset fails the PR, not the tagged publish. - cargo-deny policy — imposed by `coroboros/ci`; no consumer `deny.toml` required, and a local one is ignored. See [Security](#security). -- `ci/setup.sh` — optional. Installs native build dependencies (a `-sys` crate's toolchain, test fixtures) and exports env via `$GITHUB_ENV`. A no-op when absent. +- `ci/setup.sh` — optional native build-dependency hook. Receives `RUNNER_OS`, `RUNNER_ARCH`, and `CARGO_DIST_TARGET` (space-separated target triples on a `dist-build` cross leg; empty on host preflight). Installs `-sys` / CMake toolchains and exports env via `$GITHUB_ENV`. No-op when absent. +- `ci/test.env` — optional. `KEY=value` lines loaded into the job environment before `cargo test`, so model/fixture-gated tests fail loud instead of skipping. No-op when absent. +- `ci/test-setup.sh` — optional. Runs before `cargo test` to stage test fixtures (prefetch a model, install a runtime tool). No-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. - binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. Absent → source-only (crates.io), unchanged. @@ -150,7 +152,7 @@ Consumer requirements:
-secrets +secret-scan
@@ -176,7 +178,7 @@ Consumer requirements:
-**Trigger**: `tag push`. Gated by `supply-chain` and `secrets` (`needs:`) — cargo-deny and gitleaks must pass first. +**Trigger**: `tag push`. Gated by `supply-chain` and `secret-scan` (`needs:`) — cargo-deny and gitleaks must pass first — and skipped if `dist-build` fails, so a broken binary build never produces a crates.io publish. **Sequence**: 1. Checkout `main` with full history @@ -201,11 +203,11 @@ Consumer requirements: The shared pipeline is the sole release authority — `publish` creates the one GitHub Release (a draft for binary repos), and these jobs attach artifacts to it. cargo-dist (`dist`) only builds, never owns the release. - **`dist-plan`** — detects the metadata, pins the version, runs `dist plan` to compute the per-target build matrix. -- **`dist-build`** — matrix over the declared `targets`, gated by `supply-chain` and `secrets` (`needs:`); builds each prebuilt archive (`dist build --artifacts=local`). +- **`dist-build`** — matrix over the declared `targets`, gated by `supply-chain` and `secret-scan` (`needs:`); caches `~/.cargo` + `target/` per target via `rust-cache`; builds each prebuilt archive (`dist build --artifacts=local`). Exports `CARGO_DIST_TARGET` so the consumer's `ci/setup.sh` provisions the cross-toolchain. - **`dist-host`** — builds the global installers + Homebrew formula + npm shim (`dist build --artifacts=global`; final download URLs derive from repo + tag), uploads every asset to the release, then undrafts it. -- **`dist-publish`** — commits the formula to the declared `tap` (`HOMEBREW_TAP_TOKEN`) and publishes the npm shim (OIDC + provenance, or `NPM_PACKAGE_REGISTRY_TOKEN` bootstrap). Each self-skips when its installer or secret is absent. +- **`dist-publish`** — commits the formula to the declared `tap` (`HOMEBREW_TAP_TOKEN`, rebase-retried so concurrent releases don't clobber the shared tap) and publishes the npm shim with provenance (token bootstrap or OIDC, attested via the job's `id-token` either way). Each self-skips when its installer or secret is absent. -`dist` is installed prebuilt and SHA-256 verified (version `0.32.0`) via [`rust/install-dist`](#composable-actions). Per-target Cargo features are not expressible in cargo-dist 0.32.0. Set them consumer-side via `cfg`, e.g. a Metal build gated on `cfg(target_os = "macos")`. macOS Developer-ID signing + notarization are deferred. +`dist` is installed prebuilt and SHA-256 verified (version `0.32.0`) via [`rust/install-dist`](#composable-actions). Per-target Cargo features are not expressible in cargo-dist 0.32.0. Set them consumer-side via `cfg`, e.g. a Metal build gated on `cfg(target_os = "macos")`. The shared dist binaries are CPU-only; a consumer MAY keep a supplemental per-package workflow for GPU/accelerated builds (e.g. a Metal smoke). macOS Developer-ID signing + notarization are deferred.
@@ -230,7 +232,7 @@ Reusable sub-workflow with three parallel scans: --- -**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs:` the `supply-chain` and `secrets` gates so the release is re-checked (cargo-deny or osv-scanner, plus gitleaks) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. +**Notes** — pin via `@v0` (rolling major) or `@x.y.z`. `self-release.yml` moves `v0` to each stable release, so `@v0` always tracks the latest. `@x.y.z` pins the workflow file. The composite actions it calls are hardcoded `@v0`, so `@x.y.z` is not a full freeze — the nested actions still follow `v0`. Jobs run in parallel except each `publish`, which `needs:` the `supply-chain` and secret-scanning gates so the release is re-checked (cargo-deny or osv-scanner, plus gitleaks) before it ships. `security.yml` scans in parallel for reporting — it does not gate `publish` (see [Security](#security)). The only sub-workflow call is `security` → `security.yml`. --- @@ -240,11 +242,12 @@ Reusable sub-workflow with three parallel scans: | :--- | :--- | :--- | | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | -| `rust/base` | Rust | Resolves the toolchain from `rust-toolchain.toml`, caches the `~/.cargo` deps, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | -| `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook. Shared by `rust/base` and the `dist-build` matrix. No-op when absent. | +| `rust/base` | Rust | Installs the `rust-toolchain.toml` channel (`rustfmt` + `clippy`), caches `~/.cargo` + `target/` via `rust-cache`, runs [`rust/native-deps`](#composable-actions) then [`rust/test-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | +| `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook (sees `CARGO_DIST_TARGET` on a `dist-build` cross leg). Shared by `rust/base` and the `dist-build` matrix. No-op when absent. | +| `rust/test-deps` | Rust | Loads the optional `ci/test.env` into the job env and runs the optional `ci/test-setup.sh` fixture hook before `cargo test`. Used by `rust/base`. No-op when absent. | | `rust/install-dist` | Rust | Installs cargo-dist's `dist` binary, prebuilt and SHA-256 verified (Linux/macOS/Windows). Shared by the `dist-plan`, `dist-build`, `dist-host` jobs. | | `rust/pin-version` | Rust | Installs version-pinned `cargo-set-version` (cargo-edit) and stamps `Cargo.toml` to the release tag. Shared by `publish` and the `dist-*` jobs. | -| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package `secrets` gate, and self-CI. | +| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. The shared definition behind `security.yml`, the package secret-scanning gate, and self-CI. | | `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Shared by `security.yml`, the npm `supply-chain` gate, and self-CI. | | `security/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The Rust `supply-chain` gate. | | `release/verify-tag` | transverse | Fails the release unless the checked-out `main` HEAD matches the tag SHA. Shared by the npm and Rust `publish` jobs — the tag-time jobs that check out `main` to push back; the `dist-*` jobs pin to the tag commit (`github.sha`) instead. | @@ -327,7 +330,7 @@ All optional. A consumer that wires none still gets crates.io plus prebuilt arch | :--- | :---: | :--- | | `CARGO_REGISTRY_TOKEN` | | crates.io token. Bootstraps the first publish of a new crate; absent → OIDC Trusted Publishing. | | `HOMEBREW_TAP_TOKEN` | | Push access to the Homebrew tap repo named by `tap` in `[package.metadata.dist]`. Absent → the formula publish self-skips. | -| `NPM_PACKAGE_REGISTRY_TOKEN` | | npm token bootstrapping the first publish of the binary npm shim; absent → OIDC + provenance. | +| `NPM_PACKAGE_REGISTRY_TOKEN` | | npm token bootstrapping the first publish of the binary npm shim; absent → OIDC Trusted Publisher. The shim publishes with provenance either way. | @@ -423,7 +426,7 @@ The GitLab pipeline hardens npm at the image layer — cooldown, Socket Firewall | License drift | `cargo-deny` licenses — allow-list | | Banned or wildcard dependency | `cargo-deny` bans | -`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. The controls above are **imposed**: it applies the canonical [`security/deny.toml`](security/deny.toml) via `--config`, sparse-checked from `coroboros/ci` — the [`gitleaks`](#composable-actions) model. A consumer `deny.toml` is ignored; a `deny.exceptions.toml` fails the job. Exceptions are changed centrally in `coroboros/ci`, never per repo. +`cargo-deny` runs on every push via a SHA-pinned action and gates `publish` (`needs:`), so a tagged release is re-checked against the latest advisory DB before it ships — `security.yml` scans in parallel for reporting and does not block the release. The controls above are **imposed**: it applies the canonical [`security/deny.toml`](security/deny.toml) via `--config`, sparse-checked from `coroboros/ci` — the [`gitleaks`](#composable-actions) model. A consumer `deny.toml` is ignored; a `deny.exceptions.toml` fails the job. Exceptions are changed centrally in `coroboros/ci`, never per repo — an unfixable transitive advisory is suppressed by a PR adding a justified `ignore = ["RUSTSEC-…"]` (with a `# why` comment) to the canonical `deny.toml`. **Publish auth.** crates.io publish uses OIDC Trusted Publishing by default — `rust-lang/crates-io-auth-action` mints a short-lived token per run, no long-lived secret in the repo. `CARGO_REGISTRY_TOKEN` is needed only to bootstrap the first publish of a new crate (Trusted Publishing binds to an existing crate); configure Trusted Publishing on crates.io afterwards and drop the token. The verify build runs on publish (no `--no-verify`). It compiles the packaged tarball standalone, catching a crate that only builds in-workspace before the immutable release lands. diff --git a/security/deny.toml b/security/deny.toml index b142e65..db5fced 100644 --- a/security/deny.toml +++ b/security/deny.toml @@ -11,8 +11,10 @@ all-features = true [advisories] # v2 schema: vulnerability advisories always error and cannot be downgraded; -# `ignore` is the only suppressor and is kept empty. unmaintained/unsound at -# "all" scope error on any matching crate (an abandoned crate is a takeover vector). +# `ignore` is the only suppressor and is kept empty by default. unmaintained/unsound +# at "all" scope error on any matching crate (an abandoned crate is a takeover vector). +# Escape-hatch for an unfixable transitive advisory: add the ID below with a `# why` +# comment in a PR to coroboros/ci — never a per-repo override (consumer deny.toml is ignored). yanked = "deny" unmaintained = "all" unsound = "all" From e048aed47d52027ed037bb6a26f82f89258dcb6c Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 22:19:35 +0700 Subject: [PATCH 31/36] fix(ci): ref-aware concurrency and consistent secret-scan naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust-packages concurrency keys cancel-in-progress on ref_type: superseded branch CI cancels, an in-flight tag/release never does — was never-cancel, which queued branch runs. - javascript-npm-packages: rename the `secrets` gate job to `secret-scan`, matching rust-packages and disambiguating it from the workflow's `secrets:` block; publish needs: follows. - docs: fix the rust/base step order (native-deps, fmt, clippy, then test-deps and test), reconcile the rust/base toolchain/cache CHANGELOG line with the shipped behavior, document the rustup-on-PATH contract for custom dist-build containers, set the v0.2.0 date. --- .github/workflows/javascript-npm-packages.yml | 4 ++-- .github/workflows/rust-packages.yml | 4 ++-- CHANGELOG.md | 8 ++++---- README.md | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 2d48288..43981ef 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: coroboros/ci/.github/actions/security/osv-scanner@v0 - secrets: + secret-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -50,7 +50,7 @@ jobs: publish: if: ${{ github.ref_type == 'tag' }} - needs: [supply-chain, secrets] # no release ships until osv-scanner and gitleaks pass + needs: [supply-chain, secret-scan] # no release ships until osv-scanner and gitleaks pass runs-on: ubuntu-latest permissions: contents: write # for GitHub Release creation + commit-back to main diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 3a5e229..170f8bf 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -14,10 +14,10 @@ on: permissions: contents: read -# Serialize same-ref runs; queue behind an in-flight release rather than cancel it. +# Same-ref group: cancel superseded branch CI, never cancel an in-flight tag/release. concurrency: group: release-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.ref_type != 'tag' }} jobs: preflight: diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4f8da..dccb811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v0.2.0 - 02/06/2026 +## v0.2.0 - 06/06/2026 ### Features - `rust-packages` — host native/C++ CLIs without the shared pipeline learning zig, clang, or musl: the `dist-build` matrix exports `CARGO_DIST_TARGET` (the resolved target triple) to `rust/native-deps`'s `ci/setup.sh`, so a consumer provisions the cross-toolchain per target. Host preflight sees an empty target and runs unchanged; the shared pipeline installs no cross-toolchain itself. @@ -8,13 +8,13 @@ - `rust-packages` — bundled Cargo pipeline: `preflight` (`cargo fmt --check` / `clippy -D warnings` / `test` on a Linux, macOS, Windows matrix), `supply-chain` (`cargo-deny`), tag-driven `publish` to crates.io, and the shared `security` scan. `supply-chain` runs on every push and gates `publish` (`needs:`), so cargo-deny re-checks the release against the latest advisory DB before it ships rather than scanning in parallel. Publish authenticates with crates.io OIDC Trusted Publishing by default (`rust-lang/crates-io-auth-action`); `CARGO_REGISTRY_TOKEN` is the first-publish bootstrap for a new crate. `publish` re-runs `rust/base` (fmt / clippy / test) on the tagged commit before `cargo publish`, mirroring the npm pipeline, so a library crate is re-validated at tag time and not only at PR time; `cargo publish`'s own verify build then runs with no `--no-verify`, so a crate that only builds in-workspace fails before an immutable release. - `rust-packages` — add a branch-time `package` job (`cargo package --locked`, after `rust/native-deps`) that verify-builds the crate from its packaged tarball. A compile-time asset — an `include_str!`/`include_bytes!` file or a `build.rs` input — dropped from the package by an `exclude`/`.gitignore` rule now fails the PR rather than the tagged `cargo publish`, which verify-builds the same tarball at release time. - `rust-packages` — opt-in binary-distribution layer via cargo-dist (`dist` `0.32.0`), gated on `[package.metadata.dist]`. A tagged binary crate gets prebuilt per-target archives, `shell` + `powershell` installers, a Homebrew formula in the declared `tap`, and an npm shim — attached to the single GitHub Release, alongside the crates.io publish. The pipeline stays the sole release authority: `dist` only builds (final URLs derive from repo + tag), and the release goes live through draft → undraft. Library crates self-skip. Adds `dist-plan`, `dist-build`, `dist-host`, `dist-publish` (gated by `cargo-deny` + `gitleaks`) and a `draft` input on `release/github-release`; optional `HOMEBREW_TAP_TOKEN` and `NPM_PACKAGE_REGISTRY_TOKEN` secrets activate the tap and npm publishes. -- `rust/base`, `rust/native-deps` — composites. `rust/base` resolves the toolchain from `rust-toolchain.toml`, caches `~/.cargo`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the `dist-build` matrix. +- `rust/base`, `rust/native-deps` — composites. `rust/base` installs the `rust-toolchain.toml` channel, caches `~/.cargo` + `target/`, runs the optional `ci/setup.sh` native-dependency hook, then lints and tests; `rust/native-deps` is that hook, shared with the `dist-build` matrix. - `javascript/base` — wrap `pnpm install` in Socket Firewall (`sfw`), blocking confirmed-malicious packages before download. Fail-closed. The GitHub-runner equivalent of the image-baked firewall on GitLab. - `self-release` — move the rolling `v0` major tag to each stable `coroboros/ci` release, so `@v0` consumers track the latest release without a manual tag push. - `secrets` gate — gitleaks gates `publish` via `needs:` in the npm and Rust package workflows, alongside the supply-chain gate. A leaked secret blocks the release through the template's job graph, not the consumer's branch protection — parity with the GitLab `security-gate` stage. ### Fixes -- `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a workflow-level `concurrency` group so same-ref releases serialize, and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. +- `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a ref-keyed `concurrency` group — superseded branch CI cancels, an in-flight tag/release never does — and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. - `rust/base` — install the `rust-toolchain.toml` channel explicitly (`rustup toolchain install` with `rustfmt` + `clippy`) instead of relying on lazy auto-resolution on the first `cargo` invocation. - `rust-packages` — pass `--provenance` on both npm-shim publish paths (token bootstrap and OIDC), attesting the shim via the job's `id-token` regardless of the auth path. - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. @@ -23,7 +23,7 @@ - `rust-packages` — guard `cargo publish` with a `git status --porcelain` allowlist (`Cargo.toml`/`Cargo.lock`/`CHANGELOG.md`); an unexpected dirty file now fails the release before the immutable crates.io publish instead of shipping silently under `--allow-dirty`. ### Refactor -- `rust-packages` — rename the `secrets` gate job to `secret-scan`, disambiguating it from the workflow's `secrets:` block; the `publish` and `dist-build` `needs:` follow. +- `rust-packages`, `javascript-npm-packages` — rename the `secrets` gate job to `secret-scan`, disambiguating it from the workflow's `secrets:` block; the dependent `publish` (and `rust-packages` `dist-build`) `needs:` follow. - `security` — extract the gitleaks, osv-scanner, and cargo-deny scanners into `security/*` composites (single source). `security.yml` references them and the package `supply-chain` gates reuse them, so osv-scanner is defined once. The osv composite scans only when a supported manifest is present (`pnpm-lock.yaml`, `Cargo.lock`, `go.mod`, and the rest), so a dependency-less repo wiring in `security.yml` skips the scan instead of failing on osv's no-manifest error. `self-security.yml` runs the composites via local refs to self-test them pre-release. The `cargo-deny` composite imposes a canonical `security/deny.toml` via `--config` — a consumer `deny.toml` is ignored and a `deny.exceptions.toml` is rejected. - `release/commit-artifacts` — extract the commit-back step shared by the npm and Rust publish jobs into a composite (`files` input, `[skip ci]`), mirroring GitLab's `commit-release-artifacts`. - `release/verify-tag`, `rust/pin-version` — extract two blocks that were duplicated across the tag-time jobs into composites (single source). `release/verify-tag` is the "main HEAD matches the tag SHA" guard shared by the npm and Rust `publish` jobs; `rust/pin-version` is the `cargo-set-version` install + `cargo set-version` stamp shared by `publish` and the three `dist-*` jobs. diff --git a/README.md b/README.md index 9a69041..895d188 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Consumer requirements:
-secrets +secret-scan
@@ -85,7 +85,7 @@ Consumer requirements:
-**Trigger**: `tag push`. Gated by `supply-chain` and `secrets` (`needs:`) — osv-scanner and gitleaks must pass first. +**Trigger**: `tag push`. Gated by `supply-chain` and `secret-scan` (`needs:`) — osv-scanner and gitleaks must pass first. **Sequence**: 1. Checkout `main` with full history @@ -116,7 +116,7 @@ Calls `security.yml` — see [Security](#security). Bundled Cargo CI. Tag-driven release, same as the npm pipeline. Consumer requirements: -- `rust-toolchain.toml` — pins the channel. `rust/base` installs that channel explicitly with the `rustfmt` + `clippy` components (no reliance on lazy auto-resolution on first `cargo` use). +- `rust-toolchain.toml` — pins the channel. `rust/base` installs that channel explicitly with the `rustfmt` + `clippy` components (no reliance on lazy auto-resolution on first `cargo` use). The pipeline assumes `rustup` is on `PATH` (every GitHub-hosted runner ships it); a custom container image pinned for a `dist-build` target must provide it. - `Cargo.toml` and a committed `Cargo.lock` — `clippy` and `test` run `--locked`. - compile-time assets — any `include_str!` / `include_bytes!` / `build.rs` input must sit under the package root and stay unignored (no `exclude`/`.gitignore` rule drops it). The `package` job verify-builds the packaged crate so a dropped asset fails the PR, not the tagged publish. - cargo-deny policy — imposed by `coroboros/ci`; no consumer `deny.toml` required, and a local one is ignored. See [Security](#security). @@ -242,7 +242,7 @@ Reusable sub-workflow with three parallel scans: | :--- | :--- | :--- | | `check-docs` | transverse | Context dump + documentation check. | | `javascript/base` | JavaScript | Sets up Node + corepack pnpm, caches the store, writes `.npmrc` from env, then installs, lints, builds (when present), tests. | -| `rust/base` | Rust | Installs the `rust-toolchain.toml` channel (`rustfmt` + `clippy`), caches `~/.cargo` + `target/` via `rust-cache`, runs [`rust/native-deps`](#composable-actions) then [`rust/test-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, `test`. | +| `rust/base` | Rust | Installs the `rust-toolchain.toml` channel (`rustfmt` + `clippy`), caches `~/.cargo` + `target/` via `rust-cache`, runs [`rust/native-deps`](#composable-actions), then `cargo fmt --check`, `clippy -D warnings`, then [`rust/test-deps`](#composable-actions) and `test`. | | `rust/native-deps` | Rust | Runs the optional `ci/setup.sh` native build-dependency hook (sees `CARGO_DIST_TARGET` on a `dist-build` cross leg). Shared by `rust/base` and the `dist-build` matrix. No-op when absent. | | `rust/test-deps` | Rust | Loads the optional `ci/test.env` into the job env and runs the optional `ci/test-setup.sh` fixture hook before `cargo test`. Used by `rust/base`. No-op when absent. | | `rust/install-dist` | Rust | Installs cargo-dist's `dist` binary, prebuilt and SHA-256 verified (Linux/macOS/Windows). Shared by the `dist-plan`, `dist-build`, `dist-host` jobs. | From 1b965412c319df3e3d98311a53ea3e01f38f147f Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 22:36:51 +0700 Subject: [PATCH 32/36] fix(ci): serialize per-repo releases; smoke install-dist on 3 OSes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust-packages concurrency: key the group per repo on tags (was per ref), so two release tags of one repo serialize instead of racing release/commit-artifacts' push to main — a rebase-retry there would conflict on the Cargo.toml version line. Branch CI unchanged (per-ref, cancels superseded runs). - self-actions: run the install-dist smoke on ubuntu + macos + windows (was ubuntu-only). cargo-dist ships the Windows zip flat (dist.exe at root) and the tarballs nested, so the per-OS extraction in rust/install-dist now has coverage on every path it claims to support; the windows/macos asset SHA-256 pins were checked against the real releases. --- .github/workflows/rust-packages.yml | 5 +++-- .github/workflows/self-actions.yml | 8 +++++++- CHANGELOG.md | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index 170f8bf..b8ecccd 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -14,9 +14,10 @@ on: permissions: contents: read -# Same-ref group: cancel superseded branch CI, never cancel an in-flight tag/release. +# Tags serialize per repo so one release's commit-back can't race another's; branches key +# per ref and cancel superseded runs, never an in-flight release. concurrency: - group: release-${{ github.ref }} + group: release-${{ github.ref_type == 'tag' && github.repository || github.ref }} cancel-in-progress: ${{ github.ref_type != 'tag' }} jobs: diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index c1efc1f..7a3659e 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -126,7 +126,13 @@ jobs: echo "::notice::cargo-deny rejects consumer deny.exceptions.toml" install-dist: - runs-on: ubuntu-latest + # cargo-dist packages the Windows zip flat (dist.exe at root) but the Linux/macOS + # tarballs nested — extraction differs per OS, so smoke all three, not just Linux. + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: ./.github/actions/rust/install-dist diff --git a/CHANGELOG.md b/CHANGELOG.md index dccb811..febeec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - `secrets` gate — gitleaks gates `publish` via `needs:` in the npm and Rust package workflows, alongside the supply-chain gate. A leaked secret blocks the release through the template's job graph, not the consumer's branch protection — parity with the GitLab `security-gate` stage. ### Fixes -- `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a ref-keyed `concurrency` group — superseded branch CI cancels, an in-flight tag/release never does — and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. +- `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a `concurrency` group that serializes a repo's releases — keyed per-repo on tags so one release's commit-back can't race another's, per-ref on branches where superseded CI cancels — and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. - `rust/base` — install the `rust-toolchain.toml` channel explicitly (`rustup toolchain install` with `rustfmt` + `clippy`) instead of relying on lazy auto-resolution on the first `cargo` invocation. - `rust-packages` — pass `--provenance` on both npm-shim publish paths (token bootstrap and OIDC), attesting the shim via the job's `id-token` regardless of the auth path. - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. @@ -37,7 +37,7 @@ ### Tests - `self-actions` — smoke `rust/native-deps` target passthrough (empty on host preflight, the exported triple on a cross leg) and `rust/test-deps` (absent → no-op; present → `ci/test-setup.sh` runs and `ci/test.env` propagates to the job environment). -- `self-actions` — new self-CI workflow exercising the composites on every PR via local refs: `release/verify-tag` (HEAD match + moved-HEAD failure), `release/generate-changelog` (SemVer-gate rejection of a non-tag ref), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote, `contents: read` backstop), `security/cargo-deny` (consumer-override reject guard), and `rust/install-dist` (download + checksum + run). A regression in the reachable logic now fails the PR. Tag-driven paths that read the runner's `GITHUB_REF_NAME`/`GITHUB_SHA` (generate-changelog auto-gen, pin-version stamp) can't be faked on a non-tag event — those reserved defaults aren't overridable into a composite — so they stay validated at real release time. +- `self-actions` — new self-CI workflow exercising the composites on every PR via local refs: `release/verify-tag` (HEAD match + moved-HEAD failure), `release/generate-changelog` (SemVer-gate rejection of a non-tag ref), `release/commit-artifacts` (push / no-op / `FILES` word-split against a local bare remote, `contents: read` backstop), `security/cargo-deny` (consumer-override reject guard), and `rust/install-dist` (download + checksum + run on Linux, macOS, Windows — the zip and tarball extraction paths differ). A regression in the reachable logic now fails the PR. Tag-driven paths that read the runner's `GITHUB_REF_NAME`/`GITHUB_SHA` (generate-changelog auto-gen, pin-version stamp) can't be faked on a non-tag event — those reserved defaults aren't overridable into a composite — so they stay validated at real release time. ### Documentation - `README`, `CLAUDE.md` — document the native-CLI consumer contract (`ci/setup.sh` target-aware via `CARGO_DIST_TARGET`, `ci/test.env`, `ci/test-setup.sh`; each optional and a no-op when absent), the `deny.toml` escape-hatch for an unfixable transitive advisory (a justified central `ignore`, never per repo), and that the shared dist binaries are CPU-only so a consumer may keep a supplemental per-package workflow. From 7ee250d0c7943cd027e77a76b86f5c53524bedda Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 22:44:05 +0700 Subject: [PATCH 33/36] fix(ci): serialize npm-pipeline releases per repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit javascript-npm-packages had no `concurrency`, so two release tags of one repo could run their publish jobs concurrently and race release/commit-artifacts' push to main — one fails non-fast-forward, leaving npm published but the version bump uncommitted. Add the same ref-keyed group as rust-packages: tags serialize per repo, branches key per ref and cancel superseded CI. The two top-level blocks are duplicated because Actions can't share a concurrency key across reusable workflows. --- .github/workflows/javascript-npm-packages.yml | 6 ++++++ CHANGELOG.md | 1 + 2 files changed, 7 insertions(+) diff --git a/.github/workflows/javascript-npm-packages.yml b/.github/workflows/javascript-npm-packages.yml index 43981ef..f1caff5 100644 --- a/.github/workflows/javascript-npm-packages.yml +++ b/.github/workflows/javascript-npm-packages.yml @@ -18,6 +18,12 @@ on: permissions: contents: read +# Tags serialize per repo so one release's commit-back can't race another's; branches key +# per ref and cancel superseded runs, never an in-flight release. +concurrency: + group: release-${{ github.ref_type == 'tag' && github.repository || github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + env: NPM_CONFIG_FILE: ${{ secrets.NPM_CONFIG_FILE }} NPM_EXTRA_CONFIG: ${{ secrets.NPM_EXTRA_CONFIG }} diff --git a/CHANGELOG.md b/CHANGELOG.md index febeec2..543de09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes - `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a `concurrency` group that serializes a repo's releases — keyed per-repo on tags so one release's commit-back can't race another's, per-ref on branches where superseded CI cancels — and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. +- `javascript-npm-packages` — add the ref-keyed `concurrency` group (tags serialize per repo, branches key per ref and cancel superseded CI), matching `rust-packages`, so two release tags can't race `release/commit-artifacts`' push to `main`. - `rust/base` — install the `rust-toolchain.toml` channel explicitly (`rustup toolchain install` with `rustfmt` + `clippy`) instead of relying on lazy auto-resolution on the first `cargo` invocation. - `rust-packages` — pass `--provenance` on both npm-shim publish paths (token bootstrap and OIDC), attesting the shim via the job's `id-token` regardless of the auth path. - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. From 48a562e54c13b7b58f287e2d96c009025b3bceb9 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 23:32:17 +0700 Subject: [PATCH 34/36] docs: document the cargo-dist 0.32 binary-distribution consumer contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrating the first binary consumer surfaced that cargo-dist 0.32 reads its workspace-global keys (cargo-dist-version, ci, publish-jobs, allow-dirty) only from [workspace.metadata.dist], and needs allow-dirty = ["ci"] so `dist plan` doesn't require its own generated workflow (this pipeline owns it). Without both, a binary consumer's dist-plan job fails — a panic ("couldn't find the release") or an out-of-date-workflow error. State the requirement in the consumer contract. --- CHANGELOG.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543de09..fade8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - `README`, `CLAUDE.md` — document the native-CLI consumer contract (`ci/setup.sh` target-aware via `CARGO_DIST_TARGET`, `ci/test.env`, `ci/test-setup.sh`; each optional and a no-op when absent), the `deny.toml` escape-hatch for an unfixable transitive advisory (a justified central `ignore`, never per repo), and that the shared dist binaries are CPU-only so a consumer may keep a supplemental per-package workflow. - `README`, `SECURITY.md` — document the Rust supply-chain model (`cargo-deny` baseline, residual `build.rs` and no-cooldown gaps), the Socket Firewall and release-age cooldown, and the publish auth paths; collapse cross-references to remove duplication; add the `sfw` proxy-inspection caveat for parity with GitLab; document the opt-in binary-distribution layer (jobs, consumer contract, optional tap/npm secrets) and the imposed `cargo-deny` ruleset. - `README` — correct the `release/verify-tag` row (shared by the npm and Rust `publish` jobs, not "every job that acts on `main`"; the `dist-*` jobs pin to the tag commit) and the cargo-dist install note (`rust/install-dist`, prebuilt + SHA-256 verified); add the `rust/install-dist` composable row. +- `README` — binary-distribution consumer contract: cargo-dist `0.32` takes its workspace-global keys (`cargo-dist-version`/`ci`/`publish-jobs`/`allow-dirty`) from `[workspace.metadata.dist]` only, and needs `allow-dirty = ["ci"]` so `dist plan` doesn't claim its own workflow — without these a binary consumer's `dist plan` fails. ### Configuration - `package.json` — bump to `0.2.0` (was `0.1.13`, lagging the `0.1.14` tag). diff --git a/README.md b/README.md index 895d188..5ef5e74 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Consumer requirements: - `ci/test.env` — optional. `KEY=value` lines loaded into the job environment before `cargo test`, so model/fixture-gated tests fail loud instead of skipping. No-op when absent. - `ci/test-setup.sh` — optional. Runs before `cargo test` to stage test fixtures (prefetch a model, install a runtime tool). No-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. -- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. Absent → source-only (crates.io), unchanged. +- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. cargo-dist `0.32` reads its workspace-global keys (`cargo-dist-version`, `ci`, `publish-jobs`, `allow-dirty`) from `[workspace.metadata.dist]` only — a single-crate repo adds an empty `[workspace]` for them — and must set `allow-dirty = ["ci"]` so `dist plan` doesn't require its own generated workflow (this pipeline owns it). Absent → source-only (crates.io), unchanged.
preflight From 62611f01ef84d7f1012278a394d74971fb776533 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 23:37:02 +0700 Subject: [PATCH 35/36] fix(ci): detect cargo-dist from package or workspace metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dist-plan only grepped [package.metadata.dist], but cargo-dist 0.32 keeps its global keys in [workspace.metadata.dist] — a consumer using that layout would be misread as a library crate and skip the binary jobs. Match either table. The tap extraction already scans the whole file, so it resolves from either. --- .github/workflows/rust-packages.yml | 5 +++-- CHANGELOG.md | 1 + CLAUDE.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust-packages.yml b/.github/workflows/rust-packages.yml index b8ecccd..8d38cf2 100644 --- a/.github/workflows/rust-packages.yml +++ b/.github/workflows/rust-packages.yml @@ -73,7 +73,8 @@ jobs: name: Detect cargo-dist metadata shell: bash run: | - if grep -qE '^\[package\.metadata\.dist\]' Cargo.toml 2>/dev/null; then + # cargo-dist 0.32 keeps its global keys in [workspace.metadata.dist]; detect either table. + if grep -qE '^\[(package|workspace)\.metadata\.dist\]' Cargo.toml 2>/dev/null; then echo "::notice::cargo-dist metadata present — binary distribution enabled" tap="$(grep -E '^[[:space:]]*tap[[:space:]]*=' Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" { @@ -81,7 +82,7 @@ jobs: echo "tap=${tap}" } >> "${GITHUB_OUTPUT}" else - echo "::notice::no [package.metadata.dist] — binary jobs skip" + echo "::notice::no cargo-dist metadata — binary jobs skip" { echo "enabled=false" echo "tap=" diff --git a/CHANGELOG.md b/CHANGELOG.md index fade8fa..25de1d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. - `release` — drop the "move rolling major tag" step from the npm and Rust publish jobs. Reusable workflows run in the caller's context, so it force-pushed a meaningless `vN` ref into every consumer repo; the `v0` ref now moves on `coroboros/ci`'s own release (see `self-release`). - `rust-packages` — pin the `dist-plan`, `dist-build`, `dist-host` checkouts to the tag commit (`ref: ${{ github.sha }}`) rather than the moving `main`, dropping the unused `fetch-depth: 0` and `dist-plan`'s now-redundant `verify-tag`. The per-target binaries build the exact source the tag points to regardless of the `publish` commit-back timing; `verify-tag` stays on the `publish` jobs, the only ones that check out `main` to push back. +- `rust-packages` — `dist-plan` detects binary distribution from `[package.metadata.dist]` or `[workspace.metadata.dist]`, so a cargo-dist 0.32 workspace-layout consumer isn't misread as a library crate. - `rust-packages` — guard `cargo publish` with a `git status --porcelain` allowlist (`Cargo.toml`/`Cargo.lock`/`CHANGELOG.md`); an unexpected dirty file now fails the release before the immutable crates.io publish instead of shipping silently under `--allow-dirty`. ### Refactor diff --git a/CLAUDE.md b/CLAUDE.md index 34def16..b0455b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. ## Important files - `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `supply-chain` / `publish` / `security`). -- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `package` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]`). +- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `supply-chain` / `package` / `publish` / `security`) + opt-in cargo-dist binary layer (`dist-plan` / `dist-build` / `dist-host` / `dist-publish`, gated on `[package.metadata.dist]` or `[workspace.metadata.dist]`). - `.github/workflows/security.yml` — `gitleaks` + `dependency-review` + `osv-scanner` (gitleaks/osv wrap `security/*` composites; the package `supply-chain` + `secret-scan` gates reuse them). The `secret-scan` gate re-runs gitleaks so `publish` can `needs:` it (a job nested in `security.yml` isn't addressable); the duplicate run in `security.yml` is accepted for standalone consumers. - `.github/workflows/{self,self-security,self-release,self-actions}.yml` — self-CI: lint, gitleaks + osv (composites via local `./`), the `v0` rolling-tag move, and `self-actions` smoke-testing the composites against the real checkout on every PR. - `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,test-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,cargo-deny},release/{verify-tag,generate-changelog,github-release,commit-artifacts}}/action.yml` — composites. From 18017d58026e2f3a707b025a89aabc88b87a2278 Mon Sep 17 00:00:00 2001 From: OB Date: Sat, 6 Jun 2026 23:50:35 +0700 Subject: [PATCH 36/36] docs(ci): tighten changelog, README, and a smoke comment Drop a redundant concurrency restatement in the npm changelog entry, fold the duplicated allow-dirty mention in the binary-distribution contract, and reword the install-dist smoke comment to why-only. --- .github/workflows/self-actions.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/self-actions.yml b/.github/workflows/self-actions.yml index 7a3659e..8909727 100644 --- a/.github/workflows/self-actions.yml +++ b/.github/workflows/self-actions.yml @@ -127,7 +127,7 @@ jobs: install-dist: # cargo-dist packages the Windows zip flat (dist.exe at root) but the Linux/macOS - # tarballs nested — extraction differs per OS, so smoke all three, not just Linux. + # tarballs nested — extraction differs per OS, so the smoke covers all three. strategy: fail-fast: false matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 25de1d3..5925f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ ### Fixes - `rust-packages` — gate `publish` on `dist-build` (`needs:` plus an explicit `!cancelled() && … && needs.dist-build.result != 'failure'`), so a failed binary build never produces a crates.io publish while the GitHub release stays a draft; a library crate (`dist-build` skipped) still publishes. Add a `concurrency` group that serializes a repo's releases — keyed per-repo on tags so one release's commit-back can't race another's, per-ref on branches where superseded CI cancels — and a `git pull --rebase` retry around the Homebrew tap push so concurrent releases don't clobber the shared tap. -- `javascript-npm-packages` — add the ref-keyed `concurrency` group (tags serialize per repo, branches key per ref and cancel superseded CI), matching `rust-packages`, so two release tags can't race `release/commit-artifacts`' push to `main`. +- `javascript-npm-packages` — add the same ref-keyed `concurrency` group as `rust-packages`, so two release tags can't race `release/commit-artifacts`' push to `main`. - `rust/base` — install the `rust-toolchain.toml` channel explicitly (`rustup toolchain install` with `rustfmt` + `clippy`) instead of relying on lazy auto-resolution on the first `cargo` invocation. - `rust-packages` — pass `--provenance` on both npm-shim publish paths (token bootstrap and OIDC), attesting the shim via the job's `id-token` regardless of the auth path. - `javascript-npm-packages` — gate `publish` on a new `supply-chain` job (osv-scanner, `needs:`). A known vulnerability now blocks the npm release; previously osv-scanner ran only in the parallel `security` job and could not stop a publish. diff --git a/README.md b/README.md index 5ef5e74..ea06387 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Consumer requirements: - `ci/test.env` — optional. `KEY=value` lines loaded into the job environment before `cargo test`, so model/fixture-gated tests fail loud instead of skipping. No-op when absent. - `ci/test-setup.sh` — optional. Runs before `cargo test` to stage test fixtures (prefetch a model, install a runtime tool). No-op when absent. - crates.io publishing — configure [OIDC Trusted Publishing](#security), or set `CARGO_REGISTRY_TOKEN` to bootstrap the first publish of a new crate. Tagged builds always publish. -- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. cargo-dist `0.32` reads its workspace-global keys (`cargo-dist-version`, `ci`, `publish-jobs`, `allow-dirty`) from `[workspace.metadata.dist]` only — a single-crate repo adds an empty `[workspace]` for them — and must set `allow-dirty = ["ci"]` so `dist plan` doesn't require its own generated workflow (this pipeline owns it). Absent → source-only (crates.io), unchanged. +- binary distribution — optional. Declare `[package.metadata.dist]` in `Cargo.toml` (cargo-dist `0.32.0`) to attach prebuilt binaries and installers to the release; drop `release-plz`. cargo-dist `0.32` reads its workspace-global keys (`cargo-dist-version`, `ci`, `publish-jobs`) from `[workspace.metadata.dist]` only — a single-crate repo adds an empty `[workspace]` — and needs `allow-dirty = ["ci"]` there so `dist plan` doesn't claim its own workflow (this pipeline owns it). Absent → source-only (crates.io), unchanged.
preflight