name: 'Cargo cache (publish)' description: >- Records this run's build watermark and, on a publisher branch, atomically republishes its target directory as the immutable snapshot that other branches' caches are hardlink-cloned from. author: 'gitdan' inputs: cache-root: description: 'Mount point of the persistent cache volume. Must match the consume action.' required: false default: '/cache' protected-branches: description: >- Space-separated refs that publish snapshots. A run whose own ref is not in this list records its watermark and skips publishing. required: false default: 'dev main' mode: description: >- publish — record the watermark, publish a snapshot if eligible, release this job's cache lock (the normal call, after a green build). release-lock — release this job's cache lock and do nothing else. Use in a final `if: always()` step so a failed run does not leave a lock behind for the staleness grace period. required: false default: 'publish' own-ref: description: 'Override this run''s ref. Defaults to github.head_ref, else github.ref_name.' required: false default: '' publish-on-events: description: >- Space-separated event names on which a publisher branch actually publishes. Defaults to `push` — a pull_request run never publishes, because its ref is not the reference branch even when it targets one. required: false default: 'push' read-grace-seconds: description: >- How long the snapshot swap waits for in-flight consumers to finish cloning the generation it is replacing before reclaiming it. On timeout the old generation is LEFT ON DISK and swept by a later publish — the unlink is never forced, because unlinking a tree a consumer is walking is what silently truncates that consumer's clone. required: false default: '300' reader-stale-seconds: description: >- Age past which a consumer's read marker is treated as abandoned by a job the runner killed. Without it one crashed job would pin a snapshot generation on disk permanently. required: false default: '7200' record-watermark: description: >- Record this run's HEAD as the build watermark for this target dir. True for PR runs too, not just publishers: a feature branch accumulates its own build history across several pushes and needs its own watermark. required: false default: 'true' runs: using: 'composite' steps: # Everything here reads the environment the consume action exported, so a # workflow that forgets to run cargo-cache first fails loudly here rather # than silently publishing a snapshot of the wrong directory. - id: resolve shell: bash env: PROTECTED_BRANCHES: ${{ inputs.protected-branches }} PUBLISH_ON_EVENTS: ${{ inputs.publish-on-events }} run: | set -euo pipefail # In release-lock mode this action is called from an `if: always()` # step, which can run after a failure that happened before the # cargo-cache action ever executed. Missing environment there means # "there is no lock to release", not an error worth failing the job a # second time over. if [ -z "${CARGO_CACHE_SCRIPTS:-}" ] || [ -z "${CARGO_TARGET_DIR:-}" ]; then if [ "${{ inputs.mode }}" = "release-lock" ]; then echo "cargo-cache-publish: no cache environment in this job — nothing to release" echo "publish=no" >> "$GITHUB_OUTPUT" echo "active=no" >> "$GITHUB_OUTPUT" exit 0 fi echo "::error::cargo-cache-publish: run the cargo-cache action earlier in this job" exit 1 fi : "${CARGO_CACHE_KEY:?cargo-cache-publish: CARGO_CACHE_KEY not set by the cargo-cache action}" echo "active=yes" >> "$GITHUB_OUTPUT" OWN_REF="${{ inputs.own-ref }}" [ -n "$OWN_REF" ] || OWN_REF="${{ github.head_ref || github.ref_name }}" # A publisher is a branch other branches layer over. Two conditions, # both required: its ref is in protected-branches, AND the event is one # where this ref really is the reference branch. A pull_request run # from `dev` into `main` has own-ref `dev` and would otherwise publish # a snapshot of a merge-preview build — which is not what `dev` is. PUBLISH=no for ref in $PROTECTED_BRANCHES; do [ "$ref" = "$OWN_REF" ] || continue for ev in $PUBLISH_ON_EVENTS; do [ "$ev" = "${{ github.event_name }}" ] && PUBLISH=yes done done echo "publish=${PUBLISH}" >> "$GITHUB_OUTPUT" echo "own-ref=${OWN_REF}" >> "$GITHUB_OUTPUT" echo "publisher check: ref '${OWN_REF}', event '${{ github.event_name }}' -> publish=${PUBLISH}" # Ordered before the snapshot publish so a snapshot always carries a # watermark at least as new as the build it holds. Both steps sit after # the consuming job's build steps, so a run that fails an earlier gate # never reaches either: the watermark stays at the last GREEN build and a # red build can never overwrite a known-good snapshot. - if: ${{ steps.resolve.outputs.active == 'yes' && inputs.mode == 'publish' && inputs.record-watermark == 'true' }} shell: bash run: | set -euo pipefail bash "${CARGO_CACHE_SCRIPTS}/record-watermark.sh" \ "$CARGO_TARGET_DIR" "${CI_WATERMARK_FILE:-.ci-watermark-sha}" - if: ${{ inputs.mode == 'publish' && steps.resolve.outputs.publish == 'yes' }} shell: bash env: CACHE_READ_GRACE_SECONDS: ${{ inputs.read-grace-seconds }} CACHE_READ_STALE_SECONDS: ${{ inputs.reader-stale-seconds }} run: | set -euo pipefail bash "${CARGO_CACHE_SCRIPTS}/publish-snapshot.sh" \ "$CARGO_CACHE_KEY" "${{ inputs.cache-root }}" \ "${{ github.job }}-${{ github.run_id }}-$$" # Released in both modes. In `publish` mode this is the normal end-of-job # release; the separate `release-lock` call exists for `if: always()`, so a # failed run does not leave its lock sitting until the staleness grace # period expires. - if: ${{ steps.resolve.outputs.active == 'yes' }} shell: bash run: | set -euo pipefail if [ -z "${CARGO_CACHE_LOCK_ID:-}" ]; then echo "cargo-cache-publish: no lock id in the environment — nothing to release" exit 0 fi bash "${CARGO_CACHE_SCRIPTS}/cache-lock.sh" release \ "$CARGO_TARGET_DIR" "${CARGO_CACHE_LOCK_ID}"