Repository navigation
cargo-out-dir: resolve a build script's OUT_DIR as an action output #25
Workflow file for this run
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 | |
| # cargo-out-dir: the fixture's build.rs bakes a marker into OUT_DIR; | |
| # the action must resolve exactly that directory (dependencies' build | |
| # scripts emit the same message — the package filter picks the right | |
| # one). | |
| - name: cargo-out-dir (fixture) | |
| id: fixture_out_dir | |
| uses: ./.github/actions/cargo-out-dir | |
| with: | |
| working-directory: fixtures/sample-crate | |
| package: cicd-rust-selftest-fixture | |
| - name: cargo-out-dir resolved the baked marker | |
| run: | | |
| dir="${{ steps.fixture_out_dir.outputs.out-dir }}" | |
| test -d "$dir" || { echo "::error::out-dir is not a directory: $dir"; exit 1; } | |
| test -f "$dir/fixture-marker" || { echo "::error::marker missing in $dir"; exit 1; } | |
| echo "✓ OUT_DIR resolved with baked content" | |
| # Negative control: an unknown package must fail loudly instead of | |
| # emitting an empty path for a later step to copy from. | |
| - name: cargo-out-dir rejects an unknown package (negative control) | |
| id: bogus_out_dir | |
| continue-on-error: true | |
| uses: ./.github/actions/cargo-out-dir | |
| with: | |
| working-directory: fixtures/sample-crate | |
| package: no-such-package | |
| - name: assert the unknown package failed | |
| run: | | |
| if [ "${{ steps.bogus_out_dir.outcome }}" != "failure" ]; then | |
| echo "::error::unknown package did not fail"; exit 1 | |
| fi | |
| echo "✓ unknown package rejected" | |
| - 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 save half of rust-cache: prune target/ to dependency artifacts, | |
| # upload under the restore's key. Runs as (nearly) the last step, the | |
| # position consumers use; a fresh key exercises prune + upload, an | |
| # exact-hit re-run skips both (the stored entry is already pruned). | |
| - name: rust-cache-save (fixture; prune + upload) | |
| uses: ./.github/actions/rust-cache-save | |
| with: | |
| working-directory: fixtures/sample-crate | |
| - name: prune kept the dependency and dropped the member | |
| if: env.RUST_CI_TARGET_HIT != 'true' | |
| run: | | |
| deps=fixtures/sample-crate/target/debug/deps | |
| ls "$deps" | grep -q '^liblibc-' || { | |
| echo "::error::dependency artifacts missing after prune"; exit 1; } | |
| if ls "$deps" | grep -q '^cicd_rust_selftest_fixture-'; then | |
| echo "::error::workspace member artifacts survived the prune"; exit 1 | |
| fi | |
| echo "✓ prune kept libc, dropped the member" | |
| # save: false is the pull-request mode — the step must no-op. | |
| - name: rust-cache-save honours save=false (no-op control) | |
| uses: ./.github/actions/rust-cache-save | |
| with: | |
| save: "false" | |
| working-directory: fixtures/sample-crate | |
| # The changelog action, both modes, against the fixture crate: the check gate | |
| # (bump present, bump missing, breaking without more than a patch bump) and | |
| # the cut (heading + compare link rewritten, the version exported, a second | |
| # cut refused once nothing is unreleased). | |
| selftest-changelog: | |
| name: exercise the changelog action | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # cargo metadata resolves the crate version; the runner's default toolchain suffices. | |
| - name: install-toolchain | |
| uses: ./.github/actions/install-toolchain | |
| - name: check passes with a bumped version | |
| uses: ./.github/actions/changelog | |
| with: | |
| mode: check | |
| working-directory: fixtures/sample-crate | |
| baseline-version: "0.0.9" | |
| - name: check fails without a bump (negative control) | |
| id: nobump | |
| continue-on-error: true | |
| uses: ./.github/actions/changelog | |
| with: | |
| mode: check | |
| working-directory: fixtures/sample-crate | |
| baseline-version: "0.1.0" | |
| - name: stage a breaking entry on a patch bump | |
| run: | | |
| rm -rf "$RUNNER_TEMP/breaking" | |
| rsync -a --exclude='target*' fixtures/sample-crate/ "$RUNNER_TEMP/breaking/" | |
| sed -i 's/^version = "0.1.0"/version = "0.1.1"/' "$RUNNER_TEMP/breaking/Cargo.toml" | |
| awk '{print} /^## \[Unreleased\]/{print ""; print "- **Breaking:** a sample breaking entry."}' \ | |
| "$RUNNER_TEMP/breaking/CHANGELOG.md" > "$RUNNER_TEMP/breaking/CHANGELOG.md.new" | |
| mv "$RUNNER_TEMP/breaking/CHANGELOG.md.new" "$RUNNER_TEMP/breaking/CHANGELOG.md" | |
| - name: check refuses a breaking entry on a patch bump (negative control) | |
| id: breaking_patch | |
| continue-on-error: true | |
| uses: ./.github/actions/changelog | |
| with: | |
| mode: check | |
| working-directory: ${{ runner.temp }}/breaking | |
| baseline-version: "0.1.0" | |
| - name: check accepts a breaking entry with a minor bump | |
| run: sed -i 's/^version = "0.1.1"/version = "0.2.0"/' "$RUNNER_TEMP/breaking/Cargo.toml" | |
| - uses: ./.github/actions/changelog | |
| with: | |
| mode: check | |
| working-directory: ${{ runner.temp }}/breaking | |
| baseline-version: "0.1.0" | |
| - name: stage a copy for the cut | |
| run: | | |
| rm -rf "$RUNNER_TEMP/cut" | |
| rsync -a --exclude='target*' fixtures/sample-crate/ "$RUNNER_TEMP/cut/" | |
| - name: cut releases the [Unreleased] section | |
| uses: ./.github/actions/changelog | |
| with: | |
| mode: cut | |
| working-directory: ${{ runner.temp }}/cut | |
| date: "2026-02-02" | |
| - name: the cut rewrote the heading and link and exported the version | |
| run: | | |
| grep -q '^## \[0.1.0\] - 2026-02-02$' "$RUNNER_TEMP/cut/CHANGELOG.md" | |
| grep -q '^\[0.1.0\]: https://example.invalid/sample/compare/v0.0.9\.\.\.v0.1.0$' \ | |
| "$RUNNER_TEMP/cut/CHANGELOG.md" | |
| if grep -q 'Unreleased' "$RUNNER_TEMP/cut/CHANGELOG.md"; then | |
| echo "::error::an [Unreleased] remnant survived the cut"; exit 1 | |
| fi | |
| test "$CHANGELOG_VERSION" = "0.1.0" | |
| echo "✓ cut rewrote the section, the link, and exported CHANGELOG_VERSION" | |
| - name: a second cut is refused (negative control) | |
| id: cut_again | |
| continue-on-error: true | |
| uses: ./.github/actions/changelog | |
| with: | |
| mode: cut | |
| working-directory: ${{ runner.temp }}/cut | |
| - name: assert the negative controls failed | |
| run: | | |
| if [ "${{ steps.nobump.outcome }}" != "failure" ]; then | |
| echo "::error::a missing bump was not refused"; exit 1 | |
| fi | |
| if [ "${{ steps.breaking_patch.outcome }}" != "failure" ]; then | |
| echo "::error::a breaking entry on a patch bump was not refused"; exit 1 | |
| fi | |
| if [ "${{ steps.cut_again.outcome }}" != "failure" ]; then | |
| echo "::error::a second cut was not refused"; exit 1 | |
| fi | |
| echo "✓ all changelog negative controls refused" | |
| # 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" | |
| # build-image `msrv` sentinel: build at the fixture's DECLARED rust-version, read | |
| # from Cargo.toml (no hand-managed version), so a consumer can run the whole Docker | |
| # pipeline on the support floor. The fixture pins 1.82. | |
| - name: build-image resolves the msrv sentinel from Cargo.toml | |
| uses: ./.github/actions/build-image | |
| with: | |
| rust-version: msrv | |
| working-directory: fixtures/sample-crate | |
| tag: rust-ci:bi-msrv | |
| - name: the sentinel image IS the declared MSRV (1.82) | |
| run: | | |
| v="$(docker run --rm rust-ci:bi-msrv rustc --version)" | |
| echo "$v" | |
| case "$v" in | |
| *" 1.82"*) echo "✓ sentinel image is rust 1.82" ;; | |
| *) echo "::error::sentinel image is not rust 1.82: $v"; exit 1 ;; | |
| esac | |
| # Negative control: a non-numeric Cargo.toml rust-version is rejected by the shared | |
| # resolver before it can become a docker tag — proving build-image wires the | |
| # validation into the sentinel path (the smuggled command must not run). | |
| - name: craft a bad-MSRV manifest (for the negative control) | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/bad-msrv" | |
| printf '[package]\nname = "bad"\nversion = "0.0.0"\nrust-version = "1.0; touch pwned"\n' \ | |
| > "$RUNNER_TEMP/bad-msrv/Cargo.toml" | |
| - name: build-image rejects a non-numeric declared rust-version (negative control) | |
| id: bi_bad | |
| continue-on-error: true | |
| uses: ./.github/actions/build-image | |
| with: | |
| rust-version: msrv | |
| working-directory: ${{ runner.temp }}/bad-msrv | |
| tag: rust-ci:bi-bad | |
| - name: assert the bad declared rust-version was rejected | |
| run: | | |
| if [ "${{ steps.bi_bad.outcome }}" != "failure" ]; then | |
| echo "::error::build-image accepted a non-numeric rust-version"; exit 1 | |
| fi | |
| test ! -e "$RUNNER_TEMP/bad-msrv/pwned" | |
| echo "✓ build-image rejected a non-numeric declared rust-version" | |
| # msrv: build an image at the fixture's DECLARED rust-version and cargo check there. | |
| # Exercises the Cargo.toml extraction, the image-at-MSRV build, and the sealed check. | |
| # locked: true takes the committed-lockfile path — the fixture pins one. | |
| - name: msrv (image at the declared MSRV, sealed check) | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: fixtures/sample-crate | |
| package: cicd-rust-selftest-fixture | |
| locked: "true" | |
| # Negative control: a non-numeric rust-version is rejected before it can reach a docker | |
| # tag or shell (proves the input validation; the smuggled command must not run). | |
| - name: msrv rejects a non-numeric rust-version (negative control) | |
| id: msrv_bad | |
| continue-on-error: true | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: fixtures/sample-crate | |
| rust-version: "latest; touch pwned" | |
| - name: assert the bad rust-version was rejected | |
| run: | | |
| if [ "${{ steps.msrv_bad.outcome }}" != "failure" ]; then | |
| echo "::error::msrv accepted a non-numeric rust-version"; exit 1 | |
| fi | |
| test ! -e fixtures/sample-crate/pwned | |
| echo "✓ msrv rejected a non-numeric rust-version" | |
| # A crate with NO committed lockfile must still work: the source is mounted read-only for the | |
| # sealed check, so the action resolves the lock up front rather than failing to write it there. | |
| - name: stage a copy of the fixture with no Cargo.lock | |
| run: | | |
| rm -rf "$RUNNER_TEMP/fixture-nolock" | |
| mkdir -p "$RUNNER_TEMP/fixture-nolock" | |
| rsync -a --exclude='target*' --exclude='Cargo.lock' fixtures/sample-crate/ "$RUNNER_TEMP/fixture-nolock/" | |
| test ! -e "$RUNNER_TEMP/fixture-nolock/Cargo.lock" | |
| # Negative control: locked: true demands a committed lockfile, so a crate without one fails | |
| # with a clear up-front error — not the cryptic read-only-filesystem failure of writing it in-container. | |
| - name: msrv (locked true) rejects a crate with no committed lockfile | |
| id: msrv_nolock_locked | |
| continue-on-error: true | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: ${{ runner.temp }}/fixture-nolock | |
| package: cicd-rust-selftest-fixture | |
| locked: "true" | |
| - name: assert the missing lockfile was rejected | |
| run: | | |
| if [ "${{ steps.msrv_nolock_locked.outcome }}" != "failure" ]; then | |
| echo "::error::msrv locked:true accepted a crate with no Cargo.lock"; exit 1 | |
| fi | |
| test ! -e "$RUNNER_TEMP/fixture-nolock/Cargo.lock" | |
| echo "✓ msrv locked:true requires a committed lockfile" | |
| # Default (locked: false): resolve a fresh lockfile at the MSRV up front, then check it sealed. | |
| - name: msrv (locked false) resolves a lockfile when none is committed | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: ${{ runner.temp }}/fixture-nolock | |
| package: cicd-rust-selftest-fixture | |
| # The resolution happens in a disposable copy, so the lockless checkout must STAY lockless — | |
| # the action may not leave an untracked Cargo.lock behind. | |
| - name: assert the lockless checkout stayed lockless | |
| run: | | |
| test ! -e "$RUNNER_TEMP/fixture-nolock/Cargo.lock" | |
| echo "✓ msrv locked:false left no Cargo.lock behind" | |
| # locked: false must not rewrite a committed lockfile either: fresh resolution happens in | |
| # the disposable copy while the checkout's Cargo.lock stays byte-identical. | |
| - name: snapshot the committed lockfile | |
| run: cp fixtures/sample-crate/Cargo.lock "$RUNNER_TEMP/Cargo.lock.before" | |
| - name: msrv (locked false) resolves fresh without touching the committed lockfile | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: fixtures/sample-crate | |
| package: cicd-rust-selftest-fixture | |
| - name: assert the committed lockfile is byte-identical | |
| run: | | |
| cmp "$RUNNER_TEMP/Cargo.lock.before" fixtures/sample-crate/Cargo.lock | |
| echo "✓ msrv locked:false preserved the committed Cargo.lock byte-for-byte" | |
| # Regression: a consumer's build.rs may read git metadata (git describe, the commit | |
| # hash), so the disposable copy must preserve it — here in the plain .git DIRECTORY | |
| # form. The fixture's build.rs asserts `git rev-parse HEAD` succeeds whenever | |
| # FIXTURE_REQUIRE_GIT is set (forwarded into the container via the literal env input). | |
| - name: stage a git-repo fixture with no Cargo.lock (.git directory) | |
| run: | | |
| rm -rf "$RUNNER_TEMP/fixture-gitdir" | |
| mkdir -p "$RUNNER_TEMP/fixture-gitdir" | |
| rsync -a --exclude='target*' --exclude='Cargo.lock' fixtures/sample-crate/ "$RUNNER_TEMP/fixture-gitdir/" | |
| git -C "$RUNNER_TEMP/fixture-gitdir" init -q -b main | |
| git -C "$RUNNER_TEMP/fixture-gitdir" -c user.name=selftest -c user.email=selftest@invalid add -A | |
| git -C "$RUNNER_TEMP/fixture-gitdir" -c user.name=selftest -c user.email=selftest@invalid commit -q -m fixture | |
| test -d "$RUNNER_TEMP/fixture-gitdir/.git" | |
| - name: msrv (locked false) preserves git metadata in the copy (.git directory) | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: ${{ runner.temp }}/fixture-gitdir | |
| package: cicd-rust-selftest-fixture | |
| env: | | |
| FIXTURE_REQUIRE_GIT=1 | |
| # The same guarantee for the .git FILE form: a linked worktree of that repo (the | |
| # form submodules use as well). | |
| - name: stage a linked-worktree fixture (.git file) | |
| run: | | |
| rm -rf "$RUNNER_TEMP/fixture-gitfile" | |
| git -C "$RUNNER_TEMP/fixture-gitdir" worktree add -q -b wt "$RUNNER_TEMP/fixture-gitfile" | |
| test -f "$RUNNER_TEMP/fixture-gitfile/.git" | |
| test ! -e "$RUNNER_TEMP/fixture-gitfile/Cargo.lock" | |
| - name: msrv (locked false) preserves git metadata in the copy (.git file / worktree) | |
| uses: ./.github/actions/msrv | |
| with: | |
| working-directory: ${{ runner.temp }}/fixture-gitfile | |
| package: cicd-rust-selftest-fixture | |
| env: | | |
| FIXTURE_REQUIRE_GIT=1 | |
| # 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" | |
| # rust-cache must resolve the cargo home from CARGO_HOME (container images bake the | |
| # toolchain outside $HOME/.cargo): seed a sentinel under a custom CARGO_HOME, let the | |
| # post step save it, and prove a second job restores it into the same custom location. | |
| selftest-cache-cargo-home-save: | |
| name: rust-cache honors CARGO_HOME (save) | |
| runs-on: ubuntu-latest | |
| env: | |
| CARGO_HOME: /tmp/cargo-home | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: seed a sentinel in the custom cargo home | |
| run: | | |
| mkdir -p "$CARGO_HOME/registry/index" "$CARGO_HOME/registry/cache" "$CARGO_HOME/git/db" | |
| echo "${{ github.run_id }}" > "$CARGO_HOME/registry/cache/selftest-sentinel" | |
| - uses: ./.github/actions/rust-cache | |
| with: | |
| prefix: cargohome-${{ github.run_id }} | |
| selftest-cache-cargo-home-restore: | |
| name: rust-cache honors CARGO_HOME (restore) | |
| needs: selftest-cache-cargo-home-save | |
| runs-on: ubuntu-latest | |
| env: | |
| CARGO_HOME: /tmp/cargo-home | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/rust-cache | |
| with: | |
| prefix: cargohome-${{ github.run_id }} | |
| save: "false" | |
| - name: sentinel came back into the custom cargo home | |
| run: test "$(cat "$CARGO_HOME/registry/cache/selftest-sentinel")" = "${{ github.run_id }}" | |
| # save:"false" must not create a cache entry: run restore-only under a unique prefix, | |
| # then assert no entry with that prefix exists. | |
| selftest-cache-no-save: | |
| name: rust-cache save:false saves nothing | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/rust-cache | |
| with: | |
| prefix: nosave-${{ github.run_id }} | |
| save: "false" | |
| selftest-cache-no-save-assert: | |
| name: rust-cache save:false left no entry | |
| needs: selftest-cache-no-save | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| steps: | |
| - name: no cache entry exists for the nosave prefix | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| COUNT=$(gh cache list --repo ${{ github.repository }} --limit 100 --json key \ | |
| --jq '[.[] | select(.key | startswith("nosave-${{ github.run_id }}-"))] | length') | |
| test "$COUNT" -eq 0 |