@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,54 @@
1
+ /**
2
+ * Validates repository-owned CI release settings.
3
+ * @module create-version/ci-config
4
+ */
5
+
6
+ import { CI_ENVIRONMENT_NAME_PATTERN, CI_RESERVED_ENVIRONMENT_NAMES, CI_VERCEL_DEPLOYMENT, CI_WORKFLOW_DIRECTORY, CI_WORKFLOW_NAME_PATTERN } from "../constants/ci-release.js";
7
+
8
+ /**
9
+ * @typedef {{ workflow: string, secrets?: string[], variables?: string[], deployment?: "vercel" | null }} CiReleaseConfig
10
+ * @typedef {{ workflow: string, secrets: string[], variables: string[], deployment: "vercel" | null }} ResolvedCiReleaseConfig
11
+ */
12
+
13
+ /**
14
+ * Builds the root-relative path of a workflow file.
15
+ * @param {string} workflow - Validated workflow filename.
16
+ * @returns {string} Path under {@link CI_WORKFLOW_DIRECTORY}.
17
+ */
18
+ export function ciWorkflowPath(workflow) {
19
+ return `${CI_WORKFLOW_DIRECTORY}/${workflow}`;
20
+ }
21
+
22
+ /**
23
+ * Resolves a workflow basename and explicit environment bindings.
24
+ * @param {unknown} raw - Repository-owned configuration, or null to disable CI releases.
25
+ * @returns {ResolvedCiReleaseConfig | null} Validated configuration.
26
+ * @throws {Error} When the workflow path or environment names are invalid.
27
+ */
28
+ export function resolveCiReleaseConfig(raw) {
29
+ if (raw === undefined || raw === null) return null;
30
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("astra-rp create-version: ci debe ser un objeto con workflow.");
31
+ const config = /** @type {Record<string, unknown>} */ (raw);
32
+ const workflow = typeof config.workflow === "string" ? config.workflow.replace(`${CI_WORKFLOW_DIRECTORY}/`, "") : "";
33
+ if (!CI_WORKFLOW_NAME_PATTERN.test(workflow)) throw new Error("astra-rp create-version: ci.workflow debe ser un nombre de archivo .yml o .yaml dentro de .github/workflows.");
34
+ const secrets = resolveEnvironmentNames(config.secrets, "secrets");
35
+ const variables = resolveEnvironmentNames(config.variables, "variables");
36
+ if (secrets.some((name) => variables.includes(name))) throw new Error("astra-rp create-version: ci.secrets y ci.variables no pueden definir la misma variable.");
37
+ if (config.deployment !== undefined && config.deployment !== null && config.deployment !== CI_VERCEL_DEPLOYMENT) throw new Error("astra-rp create-version: ci.deployment debe ser vercel o null.");
38
+ return { workflow, secrets, variables, deployment: config.deployment === CI_VERCEL_DEPLOYMENT ? CI_VERCEL_DEPLOYMENT : null };
39
+ }
40
+
41
+ /**
42
+ * Validates names before they become YAML environment bindings.
43
+ * @param {unknown} value - Explicit secret or variable names.
44
+ * @param {string} field - Configuration field for the diagnostic.
45
+ * @returns {string[]} Unique environment names.
46
+ * @throws {Error} When a list contains an unsafe or repeated name.
47
+ */
48
+ function resolveEnvironmentNames(value, field) {
49
+ if (value === undefined) return [];
50
+ if (!Array.isArray(value) || Array.from(value).some((name) => typeof name !== "string" || !CI_ENVIRONMENT_NAME_PATTERN.test(name) || CI_RESERVED_ENVIRONMENT_NAMES.includes(name)) || new Set(value).size !== value.length) {
51
+ throw new Error(`astra-rp create-version: ci.${field} debe ser una lista de nombres de variables únicos (por ejemplo DATABASE_MIGRATION_URL).`);
52
+ }
53
+ return /** @type {string[]} */ (value);
54
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Reads the worker's own GitHub Actions history to prove that a custom publisher already finished,
3
+ * so neither a retry that only needs to deploy nor a duplicate dispatch queued behind the release's
4
+ * concurrency group runs a non-idempotent publication again.
5
+ * A registry publication is proven by the registry itself; a custom `publish` function leaves no
6
+ * such record, and the successful release step of an earlier run or attempt is the durable one.
7
+ * @module create-version/ci-publication-history
8
+ */
9
+
10
+ import {
11
+ CI_ACTIONS_HISTORY_ENVIRONMENT, CI_ACTIONS_HISTORY_PAGE_SIZE, CI_ACTIONS_HISTORY_TIMEOUT_MS, CI_DEFAULT_GITHUB_API_URL,
12
+ CI_GITHUB_API_VERSION, CI_RELEASE_WORKER_STEP_NAME, CI_STEP_SUCCESS_CONCLUSION,
13
+ } from "../constants/ci-release.js";
14
+ import { CREATE_VERSION_FLAG } from "../constants/create-version.js";
15
+ import { ReleaseStepError } from "./errors.js";
16
+ import { formatCiReleaseRunTitle } from "./github-workflow.js";
17
+
18
+ /**
19
+ * @typedef {{ id: number, display_title: string, run_attempt: number, html_url: string }} ActionsWorkflowRun
20
+ * @typedef {{ steps?: { name: string, conclusion: string | null }[] }} ActionsJob
21
+ * @typedef {{ url: string, attempt: number }} CompletedCiPublication
22
+ */
23
+
24
+ /**
25
+ * Tells whether the worker must prove from its Actions history that the publication already
26
+ * finished: a custom publisher leaves no registry record, and both a retry after a failed deployment
27
+ * and two concurrent `--retry-ci` dispatches (queued, not cancelled, by the workflow's concurrency
28
+ * group) reach the worker again for the same release, with or without a deployment.
29
+ * @param {import("./config.js").ResolvedCreateVersionConfig} config - Project configuration.
30
+ * @returns {boolean} True when `publish` is a function.
31
+ */
32
+ export function requiresCiPublicationHistory(config) {
33
+ return typeof config.publish === "function";
34
+ }
35
+
36
+ /**
37
+ * Builds the recovery hint shared by every history failure: nothing was published.
38
+ * @param {string} workflow - Configured workflow filename.
39
+ * @returns {string} Actionable hint.
40
+ */
41
+ function describeHistoryRecovery(workflow) {
42
+ return `Dale al job de ${workflow} permissions "actions: read" y GITHUB_TOKEN: \${{ github.token }} (regeneralo con --${CREATE_VERSION_FLAG.setupCi} después de borrarlo en un commit propio) y reintentá con --${CREATE_VERSION_FLAG.retryCi}; el publisher personalizado no se ejecutó.`;
43
+ }
44
+
45
+ /**
46
+ * Requests one Actions REST resource of the worker's repository.
47
+ * @param {{ apiUrl: string, repository: string, token: string, workflow: string }} connection - Validated Actions access.
48
+ * @param {string} resourcePath - Path below `repos/{owner}/{repo}/actions/`, with its query.
49
+ * @returns {Promise<any>} Parsed JSON body.
50
+ * @throws {ReleaseStepError} When the request fails, times out or answers a non-success status.
51
+ */
52
+ async function requestActionsResource(connection, resourcePath) {
53
+ const requestPath = `/repos/${connection.repository}/actions/${resourcePath}`;
54
+ let response;
55
+ try {
56
+ response = await fetch(`${connection.apiUrl}${requestPath}`, {
57
+ headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${connection.token}`, "X-GitHub-Api-Version": CI_GITHUB_API_VERSION },
58
+ signal: AbortSignal.timeout(CI_ACTIONS_HISTORY_TIMEOUT_MS),
59
+ });
60
+ } catch (error) {
61
+ throw new ReleaseStepError(`ci-publication-history:requestActionsResource failed: no se pudo consultar GET ${requestPath} para saber si el publisher personalizado ya terminó en otra ejecución.`, describeHistoryRecovery(connection.workflow), { cause: error });
62
+ }
63
+ if (!response.ok) {
64
+ throw new ReleaseStepError(`ci-publication-history:requestActionsResource failed: GET ${requestPath} respondió ${response.status} (x-github-request-id ${response.headers.get("x-github-request-id") ?? "ausente"}); no se puede saber si el publisher personalizado ya terminó en otra ejecución.`, describeHistoryRecovery(connection.workflow));
65
+ }
66
+ return response.json();
67
+ }
68
+
69
+ /**
70
+ * Tells whether a run attempt is the one executing this worker. GitHub lists the running attempt too;
71
+ * its release step has not succeeded yet, but it is skipped explicitly so only other attempts count.
72
+ * @param {ActionsWorkflowRun} run - Listed workflow run.
73
+ * @param {number} attempt - Attempt number of that run.
74
+ * @param {NodeJS.ProcessEnv} environment - Worker environment providing the current run id and attempt.
75
+ * @returns {boolean} True for the current run attempt.
76
+ */
77
+ function isCurrentRunAttempt(run, attempt, environment) {
78
+ return String(run.id) === environment[CI_ACTIONS_HISTORY_ENVIRONMENT.runId] && String(attempt) === environment[CI_ACTIONS_HISTORY_ENVIRONMENT.runAttempt];
79
+ }
80
+
81
+ /**
82
+ * Finds another run or attempt of this exact release whose release step succeeded, which means its
83
+ * custom publisher already finished. Only successful release steps count: a duplicate dispatch queued
84
+ * behind the release's concurrency group starts after the first run ends, so it sees that success.
85
+ * The current run attempt (`GITHUB_RUN_ID`/`GITHUB_RUN_ATTEMPT`) never counts.
86
+ * Fails closed: when the history cannot be read, the worker stops before publishing.
87
+ * @param {string} workflow - Configured workflow filename that dispatches the release.
88
+ * @param {import("./github-workflow.js").CiReleaseIdentity} release - Verified release identity.
89
+ * @param {NodeJS.ProcessEnv} [environment] - Worker environment providing the Actions API, repository and token.
90
+ * @returns {Promise<CompletedCiPublication | null>} Another successful run attempt, or null when publication never finished.
91
+ * @throws {ReleaseStepError} When the Actions history is unavailable or unreadable.
92
+ */
93
+ export async function findCompletedCiPublication(workflow, release, environment = process.env) {
94
+ const repository = environment[CI_ACTIONS_HISTORY_ENVIRONMENT.repository];
95
+ const token = environment[CI_ACTIONS_HISTORY_ENVIRONMENT.token];
96
+ if (!repository || !token) {
97
+ const missing = [CI_ACTIONS_HISTORY_ENVIRONMENT.repository, CI_ACTIONS_HISTORY_ENVIRONMENT.token].filter((name) => !environment[name]);
98
+ throw new ReleaseStepError(`ci-publication-history:findCompletedCiPublication failed: faltan ${missing.join(" y ")} para comprobar en Actions si el publisher personalizado de ${release.tag} ya terminó en otra ejecución.`, describeHistoryRecovery(workflow));
99
+ }
100
+ const connection = { apiUrl: (environment[CI_ACTIONS_HISTORY_ENVIRONMENT.apiUrl] || CI_DEFAULT_GITHUB_API_URL).replace(/\/+$/u, ""), repository, token, workflow };
101
+ const runTitle = formatCiReleaseRunTitle(release);
102
+ const listed = await requestActionsResource(connection, `workflows/${encodeURIComponent(workflow)}/runs?event=workflow_dispatch&head_sha=${release.sha}&per_page=${CI_ACTIONS_HISTORY_PAGE_SIZE}`);
103
+ const releaseRuns = /** @type {ActionsWorkflowRun[]} */ (listed.workflow_runs ?? []).filter((run) => run.display_title === runTitle);
104
+ for (const run of releaseRuns) {
105
+ for (let attempt = 1; attempt <= run.run_attempt; attempt += 1) {
106
+ if (isCurrentRunAttempt(run, attempt, environment)) continue;
107
+ const attemptJobs = await requestActionsResource(connection, `runs/${run.id}/attempts/${attempt}/jobs?per_page=${CI_ACTIONS_HISTORY_PAGE_SIZE}`);
108
+ const published = /** @type {ActionsJob[]} */ (attemptJobs.jobs ?? []).some((job) => (job.steps ?? []).some((step) => step.name === CI_RELEASE_WORKER_STEP_NAME && step.conclusion === CI_STEP_SUCCESS_CONCLUSION));
109
+ if (published) return { url: run.html_url, attempt };
110
+ }
111
+ }
112
+ return null;
113
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Renders the dependency-free workflow step that authenticates a dispatched release before any
3
+ * project-controlled code (package installation, lifecycle scripts, hooks) runs with the job's
4
+ * publication secrets and OIDC permission.
5
+ * @module create-version/ci-release-identity-step
6
+ */
7
+
8
+ import { CI_COMMIT_SHA_PATTERN, CI_RELEASE_ENVIRONMENT, CI_RELEASE_IDENTITY_STEP_NAME, CI_RELEASE_TAG_ENVIRONMENT, CI_RELEASE_VERSION_SHELL_PATTERN } from "../constants/ci-release.js";
9
+ import { MAIN_BRANCH, RELEASE_REMOTE } from "../constants/create-version.js";
10
+ import { RELEASE_TAG_PREFIX } from "../constants/versions.js";
11
+
12
+ /** Indentation of a `run: |` body inside a generated job step. */
13
+ const STEP_SCRIPT_INDENT = " ";
14
+
15
+ /**
16
+ * Builds the Bash script that mirrors the identity rules of `readCiReleaseIdentity` using only Git:
17
+ * the tag must be the stable tag of the dispatched version, resolve exactly to the dispatched commit
18
+ * (which must also be the checked-out HEAD), and that commit must already be in `origin/main`.
19
+ * Raw inputs are never echoed before their format is validated, so they cannot inject workflow commands.
20
+ * @returns {string[]} Script lines without indentation.
21
+ */
22
+ function buildIdentityScript() {
23
+ const tag = `$${CI_RELEASE_TAG_ENVIRONMENT}`;
24
+ const version = `$${CI_RELEASE_ENVIRONMENT.version}`;
25
+ const sha = `$${CI_RELEASE_ENVIRONMENT.sha}`;
26
+ const mainRef = `refs/remotes/${RELEASE_REMOTE}/${MAIN_BRANCH}`;
27
+ return [
28
+ "set -euo pipefail",
29
+ 'fail() { echo "::error title=Release no autorizado::$1"; exit 1; }',
30
+ `version_pattern='${CI_RELEASE_VERSION_SHELL_PATTERN}'`,
31
+ `sha_pattern='${CI_COMMIT_SHA_PATTERN.source}'`,
32
+ `[[ "${version}" =~ $version_pattern ]] || fail "El input version no es una versión estable X.Y.Z; no se instaló nada."`,
33
+ `[[ "${tag}" == "${RELEASE_TAG_PREFIX}${version}" ]] || fail "El input tag no es el tag de release ${RELEASE_TAG_PREFIX}${version}; no se instaló nada."`,
34
+ `[[ "${sha}" =~ $sha_pattern ]] || fail "El input sha no es un commit completo; no se instaló nada."`,
35
+ `git fetch --no-tags --quiet ${RELEASE_REMOTE} "+refs/heads/${MAIN_BRANCH}:${mainRef}" || fail "No se pudo traer ${RELEASE_REMOTE}/${MAIN_BRANCH} para verificar ${tag}."`,
36
+ `tag_sha="$(git rev-parse --verify --quiet "refs/tags/${tag}^{commit}")" || fail "${tag} no existe como tag en ${RELEASE_REMOTE}."`,
37
+ `[[ "$tag_sha" == "${sha}" ]] || fail "${tag} apunta a $tag_sha, no al commit ${sha} enviado."`,
38
+ `[[ "$(git rev-parse HEAD)" == "${sha}" ]] || fail "El checkout no está en el commit ${sha} de ${tag}."`,
39
+ `git merge-base --is-ancestor "${sha}" "${mainRef}" || fail "El commit ${sha} de ${tag} no está en ${RELEASE_REMOTE}/${MAIN_BRANCH}."`,
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * Renders the identity step placed right after checkout and before any toolchain or dependency setup.
45
+ * Inputs reach the script only through step environment variables, never through `${{ }}` interpolation.
46
+ * @returns {string} Workflow YAML for one step, indented for the release job.
47
+ */
48
+ export function renderCiReleaseIdentityStep() {
49
+ return [
50
+ ` - name: ${CI_RELEASE_IDENTITY_STEP_NAME}`,
51
+ " env:",
52
+ ` ${CI_RELEASE_ENVIRONMENT.version}: \${{ inputs.version }}`,
53
+ ` ${CI_RELEASE_ENVIRONMENT.sha}: \${{ inputs.sha }}`,
54
+ ` ${CI_RELEASE_TAG_ENVIRONMENT}: \${{ inputs.tag }}`,
55
+ " run: |",
56
+ ...buildIdentityScript().map((line) => `${STEP_SCRIPT_INDENT}${line}`),
57
+ ].join("\n");
58
+ }