1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
name: Release
on:
push:
branches:
- main
# Manual re-publish escape hatch: lets the publish job run for the version
# currently on main when an automated release published the tag but the
# publish job failed downstream (e.g. a transient Sigstore/Fulcio outage).
workflow_dispatch:
permissions:
contents: write
pull-requests: write
# Serialise release runs so a push and a manual dispatch can't race into two
# concurrent cargo publish attempts. Never cancel an in-flight publish.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
runs-on: cachekit
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5 # v4
id: release
with:
token: ${{ steps.app-token.outputs.token }}
publish:
needs: release-please
# Run on an automated release, OR on a manual dispatch — but a manual dispatch
# must target main (workflow_dispatch can be fired from any ref; restricting to
# refs/heads/main stops a feature branch from publishing its own Cargo.toml).
# Explicit == 'true' avoids relying on string-coercion of the action output.
if: ${{ needs.release-please.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
# GitHub-hosted: the self-hosted ARC pods have unreliable DNS/egress to Sigstore
# (Fulcio/Rekor), which intermittently fails build-provenance + SBOM attestation
# and blocks publish. A hosted runner has reliable egress to Sigstore + crates.io,
# and the publish job is infrequent + free on public repos.
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Cache cargo registry
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Run tests before publish
run: cargo test --all-features
- name: Package crate
run: cargo package
- name: Attest Build Provenance
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
with:
subject-path: target/package/*.crate
- name: Install cargo-sbom
# --force is required: the self-hosted runner's CARGO_HOME (/cache/cargo) is a
# persistent volume, so the binary survives between runs and a plain install
# exits 101 ("binary `cargo-sbom` already exists"). --force reinstalls the
# --locked pinned version idempotently.
run: cargo install cargo-sbom --locked --force
- name: Generate SBOM
run: |
cargo sbom --output-format cyclone_dx_json_1_6 > sbom.cdx.json
test -s sbom.cdx.json || { echo "::error::SBOM file is empty"; exit 1; }
- name: Attest SBOM
uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2
with:
subject-path: target/package/*.crate
sbom-path: sbom.cdx.json
- name: Publish to crates.io
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}