-
Notifications
You must be signed in to change notification settings - Fork 0
249 lines (245 loc) · 14.3 KB
/
Copy pathupstream-drift.yml
File metadata and controls
249 lines (245 loc) · 14.3 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
name: upstream-drift
# THE DAILY QUESTION: did a new Claude Code break clode?
#
# clode reads a bundle it does not own, so upstream can break us without breaking
# itself — and the failure is silent by nature: an anchor that no longer matches
# means a hook is NOT applied, while everything still builds and PONGs. 2.1.210
# reshaped the pkg-manager autoupdater site (2.1.207 was fine), the redirect
# stopped applying, and nobody noticed for weeks.
#
# WHY A SEPARATE JOB, when ci.yml also installs the provider: attribution. This
# asks a DIFFERENT question than a push does. A push asks "did my diff break
# clode?"; this asks "did Anthropic's release break clode?" Collapsing them into
# one red light is how an upstream break lands on an innocent commit and someone
# spends a session bisecting job conclusions to discover it was never their code.
# Red HERE means upstream moved. It says so in the job name.
#
# Same doctrine as guest-versions.yml (the weekly sweep that backstops implicit
# pins) — scripts/tjs-legs.mjs states it: Renovate owns explicit pins, a scheduled
# sweep backstops the implicit ones. The provider is an implicit pin (every job
# installs latest), so this is that backstop.
#
# START SMALL, FLESH OUT (user, 2026-07-17): one check today — every hook anchor is
# present. Add checks to scripts/upstream-drift-check.mjs as we find more ways
# upstream can break us. Deliberately NOT built on `inspect --strict`: that gates on
# every unimplemented Bun.* member and is chronically red on versions that work
# fine, and a job that is always red teaches people to ignore it.
on:
schedule:
- cron: '23 5 * * *' # daily 05:23 UTC (off the hour; nothing else runs then)
workflow_dispatch: {}
permissions:
contents: read
jobs:
anchors:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
# `next`, deliberately unpinned, and deliberately NOT `latest`.
#
# We watched both for one day and it settled the question: `next` is the leading
# channel. 2.1.243 and 2.1.245 were both on `next` before `latest`, and on
# 2026-08-25 `latest` moved from 2.1.241 to 2.1.245 — the code-split format that
# `clode build` could not read — with no warning to anyone watching `latest` alone.
# Watching the newer channel is the only version of this job that can tell us
# something BEFORE every user is already broken.
#
# One channel is enough (user, 2026-08-25). When the two are the same version this
# costs nothing; when they differ, `next` is the one worth knowing about.
#
# This job does NOT read UPSTREAM_PIN (the version ci.yml/naude-cross.yml/build-leg
# install; it held at 2.1.251 from 2026-09-01 to 2026-09-26 to dodge the 2.1.257
# SCC-merge break — see that file). Pinning here would blind the early-warning system
# at exactly the moment we choose to lag on purpose: this job's only reason to exist is
# to notice upstream moving WHILE everything else holds still. Leave it unpinned.
- name: Install the `next` Claude Code
run: npm i -g @anthropic-ai/claude-code@next 2>&1 | tail -3
- name: Every clode hook anchor still matches the newest bundle
id: anchors
run: |
set -euo pipefail
VER="$(npm view @anthropic-ai/claude-code@next version)"
echo "upstream Claude Code (next): $VER"
PROV="$(node scripts/find-provider.mjs)"
# A missing provider must fail, not skip: "we could not check" is not
# "nothing changed", and this job's only product is a trustworthy answer.
if [ -z "$PROV" ]; then echo "ERROR: no Bun provider found under @anthropic-ai/claude-code" >&2; exit 1; fi
echo "provider: $PROV"
echo "provider=$PROV" >> "$GITHUB_OUTPUT"
echo "$VER" > "$RUNNER_TEMP/upstream-version"
node scripts/upstream-drift-check.mjs "$PROV" "$VER"
# THE CARVE-TIME TEXT TRIPWIRES, against the same `next` bundle. ci.yml runs them only
# against the text oracle (the native libexec/unicode-text.cjs was generated from), so
# neither ever saw a NEWER upstream, which is the one bundle each exists to warn about:
# a Bun.sliceAnsi call gaining a 4th argument (an ellipsis, which the shim refuses with
# a throw, i.e. a blank TUI) or a spread; and ansiCodes()/SC or runWords()' uris read
# changing under the text gate's painted-style and link columns
# (test/build-gates/text-probe-gates.test.cjs). A finding
# fails this job. Either one SKIPS, saying so, if the bundle no longer calls
# Bun.sliceAnsi or has no CellSegmenter caller. It runs even when the anchor check
# above went red (a different question), but not without a provider to read.
- name: The text tripwires still hold on the newest bundle (sliceAnsi arity, painted style, link)
if: ${{ !cancelled() && steps.anchors.outputs.provider != '' }}
env:
PROV: ${{ steps.anchors.outputs.provider }}
run: |
set -euo pipefail
CLODE_PROVIDER_BIN="$PROV" node --test --test-concurrency=1 \
test/bun-slice-ansi-arity.test.cjs \
test/build-gates/text-probe-gates.test.cjs
# WHEN IT BREAKS, SAY WHAT UPSTREAM SAID. Twice now upstream has broken
# `clode build` with no change in this repo, and both times a public changelog
# already named the area while we byte-diffed binaries for hours (2.1.238's
# os.constants.errno read, and 2.1.243's switch to Bun code splitting). The
# information was never hard to get; nothing connected "the daily check went
# red" to "go read the release notes".
#
# Runs only on failure, and cannot turn a red into a green: the step above has
# already failed by the time this runs, and the tool exits 0 even when it cannot
# reach the network, so it never becomes a second thing to debug.
#
# LIMITATION, stated rather than hidden: we do not persist the last version this
# job was green on, so there is no true "since we last passed" range. Passing a
# version that is not in the changelog makes the tool print the newest few
# sections, which is the useful 90% with no state to keep in sync. If we ever
# persist a last-green version, pass it as --from and delete this comment.
- name: What did upstream say? (context only — a LEAD, never a cause)
if: failure()
run: |
VER="$(cat "$RUNNER_TEMP/upstream-version" 2>/dev/null || true)"
node scripts/upstream-release-notes.mjs --from "0.0.0-no-last-green-recorded" ${VER:+--to "$VER"}
# THE THIRD DAILY QUESTION, and the one nothing was asking: CAN CLODE CARVE NEWER
# UPSTREAM THAN THE PIN YET?
#
# The `anchors` job above checks hook sites and CLI reachability; `boots` below builds
# from the PIN. Neither reaches the SCC merge, which is the step UPSTREAM_PIN exists
# because of — so "we cannot build from newer" went 22 days and 27 versions without
# anyone re-testing it. This is the engine-backed carve probe BACKLOG.md filed, built
# once the "cannot be proven without a runner" premise stopped being true:
# scripts/bootstrap-engine.sh range-fetches a published engine slice in seconds, so a
# plain ubuntu runner can do the whole carve + merge + compile.
#
# HAIKU-PROBE DOCTRINE, one question over. It asserts the RECORDED outcome — the
# `carve-probe` line in UPSTREAM_PIN, which is where this repo already keeps measured
# facts about upstream — and goes red on EITHER change: it started working (absorb now),
# or it fails somewhere/something new (the recorded reason is stale). Neither direction
# is spelled in this file, so neither can be lost by editing the job.
#
# IT NEEDS THE NETWORK, twice: the upstream install and the engine slice. Both are
# tolerated explicitly and turn into a NAMED SKIP that says nothing about upstream was
# measured — because an infra failure reading as "still blocked" is the same
# green-that-hides this whole workflow's newer jobs exist to remove.
#
# Independent of `anchors` on purpose (attribution: a different question, a different
# red), which costs one more upstream download and buys a light that says what broke.
carve:
name: can clode carve newer than the pin yet?
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.0.0
with:
node-version-file: .tool-versions
# `next`, unpinned, for the same reason the anchors job uses it: it is the leading
# channel, and this job's only reason to exist is to look PAST the pin.
- name: Install the `next` Claude Code (a failure here is a skip, not a verdict)
id: upstream
run: |
set -uo pipefail
if ! npm i -g @anthropic-ai/claude-code@next 2>&1 | tail -3; then
echo "carve-probe: could not install upstream — this run measures nothing" >&2
fi
echo "version=$(npm view @anthropic-ai/claude-code@next version 2>/dev/null || true)" >> "$GITHUB_OUTPUT"
echo "provider=$(node scripts/find-provider.mjs 2>/dev/null || true)" >> "$GITHUB_OUTPUT"
# A PUBLISHED ENGINE SLICE, not a build. One range request out of the templates
# pack; scripts/bootstrap-engine.sh verifies the sha256 from the committed manifest
# AND makes the candidate print engine-api-floor's token before handing it over, so
# a stale slice is refused here rather than mis-read downstream as a carve failure.
- name: Resolve a tjs engine (a failure here is a skip, not a verdict)
id: engine
run: echo "tjs=$(scripts/bootstrap-engine.sh || true)" >> "$GITHUB_OUTPUT"
- name: Carve, merge and compile it — and compare against the recorded outcome
run: |
node scripts/carve-probe.mjs \
--provider "${{ steps.upstream.outputs.provider }}" \
--version "${{ steps.upstream.outputs.version }}" \
--engine "${{ steps.engine.outputs.tjs }}"
# Same lead-not-a-cause posture as the anchors job: when the carve outcome CHANGED,
# upstream's own notes usually name the area before anyone byte-diffs anything.
- name: What did upstream say? (context only — a LEAD, never a cause)
if: failure()
run: |
node scripts/upstream-release-notes.mjs --from "0.0.0-no-last-green-recorded" \
${{ steps.upstream.outputs.version && format('--to "{0}"', steps.upstream.outputs.version) || '' }}
# THE SECOND DAILY QUESTION: does a quaude built from the PINNED bundle still BOOT?
#
# READ THIS BEFORE TRUSTING THIS JOB'S GREEN (corrected 2026-09-12). It used to say
# "the newest bundle", and that was true when it was written. It is not true now:
# build-leg installs `@anthropic-ai/claude-code@$(sed -n 's/^claude-code //p'
# UPSTREAM_PIN)`, so since the pin landed (2026-09-01, 2.1.251) this job has been
# booting the PIN, not `next`. Nothing announced that; the comment simply went stale
# and the green went on reading as a statement about newest upstream.
#
# Pointing it at `next` was NOT the fix, and that was a deliberate call: `clode build`
# could not carve 2.1.257+ at all then (the SCC merge; see UPSTREAM_PIN), so this leg
# would have been red from birth — the exact "red from birth teaches everyone to ignore
# it" failure the --strict discussion in upstream-drift-check.mjs's header already
# rejected. The honest replacement is the engine-backed probe that asserts the RECORDED
# state and goes red when it CHANGES (the `carve` job above). That blocker is gone: the
# pin moved to 2.1.283 on 2026-09-26, `next` that day. This job still boots the PIN,
# which falls behind `next` again as upstream publishes; the `carve` job looks past it.
#
# What this job still genuinely buys, unchanged: the pinned bundle's shim surface is
# exercised daily on a real runner, which is how 2.1.238's os.constants.errno break
# would be caught.
#
# (Historical, and still the reason this job exists:)
#
# The anchors job above asks whether our hook sites still match. It cannot see the
# other way upstream breaks us: the bundle starts READING a node API the shim does
# not implement. 2.1.238 began evaluating
# new Map(Object.entries(require("os").constants.errno))
# at module init; os.constants carried only .signals, so Object.entries(undefined)
# threw and EVERY quaude built against 2.1.238 was dead on arrival. The anchors
# check was green throughout — nothing was mis-anchored, the shim was just missing
# a table.
#
# Nothing ran the build smoke on a schedule, so the only way to find this was for a
# human to dispatch a build; it surfaced by accident during unrelated pack work,
# and presented as CI infrastructure noise. One cheap leg daily converts that into
# a dated, attributed red light within a day of an upstream release.
#
# A FOURTH DAILY QUESTION, and the cheapest: has the Haiku blocker lifted?
#
# haiku-x64 fails because cross-platform-actions ships only an r1beta5 guest image and
# HaikuPorts moved to the current release, so the guest cannot install packages at all.
# Not our bug, and not something a build can fix. Running that leg on every push spent a
# runner slot to re-derive an answer we had already written down, while the question we
# actually care about — has a beta6 image appeared? — went unasked.
#
# This asks it in about a second. Green means "nothing we could do about Haiku today",
# NOT "Haiku is fine": the leg is deliberately out of the push matrix and publishes
# nothing (see BACKLOG). Red means an action just became possible.
# linux-x64-musl deliberately: an alpine container, no qemu, and the engine comes
# from the same PINS+patches cache the push legs use, so the marginal cost is the
# blobulate and the smoke — not an engine build.
#
# Red HERE means the PINNED bundle no longer boots under the shim. Same attribution
# doctrine as the anchors job: it is upstream that moved, not the last commit.
boots:
uses: ./.github/workflows/tjs-legs.yml
permissions:
contents: read
with:
tier: release
only: linux-x64-musl