@warlock.js/core 5.35.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 (133) hide show
  1. package/CHANGELOG.md +53 -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/dev-server/module-loader.mjs +1 -1
  16. package/esm/dev-server/parse-imports.mjs +1 -1
  17. package/esm/dev-server/type-generator.mjs +1 -1
  18. package/esm/e2e/cdp.d.mts.map +1 -1
  19. package/esm/e2e/cdp.mjs +9 -2
  20. package/esm/e2e/cdp.mjs.map +1 -1
  21. package/esm/e2e/page.d.mts +9 -1
  22. package/esm/e2e/page.d.mts.map +1 -1
  23. package/esm/e2e/page.mjs +45 -7
  24. package/esm/e2e/page.mjs.map +1 -1
  25. package/esm/e2e/types.d.mts +18 -0
  26. package/esm/e2e/types.d.mts.map +1 -1
  27. package/esm/e2e/types.mjs.map +1 -1
  28. package/esm/errors/error-codes.d.mts +24 -0
  29. package/esm/errors/error-codes.d.mts.map +1 -1
  30. package/esm/errors/error-codes.mjs +24 -0
  31. package/esm/errors/error-codes.mjs.map +1 -1
  32. package/esm/generations/features/test.feature.mjs +7 -7
  33. package/esm/generations/features/test.feature.mjs.map +1 -1
  34. package/esm/generations/features/web.feature.mjs +3 -3
  35. package/esm/generations/features/web.feature.mjs.map +1 -1
  36. package/esm/index.d.mts +6 -3
  37. package/esm/index.mjs +4 -2
  38. package/esm/localization/index.mjs +3 -1
  39. package/esm/localization/translation-usage-graph.d.mts +49 -0
  40. package/esm/localization/translation-usage-graph.d.mts.map +1 -0
  41. package/esm/localization/translation-usage-graph.mjs +128 -0
  42. package/esm/localization/translation-usage-graph.mjs.map +1 -0
  43. package/esm/localization/translation-usage.d.mts +85 -0
  44. package/esm/localization/translation-usage.d.mts.map +1 -0
  45. package/esm/localization/translation-usage.mjs +307 -0
  46. package/esm/localization/translation-usage.mjs.map +1 -0
  47. package/esm/production/build-budgets.d.mts +18 -0
  48. package/esm/production/build-budgets.d.mts.map +1 -1
  49. package/esm/production/build-budgets.mjs.map +1 -1
  50. package/esm/production/route-registration-snapshot.mjs +12 -3
  51. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  52. package/esm/release/activate.mjs +261 -0
  53. package/esm/release/activate.mjs.map +1 -0
  54. package/esm/release/candidate.mjs +107 -0
  55. package/esm/release/candidate.mjs.map +1 -0
  56. package/esm/release/cascade-migration-provider.mjs +32 -0
  57. package/esm/release/cascade-migration-provider.mjs.map +1 -0
  58. package/esm/release/config.mjs +60 -0
  59. package/esm/release/config.mjs.map +1 -0
  60. package/esm/release/gate.mjs +67 -0
  61. package/esm/release/gate.mjs.map +1 -0
  62. package/esm/release/migrations-proof.mjs +100 -0
  63. package/esm/release/migrations-proof.mjs.map +1 -0
  64. package/esm/release/prune.mjs +42 -0
  65. package/esm/release/prune.mjs.map +1 -0
  66. package/esm/release/report.mjs +25 -0
  67. package/esm/release/report.mjs.map +1 -0
  68. package/esm/release/runner.mjs +55 -0
  69. package/esm/release/runner.mjs.map +1 -0
  70. package/esm/release/semver.mjs +53 -0
  71. package/esm/release/semver.mjs.map +1 -0
  72. package/esm/release/stage.mjs +122 -0
  73. package/esm/release/stage.mjs.map +1 -0
  74. package/esm/release/store.mjs +131 -0
  75. package/esm/release/store.mjs.map +1 -0
  76. package/esm/release/types.d.mts +102 -0
  77. package/esm/release/types.d.mts.map +1 -0
  78. package/esm/release/verify.mjs +94 -0
  79. package/esm/release/verify.mjs.map +1 -0
  80. package/esm/router/index.d.mts +2 -2
  81. package/esm/router/index.mjs +2 -2
  82. package/esm/router/router.d.mts +4 -0
  83. package/esm/router/router.d.mts.map +1 -1
  84. package/esm/router/router.mjs +14 -3
  85. package/esm/router/router.mjs.map +1 -1
  86. package/esm/router/types.d.mts +45 -0
  87. package/esm/router/types.d.mts.map +1 -1
  88. package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
  89. package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
  90. package/esm/router/unguarded-route-diagnostics.mjs +10 -2
  91. package/esm/router/unguarded-route-diagnostics.mjs.map +1 -1
  92. package/esm/tooling/check/api-route-references.d.mts +16 -0
  93. package/esm/tooling/check/api-route-references.d.mts.map +1 -0
  94. package/esm/tooling/check/api-route-references.mjs +146 -0
  95. package/esm/tooling/check/api-route-references.mjs.map +1 -0
  96. package/esm/tooling/check/conventions-step.d.mts +6 -3
  97. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  98. package/esm/tooling/check/conventions-step.mjs +19 -10
  99. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  100. package/esm/tooling/check/run-check.d.mts.map +1 -1
  101. package/esm/tooling/check/run-check.mjs +4 -2
  102. package/esm/tooling/check/run-check.mjs.map +1 -1
  103. package/esm/tooling/check/security-conventions.mjs +19 -1
  104. package/esm/tooling/check/security-conventions.mjs.map +1 -1
  105. package/esm/tooling/check/translation-conventions.mjs +32 -0
  106. package/esm/tooling/check/translation-conventions.mjs.map +1 -0
  107. package/esm/tooling/inspect/collect-files.mjs +1 -1
  108. package/esm/tooling/inspect/collect-routes.mjs +3 -2
  109. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  110. package/esm/upgrade/codemods/index.mjs +2 -0
  111. package/esm/upgrade/codemods/index.mjs.map +1 -1
  112. package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
  113. package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
  114. package/esm/upgrade/notes.mjs +4 -0
  115. package/esm/upgrade/notes.mjs.map +1 -1
  116. package/esm/validation/init.mjs +11 -4
  117. package/esm/validation/init.mjs.map +1 -1
  118. package/esm/validation/translate-rule.mjs +63 -0
  119. package/esm/validation/translate-rule.mjs.map +1 -0
  120. package/esm/warlock-config/index.d.mts +3 -2
  121. package/esm/warlock-config/types.d.mts +8 -0
  122. package/esm/warlock-config/types.d.mts.map +1 -1
  123. package/llms-full.txt +221 -18
  124. package/llms.txt +1 -0
  125. package/package.json +13 -13
  126. package/skills/deploy-release/SKILL.md +76 -0
  127. package/skills/index.md +1 -1
  128. package/skills/register-route/SKILL.md +25 -0
  129. package/skills/test-service/SKILL.md +8 -8
  130. package/skills/upgrade-from-5-29/SKILL.md +91 -0
  131. package/skills/use-localization/SKILL.md +4 -0
  132. package/skills/warlock-check/SKILL.md +11 -8
  133. package/skills/warlock-conventions/SKILL.md +1 -1
