发布

  • fix(release): dedupe the published dsl, close two release-path blind spots (#1019)

    frostbyte_neo 发布于 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.

    1. @openmaic/storage, @openmaic/renderer and @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.

    2. 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 and git add it, 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.

    3. @openmaic/storage's PostgreSQL contract suites refuse to skip when
      STORAGE_PG_CONTRACT_REQUIRED=1, but that refusal is a throw inside
      the test modules, so it never fires if vitest stops collecting them.
      Collection is decided by packages/@openmaic/storage/vitest.config.ts,
      which is on the ignore list of publishable inputs, so excluding
      *.pg.test.ts there 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.

    1. The PostgreSQL assertion proved far less than it claimed. Vitest's
      assertionResults are test CASES, not expect() calls, so one
      test('x', () => {}) per required filename satisfied it — and the
      whole surface is reachable without a version bump, because test/ is
      on the same ignore list as vitest.config.ts. A vi.mock('pg', ...)
      in the already-wired test/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 inside test/ 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.

    2. Validation packed with lifecycle scripts while publishing runs
      --ignore-scripts, so committing workspace:* plus a prepack that
      rewrites it to workspace:^ 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.

    3. 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.0 a 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.

    4. The range and build-integrity guards ran only in the publish workflow,
      so a pull request restoring workspace:* 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 HEAD integrity 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.

    1. 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.

    2. "At least a MINOR" was wrong past 1.0.0: ^1.0.0 admits 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.

    3. Internal declarations outside dependencies bypassed both range
      checks. Keeping dependencies at workspace:^ while adding an exact
      peerDependencies or optionalDependencies entry published a second,
      tighter constraint and passed. An owned package must now appear exactly
      once, in dependencies; 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.

    4. 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...HEAD form the rest of diff mode uses. Deliberately not applied
      to the version comparison, where the tip is the correct reference.

    5. 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.

    6. 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 to test/,
      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.

    1. 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.

    2. 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; the on.push.paths
      list and every for pkg in ... loop are compared as exact sets, so an
      unexpected entry fails as well as a missing one; and --filter usage
      is checked more loosely, because individual steps legitimately filter
      subsets. The limits of a textual check are stated where it lives.

    3. 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.

    4. 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.

    5. 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.

    下载附件