@warlock.js/core 5.36.0 → 5.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/esm/cli/commands/migrate.command.mjs +10 -0
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/release/release-command.action.mjs +245 -0
  5. package/esm/cli/commands/release/release-command.action.mjs.map +1 -0
  6. package/esm/cli/commands/release.command.mjs +66 -0
  7. package/esm/cli/commands/release.command.mjs.map +1 -0
  8. package/esm/cli/framework-cli-commands.mjs +2 -0
  9. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  10. package/esm/database/migrate-action.mjs +24 -2
  11. package/esm/database/migrate-action.mjs.map +1 -1
  12. package/esm/dev-server/loader/load-hook.d.mts.map +1 -1
  13. package/esm/dev-server/loader/load-hook.mjs +6 -0
  14. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  15. package/esm/index.d.mts +3 -2
  16. package/esm/index.mjs +2 -2
  17. package/esm/production/route-registration-snapshot.mjs +10 -3
  18. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  19. package/esm/release/activate.mjs +261 -0
  20. package/esm/release/activate.mjs.map +1 -0
  21. package/esm/release/candidate.mjs +107 -0
  22. package/esm/release/candidate.mjs.map +1 -0
  23. package/esm/release/cascade-migration-provider.mjs +32 -0
  24. package/esm/release/cascade-migration-provider.mjs.map +1 -0
  25. package/esm/release/config.mjs +60 -0
  26. package/esm/release/config.mjs.map +1 -0
  27. package/esm/release/gate.mjs +67 -0
  28. package/esm/release/gate.mjs.map +1 -0
  29. package/esm/release/migrations-proof.mjs +100 -0
  30. package/esm/release/migrations-proof.mjs.map +1 -0
  31. package/esm/release/prune.mjs +42 -0
  32. package/esm/release/prune.mjs.map +1 -0
  33. package/esm/release/report.mjs +25 -0
  34. package/esm/release/report.mjs.map +1 -0
  35. package/esm/release/runner.mjs +55 -0
  36. package/esm/release/runner.mjs.map +1 -0
  37. package/esm/release/semver.mjs +53 -0
  38. package/esm/release/semver.mjs.map +1 -0
  39. package/esm/release/stage.mjs +122 -0
  40. package/esm/release/stage.mjs.map +1 -0
  41. package/esm/release/store.mjs +131 -0
  42. package/esm/release/store.mjs.map +1 -0
  43. package/esm/release/types.d.mts +102 -0
  44. package/esm/release/types.d.mts.map +1 -0
  45. package/esm/release/verify.mjs +94 -0
  46. package/esm/release/verify.mjs.map +1 -0
  47. package/esm/router/index.d.mts +2 -2
  48. package/esm/router/index.mjs +2 -2
  49. package/esm/router/router.d.mts +4 -2
  50. package/esm/router/router.d.mts.map +1 -1
  51. package/esm/router/router.mjs +13 -5
  52. package/esm/router/router.mjs.map +1 -1
  53. package/esm/router/types.d.mts +26 -0
  54. package/esm/router/types.d.mts.map +1 -1
  55. package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
  56. package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
  57. package/esm/router/unguarded-route-diagnostics.mjs +10 -2
  58. package/esm/router/unguarded-route-diagnostics.mjs.map +1 -1
  59. package/esm/tooling/check/security-conventions.mjs +19 -1
  60. package/esm/tooling/check/security-conventions.mjs.map +1 -1
  61. package/esm/tooling/inspect/collect-routes.mjs +3 -2
  62. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  63. package/esm/validation/init.mjs +11 -4
  64. package/esm/validation/init.mjs.map +1 -1
  65. package/esm/validation/translate-rule.mjs +63 -0
  66. package/esm/validation/translate-rule.mjs.map +1 -0
  67. package/esm/warlock-config/index.d.mts +2 -1
  68. package/esm/warlock-config/types.d.mts +8 -0
  69. package/esm/warlock-config/types.d.mts.map +1 -1
  70. package/llms-full.txt +94 -2
  71. package/llms.txt +1 -0
  72. package/package.json +13 -13
  73. package/skills/deploy-release/SKILL.md +76 -0
  74. package/skills/index.md +1 -1
  75. package/skills/register-route/SKILL.md +12 -0
  76. package/skills/warlock-conventions/SKILL.md +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/candidate.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { access, mkdir, readFile, rm, symlink } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { appendLog, releaseDir, releasesDir, writeManifest, type ReleaseContext } from \"./store\";\nimport type { ReleaseManifest } from \"./types\";\n\nconst LOCKFILES = [\"pnpm-lock.yaml\", \"package-lock.json\", \"yarn.lock\", \"bun.lock\", \"bun.lockb\"];\n\nexport function shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nexport type CandidateOptions = {\n /** Commit to freeze. Defaults to `HEAD`. */\n commit?: string;\n /** Freeze even when the working tree has uncommitted changes. */\n allowDirty?: boolean;\n};\n\nasync function exists(file: string): Promise<boolean> {\n try {\n await access(file);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function git(context: ReleaseContext, command: string): Promise<string> {\n const result = await context.runner(`git ${command}`, { cwd: context.root });\n\n if (result.code !== 0) {\n throw new Error(`git ${command} failed: ${result.output.trim() || `exit ${result.code}`}`);\n }\n\n return result.output.trim();\n}\n\n/**\n * Freeze a commit into a candidate directory.\n *\n * The commit is exported with `git archive` into `<release>/source`, so every\n * later step (gate, build, stage) works on exactly those bytes and not on a\n * working tree that keeps changing. A dirty tree is refused unless\n * `allowDirty` is set, because the candidate would not describe what is\n * checked out.\n *\n * @example\n * const manifest = await createCandidate(context, {});\n */\nexport async function createCandidate(\n context: ReleaseContext,\n options: CandidateOptions = {},\n): Promise<ReleaseManifest> {\n if (!options.allowDirty) {\n const exclude = shellQuote(`:(exclude)${context.config.directory}`);\n const dirty = await git(context, `status --porcelain -- . ${exclude}`);\n\n if (dirty) {\n throw new Error(\n `The working tree has uncommitted changes:\\n${dirty}\\nCommit or stash them, or pass --allow-dirty.`,\n );\n }\n }\n\n const commit = await git(\n context,\n `rev-parse --verify ${shellQuote(`${options.commit ?? \"HEAD\"}^{commit}`)}`,\n );\n const packageText = await git(context, `show ${shellQuote(`${commit}:package.json`)}`);\n const version = (JSON.parse(packageText) as { version?: string }).version;\n\n if (!version) throw new Error(\"package.json has no `version`; a release needs one.\");\n\n const id = `${version}-${commit.slice(0, 7)}`;\n const directory = releaseDir(context, id);\n\n if (await exists(directory)) {\n throw new Error(`Release \"${id}\" already exists. Releases are never overwritten.`);\n }\n\n const source = path.join(directory, \"source\");\n\n await mkdir(releasesDir(context), { recursive: true });\n await mkdir(source, { recursive: true });\n\n try {\n // Two steps rather than a pipe: a failing `git archive` must not be\n // masked by the exit code of `tar`.\n const archive = path.join(directory, \"source.tar\");\n\n await git(context, `archive --format=tar -o ${shellQuote(archive)} ${shellQuote(commit)}`);\n\n const extracted = await context.runner(\n `tar -xf ${shellQuote(archive)} -C ${shellQuote(source)}`,\n { cwd: context.root },\n );\n\n await rm(archive, { force: true });\n\n if (extracted.code !== 0) {\n throw new Error(`Could not extract the commit archive: ${extracted.output.trim()}`);\n }\n\n if (context.config.linkNodeModules) {\n const modules = path.join(context.root, \"node_modules\");\n\n if (await exists(modules)) await symlink(modules, path.join(source, \"node_modules\"), \"dir\");\n }\n\n let lockfile: ReleaseManifest[\"lockfile\"] = null;\n\n for (const file of LOCKFILES) {\n const location = path.join(source, file);\n\n if (await exists(location)) {\n lockfile = {\n file,\n sha256: createHash(\"sha256\")\n .update(await readFile(location))\n .digest(\"hex\"),\n };\n break;\n }\n }\n\n const manifest: ReleaseManifest = {\n schema: 1,\n id,\n version,\n commit,\n createdAt: context.now().toISOString(),\n nodeVersion: context.nodeVersion,\n lockfile,\n status: \"candidate\",\n };\n\n await writeManifest(context, manifest);\n await appendLog(context, { event: \"candidate\", release: id, ok: true, detail: commit });\n\n return manifest;\n } catch (error) {\n // The directory was created by this call and holds no finished release.\n await rm(directory, { recursive: true, force: true });\n throw error;\n }\n}\n"],"mappings":";;;;;;AAMA,MAAM,YAAY;CAAC;CAAkB;CAAqB;CAAa;CAAY;AAAW;AAE9F,SAAgB,WAAW,OAAuB;CAChD,OAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,EAAE;AAC1C;AASA,eAAe,OAAO,MAAgC;CACpD,IAAI;EACF,MAAM,OAAO,IAAI;EACjB,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,IAAI,SAAyB,SAAkC;CAC5E,MAAM,SAAS,MAAM,QAAQ,OAAO,OAAO,WAAW,EAAE,KAAK,QAAQ,KAAK,CAAC;CAE3E,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,MAAM,OAAO,QAAQ,WAAW,OAAO,OAAO,KAAK,KAAK,QAAQ,OAAO,QAAQ;CAG3F,OAAO,OAAO,OAAO,KAAK;AAC5B;;;;;;;;;;;;;AAcA,eAAsB,gBACpB,SACA,UAA4B,CAAC,GACH;CAC1B,IAAI,CAAC,QAAQ,YAAY;EAEvB,MAAM,QAAQ,MAAM,IAAI,SAAS,2BADjB,WAAW,aAAa,QAAQ,OAAO,WACW,GAAG;EAErE,IAAI,OACF,MAAM,IAAI,MACR,8CAA8C,MAAM,+CACtD;CAEJ;CAEA,MAAM,SAAS,MAAM,IACnB,SACA,sBAAsB,WAAW,GAAG,QAAQ,UAAU,OAAO,UAAU,GACzE;CACA,MAAM,cAAc,MAAM,IAAI,SAAS,QAAQ,WAAW,GAAG,OAAO,cAAc,GAAG;CACrF,MAAM,UAAW,KAAK,MAAM,WAAW,CAAC,CAA0B;CAElE,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,qDAAqD;CAEnF,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,MAAM,GAAG,CAAC;CAC1C,MAAM,YAAY,WAAW,SAAS,EAAE;CAExC,IAAI,MAAM,OAAO,SAAS,GACxB,MAAM,IAAI,MAAM,YAAY,GAAG,kDAAkD;CAGnF,MAAM,SAAS,KAAK,KAAK,WAAW,QAAQ;CAE5C,MAAM,MAAM,YAAY,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;CACrD,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEvC,IAAI;EAGF,MAAM,UAAU,KAAK,KAAK,WAAW,YAAY;EAEjD,MAAM,IAAI,SAAS,2BAA2B,WAAW,OAAO,EAAE,GAAG,WAAW,MAAM,GAAG;EAEzF,MAAM,YAAY,MAAM,QAAQ,OAC9B,WAAW,WAAW,OAAO,EAAE,MAAM,WAAW,MAAM,KACtD,EAAE,KAAK,QAAQ,KAAK,CACtB;EAEA,MAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC;EAEjC,IAAI,UAAU,SAAS,GACrB,MAAM,IAAI,MAAM,yCAAyC,UAAU,OAAO,KAAK,GAAG;EAGpF,IAAI,QAAQ,OAAO,iBAAiB;GAClC,MAAM,UAAU,KAAK,KAAK,QAAQ,MAAM,cAAc;GAEtD,IAAI,MAAM,OAAO,OAAO,GAAG,MAAM,QAAQ,SAAS,KAAK,KAAK,QAAQ,cAAc,GAAG,KAAK;EAC5F;EAEA,IAAI,WAAwC;EAE5C,KAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,WAAW,KAAK,KAAK,QAAQ,IAAI;GAEvC,IAAI,MAAM,OAAO,QAAQ,GAAG;IAC1B,WAAW;KACT;KACA,QAAQ,WAAW,QAAQ,CAAC,CACzB,OAAO,MAAM,SAAS,QAAQ,CAAC,CAAC,CAChC,OAAO,KAAK;IACjB;IACA;GACF;EACF;EAEA,MAAM,WAA4B;GAChC,QAAQ;GACR;GACA;GACA;GACA,WAAW,QAAQ,IAAI,CAAC,CAAC,YAAY;GACrC,aAAa,QAAQ;GACrB;GACA,QAAQ;EACV;EAEA,MAAM,cAAc,SAAS,QAAQ;EACrC,MAAM,UAAU,SAAS;GAAE,OAAO;GAAa,SAAS;GAAI,IAAI;GAAM,QAAQ;EAAO,CAAC;EAEtF,OAAO;CACT,SAAS,OAAO;EAEd,MAAM,GAAG,WAAW;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EACpD,MAAM;CACR;AACF"}
