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
133
134
135
136
137
# Manual distribution workflow. Push a v* tag first; `Release Prepare` runs the
# test suite and packages the crate. Then run this workflow with that tag and
# the successful prepare run ID.
#
# NOTHING HERE COMPILES the test suite again. Every stage consumes the prepare
# run's artifact, so a failure in one stage is retried by re-dispatching with
# only that stage enabled — the tests are never re-run to fix a crates.io
# timeout or a bad release body.
#
# Stages, in dependency order:
# publish_crate -> crates.io
# github_release -> GitHub Release carrying the .crate file
#
# Each stage is idempotent: an already-published version is skipped, and the
# release is overwritten in place. Re-running everything is safe; the toggles
# exist to save time, not to protect against double-publishing.
#
# Required secrets: CARGO_REGISTRY_TOKEN.
name: Release
run-name: Release ${{ inputs.tag }}
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish, e.g. v0.1.0"
required: true
type: string
prepare_run_id:
description: "Successful Release Prepare run ID holding the .crate"
required: true
type: string
publish_crate:
description: "Publish to crates.io"
type: boolean
default: true
github_release:
description: "Create the GitHub Release"
type: boolean
default: true
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# Always runs. Cheap, and it guarantees the dispatched tag still means what
# the prepare run assumed it meant.
validate-version:
uses: ./.github/workflows/release-validate.yml
with:
ref: ${{ inputs.tag }}
# ── crates.io ──────────────────────────────────────────────────────────────
publish-crate:
name: Publish to crates.io
needs: validate-version
if: inputs.publish_crate
runs-on: ubuntu-latest
environment: crates.io
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# Must run before publishing: the manifest carries only the base version,
# so a prerelease tag would otherwise publish the base version instead.
- name: Set version from tag
run: bash scripts/ci/stamp_version.sh "${{ needs.validate-version.outputs.version }}"
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
set -euo pipefail
# Skip if this exact version is already indexed, so a re-dispatch
# after a partial failure is a no-op rather than an error.
if curl -sf \
-H "User-Agent: faultbox-ci (github.com/${{ github.repository }})" \
"https://crates.io/api/v1/crates/faultbox/${VERSION}" > /dev/null 2>&1
then
echo "faultbox@${VERSION} already published — skipping"
exit 0
fi
# Verified here rather than trusting the prepare run's artifact: the
# registry is what consumers get, and this is the last gate before it.
# `--allow-dirty` because the stamp step edits Cargo.toml in the tree.
cargo publish --all-features --allow-dirty
# ── GitHub Release ─────────────────────────────────────────────────────────
# Deliberately independent of publish-crate: a crates.io outage must not block
# cutting the release, and re-running this stage alone is the fix for a bad
# release body.
github-release:
name: Create GitHub Release
needs: validate-version
if: inputs.github_release
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Download crate artifact
uses: actions/download-artifact@v8
with:
name: crate
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
name: faultbox ${{ needs.validate-version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: ${{ needs.validate-version.outputs.is_full_release != 'true' }}
files: artifacts/*
fail_on_unmatched_files: true