Files
webstudio/packages/cli/scripts/builder-contract-gate.test.ts
Oleg Isonen bc8630f413 fix: Gate Builder releases on the published CLI contract (#6282)
Closes #6280

## Summary

- Expose the deployed Builder's CLI-facing bundle contract through a
non-cached, read-only endpoint.
- Resolve and execute the exact `webstudio@latest` registry artifact,
observe the bundle version sent by its real `sync` request, and require
an exact match before assigning the release staging alias.
- Include expected and received bundle versions in runtime and
release-gate errors.

## Technical choices

- The existing generated `bundleVersion` remains the single
compatibility boundary. Builder-only changes outside that project-bundle
contract do not require a CLI release.
- The CLI sync probe runs against a local fixture API and temporary
directory. It reads the deployed runtime contract but never reads or
modifies a Cloud project.
- Verification runs after Vercel creates the exact production-built
deployment and before the release staging alias is assigned. A mismatch
therefore leaves production domains untouched.
- Registry resolution requires version and integrity metadata, then
executes the resolved immutable version rather than source from the
checkout.
- The gate requires the deployed Builder origin and cannot fall back to
the checkout's contract.

## Todo

1. [x] Add the deployed Builder contract endpoint.
2. [x] Probe the exact published CLI through its real sync request.
3. [x] Gate the release staging alias on an exact contract match.
4. [x] Report expected and received versions for compatibility failures.
5. [x] Add regression tests and verify the production Builder build.
6. [x] Review documentation and fixture impact.

## Verification

- `pnpm --filter @webstudio-is/builder exec vitest run
app/services/build-router.server.test.ts
app/services/cli-compatibility-route.node.test.ts` — 12 passed.
- `pnpm --filter webstudio exec vitest run
scripts/builder-contract-gate.test.ts
scripts/release-smoke-registry.test.ts` — 7 passed.
- Rebased onto `main` at `9ec4c820e1`.
- Builder and CLI typechecks passed.
- `pnpm lint` and changed-file formatting passed.
- `pnpm --filter @webstudio-is/builder build` passed and emitted
`/rest/cli-compatibility`.
- The changed GitHub Action parses as valid YAML.
- Real registry probe passed for `webstudio@0.293.0` against
`bundle-kzb45v`.
- Real registry probe correctly blocked current Builder contract
`bundle-1mym82p` and reported expected `bundle-1mym82p`, received
`bundle-kzb45v`.
- Missing staged Builder origin fails closed before registry execution.
- GitHub CI passed all four workflows, including all six Builder E2E
shards.
- The formerly failing empty-MDX insertion workflow passed 8 repeated
runs with 4 workers locally, then passed in Builder E2E shard 4 in CI.
- Full browser-based release smoke was not run, per the request not to
use Chrome. Agent evaluations were not run because no evaluation
approval was requested or received.
- Documentation: no user-facing documentation changes are required.
- Fixtures/generated outputs: no fixture inputs or generated outputs are
affected.

## Operational boundary

The check needs the exact deployed runtime, so Vercel creates an
unaliased `--skip-domain` deployment before verification. A failed check
cannot affect production domains or the normal release alias. Repository
automation cannot prevent a Vercel administrator from manually promoting
an arbitrary deployment outside this release path.
2026-08-31 20:43:52 +02:00

72 lines
2.3 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import {
assertPublishedCliContract,
loadBuilderBundleVersion,
} from "./builder-contract-gate";
describe("published CLI contract gate", () => {
test("accepts the exact Builder bundle contract", () => {
expect(() =>
assertPublishedCliContract({
cliVersion: "0.293.0",
expectedVersion: "bundle-shared",
receivedVersion: "bundle-shared",
})
).not.toThrow();
});
test("reports the expected and received bundle contracts", () => {
expect(() =>
assertPublishedCliContract({
cliVersion: "0.293.0",
expectedVersion: "bundle-builder",
receivedVersion: "bundle-cli",
})
).toThrowError(
"Published CLI contract mismatch for webstudio@0.293.0. Expected bundle version bundle-builder, received bundle-cli. Publish a matching CLI before deploying Builder."
);
});
test("rejects a CLI that does not send a bundle contract", () => {
expect(() =>
assertPublishedCliContract({
cliVersion: "0.293.0",
expectedVersion: "bundle-builder",
receivedVersion: undefined,
})
).toThrowError(
"Published CLI contract mismatch for webstudio@0.293.0. Expected bundle version bundle-builder, received missing. Publish a matching CLI before deploying Builder."
);
});
test("reads the contract from the staged Builder runtime", async () => {
const request = vi.fn(async () =>
Response.json({ bundleVersion: "bundle-runtime" })
);
await expect(
loadBuilderBundleVersion("https://candidate.example", {
attempts: 1,
request,
})
).resolves.toBe("bundle-runtime");
expect(request).toHaveBeenCalledWith(
new URL("https://candidate.example/rest/cli-compatibility"),
expect.objectContaining({ cache: "no-store" })
);
});
test("rejects an invalid staged Builder contract response", async () => {
const request = vi.fn(async () => Response.json({}));
await expect(
loadBuilderBundleVersion("https://candidate.example", {
attempts: 1,
request,
})
).rejects.toThrowError(
"Could not read the CLI contract from https://candidate.example/rest/cli-compatibility after 1 attempt: Builder CLI compatibility response has no bundle version."
);
});
});