@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,157 @@
1
+ /**
2
+ * Decides whether Vercel builds a commit (`vercel.json` → `ignoreCommand`).
3
+ *
4
+ * Only a stable `X.Y.Z` version that is the next patch, minor or major of the
5
+ * previous commit's version ships, the same rule `create-version` applies. An
6
+ * unchanged, lower or skipped version (`1.0.0` → `3.0.0`) and any prerelease
7
+ * or build-metadata version skip the build. When the previous version cannot
8
+ * be read there is nothing to compare, so a stable version builds.
9
+ *
10
+ * A release delegated to CI is skipped only on the commit that changes
11
+ * `.astra-rp/release.json` against its first parent, the same commit scoping the
12
+ * generated `.astra-rp/vercel-ignore-build.mjs` applies (that template stays
13
+ * dependency-free, so it inlines the check). Later commits keep the metadata
14
+ * untouched and fall through to the version rule. Without a readable parent
15
+ * (shallow clone or first commit) the release commit is recognized by its own
16
+ * `Astra-Rp-Execution: ci` trailer, which later commits never carry, so the
17
+ * deployment still waits for the release workflow.
18
+ *
19
+ * @module build-gate
20
+ */
21
+
22
+ import { execFileSync, spawnSync } from "node:child_process";
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import path from "node:path";
25
+
26
+ import { CURRENT_REVISION, GIT_DIFF_CHANGED_STATUS, PACKAGE_MANIFEST_FILE, PREVIOUS_REVISION } from "./constants/build-gate.js";
27
+ import { isStableReleaseVersion, listAllowedVersionsAfter } from "./versions.js";
28
+ import { CI_RELEASE_METADATA_FILE, RELEASE_EXECUTION, RELEASE_EXECUTION_TRAILER } from "./constants/ci-release.js";
29
+
30
+ /**
31
+ * @typedef {{ shouldBuild: boolean, reason: string }} BuildDecision
32
+ */
33
+
34
+ /**
35
+ * Decides whether a commit is built.
36
+ *
37
+ * @param {string | null} previousVersion - Version of the previous commit, or `null` when unreadable.
38
+ * @param {string | null} currentVersion - Version of the commit being deployed, or `null` when unreadable.
39
+ * @returns {BuildDecision} Decision and a log line.
40
+ */
41
+ export function decideBuild(previousVersion, currentVersion) {
42
+ if (currentVersion === null) {
43
+ return { shouldBuild: false, reason: "Current package version could not be read. Skipping build." };
44
+ }
45
+
46
+ if (!isStableReleaseVersion(currentVersion)) {
47
+ return { shouldBuild: false, reason: `Version ${currentVersion} is not a stable X.Y.Z release. Skipping build.` };
48
+ }
49
+
50
+ const allowedVersions = previousVersion === null ? null : listAllowedVersionsAfter(previousVersion);
51
+
52
+ if (allowedVersions === null) {
53
+ return { shouldBuild: true, reason: `Previous package version could not be compared. Building stable ${currentVersion}.` };
54
+ }
55
+
56
+ if (currentVersion === previousVersion) {
57
+ return { shouldBuild: false, reason: "Version did not change. Skipping build." };
58
+ }
59
+
60
+ if (!allowedVersions.includes(currentVersion)) {
61
+ return {
62
+ shouldBuild: false,
63
+ reason: `Version ${previousVersion} -> ${currentVersion} is not the next patch, minor or major (${allowedVersions.join(", ")}). Skipping build.`,
64
+ };
65
+ }
66
+
67
+ return { shouldBuild: true, reason: `Version changed: ${previousVersion} -> ${currentVersion}. Building.` };
68
+ }
69
+
70
+ /**
71
+ * Reads the `version` field of a `package.json` text.
72
+ *
73
+ * @param {() => string} readManifest - Returns the manifest contents.
74
+ * @returns {string | null} Version, or `null` when unreadable.
75
+ */
76
+ function readVersion(readManifest) {
77
+ try {
78
+ const version = JSON.parse(readManifest()).version;
79
+ return typeof version === "string" ? version : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Tells whether the deployed commit carries exactly one `Astra-Rp-Execution: ci`
87
+ * trailer, the commit-local mark `create-version` writes in a release delegated
88
+ * to CI. A Git failure counts as "no trailer".
89
+ *
90
+ * @param {string} repositoryRoot - Checkout whose `HEAD` is being deployed.
91
+ * @returns {boolean} Whether `HEAD` declares a CI release execution.
92
+ */
93
+ function hasCiReleaseTrailer(repositoryRoot) {
94
+ const trailers = spawnSync("git", ["log", "-1", `--format=%(trailers:key=${RELEASE_EXECUTION_TRAILER},valueonly=true)`, CURRENT_REVISION], {
95
+ cwd: repositoryRoot,
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ windowsHide: true,
99
+ });
100
+ if (trailers.status !== 0) return false;
101
+ const recordedValues = [...new Set(trailers.stdout.split("\n").map((value) => value.trim()).filter(Boolean))];
102
+ return recordedValues.length === 1 && recordedValues[0] === RELEASE_EXECUTION.ci;
103
+ }
104
+
105
+ /**
106
+ * Tells whether the deployed commit is the one that wrote the CI release
107
+ * metadata. With a readable first parent it compares the metadata against it;
108
+ * without one (first commit or shallow clone) it relies on the release commit's
109
+ * own `Astra-Rp-Execution: ci` trailer, so the CI gate holds in shallow checkouts.
110
+ *
111
+ * @param {string} repositoryRoot - Checkout whose `HEAD` is being deployed.
112
+ * @returns {boolean} Whether `HEAD` is the CI release commit.
113
+ */
114
+ function isReleaseMetadataCommit(repositoryRoot) {
115
+ const parentLookup = spawnSync("git", ["rev-parse", "--verify", "--quiet", `${PREVIOUS_REVISION}^{commit}`], {
116
+ cwd: repositoryRoot,
117
+ stdio: "ignore",
118
+ windowsHide: true,
119
+ });
120
+ if (parentLookup.status !== 0) return hasCiReleaseTrailer(repositoryRoot);
121
+ const metadataDiff = spawnSync("git", ["diff", "--quiet", PREVIOUS_REVISION, CURRENT_REVISION, "--", CI_RELEASE_METADATA_FILE], {
122
+ cwd: repositoryRoot,
123
+ stdio: "ignore",
124
+ windowsHide: true,
125
+ });
126
+ return metadataDiff.status === GIT_DIFF_CHANGED_STATUS;
127
+ }
128
+
129
+ /**
130
+ * Reads the previous and the current `package.json` versions of a Git
131
+ * checkout and decides whether it is built.
132
+ *
133
+ * @param {string} repositoryRoot - Checkout whose `HEAD` is being deployed.
134
+ * @returns {BuildDecision} Decision and a log line.
135
+ */
136
+ export function decideBuildForCheckout(repositoryRoot) {
137
+ const previousVersion = readVersion(() =>
138
+ execFileSync("git", ["show", `${PREVIOUS_REVISION}:${PACKAGE_MANIFEST_FILE}`], {
139
+ cwd: repositoryRoot,
140
+ encoding: "utf8",
141
+ stdio: ["ignore", "pipe", "ignore"],
142
+ })
143
+ );
144
+ const currentVersion = readVersion(() => readFileSync(path.join(repositoryRoot, PACKAGE_MANIFEST_FILE), "utf8"));
145
+ const metadataPath = path.join(repositoryRoot, CI_RELEASE_METADATA_FILE);
146
+ if (existsSync(metadataPath)) {
147
+ try {
148
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
149
+ const isDelegatedRelease = metadata.version === currentVersion && metadata.execution === RELEASE_EXECUTION.ci;
150
+ if (isDelegatedRelease && isReleaseMetadataCommit(repositoryRoot)) return { shouldBuild: false, reason: `Release ${currentVersion} is delegated to CI. Production deployment waits for its checks. Skipping build.` };
151
+ } catch {
152
+ return { shouldBuild: false, reason: "CI release metadata could not be read. Skipping build." };
153
+ }
154
+ }
155
+
156
+ return decideBuild(previousVersion, currentVersion);
157
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Internal helpers that split `CHANGELOG.md` into release blocks and rewrite the `[Unreleased]`
3
+ * block. Shared by the public `@astra-kit/rp/changelog` module and by `create-version`; not part of the
4
+ * package's public API (no `exports` entry and no reexport from `@astra-kit/rp`).
5
+ *
6
+ * @module changelog-blocks
7
+ */
8
+
9
+ import { CHANGELOG_TITLE, RELEASE_HEADING_PATTERN, UNRELEASED_HEADING, UNRELEASED_LINE_PATTERN } from "./constants/changelog.js";
10
+
11
+ /**
12
+ * @typedef {{ label: string, heading: string, start: number, bodyStart: number, end: number }} ChangelogBlock
13
+ */
14
+
15
+ /**
16
+ * Splits the changelog into release blocks in document order.
17
+ *
18
+ * @param {string} changelog - CHANGELOG.md contents.
19
+ * @returns {ChangelogBlock[]} Blocks.
20
+ */
21
+ export function listBlocks(changelog) {
22
+ const headings = [...changelog.matchAll(RELEASE_HEADING_PATTERN)];
23
+ return headings.map((match, index) => {
24
+ const start = match.index ?? 0;
25
+ return {
26
+ label: match[1],
27
+ heading: match[0],
28
+ start,
29
+ bodyStart: start + match[0].length,
30
+ end: headings[index + 1]?.index ?? changelog.length,
31
+ };
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Replaces the `[Unreleased]` block with `body`, or inserts the block above the newest release.
37
+ *
38
+ * @param {string} changelog - Existing contents, or an empty string for a new file.
39
+ * @param {string} body - New block body, already using `lineBreak`.
40
+ * @param {string} lineBreak - Line break written around the body.
41
+ * @returns {string} Changelog with the new `[Unreleased]` block.
42
+ */
43
+ export function replaceUnreleasedBody(changelog, body, lineBreak) {
44
+ const replacement = `${UNRELEASED_HEADING}${lineBreak}${lineBreak}${body}${lineBreak}${lineBreak}`;
45
+ const blocks = listBlocks(changelog);
46
+ const unreleased = blocks.find((block) => UNRELEASED_LINE_PATTERN.test(block.heading));
47
+ if (unreleased) {
48
+ return `${changelog.slice(0, unreleased.start)}${replacement}${changelog.slice(unreleased.end)}`;
49
+ }
50
+ const insertion = blocks[0]?.start ?? changelog.length;
51
+ const prefix = changelog.slice(0, insertion).trimEnd() || CHANGELOG_TITLE;
52
+ return `${prefix}${lineBreak}${lineBreak}${replacement}${changelog.slice(insertion)}`;
53
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Generates CHANGELOG.md entries from commits and releases `[Unreleased]`.
3
+ *
4
+ * A release lists the hash and original subject of every commit since the
5
+ * previous version, then moves the block under `## [X.Y.Z] - YYYY-MM-DD`.
6
+ * Published history, including legacy unbracketed headings, is preserved.
7
+ *
8
+ * `astra-rp create-version` does not generate entries from commits: it requires entries under
9
+ * `[Unreleased]`, written by hand or, with `changelogDraft`, drafted by an agent CLI, and in the
10
+ * release commit only renames that heading to `## [X.Y.Z] - YYYY-MM-DD` and adds an empty
11
+ * `[Unreleased]` on top, keeping any section names.
12
+ *
13
+ * @module changelog
14
+ */
15
+
16
+ import { listBlocks, replaceUnreleasedBody } from "./changelog-blocks.js";
17
+ import {
18
+ CHANGE_TYPES,
19
+ ENTRY_LINE_PATTERN,
20
+ LEADING_WHITESPACE_PATTERN,
21
+ LINE_BREAK_PATTERN,
22
+ SECTION_HEADING_PATTERN,
23
+ COMMIT_SHORT_SHA_LENGTH,
24
+ LF_LINE_BREAK,
25
+ UNRELEASED_HEADING,
26
+ UNRELEASED_LINE_PATTERN,
27
+ } from "./constants/changelog.js";
28
+
29
+ /**
30
+ * Replaces `[Unreleased]` with the exact commit list for the next version.
31
+ * Creates the block or document when missing and preserves released history.
32
+ *
33
+ * @param {string} changelog - Existing contents, or an empty string for a new file.
34
+ * @param {{ sha: string, subject: string }[]} commits - Commits since the previous release, newest first.
35
+ * @returns {string} Changelog with one hash and original subject per commit.
36
+ * @throws {Error} When there are no commits to release.
37
+ */
38
+ export function fillUnreleasedFromCommits(changelog, commits) {
39
+ if (commits.length === 0) throw new Error("create-version: no commits available to generate CHANGELOG.md");
40
+ const body = commits.map((commit) => `- ${commit.sha.slice(0, COMMIT_SHORT_SHA_LENGTH)} ${commit.subject}`).join(LF_LINE_BREAK);
41
+ return replaceUnreleasedBody(changelog, body, LF_LINE_BREAK);
42
+ }
43
+
44
+ /**
45
+ * @typedef {import("./changelog-blocks.js").ChangelogBlock} ChangelogBlock
46
+ * @typedef {{ exists: boolean, entryCount: number, unknownSections: string[], body: string }} UnreleasedState
47
+ */
48
+
49
+ /**
50
+ * Counts the change entries of a block body.
51
+ *
52
+ * @param {string} body - Block text below its heading.
53
+ * @returns {number} Number of `- ` lines.
54
+ */
55
+ function countEntries(body) {
56
+ return body.split(LINE_BREAK_PATTERN).filter((line) => ENTRY_LINE_PATTERN.test(line)).length;
57
+ }
58
+
59
+ /**
60
+ * Describes the `## [Unreleased]` block.
61
+ *
62
+ * @param {string} changelog - CHANGELOG.md contents.
63
+ * @returns {UnreleasedState} Unreleased state.
64
+ */
65
+ export function readUnreleased(changelog) {
66
+ const block = listBlocks(changelog).find((candidate) => UNRELEASED_LINE_PATTERN.test(candidate.heading));
67
+ if (!block) return { exists: false, entryCount: 0, unknownSections: [], body: "" };
68
+ const body = changelog.slice(block.bodyStart, block.end);
69
+ const unknownSections = [...body.matchAll(SECTION_HEADING_PATTERN)]
70
+ .map((match) => match[1])
71
+ .filter((name) => !CHANGE_TYPES.includes(name));
72
+ return { exists: true, entryCount: countEntries(body), unknownSections, body: body.trim() };
73
+ }
74
+
75
+ /**
76
+ * Returns the newest released block (the first one that is not `[Unreleased]`).
77
+ *
78
+ * @param {string} changelog - CHANGELOG.md contents.
79
+ * @returns {{ version: string, entryCount: number } | null} Latest release entry.
80
+ */
81
+ export function readLatestRelease(changelog) {
82
+ const block = listBlocks(changelog).find((candidate) => !UNRELEASED_LINE_PATTERN.test(candidate.heading));
83
+ return block ? { version: block.label, entryCount: countEntries(changelog.slice(block.bodyStart, block.end)) } : null;
84
+ }
85
+
86
+ /**
87
+ * Moves the `[Unreleased]` changes under a new version heading and leaves an empty `[Unreleased]` block.
88
+ *
89
+ * @param {string} changelog - CHANGELOG.md contents.
90
+ * @param {string} version - Version being released.
91
+ * @param {string} releaseDate - Date in `YYYY-MM-DD` format.
92
+ * @returns {string} Released changelog.
93
+ * @throws {Error} When `[Unreleased]` is missing, empty or uses an unknown section.
94
+ */
95
+ export function releaseUnreleased(changelog, version, releaseDate) {
96
+ const unreleased = readUnreleased(changelog);
97
+ if (!unreleased.exists) throw new Error(`create-version: CHANGELOG.md needs a "${UNRELEASED_HEADING}" block`);
98
+ if (unreleased.unknownSections.length > 0) {
99
+ throw new Error(
100
+ `create-version: CHANGELOG.md [Unreleased] uses unknown sections (${unreleased.unknownSections.join(", ")}); use ${CHANGE_TYPES.join(", ")}`
101
+ );
102
+ }
103
+ if (unreleased.entryCount === 0) throw new Error("create-version: CHANGELOG.md [Unreleased] has no changes to release");
104
+ const block = listBlocks(changelog).find((candidate) => UNRELEASED_LINE_PATTERN.test(candidate.heading));
105
+ if (!block) throw new Error(`create-version: CHANGELOG.md needs a "${UNRELEASED_HEADING}" block`);
106
+ const released = `${UNRELEASED_HEADING}\n\n## [${version}] - ${releaseDate}\n\n${unreleased.body}\n\n`;
107
+ return `${changelog.slice(0, block.start)}${released}${changelog.slice(block.end).replace(LEADING_WHITESPACE_PATTERN, "")}`;
108
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Contract between the `astra-rp ignore-build` command and the Vercel
3
+ * `ignoreCommand` wrapper that reads its output.
4
+ *
5
+ * @module constants/build-gate
6
+ */
7
+
8
+ /** Decision printed as the last output line of `astra-rp ignore-build`. */
9
+ export const BUILD_DECISION = Object.freeze({
10
+ build: "BUILD",
11
+ skip: "SKIP",
12
+ });
13
+
14
+ /** Git revision whose `package.json` holds the previous version. */
15
+ export const PREVIOUS_REVISION = "HEAD^";
16
+
17
+ /** Git revision being deployed. */
18
+ export const CURRENT_REVISION = "HEAD";
19
+
20
+ /** `git diff --quiet` status reporting that the compared path changed between both revisions. */
21
+ export const GIT_DIFF_CHANGED_STATUS = 1;
22
+
23
+ /** Manifest that holds the version. */
24
+ export const PACKAGE_MANIFEST_FILE = "package.json";
25
+
26
+ /** Exit code of `astra-rp ignore-build` when a decision was printed. */
27
+ export const DECISION_EXIT_CODE = 0;
28
+
29
+ /** Exit code of `astra-rp ignore-build` when it could not decide; wrappers must skip the build. */
30
+ export const GATE_FAILURE_EXIT_CODE = 2;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Contract of the `[Unreleased]` entries `create-version` drafts with an agent CLI (Claude Code or
3
+ * Codex) when `changelogDraft` is configured and the release would stop for a missing changelog.
4
+ *
5
+ * @module constants/changelog-draft
6
+ */
7
+
8
+ /** Configuration field that turns on the drafting and orders the agent CLIs it tries. */
9
+ export const CHANGELOG_DRAFT_FIELD = "changelogDraft";
10
+
11
+ /** Agent CLIs that can draft the entries. */
12
+ export const CHANGELOG_DRAFT_PROVIDER = Object.freeze({ claude: "claude", codex: "codex" });
13
+
14
+ /** Order tried with `changelogDraft: true`: Claude Code first, Codex when Claude Code fails or has no quota left. */
15
+ export const DEFAULT_CHANGELOG_DRAFT_PROVIDERS = Object.freeze([CHANGELOG_DRAFT_PROVIDER.claude, CHANGELOG_DRAFT_PROVIDER.codex]);
16
+
17
+ /** Names printed for each agent CLI. */
18
+ export const CHANGELOG_DRAFT_PROVIDER_LABELS = Object.freeze({ [CHANGELOG_DRAFT_PROVIDER.claude]: "Claude Code", [CHANGELOG_DRAFT_PROVIDER.codex]: "Codex" });
19
+
20
+ /** Executable of each agent CLI, resolved from `PATH`. */
21
+ export const CHANGELOG_DRAFT_EXECUTABLES = Object.freeze({ [CHANGELOG_DRAFT_PROVIDER.claude]: "claude", [CHANGELOG_DRAFT_PROVIDER.codex]: "codex" });
22
+
23
+ /**
24
+ * Prefix of the isolated workspace the agents run in: a temporary directory outside the repository,
25
+ * removed when the drafting ends.
26
+ */
27
+ export const CHANGELOG_DRAFT_DIRECTORY_PREFIX = "astra-rp-changelog-draft-";
28
+
29
+ /**
30
+ * Directory of the workspace with the files Git tracks at `HEAD` (from `git checkout-index`): no `.git`,
31
+ * no untracked or ignored files, so local credentials such as an untracked `.env` or `.npmrc` stay
32
+ * out of the agents' reach.
33
+ */
34
+ export const CHANGELOG_DRAFT_SNAPSHOT_DIRECTORY = "repository";
35
+
36
+ /** Directory of the workspace with the release context astra-rp writes for the agents. */
37
+ export const CHANGELOG_DRAFT_CONTEXT_DIRECTORY = "release-context";
38
+
39
+ /** Release context file with the diff of the release range, without the changelog itself. */
40
+ export const CHANGELOG_DRAFT_DIFF_FILE = "changes.diff";
41
+
42
+ /** Release context file with the `git log --stat` of the release range. */
43
+ export const CHANGELOG_DRAFT_LOG_FILE = "commits.log";
44
+
45
+ /** `rmSync` retries when removing the workspace, while Windows releases the files of a stopped agent. */
46
+ export const WORKSPACE_REMOVAL_RETRIES = Object.freeze({ maxRetries: 10, retryDelay: 100 });
47
+
48
+ /**
49
+ * Settings file, at the workspace root, that Claude Code loads with `--settings`: its file tools
50
+ * refuse reads outside the working directory in every permission mode, even when the user's own
51
+ * settings allow them.
52
+ */
53
+ export const CLAUDE_DRAFT_SETTINGS_FILE = "claude-settings.json";
54
+
55
+ /** Contents of {@link CLAUDE_DRAFT_SETTINGS_FILE}. */
56
+ export const CLAUDE_DRAFT_SETTINGS = Object.freeze({ permissions: Object.freeze({ blockReadsOutsideWorkingDirectories: true }) });
57
+
58
+ /**
59
+ * Claude Code in print mode, inside the isolated workspace, with only its file tools (`Read`,
60
+ * `Grep`, `Glob`): no Bash, so no command can write files, and `--strict-mcp-config` without
61
+ * `--mcp-config` loads no MCP server. Reads inside the working directory need no approval; reads
62
+ * outside it need one, which `dontAsk` denies instead of asking (Claude Code documentation,
63
+ * "Configure permissions"), and {@link CLAUDE_DRAFT_SETTINGS_FILE} also refuses them.
64
+ * `--restricted` (Claude Code v2.1.248 or later) loads only managed settings and `--settings`, not
65
+ * the user's or the project's, so an inherited `permissions.additionalDirectories` cannot widen the
66
+ * reads, and confines the file tools to the working directories (Claude Code documentation, "CLI
67
+ * reference", https://code.claude.com/docs/en/cli-reference). `--no-session-persistence` keeps the
68
+ * run, with its prompt and the repository excerpts it read, off the disk and not resumable, as
69
+ * `--ephemeral` does for Codex. Every argument is fixed, so a Windows `.cmd` shim receives no
70
+ * variable path on its command line. The answer comes back as JSON.
71
+ */
72
+ export const CLAUDE_DRAFT_ARGUMENTS = Object.freeze([
73
+ "-p",
74
+ "--restricted",
75
+ "--no-session-persistence",
76
+ "--output-format",
77
+ "json",
78
+ "--permission-mode",
79
+ "dontAsk",
80
+ "--tools",
81
+ "Read,Grep,Glob",
82
+ "--strict-mcp-config",
83
+ "--settings",
84
+ CLAUDE_DRAFT_SETTINGS_FILE,
85
+ ]);
86
+
87
+ /** `--output-format json` result that reports a successful answer. */
88
+ export const CLAUDE_SUCCESS_SUBTYPE = "success";
89
+
90
+ /** File, at the workspace root, where Codex writes its last message (relative to its working directory). */
91
+ export const CODEX_LAST_MESSAGE_FILE = "last-message.md";
92
+
93
+ /**
94
+ * Codex in non-interactive mode inside the isolated workspace (its working directory, which is not
95
+ * a Git repository), sandboxed read-only, without saving the session. Its last message goes to
96
+ * {@link CODEX_LAST_MESSAGE_FILE} and the prompt comes from stdin (`-`). Every argument is fixed,
97
+ * so a Windows `.cmd` shim receives no variable path on its command line.
98
+ *
99
+ * Residual risk: Codex 0.162 documents no option that limits reads to one directory or turns off
100
+ * its shell tool, and `read-only` blocks writes but not reads. The workspace keeps the repository's
101
+ * untracked credentials away, yet Codex could still read other files of the host.
102
+ */
103
+ export const CODEX_DRAFT_ARGUMENTS = Object.freeze([
104
+ "exec",
105
+ "--sandbox",
106
+ "read-only",
107
+ "--skip-git-repo-check",
108
+ "--ephemeral",
109
+ "--color",
110
+ "never",
111
+ "--output-last-message",
112
+ CODEX_LAST_MESSAGE_FILE,
113
+ "-",
114
+ ]);
115
+
116
+ /** Time an agent CLI has to explore the repository and answer before the next one is tried. */
117
+ export const CHANGELOG_DRAFT_TIMEOUT_MS = 600_000;
118
+
119
+ /** Characters of an agent failure shown to the user. */
120
+ export const CHANGELOG_DRAFT_MAX_FAILURE_CHARACTERS = 300;
121
+
122
+ /** Markdown code fence an agent may wrap its answer in. */
123
+ export const MARKDOWN_FENCE_PATTERN = /^```[^\n]*\n([\s\S]*?)\n```$/u;
124
+
125
+ /**
126
+ * A C0 or C1 control character other than a line break or a tab, such as the ESC that starts a
127
+ * terminal escape sequence (OSC, CSI): an agent answer holding one is rejected before it is shown
128
+ * or written.
129
+ */
130
+ // oxlint-disable-next-line no-control-regex -- matching control characters is the purpose of this pattern.
131
+ export const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/u;
132
+
133
+ /** Every character {@link CONTROL_CHARACTER_PATTERN} matches, to remove them from agent text shown in the terminal. */
134
+ export const CONTROL_CHARACTERS_PATTERN = new RegExp(CONTROL_CHARACTER_PATTERN.source, "gu");
135
+
136
+ /**
137
+ * A line a draft may hold: blank, a `### Section` heading or a complete `- ` entry. Each change is a
138
+ * single `- ` line, so an indented continuation of an entry is rejected.
139
+ */
140
+ export const CHANGELOG_DRAFT_LINE_PATTERN = /^(?:\s*|### +\S.*|\s*- +\S.*)$/u;
141
+
142
+ /** An indented line that is not an entry: an entry split across several lines, which a draft must not hold. */
143
+ export const CHANGELOG_DRAFT_CONTINUATION_LINE_PATTERN = /^\s+\S/u;
144
+
145
+ /** A level-2 heading such as `## [Unreleased]` or `## [1.2.0] - 2026-01-01`, which the draft must not contain. */
146
+ export const RELEASE_LEVEL_HEADING_PATTERN = /^##(?!#)/mu;
147
+
148
+ /** A single `### Section` line of a draft, capturing the section name. */
149
+ export const CHANGELOG_DRAFT_SECTION_PATTERN = /^### +(.+?)\s*$/u;
150
+
151
+ /** A blank line of a draft. */
152
+ export const BLANK_LINE_PATTERN = /^\s*$/u;
153
+
154
+ /** Windows command that resolves an executable on `PATH`, including the `.cmd` shims npm installs. */
155
+ export const WINDOWS_EXECUTABLE_LOOKUP_COMMAND = "where";
156
+
157
+ /** Files Windows can run; `where` also lists the extensionless shell script next to an npm `.cmd` shim. */
158
+ export const WINDOWS_RUNNABLE_FILE_PATTERN = /\.(?:exe|com|cmd|bat)$/iu;
159
+
160
+ /** Windows scripts that only a shell can run, such as the `claude.cmd` shim npm installs. */
161
+ export const WINDOWS_SHELL_SCRIPT_PATTERN = /\.(?:cmd|bat)$/iu;
162
+
163
+ /**
164
+ * Characters `cmd.exe` would interpret inside a double-quoted argument: `"` ends the quotes, `%`
165
+ * and `!` (with delayed expansion) expand variables, and `^&|<>` are its operators. The shim path
166
+ * and every agent argument on a Windows shell command line are checked against it.
167
+ */
168
+ export const UNSAFE_SHELL_ARGUMENT_PATTERN = /["%!^&|<>]/u;
169
+
170
+ /**
171
+ * Temporary Git index (`GIT_INDEX_FILE`), inside the workspace, that checks out the snapshot
172
+ * without touching the repository index; removed once the snapshot is written.
173
+ */
174
+ export const CHANGELOG_DRAFT_INDEX_FILE = "snapshot.index";
175
+
176
+ /**
177
+ * Release context file with the working tree changelog the agents will update, so they see it
178
+ * even when it is untracked or only changed locally.
179
+ */
180
+ export const CHANGELOG_DRAFT_CURRENT_CHANGELOG_FILE = "CHANGELOG.md";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Keep a Changelog contract used to read and release `CHANGELOG.md`.
3
+ *
4
+ * @module constants/changelog
5
+ */
6
+
7
+ /** Changelog file released together with `package.json`. */
8
+ export const CHANGELOG_FILE = "CHANGELOG.md";
9
+
10
+ /** Blocking release condition that exits with failure until the manual changelog is updated and its `[Unreleased]` block has entries. */
11
+ export const CHANGELOG_UPDATE_REQUIRED_CODE = "changelog-update-required";
12
+
13
+ /** Default title when creating a changelog document. */
14
+ export const CHANGELOG_TITLE = "# Changelog";
15
+
16
+ /** Length of commit hashes displayed in changelog entries. */
17
+ export const COMMIT_SHORT_SHA_LENGTH = 7;
18
+
19
+ /** Heading of the block that collects changes not yet released. */
20
+ export const UNRELEASED_HEADING = "## [Unreleased]";
21
+
22
+ /** Change types allowed as `###` sections, in Keep a Changelog order. */
23
+ export const CHANGE_TYPES = Object.freeze(["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]);
24
+
25
+ /** Any release-level heading: `## [Unreleased]`, `## [0.7.0] - 2026-09-26` or `## 0.6.0 - 2026-09-23`. */
26
+ export const RELEASE_HEADING_PATTERN = /^## +\[?([^\]\s]+)\]?[^\n]*$/gmu;
27
+
28
+ /** Unreleased heading, case-insensitive, as its own line. */
29
+ export const UNRELEASED_LINE_PATTERN = /^## +\[Unreleased\][ \t]*$/imu;
30
+
31
+ /** A change entry line. */
32
+ export const ENTRY_LINE_PATTERN = /^\s*- +\S/mu;
33
+
34
+ /** A change-type section heading inside a release block. */
35
+ export const SECTION_HEADING_PATTERN = /^### +(.+?)\s*$/gmu;
36
+
37
+ /** Leading whitespace left before the next block after moving `[Unreleased]`. */
38
+ export const LEADING_WHITESPACE_PATTERN = /^\s+/u;
39
+
40
+ /** Line break in a changelog written on any platform. */
41
+ export const LINE_BREAK_PATTERN = /\r?\n/u;
42
+
43
+ /** Line break of a changelog written on Windows, kept when closing `[Unreleased]`. */
44
+ export const CRLF_LINE_BREAK = "\r\n";
45
+
46
+ /** Line break of a changelog written on any other platform. */
47
+ export const LF_LINE_BREAK = "\n";
48
+
49
+ /** Separator between the year, month and day of a release date (`YYYY-MM-DD`). */
50
+ export const RELEASE_DATE_SEPARATOR = "-";
51
+
52
+ /** Digits of the month and the day of a release date. */
53
+ export const RELEASE_DATE_PART_LENGTH = 2;
54
+
55
+ /** Padding of a one-digit month or day in a release date. */
56
+ export const RELEASE_DATE_PAD = "0";
57
+
58
+ /**
59
+ * Locale whose `Intl.DateTimeFormat` parts are Gregorian with Latin digits, to read the year, month
60
+ * and day of a release in its configured time zone.
61
+ */
62
+ export const RELEASE_DATE_LOCALE = "en-US";
63
+
64
+ /** `Intl.DateTimeFormat` fields of a `YYYY-MM-DD` release date. */
65
+ export const RELEASE_DATE_FORMAT_OPTIONS = Object.freeze({ year: "numeric", month: "2-digit", day: "2-digit" });
66
+
67
+ /** `Intl.DateTimeFormat` part types of a release date, in `YYYY-MM-DD` order. */
68
+ export const RELEASE_DATE_PART_ORDER = Object.freeze(["year", "month", "day"]);
69
+
70
+ /** Configuration field naming the IANA time zone that dates the closed `[Unreleased]` section. */
71
+ export const RELEASE_TIME_ZONE_FIELD = "releaseTimeZone";
72
+
73
+ /** IANA time zone suggested by the messages about `releaseTimeZone`. */
74
+ export const RELEASE_TIME_ZONE_EXAMPLE = "America/Argentina/Buenos_Aires";