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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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:
# GitHub-hosted: this job inherits contents+PR write and mints an App
# installation token, so it must not share a writable build cache with other
# jobs — job-level `permissions:` does not isolate a filesystem, and untrusted
# build-script/proc-macro code could poison a cache a later credentialed job
# runs against. A fresh hosted VM has no shared cache to poison, and
# release-please needs no warm cache anyway.
release-please:
runs-on: ubuntu-latest
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 }}
# release-please's config schema has no assignees option, so assign the
# release PR here. outputs.pr is set whenever the release PR was created
# OR updated, so this re-runs on every push to main while a release PR is
# open — adding an already-present assignee is a no-op, so that's safe.
# github-script, NOT `gh`: chosen when this job ran on a self-hosted
# runner without the gh CLI; it works identically on
# a hosted runner. outputs.pr is passed raw via env and parsed in JS — never
# through template-position fromJson, which is evaluated even when if: is
# false and crashes on '' for no-release pushes (LAB-865).
- name: Assign release PR to 27Bslash6
if: ${{ steps.release.outputs.pr }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
PR_JSON: ${{ steps.release.outputs.pr }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
try {
const pr = JSON.parse(process.env.PR_JSON);
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
assignees: ['27Bslash6'],
});
core.info(`Assigned 27Bslash6 to release PR #${pr.number}`);
} catch (error) {
core.setFailed(`Failed to assign release PR in ${context.repo.owner}/${context.repo.repo} (PR_JSON length ${process.env.PR_JSON.length}): ${error.message}`);
}
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 kept from when this job ran self-hosted with a persistent
# CARGO_HOME (/cache/cargo), where a plain install exits 101 ("binary
# `cargo-sbom` already exists"). Harmless no-op cost on a hosted runner.
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 }}