-
Notifications
You must be signed in to change notification settings - Fork 26
286 lines (258 loc) · 12.5 KB
/
Copy pathrelease-please.yaml
File metadata and controls
286 lines (258 loc) · 12.5 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
name: Release Please
# Single release pipeline for the whole repo. Day-to-day work lands on `main`
# (all PRs merge straight into main). release-please watches `main` and maintains
# ONE grouped *Release PR* that bumps web/package.json + web/CHANGELOG.md and
# cli/CHANGELOG.md together. The `linked-versions` plugin keeps web and cli in
# lockstep: any release-worthy commit bumps BOTH to the same version, even when
# only one side changed. Merging that Release PR creates both `web-vX.Y.Z` and
# `cli-vX.Y.Z` tags.
#
# Downstream of the tags, in this same run:
# - web: build + deploy the tagged commit to classroom50.org (GitHub Pages),
# and mark web-v* as the repo's "Latest" release.
# - cli: dispatch cli-release.yaml with the cli-v* tag to build the extensions
# and publish binaries to foundation50/gh-teacher + gh-student.
#
# Token model: release-please runs with GITHUB_TOKEN. Every write it makes is to
# THIS repo (tags, Releases, Pages), which GITHUB_TOKEN can do. The one thing it
# can't do is start cli-release.yaml: a GITHUB_TOKEN-pushed tag does NOT trigger
# other workflows (Actions' recursion guard), so we dispatch cli-release.yaml
# explicitly below (at the cli-v* tag) and wait on it, so a failed CLI publish
# fails this release instead of reporting green. The cross-repo binary publish
# still uses CLI_RELEASE_PAT — but only inside cli-release.yaml, where the writes
# to gh-teacher/gh-student actually happen. (Opening the Release PR requires
# "Allow GitHub Actions to create and approve pull requests".)
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
actions: write
concurrency:
group: release-please
cancel-in-progress: false
jobs:
release-please:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
# Effective release tags for this run (see "Resolve effective release
# tags"). Empty => no release for this commit, so deploy/publish are gated
# off. Resolving them separately (vs reading steps.release.outputs) lets a
# re-run finish a release whose tags release-please already created — a
# transient failure downstream of tagging used to strand the release AND
# report green on re-run (empty tag_name => everything skipped).
web_tag: ${{ steps.tags.outputs.web_tag }}
cli_tag: ${{ steps.tags.outputs.cli_tag }}
steps:
- uses: actions/checkout@v7
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
target-branch: main
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
- name: Debug release-please outputs
env:
RP_OUTPUTS: ${{ toJSON(steps.release.outputs) }}
run: echo "$RP_OUTPUTS"
# Prefer release-please's own tag outputs. When they're empty this can be
# either (a) a plain push with no release, or (b) a re-run of the run that
# released, where the release commit is already tagged so release-please
# reports nothing. Distinguish them: reconstruct web-v<ver>/cli-v<ver> from
# the manifest and adopt them ONLY if the tag exists AND points at THIS
# commit (github.sha). A normal push (tag absent or on another commit) thus
# still yields empty tags and never deploys — while a re-run of the release
# run resumes and completes it idempotently.
- name: Resolve effective release tags
id: tags
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RP_WEB_TAG: ${{ steps.release.outputs['web--tag_name'] }}
RP_CLI_TAG: ${{ steps.release.outputs['cli--tag_name'] }}
run: |
set -euo pipefail
# Emit a resolved tag for a package: pass through release-please's tag,
# else fall back to the manifest tag if it exists on THIS commit. A
# false trailing `if` returns 0, so the no-release path yields an empty
# tag without tripping `set -e`; keep that `if` last.
resolve() {
local pkg="$1" rp_tag="$2"
if [ -n "$rp_tag" ]; then
printf '%s' "$rp_tag"
return
fi
local ver
ver="$(jq -r --arg p "$pkg" '.[$p] // empty' .release-please-manifest.json)"
[ -n "$ver" ] || return 0
local tag="${pkg}-v${ver}"
local tag_sha
tag_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" \
--jq '.object.sha' 2>/dev/null || true)"
# Annotated tags resolve to a tag object; dereference to the commit.
if [ -n "$tag_sha" ]; then
local obj_type
obj_type="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_sha}" \
--jq '.object.sha' 2>/dev/null || true)"
[ -n "$obj_type" ] && tag_sha="$obj_type"
fi
if [ "$tag_sha" = "$GITHUB_SHA" ]; then
printf '%s' "$tag"
fi
}
web_tag="$(resolve web "$RP_WEB_TAG")"
cli_tag="$(resolve cli "$RP_CLI_TAG")"
echo "web_tag=$web_tag" >>"$GITHUB_OUTPUT"
echo "cli_tag=$cli_tag" >>"$GITHUB_OUTPUT"
echo "Resolved web_tag='$web_tag' cli_tag='$cli_tag' (sha $GITHUB_SHA)"
# "Latest" on this repo tracks the web app (classroom50.org). release-please
# marks every release it creates as latest (no make_latest option —
# googleapis/release-please#2317), so pin it explicitly: web is latest, the
# cli-v* release never is.
- name: Pin the Latest release to web
if: ${{ steps.tags.outputs.web_tag != '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WEB_TAG: ${{ steps.tags.outputs.web_tag }}
CLI_TAG: ${{ steps.tags.outputs.cli_tag }}
run: |
set -euo pipefail
if [ -n "$CLI_TAG" ]; then
gh release edit "$CLI_TAG" --repo "$GITHUB_REPOSITORY" --latest=false
fi
gh release edit "$WEB_TAG" --repo "$GITHUB_REPOSITORY" --latest=true
# A GITHUB_TOKEN-pushed cli-v* tag won't trigger cli-release.yaml (recursion
# guard), so kick it off explicitly. cli-release.yaml holds CLI_RELEASE_PAT
# and does the cross-repo publish to gh-teacher/gh-student.
#
# Dispatch AT the cli-v* tag (not main): cli-release.yaml's checkout then
# builds the released commit and stamps github.sha from it, matching the
# web deploy's "build the tag, not the main tip" contract. Then block on
# the dispatched run so a failed CLI publish fails this release rather than
# reporting green while gh-teacher/gh-student got no binaries.
- name: Publish CLI extensions for the new tag
if: ${{ steps.tags.outputs.cli_tag != '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLI_TAG: ${{ steps.tags.outputs.cli_tag }}
run: |
set -euo pipefail
# Retry a gh call through transient GitHub-side failures (e.g. a TLS
# cert blip once stranded a release here). Fail only after retries.
gh_retry() {
local n=0 max=5 delay=5
until gh "$@"; do
n=$((n + 1))
if [ "$n" -ge "$max" ]; then
echo "::error::gh $* failed after $max attempts." >&2
return 1
fi
echo "gh $* failed (attempt $n/$max); retrying in ${delay}s…" >&2
sleep "$delay"
delay=$((delay * 2))
done
}
# Idempotent: on a re-run, a successful cli-release for this tag may
# already exist — reuse it instead of dispatching a duplicate. If one
# is still in progress, wait on it; otherwise dispatch a fresh run.
existing="$(gh_retry run list --workflow cli-release.yaml --repo "$GITHUB_REPOSITORY" \
--limit 30 --json databaseId,headBranch,status,conclusion \
--jq "[.[] | select(.headBranch == \"${CLI_TAG}\")] | first // empty")"
run_id=""
if [ -n "$existing" ]; then
status="$(printf '%s' "$existing" | jq -r '.status')"
conclusion="$(printf '%s' "$existing" | jq -r '.conclusion')"
run_id="$(printf '%s' "$existing" | jq -r '.databaseId')"
if [ "$status" = "completed" ] && [ "$conclusion" = "success" ]; then
echo "cli-release for $CLI_TAG already succeeded (run $run_id); nothing to publish."
exit 0
fi
if [ "$status" = "completed" ]; then
echo "Prior cli-release for $CLI_TAG ended '$conclusion'; dispatching a fresh run."
run_id=""
else
echo "cli-release for $CLI_TAG already in progress (run $run_id); waiting on it."
fi
fi
if [ -z "$run_id" ]; then
# Record the newest existing cli-release run so we can identify the
# one we're about to create (gh workflow run returns no run id).
before="$(gh_retry run list --workflow cli-release.yaml --repo "$GITHUB_REPOSITORY" \
--limit 1 --json databaseId --jq '.[0].databaseId // 0')"
gh_retry workflow run cli-release.yaml --repo "$GITHUB_REPOSITORY" --ref "$CLI_TAG" -f tag="$CLI_TAG"
# Poll for the run dispatch created after `before`. Registration is
# not instantaneous, so retry briefly before giving up.
for _ in $(seq 1 30); do
run_id="$(gh_retry run list --workflow cli-release.yaml --repo "$GITHUB_REPOSITORY" \
--event workflow_dispatch --limit 10 \
--json databaseId --jq "[.[].databaseId | select(. > ${before})] | max // empty")"
[ -n "$run_id" ] && break
sleep 5
done
if [ -z "$run_id" ]; then
echo "::error::Dispatched cli-release.yaml but could not find the new run to wait on."
exit 1
fi
fi
echo "Waiting on cli-release run $run_id ($CLI_TAG)…"
# Exit non-zero (failing this release) if the CLI publish run fails.
# Not wrapped in gh_retry: a failed run must fail fast, not be re-watched.
gh run watch "$run_id" --repo "$GITHUB_REPOSITORY" --exit-status
# Build + deploy the freshly tagged web release to production. Gated on a real
# web release (non-empty tag => Release PR was merged) so a plain push to main
# never deploys. Mirrors web-deploy.yaml's build so the tagged commit ships
# stamped with its version.
deploy:
needs: release-please
if: ${{ needs.release-please.outputs.web_tag != '' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: pages
cancel-in-progress: false
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v7
with:
# Check out the tag release-please just created so the build stamps
# the released commit + version, not a later main tip.
ref: ${{ needs.release-please.outputs.web_tag }}
# Shared gate: setup + deps + node test suite (incl. skeleton parity). See
# .github/actions/web-build-gate. Runs in the tag checkout so the build
# stamps the released commit.
- uses: ./.github/actions/web-build-gate
# Resolve the SHA of the checked-out tag so the build stamps the commit
# that was actually released, not github.sha (the main tip that triggered
# the run, which can differ from the release commit).
- id: sha
run: echo "value=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- run: npm run build
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
# GitHub proxy base; falls back to the baked-in default when unset.
VITE_GITHUB_PROXY_BASE: ${{ vars.VITE_GITHUB_PROXY_BASE }}
# Tag name (web-vX.Y.Z); vite.config.ts strips the web-v prefix.
VITE_APP_VERSION: ${{ needs.release-please.outputs.web_tag }}
VITE_APP_COMMIT: ${{ steps.sha.outputs.value }}
- name: Add SPA fallback
run: cp dist/index.html dist/404.html
- name: Write CNAME into dist
run: cp CNAME dist/CNAME
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: web/dist
- id: deployment
uses: actions/deploy-pages@v5