cultnet-ts 0.4.0 release notes; npm publish gate runs the declared-range check; fixture builds like cultnet-ts
All checks were successful
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Successful in 26m28s
All checks were successful
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Successful in 26m28s
cultnet-ts's Unreleased section becomes [0.4.0]; cultmesh-ts and cultmesh-browser record the move to cultnet-ts ^0.4.0. release-after-suite builds the workspace and runs the range check for an npm tag release, before the package's tests. The fixture package generates a module from repo-level contracts/, so a tag built by a bare tsc or archived without contracts/ fails it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
47e8e95d86
commit
4fb07ab3bb
6 changed files with 35 additions and 12 deletions
|
|
@ -9,6 +9,10 @@ All notable changes to `@gamecult/cultmesh-browser` are recorded here, following
|
|||
|
||||
- The package is renamed `cultmesh-browser` to `@gamecult/cultmesh-browser` (the repo already scopes `@gamecult/cultcache-ts`), so the unscoped name cannot be claimed by someone else. Import from `@gamecult/cultmesh-browser` and its subpaths; the API is unchanged. Depends on `@gamecult/cultnet-ts`; the interop test uses `@gamecult/cultmesh-ts`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Needs `@gamecult/cultnet-ts` `^0.4.0` (was `^0.3.0`), moving with the `cultnet-ts` 0.4.0 release.
|
||||
|
||||
## [0.4.0]
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ All notable changes to `@gamecult/cultmesh-ts` are recorded here, following
|
|||
|
||||
- Needs the cultcache-ts release that ships with this change (its manifest range moves with that release): it imports `schemaIdentityOf` and `SchemaConflictError`, which 0.14.x does not export.
|
||||
|
||||
- Needs `@gamecult/cultnet-ts` `^0.4.0` (was `^0.3.0`): this package imports names that `cultnet-ts` 0.3.0 does not export (`cultNetMessageInvalidError`, `cultNetTextError`, `cultNetWriteNotAuthorizedError`, `WriteTracker`, `CULTNET_WRITE_RECEIPT_MESSAGE_VERSIONS`, `helloAdvertisesWriteReceipts`, `resultWindowMs`). The declared-range check (`npm run test:pack:ts`) found the old range admitting a release that lacks them.
|
||||
|
||||
## [0.2.0]
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
All notable changes to `@gamecult/cultnet-ts` are recorded here, following
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## Unreleased
|
||||
## [0.4.0]
|
||||
|
||||
### Breaking
|
||||
|
||||
|
|
|
|||
|
|
@ -857,7 +857,7 @@ test("workflow: each job releases only through release-after-suite.mjs", () => {
|
|||
});
|
||||
|
||||
test("release-after-suite checks the tag, then runs the suite, the semver check and the package's tests, and releases only when all passed", async () => {
|
||||
const { releaseAfterSuite, suite, checker } = await import("./release-after-suite.mjs");
|
||||
const { releaseAfterSuite, suite, checker, rangeCheck } = await import("./release-after-suite.mjs");
|
||||
const npmVersion = JSON.parse(readFileSync(join(repoRoot, "packages", "cultcache-ts", "package.json"), "utf8")).version;
|
||||
const npmDir = join(repoRoot, "packages", "cultcache-ts");
|
||||
const pyDir = join(repoRoot, "packages", "cultnet-py");
|
||||
|
|
@ -881,13 +881,15 @@ test("release-after-suite checks the tag, then runs the suite, the semver check
|
|||
const npmCalls = [
|
||||
suiteCall,
|
||||
checkerCall("@gamecult/cultcache-ts", npmVersion),
|
||||
["npm", ["run", "build:ts"], repoRoot, checked],
|
||||
[process.execPath, [rangeCheck], repoRoot, checked],
|
||||
["npm", ["test"], npmDir, checked],
|
||||
["npm", ["publish", "--access", "public"], npmDir, env],
|
||||
];
|
||||
assert.equal(go(["npm-publish", npmDir, npmTag]), 0);
|
||||
assert.deepEqual(calls, npmCalls);
|
||||
// any check that fails stops the release at that check
|
||||
for (const fail of [0, 1, 2]) {
|
||||
for (const fail of [0, 1, 2, 3, 4]) {
|
||||
assert.equal(go(["npm-publish", npmDir, npmTag], fail), 1, `check ${fail}`);
|
||||
assert.deepEqual(calls, npmCalls.slice(0, fail + 1), `check ${fail}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// How publish-packages.yml releases a package. From this one process, in order: the release tag must name
|
||||
// the package's declared prefix and its manifest version, the release-checks suite must pass, the semver
|
||||
// policy check must pass for that version, the package's own tests must pass, and only then the release
|
||||
// action runs. No workflow condition can then release what any of those checks did not pass.
|
||||
// policy check must pass for that version, an npm release's declared dependency ranges must hold (the
|
||||
// workspace is built, then every tagged release a sibling's range admits is checked against what the
|
||||
// sibling imports), the package's own tests must pass, and only then the release action runs. No workflow
|
||||
// condition can then release what any of those checks did not pass.
|
||||
// docs/semver-policy.md, "The suite gates every release".
|
||||
//
|
||||
// node scripts/release-after-suite.mjs npm-publish <package dir> <git ref> npm publish --access public in <dir>
|
||||
|
|
@ -18,6 +20,7 @@ import releasePackages from "./release-packages.mjs";
|
|||
|
||||
export const suite = fileURLToPath(new URL("./check-changelog-semver.test.mjs", import.meta.url));
|
||||
export const checker = fileURLToPath(new URL("./check-changelog-semver.mjs", import.meta.url));
|
||||
export const rangeCheck = fileURLToPath(new URL("./test-declared-dependency-ranges.mjs", import.meta.url));
|
||||
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
|
||||
// The checks run without the variables that can make `node --test` run nothing and exit 0
|
||||
|
|
@ -34,6 +37,8 @@ export const actions = {
|
|||
"npm-publish": {
|
||||
manifest: (dir) => JSON.parse(readFileSync(join(dir, "package.json"), "utf8")),
|
||||
test: (dir) => ["npm", ["test"], dir],
|
||||
// The range check builds each tagged release against the workspace's built dists, so it follows a workspace build.
|
||||
ranges: [["npm", ["run", "build:ts"], repoRoot], [process.execPath, [rangeCheck], repoRoot]],
|
||||
release: (dir) => ["npm", ["publish", "--access", "public"], dir],
|
||||
tagOnly: true,
|
||||
},
|
||||
|
|
@ -83,6 +88,9 @@ export function releaseAfterSuite(argv, run = spawn, env = process.env) {
|
|||
if (isTag && run(process.execPath, [checker, "--package", manifest.name, "--version", manifest.version, "--cwd", repoRoot], undefined, clean) !== 0) {
|
||||
return refuse(`${manifest.name} ${manifest.version} fails the semver policy (docs/semver-policy.md)`);
|
||||
}
|
||||
for (const [command, args, cwd] of isTag ? action.ranges ?? [] : []) {
|
||||
if (run(command, args, cwd, clean) !== 0) return refuse(`${manifest.name}'s declared dependency ranges do not hold (scripts/test-declared-dependency-ranges.mjs)`);
|
||||
}
|
||||
if (run(...action.test(dir), clean) !== 0) return refuse(`${manifest.name}'s own tests failed`);
|
||||
return run(...action.release(dir), env);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// test-declared-dependency-ranges.mjs against a fixture repo: a dependency that gained an export a dependent imports,
|
||||
// while its manifest still carries the version of its latest release tag, must fail the check, because a consumer's
|
||||
// range admits that tagged release and it lacks the export. Bumping the version and the range makes it pass.
|
||||
// The fixture package a builds like cultnet-ts: its build script first generates a module from the repo-level contracts/
|
||||
// directory, so a tag built by a bare tsc, or archived without contracts/, fails here as it does on the real tree.
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
|
|
@ -17,17 +19,22 @@ const write = (root, path, text) => {
|
|||
writeFileSync(join(root, path), text);
|
||||
};
|
||||
const git = (root, ...args) => execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...args], { cwd: root, stdio: "pipe" });
|
||||
const manifest = (name, version, extra = {}) =>
|
||||
JSON.stringify({ name: `@t/${name}`, version, scripts: { build: "node ../../node_modules/typescript/bin/tsc -p tsconfig.json" }, ...extra });
|
||||
const tsc = "node ../../node_modules/typescript/bin/tsc -p tsconfig.json";
|
||||
const manifest = (name, version, extra = {}, build = tsc) =>
|
||||
JSON.stringify({ name: `@t/${name}`, version, scripts: { build }, ...extra });
|
||||
const generated = "node tools/generate.mjs && " + tsc;
|
||||
const tsconfig = JSON.stringify({ compilerOptions: { outDir: "dist", module: "commonjs", target: "es2020", declaration: false }, include: ["src"] });
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "declared-ranges-fixture-"));
|
||||
write(root, "package.json", JSON.stringify({ name: "fixture", private: true, workspaces: ["packages/a", "packages/b"] }));
|
||||
write(root, "contracts/.keep", "");
|
||||
write(root, "packages/a/package.json", manifest("a", "1.0.0"));
|
||||
write(root, "contracts/value.json", "2");
|
||||
write(root, "packages/a/tools/generate.mjs",
|
||||
'import { readFileSync, writeFileSync } from "node:fs";\n' +
|
||||
'writeFileSync("src/data.ts", `export const contractValue = ${readFileSync("../../contracts/value.json", "utf8")};\\n`);\n');
|
||||
write(root, "packages/a/package.json", manifest("a", "1.0.0", {}, generated));
|
||||
write(root, "packages/a/tsconfig.json", tsconfig);
|
||||
write(root, "packages/a/src/index.ts", "export const foo = 1;\n");
|
||||
write(root, "packages/a/src/index.ts", 'export { contractValue } from "./data";\nexport const foo = 1;\n');
|
||||
write(root, "packages/b/package.json", manifest("b", "1.0.0", { dependencies: { "@t/a": "^1.0.0" } }));
|
||||
write(root, "packages/b/tsconfig.json", tsconfig);
|
||||
write(root, "packages/b/src/index.ts", 'import { foo } from "@t/a";\nexport const b = foo;\n');
|
||||
|
|
@ -48,13 +55,13 @@ test("a dependency that gained an export a dependent imports, still at its lates
|
|||
try {
|
||||
assert.equal(check(root).status, 0, "the fixture passes before the export is added");
|
||||
|
||||
write(root, "packages/a/src/index.ts", "export const foo = 1;\nexport const bar = 2;\n");
|
||||
write(root, "packages/a/src/index.ts", 'export { contractValue } from "./data";\nexport const foo = 1;\nexport const bar = 2;\n');
|
||||
write(root, "packages/b/src/index.ts", 'import { foo, bar } from "@t/a";\nexport const b = foo + bar;\n');
|
||||
const stale = check(root);
|
||||
assert.notEqual(stale.status, 0, stale.stdout);
|
||||
assert.match(stale.stderr, /a-v1\.0\.0 does not export bar/u);
|
||||
|
||||
write(root, "packages/a/package.json", manifest("a", "1.1.0"));
|
||||
write(root, "packages/a/package.json", manifest("a", "1.1.0", {}, generated));
|
||||
write(root, "packages/b/package.json", manifest("b", "1.0.0", { dependencies: { "@t/a": "^1.1.0" } }));
|
||||
const bumped = check(root);
|
||||
assert.equal(bumped.status, 0, bumped.stderr);
|
||||
|
|
|
|||
Loading…
Reference in a new issue