diff --git a/.github/actions/release/generate-changelog/action.yml b/.github/actions/release/generate-changelog/action.yml index 42df8d7..fba7795 100644 --- a/.github/actions/release/generate-changelog/action.yml +++ b/.github/actions/release/generate-changelog/action.yml @@ -36,6 +36,15 @@ runs: echo "::notice::Using existing CHANGELOG section for ${tag}" body="${existing}" else + # Refuse rather than bury a hand-curated section under an auto-generated one-liner and + # commit that back. The tag-resolves gate spares a tagless shallow checkout a false alarm. + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null 2>&1; then + newest="$(grep -m1 -oE '^## v?[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)*' CHANGELOG.md 2>/dev/null | sed -E 's/^## v?//')" + if [ -n "${newest}" ] && [ "${newest}" != "${tag}" ] && ! git rev-parse -q --verify "refs/tags/${newest}" >/dev/null 2>&1; then + echo "::error::CHANGELOG.md's newest section is '## v${newest}', but you are releasing '${tag}' and '${newest}' was never tagged. A hand-curated section's version was likely not bumped to the release tag. Rename it to '## v${tag}' (or delete it to auto-generate from commits), then re-tag." + exit 1 + fi + fi 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/workflows/security-gate.yml b/.github/workflows/security-gate.yml index 40c4ca1..900dfa5 100644 --- a/.github/workflows/security-gate.yml +++ b/.github/workflows/security-gate.yml @@ -8,7 +8,7 @@ permissions: contents: read jobs: - supply-chain: + scan-supply-chain: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -35,7 +35,7 @@ jobs: - if: ${{ steps.detect.outputs.ecosystem == 'other' }} uses: coroboros/ci/.github/actions/security/osv-scanner@v0 - secret-scan: + scan-secrets: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index a3ab33e..269b511 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -8,7 +8,7 @@ permissions: contents: read jobs: - dependency-review: + review-dependencies: if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest steps: @@ -18,7 +18,7 @@ jobs: fail-on-severity: high comment-summary-in-pr: never - licenses: + check-licenses: runs-on: ubuntu-latest # Advisory — reports a non-allowed license, never blocks the release. continue-on-error: true diff --git a/.github/workflows/self-lint.yml b/.github/workflows/self-lint.yml index 3febeed..fd8746d 100644 --- a/.github/workflows/self-lint.yml +++ b/.github/workflows/self-lint.yml @@ -15,7 +15,7 @@ env: YAMLLINT_VERSION: "1.38.0" jobs: - actionlint: + check-actions: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -38,7 +38,7 @@ jobs: shell: bash run: actionlint -color - yamllint: + check-yaml: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -51,7 +51,7 @@ jobs: shell: bash run: yamllint -f colored . - shellcheck: + check-shell: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/self-release.yml b/.github/workflows/self-release.yml index 7c5d2d5..1f817cc 100644 --- a/.github/workflows/self-release.yml +++ b/.github/workflows/self-release.yml @@ -13,7 +13,7 @@ permissions: contents: read jobs: - rolling-tag: + move-rolling-tag: runs-on: ubuntu-latest permissions: contents: write # force-push the rolling major tag diff --git a/.github/workflows/self-security.yml b/.github/workflows/self-security.yml index cbedfe2..44020ee 100644 --- a/.github/workflows/self-security.yml +++ b/.github/workflows/self-security.yml @@ -11,7 +11,7 @@ permissions: contents: read jobs: - gitleaks: + scan-secrets: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/security/gitleaks - osv-scanner: + scan-deps: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 7903695..0b66149 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -16,7 +16,7 @@ permissions: contents: read jobs: - verify-tag: + test-verify-tag: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -39,7 +39,7 @@ jobs: [ "${{ steps.moved.outcome }}" = "failure" ] || { echo "::error::verify-tag must fail when HEAD != GITHUB_SHA"; exit 1; } echo "::notice::verify-tag passes on match, fails on divergence" - generate-changelog: + test-generate-changelog: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -54,7 +54,7 @@ jobs: [ "${{ 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: + test-commit-artifacts: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -102,7 +102,7 @@ jobs: || { echo "::error::no-op branch pushed unexpectedly"; exit 1; } echo "::notice::commit-artifacts no-op left main untouched" - cargo-deny-guard: + test-cargo-deny: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -122,7 +122,7 @@ jobs: [ "${{ 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: + test-install-dist: # cargo-dist packages the Windows zip flat (dist.exe at root) but the Linux/macOS # tarballs nested — extraction differs per OS, so the smoke covers all three. strategy: @@ -140,7 +140,7 @@ jobs: dist --version || { echo "::error::dist not on PATH after install-dist"; exit 1; } echo "::notice::install-dist OK — $(dist --version)" - native-deps-target: + test-native-deps: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -174,7 +174,7 @@ jobs: [ "${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: + test-test-deps: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -200,7 +200,7 @@ jobs: [ "${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" - javascript-base: + test-javascript-base: runs-on: ubuntu-latest env: NPM_CONFIG_FILE: "registry=https://registry.npmjs.org/" @@ -221,7 +221,7 @@ jobs: done echo "::notice::javascript/base ran install (sfw + frozen lockfile), lint, build, test on the fixture" - rust-base: + test-rust-base: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 267a2e8..1e842ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.2.6 - 09/06/2026 + +### Fixes +- `generate-changelog` — fail loud when the newest `CHANGELOG.md` section's version was never tagged and ≠ the release tag, instead of auto-generating a degraded one-liner over a mis-bumped hand-curated section and committing it back. Gated on the release tag resolving locally, so a shallow checkout can't false-positive. + +### Refactor +- All workflows — align every remaining job id to the `verb-noun` convention: `self-lint` → `check-actions`/`check-yaml`/`check-shell`; `self-security` → `scan-secrets`/`scan-deps`; `self-release` → `move-rolling-tag`; `self-test` → `test-`; `security-gate` → `scan-supply-chain`/`scan-secrets`; `security` → `review-dependencies`/`check-licenses`. Job ids only — consumers `uses:` the workflows, so `@v0` references are unaffected. + ## v0.2.5 - 09/06/2026 ### Refactor diff --git a/CLAUDE.md b/CLAUDE.md index d4b7a2c..ed9c9cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,10 +9,10 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. ## Important files -- `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `security-gate` / `publish` / `security`). -- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `security-gate` / `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-gate.yml` — blocking gate `publish` `needs:`. `supply-chain` (auto-routed: `Cargo.toml` → `security/rust/cargo-deny` advisories+bans+sources, else `security/osv-scanner`) + `secret-scan` (gitleaks). A separate reusable workflow so the caller's `publish` can `needs:` the whole gate as one job, running each scan once. Imposed via the package workflows, importable standalone by a non-package repo. -- `.github/workflows/security.yml` — advisory layer, never blocks: `dependency-review` (PR-only) + `licenses` (Rust, `continue-on-error`, `security/rust/cargo-deny` `checks: licenses`). License/quality policy lives here, off the gate. +- `.github/workflows/javascript-npm-packages.yml` — bundled NPM pipeline (`preflight` / `security-gate` / `publish-package` / `security`). +- `.github/workflows/rust-packages.yml` — bundled Cargo pipeline (`preflight` matrix / `security-gate` / `verify-package` / `publish-package` / `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-gate.yml` — blocking gate `publish-package` `needs:`. `scan-supply-chain` (auto-routed: `Cargo.toml` → `security/rust/cargo-deny` advisories+bans+sources, else `security/osv-scanner`) + `scan-secrets` (gitleaks). A separate reusable workflow so the caller's `publish` can `needs:` the whole gate as one job, running each scan once. Imposed via the package workflows, importable standalone by a non-package repo. +- `.github/workflows/security.yml` — advisory layer, never blocks: `review-dependencies` (PR-only) + `check-licenses` (Rust, `continue-on-error`, `security/rust/cargo-deny` `checks: licenses`). License/quality policy lives here, off the gate. - `.github/workflows/{self-lint,self-test,self-security,self-release}.yml` — self-CI: lint, the security composites + `security-gate`/`security` workflows via local `./`, the `v0` rolling-tag move, and `self-test` smoke-testing every composite (plus `javascript/base`/`rust/base` on `test/fixtures/`) every PR. Workflow self-tests resolve their `@v0` composites against the released `v0`, so a brand-new composite is testable only once a release moves `v0` onto it. - `.github/actions/{check-docs,javascript/base,rust/{base,native-deps,test-deps,install-dist,pin-version},security/{gitleaks,osv-scanner,rust/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. @@ -32,6 +32,7 @@ Reusable GitHub Actions workflows + composite actions for the Coroboros stack. - Env values quoted: `KEY: "value"`. - GH workflow log commands: `::error::`, `::warning::`, `::notice::`. No ANSI codes. - Declare env keys only where consumed. + - Job ids: `verb-noun`, kebab-case (imperative verb + object), mirroring the GitLab CI pipelines — `verify-package`, `publish-package`, `generate-changelog`, `commit-artifacts`, `verify-tag`. Phase call-jobs that `uses:` another workflow may stay single-word (`preflight`, `security-gate`, `security`); the cargo-dist `dist-plan`/`dist-build`/`dist-host`/`dist-publish` jobs mirror its subcommands. Reusable-workflow job ids are consumer-visible — rename deliberately. - **Action and workflow files = implementation only.** Rationale lives in `CLAUDE.md` or `CHANGELOG.md`. ## Adding a workflow or composite diff --git a/README.md b/README.md index 29d0423..94601e9 100644 --- a/README.md +++ b/README.md @@ -223,8 +223,8 @@ Calls the advisory [`security.yml`](#securityyml). Reports, never blocks. The blocking gate, split from the advisory layer so it can be owned as a black box. Two parallel jobs, both fail the release through the caller's `needs:` graph — a dev can't bypass them: -- **`supply-chain`** — auto-routed by ecosystem: a `Cargo.toml` repo runs [`security/rust/cargo-deny`](#composable-actions) (advisories + bans + sources); any other runs [`security/osv-scanner`](#composable-actions). One tool per repo, never both, so a crate isn't vuln-scanned twice. A repo with no supported manifest skips (osv's no-manifest path). -- **`secret-scan`** — [`security/gitleaks`](#composable-actions), full git history, canonical ruleset. +- **`scan-supply-chain`** — auto-routed by ecosystem: a `Cargo.toml` repo runs [`security/rust/cargo-deny`](#composable-actions) (advisories + bans + sources); any other runs [`security/osv-scanner`](#composable-actions). One tool per repo, never both, so a crate isn't vuln-scanned twice. A repo with no supported manifest skips (osv's no-manifest path). +- **`scan-secrets`** — [`security/gitleaks`](#composable-actions), full git history, canonical ruleset. Imposed on every package pipeline (a `security-gate` job `needs:`-ed by `publish-package`) and importable directly by a non-package repo. Holds only what *blocks*: a compromised dependency or a leaked secret. License and quality policy live in `security.yml`. @@ -232,8 +232,8 @@ Imposed on every package pipeline (a `security-gate` job `needs:`-ed by `publish The advisory layer — reports, never blocks (parity with GitLab's `allow_failure: true`): -- **`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`. -- **`licenses`** — Rust-only (`continue-on-error`): [`security/rust/cargo-deny`](#composable-actions) `checks: licenses` against the canonical allow-list. A non-allowed license is surfaced, never blocks the release. Skips a repo with no `Cargo.toml`. +- **`review-dependencies`** — PR-only; needs repo's **Dependency graph** enabled. Fails on high-severity CVE introduced by the dep diff. Uses `actions/dependency-review-action@v4`. +- **`check-licenses`** — Rust-only (`continue-on-error`): [`security/rust/cargo-deny`](#composable-actions) `checks: licenses` against the canonical allow-list. A non-allowed license is surfaced, never blocks the release. Skips a repo with no `Cargo.toml`. --- @@ -248,11 +248,11 @@ The advisory layer — reports, never blocks (parity with GitLab's `allow_failur | `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-package` and the `dist-*` jobs. | -| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. Behind `security-gate.yml`'s `secret-scan` and self-CI. | -| `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Behind `security-gate.yml`'s `supply-chain` (non-Rust) and self-CI. | -| `security/rust/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The `checks` input selects which checks run — `advisories bans sources` for the `security-gate.yml` supply-chain, `licenses` for the `security.yml` advisory layer. | +| `security/gitleaks` | transverse | Installs gitleaks (SHA-256 verified), scans with the canonical ruleset, emits SARIF. Behind `security-gate.yml`'s `scan-secrets` and self-CI. | +| `security/osv-scanner` | transverse | Scans dependency manifests for known vulnerabilities (OSV.dev); skips a repo with no supported manifest. Behind `security-gate.yml`'s `scan-supply-chain` (non-Rust) and self-CI. | +| `security/rust/cargo-deny` | Rust | Runs cargo-deny against the canonical imposed `security/deny.toml` (sparse-checked from `coroboros/ci`, no consumer override). The `checks` input selects which checks run — `advisories bans sources` for the `security-gate.yml` `scan-supply-chain`, `licenses` for the `security.yml` advisory layer. | | `release/verify-tag` | transverse | Fails the release unless the checked-out `main` HEAD matches the tag SHA. Shared by the npm and Rust `publish-package` 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/generate-changelog` | transverse | SemVer-strict tag guard + generates or reuses the `## vX.Y.Z` section in `CHANGELOG.md` from Conventional Commits. Outputs `body`. Idempotent. Fails the release when the newest section's version was never tagged and ≠ the release tag — a hand-curated section whose version wasn't bumped. | | `release/github-release` | transverse | Creates the GitHub Release for the current tag, optionally as a `draft`. Body typically chained from `release/generate-changelog`. | | `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. | @@ -295,7 +295,7 @@ Nobody pushes directly to protected branches (`main`, `develop`, `release/x.y.z` | Other / non-standard | Others | | `!:` or `BREAKING CHANGE:` | Breaking Changes (always first) | -Section format: `## vX.Y.Z - DD/MM/YYYY`. Idempotent. Reuses an existing hand-curated section for the tag if present. +Section format: `## vX.Y.Z - DD/MM/YYYY`. Idempotent. Reuses an existing hand-curated section for the tag if present. Fails loud on a mis-bumped section — newest `## vX.Y.Z` untagged and ≠ the release tag — instead of auto-generating over it. @@ -305,8 +305,8 @@ Section format: `## vX.Y.Z - DD/MM/YYYY`. Idempotent. Reuses an existing hand-cu `coroboros/ci` runs a CI on itself — lint, security, and the `v0` release move — plus a test layer that exercises its own composite actions, which are the product: -- **Lint** (`self-lint.yml`) — `actionlint`, `yamllint`, `shellcheck`. -- **Security** (`self-security.yml`) — the `gitleaks` / `osv-scanner` composites and the `security-gate` / `security` workflows, via local `./` refs. +- **Lint** (`self-lint.yml`) — `check-actions` (actionlint), `check-yaml` (yamllint), `check-shell` (shellcheck). +- **Security** (`self-security.yml`) — `scan-secrets` / `scan-deps` (the `gitleaks` / `osv-scanner` composites) and the `security-gate` / `security` workflows, via local `./` refs. - **Release** (`self-release.yml`) — moves the rolling `v0` tag onto each stable release. - **Test** (`self-test.yml`) — smoke every composite (`release/*`, `rust/*`, `security/*`) against the real checkout, and run `javascript/base` + `rust/base` end-to-end on a `test/fixtures/` package and crate. diff --git a/package.json b/package.json index b8dca1a..b5f2a31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coroboros/ci", - "version": "0.2.5", + "version": "0.2.6", "private": true, "description": "Reusable GitHub Actions CI for the Coroboros stack.", "license": "SEE LICENSE IN LICENSE.md",