-
Notifications
You must be signed in to change notification settings - Fork 26
230 lines (213 loc) · 8.88 KB
/
Copy pathcli-release.yaml
File metadata and controls
230 lines (213 loc) · 8.88 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
name: CLI extension release
# Cross-compiles the two `gh` CLI extensions and publishes per-platform
# binaries as Releases on the standalone extension repos (gh-teacher,
# gh-student) so users can `gh extension install foundation50/gh-teacher`.
#
# Why build here and push out (vs per-repo gh-extension-precompile): the
# relative `replace => ../shared` already yields self-contained binaries, so
# there is one source of truth and no need to publish cli/shared. Build
# provenance is attested so the source-less target repos' binaries are
# verifiable (see wiki/Installation.md for the verify command).
#
# Triggered by release-please.yaml, which dispatches this workflow with the
# `cli-vX.Y.Z` tag after creating it (a GITHUB_TOKEN-pushed tag can't fire the
# `push: tags` trigger below — Actions' recursion guard — so the tag trigger is
# a manual fallback). VERSION is the tag with `cli-` stripped (the prefix scopes
# it apart from the `v1` branch / web tags). A VERSION containing `-`
# (v1.0.0-rc.1) publishes as a prerelease.
#
# Setup (repo Settings, before the first release):
# - Secret `CLI_RELEASE_PAT`: a dedicated fine-grained PAT, `contents:write`
# on gh-teacher + gh-student only, short expiry. Not MIRROR_PAT — separate
# scopes limit blast radius.
# - Environment `cli-release` (no required reviewers): the `publish` jobs run
# in it so the PAT is exposed only to those jobs, but publishing is
# automatic on release — release authority == merging the Release PR.
# - Target repos must be named exactly gh-teacher / gh-student for
# `gh extension install <owner>/<repo>` to resolve.
on:
push:
tags: ['cli-v*']
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., cli-v1.0.0). VERSION is this with the cli- prefix stripped.'
required: true
type: string
attest:
description: 'Attach build provenance (needs a public repo or supported plan).'
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: cli-release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
# Resolve VERSION once so the cli- tag scheme has a single source of truth.
resolve:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- name: Resolve VERSION
id: ver
# Bind the untrusted tag to env and use the shell var — never
# interpolate ${{ }} into run: (script injection; the dispatch input
# bypasses the cli-v* push filter).
env:
TAG: ${{ github.event.inputs.tag || github.ref_name }}
run: |
set -euo pipefail
version="${TAG#cli-}"
if [ "$version" = "$TAG" ]; then
echo "::error::Tag '$TAG' does not start with the 'cli-' prefix; expected e.g., cli-v1.0.0."
exit 1
fi
# Restrict VERSION to a semver shape and charset before it reaches
# ldflags, the asset filename, and the release tag.
case "$version" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*)
echo "::error::VERSION '$version' is not a vMAJOR.MINOR.PATCH[-prerelease] release tag (got from tag '$TAG')."
exit 1
;;
esac
case "$version" in
*[!0-9A-Za-z.+_-]*)
echo "::error::VERSION '$version' contains characters outside the allowed semver set [0-9A-Za-z.+_-] (got from tag '$TAG')."
exit 1
;;
esac
echo "version=$version" >>"$GITHUB_OUTPUT"
build:
needs: resolve
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
# Independent per leg: one target's failure must not cancel the rest.
fail-fast: false
matrix:
extension: [gh-teacher, gh-student]
target:
- { os: darwin, arch: arm64 }
- { os: darwin, arch: amd64 }
- { os: linux, arch: amd64 }
- { os: linux, arch: arm64 }
- { os: windows, arch: amd64 }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: cli/${{ matrix.extension }}/go.mod
cache-dependency-path: cli/${{ matrix.extension }}/go.sum
- name: Build
working-directory: cli/${{ matrix.extension }}
env:
GOWORK: 'off'
CGO_ENABLED: '0'
GOOS: ${{ matrix.target.os }}
GOARCH: ${{ matrix.target.arch }}
VERSION: ${{ needs.resolve.outputs.version }}
# Short commit and UTC build date, baked into --version for support
# and bug reports (see cli/gh-*/main.go versionString).
COMMIT: ${{ github.sha }}
run: |
set -euo pipefail
ext=""
if [ "$GOOS" = "windows" ]; then ext=".exe"; fi
# Canonical gh precompiled-extension asset name: gh-<name>_<os>-<arch>.
# The version lives in the release tag, not the filename, so `gh
# extension install`/`upgrade` resolve the per-platform binary.
out="${GITHUB_WORKSPACE}/dist/${{ matrix.extension }}_${GOOS}-${GOARCH}${ext}"
mkdir -p "$(dirname "$out")"
short_commit="${COMMIT:0:12}"
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
go build -trimpath \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${short_commit} -X main.date=${build_date}" \
-o "$out" .
ls -l "$out"
- name: Upload binary artifact
uses: actions/upload-artifact@v7
with:
name: dist-${{ matrix.extension }}-${{ matrix.target.os }}-${{ matrix.target.arch }}
path: dist/*
if-no-files-found: error
retention-days: 7
publish:
needs: [resolve, build]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: cli-release
permissions:
contents: read
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
extension: [gh-teacher, gh-student]
steps:
- name: Download this extension's binaries
uses: actions/download-artifact@v8
with:
pattern: dist-${{ matrix.extension }}-*
merge-multiple: true
path: dist
- name: Generate checksums
working-directory: dist
run: |
set -euo pipefail
sha256sum gh-* >checksums.txt
cat checksums.txt
# Provenance attestation requires a public repo (or an upgraded plan);
# on a private repo the API rejects it. Attest on public repos, or when a
# dispatch run forces it; otherwise skip so testing still publishes.
- name: Attest build provenance
if: ${{ !github.event.repository.private || github.event.inputs.attest == 'true' }}
uses: actions/attest-build-provenance@v4
with:
subject-path: 'dist/gh-*'
- name: Note skipped attestation
if: ${{ github.event.repository.private && github.event.inputs.attest != 'true' }}
run: echo "::warning::Provenance attestation skipped (private repo). The release publishes without provenance; run on the public repo to attach it."
- name: Publish release
env:
GH_TOKEN: ${{ secrets.CLI_RELEASE_PAT }}
REPO: foundation50/${{ matrix.extension }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::CLI_RELEASE_PAT secret is missing. Create a fine-grained PAT with contents:write on foundation50/gh-teacher and foundation50/gh-student and add it as repository secret CLI_RELEASE_PAT."
exit 1
fi
prerelease="false"
case "$VERSION" in
*-*) prerelease="true" ;;
esac
# Crash-safe idempotency: ensure the release exists (notes generated
# once), reconcile prerelease state, then always clobber assets — so a
# run that died mid-upload is repaired on re-run without dup notes.
if ! gh release view "$VERSION" --repo "$REPO" >/dev/null 2>&1; then
gh release create "$VERSION" \
--repo "$REPO" \
--title "$VERSION" \
--generate-notes \
--prerelease="$prerelease"
else
echo "Release $VERSION already exists on $REPO; reconciling metadata and assets."
gh release edit "$VERSION" --repo "$REPO" --prerelease="$prerelease"
fi
gh release upload "$VERSION" dist/* --repo "$REPO" --clobber
# gh-teacher and gh-student ship together — fail the run if only one leg
# published, so cross-repo version skew can't pass silently.
publish-complete:
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Assert both extensions published
run: echo "Both gh-teacher and gh-student published successfully."