Skip to content

bump-objectui.sh writes the pin from local HEAD without asking whether that commit is on objectui main #10495

Description

@os-zhuang

Found while implementing #9450 (PR #10494). Filing rather than folding in: that PR's surface is .github/workflows/cut-rc.yml, and this is the producer half of the same question, in a different file.

The site

scripts/bump-objectui.sh:

if [[ -n "$EXPLICIT_SHA" ]]; then
  NEW_SHA="$(git -C "$OBJECTUI_ROOT" rev-parse "$EXPLICIT_SHA^{commit}")"
else
  NEW_SHA="$(git -C "$OBJECTUI_ROOT" rev-parse HEAD)"
fi

HEAD of the operator's local objectui checkout — whatever branch that happens to be. Nothing between here and .objectui-sha asks whether the commit is reachable from objectui main. An operator who bumps with a feature branch checked out (or passes a branch name as the explicit argument) writes a pin naming a revision that is not on main, and the script reports success.

Why this is worth a card even though the release now fails closed

PR #10494 gives cut-rc.yml the reachability test it was only claiming to have, so a non-main pin is now caught — at release time, by a workflow that is workflow_dispatch-only and run a handful of times a month. The gap between "the bad pin was written" and "someone finds out" is therefore however long it takes to cut the next RC, and in between the pin is merged, and pnpm sdui:manifest (ADR-0082 D4) will have ratcheted spec↔registry parity against a tree that is not on main.

The check is one line at the point of writing, and it is the same predicate the release path now uses:

git -C "$OBJECTUI_ROOT" merge-base --is-ancestor "$NEW_SHA" origin/main

with the usual care that --is-ancestor exits 128 on an absent object rather than returning a verdict, and that a checkout with no origin/main cannot answer the question at all and must say so rather than guess.

Open design question for whoever takes it

Whether this should be a hard failure or a confirmable warning. The script is deliberately usable offline and on a machine that cannot run the full procedure (see the print_sdui_next_step rationale — it is a reminder, not a gate, on purpose), and origin/main in a local checkout is only as fresh as the last fetch, so a hard failure would reject a legitimately-just-merged commit whose origin/main is stale. A loud warning naming the branch the commit is on, plus the hard gate at cut time from #10494, may be the right split.

Scale of the thing it guards against

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. Any of those is a rev-parse HEAD away from being the pin.

Related: #9450 / PR #10494 (the release-time half), #10134 (the pin is objectstack's decision, not a resolution of objectui main), #9408 (the other measured case of a git predicate answering a narrower question than the message claimed).

