Repository navigation
265 lines (243 loc) · 11.7 KB
/
Copy pathrelease.yml
File metadata and controls
265 lines (243 loc) · 11.7 KB
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
name: Build & Release
# Primary CI gate. Runs `just check-ci` + cross-compiles all four release
# binaries on every push to main and every PR — proving the release artifact
# step still works on every commit, not just at release time.
#
# Publishing is opt-in via commit message:
# - `[release]` on a main commit → publish after the gate passes.
# - workflow_dispatch with publish=true → manual override.
#
# Publishes are immutable CalVer releases: the tag is vYYYY.MM.DD (UTC),
# with a .N suffix when the day already has one. Release notes are generated
# by git-cliff (cliff.toml) from the Conventional Commit subjects since the
# previous CalVer tag; commit subjects are lint-checked on every run so the
# notes can't silently degrade. GitHub's native
# `releases/latest` alias serves install.sh and self-update, so nothing
# is ever deleted or overwritten — old versions stay downloadable for
# rollback (`gh release download vYYYY.MM.DD`). A temporary bridge step
# additionally refreshes the old rolling `latest` tag for pre-CalVer
# binaries still polling it; see that step's comment for removal criteria.
#
# The Environment Scenarios workflow is fully decoupled: it runs weekly and
# on opt-in (`[scenarios]` in the commit message). If you want a release
# that's also been validated against the scenario matrix, drop both
# `[release]` and `[scenarios]` into the same commit.
#
# Bracket markers (rather than UPPERCASE phrases) so a commit message that
# describes the trigger in prose doesn't accidentally fire it. Match style
# follows the existing `[skip ci]` convention.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
publish:
description: "Publish a release (otherwise the run is a test build only)"
type: boolean
required: false
default: false
# main runs never cancel — a release commit's build must finish even if
# another push lands on top of it. PR runs cancel on push, which is normal.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions:
contents: write
jobs:
build:
runs-on: macos-latest
# Skip [skip ci] / [ci skip] commits. head_commit is undefined on PRs;
# `contains(null, …)` evaluates to false, so PRs always run.
if: >
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]')
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# Full history + tags: git-cliff walks back to the previous CalVer
# tag for release notes, and the commit lint below checks a range.
fetch-depth: 0
fetch-tags: true
- name: Lint commit messages
# Enforce the commit convention (CLAUDE.md; scripts/lint-commits.sh —
# same logic as the local commit-msg hook, for machines without it).
# Push: everything in this push event. PR: the branch commits.
# github.event.before is all-zeros on a newly created ref — fall back
# to the head commit alone. Runs before the gate: it's the cheapest
# check and a bad subject would land in the published release notes.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
elif [ "${{ github.event_name }}" = "push" ] && ! echo "${{ github.event.before }}" | grep -qE '^0*$'; then
RANGE="${{ github.event.before }}..${{ github.sha }}"
else
RANGE="${{ github.sha }}~1..${{ github.sha }}"
fi
./scripts/lint-commits.sh "$RANGE"
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Cache Homebrew downloads
# Caches bottle downloads under $(brew --cache). On hit, the brew
# install step below skips the network fetch and only links — the
# formulae below collectively pull ~hundreds of MB of bottles.
# The cache key hashes this workflow file so it auto-invalidates
# when the formula list changes; restore-keys give a partial hit
# for any prior release-cache.
uses: actions/cache@v4
with:
path: ~/Library/Caches/Homebrew
key: brew-cache-${{ runner.os }}-release-${{ hashFiles('.github/workflows/release.yml') }}
restore-keys: |
brew-cache-${{ runner.os }}-release-
- name: Install tools
# Go dev tools (goimports, golangci-lint, gopls, gofumpt, dlv) are
# pinned in go.mod's `tool` block and installed via `go install tool`
# — keeps local↔CI versions identical with no drift.
# just — drives the local↔CI gate (justfile `check-ci` recipe).
# rg/fd/jaq/ast-grep — used by scripts/lint-guard.sh under
# set -euo pipefail, so a missing binary aborts the rule script.
# duckdb — required dep (internal/doctor/Brewfile); internal/duck +
# `sci db` parity tests need it on PATH.
run: |
go install tool
brew install just ripgrep fd jaq ast-grep duckdb git-cliff
- name: Gate (just check-ci)
# Single source of truth with the local gate: tidy/fmt/vet/lint/
# lint-guard/test -race. Add a step in justfile's `check-ci`, it
# lands here automatically.
run: just check-ci
- name: Compute release version (CalVer)
id: ver
if: >
(github.event_name == 'workflow_dispatch' && inputs.publish) ||
(github.event_name == 'push' &&
contains(github.event.head_commit.message, '[release]'))
# vYYYY.MM.DD in UTC; if that tag already exists (same-day follow-up
# release), append .1, .2, … — the self-updater orders these suffixes
# numerically. Zero-padding matters: internal/selfupdate's tag parser
# requires it.
run: |
BASE="v$(date -u +%Y.%m.%d)"
VERSION="${BASE}"
N=0
while gh release view "${VERSION}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; do
N=$((N+1))
VERSION="${BASE}.${N}"
done
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Releasing as ${VERSION}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
run: |
COMMIT="${GITHUB_SHA}"
# Dev=false marks this as the released build — the only one allowed
# to tell users an update is available (see internal/version).
LDFLAGS="-s -w -X github.com/sciminds/sci/internal/version.Commit=${COMMIT} -X github.com/sciminds/sci/internal/version.Dev=false"
# Publishing runs stamp the CalVer tag as the binary's orderable
# identity; test-only builds carry no version.
VERSION="${{ steps.ver.outputs.version }}"
if [ -n "${VERSION}" ]; then
LDFLAGS="${LDFLAGS} -X github.com/sciminds/sci/internal/version.Version=${VERSION}"
fi
mkdir -p dist
for PAIR in darwin/arm64 darwin/amd64 linux/arm64 linux/amd64; do
OS="${PAIR%/*}"
ARCH="${PAIR#*/}"
SUFFIX="${OS}-${ARCH}"
echo "==> Building sci-${SUFFIX}"
GOOS="${OS}" GOARCH="${ARCH}" go build -ldflags="${LDFLAGS}" -o "dist/sci-${SUFFIX}" ./cmd/sci
done
echo "==> Verify native build"
./dist/sci-darwin-arm64 --help || ./dist/sci-darwin-amd64 --help
- name: Compute SHA256 checksums
# Emit one **SHA256(<asset>):** line per sci-* binary into the release
# body (via /tmp/sha256-lines.txt, consumed by the compose and bridge
# steps). `sci update` parses these and refuses to install a binary
# whose hash doesn't match.
run: |
{
for f in dist/sci-*; do
base="$(basename "$f")"
sum="$(shasum -a 256 "$f" | awk '{print $1}')"
echo "**SHA256(${base}):** ${sum}"
done
} | tee /tmp/sha256-lines.txt
- name: Compose release notes
if: >
(github.event_name == 'workflow_dispatch' && inputs.publish) ||
(github.event_name == 'push' &&
contains(github.event.head_commit.message, '[release]'))
# git-cliff renders everything since the previous CalVer tag, grouped
# per cliff.toml (Breaking/Features/Fixes/…). The sed drops cliff's
# own `## vX` heading — the release title already carries the version.
# The **Commit:** and **SHA256(...):** lines are a parsing contract
# with internal/selfupdate — notes go above them, never instead.
run: |
git-cliff --unreleased --tag "${VERSION}" --strip all \
| sed -e '1{/^## /d;}' -e '/./,$!d' > /tmp/release-notes.md
{
cat /tmp/release-notes.md
echo
echo "**Commit:** ${GITHUB_SHA}"
echo
cat /tmp/sha256-lines.txt
echo
echo "**Install:** \`curl -fsSL https://raw.githubusercontent.com/sciminds/sci/main/install.sh | sh\`"
} > /tmp/release-body.md
env:
VERSION: ${{ steps.ver.outputs.version }}
- name: Create GitHub Release
if: >
(github.event_name == 'workflow_dispatch' && inputs.publish) ||
(github.event_name == 'push' &&
contains(github.event.head_commit.message, '[release]'))
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: sci ${{ steps.ver.outputs.version }}
files: dist/*
make_latest: true
draft: false
body_path: /tmp/release-body.md
# TEMPORARY (added 2026-08-03): pre-CalVer binaries in the wild poll
# releases/tags/latest and decide by commit-SHA inequality, so if the
# rolling `latest` release disappeared they would 404 quietly and never
# learn another update exists. Republish the alias alongside each
# versioned release (same assets, same body contract) so they see one
# more "update available" and self-update onto a CalVer binary.
# --latest=false keeps the versioned release as GitHub's "latest
# release" (which install.sh and the new self-updater resolve).
# Remove this step — and delete the `latest` tag — once every machine
# runs a CalVer build (sci --version shows a vYYYY.MM.DD tag).
- name: Bridge release for pre-CalVer binaries (temporary)
if: >
(github.event_name == 'workflow_dispatch' && inputs.publish) ||
(github.event_name == 'push' &&
contains(github.event.head_commit.message, '[release]'))
run: |
{
echo "Transitional alias for pre-CalVer binaries — releases are now CalVer-tagged (this build: ${VERSION})."
echo
echo "**Commit:** ${GITHUB_SHA}"
echo
cat /tmp/sha256-lines.txt
echo
echo "**Install:** \`curl -fsSL https://raw.githubusercontent.com/sciminds/sci/main/install.sh | sh\`"
} > /tmp/bridge-notes.md
gh release delete latest --yes --cleanup-tag 2>/dev/null || true
gh release create latest dist/* \
--title "sci latest (transition alias)" \
--target "${GITHUB_SHA}" \
--latest=false \
--notes-file /tmp/bridge-notes.md
env:
VERSION: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}