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
name: release
# Pushing the tag `v0.1.0` publishes 0.1.0. NOTHING else does — and publishing
# is forever, so every gate CI runs runs here too, plus two more: the tag must
# agree with the workspace version, and CHANGELOG must have that section.
on:
push:
tags:
# Rehearse the whole path (gates + a real dry-run publish) with no side
# effects. Safe from ANY ref, tags included: the publish steps below key on
# the EVENT, never on the ref shape, because GitHub happily dispatches a
# workflow against a tag — and "I pressed rehearse" must never upload.
workflow_dispatch:
permissions:
contents: write # create the GitHub release
# One release at a time, and NEVER cancel one in flight: a half-cancelled
# publish leaves crate names claimed with no release to show for it. The
# rate-limited first publish can run for an hour; a re-pushed tag waits.
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
# The real thing only on a pushed tag. Every irreversible step tests
# this one variable, so the rehearsal path cannot drift into uploading.
SHIPPING: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: wasm32-unknown-unknown
# A release is not a reason to skip the gates.
- run: cargo fmt --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
- run: cargo check --workspace --target wasm32-unknown-unknown
- run: bash scripts/caps.sh
- name: The tag is a version claim — check it
if: env.SHIPPING == 'true'
run: |
want="${GITHUB_REF_NAME#v}"
got=$(cargo tree --workspace --depth 0 |
awk '$1 == "litelite" {print substr($2, 2); exit}')
if [ "$want" != "$got" ]; then
echo "FAIL: tag ${GITHUB_REF_NAME} claims $want, the workspace says $got"
exit 1
fi
echo "tag ${GITHUB_REF_NAME} == workspace $got"
# Exact field match, not a regex: a version's dots are not wildcards,
# and `## 0.2.0` with no date suffix is still a real section.
- name: The tag has release notes — check it
if: env.SHIPPING == 'true'
run: |
awk -v v="${GITHUB_REF_NAME#v}" \
'$1 == "##" && $2 == v {f = 1; next} /^## /{f = 0} f' CHANGELOG.md > NOTES.md
if [ ! -s NOTES.md ]; then
echo "FAIL: CHANGELOG.md has no '## ${GITHUB_REF_NAME#v}' section"
exit 1
fi
cat NOTES.md
# Manual runs stop here: gates + a real dry-run upload, no side effects.
- name: Rehearse the publish
if: env.SHIPPING != 'true'
run: bash scripts/publish.sh
# crates.io rate-limits NEW crates, so the first release may stop
# partway. Re-run this job: publish.sh skips whatever is already live.
- name: Publish to crates.io
if: env.SHIPPING == 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then
echo "FAIL: the CARGO_REGISTRY_TOKEN secret is not set"
exit 1
fi
bash scripts/publish.sh --execute
# Last, so a release never claims what crates.io refused. Idempotent:
# re-running a job whose publish already finished must not fail here.
- name: GitHub release
if: env.SHIPPING == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
echo "release ${GITHUB_REF_NAME} already exists — leaving it alone"
exit 0
fi
gh release create "${GITHUB_REF_NAME}" \
--title "litelite ${GITHUB_REF_NAME}" \
--notes-file NOTES.md \
--verify-tag