@@ -0,0 +1,32 @@
1
+ import { loadAllMigrations } from "../database/migrate-action.mjs";
2
+ import { dataSourceRegistry, listExecutedMigrations, listPendingMigrations, runMigrations } from "@warlock.js/cascade";
3
+
4
+ //#region ../../packages/core/src/release/cascade-migration-provider.ts
5
+ /**
6
+ * The provider behind `warlock release migrations --prove`: migrations are
7
+ * loaded the way `warlock migrate` loads them, the ledger and the run go
8
+ * through cascade's migration API, and tables and indexes come from the
9
+ * data source's driver blueprint, so it works for any driver that has one.
10
+ *
11
+ * The database connector must already be started.
12
+ */
13
+ async function createCascadeMigrationProvider() {
14
+ await loadAllMigrations();
15
+ const blueprint = () => dataSourceRegistry.get().driver.blueprint;
16
+ return {
17
+ listExecuted: async () => (await listExecutedMigrations()).map((record) => record.name),
18
+ listPending: async () => (await listPendingMigrations()).map((migration) => migration.name),
19
+ listTables: () => blueprint().listTables(),
20
+ listIndexes: async (table) => (await blueprint().listIndexes(table)).map((index) => index.name),
21
+ runPending: async () => {
22
+ const results = await runMigrations();
23
+ const failed = results.find((result) => !result.success);
24
+ if (failed) throw new Error(`${failed.name}: ${failed.error ?? "failed"}`);
25
+ return results.filter((result) => result.direction === "up").map((result) => result.name);
26
+ }
27
+ };
28
+ }
29
+
30
+ //#endregion
31
+ export { createCascadeMigrationProvider };
32
+ //# sourceMappingURL=cascade-migration-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cascade-migration-provider.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/cascade-migration-provider.ts"],"sourcesContent":["import {\n dataSourceRegistry,\n listExecutedMigrations,\n listPendingMigrations,\n runMigrations,\n} from \"@warlock.js/cascade\";\nimport { loadAllMigrations } from \"../database/migrate-action\";\nimport type { MigrationProofProvider } from \"./migrations-proof\";\n\n/**\n * The provider behind `warlock release migrations --prove`: migrations are\n * loaded the way `warlock migrate` loads them, the ledger and the run go\n * through cascade's migration API, and tables and indexes come from the\n * data source's driver blueprint, so it works for any driver that has one.\n *\n * The database connector must already be started.\n */\nexport async function createCascadeMigrationProvider(): Promise<MigrationProofProvider> {\n await loadAllMigrations();\n\n const blueprint = () => dataSourceRegistry.get().driver.blueprint;\n\n return {\n listExecuted: async () => (await listExecutedMigrations()).map((record) => record.name),\n listPending: async () => (await listPendingMigrations()).map((migration) => migration.name),\n listTables: () => blueprint().listTables(),\n listIndexes: async (table) => (await blueprint().listIndexes(table)).map((index) => index.name),\n runPending: async () => {\n const results = await runMigrations();\n const failed = results.find((result) => !result.success);\n\n if (failed) throw new Error(`${failed.name}: ${failed.error ?? \"failed\"}`);\n\n return results.filter((result) => result.direction === \"up\").map((result) => result.name);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,eAAsB,iCAAkE;CACtF,MAAM,kBAAkB;CAExB,MAAM,kBAAkB,mBAAmB,IAAI,CAAC,CAAC,OAAO;CAExD,OAAO;EACL,cAAc,aAAa,MAAM,uBAAuB,EAAC,CAAE,KAAK,WAAW,OAAO,IAAI;EACtF,aAAa,aAAa,MAAM,sBAAsB,EAAC,CAAE,KAAK,cAAc,UAAU,IAAI;EAC1F,kBAAkB,UAAU,CAAC,CAAC,WAAW;EACzC,aAAa,OAAO,WAAW,MAAM,UAAU,CAAC,CAAC,YAAY,KAAK,EAAC,CAAE,KAAK,UAAU,MAAM,IAAI;EAC9F,YAAY,YAAY;GACtB,MAAM,UAAU,MAAM,cAAc;GACpC,MAAM,SAAS,QAAQ,MAAM,WAAW,CAAC,OAAO,OAAO;GAEvD,IAAI,QAAQ,MAAM,IAAI,MAAM,GAAG,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU;GAEzE,OAAO,QAAQ,QAAQ,WAAW,OAAO,cAAc,IAAI,CAAC,CAAC,KAAK,WAAW,OAAO,IAAI;EAC1F;CACF;AACF"}
@@ -0,0 +1,60 @@
1
+ //#region ../../packages/core/src/release/config.ts
2
+ const DEFAULT_CHECKS = [
3
+ {
4
+ name: "typecheck",
5
+ command: "npx warlock typecheck"
6
+ },
7
+ {
8
+ name: "check",
9
+ command: "npx warlock check"
10
+ },
11
+ {
12
+ name: "test",
13
+ command: "npx warlock test"
14
+ },
15
+ {
16
+ name: "build",
17
+ command: "NODE_ENV=production npx warlock build"
18
+ }
19
+ ];
20
+ /**
21
+ * Fill every default of the `release` config.
22
+ *
23
+ * Rejects settings that would make a later step guess: a check without a
24
+ * name or command, and duplicate check names.
25
+ */
26
+ function resolveReleaseConfig(raw = {}) {
27
+ const checks = raw.gate?.checks ?? DEFAULT_CHECKS;
28
+ const seen = /* @__PURE__ */ new Set();
29
+ for (const check of checks) {
30
+ if (!check.name || !check.command) throw new Error("Every release.gate check needs a `name` and a `command`.");
31
+ if (seen.has(check.name)) throw new Error(`release.gate has two checks named "${check.name}".`);
32
+ seen.add(check.name);
33
+ }
34
+ return {
35
+ directory: raw.directory ?? ".warlock",
36
+ distDir: raw.distDir ?? "dist",
37
+ include: raw.include ?? [],
38
+ linkNodeModules: raw.linkNodeModules ?? true,
39
+ checks,
40
+ migrations: raw.migrations ?? {},
41
+ restart: {
42
+ command: raw.restart?.command,
43
+ timeoutMs: raw.restart?.timeoutMs ?? 12e4
44
+ },
45
+ verify: {
46
+ url: raw.verify?.url,
47
+ expectedStatus: raw.verify?.expectedStatus ?? 200,
48
+ retries: raw.verify?.retries ?? 5,
49
+ intervalMs: raw.verify?.intervalMs ?? 2e3,
50
+ timeoutMs: raw.verify?.timeoutMs ?? 5e3,
51
+ versionPath: raw.verify?.versionPath ?? "version",
52
+ commitPath: raw.verify?.commitPath ?? "commit",
53
+ match: raw.verify?.match ?? ["version", "commit"]
54
+ }
55
+ };
56
+ }
57
+
58
+ //#endregion
59
+ export { resolveReleaseConfig };
60
+ //# sourceMappingURL=config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/config.ts"],"sourcesContent":["import type { ReleaseCheck, ReleaseConfig, ResolvedReleaseConfig } from \"./types\";\n\nconst DEFAULT_CHECKS: ReleaseCheck[] = [\n { name: \"typecheck\", command: \"npx warlock typecheck\" },\n { name: \"check\", command: \"npx warlock check\" },\n { name: \"test\", command: \"npx warlock test\" },\n { name: \"build\", command: \"NODE_ENV=production npx warlock build\" },\n];\n\n/**\n * Fill every default of the `release` config.\n *\n * Rejects settings that would make a later step guess: a check without a\n * name or command, and duplicate check names.\n */\nexport function resolveReleaseConfig(raw: ReleaseConfig = {}): ResolvedReleaseConfig {\n const checks = raw.gate?.checks ?? DEFAULT_CHECKS;\n const seen = new Set<string>();\n\n for (const check of checks) {\n if (!check.name || !check.command) {\n throw new Error(\"Every release.gate check needs a `name` and a `command`.\");\n }\n\n if (seen.has(check.name)) {\n throw new Error(`release.gate has two checks named \"${check.name}\".`);\n }\n\n seen.add(check.name);\n }\n\n return {\n directory: raw.directory ?? \".warlock\",\n distDir: raw.distDir ?? \"dist\",\n include: raw.include ?? [],\n linkNodeModules: raw.linkNodeModules ?? true,\n checks,\n migrations: raw.migrations ?? {},\n restart: {\n command: raw.restart?.command,\n timeoutMs: raw.restart?.timeoutMs ?? 120_000,\n },\n verify: {\n url: raw.verify?.url,\n expectedStatus: raw.verify?.expectedStatus ?? 200,\n retries: raw.verify?.retries ?? 5,\n intervalMs: raw.verify?.intervalMs ?? 2000,\n timeoutMs: raw.verify?.timeoutMs ?? 5000,\n versionPath: raw.verify?.versionPath ?? \"version\",\n commitPath: raw.verify?.commitPath ?? \"commit\",\n match: raw.verify?.match ?? [\"version\", \"commit\"],\n },\n };\n}\n"],"mappings":";AAEA,MAAM,iBAAiC;CACrC;EAAE,MAAM;EAAa,SAAS;CAAwB;CACtD;EAAE,MAAM;EAAS,SAAS;CAAoB;CAC9C;EAAE,MAAM;EAAQ,SAAS;CAAmB;CAC5C;EAAE,MAAM;EAAS,SAAS;CAAwC;AACpE;;;;;;;AAQA,SAAgB,qBAAqB,MAAqB,CAAC,GAA0B;CACnF,MAAM,SAAS,IAAI,MAAM,UAAU;CACnC,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,SACxB,MAAM,IAAI,MAAM,0DAA0D;EAG5E,IAAI,KAAK,IAAI,MAAM,IAAI,GACrB,MAAM,IAAI,MAAM,sCAAsC,MAAM,KAAK,GAAG;EAGtE,KAAK,IAAI,MAAM,IAAI;CACrB;CAEA,OAAO;EACL,WAAW,IAAI,aAAa;EAC5B,SAAS,IAAI,WAAW;EACxB,SAAS,IAAI,WAAW,CAAC;EACzB,iBAAiB,IAAI,mBAAmB;EACxC;EACA,YAAY,IAAI,cAAc,CAAC;EAC/B,SAAS;GACP,SAAS,IAAI,SAAS;GACtB,WAAW,IAAI,SAAS,aAAa;EACvC;EACA,QAAQ;GACN,KAAK,IAAI,QAAQ;GACjB,gBAAgB,IAAI,QAAQ,kBAAkB;GAC9C,SAAS,IAAI,QAAQ,WAAW;GAChC,YAAY,IAAI,QAAQ,cAAc;GACtC,WAAW,IAAI,QAAQ,aAAa;GACpC,aAAa,IAAI,QAAQ,eAAe;GACxC,YAAY,IAAI,QAAQ,cAAc;GACtC,OAAO,IAAI,QAAQ,SAAS,CAAC,WAAW,QAAQ;EAClD;CACF;AACF"}
@@ -0,0 +1,67 @@
1
+ import { appendLog, readManifest, releaseDir, writeManifest } from "./store.mjs";
2
+ import path from "node:path";
3
+
4
+ //#region ../../packages/core/src/release/gate.ts
5
+ const DEFAULT_CHECK_TIMEOUT_MS = 18e5;
6
+ /**
7
+ * Run the configured checks, in order, inside the candidate's frozen source.
8
+ *
9
+ * The gate stops at the first failing check: later checks do not run and are
10
+ * not recorded, so the manifest never claims a result that did not happen.
11
+ * Results are written to the manifest either way. A passing gate moves the
12
+ * candidate to `gated`; a failing one returns it to `candidate`. Either way
13
+ * an earlier staging is discarded from the manifest.
14
+ *
15
+ * @example
16
+ * const manifest = await runGate(context, "1.4.0-a1b2c3d");
17
+ * if (!manifest.gate?.passed) process.exitCode = 1;
18
+ */
19
+ async function runGate(context, id) {
20
+ const manifest = await readManifest(context, id);
21
+ if (manifest.status === "active" || manifest.status === "superseded") throw new Error(`Release "${id}" is ${manifest.status}; only a new candidate can be gated.`);
22
+ const cwd = path.join(releaseDir(context, id), "source");
23
+ const results = [];
24
+ let passed = true;
25
+ for (const check of context.config.checks) {
26
+ const started = Date.now();
27
+ const result = await context.runner(check.command, {
28
+ cwd,
29
+ timeoutMs: check.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS
30
+ });
31
+ const ok = result.code === 0 && !result.timedOut;
32
+ results.push({
33
+ name: check.name,
34
+ command: check.command,
35
+ status: ok ? "passed" : "failed",
36
+ exitCode: result.code,
37
+ durationMs: Date.now() - started,
38
+ ...ok ? {} : { output: result.timedOut ? `${result.output}\n(timed out)` : result.output }
39
+ });
40
+ if (!ok) {
41
+ passed = false;
42
+ break;
43
+ }
44
+ }
45
+ const updated = {
46
+ ...manifest,
47
+ status: passed ? "gated" : "candidate",
48
+ gate: {
49
+ passed,
50
+ ranAt: context.now().toISOString(),
51
+ results
52
+ }
53
+ };
54
+ delete updated.staged;
55
+ await writeManifest(context, updated);
56
+ await appendLog(context, {
57
+ event: "gate",
58
+ release: id,
59
+ ok: passed,
60
+ detail: passed ? `${results.length} check(s) passed` : `failed at ${results[results.length - 1]?.name}`
61
+ });
62
+ return updated;
63
+ }
64
+
65
+ //#endregion
66
+ export { runGate };
67
+ //# sourceMappingURL=gate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/gate.ts"],"sourcesContent":["import path from \"node:path\";\nimport { appendLog, readManifest, releaseDir, writeManifest, type ReleaseContext } from \"./store\";\nimport type { GateCheckResult, ReleaseManifest } from \"./types\";\n\nconst DEFAULT_CHECK_TIMEOUT_MS = 1_800_000;\n\n/**\n * Run the configured checks, in order, inside the candidate's frozen source.\n *\n * The gate stops at the first failing check: later checks do not run and are\n * not recorded, so the manifest never claims a result that did not happen.\n * Results are written to the manifest either way. A passing gate moves the\n * candidate to `gated`; a failing one returns it to `candidate`. Either way\n * an earlier staging is discarded from the manifest.\n *\n * @example\n * const manifest = await runGate(context, \"1.4.0-a1b2c3d\");\n * if (!manifest.gate?.passed) process.exitCode = 1;\n */\nexport async function runGate(context: ReleaseContext, id: string): Promise<ReleaseManifest> {\n const manifest = await readManifest(context, id);\n\n if (manifest.status === \"active\" || manifest.status === \"superseded\") {\n throw new Error(`Release \"${id}\" is ${manifest.status}; only a new candidate can be gated.`);\n }\n\n const cwd = path.join(releaseDir(context, id), \"source\");\n const results: GateCheckResult[] = [];\n let passed = true;\n\n for (const check of context.config.checks) {\n const started = Date.now();\n const result = await context.runner(check.command, {\n cwd,\n timeoutMs: check.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS,\n });\n const ok = result.code === 0 && !result.timedOut;\n\n results.push({\n name: check.name,\n command: check.command,\n status: ok ? \"passed\" : \"failed\",\n exitCode: result.code,\n durationMs: Date.now() - started,\n ...(ok ? {} : { output: result.timedOut ? `${result.output}\\n(timed out)` : result.output }),\n });\n\n if (!ok) {\n passed = false;\n break;\n }\n }\n\n const updated: ReleaseManifest = {\n ...manifest,\n status: passed ? \"gated\" : \"candidate\",\n gate: { passed, ranAt: context.now().toISOString(), results },\n };\n\n // A new gate run invalidates anything staged from an earlier one, so the\n // candidate has to be staged again.\n delete updated.staged;\n\n await writeManifest(context, updated);\n await appendLog(context, {\n event: \"gate\",\n release: id,\n ok: passed,\n detail: passed\n ? `${results.length} check(s) passed`\n : `failed at ${results[results.length - 1]?.name}`,\n });\n\n return updated;\n}\n"],"mappings":";;;;AAIA,MAAM,2BAA2B;;;;;;;;;;;;;;AAejC,eAAsB,QAAQ,SAAyB,IAAsC;CAC3F,MAAM,WAAW,MAAM,aAAa,SAAS,EAAE;CAE/C,IAAI,SAAS,WAAW,YAAY,SAAS,WAAW,cACtD,MAAM,IAAI,MAAM,YAAY,GAAG,OAAO,SAAS,OAAO,qCAAqC;CAG7F,MAAM,MAAM,KAAK,KAAK,WAAW,SAAS,EAAE,GAAG,QAAQ;CACvD,MAAM,UAA6B,CAAC;CACpC,IAAI,SAAS;CAEb,KAAK,MAAM,SAAS,QAAQ,OAAO,QAAQ;EACzC,MAAM,UAAU,KAAK,IAAI;EACzB,MAAM,SAAS,MAAM,QAAQ,OAAO,MAAM,SAAS;GACjD;GACA,WAAW,MAAM,aAAa;EAChC,CAAC;EACD,MAAM,KAAK,OAAO,SAAS,KAAK,CAAC,OAAO;EAExC,QAAQ,KAAK;GACX,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,QAAQ,KAAK,WAAW;GACxB,UAAU,OAAO;GACjB,YAAY,KAAK,IAAI,IAAI;GACzB,GAAI,KAAK,CAAC,IAAI,EAAE,QAAQ,OAAO,WAAW,GAAG,OAAO,OAAO,iBAAiB,OAAO,OAAO;EAC5F,CAAC;EAED,IAAI,CAAC,IAAI;GACP,SAAS;GACT;EACF;CACF;CAEA,MAAM,UAA2B;EAC/B,GAAG;EACH,QAAQ,SAAS,UAAU;EAC3B,MAAM;GAAE;GAAQ,OAAO,QAAQ,IAAI,CAAC,CAAC,YAAY;GAAG;EAAQ;CAC9D;CAIA,OAAO,QAAQ;CAEf,MAAM,cAAc,SAAS,OAAO;CACpC,MAAM,UAAU,SAAS;EACvB,OAAO;EACP,SAAS;EACT,IAAI;EACJ,QAAQ,SACJ,GAAG,QAAQ,OAAO,oBAClB,aAAa,QAAQ,QAAQ,SAAS,EAAE,EAAE;CAChD,CAAC;CAED,OAAO;AACT"}
@@ -0,0 +1,100 @@
1
+ import { appendLog, readManifest, writeManifest } from "./store.mjs";
2
+
3
+ //#region ../../packages/core/src/release/migrations-proof.ts
4
+ async function snapshot(provider) {
5
+ const tables = [...await provider.listTables()].sort();
6
+ const indexes = {};
7
+ for (const table of tables) indexes[table] = [...await provider.listIndexes(table)].sort();
8
+ return {
9
+ executed: await provider.listExecuted(),
10
+ tables,
11
+ indexes
12
+ };
13
+ }
14
+ function difference(left, right) {
15
+ const other = new Set(right);
16
+ return left.filter((item) => !other.has(item));
17
+ }
18
+ /** What `--prove` would do, without touching the database. */
19
+ async function planMigrationProof(context, provider, target) {
20
+ const pending = await provider.listPending();
21
+ return {
22
+ target,
23
+ pending,
24
+ expected: context.config.migrations.expected ?? pending
25
+ };
26
+ }
27
+ /**
28
+ * Snapshot the ledger, tables and indexes, run the pending migrations,
29
+ * snapshot again and compare the result with the expected set.
30
+ *
31
+ * The proof passes when exactly the expected migrations were applied, in
32
+ * order, the ledger grew by exactly those names, and every table listed in
33
+ * `release.migrations.expectedTables` exists afterwards. The full before and
34
+ * after snapshots go into the manifest, so the schema change is on record
35
+ * with the release.
36
+ *
37
+ * Run it against a target that is safe to migrate (a staging copy): the
38
+ * migrations really execute.
39
+ *
40
+ * @example
41
+ * const proof = await proveMigrations(context, provider, "staging");
42
+ */
43
+ async function proveMigrations(context, provider, options) {
44
+ const plan = await planMigrationProof(context, provider, options.target);
45
+ const before = await snapshot(provider);
46
+ const problems = [];
47
+ let applied = [];
48
+ try {
49
+ applied = await provider.runPending();
50
+ } catch (error) {
51
+ problems.push(`Running migrations failed: ${error.message}`);
52
+ }
53
+ const after = await snapshot(provider);
54
+ const ledgerGrowth = difference(after.executed, before.executed);
55
+ if (JSON.stringify(applied) !== JSON.stringify(plan.expected)) problems.push(`Applied [${applied.join(", ")}] but expected [${plan.expected.join(", ")}].`);
56
+ if (JSON.stringify(ledgerGrowth) !== JSON.stringify(plan.expected)) problems.push(`The ledger gained [${ledgerGrowth.join(", ")}] but expected [${plan.expected.join(", ")}].`);
57
+ const removedFromLedger = difference(before.executed, after.executed);
58
+ if (removedFromLedger.length > 0) problems.push(`The ledger lost entries: ${removedFromLedger.join(", ")}.`);
59
+ for (const table of context.config.migrations.expectedTables ?? []) if (!after.tables.includes(table)) problems.push(`Expected table "${table}" is missing.`);
60
+ const indexesChanged = {};
61
+ for (const table of /* @__PURE__ */ new Set([...Object.keys(before.indexes), ...Object.keys(after.indexes)])) {
62
+ const was = before.indexes[table] ?? [];
63
+ const now = after.indexes[table] ?? [];
64
+ const added = difference(now, was);
65
+ const removed = difference(was, now);
66
+ if (added.length > 0 || removed.length > 0) indexesChanged[table] = {
67
+ added,
68
+ removed
69
+ };
70
+ }
71
+ const proof = {
72
+ target: options.target,
73
+ provedAt: context.now().toISOString(),
74
+ passed: problems.length === 0,
75
+ before,
76
+ after,
77
+ expected: plan.expected,
78
+ applied,
79
+ tablesAdded: difference(after.tables, before.tables),
80
+ tablesRemoved: difference(before.tables, after.tables),
81
+ indexesChanged,
82
+ problems
83
+ };
84
+ const manifest = await readManifest(context, options.releaseId);
85
+ await writeManifest(context, {
86
+ ...manifest,
87
+ migrations: proof
88
+ });
89
+ await appendLog(context, {
90
+ event: "migrations",
91
+ release: options.releaseId,
92
+ ok: proof.passed,
93
+ detail: proof.passed ? `${applied.length} migration(s) proved` : problems[0]
94
+ });
95
+ return proof;
96
+ }
97
+
98
+ //#endregion
99
+ export { planMigrationProof, proveMigrations };
100
+ //# sourceMappingURL=migrations-proof.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations-proof.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/migrations-proof.ts"],"sourcesContent":["import { appendLog, readManifest, writeManifest, type ReleaseContext } from \"./store\";\nimport type { MigrationProof, MigrationSnapshot } from \"./types\";\n\n/**\n * What the proof needs from a database. Kept driver-agnostic: the default\n * implementation (see `cascade-migration-provider.ts`) reads the migration\n * ledger and the schema through cascade, and tests pass a plain object.\n */\nexport type MigrationProofProvider = {\n /** Names in the migration ledger, in execution order */\n listExecuted(): Promise<string[]>;\n /** Registered migrations that have not run, in the order they will run */\n listPending(): Promise<string[]>;\n /** Tables or collections */\n listTables(): Promise<string[]>;\n /** Index names of one table or collection */\n listIndexes(table: string): Promise<string[]>;\n /** Run the pending migrations; resolves to the names that were applied */\n runPending(): Promise<string[]>;\n};\n\nexport type MigrationPlan = {\n target: string | null;\n pending: string[];\n expected: string[];\n};\n\nasync function snapshot(provider: MigrationProofProvider): Promise<MigrationSnapshot> {\n const tables = [...(await provider.listTables())].sort();\n const indexes: Record<string, string[]> = {};\n\n for (const table of tables) {\n indexes[table] = [...(await provider.listIndexes(table))].sort();\n }\n\n return { executed: await provider.listExecuted(), tables, indexes };\n}\n\nfunction difference(left: string[], right: string[]): string[] {\n const other = new Set(right);\n\n return left.filter((item) => !other.has(item));\n}\n\n/** What `--prove` would do, without touching the database. */\nexport async function planMigrationProof(\n context: ReleaseContext,\n provider: MigrationProofProvider,\n target: string | null,\n): Promise<MigrationPlan> {\n const pending = await provider.listPending();\n\n return { target, pending, expected: context.config.migrations.expected ?? pending };\n}\n\n/**\n * Snapshot the ledger, tables and indexes, run the pending migrations,\n * snapshot again and compare the result with the expected set.\n *\n * The proof passes when exactly the expected migrations were applied, in\n * order, the ledger grew by exactly those names, and every table listed in\n * `release.migrations.expectedTables` exists afterwards. The full before and\n * after snapshots go into the manifest, so the schema change is on record\n * with the release.\n *\n * Run it against a target that is safe to migrate (a staging copy): the\n * migrations really execute.\n *\n * @example\n * const proof = await proveMigrations(context, provider, \"staging\");\n */\nexport async function proveMigrations(\n context: ReleaseContext,\n provider: MigrationProofProvider,\n options: { target: string | null; releaseId: string },\n): Promise<MigrationProof> {\n const plan = await planMigrationProof(context, provider, options.target);\n const before = await snapshot(provider);\n const problems: string[] = [];\n let applied: string[] = [];\n\n try {\n applied = await provider.runPending();\n } catch (error) {\n problems.push(`Running migrations failed: ${(error as Error).message}`);\n }\n\n const after = await snapshot(provider);\n const ledgerGrowth = difference(after.executed, before.executed);\n\n if (JSON.stringify(applied) !== JSON.stringify(plan.expected)) {\n problems.push(`Applied [${applied.join(\", \")}] but expected [${plan.expected.join(\", \")}].`);\n }\n\n if (JSON.stringify(ledgerGrowth) !== JSON.stringify(plan.expected)) {\n problems.push(\n `The ledger gained [${ledgerGrowth.join(\", \")}] but expected [${plan.expected.join(\", \")}].`,\n );\n }\n\n const removedFromLedger = difference(before.executed, after.executed);\n\n if (removedFromLedger.length > 0) {\n problems.push(`The ledger lost entries: ${removedFromLedger.join(\", \")}.`);\n }\n\n for (const table of context.config.migrations.expectedTables ?? []) {\n if (!after.tables.includes(table)) problems.push(`Expected table \"${table}\" is missing.`);\n }\n\n const indexesChanged: MigrationProof[\"indexesChanged\"] = {};\n\n for (const table of new Set([...Object.keys(before.indexes), ...Object.keys(after.indexes)])) {\n const was = before.indexes[table] ?? [];\n const now = after.indexes[table] ?? [];\n const added = difference(now, was);\n const removed = difference(was, now);\n\n if (added.length > 0 || removed.length > 0) indexesChanged[table] = { added, removed };\n }\n\n const proof: MigrationProof = {\n target: options.target,\n provedAt: context.now().toISOString(),\n passed: problems.length === 0,\n before,\n after,\n expected: plan.expected,\n applied,\n tablesAdded: difference(after.tables, before.tables),\n tablesRemoved: difference(before.tables, after.tables),\n indexesChanged,\n problems,\n };\n\n const manifest = await readManifest(context, options.releaseId);\n\n await writeManifest(context, { ...manifest, migrations: proof });\n await appendLog(context, {\n event: \"migrations\",\n release: options.releaseId,\n ok: proof.passed,\n detail: proof.passed ? `${applied.length} migration(s) proved` : problems[0],\n });\n\n return proof;\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { appendLog, listManifests, readPointer, releaseDir } from "./store.mjs";
2
+ import { rm } from "node:fs/promises";
3
+
4
+ //#region ../../packages/core/src/release/prune.ts
5
+ /**
6
+ * Delete old releases, keeping the newest `keep`. The release `current` names
7
+ * and the one `previous` names are always kept, whatever their age. This is
8
+ * the only operation that removes a release.
9
+ *
10
+ * @example
11
+ * await pruneReleases(context, { keep: 5 });
12
+ */
13
+ async function pruneReleases(context, options) {
14
+ if (!Number.isInteger(options.keep) || options.keep < 1) throw new Error("--keep must be a whole number of at least 1.");
15
+ const manifests = await listManifests(context);
16
+ const pinned = new Set([await readPointer(context, "current"), await readPointer(context, "previous")].filter((id) => id !== null));
17
+ const kept = new Set(manifests.slice(0, options.keep).map((manifest) => manifest.id));
18
+ const removed = [];
19
+ for (const manifest of manifests) {
20
+ if (kept.has(manifest.id) || pinned.has(manifest.id)) continue;
21
+ removed.push(manifest.id);
22
+ if (!options.dryRun) await rm(releaseDir(context, manifest.id), {
23
+ recursive: true,
24
+ force: true
25
+ });
26
+ }
27
+ if (!options.dryRun && removed.length > 0) await appendLog(context, {
28
+ event: "prune",
29
+ release: null,
30
+ ok: true,
31
+ detail: `removed ${removed.join(", ")}`
32
+ });
33
+ return {
34
+ kept: manifests.filter((manifest) => !removed.includes(manifest.id)).map((manifest) => manifest.id),
35
+ removed,
36
+ dryRun: options.dryRun === true
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ export { pruneReleases };
42
+ //# sourceMappingURL=prune.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/prune.ts"],"sourcesContent":["import { rm } from \"node:fs/promises\";\nimport { appendLog, listManifests, readPointer, releaseDir, type ReleaseContext } from \"./store\";\n\nexport type PruneResult = {\n kept: string[];\n removed: string[];\n dryRun: boolean;\n};\n\n/**\n * Delete old releases, keeping the newest `keep`. The release `current` names\n * and the one `previous` names are always kept, whatever their age. This is\n * the only operation that removes a release.\n *\n * @example\n * await pruneReleases(context, { keep: 5 });\n */\nexport async function pruneReleases(\n context: ReleaseContext,\n options: { keep: number; dryRun?: boolean },\n): Promise<PruneResult> {\n if (!Number.isInteger(options.keep) || options.keep < 1) {\n throw new Error(\"--keep must be a whole number of at least 1.\");\n }\n\n const manifests = await listManifests(context);\n const pinned = new Set(\n [await readPointer(context, \"current\"), await readPointer(context, \"previous\")].filter(\n (id): id is string => id !== null,\n ),\n );\n const kept = new Set(manifests.slice(0, options.keep).map((manifest) => manifest.id));\n const removed: string[] = [];\n\n for (const manifest of manifests) {\n if (kept.has(manifest.id) || pinned.has(manifest.id)) continue;\n\n removed.push(manifest.id);\n\n if (!options.dryRun) {\n await rm(releaseDir(context, manifest.id), { recursive: true, force: true });\n }\n }\n\n if (!options.dryRun && removed.length > 0) {\n await appendLog(context, {\n event: \"prune\",\n release: null,\n ok: true,\n detail: `removed ${removed.join(\", \")}`,\n });\n }\n\n return {\n kept: manifests\n .filter((manifest) => !removed.includes(manifest.id))\n .map((manifest) => manifest.id),\n removed,\n dryRun: options.dryRun === true,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,eAAsB,cACpB,SACA,SACsB;CACtB,IAAI,CAAC,OAAO,UAAU,QAAQ,IAAI,KAAK,QAAQ,OAAO,GACpD,MAAM,IAAI,MAAM,8CAA8C;CAGhE,MAAM,YAAY,MAAM,cAAc,OAAO;CAC7C,MAAM,SAAS,IAAI,IACjB,CAAC,MAAM,YAAY,SAAS,SAAS,GAAG,MAAM,YAAY,SAAS,UAAU,CAAC,CAAC,CAAC,QAC7E,OAAqB,OAAO,IAC/B,CACF;CACA,MAAM,OAAO,IAAI,IAAI,UAAU,MAAM,GAAG,QAAQ,IAAI,CAAC,CAAC,KAAK,aAAa,SAAS,EAAE,CAAC;CACpF,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,KAAK,IAAI,SAAS,EAAE,KAAK,OAAO,IAAI,SAAS,EAAE,GAAG;EAEtD,QAAQ,KAAK,SAAS,EAAE;EAExB,IAAI,CAAC,QAAQ,QACX,MAAM,GAAG,WAAW,SAAS,SAAS,EAAE,GAAG;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CAE/E;CAEA,IAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,GACtC,MAAM,UAAU,SAAS;EACvB,OAAO;EACP,SAAS;EACT,IAAI;EACJ,QAAQ,WAAW,QAAQ,KAAK,IAAI;CACtC,CAAC;CAGH,OAAO;EACL,MAAM,UACH,QAAQ,aAAa,CAAC,QAAQ,SAAS,SAAS,EAAE,CAAC,CAAC,CACpD,KAAK,aAAa,SAAS,EAAE;EAChC;EACA,QAAQ,QAAQ,WAAW;CAC7B;AACF"}
@@ -0,0 +1,25 @@
1
+ import { listManifests, readLog, readPointer } from "./store.mjs";
2
+
3
+ //#region ../../packages/core/src/release/report.ts
4
+ /** Every release, newest first, with the two pointers. */
5
+ async function listReleases(context) {
6
+ return {
7
+ current: await readPointer(context, "current"),
8
+ previous: await readPointer(context, "previous"),
9
+ releases: await listManifests(context)
10
+ };
11
+ }
12
+ /** The active release with the last entries of the release log. */
13
+ async function releaseStatus(context, logLimit = 10) {
14
+ const listing = await listReleases(context);
15
+ return {
16
+ current: listing.current,
17
+ previous: listing.previous,
18
+ active: listing.releases.find((release) => release.id === listing.current) ?? null,
19
+ log: (await readLog(context)).slice(-logLimit)
20
+ };
21
+ }
22
+
23
+ //#endregion
24
+ export { listReleases, releaseStatus };
25
+ //# sourceMappingURL=report.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/report.ts"],"sourcesContent":["import { readLog, listManifests, readPointer, type ReleaseContext } from \"./store\";\nimport type { ReleaseLogEntry, ReleaseManifest } from \"./types\";\n\nexport type ReleaseListing = {\n current: string | null;\n previous: string | null;\n releases: ReleaseManifest[];\n};\n\nexport type ReleaseStatusReport = {\n current: string | null;\n previous: string | null;\n active: ReleaseManifest | null;\n log: ReleaseLogEntry[];\n};\n\n/** Every release, newest first, with the two pointers. */\nexport async function listReleases(context: ReleaseContext): Promise<ReleaseListing> {\n return {\n current: await readPointer(context, \"current\"),\n previous: await readPointer(context, \"previous\"),\n releases: await listManifests(context),\n };\n}\n\n/** The active release with the last entries of the release log. */\nexport async function releaseStatus(\n context: ReleaseContext,\n logLimit = 10,\n): Promise<ReleaseStatusReport> {\n const listing = await listReleases(context);\n\n return {\n current: listing.current,\n previous: listing.previous,\n active: listing.releases.find((release) => release.id === listing.current) ?? null,\n log: (await readLog(context)).slice(-logLimit),\n };\n}\n"],"mappings":";;;;AAiBA,eAAsB,aAAa,SAAkD;CACnF,OAAO;EACL,SAAS,MAAM,YAAY,SAAS,SAAS;EAC7C,UAAU,MAAM,YAAY,SAAS,UAAU;EAC/C,UAAU,MAAM,cAAc,OAAO;CACvC;AACF;;AAGA,eAAsB,cACpB,SACA,WAAW,IACmB;CAC9B,MAAM,UAAU,MAAM,aAAa,OAAO;CAE1C,OAAO;EACL,SAAS,QAAQ;EACjB,UAAU,QAAQ;EAClB,QAAQ,QAAQ,SAAS,MAAM,YAAY,QAAQ,OAAO,QAAQ,OAAO,KAAK;EAC9E,MAAM,MAAM,QAAQ,OAAO,EAAC,CAAE,MAAM,CAAC,QAAQ;CAC/C;AACF"}
@@ -0,0 +1,55 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ //#region ../../packages/core/src/release/runner.ts
4
+ /**
5
+ * Default runner: the command goes through the shell, so config entries such
6
+ * as `NODE_ENV=production npx warlock build` work as written. Output is
7
+ * captured (the tail is kept) rather than streamed.
8
+ *
9
+ * On timeout only the child this function started is signalled.
10
+ */
11
+ const shellRunner = (command, options) => new Promise((resolve) => {
12
+ const child = spawn(command, {
13
+ cwd: options.cwd,
14
+ env: {
15
+ ...process.env,
16
+ ...options.env
17
+ },
18
+ shell: true,
19
+ stdio: [
20
+ "ignore",
21
+ "pipe",
22
+ "pipe"
23
+ ]
24
+ });
25
+ let output = "";
26
+ let timedOut = false;
27
+ const append = (chunk) => {
28
+ output = (output + chunk.toString("utf8")).slice(-8e3);
29
+ };
30
+ child.stdout.on("data", append);
31
+ child.stderr.on("data", append);
32
+ const timer = options.timeoutMs ? setTimeout(() => {
33
+ timedOut = true;
34
+ child.kill("SIGTERM");
35
+ }, options.timeoutMs) : void 0;
36
+ child.on("error", (error) => {
37
+ if (timer) clearTimeout(timer);
38
+ resolve({
39
+ code: 127,
40
+ output: `${output}${error.message}`.slice(-8e3)
41
+ });
42
+ });
43
+ child.on("close", (code) => {
44
+ if (timer) clearTimeout(timer);
45
+ resolve({
46
+ code: code ?? 1,
47
+ output,
48
+ timedOut
49
+ });
50
+ });
51
+ });
52
+
53
+ //#endregion
54
+ export { shellRunner };
55
+ //# sourceMappingURL=runner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/runner.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { CommandResult, CommandRunner } from \"./types\";\n\nconst OUTPUT_LIMIT = 8000;\n\n/**\n * Default runner: the command goes through the shell, so config entries such\n * as `NODE_ENV=production npx warlock build` work as written. Output is\n * captured (the tail is kept) rather than streamed.\n *\n * On timeout only the child this function started is signalled.\n */\nexport const shellRunner: CommandRunner = (command, options) =>\n new Promise<CommandResult>((resolve) => {\n const child = spawn(command, {\n cwd: options.cwd,\n env: { ...process.env, ...options.env },\n shell: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let output = \"\";\n let timedOut = false;\n const append = (chunk: Buffer) => {\n output = (output + chunk.toString(\"utf8\")).slice(-OUTPUT_LIMIT);\n };\n\n child.stdout.on(\"data\", append);\n child.stderr.on(\"data\", append);\n\n const timer = options.timeoutMs\n ? setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n }, options.timeoutMs)\n : undefined;\n\n child.on(\"error\", (error) => {\n if (timer) clearTimeout(timer);\n resolve({ code: 127, output: `${output}${error.message}`.slice(-OUTPUT_LIMIT) });\n });\n\n child.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n resolve({ code: code ?? 1, output, timedOut });\n });\n });\n"],"mappings":";;;;;;;;;;AAYA,MAAa,eAA8B,SAAS,YAClD,IAAI,SAAwB,YAAY;CACtC,MAAM,QAAQ,MAAM,SAAS;EAC3B,KAAK,QAAQ;EACb,KAAK;GAAE,GAAG,QAAQ;GAAK,GAAG,QAAQ;EAAI;EACtC,OAAO;EACP,OAAO;GAAC;GAAU;GAAQ;EAAM;CAClC,CAAC;CACD,IAAI,SAAS;CACb,IAAI,WAAW;CACf,MAAM,UAAU,UAAkB;EAChC,UAAU,SAAS,MAAM,SAAS,MAAM,EAAC,CAAE,MAAM,IAAa;CAChE;CAEA,MAAM,OAAO,GAAG,QAAQ,MAAM;CAC9B,MAAM,OAAO,GAAG,QAAQ,MAAM;CAE9B,MAAM,QAAQ,QAAQ,YAClB,iBAAiB;EACf,WAAW;EACX,MAAM,KAAK,SAAS;CACtB,GAAG,QAAQ,SAAS,IACpB;CAEJ,MAAM,GAAG,UAAU,UAAU;EAC3B,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ;GAAE,MAAM;GAAK,QAAQ,GAAG,SAAS,MAAM,UAAU,MAAM,IAAa;EAAE,CAAC;CACjF,CAAC;CAED,MAAM,GAAG,UAAU,SAAS;EAC1B,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ;GAAE,MAAM,QAAQ;GAAG;GAAQ;EAAS,CAAC;CAC/C,CAAC;AACH,CAAC"}
@@ -0,0 +1,53 @@
1
+ //#region ../../packages/core/src/release/semver.ts
2
+ function parseVersion(version) {
3
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
4
+ if (!match) throw new Error(`"${version}" is not a valid semantic version (expected MAJOR.MINOR.PATCH).`);
5
+ return {
6
+ core: [
7
+ Number(match[1]),
8
+ Number(match[2]),
9
+ Number(match[3])
10
+ ],
11
+ prerelease: match[4] ? match[4].split(".") : []
12
+ };
13
+ }
14
+ function comparePrerelease(a, b) {
15
+ if (a.length === 0 && b.length === 0) return 0;
16
+ if (a.length === 0) return 1;
17
+ if (b.length === 0) return -1;
18
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
19
+ const left = a[index];
20
+ const right = b[index];
21
+ if (left === void 0) return -1;
22
+ if (right === void 0) return 1;
23
+ if (left === right) continue;
24
+ const leftNumeric = /^\d+$/.test(left);
25
+ const rightNumeric = /^\d+$/.test(right);
26
+ if (leftNumeric && rightNumeric) return Number(left) < Number(right) ? -1 : 1;
27
+ if (leftNumeric) return -1;
28
+ if (rightNumeric) return 1;
29
+ return left < right ? -1 : 1;
30
+ }
31
+ return 0;
32
+ }
33
+ /**
34
+ * Compare two semantic versions. Returns a negative number when `a < b`,
35
+ * zero when equal and a positive number when `a > b`. Build metadata is
36
+ * ignored. Throws on a string that is not a semantic version.
37
+ *
38
+ * @example
39
+ * compareVersions("1.2.0", "1.10.0"); // < 0
40
+ */
41
+ function compareVersions(a, b) {
42
+ const left = parseVersion(a);
43
+ const right = parseVersion(b);
44
+ for (let index = 0; index < 3; index++) {
45
+ const difference = left.core[index] - right.core[index];
46
+ if (difference !== 0) return difference < 0 ? -1 : 1;
47
+ }
48
+ return comparePrerelease(left.prerelease, right.prerelease);
49
+ }
50
+
51
+ //#endregion
52
+ export { compareVersions };
53
+ //# sourceMappingURL=semver.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/semver.ts"],"sourcesContent":["type ParsedVersion = { core: [number, number, number]; prerelease: string[] };\n\nfunction parseVersion(version: string): ParsedVersion {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+[0-9A-Za-z.-]+)?$/.exec(\n version.trim(),\n );\n\n if (!match) {\n throw new Error(`\"${version}\" is not a valid semantic version (expected MAJOR.MINOR.PATCH).`);\n }\n\n return {\n core: [Number(match[1]), Number(match[2]), Number(match[3])],\n prerelease: match[4] ? match[4].split(\".\") : [],\n };\n}\n\nfunction comparePrerelease(a: string[], b: string[]): number {\n // A version without a prerelease tag is greater than one with it.\n if (a.length === 0 && b.length === 0) return 0;\n if (a.length === 0) return 1;\n if (b.length === 0) return -1;\n\n for (let index = 0; index < Math.max(a.length, b.length); index++) {\n const left = a[index];\n const right = b[index];\n\n if (left === undefined) return -1;\n if (right === undefined) return 1;\n if (left === right) continue;\n\n const leftNumeric = /^\\d+$/.test(left);\n const rightNumeric = /^\\d+$/.test(right);\n\n if (leftNumeric && rightNumeric) return Number(left) < Number(right) ? -1 : 1;\n if (leftNumeric) return -1;\n if (rightNumeric) return 1;\n\n return left < right ? -1 : 1;\n }\n\n return 0;\n}\n\n/**\n * Compare two semantic versions. Returns a negative number when `a < b`,\n * zero when equal and a positive number when `a > b`. Build metadata is\n * ignored. Throws on a string that is not a semantic version.\n *\n * @example\n * compareVersions(\"1.2.0\", \"1.10.0\"); // < 0\n */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n\n for (let index = 0; index < 3; index++) {\n const difference = left.core[index]! - right.core[index]!;\n\n if (difference !== 0) return difference < 0 ? -1 : 1;\n }\n\n return comparePrerelease(left.prerelease, right.prerelease);\n}\n"],"mappings":";AAEA,SAAS,aAAa,SAAgC;CACpD,MAAM,QAAQ,qEAAqE,KACjF,QAAQ,KAAK,CACf;CAEA,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,IAAI,QAAQ,gEAAgE;CAG9F,OAAO;EACL,MAAM;GAAC,OAAO,MAAM,EAAE;GAAG,OAAO,MAAM,EAAE;GAAG,OAAO,MAAM,EAAE;EAAC;EAC3D,YAAY,MAAM,KAAK,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC;CAChD;AACF;AAEA,SAAS,kBAAkB,GAAa,GAAqB;CAE3D,IAAI,EAAE,WAAW,KAAK,EAAE,WAAW,GAAG,OAAO;CAC7C,IAAI,EAAE,WAAW,GAAG,OAAO;CAC3B,IAAI,EAAE,WAAW,GAAG,OAAO;CAE3B,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS;EACjE,MAAM,OAAO,EAAE;EACf,MAAM,QAAQ,EAAE;EAEhB,IAAI,SAAS,QAAW,OAAO;EAC/B,IAAI,UAAU,QAAW,OAAO;EAChC,IAAI,SAAS,OAAO;EAEpB,MAAM,cAAc,QAAQ,KAAK,IAAI;EACrC,MAAM,eAAe,QAAQ,KAAK,KAAK;EAEvC,IAAI,eAAe,cAAc,OAAO,OAAO,IAAI,IAAI,OAAO,KAAK,IAAI,KAAK;EAC5E,IAAI,aAAa,OAAO;EACxB,IAAI,cAAc,OAAO;EAEzB,OAAO,OAAO,QAAQ,KAAK;CAC7B;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,gBAAgB,GAAW,GAAmB;CAC5D,MAAM,OAAO,aAAa,CAAC;CAC3B,MAAM,QAAQ,aAAa,CAAC;CAE5B,KAAK,IAAI,QAAQ,GAAG,QAAQ,GAAG,SAAS;EACtC,MAAM,aAAa,KAAK,KAAK,SAAU,MAAM,KAAK;EAElD,IAAI,eAAe,GAAG,OAAO,aAAa,IAAI,KAAK;CACrD;CAEA,OAAO,kBAAkB,KAAK,YAAY,MAAM,UAAU;AAC5D"}