Repository navigation
feat: build the CI image locally instead of pulling from GHCR #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: selftest | |
| # Exercise every composite action end-to-end against the fixture crate, so the | |
| # actions are verified before any consumer relies on them. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| jobs: | |
| selftest: | |
| name: exercise the composite actions | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: install-toolchain | |
| uses: ./.github/actions/install-toolchain | |
| with: | |
| components: rustfmt clippy | |
| # rust-cache: warms the cargo cache + sets CARGO_INCREMENTAL=0 for the build steps below. | |
| - name: rust-cache (fixture) | |
| uses: ./.github/actions/rust-cache | |
| with: | |
| prefix: fixture | |
| cache-target: "true" | |
| target-dir: fixtures/sample-crate/target | |
| lockfiles: fixtures/sample-crate/Cargo.lock | |
| # Prove the env export crosses into a later step (the build inherits incremental=off). | |
| - name: rust-cache exported CARGO_INCREMENTAL=0 | |
| run: test "$CARGO_INCREMENTAL" = "0" | |
| - name: lint-and-test (fixture) | |
| uses: ./.github/actions/lint-and-test | |
| with: | |
| working-directory: fixtures/sample-crate | |
| - name: check-release-readiness (fixture) | |
| uses: ./.github/actions/check-release-readiness | |
| with: | |
| working-directory: fixtures/sample-crate | |
| expected-version: "0.1.0" | |
| # cargo-install + cargo-use in HOST mode (docker:false): wrap a plain | |
| # `cargo install` on the runner, then run the tool from the shared bin dir. | |
| - name: cargo-install (host) a CLI crate into a shared cache | |
| uses: ./.github/actions/cargo-install | |
| with: | |
| docker: "false" | |
| tool: npm-utils | |
| args: "--features cli" | |
| locked: "false" # probe tests the action plumbing, not --locked semantics | |
| cargo-cache: .cargo-tools-host | |
| - name: cargo-use (host) runs the installed tool | |
| uses: ./.github/actions/cargo-use | |
| with: | |
| docker: "false" | |
| cargo-cache: .cargo-tools-host | |
| args: "npm-utils --version" | |
| # Negative control: a malicious tool name is rejected on the runner by the | |
| # cargo-install allowlist before any shell runs it — no command injection. | |
| - name: cargo-install rejects a malicious tool name (negative control) | |
| id: evil_tool | |
| continue-on-error: true | |
| uses: ./.github/actions/cargo-install | |
| with: | |
| docker: "false" | |
| tool: "evil; touch /tmp/cicd-pwned" | |
| - name: assert the injection was rejected | |
| run: | | |
| if [ "${{ steps.evil_tool.outcome }}" != "failure" ]; then | |
| echo "::error::malicious tool name was not rejected"; exit 1 | |
| fi | |
| if [ -e /tmp/cicd-pwned ]; then | |
| echo "::error::injection executed: /tmp/cicd-pwned exists"; exit 1 | |
| fi | |
| echo "✓ malicious tool name rejected; no injection" | |
| # The Docker action set: build the image locally (no GHCR dependency to prove) | |
| # and run fetch (networked) -> lint/test (sealed, --network none) -> readiness | |
| # (networked) against the fixture. | |
| selftest-docker: | |
| name: exercise the Docker actions | |
| runs-on: ubuntu-latest | |
| # CICD_PROBE rides the runner env so the env-passthrough proofs below can | |
| # (conditionally) forward it into the container; the expectation is supplied | |
| # per step via the literal `env` input (CICD_EXPECT) so it always crosses. | |
| env: | |
| CICD_PROBE: forwarded | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: build-image (local, cached) | |
| uses: ./.github/actions/build-image | |
| with: | |
| tag: rust-ci:selftest | |
| cache: "true" | |
| # Negative control: the sealing flag seal_run applies (--network=none) really | |
| # blocks egress — a curl from inside a sealed container must fail. | |
| - name: sealed network is blocked (negative control) | |
| run: | | |
| if docker run --rm --network=none --cap-drop=ALL --security-opt=no-new-privileges \ | |
| rust-ci:selftest bash -c 'curl -sSf --max-time 5 https://crates.io >/dev/null 2>&1'; then | |
| echo "::error::network reachable inside a --network=none container"; exit 1 | |
| fi | |
| echo "✓ --network=none blocks network egress" | |
| - name: cargo-fetch (networked) | |
| uses: ./.github/actions/cargo-fetch | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| # cargo-docker: a sealed build off the warmed cache (exercises the low-level wrapper). | |
| - name: cargo-docker (sealed build) | |
| uses: ./.github/actions/cargo-docker | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| args: "build --locked" | |
| - name: lint-and-test (sealed, --network none) + env passthrough | |
| uses: ./.github/actions/lint-and-test-docker | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| # Positive proof: env-include forwards CICD_PROBE across the sealed | |
| # boundary; CICD_EXPECT=present makes the fixture assert it arrived. | |
| env-include: "CICD_.*" | |
| env: | | |
| CICD_EXPECT=present | |
| # Negative control: the DEFAULT include (CARGO_.*) must NOT forward an | |
| # unrelated host var. CICD_PROBE is in the runner env but should not cross. | |
| - name: lint-and-test (default include does not leak) | |
| uses: ./.github/actions/lint-and-test-docker | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| env: | | |
| CICD_EXPECT=absent | |
| # Prove the no-warmup path: networked, a cold cache, no cargo-fetch before it. | |
| - name: lint-and-test (networked, no warmup) + exclusion wins | |
| uses: ./.github/actions/lint-and-test-docker | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| offline: "false" | |
| cargo-cache: .cargo-cache-online | |
| target-dir: target-online | |
| # Exclusion wins: CICD_PROBE matches env-include but is also in | |
| # env-exclude, so it must NOT cross (CICD_EXPECT=absent asserts that). | |
| env-include: "CICD_.*" | |
| env-exclude: "CARGO_HOME|RUSTUP_HOME|CARGO_TARGET_DIR|CICD_PROBE" | |
| env: | | |
| CICD_EXPECT=absent | |
| - name: publish-dry-run (networked prep, sealed verify-build) | |
| uses: ./.github/actions/publish-dry-run | |
| with: | |
| image: rust-ci:selftest | |
| working-directory: fixtures/sample-crate | |
| expected-version: "0.1.0" | |
| # cargo-install + cargo-use: install a real CLI crate into a SHARED cache in | |
| # one sealed container, then run it from a SEPARATE sealed, offline container. | |
| # Proves an installed tool persists in the host-mounted CARGO_HOME and resolves | |
| # via the prepended bin dir — including the `cargo <subcommand>` form. npm-utils | |
| # is pure-Rust (no openssl) and gates its binaries behind the `cli` feature, so | |
| # this also exercises the free-form `args` pass-through. | |
| - name: cargo-install (sealed) a CLI crate into the shared cache | |
| uses: ./.github/actions/cargo-install | |
| with: | |
| image: rust-ci:selftest | |
| tool: npm-utils | |
| args: "--features cli" | |
| locked: "false" # probe tests the action plumbing, not --locked semantics | |
| cargo-cache: .cargo-tools | |
| - name: both installed binaries landed in the shared host dir | |
| run: test -x .cargo-tools/bin/npm-utils && test -x .cargo-tools/bin/cargo-npm-utils | |
| - name: cargo-use (sealed, offline) runs the installed cargo subcommand | |
| uses: ./.github/actions/cargo-use | |
| with: | |
| image: rust-ci:selftest | |
| cargo-cache: .cargo-tools | |
| offline: "true" | |
| args: "cargo npm-utils --help" | |
| # Negative control: cargo-use offline really seals the network — a tool that | |
| # reaches out must fail (proves the action wires OFFLINE -> --network=none). | |
| - name: cargo-use offline blocks egress (negative control) | |
| id: use_offline_egress | |
| continue-on-error: true | |
| uses: ./.github/actions/cargo-use | |
| with: | |
| image: rust-ci:selftest | |
| cargo-cache: .cargo-tools | |
| offline: "true" | |
| args: "curl -sSf --max-time 5 https://crates.io" | |
| - name: assert cargo-use sealed the network | |
| run: | | |
| if [ "${{ steps.use_offline_egress.outcome }}" != "failure" ]; then | |
| echo "::error::cargo-use offline did not block network egress"; exit 1 | |
| fi | |
| echo "✓ cargo-use offline sealed the network" | |
| # install-toolchain must also work on Windows. windows-latest exercises the rustup-present path; | |
| # windows-11-arm ships no Rust at all, exercising the rustup-init.exe bootstrap. Building the | |
| # fixture proves the toolchain is usable, not merely installed (and, on arm64, that the image | |
| # provides the MSVC arm64 link tools). | |
| selftest-windows: | |
| name: install-toolchain · ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runner: [windows-latest, windows-11-arm] | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/install-toolchain | |
| with: | |
| components: clippy | |
| - name: toolchain is installed + on PATH | |
| shell: bash | |
| run: rustc --version && cargo --version && cargo clippy --version | |
| - name: build the fixture | |
| shell: bash | |
| working-directory: fixtures/sample-crate | |
| run: cargo build --locked | |
| # rust-cache must OBEY an explicit CARGO_INCREMENTAL (not force it off): set it at job level and | |
| # confirm the action leaves it untouched for the build steps that would follow. | |
| selftest-cache-passthrough: | |
| name: rust-cache obeys CARGO_INCREMENTAL | |
| runs-on: ubuntu-latest | |
| env: | |
| CARGO_INCREMENTAL: "1" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/rust-cache | |
| with: | |
| prefix: passthrough | |
| - name: explicit CARGO_INCREMENTAL is preserved | |
| run: test "$CARGO_INCREMENTAL" = "1" |