Skip to content

Commit 4b8f51e

Browse files
ewelsclaude
andauthored
refactor: trigger releases from [release] commit message, not tag push (#92)
Restructure the release workflow so that all builds (binaries + Docker) must succeed before any irreversible action happens. Previously, pushing a tag would immediately start creating releases and publishing to crates.io in parallel with builds, so a Docker failure could leave a half-finished release with an irrevocable crates.io publish. New flow: - Trigger on push to main only (tag trigger removed) - A [release] marker in the commit message signals a release - All builds run first (binaries, Docker, SIMD Docker) - Only after all pass: create git tag, draft release, upload artifacts, publish to crates.io, and finally mark the release as non-draft - Dev Docker images still built on every main push Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 405670a commit 4b8f51e

1 file changed

Lines changed: 114 additions & 61 deletions

File tree

.github/workflows/release.yml

Lines changed: 114 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ on:
44
push:
55
branches:
66
- main
7-
tags:
8-
- "v[0-9]+.*"
7+
workflow_dispatch: {}
98

109
permissions:
1110
contents: write
@@ -17,37 +16,48 @@ env:
1716

1817
jobs:
1918
# ------------------------------------------------------------------
20-
# 1. Create a draft GitHub release (tags only)
19+
# 0. Detect whether this is a release run
2120
# ------------------------------------------------------------------
22-
create-release:
23-
name: Create release
24-
if: startsWith(github.ref, 'refs/tags/')
21+
check-release:
22+
name: Check release
2523
runs-on: ubuntu-latest
2624
outputs:
27-
tag: ${{ steps.tag.outputs.tag }}
25+
is_release: ${{ steps.check.outputs.is_release }}
26+
version: ${{ steps.check.outputs.version }}
27+
version_major: ${{ steps.check.outputs.version_major }}
28+
version_minor: ${{ steps.check.outputs.version_minor }}
2829
steps:
2930
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2
31+
with:
32+
fetch-depth: 1
33+
fetch-tags: true
3034

31-
- name: Get tag
32-
id: tag
33-
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
34-
35-
- name: Verify tag matches Cargo.toml version
35+
- name: Detect release
36+
id: check
3637
run: |
37-
TAG="${{ steps.tag.outputs.tag }}"
38-
CARGO_VERSION="v$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')"
39-
if [ "$TAG" != "$CARGO_VERSION" ]; then
40-
echo "::error::Tag $TAG does not match Cargo.toml version $CARGO_VERSION"
41-
exit 1
38+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
39+
VERSION="v$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')"
40+
RAW="${VERSION#v}"
41+
MAJOR=$(echo "$RAW" | cut -d. -f1)
42+
MINOR="${MAJOR}.$(echo "$RAW" | cut -d. -f2)"
43+
echo "is_release=true" >> "$GITHUB_OUTPUT"
44+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
45+
echo "version_major=$MAJOR" >> "$GITHUB_OUTPUT"
46+
echo "version_minor=$MINOR" >> "$GITHUB_OUTPUT"
47+
48+
if git tag -l "$VERSION" | grep -q .; then
49+
echo "::error::Tag $VERSION already exists"
50+
exit 1
51+
fi
52+
echo "Detected release: $VERSION"
53+
else
54+
echo "is_release=false" >> "$GITHUB_OUTPUT"
55+
echo "version=" >> "$GITHUB_OUTPUT"
56+
echo "Not a release run"
4257
fi
4358
44-
- name: Create draft release
45-
env:
46-
GH_TOKEN: ${{ github.token }}
47-
run: gh release create "${{ steps.tag.outputs.tag }}" --draft --verify-tag --title "${{ steps.tag.outputs.tag }}"
48-
4959
# ------------------------------------------------------------------
50-
# 2. Build binaries for each target (always)
60+
# 1. Build binaries for each target (always)
5161
# ------------------------------------------------------------------
5262
build-binaries:
5363
name: Build ${{ matrix.name }}
@@ -148,17 +158,17 @@ jobs:
148158
echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT"
149159
echo "checksum=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT"
150160
151-
- name: Upload to release
152-
if: startsWith(github.ref, 'refs/tags/')
153-
env:
154-
GH_TOKEN: ${{ github.token }}
155-
run: |
156-
gh release upload "${GITHUB_REF#refs/tags/}" \
157-
"${{ steps.package.outputs.archive }}" \
158-
"${{ steps.package.outputs.checksum }}"
161+
- name: Upload build artifact
162+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7.0.0
163+
with:
164+
name: binary-${{ matrix.name }}
165+
path: |
166+
${{ steps.package.outputs.archive }}
167+
${{ steps.package.outputs.checksum }}
168+
retention-days: 1
159169

160170
# ------------------------------------------------------------------
161-
# 3. Build per-platform Docker images on native runners
171+
# 2. Build per-platform Docker images on native runners
162172
# ------------------------------------------------------------------
163173
docker-build:
164174
name: Docker ${{ matrix.platform }}
@@ -218,10 +228,11 @@ jobs:
218228
retention-days: 1
219229

220230
# ------------------------------------------------------------------
221-
# 3a. Build SIMD-optimized Docker images (single-arch each)
231+
# 2a. Build SIMD-optimized Docker images (single-arch each)
222232
# ------------------------------------------------------------------
223233
docker-build-simd:
224234
name: Docker ${{ matrix.name }}
235+
needs: [check-release]
225236
runs-on: ${{ matrix.runner }}
226237
strategy:
227238
fail-fast: false
@@ -267,9 +278,9 @@ jobs:
267278
with:
268279
images: ghcr.io/${{ env.IMAGE_NAME }}
269280
tags: |
270-
type=semver,pattern={{version}}-${{ matrix.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
271-
type=raw,value=latest-${{ matrix.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
272-
type=raw,value=dev-${{ matrix.tag_suffix }},enable=${{ github.ref == 'refs/heads/main' }}
281+
type=raw,value=${{ needs.check-release.outputs.version }}-${{ matrix.tag_suffix }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
282+
type=raw,value=latest-${{ matrix.tag_suffix }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
283+
type=raw,value=dev-${{ matrix.tag_suffix }}
273284
274285
- name: Build and push
275286
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # ratchet:docker/build-push-action@v7.0.0
@@ -285,11 +296,11 @@ jobs:
285296
cache-to: type=gha,scope=${{ matrix.name }},mode=max
286297

287298
# ------------------------------------------------------------------
288-
# 3b. Merge per-platform images into a multi-arch manifest
299+
# 2b. Merge per-platform images into a multi-arch manifest
289300
# ------------------------------------------------------------------
290301
docker-merge:
291302
name: Docker merge
292-
needs: docker-build
303+
needs: [check-release, docker-build]
293304
runs-on: ubuntu-latest
294305
steps:
295306
- name: Lowercase image name
@@ -308,11 +319,11 @@ jobs:
308319
with:
309320
images: ghcr.io/${{ env.IMAGE_NAME }}
310321
tags: |
311-
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/') }}
312-
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/') }}
313-
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/') }}
314-
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
315-
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }}
322+
type=raw,value=${{ needs.check-release.outputs.version }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
323+
type=raw,value=${{ needs.check-release.outputs.version_minor }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
324+
type=raw,value=${{ needs.check-release.outputs.version_major }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
325+
type=raw,value=latest,enable=${{ needs.check-release.outputs.is_release == 'true' }}
326+
type=raw,value=dev
316327
317328
- name: Set up Docker Buildx
318329
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4.0.0
@@ -332,12 +343,69 @@ jobs:
332343
$(printf 'ghcr.io/${{ env.IMAGE_NAME }}@sha256:%s ' *)
333344
334345
# ------------------------------------------------------------------
335-
# 4. Publish to crates.io (tags only)
346+
# 3. Create tag and draft release (release only, after all builds)
347+
# ------------------------------------------------------------------
348+
create-tag-and-release:
349+
name: Create tag and release
350+
if: needs.check-release.outputs.is_release == 'true'
351+
needs: [check-release, build-binaries, docker-merge, docker-build-simd]
352+
runs-on: ubuntu-latest
353+
outputs:
354+
tag: ${{ needs.check-release.outputs.version }}
355+
steps:
356+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2
357+
358+
- name: Create tag
359+
run: |
360+
git tag "${{ needs.check-release.outputs.version }}"
361+
git push origin "${{ needs.check-release.outputs.version }}"
362+
363+
- name: Extract changelog
364+
run: |
365+
RAW="${{ needs.check-release.outputs.version }}"
366+
RAW="${RAW#v}"
367+
awk '/^## \[Version '"$RAW"'\]/{found=1; next} /^## \[/{if(found) exit} found' CHANGELOG.md > /tmp/release-notes.md
368+
369+
- name: Create release
370+
env:
371+
GH_TOKEN: ${{ github.token }}
372+
run: |
373+
gh release create "${{ needs.check-release.outputs.version }}" \
374+
--target "${{ github.sha }}" \
375+
--title "RustQC ${{ needs.check-release.outputs.version }}" \
376+
--notes-file /tmp/release-notes.md
377+
378+
# ------------------------------------------------------------------
379+
# 4. Upload binaries to the release
380+
# ------------------------------------------------------------------
381+
upload-binaries:
382+
name: Upload binaries
383+
if: needs.check-release.outputs.is_release == 'true'
384+
needs: [check-release, create-tag-and-release, build-binaries]
385+
runs-on: ubuntu-latest
386+
steps:
387+
- name: Download all binary artifacts
388+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8.0.1
389+
with:
390+
pattern: binary-*
391+
path: /tmp/binaries
392+
merge-multiple: true
393+
394+
- name: Upload to release
395+
env:
396+
GH_TOKEN: ${{ github.token }}
397+
run: |
398+
gh release upload "${{ needs.create-tag-and-release.outputs.tag }}" \
399+
/tmp/binaries/*.tar.gz \
400+
/tmp/binaries/*.sha256
401+
402+
# ------------------------------------------------------------------
403+
# 5. Publish to crates.io (release only, after all builds)
336404
# ------------------------------------------------------------------
337405
publish-crate:
338406
name: Publish to crates.io
339-
if: startsWith(github.ref, 'refs/tags/')
340-
needs: [create-release, build-binaries]
407+
if: needs.check-release.outputs.is_release == 'true'
408+
needs: [check-release, create-tag-and-release]
341409
runs-on: ubuntu-latest
342410
permissions:
343411
id-token: write
@@ -353,18 +421,3 @@ jobs:
353421
run: cargo publish --no-verify
354422
env:
355423
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
356-
357-
# ------------------------------------------------------------------
358-
# 5. Publish the release (tags only)
359-
# ------------------------------------------------------------------
360-
publish-release:
361-
name: Publish release
362-
if: startsWith(github.ref, 'refs/tags/')
363-
needs: [create-release, build-binaries, docker-merge, docker-build-simd, publish-crate]
364-
runs-on: ubuntu-latest
365-
steps:
366-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2
367-
- name: Publish release
368-
env:
369-
GH_TOKEN: ${{ github.token }}
370-
run: gh release edit "${{ needs.create-release.outputs.tag }}" --draft=false

0 commit comments

Comments
 (0)