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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 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 }}
# Needed to find the preceding tag for the compare link.
fetch-depth: 0
- name: Download crate artifact
uses: actions/download-artifact@v8
with:
name: crate
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./artifacts
# The changelog is the release notes. Auto-generated notes list commit
# subjects, which describe what was done to the code; the changelog
# describes what changed for the person upgrading. `validate-version`
# has already proved this section exists and is non-empty.
- name: Build release notes from the changelog
env:
TAG: ${{ inputs.tag }}
BASE: ${{ needs.validate-version.outputs.base_version }}
run: |
PREVIOUS_TAG=$(git tag --sort=-v:refname 'v*' | grep -v "^${TAG}$" | head -n 1 || true)
bash scripts/ci/changelog_section.sh "$BASE" /tmp/changelog-section.md
{
cat /tmp/changelog-section.md
echo
if [[ -n "$PREVIOUS_TAG" ]]; then
echo "**Full diff:** https://github.com/farhan-syah/faultbox/compare/${PREVIOUS_TAG}...${TAG}"
else
echo "**Tree:** https://github.com/farhan-syah/faultbox/tree/${TAG}"
fi
} > /tmp/release-notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
name: faultbox ${{ needs.validate-version.outputs.version }}
body_path: /tmp/release-notes.md
draft: false
prerelease: ${{ needs.validate-version.outputs.is_full_release != 'true' }}
files: artifacts/*
fail_on_unmatched_files: true