@@ -0,0 +1,261 @@
1
+ import { compareVersions } from "./semver.mjs";
2
+ import { appendLog, pointerPath, readManifest, readPointer, releaseDir, switchPointer, writeManifest } from "./store.mjs";
3
+ import { hasStagedArtefacts } from "./stage.mjs";
4
+
5
+ //#region ../../packages/core/src/release/activate.ts
6
+ /**
7
+ * The checks every activation must pass.
8
+ *
9
+ * The version rule is strict on purpose: the candidate has to be greater than
10
+ * the active release. Equal versions need `allowSame` (a rebuild of the same
11
+ * version from another commit); a lower version is never activated this way,
12
+ * `release rollback` is the path back.
13
+ */
14
+ async function runPreflight(context, manifest, options = {}) {
15
+ const checks = [];
16
+ const activeId = await readPointer(context, "current");
17
+ checks.push({
18
+ name: "staged",
19
+ ok: manifest.status === "staged" && await hasStagedArtefacts(context, manifest.id),
20
+ detail: "staged artefacts are present (run `warlock release stage`)"
21
+ });
22
+ checks.push({
23
+ name: "gate",
24
+ ok: manifest.gate?.passed === true,
25
+ detail: "the gate passed"
26
+ });
27
+ if (manifest.migrations) checks.push({
28
+ name: "migrations",
29
+ ok: manifest.migrations.passed,
30
+ detail: "the recorded migration proof passed"
31
+ });
32
+ if (activeId === manifest.id) checks.push({
33
+ name: "not-active",
34
+ ok: false,
35
+ detail: "this release is already active"
36
+ });
37
+ else if (activeId) {
38
+ const active = await readManifest(context, activeId);
39
+ const order = compareVersions(manifest.version, active.version);
40
+ checks.push({
41
+ name: "version",
42
+ ok: order > 0 || order === 0 && options.allowSame === true,
43
+ detail: order > 0 ? `${manifest.version} is greater than the active ${active.version}` : order === 0 ? `${manifest.version} equals the active version (pass --allow-same to accept)` : `${manifest.version} is lower than the active ${active.version} (use \`release rollback\`)`
44
+ });
45
+ } else checks.push({
46
+ name: "version",
47
+ ok: true,
48
+ detail: "no active release yet"
49
+ });
50
+ return {
51
+ checks,
52
+ activeId
53
+ };
54
+ }
55
+ async function restart(context, id) {
56
+ const command = context.config.restart.command;
57
+ if (!command) return {
58
+ ran: false,
59
+ ok: true
60
+ };
61
+ const result = await context.runner(command, {
62
+ cwd: context.root,
63
+ timeoutMs: context.config.restart.timeoutMs,
64
+ env: {
65
+ WARLOCK_RELEASE_ID: id,
66
+ WARLOCK_RELEASE_DIR: releaseDir(context, id),
67
+ WARLOCK_RELEASE_CURRENT: pointerPath(context, "current")
68
+ }
69
+ });
70
+ return {
71
+ ran: true,
72
+ ok: result.code === 0 && !result.timedOut,
73
+ output: result.output
74
+ };
75
+ }
76
+ /**
77
+ * Preflight, then switch `current` to the release and run the restart
78
+ * command.
79
+ *
80
+ * The switch is one atomic symlink rename. The old `current` becomes
81
+ * `previous`. Warlock restarts nothing itself: only the configured
82
+ * `release.restart.command` runs, and when it is unset the pointer switch is
83
+ * the whole activation. If the restart command fails the pointer stays on the
84
+ * new release (the restart may have partly taken effect) and the result says
85
+ * so; `release rollback` is the way back.
86
+ *
87
+ * @example
88
+ * const result = await activateRelease(context, "1.4.0-a1b2c3d", { dryRun: true });
89
+ */
90
+ async function activateRelease(context, id, options = {}) {
91
+ const manifest = await readManifest(context, id);
92
+ const { checks, activeId } = await runPreflight(context, manifest, options);
93
+ const failed = checks.filter((check) => !check.ok);
94
+ const base = {
95
+ id,
96
+ previousId: activeId,
97
+ checks,
98
+ switched: false,
99
+ dryRun: options.dryRun === true,
100
+ restarted: null
101
+ };
102
+ if (failed.length > 0) {
103
+ const message = `Preflight failed: ${failed.map((check) => check.name).join(", ")}.`;
104
+ if (!options.preflightOnly && !options.dryRun) await appendLog(context, {
105
+ event: "activate",
106
+ release: id,
107
+ ok: false,
108
+ detail: message
109
+ });
110
+ return {
111
+ ...base,
112
+ ok: false,
113
+ message
114
+ };
115
+ }
116
+ if (options.preflightOnly) return {
117
+ ...base,
118
+ ok: true,
119
+ message: "Preflight passed."
120
+ };
121
+ if (options.dryRun) return {
122
+ ...base,
123
+ ok: true,
124
+ message: `Dry run: would switch current to ${id}${activeId ? ` (previous becomes ${activeId})` : ""}${context.config.restart.command ? " and run the restart command" : ""}.`
125
+ };
126
+ if (activeId) await switchPointer(context, "previous", activeId);
127
+ await switchPointer(context, "current", id);
128
+ if (activeId) {
129
+ const old = await readManifest(context, activeId);
130
+ await writeManifest(context, {
131
+ ...old,
132
+ status: "superseded"
133
+ });
134
+ }
135
+ const restarted = await restart(context, id);
136
+ await writeManifest(context, {
137
+ ...manifest,
138
+ status: "active",
139
+ activation: {
140
+ at: context.now().toISOString(),
141
+ previousId: activeId,
142
+ restarted: restarted.ran && restarted.ok
143
+ }
144
+ });
145
+ if (!restarted.ok) {
146
+ const message = `Switched current to ${id}, but the restart command failed. Fix it or run \`warlock release rollback\`.`;
147
+ await appendLog(context, {
148
+ event: "activate",
149
+ release: id,
150
+ ok: false,
151
+ detail: message
152
+ });
153
+ return {
154
+ ...base,
155
+ switched: true,
156
+ restarted: false,
157
+ restartOutput: restarted.output,
158
+ ok: false,
159
+ message
160
+ };
161
+ }
162
+ const message = `Activated ${id}${restarted.ran ? " and ran the restart command" : ""}.`;
163
+ await appendLog(context, {
164
+ event: "activate",
165
+ release: id,
166
+ ok: true,
167
+ detail: message
168
+ });
169
+ return {
170
+ ...base,
171
+ switched: true,
172
+ restarted: restarted.ran ? true : null,
173
+ ok: true,
174
+ message
175
+ };
176
+ }
177
+ /**
178
+ * Switch `current` back to `previous`, and `previous` to the release that was
179
+ * just left (so a second rollback returns to it), then run the restart
180
+ * command.
181
+ *
182
+ * Migrations are never rolled back here. A migration may have dropped or
183
+ * rewritten data that no reverse step can restore, and the code that ran
184
+ * before usually tolerates the newer schema; deciding to undo a schema change
185
+ * is a separate, deliberate step (`warlock migrate --rollback`) taken with the
186
+ * data in view.
187
+ */
188
+ async function rollbackRelease(context, options = {}) {
189
+ const current = await readPointer(context, "current");
190
+ const previous = await readPointer(context, "previous");
191
+ const base = {
192
+ from: current,
193
+ to: previous,
194
+ switched: false,
195
+ dryRun: options.dryRun === true,
196
+ restarted: null
197
+ };
198
+ if (!current || !previous) return {
199
+ ...base,
200
+ ok: false,
201
+ message: "There is no previous release to roll back to."
202
+ };
203
+ const target = await readManifest(context, previous);
204
+ const left = await readManifest(context, current);
205
+ if (!await hasStagedArtefacts(context, previous)) return {
206
+ ...base,
207
+ ok: false,
208
+ message: `The staged files of ${previous} are gone; cannot roll back to it.`
209
+ };
210
+ if (options.dryRun) return {
211
+ ...base,
212
+ ok: true,
213
+ message: `Dry run: would switch current back to ${previous}${context.config.restart.command ? " and run the restart command" : ""}.`
214
+ };
215
+ await switchPointer(context, "previous", current);
216
+ await switchPointer(context, "current", previous);
217
+ await writeManifest(context, {
218
+ ...left,
219
+ status: "superseded"
220
+ });
221
+ await writeManifest(context, {
222
+ ...target,
223
+ status: "active"
224
+ });
225
+ const restarted = await restart(context, previous);
226
+ if (!restarted.ok) {
227
+ const message = `Rolled current back to ${previous}, but the restart command failed.`;
228
+ await appendLog(context, {
229
+ event: "rollback",
230
+ release: previous,
231
+ ok: false,
232
+ detail: message
233
+ });
234
+ return {
235
+ ...base,
236
+ switched: true,
237
+ restarted: false,
238
+ restartOutput: restarted.output,
239
+ ok: false,
240
+ message
241
+ };
242
+ }
243
+ const message = `Rolled back from ${current} to ${previous}${restarted.ran ? " and ran the restart command" : ""}.`;
244
+ await appendLog(context, {
245
+ event: "rollback",
246
+ release: previous,
247
+ ok: true,
248
+ detail: message
249
+ });
250
+ return {
251
+ ...base,
252
+ switched: true,
253
+ restarted: restarted.ran ? true : null,
254
+ ok: true,
255
+ message
256
+ };
257
+ }
258
+
259
+ //#endregion
260
+ export { activateRelease, rollbackRelease, runPreflight };
261
+ //# sourceMappingURL=activate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activate.mjs","names":[],"sources":["../../../../../../../../packages/core/src/release/activate.ts"],"sourcesContent":["import { compareVersions } from \"./semver\";\nimport { hasStagedArtefacts } from \"./stage\";\nimport {\n appendLog,\n pointerPath,\n readManifest,\n readPointer,\n releaseDir,\n switchPointer,\n writeManifest,\n type ReleaseContext,\n} from \"./store\";\nimport type { ReleaseManifest } from \"./types\";\n\nexport type PreflightCheck = { name: string; ok: boolean; detail: string };\n\nexport type ActivateOptions = {\n /** Run the preflight checks and stop. */\n preflightOnly?: boolean;\n /** Report what would change without changing anything. */\n dryRun?: boolean;\n /** Accept a version equal to the active one (a different commit). */\n allowSame?: boolean;\n};\n\nexport type ActivationResult = {\n id: string;\n /** The release that was active before, if any */\n previousId: string | null;\n checks: PreflightCheck[];\n /** Whether the pointer was switched */\n switched: boolean;\n dryRun: boolean;\n /** Whether the restart command ran, and succeeded */\n restarted: boolean | null;\n restartOutput?: string;\n ok: boolean;\n message: string;\n};\n\n/**\n * The checks every activation must pass.\n *\n * The version rule is strict on purpose: the candidate has to be greater than\n * the active release. Equal versions need `allowSame` (a rebuild of the same\n * version from another commit); a lower version is never activated this way,\n * `release rollback` is the path back.\n */\nexport async function runPreflight(\n context: ReleaseContext,\n manifest: ReleaseManifest,\n options: Pick<ActivateOptions, \"allowSame\"> = {},\n): Promise<{ checks: PreflightCheck[]; activeId: string | null }> {\n const checks: PreflightCheck[] = [];\n const activeId = await readPointer(context, \"current\");\n\n checks.push({\n name: \"staged\",\n ok: manifest.status === \"staged\" && (await hasStagedArtefacts(context, manifest.id)),\n detail: \"staged artefacts are present (run `warlock release stage`)\",\n });\n\n checks.push({\n name: \"gate\",\n ok: manifest.gate?.passed === true,\n detail: \"the gate passed\",\n });\n\n if (manifest.migrations) {\n checks.push({\n name: \"migrations\",\n ok: manifest.migrations.passed,\n detail: \"the recorded migration proof passed\",\n });\n }\n\n if (activeId === manifest.id) {\n checks.push({ name: \"not-active\", ok: false, detail: \"this release is already active\" });\n } else if (activeId) {\n const active = await readManifest(context, activeId);\n const order = compareVersions(manifest.version, active.version);\n\n checks.push({\n name: \"version\",\n ok: order > 0 || (order === 0 && options.allowSame === true),\n detail:\n order > 0\n ? `${manifest.version} is greater than the active ${active.version}`\n : order === 0\n ? `${manifest.version} equals the active version (pass --allow-same to accept)`\n : `${manifest.version} is lower than the active ${active.version} (use \\`release rollback\\`)`,\n });\n } else {\n checks.push({ name: \"version\", ok: true, detail: \"no active release yet\" });\n }\n\n return { checks, activeId };\n}\n\nasync function restart(\n context: ReleaseContext,\n id: string,\n): Promise<{ ran: boolean; ok: boolean; output?: string }> {\n const command = context.config.restart.command;\n\n if (!command) return { ran: false, ok: true };\n\n const result = await context.runner(command, {\n cwd: context.root,\n timeoutMs: context.config.restart.timeoutMs,\n env: {\n WARLOCK_RELEASE_ID: id,\n WARLOCK_RELEASE_DIR: releaseDir(context, id),\n WARLOCK_RELEASE_CURRENT: pointerPath(context, \"current\"),\n },\n });\n\n return { ran: true, ok: result.code === 0 && !result.timedOut, output: result.output };\n}\n\n/**\n * Preflight, then switch `current` to the release and run the restart\n * command.\n *\n * The switch is one atomic symlink rename. The old `current` becomes\n * `previous`. Warlock restarts nothing itself: only the configured\n * `release.restart.command` runs, and when it is unset the pointer switch is\n * the whole activation. If the restart command fails the pointer stays on the\n * new release (the restart may have partly taken effect) and the result says\n * so; `release rollback` is the way back.\n *\n * @example\n * const result = await activateRelease(context, \"1.4.0-a1b2c3d\", { dryRun: true });\n */\nexport async function activateRelease(\n context: ReleaseContext,\n id: string,\n options: ActivateOptions = {},\n): Promise<ActivationResult> {\n const manifest = await readManifest(context, id);\n const { checks, activeId } = await runPreflight(context, manifest, options);\n const failed = checks.filter((check) => !check.ok);\n const base = {\n id,\n previousId: activeId,\n checks,\n switched: false,\n dryRun: options.dryRun === true,\n restarted: null,\n };\n\n if (failed.length > 0) {\n const message = `Preflight failed: ${failed.map((check) => check.name).join(\", \")}.`;\n\n if (!options.preflightOnly && !options.dryRun) {\n await appendLog(context, { event: \"activate\", release: id, ok: false, detail: message });\n }\n\n return { ...base, ok: false, message };\n }\n\n if (options.preflightOnly) {\n return { ...base, ok: true, message: \"Preflight passed.\" };\n }\n\n if (options.dryRun) {\n return {\n ...base,\n ok: true,\n message: `Dry run: would switch current to ${id}${activeId ? ` (previous becomes ${activeId})` : \"\"}${\n context.config.restart.command ? \" and run the restart command\" : \"\"\n }.`,\n };\n }\n\n if (activeId) await switchPointer(context, \"previous\", activeId);\n await switchPointer(context, \"current\", id);\n\n if (activeId) {\n const old = await readManifest(context, activeId);\n\n await writeManifest(context, { ...old, status: \"superseded\" });\n }\n\n const restarted = await restart(context, id);\n\n await writeManifest(context, {\n ...manifest,\n status: \"active\",\n activation: {\n at: context.now().toISOString(),\n previousId: activeId,\n restarted: restarted.ran && restarted.ok,\n },\n });\n\n if (!restarted.ok) {\n const message = `Switched current to ${id}, but the restart command failed. Fix it or run \\`warlock release rollback\\`.`;\n\n await appendLog(context, { event: \"activate\", release: id, ok: false, detail: message });\n\n return {\n ...base,\n switched: true,\n restarted: false,\n restartOutput: restarted.output,\n ok: false,\n message,\n };\n }\n\n const message = `Activated ${id}${restarted.ran ? \" and ran the restart command\" : \"\"}.`;\n\n await appendLog(context, { event: \"activate\", release: id, ok: true, detail: message });\n\n return {\n ...base,\n switched: true,\n restarted: restarted.ran ? true : null,\n ok: true,\n message,\n };\n}\n\nexport type RollbackResult = {\n from: string | null;\n to: string | null;\n switched: boolean;\n dryRun: boolean;\n restarted: boolean | null;\n restartOutput?: string;\n ok: boolean;\n message: string;\n};\n\n/**\n * Switch `current` back to `previous`, and `previous` to the release that was\n * just left (so a second rollback returns to it), then run the restart\n * command.\n *\n * Migrations are never rolled back here. A migration may have dropped or\n * rewritten data that no reverse step can restore, and the code that ran\n * before usually tolerates the newer schema; deciding to undo a schema change\n * is a separate, deliberate step (`warlock migrate --rollback`) taken with the\n * data in view.\n */\nexport async function rollbackRelease(\n context: ReleaseContext,\n options: { dryRun?: boolean } = {},\n): Promise<RollbackResult> {\n const current = await readPointer(context, \"current\");\n const previous = await readPointer(context, \"previous\");\n const base = {\n from: current,\n to: previous,\n switched: false,\n dryRun: options.dryRun === true,\n restarted: null,\n };\n\n if (!current || !previous) {\n return { ...base, ok: false, message: \"There is no previous release to roll back to.\" };\n }\n\n // Both manifests must exist before anything moves.\n const target = await readManifest(context, previous);\n const left = await readManifest(context, current);\n\n if (!(await hasStagedArtefacts(context, previous))) {\n return {\n ...base,\n ok: false,\n message: `The staged files of ${previous} are gone; cannot roll back to it.`,\n };\n }\n\n if (options.dryRun) {\n return {\n ...base,\n ok: true,\n message: `Dry run: would switch current back to ${previous}${\n context.config.restart.command ? \" and run the restart command\" : \"\"\n }.`,\n };\n }\n\n await switchPointer(context, \"previous\", current);\n await switchPointer(context, \"current\", previous);\n await writeManifest(context, { ...left, status: \"superseded\" });\n await writeManifest(context, { ...target, status: \"active\" });\n\n const restarted = await restart(context, previous);\n\n if (!restarted.ok) {\n const message = `Rolled current back to ${previous}, but the restart command failed.`;\n\n await appendLog(context, { event: \"rollback\", release: previous, ok: false, detail: message });\n\n return {\n ...base,\n switched: true,\n restarted: false,\n restartOutput: restarted.output,\n ok: false,\n message,\n };\n }\n\n const message = `Rolled back from ${current} to ${previous}${\n restarted.ran ? \" and ran the restart command\" : \"\"\n }.`;\n\n await appendLog(context, { event: \"rollback\", release: previous, ok: true, detail: message });\n\n return {\n ...base,\n switched: true,\n restarted: restarted.ran ? true : null,\n ok: true,\n message,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ import { appendLog, releaseDir, releasesDir, writeManifest } from "./store.mjs";
2
+ import { createHash } from "node:crypto";
3
+ import path from "node:path";
4
+ import { access, mkdir, readFile, rm, symlink } from "node:fs/promises";
5
+
6
+ //#region ../../packages/core/src/release/candidate.ts
7
+ const LOCKFILES = [
8
+ "pnpm-lock.yaml",
9
+ "package-lock.json",
10
+ "yarn.lock",
11
+ "bun.lock",
12
+ "bun.lockb"
13
+ ];
14
+ function shellQuote(value) {
15
+ return `'${value.replace(/'/g, `'\\''`)}'`;
16
+ }
17
+ async function exists(file) {
18
+ try {
19
+ await access(file);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function git(context, command) {
26
+ const result = await context.runner(`git ${command}`, { cwd: context.root });
27
+ if (result.code !== 0) throw new Error(`git ${command} failed: ${result.output.trim() || `exit ${result.code}`}`);
28
+ return result.output.trim();
29
+ }
30
+ /**
31
+ * Freeze a commit into a candidate directory.
32
+ *
33
+ * The commit is exported with `git archive` into `<release>/source`, so every
34
+ * later step (gate, build, stage) works on exactly those bytes and not on a
35
+ * working tree that keeps changing. A dirty tree is refused unless
36
+ * `allowDirty` is set, because the candidate would not describe what is
37
+ * checked out.
38
+ *
39
+ * @example
40
+ * const manifest = await createCandidate(context, {});
41
+ */
42
+ async function createCandidate(context, options = {}) {
43
+ if (!options.allowDirty) {
44
+ const dirty = await git(context, `status --porcelain -- . ${shellQuote(`:(exclude)${context.config.directory}`)}`);
45
+ if (dirty) throw new Error(`The working tree has uncommitted changes:\n${dirty}\nCommit or stash them, or pass --allow-dirty.`);
46
+ }
47
+ const commit = await git(context, `rev-parse --verify ${shellQuote(`${options.commit ?? "HEAD"}^{commit}`)}`);
48
+ const packageText = await git(context, `show ${shellQuote(`${commit}:package.json`)}`);
49
+ const version = JSON.parse(packageText).version;
50
+ if (!version) throw new Error("package.json has no `version`; a release needs one.");
51
+ const id = `${version}-${commit.slice(0, 7)}`;
52
+ const directory = releaseDir(context, id);
53
+ if (await exists(directory)) throw new Error(`Release "${id}" already exists. Releases are never overwritten.`);
54
+ const source = path.join(directory, "source");
55
+ await mkdir(releasesDir(context), { recursive: true });
56
+ await mkdir(source, { recursive: true });
57
+ try {
58
+ const archive = path.join(directory, "source.tar");
59
+ await git(context, `archive --format=tar -o ${shellQuote(archive)} ${shellQuote(commit)}`);
60
+ const extracted = await context.runner(`tar -xf ${shellQuote(archive)} -C ${shellQuote(source)}`, { cwd: context.root });
61
+ await rm(archive, { force: true });
62
+ if (extracted.code !== 0) throw new Error(`Could not extract the commit archive: ${extracted.output.trim()}`);
63
+ if (context.config.linkNodeModules) {
64
+ const modules = path.join(context.root, "node_modules");
65
+ if (await exists(modules)) await symlink(modules, path.join(source, "node_modules"), "dir");
66
+ }
67
+ let lockfile = null;
68
+ for (const file of LOCKFILES) {
69
+ const location = path.join(source, file);
70
+ if (await exists(location)) {
71
+ lockfile = {
72
+ file,
73
+ sha256: createHash("sha256").update(await readFile(location)).digest("hex")
74
+ };
75
+ break;
76
+ }
77
+ }
78
+ const manifest = {
79
+ schema: 1,
80
+ id,
81
+ version,
82
+ commit,
83
+ createdAt: context.now().toISOString(),
84
+ nodeVersion: context.nodeVersion,
85
+ lockfile,
86
+ status: "candidate"
87
+ };
88
+ await writeManifest(context, manifest);
89
+ await appendLog(context, {
90
+ event: "candidate",
91
+ release: id,
92
+ ok: true,
93
+ detail: commit
94
+ });
95
+ return manifest;
96
+ } catch (error) {
97
+ await rm(directory, {
98
+ recursive: true,
99
+ force: true
100
+ });
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ //#endregion
106
+ export { createCandidate, shellQuote };
107
+ //# sourceMappingURL=candidate.mjs.map
@@ -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":"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"}
@@ -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"}