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
name: GitHub Release
# Cut a GitHub Release for a version. Triggered two ways:
# * `workflow_call` from auto-release.yml, right after it pushes the tag on a
# version bump (the automated path — see auto-release.yml). That commit
# already passed lint/test before landing on main (via cut-release.yml's
# `needs: [version, lint, test]` gate and/or PR branch protection), so
# re-running here would be redundant — skipped below.
# * a manual `v*` tag push (escape hatch for hotfixes cut by hand). Unlike
# the automated path, this can tag *any* commit, verified or not, so this
# is where the gap was (see #260): a release could be cut without ever
# checking cargo test/fmt/clippy. Fixed by calling lint.yml/test.yml
# (already workflow_call-callable) and gating `release` on them via
# `needs:`.
# Release notes are extracted from the matching version section in CHANGELOG.md
# so the published notes stay in sync with the changelog. Runs independently of
# publish.yml — neither blocks the other.
#
# The `lint`/`test` skip below keys off `inputs.tag`, not `github.event_name`:
# a *nested* `workflow_call` (this workflow, called from auto-release.yml,
# itself triggered by a `push` to main) inherits `github.event_name` from the
# chain's originating event — it reads `push` here too, not `workflow_call` —
# so `github.event_name == 'push'` was always true and never actually skipped
# anything. That silently ran this workflow's own `test.yml` call alongside
# publish.yml's identical call *and* the plain push-triggered `test.yml`, all
# three racing for the same `test-${{ github.ref }}` concurrency group with
# `cancel-in-progress: true` — auto-release.yml's `publish`/`release` jobs
# routinely lost that race. `inputs` is only populated on the `workflow_call`
# path (`required: true` there), so `inputs.tag == ''` reliably means "not
# called with a tag" — i.e. the direct tag-push escape hatch — regardless of
# what `github.event_name` reports.
on:
push:
tags:
- 'v*'
workflow_call:
inputs:
tag:
description: Tag to release (e.g. v0.14.0)
required: true
type: string
permissions:
contents: write
id-token: write # required for npm provenance in the called npm-packages workflow
jobs:
# Gate for the manual `push: tags` escape hatch only — see #260. Skipped
# entirely on the `workflow_call` path since that commit is already verified
# (see header comment above).
lint:
name: Lint tagged commit
if: inputs.tag == ''
uses: ./.github/workflows/lint.yml
test:
name: Test tagged commit
if: inputs.tag == ''
uses: ./.github/workflows/test.yml
release:
name: Create GitHub Release
needs:
# Run if lint/test passed (push path) or were skipped (workflow_call path).
# Do NOT use the default `success()` here: a skipped need would otherwise
# make this job skip too.
if: always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.test.result == 'success' || needs.test.result == 'skipped')
runs-on: ubuntu-latest
outputs:
tag: ${{ env.TAG }}
env:
TAG: ${{ inputs.tag || github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Extract changelog section for this tag
id: notes
run: |
VERSION=${TAG#v}
# Pull lines between '## [VERSION]' and the next '## [' heading.
awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { capture=1; next }
capture && /^## \[/ { exit }
capture { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "No changelog section found for [$VERSION]" >&2
exit 1
fi
echo "Release notes for $VERSION:"
cat release-notes.md
- name: Create release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
tag_name: ${{ env.TAG }}
name: ${{ env.TAG }}
body_path: release-notes.md
npm-packages:
name: Build and publish npm packages
needs: release
uses: ./.github/workflows/npm-packages.yml
with:
tag: ${{ needs.release.outputs.tag }}
publish: true
# Cross-compiled binary archives attached to the release created above, so `cargo binstall
# moadim` and users without a Rust toolchain have a prebuilt executable to download instead of
# `cargo install`'s from-source compile (#329). `cargo build --release` alone already embeds the
# UI: build.rs falls back to the committed prebuilt.html verbatim whenever pnpm isn't installed
# (see prebuilt.yml's header comment), which is exactly this job's case, so no extra UI-build
# step is needed here.
#
# Unix-only, matching the daemon itself (see #187's tmux/crontab dependency). Linux aarch64 is a
# nice-to-have per #329 and left for later — the three targets below cover the common case.
build-binaries:
name: Build binary (${{ matrix.target }})
needs: release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
env:
TAG: ${{ needs.release.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Package archive and checksum
id: package
run: |
VERSION=${TAG#v}
ARCHIVE="moadim-${VERSION}-${{ matrix.target }}.tar.gz"
tar -czf "$ARCHIVE" -C "target/${{ matrix.target }}/release" moadim
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
- name: Attach to release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
tag_name: ${{ env.TAG }}
files: |
${{ steps.package.outputs.archive }}
${{ steps.package.outputs.archive }}.sha256