@astra-kit/rp 0.0.0-stage → 0.10.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 (125) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +352 -2
  4. package/bin/astra-rp.js +51 -0
  5. package/package.json +93 -4
  6. package/src/build-gate.js +157 -0
  7. package/src/changelog-blocks.js +53 -0
  8. package/src/changelog.js +108 -0
  9. package/src/constants/build-gate.js +30 -0
  10. package/src/constants/changelog-draft.js +180 -0
  11. package/src/constants/changelog.js +74 -0
  12. package/src/constants/ci-release.js +346 -0
  13. package/src/constants/cli.js +12 -0
  14. package/src/constants/create-version.js +523 -0
  15. package/src/constants/guard-publish.js +29 -0
  16. package/src/constants/index.js +18 -0
  17. package/src/constants/monorepo.js +57 -0
  18. package/src/constants/package-manager.js +56 -0
  19. package/src/constants/registry.js +72 -0
  20. package/src/constants/terminal-ui.js +93 -0
  21. package/src/constants/version-files.js +63 -0
  22. package/src/constants/versions.js +60 -0
  23. package/src/create-version/artifact.js +343 -0
  24. package/src/create-version/changelog-draft-workspace.js +124 -0
  25. package/src/create-version/changelog-draft.js +380 -0
  26. package/src/create-version/changelog.js +264 -0
  27. package/src/create-version/ci-config.js +54 -0
  28. package/src/create-version/ci-publication-history.js +113 -0
  29. package/src/create-version/ci-release-identity-step.js +58 -0
  30. package/src/create-version/ci-setup.js +794 -0
  31. package/src/create-version/ci.js +555 -0
  32. package/src/create-version/committed-tree.js +75 -0
  33. package/src/create-version/config.js +489 -0
  34. package/src/create-version/errors.js +46 -0
  35. package/src/create-version/github-workflow.js +199 -0
  36. package/src/create-version/index.js +54 -0
  37. package/src/create-version/json.js +32 -0
  38. package/src/create-version/jsonc.js +76 -0
  39. package/src/create-version/jsr.js +96 -0
  40. package/src/create-version/local-changes.js +178 -0
  41. package/src/create-version/npm-auth.js +142 -0
  42. package/src/create-version/npm.js +812 -0
  43. package/src/create-version/plan.js +1200 -0
  44. package/src/create-version/process.js +334 -0
  45. package/src/create-version/registry-config.js +121 -0
  46. package/src/create-version/registry-http.js +62 -0
  47. package/src/create-version/registry.js +220 -0
  48. package/src/create-version/release-execution.js +45 -0
  49. package/src/create-version/release-tag-config.js +76 -0
  50. package/src/create-version/run.js +2087 -0
  51. package/src/create-version/state.js +461 -0
  52. package/src/create-version/strict-utf8.js +11 -0
  53. package/src/create-version/templates/release.yml +52 -0
  54. package/src/create-version/templates/vercel-ignore-build.mjs +66 -0
  55. package/src/guard-publish.js +77 -0
  56. package/src/index.js +42 -0
  57. package/src/monorepo/plan.js +475 -0
  58. package/src/monorepo/release-commit.js +46 -0
  59. package/src/monorepo/run.js +927 -0
  60. package/src/monorepo/state.js +206 -0
  61. package/src/monorepo/workspaces.js +327 -0
  62. package/src/package-manager.js +107 -0
  63. package/src/terminal-ui.js +548 -0
  64. package/src/testing.js +70 -0
  65. package/src/version-files.js +142 -0
  66. package/src/versions.js +303 -0
  67. package/types/build-gate.d.ts +43 -0
  68. package/types/changelog-blocks.d.ts +33 -0
  69. package/types/changelog.d.ts +61 -0
  70. package/types/constants/build-gate.d.ts +23 -0
  71. package/types/constants/changelog-draft.d.ts +138 -0
  72. package/types/constants/changelog.d.ts +56 -0
  73. package/types/constants/ci-release.d.ts +284 -0
  74. package/types/constants/cli.d.ts +11 -0
  75. package/types/constants/create-version.d.ts +397 -0
  76. package/types/constants/guard-publish.d.ts +25 -0
  77. package/types/constants/index.d.ts +17 -0
  78. package/types/constants/monorepo.d.ts +45 -0
  79. package/types/constants/package-manager.d.ts +40 -0
  80. package/types/constants/registry.d.ts +79 -0
  81. package/types/constants/terminal-ui.d.ts +73 -0
  82. package/types/constants/version-files.d.ts +51 -0
  83. package/types/constants/versions.d.ts +47 -0
  84. package/types/create-version/artifact.d.ts +113 -0
  85. package/types/create-version/changelog-draft-workspace.d.ts +33 -0
  86. package/types/create-version/changelog-draft.d.ts +127 -0
  87. package/types/create-version/changelog.d.ts +141 -0
  88. package/types/create-version/ci-config.d.ts +33 -0
  89. package/types/create-version/ci-publication-history.d.ts +51 -0
  90. package/types/create-version/ci-release-identity-step.d.ts +12 -0
  91. package/types/create-version/ci-setup.d.ts +148 -0
  92. package/types/create-version/ci.d.ts +145 -0
  93. package/types/create-version/committed-tree.d.ts +55 -0
  94. package/types/create-version/config.d.ts +128 -0
  95. package/types/create-version/errors.d.ts +39 -0
  96. package/types/create-version/github-workflow.d.ts +41 -0
  97. package/types/create-version/index.d.ts +28 -0
  98. package/types/create-version/json.d.ts +21 -0
  99. package/types/create-version/jsonc.d.ts +21 -0
  100. package/types/create-version/jsr.d.ts +52 -0
  101. package/types/create-version/local-changes.d.ts +52 -0
  102. package/types/create-version/npm-auth.d.ts +60 -0
  103. package/types/create-version/npm.d.ts +331 -0
  104. package/types/create-version/plan.d.ts +369 -0
  105. package/types/create-version/process.d.ts +151 -0
  106. package/types/create-version/registry-config.d.ts +56 -0
  107. package/types/create-version/registry-http.d.ts +22 -0
  108. package/types/create-version/registry.d.ts +86 -0
  109. package/types/create-version/release-execution.d.ts +22 -0
  110. package/types/create-version/release-tag-config.d.ts +18 -0
  111. package/types/create-version/run.d.ts +407 -0
  112. package/types/create-version/state.d.ts +204 -0
  113. package/types/create-version/strict-utf8.d.ts +10 -0
  114. package/types/guard-publish.d.ts +49 -0
  115. package/types/index.d.ts +23 -0
  116. package/types/monorepo/plan.d.ts +68 -0
  117. package/types/monorepo/release-commit.d.ts +29 -0
  118. package/types/monorepo/run.d.ts +68 -0
  119. package/types/monorepo/state.d.ts +96 -0
  120. package/types/monorepo/workspaces.d.ts +86 -0
  121. package/types/package-manager.d.ts +58 -0
  122. package/types/terminal-ui.d.ts +196 -0
  123. package/types/testing.d.ts +31 -0
  124. package/types/version-files.d.ts +59 -0
  125. package/types/versions.d.ts +158 -0
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Loads and validates `astra-rp.config.js`, the per-project part of
3
+ * `astra-rp create-version`: version
4
+ * descriptions, registry, checks, migrations, preparation and publication.
5
+ *
6
+ * The configuration never needs to import `@astra-kit/rp`: hooks receive every
7
+ * helper through their context, so a project can type it with
8
+ * `@type {import("@astra-kit/rp/create-version").CreateVersionConfig}` only.
9
+ *
10
+ * @module create-version/config
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import path from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+
17
+ import {
18
+ ARTIFACT_VERSION_PLACEHOLDER,
19
+ CREATE_VERSION_CONFIG_FILE,
20
+ CREATE_VERSION_CONFIG_FILES,
21
+ DEFAULT_CHECKS_SCRIPT,
22
+ DEFAULT_RELEASE_TYPE_DESCRIPTIONS,
23
+ NPM_PUBLISHER,
24
+ PACKAGE_MANIFEST_FILE,
25
+ PRE_MAJOR_SHIFTED_RELEASE_TYPE_DESCRIPTIONS,
26
+ RELEASE_REGISTRY,
27
+ } from "../constants/create-version.js";
28
+ import { DEFAULT_MONOREPO_TAG_FORMAT, TAG_FORMAT_PLACEHOLDER, WORKSPACES_PACKAGES } from "../constants/monorepo.js";
29
+ import { RELEASE_COMMIT_BUILT_IN_FILES } from "../constants/version-files.js";
30
+ import { CHANGELOG_FILE, RELEASE_TIME_ZONE_EXAMPLE, RELEASE_TIME_ZONE_FIELD } from "../constants/changelog.js";
31
+ import { CHANGELOG_DRAFT_FIELD, CHANGELOG_DRAFT_PROVIDER, DEFAULT_CHANGELOG_DRAFT_PROVIDERS } from "../constants/changelog-draft.js";
32
+ import { RELEASE_TYPE, RELEASE_TYPE_ORDER } from "../constants/versions.js";
33
+ import { DEFAULT_PROJECT_COMMANDS, describeProjectCommands, detectPackageManager } from "../package-manager.js";
34
+ import { isRegistryProvider, resolvePublicationOptions } from "./registry-config.js";
35
+ import { isPreMajorShiftActive } from "../versions.js";
36
+ import { resolveCiReleaseConfig } from "./ci-config.js";
37
+ import { isRecord } from "./json.js";
38
+ import { formatReleaseDate } from "./changelog.js";
39
+ import { CI_CONFIG_EXPORT } from "../constants/ci-release.js";
40
+
41
+ /**
42
+ * @typedef {import("./process.js").GitReader} GitReader
43
+ * @typedef {import("../package-manager.js").ProjectCommands} ProjectCommands
44
+ * @typedef {"patch" | "minor" | "major"} ReleaseType
45
+ * @typedef {{ status: "up-to-date" | "pending" | "unknown", pending: string[], target: string | null, reason: string | null }} MigrationCheck
46
+ * @typedef {{
47
+ * repositoryRoot: string,
48
+ * version: string | null,
49
+ * git: GitReader,
50
+ * run: (commandLine: string) => Promise<number>,
51
+ * print: (text?: string) => void,
52
+ * fail: (message: string, hint: string) => never,
53
+ * releases?: { name: string, version: string, directory: string }[],
54
+ * }} HookContext
55
+ * In monorepo mode `version` is `null` for the steps that cover several packages (`prepare`) and
56
+ * `releases` lists them; `publish` hooks run once per package with its `version` and `releases`
57
+ * holding only that package.
58
+ * @typedef {(context: HookContext) => Promise<void> | void} ReleaseHook
59
+ * @typedef {{
60
+ * check: (context: HookContext) => Promise<MigrationCheck> | MigrationCheck,
61
+ * apply: (context: HookContext) => Promise<void> | void,
62
+ * targetHint?: string,
63
+ * }} MigrationsAdapter
64
+ * @typedef {{
65
+ * projectName?: string,
66
+ * releaseTypeDescriptions?: Partial<Record<ReleaseType, string>>,
67
+ * preMajorShift?: boolean,
68
+ * registry?: import("./registry-config.js").RegistryProvider | null,
69
+ * publication?: import("./registry-config.js").PublicationOptions,
70
+ * publishedLabel?: string,
71
+ * checks?: string[] | false,
72
+ * migrations?: MigrationsAdapter | null,
73
+ * prepare?: string[] | ReleaseHook | null,
74
+ * publish?: import("./registry-config.js").RegistryProvider | ReleaseHook | null,
75
+ * artifact?: string | null,
76
+ * summary?: string[],
77
+ * versionFiles?: string[],
78
+ * packages?: "workspaces" | string[],
79
+ * tagFormat?: string,
80
+ * ci?: import("./ci-config.js").CiReleaseConfig | null,
81
+ * releaseTimeZone?: string,
82
+ * changelogDraft?: boolean | import("./changelog-draft.js").ChangelogDraftProvider[],
83
+ * }} CreateVersionConfig
84
+ * `summary` lines replace `{version}` with the released version (in monorepo mode, a line with
85
+ * `{version}` or `{name}` is printed once per released package). Without `checks`, the release
86
+ * runs `<package manager> run ci` (pnpm, bun, npm or yarn, detected from `packageManager` or the
87
+ * lockfile) when `package.json` declares a `ci` script; `false` skips the checks on purpose.
88
+ * `versionFiles` lists files (relative to the root) whose marked lines get the new version in the
89
+ * release commit: `astra-rp-version` / `x-release-please-version` lines and
90
+ * `astra-rp-start-version`…`astra-rp-end` blocks. Entries may use `/` or `\` and are resolved to the
91
+ * `/`-separated form (`.\src\cli.js` → `src/cli.js`); each must be a file tracked by Git.
92
+ * `packages` turns on the monorepo mode: every non-private workspace package (`"workspaces"`: the
93
+ * ones the root `package.json` declares; or explicit patterns such as `["packages/*"]`) gets its
94
+ * own version, CHANGELOG and tag, formatted with `tagFormat` (`{component}-v{version}` by default).
95
+ * Changelog entries are written by hand under `## [Unreleased]`, which must change since the last
96
+ * release and hold at least one entry; the release commit renames it to `## [X.Y.Z] - YYYY-MM-DD`
97
+ * and leaves a new, empty `## [Unreleased]` on top. `releaseTimeZone` is the IANA time zone of that
98
+ * date (such as `America/Argentina/Buenos_Aires`), in single-package and monorepo mode, locally and
99
+ * in CI; without it the date is the local day of the machine that creates the release commit.
100
+ * `changelogDraft` drafts missing `[Unreleased]` entries with an agent CLI before a new local release
101
+ * stops for them: `true` tries Claude Code and then Codex, a list sets which ones and their order.
102
+ * `preMajorShift` lowers the suggested release type one level while a version is `0.x`
103
+ * (breaking → minor, features → patch).
104
+ * @typedef {{
105
+ * projectName: string | null,
106
+ * releaseTypeDescriptions: Record<ReleaseType, string>,
107
+ * preMajorShift: boolean,
108
+ * registry: import("./registry-config.js").RegistryProvider | null,
109
+ * publication: import("./registry-config.js").ResolvedPublicationOptions,
110
+ * publishedLabel: string,
111
+ * checks: string[] | null,
112
+ * migrations: MigrationsAdapter | null,
113
+ * prepare: string[] | ReleaseHook | null,
114
+ * publish: import("./registry-config.js").RegistryProvider | ReleaseHook | null,
115
+ * artifact: string | null,
116
+ * summary: string[],
117
+ * versionFiles: string[],
118
+ * packages: "workspaces" | string[] | null,
119
+ * tagFormat: string | null,
120
+ * ci: import("./ci-config.js").ResolvedCiReleaseConfig | null,
121
+ * releaseTimeZone: string | null,
122
+ * changelogDraft: import("./changelog-draft.js").ChangelogDraftProvider[],
123
+ * commands: ProjectCommands,
124
+ * }} ResolvedCreateVersionConfig
125
+ * `checks` is empty when they are skipped on purpose and `null` when none are configured nor
126
+ * found, which blocks a new release. `commands` are the project's package manager commands, used
127
+ * by the default checks and by every "run it again" hint.
128
+ * @typedef {{ packageScripts?: Record<string, unknown>, commands?: ProjectCommands }} ConfigResolutionContext
129
+ */
130
+
131
+ /** Banner suffix of the version on `origin/main` when a project does not name its environment. */
132
+ const DEFAULT_PUBLISHED_LABEL = "publicada";
133
+
134
+ /**
135
+ * Describes the release types offered after `currentVersion`. While `preMajorShift` applies (the
136
+ * same `0.x` rule as the suggestion), the default descriptions move one level down with the
137
+ * suggestion: a patch carries features and a minor breaking changes. Descriptions the project
138
+ * defined are kept as they are.
139
+ *
140
+ * @param {Pick<ResolvedCreateVersionConfig, "releaseTypeDescriptions" | "preMajorShift">} config - Resolved configuration.
141
+ * @param {string} currentVersion - Current `X.Y.Z` version.
142
+ * @returns {Record<ReleaseType, string>} Description of each release type.
143
+ */
144
+ export function describeReleaseTypes({ releaseTypeDescriptions, preMajorShift }, currentVersion) {
145
+ if (!isPreMajorShiftActive(currentVersion, { preMajorShift })) {
146
+ return releaseTypeDescriptions;
147
+ }
148
+
149
+ /**
150
+ * @param {ReleaseType} releaseType - Release type to describe.
151
+ * @returns {string} The project's description, or the shifted default.
152
+ */
153
+ const describe = (releaseType) => {
154
+ const description = releaseTypeDescriptions[releaseType];
155
+ return description === DEFAULT_RELEASE_TYPE_DESCRIPTIONS[releaseType] ? PRE_MAJOR_SHIFTED_RELEASE_TYPE_DESCRIPTIONS[releaseType] : description;
156
+ };
157
+
158
+ return {
159
+ [RELEASE_TYPE.patch]: describe(RELEASE_TYPE.patch),
160
+ [RELEASE_TYPE.minor]: describe(RELEASE_TYPE.minor),
161
+ [RELEASE_TYPE.major]: describe(RELEASE_TYPE.major),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Types a configuration in editors; returns it unchanged.
167
+ *
168
+ * @param {CreateVersionConfig} config - Project configuration.
169
+ * @returns {CreateVersionConfig} The same configuration.
170
+ */
171
+ export function defineCreateVersionConfig(config) {
172
+ return config;
173
+ }
174
+
175
+ /**
176
+ * Builds a configuration error that names the file and the invalid field.
177
+ *
178
+ * @param {string} field - Offending field path.
179
+ * @param {string} expectation - What the field must be.
180
+ * @returns {Error} Error to throw.
181
+ */
182
+ function invalidField(field, expectation) {
183
+ return new Error(`astra-rp create-version: ${CREATE_VERSION_CONFIG_FILE}: ${field} must be ${expectation}`);
184
+ }
185
+
186
+ /**
187
+ * Checks that a value is an array of non-empty strings.
188
+ *
189
+ * @param {unknown} value - Candidate.
190
+ * @returns {value is string[]} Whether it is a string list.
191
+ */
192
+ function isStringList(value) {
193
+ // Array.from turns holes into undefined, which every() would otherwise skip.
194
+ return Array.isArray(value) && Array.from(value).every((item) => typeof item === "string" && item.trim().length > 0);
195
+ }
196
+
197
+ /**
198
+ * Resolves the release checks: the configured commands, none when `false` skips them on purpose,
199
+ * or `<package manager> run ci` when the configuration leaves them out and `package.json` declares `ci`.
200
+ *
201
+ * @param {unknown} checks - Raw `checks` field.
202
+ * @param {Record<string, unknown>} packageScripts - `scripts` of the project `package.json`.
203
+ * @param {ProjectCommands} commands - Commands of the project package manager.
204
+ * @returns {string[] | null} Commands to run, or `null` when there is nothing to run.
205
+ * @throws {Error} When `checks` is neither a non-empty list of command lines nor `false`.
206
+ */
207
+ function resolveChecks(checks, packageScripts, commands) {
208
+ if (checks === undefined) {
209
+ return Object.hasOwn(packageScripts, DEFAULT_CHECKS_SCRIPT) ? [commands.runScript(DEFAULT_CHECKS_SCRIPT)] : null;
210
+ }
211
+
212
+ if (checks === false) {
213
+ return [];
214
+ }
215
+
216
+ if (!isStringList(checks) || checks.length === 0) {
217
+ throw invalidField("checks", "a non-empty list of command lines, or false to skip them on purpose");
218
+ }
219
+
220
+ return checks;
221
+ }
222
+
223
+ /**
224
+ * Validates `changelogDraft`: `true` tries every agent CLI in the default order (Claude Code, then
225
+ * Codex), a list chooses which ones and their order, and `false` or no field turns drafting off.
226
+ *
227
+ * @param {unknown} changelogDraft - Raw `changelogDraft` field.
228
+ * @returns {import("./changelog-draft.js").ChangelogDraftProvider[]} Agent CLIs to try, in order; empty when off.
229
+ * @throws {Error} When the field is neither a boolean nor a non-empty list of distinct known agents.
230
+ */
231
+ function resolveChangelogDraft(changelogDraft) {
232
+ if (changelogDraft === undefined || changelogDraft === false) return [];
233
+ if (changelogDraft === true) return [...DEFAULT_CHANGELOG_DRAFT_PROVIDERS];
234
+ const knownProviders = /** @type {readonly unknown[]} */ (Object.values(CHANGELOG_DRAFT_PROVIDER));
235
+ if (
236
+ !Array.isArray(changelogDraft) ||
237
+ changelogDraft.length === 0 ||
238
+ changelogDraft.some((provider) => !knownProviders.includes(provider)) ||
239
+ new Set(changelogDraft).size !== changelogDraft.length
240
+ ) {
241
+ throw invalidField(CHANGELOG_DRAFT_FIELD, `true, false or a non-empty list of distinct agents among ${knownProviders.map((provider) => JSON.stringify(provider)).join(", ")}`);
242
+ }
243
+ return /** @type {import("./changelog-draft.js").ChangelogDraftProvider[]} */ ([...changelogDraft]);
244
+ }
245
+
246
+ /**
247
+ * Validates `releaseTimeZone`, the IANA time zone that dates the closed `[Unreleased]` section, with
248
+ * the same formatter that dates the release, so an unknown zone stops the command before any
249
+ * change instead of failing in the middle of the release commit.
250
+ *
251
+ * @param {unknown} releaseTimeZone - Raw `releaseTimeZone` field.
252
+ * @returns {string | null} The configured zone, or `null` when the release keeps the local day.
253
+ * @throws {Error} When the field is not a string or not a time zone `Intl.DateTimeFormat` supports.
254
+ */
255
+ function resolveReleaseTimeZone(releaseTimeZone) {
256
+ if (releaseTimeZone === undefined) {
257
+ return null;
258
+ }
259
+
260
+ const correctionHint = `usá un nombre como "${RELEASE_TIME_ZONE_EXAMPLE}" (Intl.supportedValuesOf("timeZone") lista las disponibles) o quitá la opción para fechar el release con el día local`;
261
+ if (typeof releaseTimeZone !== "string") {
262
+ throw new Error(`astra-rp create-version: ${CREATE_VERSION_CONFIG_FILE}: ${RELEASE_TIME_ZONE_FIELD} debe ser un string con una zona horaria IANA; ${correctionHint}.`);
263
+ }
264
+
265
+ try {
266
+ formatReleaseDate(new Date(), releaseTimeZone);
267
+ } catch (error) {
268
+ if (!(error instanceof RangeError)) throw error;
269
+ throw new Error(`astra-rp create-version: ${CREATE_VERSION_CONFIG_FILE}: ${RELEASE_TIME_ZONE_FIELD} ${JSON.stringify(releaseTimeZone)} no es una zona horaria IANA válida; ${correctionHint}.`, { cause: error });
270
+ }
271
+
272
+ return releaseTimeZone;
273
+ }
274
+
275
+ /**
276
+ * Validates a raw configuration and fills its defaults.
277
+ *
278
+ * @param {unknown} rawConfig - Default export of `astra-rp.config.js`.
279
+ * @param {ConfigResolutionContext} [resolutionContext] - Project data the defaults depend on.
280
+ * @returns {ResolvedCreateVersionConfig} Configuration ready for the command.
281
+ * @throws {Error} When a field has an unsupported type or value.
282
+ */
283
+ export function resolveCreateVersionConfig(rawConfig, { packageScripts = {}, commands = DEFAULT_PROJECT_COMMANDS } = {}) {
284
+ if (!isRecord(rawConfig)) {
285
+ throw invalidField("the default export", "an object");
286
+ }
287
+
288
+ const config = /** @type {Record<string, unknown>} */ (rawConfig);
289
+ const ci = resolveCiReleaseConfig(config.ci);
290
+ if (config.projectName !== undefined && (typeof config.projectName !== "string" || config.projectName.trim() === "")) {
291
+ throw invalidField("projectName", "a non-empty string");
292
+ }
293
+
294
+ const descriptions = config.releaseTypeDescriptions ?? {};
295
+ if (!isRecord(descriptions)) {
296
+ throw invalidField("releaseTypeDescriptions", "an object keyed by patch, minor and major");
297
+ }
298
+ for (const [releaseType, description] of Object.entries(descriptions)) {
299
+ if (!(/** @type {readonly string[]} */ (RELEASE_TYPE_ORDER)).includes(releaseType) || typeof description !== "string") {
300
+ throw invalidField(`releaseTypeDescriptions.${releaseType}`, "a string for patch, minor or major");
301
+ }
302
+ }
303
+
304
+ const preMajorShift = config.preMajorShift ?? false;
305
+ if (typeof preMajorShift !== "boolean") {
306
+ throw invalidField("preMajorShift", "a boolean");
307
+ }
308
+
309
+ const publish = config.publish ?? null;
310
+ if (publish !== null && !isRegistryProvider(publish) && typeof publish !== "function") {
311
+ throw invalidField("publish", "npm, github, gitlab, jsr, a function or null");
312
+ }
313
+
314
+ const registry = config.registry ?? (isRegistryProvider(publish) ? publish : null);
315
+ if (registry !== null && !(/** @type {readonly unknown[]} */ (Object.values(RELEASE_REGISTRY))).includes(registry)) {
316
+ throw invalidField("registry", `one of ${Object.values(RELEASE_REGISTRY).join(", ")} or null`);
317
+ }
318
+ if (isRegistryProvider(publish) && registry !== null && registry !== publish) {
319
+ throw invalidField("registry", "the same provider as publish: each project publishes to a single registry");
320
+ }
321
+ const publication = resolvePublicationOptions(config.publication, isRegistryProvider(publish) ? publish : isRegistryProvider(registry) ? registry : undefined);
322
+
323
+ const checks = resolveChecks(config.checks, packageScripts, commands);
324
+
325
+ const prepare = config.prepare ?? null;
326
+ if (prepare !== null && typeof prepare !== "function" && !isStringList(prepare)) {
327
+ throw invalidField("prepare", "a list of command lines, a function or null");
328
+ }
329
+
330
+ const migrations = /** @type {MigrationsAdapter | null} */ (config.migrations ?? null);
331
+ if (migrations !== null && (typeof migrations.check !== "function" || typeof migrations.apply !== "function")) {
332
+ throw invalidField("migrations", "an object with check and apply functions, or null");
333
+ }
334
+ if (migrations?.targetHint !== undefined && typeof migrations.targetHint !== "string") {
335
+ throw invalidField("migrations.targetHint", "a string");
336
+ }
337
+
338
+ const artifact = config.artifact ?? null;
339
+ if (artifact !== null) {
340
+ if (typeof artifact !== "string" || !artifact.includes(ARTIFACT_VERSION_PLACEHOLDER)) {
341
+ throw invalidField("artifact", `a path pattern containing ${ARTIFACT_VERSION_PLACEHOLDER}, such as releases/{version}-*/{name}-{version}.tgz`);
342
+ }
343
+ if (!isRegistryProvider(publish) || publish === RELEASE_REGISTRY.jsr) {
344
+ throw invalidField("artifact", `used only with publish: "${NPM_PUBLISHER}"`);
345
+ }
346
+ }
347
+
348
+ const summary = config.summary ?? [];
349
+ if (!isStringList(summary)) {
350
+ throw invalidField("summary", "a list of lines");
351
+ }
352
+
353
+ if (config.publishedLabel !== undefined && typeof config.publishedLabel !== "string") {
354
+ throw invalidField("publishedLabel", "a string");
355
+ }
356
+
357
+ const packages = config.packages ?? null;
358
+ if (packages !== null && packages !== WORKSPACES_PACKAGES && (!isStringList(packages) || packages.length === 0)) {
359
+ throw invalidField("packages", `"${WORKSPACES_PACKAGES}" or a non-empty list of workspace patterns such as packages/*`);
360
+ }
361
+
362
+ const tagFormat = config.tagFormat ?? (packages === null ? null : DEFAULT_MONOREPO_TAG_FORMAT);
363
+ if (tagFormat !== null) {
364
+ if (packages === null) {
365
+ throw invalidField("tagFormat", "used only with packages (the single-package mode tags vX.Y.Z)");
366
+ }
367
+ if (typeof tagFormat !== "string" || !tagFormat.includes(TAG_FORMAT_PLACEHOLDER.version)) {
368
+ throw invalidField("tagFormat", `a string containing ${TAG_FORMAT_PLACEHOLDER.version}, such as ${DEFAULT_MONOREPO_TAG_FORMAT}`);
369
+ }
370
+ }
371
+
372
+ const releaseTimeZone = resolveReleaseTimeZone(config.releaseTimeZone);
373
+ const changelogDraft = resolveChangelogDraft(config[CHANGELOG_DRAFT_FIELD]);
374
+
375
+ const configuredVersionFiles = config.versionFiles ?? [];
376
+ if (!isStringList(configuredVersionFiles) || configuredVersionFiles.some((filePath) => !isPathInsideRoot(filePath))) {
377
+ throw invalidField("versionFiles", "a list of file paths relative to the project root, inside it");
378
+ }
379
+ const versionFiles = configuredVersionFiles.map(toSlashSeparatedPath);
380
+ const builtInVersionFile = versionFiles.find((filePath) => RELEASE_COMMIT_BUILT_IN_FILES.includes(filePath) || filePath.toLowerCase() === CHANGELOG_FILE.toLowerCase());
381
+ if (builtInVersionFile !== undefined) {
382
+ throw invalidField(
383
+ "versionFiles",
384
+ `a list without ${RELEASE_COMMIT_BUILT_IN_FILES.join(" nor ")}, which the release commit owns (it bumps package.json and closes the [Unreleased] section of CHANGELOG.md) (found ${JSON.stringify(builtInVersionFile)})`
385
+ );
386
+ }
387
+
388
+ return {
389
+ projectName: /** @type {string | undefined} */ (config.projectName) ?? null,
390
+ releaseTypeDescriptions: { ...DEFAULT_RELEASE_TYPE_DESCRIPTIONS, .../** @type {Partial<Record<ReleaseType, string>>} */ (descriptions) },
391
+ preMajorShift,
392
+ registry: /** @type {import("./registry-config.js").RegistryProvider | null} */ (registry),
393
+ publication,
394
+ publishedLabel: /** @type {string | undefined} */ (config.publishedLabel) ?? DEFAULT_PUBLISHED_LABEL,
395
+ checks,
396
+ migrations,
397
+ prepare: /** @type {string[] | ReleaseHook | null} */ (prepare),
398
+ publish: /** @type {import("./registry-config.js").RegistryProvider | ReleaseHook | null} */ (publish),
399
+ artifact: /** @type {string | null} */ (artifact),
400
+ summary,
401
+ versionFiles,
402
+ packages: /** @type {"workspaces" | string[] | null} */ (packages),
403
+ tagFormat: /** @type {string | null} */ (tagFormat),
404
+ commands,
405
+ ci,
406
+ releaseTimeZone,
407
+ changelogDraft,
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Tells whether a configured path stays inside the project root: relative, without `..` segments.
413
+ *
414
+ * @param {string} filePath - Configured path.
415
+ * @returns {boolean} Whether it is a relative path that cannot escape the root.
416
+ */
417
+ export function isPathInsideRoot(filePath) {
418
+ // The win32 root covers `/x`, `\x`, `C:\x` and the drive-relative `C:x` alike, on every platform.
419
+ return path.win32.parse(filePath).root === "" && !filePath.split(/[\\/]/u).includes("..");
420
+ }
421
+
422
+ /**
423
+ * Converts a configured path, written with `/` or `\` separators on any platform, to the
424
+ * `/`-separated form without `./` segments nor a trailing `/`: the form the file system, Git and
425
+ * the messages use.
426
+ *
427
+ * @param {string} filePath - Configured path, already known to stay inside the root.
428
+ * @returns {string} Path relative to the root with `/` separators, such as `src/cli.js`.
429
+ */
430
+ function toSlashSeparatedPath(filePath) {
431
+ return path.posix.normalize(filePath.replaceAll("\\", "/")).replace(/\/+$/u, "");
432
+ }
433
+
434
+ /**
435
+ * Imports `astra-rp.config.mjs` or `astra-rp.config.js` from the repository root and validates it.
436
+ * It is imported once per process: after syncing `main` the command stops and asks to run it
437
+ * again, so a new process imports the updated file and everything it imports.
438
+ *
439
+ * @param {string} repositoryRoot - Repository root.
440
+ * @returns {Promise<ResolvedCreateVersionConfig>} Resolved configuration.
441
+ * @throws {Error} When the file is missing, fails to load or is invalid.
442
+ */
443
+ export async function loadCreateVersionConfig(repositoryRoot) {
444
+ const configPath = CREATE_VERSION_CONFIG_FILES.map((fileName) => path.join(repositoryRoot, fileName)).find((candidate) => existsSync(candidate));
445
+
446
+ if (!configPath) {
447
+ throw new Error(
448
+ `astra-rp create-version: ${CREATE_VERSION_CONFIG_FILES.join(" or ")} not found in ${repositoryRoot}; create it with the release checks and publication settings`
449
+ );
450
+ }
451
+
452
+ let module;
453
+ try {
454
+ module = await import(pathToFileURL(configPath).href);
455
+ } catch (error) {
456
+ throw new Error(`astra-rp create-version: could not load ${configPath}`, { cause: error });
457
+ }
458
+
459
+ const manifest = readProjectManifest(repositoryRoot);
460
+ const scripts = manifest?.scripts;
461
+ return resolveCreateVersionConfig(isRecord(module.default)
462
+ ? { ...module.default, ci: module.default.ci ?? module[CI_CONFIG_EXPORT] }
463
+ : module.default, {
464
+ packageScripts: isRecord(scripts) ? scripts : {},
465
+ commands: describeProjectCommands(detectPackageManager(repositoryRoot, manifest ?? undefined)),
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Reads the project `package.json`, whose scripts decide the default release checks and whose
471
+ * `packageManager` decides the commands astra-rp runs and prints.
472
+ *
473
+ * @param {string} repositoryRoot - Repository root.
474
+ * @returns {Record<string, unknown> | null} Manifest, or `null` without one.
475
+ * @throws {Error} When `package.json` exists but cannot be parsed.
476
+ */
477
+ function readProjectManifest(repositoryRoot) {
478
+ const manifestPath = path.join(repositoryRoot, PACKAGE_MANIFEST_FILE);
479
+
480
+ if (!existsSync(manifestPath)) {
481
+ return null;
482
+ }
483
+
484
+ try {
485
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
486
+ } catch (error) {
487
+ throw new Error(`astra-rp create-version: could not read the scripts of ${manifestPath}`, { cause: error });
488
+ }
489
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Errors of `astra-rp create-version`.
3
+ *
4
+ * @module create-version/errors
5
+ */
6
+
7
+ /**
8
+ * Describes any thrown value for a Spanish message: the message of an `Error`, otherwise its text.
9
+ *
10
+ * @param {unknown} error - Thrown value.
11
+ * @returns {string} Message to quote.
12
+ */
13
+ export function describeError(error) {
14
+ return error instanceof Error ? error.message : String(error);
15
+ }
16
+
17
+ /** Failure of a release step or hook, with a Spanish explanation and the next action to take. */
18
+ export class ReleaseStepError extends Error {
19
+ /**
20
+ * @param {string} message - What failed, in Spanish.
21
+ * @param {string} hint - What to do next, in Spanish.
22
+ * @param {{ cause?: unknown }} [options] - Original error, when wrapping one.
23
+ */
24
+ constructor(message, hint, options) {
25
+ super(message, options);
26
+ this.name = "ReleaseStepError";
27
+ this.hint = hint;
28
+ }
29
+ }
30
+
31
+ /** Reports a CI boundary failure while retaining the release identity for a safe retry.
32
+ * @extends ReleaseStepError
33
+ */
34
+ export class CiReleaseError extends ReleaseStepError {
35
+ /**
36
+ * @param {"ci-preflight-failed" | "ci-run-lookup-failed" | "ci-dispatch-unconfirmed"} code - Stable failure category.
37
+ * @param {string} message - Safe Spanish diagnostic.
38
+ * @param {string} hint - Recovery action preserving the existing release.
39
+ * @param {{ cause?: unknown }} [options] - Original dependency failure.
40
+ */
41
+ constructor(code, message, hint, options) {
42
+ super(message, hint, options);
43
+ this.name = "CiReleaseError";
44
+ this.code = code;
45
+ }
46
+ }