diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec5add3..0047f53 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,10 @@ updates: directory: / schedule: interval: weekly + # A release has a week to be yanked or flagged before it is proposed. + # Security updates ignore the cooldown. + cooldown: + default-days: 7 groups: actions: patterns: ["*"] @@ -18,6 +22,10 @@ updates: directory: / schedule: interval: weekly + # A release has a week to be yanked or flagged before it is proposed. + # Security updates ignore the cooldown. + cooldown: + default-days: 7 groups: cargo: update-types: [minor, patch] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4fc60f..83307ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,6 @@ on: push: branches: [main] pull_request: - # Advisories land without a code change; surface them on main rather than - # on whichever unrelated pull request happens to run next. - schedule: - - cron: "0 6 * * 1" permissions: contents: read @@ -15,10 +11,9 @@ permissions: # A newer run cancels an older one, except on main, where a run may be holding # the release window below open with the previous release already deleted, and # cancelling it would leave no release at all. A newer pending run still replaces an older pending one, so -# only the newest push waiting on main survives. The event is in the key so -# the weekly scheduled run, which never releases, cannot be what replaces it. +# only the newest push waiting on main survives. concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} # Every action is pinned to a commit, with its release in a comment for @@ -45,6 +40,8 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: toolchain: ${{ env.PINNED_RUST }} @@ -72,11 +69,19 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: toolchain: ${{ env.PINNED_RUST }} components: clippy, rustfmt - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + # Workflow security audit (unpinned actions, excess permissions, template + # injection). Offline because the online audits need a token. + - uses: taiki-e/install-action@a4d3f3f761dcd95cce1b72cb61cb6d1963438177 # v2.87.30 + with: + tool: zizmor@1.31.1 + - run: zizmor --offline .github - run: cargo fmt --check - run: python3 tests/coverage.py - uses: taiki-e/install-action@a4d3f3f761dcd95cce1b72cb61cb6d1963438177 # v2.87.30 @@ -91,13 +96,13 @@ jobs: coverage: needs: test - if: github.event_name != 'schedule' runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: coverage-* @@ -145,6 +150,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: toolchain: 1.88.0 @@ -156,6 +163,8 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # cargo audit itself rather than rustsec/audit-check, whose releases # still run on Node.js 20. Pinned like the lint toolchain. - uses: taiki-e/install-action@a4d3f3f761dcd95cce1b72cb61cb6d1963438177 # v2.87.30 @@ -168,12 +177,8 @@ jobs: # Prebuilt binaries, one per platform the test matrix above runs on. A binary # for a platform whose suite never ran there is a promise nobody checked, so # widen that matrix before adding a row here. - # - # Not on the weekly schedule: that run exists for the audit and never - # releases, so binaries built there would go nowhere. build: name: Binary ${{ matrix.target }} - if: github.event_name != 'schedule' strategy: fail-fast: false matrix: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..31bc73f --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,26 @@ +name: Security + +# Advisories land without a code change, so this runs on a clock rather than +# on pushes. ci.yml still audits every push and pull request, and gates the +# release on it; this is the run that notices a new advisory between them. +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: taiki-e/install-action@a4d3f3f761dcd95cce1b72cb61cb6d1963438177 # v2.87.30 + with: + tool: cargo-audit@0.22.2 + # Same flags as ci.yml: unsound and yanked crates fail like vulnerabilities. + - run: cargo audit --deny warnings diff --git a/Cargo.lock b/Cargo.lock index 0691fda..0f62dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,9 +1702,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -1985,7 +1985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2471,9 +2471,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" dependencies = [ "rustls-pki-types", ] diff --git a/docs/development.md b/docs/development.md index 92afff3..6218e7f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -152,19 +152,21 @@ TTL alone never supplies content for conflict detection. ## Continuous integration -`.github/workflows/ci.yml` runs on pushes to `main`, on pull requests, and -weekly: +`.github/workflows/ci.yml` runs on pushes to `main` and on pull requests: | Job | What it checks | |-----|----------------| | `test` | the full suite with line coverage on Linux, macOS, and Windows | -| `lint` | formatting, coverage-script tests, unused dependencies, duplicate reporting, and clippy on a pinned toolchain | -| `coverage` | merged platform reports and the changed-line coverage ratchet; skipped on the weekly run | +| `lint` | formatting, workflow security (zizmor), coverage-script tests, unused dependencies, duplicate reporting, and clippy on a pinned toolchain | +| `coverage` | merged platform reports and the changed-line coverage ratchet | | `msrv` | the build on the `rust-version` in `Cargo.toml` | | `audit` | RustSec advisories against `Cargo.lock` | -| `build` | release binaries for each tested platform, Linux against glibc 2.17; skipped on the weekly run | +| `build` | release binaries for each tested platform, Linux against glibc 2.17 | | `release` | on pushes to `main` only, after every job above passes, replaces the `latest` GitHub release | +`.github/workflows/security.yml` runs the same `cargo audit` daily and on demand, +because advisories land without a code change; Dependabot proposes the fix. + The release scripts live in `.ci/`; their header comments explain the ordering that keeps a failed run from leaving no release behind.