Repository navigation
372 lines (344 loc) · 19.7 KB
/
Copy pathvale-upgrade.yml
File metadata and controls
372 lines (344 loc) · 19.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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# SPDX-License-Identifier: MIT
# Vale — ship the platform packages we already published.
#
# THE STAGE THAT WAS MISSING. Vendoring Vale has two halves, and until now only
# one of them was automated:
#
# republish release-vale.yml. Watches upstream Vale, proposes a manifest
# with new digests on `vendor/vale/republish`, and on merge
# publishes the six @taskless/vale-* packages at
# <valeVersion>-<stamp>. Changes nothing for a consumer.
#
# upgrade this workflow. Moves the six pins in packages/cli/package.json
# to the published set on `vendor/vale/upgrade`. THIS is what
# reaches a user.
#
# release-vale.yml states the dependency in its own header: "a new version
# reaches a user only when someone reviews a bump to that pin". Somebody always
# did — `git log` on the pins shows 3.18.0, 3.19.0 and 3.20.0 each moved in its
# own hand-written commit, and the pins are current as this is written. So this
# automates a step that was working, which is worth being honest about: what it
# removes is the dependency on remembering, and what it adds is the changelog
# next to the diff. The failure it prevents is quiet — a publish lands, nobody
# notices the pins are behind, and the release simply does not ship.
#
# ast-grep has only the upgrade half, because nothing here repackages it; there
# is no `vendor/ast-grep/republish` and its absence is the point. Beyond that
# the two upgrades are the same operation, and they share the same scripts:
# `pin-bump.cjs` rewrites the pins, `release-notes.cjs` renders the changelog,
# and `vendor-pr.cjs` owns the rolling branch and its force-push guards. What
# differs is which registry answers "what is published" and which tag carries
# the release notes.
#
# THE CHANGELOG HERE IS UPSTREAM'S, NOT OURS. Our stamp records when a package
# was built, which tells a reviewer nothing about what changed. The base version
# inside the stamp is the release upstream actually tagged, and that is what
# gets fetched.
#
# THE BOT COMMIT IS SELF-CONSISTENT, AND CHECKED BEFORE IT IS PUSHED. Moving the
# pins alone is not a green commit: `VALE_VERSION` in
# packages/cli/src/rules/capabilities.ts is maintained by hand and
# test/engine-version-consistency.test.ts holds it to the pins (deliberately —
# read that test's docblock before deriving the constant from package.json),
# and src/generated/vale-vocabulary.ts is derived from the binary and
# typechecked against the constant. A commit that moved only the pins therefore
# failed Validate BY CONSTRUCTION. Measured on #368: the bump was red before
# anyone read it, the child #372 had to carry the constant, the regenerated
# vocabulary and a test literal along with its real work, and the stack could
# not use GitHub's landing because its base was red.
#
# So this job now writes everything the consistency check compares — the pins,
# the constant, the lockfile, and the vocabulary regenerated from the new
# binary — and runs that check itself before `vendor-pr.cjs` pushes. If it
# fails, the run fails and nothing is proposed, which is the correct outcome:
# pushing an inconsistent commit is the thing being removed. What stays for a
# human child pull request is judgment, not bookkeeping: the vendor contract
# (the measured `Vale <version>` block in vale-vendor-contract.test.ts and the
# verdict corpus in vale-schema-contract.test.ts), the `update` ledger, any
# migration, and the changeset prose. Those tests are NOT gated here, on
# purpose: see the check step. They ARE run, and their verdict is written into
# the pull request body, because Validate does not run on a pull request this
# job opens until a maintainer approves it (see the report step).
#
# Action refs are pinned to commit SHAs; the trailing comment records the tag.
name: Upgrade Vale
on:
# THE MANIFEST COMMIT, not a clock. The only event that makes the pins fall
# behind is a republish, and a republish starts exactly when a reviewed
# manifest change lands on main — the merge of a release-vale.yml detect pull
# request. This is the same trigger release-vale.yml publishes on, so the two
# halves start from the same commit rather than this one polling for the
# other's result.
#
# Starting together is also the race. release-vale.yml has to fetch, verify,
# pack, and publish six packages before the registry can answer "what is
# published" with the new set, and the detect step below asks the registry.
# Measured on 3.22.0: the manifest merged at 18:09:13Z, the publish run
# started three seconds later, the packages were stamped 18:09:30 — and a run
# of this workflow started at 18:09 would have compared the pins against the
# OLD latest, found nothing to do, and exited clean. The wait step before
# detect exists for that: vale-upgrade-wait.cjs holds until the `Release
# Vale` run for this same commit has concluded, then until the registry
# actually serves a newer set. It cannot fail the run; see below.
push:
branches: [main]
paths:
- ".github/scripts/vale-manifest.json"
# A BACKSTOP, not the trigger. Before the push trigger above existed, this
# daily cron was the only thing that noticed a publish, and on 3.22.0 it
# noticed thirteen hours after the packages were on npm. Now it covers the
# cases the push run cannot: a publish that failed and was re-run by hand
# (a re-run is not a new push, so nothing above fires), and registry
# propagation slower than the wait below is willing to sit through. A push
# run that gives up says so in a warning and leaves the work to this.
schedule:
- cron: "52 7 * * *"
workflow_dispatch:
permissions: {}
concurrency: vale-upgrade
jobs:
upgrade:
name: Upgrade the pinned Vale
runs-on: ubuntu-latest
permissions:
actions: read # wait on the Release Vale run for this commit
contents: write # push the vendor/vale/upgrade branch
pull-requests: write # open the upgrade PR
steps:
# Credentials persist because this job pushes a branch. It holds no npm
# identity and no id-token.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
# THE PUBLISH RACE. On a push this run and the `Release Vale` run for the
# same commit started within seconds of each other, and the registry
# cannot serve packages that have not been published yet. The script
# holds until that run has concluded (polling every 30 s for up to 25
# minutes; on 3.22.0 the run had stamped the packages 14 s after it
# started, so the bound is for a queued runner, not for the work), then
# until the registry answers the exact question the detect step is about
# to ask with "a newer set" (five probes a minute apart, using the same
# script with `--json` alone, a read that changes nothing).
#
# EVERY EXIT HERE IS 0. A publish that concludes anything but success,
# one that never appears inside the bound, or a registry that still says
# "current" at the end is a `::warning::` and `ready=false`, never a
# failed check: this workflow must not go red over a race it did not
# cause, and a red here would say "the upgrade is broken" when the truth
# is "the publish is". The daily schedule above retries once the packages
# are actually there, which is what a backstop is for.
#
# Not `if: github.event_name == 'push'`: the script reads the event
# itself and skips both waits with `ready=true` on anything else, because
# a scheduled or dispatched run has no publish to wait for (its commit is
# whatever main is, and the run list for that commit is empty by
# construction). Deciding inside the script rather than on the step keeps
# `ready` defined on every event; a skipped step has no outputs, and the
# detect step gates on this one. The full table of outcomes, including
# why a probe that ERRORS every time is also `ready=true`, is in the
# script's header.
- id: wait
name: Wait for the Release Vale run and the registry
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .github/scripts/vale-upgrade-wait.cjs
# No dependency install yet: the script is zero-dependency CommonJS, and
# the install below has to come AFTER the bump so it resolves the new
# pins. GITHUB_TOKEN is only for the rate limit on the release-notes
# lookup.
#
# `--write` moves the six pins and `VALE_VERSION` in capabilities.ts
# together, the latter to the base version the binary reports. It fails
# the run if the declaration is not found exactly once.
#
# `--notes-out` writes third-party Markdown to a FILE rather than a step
# output, because a $GITHUB_OUTPUT line is delimited text that a release
# body containing the delimiter can break out of.
- id: detect
run: |
node .github/scripts/vale-upgrade-detect.cjs \
--write --notes-out "${RUNNER_TEMP}/vale-release-notes.md"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Skipped when the wait established there is nothing to upgrade to yet;
# every later step gates on this step's `update`, which a skipped step
# leaves empty, so they skip with it.
if: steps.wait.outputs.ready == 'true'
# Resolution, not editing. `--lockfile-only` skips linking, so nothing is
# downloaded and no lifecycle script runs. It fails if the six packages
# are not all resolvable at the new version — a second, independent check
# on the half-published set the detect step already refuses.
- name: Regenerate the lockfile
if: steps.detect.outputs.update == 'true'
run: pnpm install --lockfile-only --ignore-scripts
# A real install this time, because the next step spawns the NEW Vale
# and the check after it imports the package's test toolchain. Under CI
# pnpm applies `--frozen-lockfile` on its own, and the lockfile rewritten
# one step ago satisfies it — a third check that resolution matched the
# pins. This is still a registry-only install under the default
# GITHUB_TOKEN, with no NODE_AUTH_TOKEN and no npm identity: the platform
# packages carry no lifecycle script, and the root `postinstall` skips
# itself under CI.
- name: Install
if: steps.detect.outputs.update == 'true'
run: pnpm install
# The vocabulary is derived by running the vendored binary, and its
# generator refuses a binary that does not report VALE_VERSION — which is
# why the constant had to move first. The output is deterministic for a
# given binary (no timestamp), so a rerun against an unchanged upstream
# leaves the tree unchanged and `vendor-pr.cjs` stays inert.
- name: Regenerate the Vale vocabulary
if: steps.detect.outputs.update == 'true'
run: pnpm --filter @taskless/cli generate:vale-schema
# The test itself imports source, but the `cli` vitest project it runs in
# does not: its globalSetup (test/support/distribution-snapshot.ts)
# snapshots `dist/` for every run and throws when there is none, before
# selecting a single test file. That setup landed 2026-10-01 and every
# run that found an update failed here from then on, proposing nothing,
# because this job had never needed to build. Built after the
# regeneration above so the bundle carries the new vocabulary.
- name: Build the CLI
if: steps.detect.outputs.update == 'true'
run: pnpm --filter @taskless/cli build
# The consistency check Validate would run, before anything is pushed. It
# holds the constant to the pins and spawns the pinned binary to confirm
# it reports that version. A failure here is a bookkeeping defect in this
# job's own output, so the run fails and proposes nothing.
#
# Deliberately ONLY that test. vale-schema-contract.test.ts and
# vale-vendor-contract.test.ts hold a recorded corpus of what the binary
# DOES, and a new Vale that changes a verdict fails them — correctly, and
# the fix is a measured edit that belongs in the child pull request. Gated
# here, that signal would become a scheduled run failing every morning
# with no pull request and no changelog to read. The next two steps run
# them anyway and report the verdict in the body instead.
- name: Check the bump is self-consistent
if: steps.detect.outputs.update == 'true'
run: >-
pnpm --filter @taskless/cli test --project cli
engine-version-consistency
# REPORTED, NOT GATED, AND NOT LEFT TO VALIDATE. The comment above used to
# end "left to Validate, it is a red check on a pull request". It was
# not: a pull_request run triggered by github-actions[bot] waits at
# `action_required` until a maintainer approves it. Measured on every
# bot-opened upgrade, Vale and ast-grep alike, and #427 read as a pull
# request with no checks while three contract tests were failing.
#
# So the suites run here, `continue-on-error` so a changed verdict still
# proposes the upgrade, and write a JSON report the next step renders.
# The report path is absolute because `--filter` runs vitest from
# packages/cli.
- name: Run the Vale contract suites
if: steps.detect.outputs.update == 'true'
continue-on-error: true
run: >-
pnpm --filter @taskless/cli exec vitest run --project cli
vale-schema-contract vale-vendor-contract
--reporter=default --reporter=json
--outputFile.json="${RUNNER_TEMP}/vale-contract.json"
# Renders the contract verdict, and the source check capabilities.ts asks
# for on every bump by hand: the files upstream ADDED under
# internal/lint/, where a new format reader lands. A reader adds an
# extension no VALE_FORMAT_TIERS row probes, so no test can catch it;
# 3.23.0's kt.go is the one that slipped through. See the script header.
# `continue-on-error` because the body is worth opening without it, and
# the propose step says so when the file is missing.
- name: Report the contract and upstream's new format readers
if: steps.detect.outputs.update == 'true'
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PINNED_VERSION: ${{ steps.detect.outputs.pinned_version }}
BASE_VERSION: ${{ steps.detect.outputs.base_version }}
run: |
node .github/scripts/vale-upgrade-report.cjs \
--pinned "$PINNED_VERSION" \
--to "$BASE_VERSION" \
--repository "$(node -p 'require("./.github/scripts/vale-manifest.json").upstream.repository')" \
--contract "${RUNNER_TEMP}/vale-contract.json" \
--out "${RUNNER_TEMP}/vale-upgrade-report.md"
- name: Propose the upgrade
if: steps.detect.outputs.update == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALE_VERSION: ${{ steps.detect.outputs.vale_version }}
BASE_VERSION: ${{ steps.detect.outputs.base_version }}
PINNED_VERSION: ${{ steps.detect.outputs.pinned_version }}
run: |
set -euo pipefail
# A changeset, because this is the bump a consumer experiences — the
# republish that produced the package did not change anyone's install.
# `patch` is correct while @taskless/cli is pre-1.0 even for a new Vale
# minor; see the bump guidance in AGENTS.md before reaching for
# `minor`. Named for the BASE version, not the stamped one: a
# republish mints a fresh timestamp for the same upstream Vale, so a
# stamped filename would churn on every republish while the release
# note it contains says the same thing. The base version is also what
# the title, the body, and the note itself say.
changeset=".changeset/vale-${BASE_VERSION//./-}.md"
cat > "$changeset" <<CHANGESET
---
"@taskless/cli": patch
---
Update the bundled Vale to ${BASE_VERSION}.
CHANGESET
# Composed as a FILE, not as an argument. The preamble is ours and the
# notes appended below it are upstream's, and the quoted heredoc plus
# `cat` means neither the shell nor gh ever interprets the latter.
body="${RUNNER_TEMP}/vale-upgrade-body.md"
cat > "$body" <<BODY
The \`@taskless/vale-*\` packages are published ahead of what
\`@taskless/cli\` pins.
This moves all six pins from \`${PINNED_VERSION}\` to
\`${VALE_VERSION}\` — Vale **${BASE_VERSION}** — and regenerates
\`pnpm-lock.yaml\`. The pins move **together** on purpose: the platform
packages are selected by optional dependency, so a straggler left at
the old version is a different Vale on one platform than on the others.
It also moves \`VALE_VERSION\` in \`capabilities.ts\` to
\`${BASE_VERSION}\` and regenerates \`src/generated/vale-vocabulary.ts\`
from the new binary, and the workflow ran
\`engine-version-consistency\` against the result before pushing. What
this does **not** carry is judgment: if the contract suites below
report a failure, Vale ${BASE_VERSION} changed a measured behaviour,
and that edit, the \`update\` ledger, any migration, and the release
note's prose belong in a child pull request. So does a row for each
format reader listed below.
**This is the half that reaches a user.** Republishing the platform
packages changes nobody's install, because the CLI pins each one
exactly; merging this is what ships the new Vale.
This pull request rolls: if another set is published before it merges,
the branch, title, and body are rewritten to the newer version rather
than a second pull request being opened. Push a commit to the branch
and that stops — the workflow will not force-push over a commit it did
not write.
BODY
# Ours, rendered from the two steps above. Missing only if the report
# step itself failed, which must not read as a clean upgrade.
report="${RUNNER_TEMP}/vale-upgrade-report.md"
if [ -f "$report" ]; then
printf '\n' >> "$body"
cat "$report" >> "$body"
else
printf '\n## Measured against Vale %s\n\n**Not measured.** The report step failed; read its log in this run.\n' "$BASE_VERSION" >> "$body"
echo "::warning::no upgrade report at ${report}"
fi
# Absent only if the detect step reported ahead and then wrote no
# notes, which it cannot — so warn rather than skip silently.
notes="${RUNNER_TEMP}/vale-release-notes.md"
if [ -f "$notes" ]; then
printf '\n---\n\n' >> "$body"
cat "$notes" >> "$body"
else
echo "::warning::no release notes file at ${notes}"
fi
node .github/scripts/vendor-pr.cjs \
--branch vendor/vale/upgrade \
--title "Release Vale ${BASE_VERSION}" \
--message "chore(vale): release Vale ${BASE_VERSION}" \
--body-file "$body" \
-- packages/cli/package.json pnpm-lock.yaml "$changeset" \
packages/cli/src/rules/capabilities.ts \
packages/cli/src/generated/vale-vocabulary.ts \
packages/cli/src/generated/vale-vocabulary-report.md