Skip to content

feat(promotion-gate): share the develop gate — nine identical copies become one - #51

Merged
usetheodev merged 6 commits into
mainfrom
workspace
Sep 5, 2026
Merged

feat(promotion-gate): share the develop gate — nine identical copies become one#51
usetheodev merged 6 commits into
mainfrom
workspace

Conversation

@usetheodev

Copy link
Copy Markdown
Contributor

Measured 2026-09-05 across the ten consumers of this repository:

workflow state
promotion-gate.yml 9 of 10 byte-identical, 1 improved variant
release.yml all 9 differ (164–365 lines) — genuine divergence, left alone
ci.yml all 10 differ (87–627 lines) — not drifted copies, left alone

Only the first is a copy-paste problem, so only the first is changed here. release.yml and
ci.yml differ because the repositories genuinely release and build differently; folding them
into one caller would be standardisation as a slogan rather than as a fix.

What the shared version is

The union of the two variants, not a promotion of either:

  • from the nine: the multi-line diagnostics citing rules/git-safety.md § 1, which tell the author
    what to do next instead of only refusing;
  • from theokit-sdk: timeout-minutes, and the exemption for changeset-release/develop.

The changesets exemption is what usetheokit/theokit-sdk#535 cost on 2026-09-03 — every Version
Packages PR against develop failed unconditionally and branch protection refused even an admin
override. Measured: every consumer currently releases from main, so the bot's branch is
changeset-release/main and never reaches this gate. It costs those repositories nothing today and
spares the next one that moves its release.

concurrency was deliberately not carried over. The file states the reason inline, so its
absence cannot read as an oversight.

Verification

  • actionlint (the pinned image this repository's own workflow-lint uses) — clean, and verified
    against a positive control: a file with a known script injection is flagged, this one is not.
  • zizmorNo findings to report.

Follow-up, not in this PR

Converting the ten consumers to thin callers is a separate change per repository, and it happens
after v1 serves this workflow — a caller pointing at a ref that does not yet resolve is a
repository that cannot open a pull request into develop.

usetheokit/theokit-sdk#569. The floor leg pins the dep under the package
manager's override field and reinstalls. When the dep is a package of THIS
workspace, that override rewrites its spec into a plain semver range, which
destroys the `workspace:` protocol guarantee -- pnpm 'will refuse to resolve to
anything other than a local workspace package' only while that protocol is in
the spec. With linkWorkspacePackages defaulting to false, the range resolves
from the registry and the published tarball is installed BESIDE the local copy.

Measured on theokit-sdk, same checkout and machine:

  without the override   452 MB peak,   6.6s
  with it              4,432 MB peak,   OOM on the runner

9.8x. tsup's DTS worker walks 14 MB of published .d.ts instead of the workspace
source. It blocked the 5.0.1 release twice before anyone looked at why.

It is also the wrong question. A floor is a claim about what a CONSUMER
resolves, and a consumer never has this workspace's copy -- so the leg was
validating a package against its own published output.

WHY NOT isSibling. My first attempt excluded every ecosystem sibling and broke
two existing tests, which was the code telling me I was deleting the feature
rather than fixing it: its primary case is theokit-plugins, where fourteen
packages declare `theokit >=0.50.1` and theokit is a DIFFERENT repository.
Overriding that duplicates nothing and must keep running. The predicate is 'the
dep is published by this very workspace', not 'the dep is ours'.

Measured across the ecosystem after the change:

  theokit           1 leg kept, 0 skipped
  theokit-plugins   1 leg kept (theokit, external), 1 skipped
  theokit-gateways  0 kept, 1 skipped
  theokit-tui       unchanged

theokit-plugins is the proof: the leg the docblock names as the reason this
check exists survives.

An empty workspace list fails OPEN -- a caller that could not read the manifests
must not silently drop every floor and report a green check that ran nothing.
Every dropped gap is announced via ::notice::, never silently.

The uv and Cargo ecosystems draw this same line: --resolution lowest-direct and
-Z direct-minimal-versions lower the direct edges you do not control.
Bumps the manifest and the pin in dep-check.yml together, which the release
gate requires -- 0.5.0 and 0.2.0 each published green while the pin stayed
behind, so no consumer received the change.

Carries the fix for usetheokit/theokit-sdk#569: floor legs whose dep this
workspace publishes are dropped, because pinning one installs its own tarball
beside the local copy (452 MB -> 4,432 MB peak, measured) and asks a question a
consumer never asks.

Eleven repositories consume this gate. Moving v1 is the separate step.
vitest-dev/vitest sets it repo-wide and documents ~15-20s per job -- the only
hard number anyone published for an install optimisation. Measured on
theokit-sdk, pnpm 10.34.1, two runs each against a warm store:

  default             2.78s, 2.54s
  TRUST_LOCKFILE      2.52s, 2.54s

Nothing. Their saving is real and ours is not, most plausibly because every job
here restores a warm store through the cache: pnpm above.

Recorded rather than left silent, following the precedent of the turbo-cache
note in theokit-sdk's ci.yml: configuration that does nothing is debt wearing an
optimisation's clothes, and the next person to read the vitest number should
find this instead of re-deriving it. It would have traded a supply-chain check
for zero measured seconds.
…kage

The leg builds every package that claims the floor. Doing it one --filter at a
time re-plans the task graph once per package and rebuilds the shared
dependencies each round.

Measured on theokit-plugins, whose leg claims 10 packages, cold cache (dist and
turbo cleared), two rounds each:

  loop (10 invocations)   35.2s, 39.6s
  batched (1 invocation)  23.4s, 24.0s

-34% and -39%, producing the same 10 dist/ directories and the same 20 build
successes.

Two things this deliberately does NOT do:

- batchedWithDeps returns null for npm and yarn rather than guessing. npm has no
  '...' equivalent at all, and yarn's 'yarn workspace <pkg> run' takes exactly
  one name -- batching there needs 'workspaces foreach', different semantics
  nobody here has measured. Those callers keep the loop, correct if slower.
- an empty package list returns null, because 'pnpm run build' with no filter
  builds the WHOLE workspace, which is the defect the per-package filter exists
  to prevent.

--package is now repeatable, and that needed parseArgs 'multiple: true'. Without
it the parser keeps only the LAST occurrence, so the repeated flag would have
silently built one package and reported the whole leg green -- caught by running
the command with three packages and reading the output, not by the tests.

Bumps to 0.10.0 with the workflow pin, together, as the release gate requires.
The obvious move is `cache: pnpm` on its Setup Node, and it would be dead
configuration. This job only runs when base_ref == 'main' -- a pull request,
never a push. A run restores a cache from its own branch or from the DEFAULT
branch, so a cache only ever written on release pull requests is one no other
run can reach: every leg a guaranteed miss, paying the save cost forever.

cloudflare/workers-sdk hit this exact shape and wrote it down: actions/cache
saves from a post step declaring post-if: success(), so a failing job never
saves either -- and this job has been failing.

What it was worth, measured on run 101194428887: install 26s and reinstall 10s
of a 319s run, about 8%. The two changes that matter took the other 92%.

Doing it properly needs cache/restore + explicit cache/save plus a job on
push: [main] to populate what the pull requests read. New machinery for 8%,
recorded rather than built.
…ecome one

Measured 2026-09-05 across the ten consumers: nine held byte-identical copies of
the 74-line gate and one held a variant that had learned two things the others
never received. A fix in one copy reached one repository.

The shared version is the union of both: the nine contributed the diagnostics
that cite git-safety.md and name the next step, theokit-sdk contributed
timeout-minutes and the changesets-bot exemption that cost #535 on 2026-09-03.

concurrency was deliberately not carried over, and the file says why, so its
absence does not read as an oversight.

actionlint and zizmor clean, both verified against a positive control.
@usetheodev

Copy link
Copy Markdown
Contributor Author

Correcting the "Follow-up, not in this PR" note above: it says the converts must wait because v1
would not serve this workflow until a package release. That is wrong, and I filed #52 on the
same wrong premise before checking — it is closed as invalid with the details.

ci.yml has advanced v1 on every push to main since 2026-08-27 (073cbaa, "v1 follows main,
not only releases"
), gated on the pinned @theokit/dep-check version being published. Verified:
advance v1: success on runs 33687196411 and 33561230773.

So once this merges, v1 moves on that push and the shared gate is reachable by all ten consumers
immediately. The conversion to thin callers can follow directly, with no release in between.

@usetheodev
usetheodev merged commit df8a1c4 into main Sep 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants