-
Notifications
You must be signed in to change notification settings - Fork 115
239 lines (218 loc) · 9.14 KB
/
Copy pathcut-release.yml
File metadata and controls
239 lines (218 loc) · 9.14 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
# Cut a release from main via workflow_dispatch.
#
# Steps:
# 1. Resolve the next version (patch/minor/major, or an explicit version)
# 2. Promote CHANGELOG.md "## Unreleased" → "## vX.Y.Z - <date>"
# 3. Run tests
# 4. Commit CHANGELOG on main, create annotated tag, push both
# 5. Publish via GoReleaser (GitHub release + Homebrew) and npm
#
# The job runs on a macOS arm64 runner so GoReleaser can produce cgo builds
# of `xurl chat` for every platform chat-xdk supports: darwin targets build
# natively with Apple clang, and linux/amd64 cross-compiles with `zig cc`
# against chat-xdk's musl static library (see .goreleaser.yaml).
#
# No pull request is opened. GITHUB_TOKEN cannot create PRs unless the repo
# setting "Allow GitHub Actions to create and approve pull requests" is on,
# and the PR path is unnecessary for a release cut.
#
# Requirements:
# - Repo secret RELEASE_GITHUB_TOKEN: a PAT (or gh OAuth token) for a user who
# can bypass the main ruleset (org/repo admin). GITHUB_TOKEN cannot bypass
# rulesets — there is no "GitHub Actions" bypass actor for it.
# - npm is published by dispatching the existing Release workflow (release.yml),
# which is the OIDC trusted publisher configured on npmjs.com.
#
# Why publish here instead of relying on the tag-triggered Release workflow?
# Tag pushes authenticated with GITHUB_TOKEN do not start other workflows, so
# this job runs the publish steps itself after tagging.
#
# Usage (GitHub UI):
# Actions → Cut Release → Run workflow
#
# Usage (CLI):
# gh workflow run "Cut Release" -f bump=patch
# gh workflow run "Cut Release" -f version=1.2.3
# gh workflow run "Cut Release" -f bump=patch -f dry_run=true
# gh workflow run "Cut Release" -f version=1.2.3 -f skip_changelog=true
name: Cut Release
on:
workflow_dispatch:
inputs:
bump:
description: Semver bump when "version" is empty
type: choice
options:
- patch
- minor
- major
default: patch
version:
description: Explicit version (e.g. 1.2.3). Overrides bump. Optional.
required: false
type: string
default: ""
skip_changelog:
description: Skip CHANGELOG edits (tag + publish current main only)
type: boolean
default: false
dry_run:
description: Resolve version / validate only; no commit, tag, or publish
type: boolean
default: false
# Prevent two concurrent cuts from racing on tags / CHANGELOG.
concurrency:
group: cut-release
cancel-in-progress: false
permissions:
contents: write
actions: write # dispatch Release workflow for npm publish
jobs:
cut:
name: Cut ${{ inputs.version || inputs.bump }}
# macOS arm64: darwin cgo builds need Apple clang, and zig cross-compiles
# the linux/amd64 cgo build. Stub platforms cross-compile from anywhere.
runs-on: macos-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
# Prefer RELEASE_GITHUB_TOKEN when set so pushes can bypass rulesets
# that block the default GITHUB_TOKEN. Falls back to GITHUB_TOKEN.
token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
# zig is the C cross-compiler for the linux/amd64 cgo build.
- name: Set up Zig
uses: mlugg/setup-zig@v2
with:
version: "0.16.0"
- name: Resolve version and promote changelog
id: meta
run: |
set -euo pipefail
args=(--repo . --github-output "$GITHUB_OUTPUT" --bump "${{ inputs.bump }}")
if [ -n "${{ inputs.version }}" ]; then
args+=(--version "${{ inputs.version }}")
fi
if [ "${{ inputs.skip_changelog }}" = "true" ]; then
args+=(--skip-changelog)
fi
if [ "${{ inputs.dry_run }}" = "true" ]; then
args+=(--dry-run)
fi
python3 scripts/cut_release.py "${args[@]}"
- name: Run tests
run: go test ./...
- name: Dry-run summary
if: ${{ inputs.dry_run }}
run: |
{
echo "## Cut Release (dry-run)"
echo ""
echo "- version: \`${{ steps.meta.outputs.version }}\`"
echo "- tag: \`${{ steps.meta.outputs.tag }}\`"
echo "- changelog_changed: \`${{ steps.meta.outputs.changelog_changed }}\`"
echo ""
echo "No commit, tag, or publish was performed."
} >> "$GITHUB_STEP_SUMMARY"
- name: Configure git identity
if: ${{ !inputs.dry_run }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Commit changelog, tag, and push
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag ${TAG} already exists locally"
exit 1
fi
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
echo "::error::Tag ${TAG} already exists on origin"
exit 1
fi
# We checked out main at the start of the job (concurrency group prevents
# concurrent cuts). cut_release.py may have already rewritten CHANGELOG.md
# in the working tree — commit that directly; do not pull over the edit.
if [ "${{ steps.meta.outputs.changelog_changed }}" = "true" ]; then
if git diff --quiet -- CHANGELOG.md; then
echo "::error::Expected CHANGELOG.md changes but the working tree is clean"
exit 1
fi
git add CHANGELOG.md
git commit -m "chore(release): ${TAG}"
fi
git tag -a "$TAG" -m "${TAG}"
# Push main (if we committed) and the tag. This job owns publishing:
# GoReleaser runs below and npm is dispatched to the Release workflow.
# The Release workflow has no tag-push trigger, so this tag push does
# not spawn a second, racing GoReleaser run.
if ! git push origin HEAD:main "refs/tags/${TAG}"; then
{
echo "## Cut Release: push rejected"
echo ""
echo "Could not push to \`main\` / \`${TAG}\`. The workflow token cannot"
echo "bypass branch rules. Set repo secret \`RELEASE_GITHUB_TOKEN\` to a"
echo "PAT for an org/repo admin (who is on the ruleset bypass list), then re-run."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Push to main/${TAG} rejected. Ensure RELEASE_GITHUB_TOKEN is set to an admin PAT that can bypass the main ruleset."
# Drop the local tag so a re-run is clean if the remote rejected it.
git tag -d "$TAG" || true
exit 1
fi
echo "Pushed main and ${TAG} at $(git rev-parse HEAD)"
- name: Run GoReleaser
if: ${{ !inputs.dry_run }}
uses: goreleaser/goreleaser-action@v6
with:
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
# npm OIDC trusted publishing is bound to release.yml on npmjs.com.
# Publishing from this workflow file fails with ENEEDAUTH; dispatch instead.
- name: Publish npm via Release workflow
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }}
run: |
set -euo pipefail
VERSION="${{ steps.meta.outputs.version }}"
BEFORE=$(gh run list --workflow=Release --event=workflow_dispatch --limit 1 \
--json databaseId --jq '.[0].databaseId // empty')
gh workflow run Release --field version="$VERSION"
RUN_ID=""
for _ in $(seq 1 60); do
CANDIDATE=$(gh run list --workflow=Release --event=workflow_dispatch --limit 1 \
--json databaseId --jq '.[0].databaseId // empty')
if [ -n "$CANDIDATE" ] && [ "$CANDIDATE" != "$BEFORE" ]; then
RUN_ID="$CANDIDATE"
break
fi
sleep 2
done
if [ -z "$RUN_ID" ]; then
echo "::error::Timed out waiting for Release workflow_dispatch run"
exit 1
fi
echo "Watching Release run ${RUN_ID} for npm ${VERSION}…"
gh run watch "$RUN_ID" --exit-status
- name: Release summary
if: ${{ !inputs.dry_run }}
run: |
{
echo "## Cut Release complete"
echo ""
echo "- version: \`${{ steps.meta.outputs.version }}\`"
echo "- tag: \`${{ steps.meta.outputs.tag }}\`"
echo "- release: https://github.com/${{ github.repository }}/releases/tag/${{ steps.meta.outputs.tag }}"
echo "- npm: \`@xdevplatform/xurl@${{ steps.meta.outputs.version }}\`"
} >> "$GITHUB_STEP_SUMMARY"