Skip to content

Commit f871db8

Browse files
committed
fix(tooling): bump-objectui.sh asks whether the pin is ON objectui main, and says so three ways
`bump-objectui.sh` wrote `.objectui-sha` from `git rev-parse HEAD` of the operator's objectui checkout and never asked whether that commit is on main. Bump with a feature branch checked out and the pin names a revision that is not on main -- and the script reported success. Measured on a fresh --no-tags clone of objectui, 2026-08-21: 941 remote branches, 118 branch tips not reachable from main, 291 commits present and not on main. The bump now asks, and reports THREE answers, never two: - on origin/main -> no banner; the pin line that prints anyway says "(on origin/main)", so "checked, and fine" is stated rather than inferred from silence. - not on origin/main -> a loud warning naming the branch(es) the commit IS on, and saying which situation it is: pushed onto a branch that never merged, or never pushed at all. The pin is still written -- this is a warning. - cannot be answered -> it says THAT, and borrows the wording of neither verdict. Both causes are handled: no origin/main in the checkout, and an absent commit object. Warning, not gate, deliberately: `origin/main` is only as fresh as the last fetch, so hard-failing would reject a legitimately-just-merged commit, and this script is deliberately usable offline. The hard gate is at the chokepoint that must fail closed -- cut-rc.yml re-asks the same predicate against a fresh full clone and refuses to cut (#9450 / PR #10494). It never fetches for the operator; it prints the fetch they may want. Two git behaviours the three-way split rests on, measured on git 2.43.0 and pinned in the self-test rather than asserted in prose: - `merge-base --is-ancestor <absent-object> origin/main` exits 128, an error, not the 1 that means "not an ancestor". Reading 128 as "no" invents a false alarm; reading anything-not-1 as "fine" reinstates the silent pass. - `git rev-parse HEAD` exits 0 and prints the sha even when that commit object is missing from the object store -- it resolves the ref without reading the object. So NEW_SHA arriving is no proof of presence, and the absent-object case is reachable from the DEFAULT path, not only from an explicit argument. 14 cases added to `objectui-changeset-digest.mjs --self-test`, which already drives the real shell script end to end (pnpm check:objectui-changeset). Fixes #10495 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
1 parent 6936d07 commit f871db8

3 files changed

Lines changed: 434 additions & 4 deletions

File tree