Generated by Claude Code

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands in scripts/bump-objectui.sh → pm:queue (domain:devx already set by filer, kept), type Task.

    Direction on the card's open design question, from its own analysis rather than a fresh ruling: loud warning, not hard failure — the script is deliberately offline-usable and origin/main freshness is not guaranteed locally, while the hard gate now exists at the right chokepoint (cut-rc.yml via PR #10494). Warning must name the branch the commit is on, handle --is-ancestor's exit-128-on-absent-object honestly, and say "cannot answer" when no origin/main exists rather than guessing (all three cases are in the card). If the implementing dev finds the warning practically invisible in the bump flow, escalate then — not now.

    Serial note: verify PR #10494's merge state at dispatch; if still in flight, this card edits a different file (bump script vs workflow) so no hard block, but the PR body should cross-reference the split (producer warns, release gates).

    Size/model suggestion: S, mode:subagent, sonnet–opus.


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    Contributor

    Claim: session session_01DdCnBGcHeufjrq7drTD3wt, branch claude/issue-10495-objectui-pin-reachability.

    Taking the triage direction (loud warning, not hard failure). Verified at start: PR #10494 is merged (2026-08-21T02:49:03Z) and the hard reachability gate is live on main at .github/workflows/cut-rc.yml:319, so the release path already fails closed and this is the producer-warning half only.

    Generated by Claude Code


    Generated by Claude Code

  4. added a commit that references this issue on Aug 21, 2026
    f871db8
  5. claude commented on Aug 21, 2026

    @claude
    Contributor
    {
      "issue": 10495,
      "status": "done",
      "branch": "claude/issue-10495-objectui-pin-reachability",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10796",
      "premise_still_valid": true,
      "summary": "scripts/bump-objectui.sh now asks whether the revision it is about to pin is reachable from the objectui checkout's origin/main, and reports THREE answers rather than two: reachable (no banner; the pin line that prints anyway carries '(on origin/main)'), not reachable (loud stderr warning naming the branch(es) the commit IS on and distinguishing 'pushed but never merged' from 'never pushed', plus the local origin/main tip and its age, plus a recall on the run's last line), and cannot-be-answered (says that, names which failure, and borrows the wording of neither verdict). Both causes of the third state are handled: no origin/main in the checkout, and an absent commit object. It stays a warning, not a gate — the pin is still written and the run still exits 0 — because origin/main is only as fresh as the last fetch and the script is deliberately offline-usable; the hard gate is at cut-rc.yml:319, which I verified is live on main (PR #10494 merged 2026-08-21T02:49:03Z). It never fetches for the operator, it prints the fetch. cut-rc.yml untouched (#9465 fence). Docs note added to docs/releases-maintenance.md. Two git behaviours the three-way split rests on are measured and pinned in the self-test rather than asserted in prose: merge-base --is-ancestor exits 128 on an absent object, and git rev-parse HEAD exits 0 for a commit whose object is missing (which is why the absent-object case is reachable from the DEFAULT path, not just from an explicit argument).",
      "tests": "All evidence at final commit f871db8c45 (branch head, clean tree; base origin/main was 6936d0755b at fetch — the PM's measured 9c0215f61e had already moved). REPRODUCED FIRST on origin/main's script: a throwaway objectui with feature/never-merged checked out -> 'EXIT=0', pin written to the feature tip, output '→ objectui pin: d22f34052d6a → c226a5d25d94' with no warning of any kind. AFTER, same input: '→ objectui pin: 531f88859803 → bfdafd075b09 (NOT on origin/main)' + stderr '⚠️ objectui pin bfdafd075b09 is NOT reachable from origin/main ... on local branches : feature/never-merged / on remote branches: none / → It is on a LOCAL branch only — this commit has not been pushed to objectui'; a pin that IS on main stays silent (self-test R1). STATE 3 PROVEN DIRECTLY, both causes, exit codes quoted: (3a) no origin/main — 'git rev-parse --verify --quiet origin/main' -> exit 1, and 'merge-base --is-ancestor <sha> origin/main' -> exit 128 (would read as 'left main' if trusted); script EXIT=0 and prints 'COULD NOT DETERMINE ... has no origin/main ref ... This is NOT the pin is fine and NOT the pin left main'. (3b) absent object — 'git rev-parse HEAD' -> exit 0 printing the sha anyway, 'git cat-file -e <sha>^{commit}' -> exit 128, 'merge-base --is-ancestor' -> exit 128; script prints 'COULD NOT DETERMINE ... The commit object is NOT PRESENT'. Neither says 'not reachable'; neither passes. SELF-TEST: bump-objectui.sh has no --self-test of its own, so I extended scripts/objectui-changeset-digest.mjs --self-test, which already drives the real shell script end to end over throwaway git repos and is gated by 'pnpm check:objectui-changeset' in lint.yml; 14 cases added (R1..R6), including the card's exact shape through the front door (no argument, rev-parse HEAD, feature branch checked out). Baseline origin/main run in a comparison worktree (needed — the self-test reads docs/adr/, so a temp-dir copy aborts at 63 checks and would have been a false baseline): 99 checks exit 0. Patched: 113 checks exit 0. Name-diff of the two check lists: nothing present in baseline and missing after, delta exactly the 14 new. ABLATION: report_objectui_reachability() returns before asking anything, so the check can never fire. No build step is involved — the self-test copies bump-objectui.sh verbatim from scripts/, so the mutated bytes are what runs; still confirmed on disk by anchor count rather than an editor's exit code — injected marker 'ABLATION-10495' grep -c = 1, function header grep -c = 1, sha256 444a5aac... -> e0fcf19b... PREDICTED BEFORE RUNNING: 11 red (R1b, R2, R2b, R2c, R2d, R3, R3b, R4, R4b, R5c, R6), 3 green — R1 because it asserts the ABSENCE of a warning and a never-firing check passes that control, R5a/R5b because they are raw git measurements independent of the script — 102 green, exit 1. OBSERVED: exactly that, 11 red / 102 green / exit 1, red case names matching one for one, green being R1, R5a, R5b. Restored: marker grep -c = 0, sha256 back to 444a5aac... (BYTE-IDENTICAL to pre-ablation), self-test 113 green / 0 red / exit 0. GATES: derived with 'node scripts/pm/dispatch-gates.mjs' with no paths passed (it takes its own change set from the merge base), exit 0, 3 paths, 7 families; RE-DERIVED after the final commit (same 7, no new family) and all re-run at f871db8c45 on a clean tree, exit codes captured before any pipe: check:objectui-changeset=0 ('✓ objectui-changeset-digest --self-test: all checks passed'), check:cross-package-test-inputs=0 ('OK: 13 package(s) read outside themselves, all declared'), check:doc-authoring=0 ('✓ doc authoring guard: 389 files clean'), check:required-contexts=0 ('✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s)'), --filter @objectstack/lint check:doc-formula-expressions=0 ('✓ ... 22 record-scoped formula example(s) across 416 files / 1443 TS blocks judged clean'), check:entry-guard=0 ('✓ check:entry-guard: 127 scripts/ file(s) — every entry guard goes through invoked-as.mjs'), check:nul-bytes=0 ('check-nul-bytes: OK (scanned 6242 text file(s) ... no raw ASCII control bytes)'). entry-guard and nul-bytes were run although the derivation did not name them (a .mjs entry point was touched; every edit gets the control-byte scan); the three changed files were also scanned directly with grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' — no hits. One trap hit and corrected mid-run: 'pnpm ... | tail -12' made os-verify-lock print 'VERDICT command-exit 0' for a pnpm run that had actually failed on an unknown flag; re-run without the pipe. doc-formula-expressions first failed ERR_MODULE_NOT_FOUND on @objectstack/formula/dist — unbuilt closure in a fresh worktree, green after pnpm --filter '@objectstack/lint^...' build. Heavy steps (install, build) went through scripts/pm/os-verify-lock.sh.",
      "open_questions": [
        {
          "question": "You specified 'reachable from origin/main -> silent, say nothing'. I honoured the no-new-output part but let the pin line that already prints carry the answer: '→ objectui pin: A → B (on origin/main)', with '(NOT on origin/main)' / '(reachability UNKNOWN)' in the same slot for the loud states. Flagging it because it is a deviation from the letter of your ruling, not because I think it is wrong.",
          "options": [
            "A — keep it: zero added lines, and it removes the residual ambiguity that pure silence leaves, namely 'checked and fine' being indistinguishable from 'this build of the script never asked'. That is this card's own defect class one level up, and cut-rc.yml prints a positive line for the same reason.",
            "B — strip the '(on origin/main)' tag so the healthy path is byte-identical to today's output, keeping only the two loud states. One-line change."
          ],
          "recommendation": "A. The cost is zero output and the thing it buys is exactly what the card is about; but this is your ruling to reverse and B is a one-liner if you want the letter of it."
        },
        {
          "question": "You left the stale-origin/main handling to me: suggest 'git fetch origin main' or merely report?",
          "options": [
            "A — suggest the fetch, never run it: the not-reachable warning prints the exact command AND the local origin/main tip with its age ('it points at 5104037, committed 3 weeks ago'), so 'your ref is simply stale' becomes a fact the operator can judge rather than a hedge they have to check by hand.",
            "B — report only, no command, on the grounds that any suggestion nudges toward a fetch the script deliberately does not perform."
          ],
          "recommendation": "A, implemented. Suggesting is not fetching, and the tip-age line is what makes the suggestion actionable instead of boilerplate; the message says '(not run for you — this script stays usable offline)' at both sites so the deliberate non-fetch is stated, not merely implied."
        }
      ],
      "out_of_scope_findings": [
        "filed as #10797 (domain:devx, tooling, unassigned, no pm:queue): bump-objectui.sh writes .objectui-sha BEFORE it reads the commit's subject line, so an objectui commit whose object is absent leaves a half-applied bump — pin file rewritten, then the run dies under set -e at 'git log -1 --format=%s' with a bare 'fatal: bad object', exit 128, no changeset, no commit, and re-running does not self-correct. Reachable from the default no-argument path because git rev-parse HEAD exits 0 for a missing object. PR #10796 makes the run loud about it but deliberately does not change the write ordering."
      ]
    }
    

    Generated by Claude Code

  6. added 2 commits that reference this issue on Aug 23, 2026
    2499d18
    cfb8ae4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions