-
fix(release): dedupe the published dsl, close two release-path blind spots (#1019)
发布于
2026-08-03 02:44:34 +00:00 - fix(release): dedupe the published dsl, close two release-path blind spots
Three independent defects on the release path.
-
@openmaic/storage,@openmaic/rendererand@openmaic/importer
declared"@openmaic/dsl": "workspace:*", which pnpm publishes as an
EXACT pin. The tarballs on the registry today say storage@0.1.1 needs
dsl 0.5.0, renderer@0.0.3 needs dsl 0.4.0 and importer@0.1.1 needs dsl
0.4.0, so installing storage together with renderer yields two copies
of the dsl. The dsl carries the schema, the validators and the version
constants, so a document produced against one copy can be validated by
the other's schema revision.workspace:^publishes as^0.5.0. Under 0.x that admits 0.5.x, so a
dsl patch reaches dependents without republishing them while a dsl
minor still requires a deliberate dependent release. All three
packages take a patch version increase, both because
check-package-version-bumps.mjs requires one and because the corrected
range only reaches consumers through a new release.The tarball smoke test now asserts the packed range, so an exact pin
cannot come back unnoticed. -
The publish workflow's build-integrity guard ran
git diff --quiet -- packages/@openmaic, which compares the worktree to the INDEX. The
steps before it run third-party package code, which needs no
credential to rewrite a tracked file andgit addit, after which the
guard reported clean and the rewritten bytes were packed and
published. Both copies of the guard now compare against HEAD, the
commit actually being released. -
@openmaic/storage's PostgreSQL contract suites refuse to skip when
STORAGE_PG_CONTRACT_REQUIRED=1, but that refusal is athrowinside
the test modules, so it never fires if vitest stops collecting them.
Collection is decided bypackages/@openmaic/storage/vitest.config.ts,
which is on the ignore list of publishable inputs, so excluding
*.pg.test.tsthere turns both PostgreSQL backends off with a green
run and no version bump.scripts/assert-pg-contract-suites.mjs reads vitest's json results and
requires both suite files to be present, passed and non-empty, keyed
on the file names rather than on a count. It runs in the publish
workflow's validate job and in the storage PostgreSQL contract
workflow.
Merging this publishes @openmaic/storage@0.1.2, @openmaic/renderer@0.0.4
and @openmaic/importer@0.1.2. That republish is the fix for (1), not a
side effect of it: the corrected dependency range exists only in a
tarball that has not been cut yet.- fix(release): audit the contract database, pin the format version to a minor
Follow-up on cross-vendor review of the three release-path fixes.
-
The PostgreSQL assertion proved far less than it claimed. Vitest's
assertionResultsare test CASES, notexpect()calls, so one
test('x', () => {})per required filename satisfied it — and the
whole surface is reachable without a version bump, becausetest/is
on the same ignore list asvitest.config.ts. Avi.mock('pg', ...)
in the already-wiredtest/setup.ts, or a stubbed suite body, made it
print "ran against a real database" having checked nothing of the
sort.The script now runs in two phases. Phase 1 keeps the file/status
checks. Phase 2 connects to PG_CONTRACT_URL from outside the vitest
process and requires all five tables the two backends own to exist and
to show inserts in pg_stat_user_tables. Nothing insidetest/can
forge that. Cumulative insert counters rather than surviving rows,
because the suites clean up after themselves. The success message now
states only what was checked. -
Validation packed with lifecycle scripts while publishing runs
--ignore-scripts, so committingworkspace:*plus aprepackthat
rewrites it toworkspace:^passed the tarball assertion and then
published the exact pin. Both the smoke test and the workflow's
dry-run pack now pass--config.ignore-scripts=true, so validation
and publication have identical lifecycle semantics. -
workspace:^unpins the serialized-format version that the exact pin
was silently holding. DSL_VERSION and RUNTIME_DSL_VERSION are
decoupled from the npm version by design, and storage compares them by
value across the package boundary. Under^0.5.0a dsl PATCH could
therefore hand a new format to an already-published storage: one
install resolves it and stamps the new version into document_stages,
another install pinned to the older patch reads that row and hard-
fails. Two installs of one published storage version, data-
incompatible.check-package-version-bumps.mjs now requires a change to either
constant to carry at least a MINOR increase of the dsl package
version, since a caret does not cross a 0.x minor. The rule is
documented next to both constants. That doc edit is itself a
publishable change, so dsl takes a patch bump to 0.5.1. -
The range and build-integrity guards ran only in the publish workflow,
so a pull request restoringworkspace:*or leaving a stale generated
file failed at release time, after a version number had been spent.
ci.yml gains a cheap source-level range check
(scripts/check-internal-dependency-ranges.mjs) and the
git diff --quiet HEADintegrity check after its install step. The
tarball assertion stays where it is as the release-time proof of
actual published behaviour.
Merging this now publishes @openmaic/dsl@0.5.1 as well as
@openmaic/storage@0.1.2, @openmaic/renderer@0.0.4 and
@openmaic/importer@0.1.2.- fix(release): fail the gates closed, and state what they actually prove
Confirmation round on the release-path fixes.
-
The format-version rule failed open if version.ts moved. It hard-coded
one source path and returned successfully when either revision lacked
it, so renaming the file, changing DSL_VERSION and bumping dsl by a
patch passed both checks. It now reports an error whenever the file is
absent at either revision while dsl has any publishable change. The
constant regex is anchored to the start of a line, so a commented-out
declaration cannot be read as the value, and a duplicate declaration is
an error rather than a coin flip. -
"At least a MINOR" was wrong past 1.0.0:
^1.0.0admits minors, so a
format change shipped as a minor would float into published dependents
exactly as the rule exists to prevent. The rule is now stated as what
it needs to be — an increase the dependents' caret range will NOT admit
— computed from the pre-change version: a minor while dsl is 0.x, a
major once it reaches 1.0.0. The error message and the docs in
version.ts say the same thing. -
Internal declarations outside
dependenciesbypassed both range
checks. Keepingdependenciesatworkspace:^while adding an exact
peerDependenciesoroptionalDependenciesentry published a second,
tighter constraint and passed. An owned package must now appear exactly
once, independencies; entries in the peer and optional fields are
rejected outright, in the source check and in the packed-manifest
assertion. The global count test is replaced by naming the three
dependents that must be present. -
The format rule compared against the base tip, so an un-rebased branch
produced an inverted message. It now uses the merge base, matching the
base...HEADform the rest of diff mode uses. Deliberately not applied
to the version comparison, where the tip is the correct reference. -
Phase 2 had no baseline, so cumulative counters from an earlier run
satisfied it forever against a non-ephemeral database. It now captures
the counters before the vitest step and requires a positive delta.
Both workflows gained the capture step. -
Phase 2 overclaimed. It proves rows were inserted into those five
tables during the run, not that the built PgDocumentStore and
PgRuntimeStore inserted them —test/is on the publishable-input
ignore list, so test code could write directly. The success message now
says exactly that, and the script documents the threat model: this
catches accidental silencing, which is the defect it was written for;
it is not a defence against someone who can merge changes totest/,
who can alter production code just as easily.
Also: scripts/openmaic-packages.mjs is now the single source for the
owned package list, consumed by all three checking scripts, and it
cross-checks itself against both the packages directory and
publish-packages.yml so a package added in one place and forgotten in
another fails rather than going quietly exempt. The workflow's own YAML
lists cannot import it, which is why they are cross-checked instead.- fix(release): let the format-version source move without opening the gate
Failing closed on a missing version.ts is right, but on its own it made a
rename impossible to merge: the new path does not exist at the base
revision either, so the check refused both before and after the move.DSL_VERSION_SOURCES is now an ordered candidate list, resolved per
revision against the first path that exists there. A rename lands by
prepending the new path in the same change, and the comparison still
happens across the move. Absent everywhere at either revision, while dsl
has any publishable change, remains an error.- fix(release): report package-list drift instead of crashing on it
Every check in check-internal-dependency-ranges.mjs reads the shared
package list, so a list naming a package that does not exist threw an
ENOENT out of readManifest before any finding was printed. It failed
closed, but unreadably. The list is now validated first and fatally, with
its own headline.- fix(release): anchor the caret to the base branch, and check the gates' own inputs
Final confirmation round.
-
The caret escape was computed from the merge base, which is the wrong
reference for it. With merge base dsl 0.5.1, an ordinary minor landing
on main taking the tip to 0.6.0, and HEAD at 0.6.1 with a changed
DSL_VERSION, the rule computed the escape from 0.5.1, got 0.6.0, and
accepted 0.6.1 — but dependents released against 0.6.0 publish
^0.6.0, which admits 0.6.1, so the new format reached them anyway.
0.6.1 is also the minimum the ordinary version check allows, making
that the default outcome rather than an unlucky one.The escape is now computed from the HIGHEST dsl version reachable on
the base branch. Whether the format moved still comes from the merge
base, because that is a question about the branch. The comment on
mergeBaseWithHead now records that it is a no-op in both current CI
invocations (checkout uses the merge ref on pull_request, and the
before-SHA is an ancestor on push) rather than claiming a case that is
not exercised there. -
The workflow cross-check matched raw text, so a commented-out trigger
path still satisfied it while silently disabling that package's
release. Full-line comments are now stripped first; theon.push.paths
list and everyfor pkg in ...loop are compared as exact sets, so an
unexpected entry fails as well as a missing one; and--filterusage
is checked more loosely, because individual steps legitimately filter
subsets. The limits of a textual check are stated where it lives. -
locate()returned the first candidate format-version source that
existed, so during a half-finished rename the gate could compare a file
that is no longer the exported one. It now resolves every candidate per
revision and fails unless exactly one declares the constants. -
INTERNAL_DEPENDENTS was checked in one direction only, so deleting an
entry exempted that package from the source check and from the packed
assertion that iterates the same map. It is now cross-checked both
ways, an owned package in another owned package's devDependencies is
rejected outright, and multiple owned dependencies no longer collapse
to whichever was declared last. -
assertPackageListIsComplete() now also runs in release mode, which is
the gate that decides what gets published; and the PostgreSQL script
consumes flag values properly instead of treating the first non-flag
argument as the results path.
Also documents, in openmaic-packages.mjs, the threat model these gates
share: they catch mistakes, and anyone who can merge edits to scripts/
can edit production code directly, so deliberate subversion is out of
scope and cannot be closed by adding further checks.下载附件