‎docs/releases-maintenance.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,34 @@ CONSOLE_BUMP=patch scripts/bump-objectui.sh # force the bump type
5050
scripts/bump-objectui.sh --no-changeset # opt out (rarely)
5151
```
5252

53+
#### The pin must be a commit on objectui `main` (#10495)
54+
55+
`bump-objectui.sh` pins `git rev-parse HEAD` of your objectui checkout, which
56+
answers *what is checked out* — never *is it on main*. Bump with a feature branch
57+
checked out and you pin a revision that is not on main. So the bump now asks the
58+
question and **warns** — it does not refuse:
59+
60+
* **On `origin/main`** — the pin line says `(on origin/main)` and nothing else is
61+
printed. "Checked, and fine" is stated, not inferred from silence.
62+
* **Not on `origin/main`** — a loud warning naming the branch(es) the commit *is*
63+
on, and saying which situation it is: pushed onto a branch that never merged, or
64+
never pushed at all. The pin is still written; deciding is yours.
65+
* **Cannot be answered** — no `origin/main` in the checkout, or the commit object
66+
is absent — it says *that*, and never borrows the wording of either verdict.
67+
(`git merge-base --is-ancestor` exits **128** on an absent object: an error, not
68+
a "no". And `git rev-parse HEAD` exits 0 for a commit whose object is missing,
69+
so the pin arriving is no proof the object is there.)
70+
71+
It is a warning rather than a gate on purpose: `origin/main` is only as fresh as
72+
your last fetch, so a hard failure here would reject a legitimately-just-merged
73+
commit, and this script stays usable offline. It never fetches for you — it prints
74+
the `git fetch origin main` you may want. The hard gate lives at the chokepoint
75+
that must fail closed: `.github/workflows/cut-rc.yml` re-asks the same question
76+
against a fresh full clone and **refuses to cut** (#9450). Until an RC is cut,
77+
though, a bad pin merges and `pnpm sdui:manifest` below would ratchet
78+
spec↔registry parity against a tree that is not on main — which is why the
79+
producer half warns at all.
80+
5381
#### After the pin moves: run the declaration-parity ratchet (#5960)
5482

5583
The bump has a second half, and it is not optional:

‎scripts/bump-objectui.sh‎

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
# # on-demand gate by decision, never a CI job. Needs
1515
# # Playwright chromium. This script prints the reminder.
1616
#
17+
# The pin must name a commit that is on objectui MAIN. This script WARNS — it
18+
# does not refuse — when the revision being pinned is not reachable from the
19+
# objectui checkout's `origin/main`, and names the branch(es) it IS on. It never
20+
# fetches for you: `origin/main` is only as fresh as your last fetch and this
21+
# script stays usable offline. The release cut re-asks the same question against
22+
# a fresh full clone and REFUSES to cut (#9450). Three answers, never two — a
23+
# checkout that cannot answer the question says so rather than guessing (#10495).
24+
#
1725
# Env:
1826
# CONSOLE_BUMP=major|minor|patch # force the changeset bump type (default: auto —
1927
# # the HIGHEST level objectui itself declared in the
@@ -122,15 +130,188 @@ else
122130
NEW_SHA="$(git -C "$OBJECTUI_ROOT" rev-parse HEAD)"
123131
fi
124132

133+
# --- Is the revision we are about to pin actually ON objectui main? (#10495) --
134+
# `rev-parse HEAD` answers "what is checked out", never "is it on main". Bump
135+
# with a feature branch checked out — or pass a branch name — and the pin names
136+
# a revision that is not on main, and this script used to report success.
137+
# Measured on a fresh `--no-tags` clone of objectui, 2026-08-21: 941 remote
138+
# branches, 118 branch tips not reachable from `main`, 291 commits present and
139+
# not on main. Any of those is one `rev-parse HEAD` away from being the pin.
140+
#
141+
# A WARNING, NOT A GATE — deliberately, and the split is with #9450 / PR #10494:
142+
# `.github/workflows/cut-rc.yml` now runs this same predicate against a fresh
143+
# full clone and refuses to cut. That is the chokepoint that must fail closed.
144+
# Here it must not: `origin/main` in a local checkout is only as fresh as the
145+
# last fetch, so hard-failing would reject a legitimately-just-merged commit,
146+
# and this script is deliberately usable offline and on a machine that cannot
147+
# run the full procedure (same rationale as `print_sdui_next_step`). So: say it
148+
# loudly, name the branch it IS on, and let the operator decide. Never fetch on
149+
# the operator's behalf — unlike the `--unshallow` deepen below, which repairs
150+
# an input this script is about to DERIVE from, a fetch here would only change
151+
# the answer to a question the operator is being asked to judge.
152+
#
153+
# THREE ANSWERS, NOT TWO. `merge-base --is-ancestor` exits 0 (ancestor), 1 (not
154+
# an ancestor) and **128 on an absent object** — an error, not a verdict.
155+
# Measured, git 2.43.0:
156+
# git merge-base --is-ancestor <absent-sha> origin/main
157+
# fatal: Not a valid commit name <absent-sha> -> exit 128
158+
# Reading 128 as "not an ancestor" invents a false alarm; reading anything that
159+
# is not 1 as "fine" reinstates exactly the silent pass this check exists to
160+
# remove. A checkout with no `origin/main` cannot answer the question at all.
161+
# So the exits are split three ways, and the third says what it could not
162+
# determine and why — it never borrows the wording of either verdict.
163+
#
164+
# The absent-object case is reachable from the DEFAULT path, not just from an
165+
# explicit argument. Measured, git 2.43.0: `git rev-parse HEAD` exits **0** and
166+
# prints the sha even when that commit object is missing from the object store —
167+
# it resolves the ref, it does not read the object. So NEW_SHA arriving here is
168+
# not proof the object is present, and presence is asked as its own question.
169+
REACH_TAG="" # parenthetical for the one line that is printed anyway
170+
REACH_RECALL="" # tail recall, so a warning cannot scroll out of the run
171+
172+
# Render at most 10 ref names. A cap that fires SAYS SO, with the real count —
173+
# a capped list that reads as complete is this file's other standing lesson
174+
# (CONSOLE_CHANGES_MAX, #4731).
175+
reach_ref_list() {
176+
local n="$#"
177+
if (( n == 0 )); then printf 'none'; return 0; fi
178+
local out
179+
out="$(printf '%s, ' "${@:1:10}")"
180+
out="${out%, }"
181+
if (( n > 10 )); then out="${out} … (+$(( n - 10 )) more; ${n} total)"; fi
182+
printf '%s' "$out"
183+
}
184+
185+
report_objectui_reachability() {
186+
local sha="$1"
187+
local short="${sha:0:12}"
188+
local rc=0
189+
190+
# Q1 — can this checkout answer the question AT ALL? Asked first because the
191+
# later questions are meaningless without it: with no origin/main,
192+
# `--is-ancestor` would report "the pin left main", which is this check's own
193+
# overclaim wearing a new message.
194+
if ! git -C "$OBJECTUI_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
195+
REACH_TAG=" (reachability UNKNOWN)"
196+
REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above"
197+
{
198+
echo
199+
echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main."
200+
echo " ${OBJECTUI_ROOT} has no 'origin/main' ref, so the question cannot be asked"
201+
echo " in this checkout at all."
202+
echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered,"
203+
echo " and the pin is being written anyway."
204+
echo " To make it answerable here: git -C ${OBJECTUI_ROOT} fetch origin main"
205+
echo " (not run for you — this script stays usable offline.)"
206+
echo " The release cut re-asks it against a fresh full clone and fails closed (#9450)."
207+
echo
208+
} >&2
209+
return 0
210+
fi
211+
212+
# Q2 — is the object even present? `--is-ancestor` cannot return a verdict for
213+
# an object it cannot read; it exits 128 there, and 128 must not be read as
214+
# "no". Asked separately so the report can name WHICH failure this was.
215+
if ! git -C "$OBJECTUI_ROOT" cat-file -e "${sha}^{commit}" 2>/dev/null; then
216+
REACH_TAG=" (reachability UNKNOWN)"
217+
REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above"
218+
{
219+
echo
220+
echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main."
221+
echo " The commit object is NOT PRESENT in ${OBJECTUI_ROOT}"
222+
echo " ('git cat-file -e ${short}^{commit}' exits 128). A ref can name a commit"
223+
echo " whose object is missing — 'git rev-parse' resolves the ref without reading"
224+
echo " the object — so this is reachable even from a plain HEAD bump."
225+
echo " 'merge-base --is-ancestor' exits 128 here too: an error, not a verdict."
226+
echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered."
227+
echo " Fetch or repair the objectui object store, then re-run this bump."
228+
echo
229+
} >&2
230+
return 0
231+
fi
232+
233+
# Q3 — the verdict. 0 / 1 / anything else, distinguished on purpose.
234+
git -C "$OBJECTUI_ROOT" merge-base --is-ancestor "$sha" origin/main 2>/dev/null || rc=$?
235+
236+
if [[ "$rc" -eq 0 ]]; then
237+
# Reachable. No banner — but the line that prints anyway says so, so that
238+
# "checked, and fine" is stated rather than inferred from silence.
239+
REACH_TAG=" (on origin/main)"
240+
return 0
241+
fi
242+
243+
if [[ "$rc" -ne 1 ]]; then
244+
REACH_TAG=" (reachability UNKNOWN)"
245+
REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above"
246+
{
247+
echo
248+
echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main."
249+
echo " 'git merge-base --is-ancestor ${short} origin/main' exited ${rc}; only 0"
250+
echo " (is an ancestor) and 1 (is not) are verdicts. Anything else is an error,"
251+
echo " and is being reported as one rather than folded into either answer."
252+
echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered."
253+
echo
254+
} >&2
255+
return 0
256+
fi
257+
258+
# rc == 1: a real verdict, and the one this card exists for.
259+
REACH_TAG=" (NOT on origin/main)"
260+
REACH_RECALL=" — ⚠️ pin is NOT on objectui origin/main, see above"
261+
262+
# Enumerated only on this branch: with ~941 branches a --contains walk is not
263+
# free, and the healthy path must not pay for it.
264+
local -a local_refs=() remote_refs=()
265+
mapfile -t local_refs < <(
266+
git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/heads 2>/dev/null || true
267+
)
268+
mapfile -t remote_refs < <(
269+
git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/remotes 2>/dev/null || true
270+
)
271+
272+
local main_tip
273+
main_tip="$(git -C "$OBJECTUI_ROOT" log -1 --format='%h, committed %cr' origin/main 2>/dev/null || echo 'unknown')"
274+
275+
{
276+
echo
277+
echo "⚠️ objectui pin ${short} is NOT reachable from origin/main in"
278+
echo " ${OBJECTUI_ROOT} — it names a revision that is not on objectui main."
279+
echo " on local branches : $(reach_ref_list ${local_refs[@]+"${local_refs[@]}"})"
280+
echo " on remote branches: $(reach_ref_list ${remote_refs[@]+"${remote_refs[@]}"})"
281+
# Which of the two situations this is, said out loud, so the operator does
282+
# not have to go and look: a branch that never merged and a commit that was
283+
# never pushed take different remedies.
284+
if (( ${#remote_refs[@]} > 0 )); then
285+
echo " → It IS pushed, but only onto branch(es) that have not merged into main."
286+
elif (( ${#local_refs[@]} > 0 )); then
287+
echo " → It is on a LOCAL branch only — this commit has not been pushed to objectui."
288+
else
289+
echo " → No branch in this checkout contains it (detached HEAD on a discarded commit?)."
290+
fi
291+
echo " Or your origin/main is simply STALE: it points at ${main_tip}."
292+
echo " Refresh it with: git -C ${OBJECTUI_ROOT} fetch origin main"
293+
echo " (not run for you — this script stays usable offline, and the answer to"
294+
echo " this question is yours to judge.)"
295+
echo " Pinning it anyway. Downstream: the release cut re-asks this against a fresh"
296+
echo " full clone and REFUSES to cut (#9450), so a bad pin surfaces at RC time at"
297+
echo " the earliest; until then 'pnpm sdui:manifest' would ratchet spec↔registry"
298+
echo " declaration parity against a tree that is not on main (ADR-0082 D4)."
299+
echo
300+
} >&2
301+
return 0
302+
}
303+
304+
report_objectui_reachability "$NEW_SHA"
305+
125306
OLD_SHA="$(cat "${FRAMEWORK_ROOT}/.objectui-sha" 2>/dev/null | tr -d '[:space:]' || echo '<none>')"
126307

127308
if [[ "$OLD_SHA" == "$NEW_SHA" ]]; then
128-
echo "→ Already at ${NEW_SHA:0:12}, nothing to do."
309+
echo "→ Already at ${NEW_SHA:0:12}${REACH_TAG}, nothing to do."
129310
exit 0
130311
fi
131312

132313
echo "$NEW_SHA" > "${FRAMEWORK_ROOT}/.objectui-sha"
133-
echo "→ objectui pin: ${OLD_SHA:0:12} → ${NEW_SHA:0:12}"
314+
echo "→ objectui pin: ${OLD_SHA:0:12} → ${NEW_SHA:0:12}${REACH_TAG}"
134315

135316
SHORT="${NEW_SHA:0:12}"
136317
SUBJECT_LINE="$(git -C "$OBJECTUI_ROOT" log -1 --format=%s "$NEW_SHA")"
@@ -299,7 +480,7 @@ print_sdui_next_step() {
299480
}
300481

301482
if [[ "$NO_COMMIT" -eq 1 ]]; then
302-
echo "→ --no-commit: leaving files unstaged."
483+
echo "→ --no-commit: leaving files unstaged${REACH_RECALL}."
303484
print_sdui_next_step
304485
exit 0
305486
fi
@@ -311,5 +492,5 @@ git -C "$FRAMEWORK_ROOT" commit -m "chore: bump objectui to ${SHORT}
311492
${SUBJECT_LINE}
312493
313494
objectui@${NEW_SHA}" -- .objectui-sha ${CS_FILE:+"$CS_FILE"}
314-
echo "✓ Committed. Push with: git push"
495+
echo "✓ Committed${REACH_RECALL}. Push with: git push"
315496
print_sdui_next_step

0 commit comments

Comments
 (0)