@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.
- package/CHANGELOG.md +196 -0
- package/LICENSE.md +21 -0
- package/README.md +352 -2
- package/bin/astra-rp.js +51 -0
- package/package.json +93 -4
- package/src/build-gate.js +157 -0
- package/src/changelog-blocks.js +53 -0
- package/src/changelog.js +108 -0
- package/src/constants/build-gate.js +30 -0
- package/src/constants/changelog-draft.js +180 -0
- package/src/constants/changelog.js +74 -0
- package/src/constants/ci-release.js +346 -0
- package/src/constants/cli.js +12 -0
- package/src/constants/create-version.js +523 -0
- package/src/constants/guard-publish.js +29 -0
- package/src/constants/index.js +18 -0
- package/src/constants/monorepo.js +57 -0
- package/src/constants/package-manager.js +56 -0
- package/src/constants/registry.js +72 -0
- package/src/constants/terminal-ui.js +93 -0
- package/src/constants/version-files.js +63 -0
- package/src/constants/versions.js +60 -0
- package/src/create-version/artifact.js +343 -0
- package/src/create-version/changelog-draft-workspace.js +124 -0
- package/src/create-version/changelog-draft.js +380 -0
- package/src/create-version/changelog.js +264 -0
- package/src/create-version/ci-config.js +54 -0
- package/src/create-version/ci-publication-history.js +113 -0
- package/src/create-version/ci-release-identity-step.js +58 -0
- package/src/create-version/ci-setup.js +794 -0
- package/src/create-version/ci.js +555 -0
- package/src/create-version/committed-tree.js +75 -0
- package/src/create-version/config.js +489 -0
- package/src/create-version/errors.js +46 -0
- package/src/create-version/github-workflow.js +199 -0
- package/src/create-version/index.js +54 -0
- package/src/create-version/json.js +32 -0
- package/src/create-version/jsonc.js +76 -0
- package/src/create-version/jsr.js +96 -0
- package/src/create-version/local-changes.js +178 -0
- package/src/create-version/npm-auth.js +142 -0
- package/src/create-version/npm.js +812 -0
- package/src/create-version/plan.js +1200 -0
- package/src/create-version/process.js +334 -0
- package/src/create-version/registry-config.js +121 -0
- package/src/create-version/registry-http.js +62 -0
- package/src/create-version/registry.js +220 -0
- package/src/create-version/release-execution.js +45 -0
- package/src/create-version/release-tag-config.js +76 -0
- package/src/create-version/run.js +2087 -0
- package/src/create-version/state.js +461 -0
- package/src/create-version/strict-utf8.js +11 -0
- package/src/create-version/templates/release.yml +52 -0
- package/src/create-version/templates/vercel-ignore-build.mjs +66 -0
- package/src/guard-publish.js +77 -0
- package/src/index.js +42 -0
- package/src/monorepo/plan.js +475 -0
- package/src/monorepo/release-commit.js +46 -0
- package/src/monorepo/run.js +927 -0
- package/src/monorepo/state.js +206 -0
- package/src/monorepo/workspaces.js +327 -0
- package/src/package-manager.js +107 -0
- package/src/terminal-ui.js +548 -0
- package/src/testing.js +70 -0
- package/src/version-files.js +142 -0
- package/src/versions.js +303 -0
- package/types/build-gate.d.ts +43 -0
- package/types/changelog-blocks.d.ts +33 -0
- package/types/changelog.d.ts +61 -0
- package/types/constants/build-gate.d.ts +23 -0
- package/types/constants/changelog-draft.d.ts +138 -0
- package/types/constants/changelog.d.ts +56 -0
- package/types/constants/ci-release.d.ts +284 -0
- package/types/constants/cli.d.ts +11 -0
- package/types/constants/create-version.d.ts +397 -0
- package/types/constants/guard-publish.d.ts +25 -0
- package/types/constants/index.d.ts +17 -0
- package/types/constants/monorepo.d.ts +45 -0
- package/types/constants/package-manager.d.ts +40 -0
- package/types/constants/registry.d.ts +79 -0
- package/types/constants/terminal-ui.d.ts +73 -0
- package/types/constants/version-files.d.ts +51 -0
- package/types/constants/versions.d.ts +47 -0
- package/types/create-version/artifact.d.ts +113 -0
- package/types/create-version/changelog-draft-workspace.d.ts +33 -0
- package/types/create-version/changelog-draft.d.ts +127 -0
- package/types/create-version/changelog.d.ts +141 -0
- package/types/create-version/ci-config.d.ts +33 -0
- package/types/create-version/ci-publication-history.d.ts +51 -0
- package/types/create-version/ci-release-identity-step.d.ts +12 -0
- package/types/create-version/ci-setup.d.ts +148 -0
- package/types/create-version/ci.d.ts +145 -0
- package/types/create-version/committed-tree.d.ts +55 -0
- package/types/create-version/config.d.ts +128 -0
- package/types/create-version/errors.d.ts +39 -0
- package/types/create-version/github-workflow.d.ts +41 -0
- package/types/create-version/index.d.ts +28 -0
- package/types/create-version/json.d.ts +21 -0
- package/types/create-version/jsonc.d.ts +21 -0
- package/types/create-version/jsr.d.ts +52 -0
- package/types/create-version/local-changes.d.ts +52 -0
- package/types/create-version/npm-auth.d.ts +60 -0
- package/types/create-version/npm.d.ts +331 -0
- package/types/create-version/plan.d.ts +369 -0
- package/types/create-version/process.d.ts +151 -0
- package/types/create-version/registry-config.d.ts +56 -0
- package/types/create-version/registry-http.d.ts +22 -0
- package/types/create-version/registry.d.ts +86 -0
- package/types/create-version/release-execution.d.ts +22 -0
- package/types/create-version/release-tag-config.d.ts +18 -0
- package/types/create-version/run.d.ts +407 -0
- package/types/create-version/state.d.ts +204 -0
- package/types/create-version/strict-utf8.d.ts +10 -0
- package/types/guard-publish.d.ts +49 -0
- package/types/index.d.ts +23 -0
- package/types/monorepo/plan.d.ts +68 -0
- package/types/monorepo/release-commit.d.ts +29 -0
- package/types/monorepo/run.d.ts +68 -0
- package/types/monorepo/state.d.ts +96 -0
- package/types/monorepo/workspaces.d.ts +86 -0
- package/types/package-manager.d.ts +58 -0
- package/types/terminal-ui.d.ts +196 -0
- package/types/testing.d.ts +31 -0
- package/types/version-files.d.ts +59 -0
- package/types/versions.d.ts +158 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepares automatic CI setup as rollback-capable files of the release commit.
|
|
3
|
+
* @module create-version/ci-setup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
CI_CONFIG_EXPORT, CI_DEFAULT_PNPM_VERSION, CI_DEFAULT_YARN_VERSION, CI_PACKAGE_MANAGER_VERSION_PATTERN, CI_RELEASE_TEMPLATE_FILE,
|
|
11
|
+
CI_TEMPLATE_PLACEHOLDER, DEFAULT_CI_WORKFLOW,
|
|
12
|
+
CI_RELEASE_METADATA_FILE, CI_VERCEL_CLI_VERSION, CI_VERCEL_DEPLOYMENT, CI_VERCEL_SECRETS,
|
|
13
|
+
CI_VERCEL_CONFIG_FILE, CI_VERCEL_GATE_FILE, CI_VERCEL_GATE_TEMPLATE, CI_VERCEL_IGNORE_PLACEHOLDER, CI_VERCEL_WORKER_WRITTEN_PATHS,
|
|
14
|
+
CI_DENO_JSR_CLIENT, CI_DENO_VERSION,
|
|
15
|
+
CI_GITHUB_PACKAGES_AUTHENTICATION_STEP, CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY, CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL, CI_GITHUB_PACKAGES_TOKEN_VARIABLE, CI_NPM_USER_CONFIG_LOCATION_VARIABLES, CI_PACKAGE_MANAGER_CONFIG_VARIABLE_PREFIXES, CI_RUNNER_NPM_USER_CONFIG_PATH,
|
|
16
|
+
NPM_CONFIG_ARRAY_KEY_SUFFIX, NPM_CONFIG_COMMENT_PREFIXES, NPM_CONFIG_JSON_QUOTE, NPM_CONFIG_LINE_BREAK_PATTERN, NPM_CONFIG_ENVIRONMENT_REFERENCE_PATTERN, NPM_CONFIG_ENVIRONMENT_REFERENCES_PATTERN, NPM_CONFIG_GITHUB_PACKAGES_KEY_PATTERN, NPM_CONFIG_INLINE_COMMENT_PATTERN, NPM_CONFIG_QUOTES, NPM_CONFIG_REGISTRY_KEY_PATTERN, NPM_CONFIG_SECTION_HEADER_PATTERN, NPM_CONFIG_VALUELESS_KEY_VALUE,
|
|
17
|
+
CI_GITHUB_TOKEN_JOB_BINDING, CI_GITHUB_PACKAGES_AUTHENTICATED_PACKAGE_MANAGERS, CI_UNAUTHENTICATED_PACKAGE_MANAGER_CONFIG_FILES,
|
|
18
|
+
GITHUB_PACKAGES_HOST_MENTION_PATTERN, PACKAGE_MANAGER_CONFIG_COMMENT_PREFIX, PNPM_WORKSPACE_CONFIG_FILE, PNPM_WORKSPACE_NETWORK_SETTING_LINE_PATTERN,
|
|
19
|
+
CI_TLS_VERIFICATION_DISABLING_VARIABLES, LOCKFILE_GITHUB_PACKAGES_HTTP_URL, LOCKFILE_GITHUB_PACKAGES_HTTP_URL_PATTERN,
|
|
20
|
+
NPM_CONFIG_DISABLED_VALUE, NPM_CONFIG_STRICT_SSL_KEY,
|
|
21
|
+
} from "../constants/ci-release.js";
|
|
22
|
+
import { CREATE_VERSION_CONFIG_FILES, CREATE_VERSION_FLAG, PINNED_NODE_VERSION_FILE, PROJECT_NPM_CONFIG_FILE, VERSION_PREFIX_PATTERN } from "../constants/create-version.js";
|
|
23
|
+
import { describeCreateVersionWithOptions } from "../package-manager.js";
|
|
24
|
+
import { LOCKFILE_NAMES, PACKAGE_MANAGER } from "../constants/package-manager.js";
|
|
25
|
+
import { ciWorkflowPath } from "./ci-config.js";
|
|
26
|
+
import { requiresCiPublicationHistory } from "./ci-publication-history.js";
|
|
27
|
+
import { isCommittedRegularFile, listCommittedTreeEntries, readCommittedFileEntry, readCommittedRegularFile } from "./committed-tree.js";
|
|
28
|
+
import { renderCiReleaseIdentityStep } from "./ci-release-identity-step.js";
|
|
29
|
+
import { ReleaseStepError } from "./errors.js";
|
|
30
|
+
import { readJsonManifest } from "./json.js";
|
|
31
|
+
import { runCaptured } from "./process.js";
|
|
32
|
+
import { GITHUB_PACKAGES_HOST, GITHUB_PACKAGES_REGISTRY_URL, GITHUB_REGISTRY_TOKEN_VARIABLE, JSR_REGISTRY_PROVIDER, NPM_OIDC_MINIMUM_VERSION, NPM_REGISTRY_PROVIDER, OIDC_AUTHENTICATION, TOKEN_AUTHENTICATION } from "../constants/registry.js";
|
|
33
|
+
import { GIT_LITERAL_PATHSPEC_PREFIX } from "../constants/version-files.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {import("./config.js").ResolvedCreateVersionConfig} ResolvedCreateVersionConfig
|
|
37
|
+
* @typedef {import("./run.js").ReleaseFileUpdate} ReleaseFileUpdate
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds the worker's environment from explicit bindings and the selected registry credential.
|
|
42
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
43
|
+
* Every Vercel credential is listed once in `secrets` (so preflight requires it) and always in
|
|
44
|
+
* `deploymentSecrets`, even when also declared in `ci.secrets`, which keeps it out of the job-level environment.
|
|
45
|
+
* @returns {{ secrets: string[], variables: string[], githubToken: boolean, githubTokenWrite: boolean, deploymentSecrets: string[] }} Required worker bindings, step-scoped deployment credentials and package permission.
|
|
46
|
+
*/
|
|
47
|
+
export function describeCiEnvironment(config) {
|
|
48
|
+
const declaredSecrets = config.ci?.secrets ?? [];
|
|
49
|
+
const variables = [...(config.ci?.variables ?? [])];
|
|
50
|
+
const publicationTokenVariable = readPublicationTokenVariable(config);
|
|
51
|
+
const publisherUsesGithubToken = publicationTokenVariable === GITHUB_REGISTRY_TOKEN_VARIABLE;
|
|
52
|
+
const githubToken = publisherUsesGithubToken || declaredSecrets.includes(GITHUB_REGISTRY_TOKEN_VARIABLE);
|
|
53
|
+
const secrets = declaredSecrets.filter((name) => !githubToken || name !== GITHUB_REGISTRY_TOKEN_VARIABLE);
|
|
54
|
+
if (githubToken && variables.includes(GITHUB_REGISTRY_TOKEN_VARIABLE)) throw new ReleaseStepError("GITHUB_TOKEN es una credencial del workflow y no puede declararse en ci.variables.", "Usá ci.secrets para solicitar el token integrado de Actions o elegí otra variable de token.");
|
|
55
|
+
if (publicationTokenVariable !== null && !publisherUsesGithubToken && !secrets.includes(publicationTokenVariable)) {
|
|
56
|
+
secrets.push(publicationTokenVariable);
|
|
57
|
+
}
|
|
58
|
+
const deploymentSecrets = config.ci?.deployment === CI_VERCEL_DEPLOYMENT ? [...CI_VERCEL_SECRETS] : [];
|
|
59
|
+
secrets.push(...deploymentSecrets.filter((name) => !secrets.includes(name)));
|
|
60
|
+
if (secrets.some((name) => variables.includes(name))) throw new ReleaseStepError("Una credencial de publicación también figura en ci.variables.", "Guardala solo como secret; no se creó ningún archivo.");
|
|
61
|
+
return { secrets, variables, githubToken, githubTokenWrite: publisherUsesGithubToken, deploymentSecrets };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads the variable holding the publication token when a registry publisher authenticates with a
|
|
66
|
+
* stored token instead of OIDC or a custom publish function.
|
|
67
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
68
|
+
* @returns {string | null} `publication.tokenEnv`, or `null` when the publication reads no token variable.
|
|
69
|
+
*/
|
|
70
|
+
function readPublicationTokenVariable(config) {
|
|
71
|
+
const publishesToRegistry = Boolean(config.publish) && typeof config.publish !== "function";
|
|
72
|
+
return publishesToRegistry && config.publication.authentication === TOKEN_AUTHENTICATION ? config.publication.tokenEnv : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads the package manager version pinned by `packageManager`, the exact one the generated workflow installs.
|
|
77
|
+
* @param {Record<string, unknown>} manifest - Parsed project `package.json`.
|
|
78
|
+
* @returns {string | null} Pinned version such as `11.5.1`, or `null` when the field is absent or not a safe pin.
|
|
79
|
+
*/
|
|
80
|
+
export function extractPinnedPackageManagerVersion(manifest) {
|
|
81
|
+
return typeof manifest.packageManager === "string" ? CI_PACKAGE_MANAGER_VERSION_PATTERN.exec(manifest.packageManager)?.[1] ?? null : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads the major of the Node.js running setup, the runtime the generated workflow falls back to
|
|
86
|
+
* when HEAD commits no `.nvmrc`.
|
|
87
|
+
* @returns {string} Major version such as `22`.
|
|
88
|
+
*/
|
|
89
|
+
export function readRunningNodeMajor() {
|
|
90
|
+
return process.versions.node.split(".")[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Describes the Node.js version the generated workflow asks `actions/setup-node` for: the
|
|
95
|
+
* `.nvmrc` pin when HEAD commits it as a regular file, otherwise the major of the Node.js running
|
|
96
|
+
* setup. Only HEAD counts because the worker checks out the release commit built from it: an
|
|
97
|
+
* untracked, ignored or set-aside `.nvmrc` would make `node-version-file` fail there after the tag
|
|
98
|
+
* was pushed. Reading HEAD also keeps the rendered workflow deterministic for the drift check.
|
|
99
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
100
|
+
* @returns {Promise<{ pinnedFile: string | null, version: string }>} Pin file the workflow reads (or `null`) and its version without a `v` prefix.
|
|
101
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD.
|
|
102
|
+
*/
|
|
103
|
+
export async function describeCiNodeVersion(repositoryRoot) {
|
|
104
|
+
const committedPin = await readCommittedRegularFile(repositoryRoot, PINNED_NODE_VERSION_FILE);
|
|
105
|
+
if (committedPin === null) return { pinnedFile: null, version: readRunningNodeMajor() };
|
|
106
|
+
return { pinnedFile: PINNED_NODE_VERSION_FILE, version: committedPin.trim().replace(VERSION_PREFIX_PATTERN, "") };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tells whether the worker publishes to npm through OIDC with an npm client the workflow does not
|
|
111
|
+
* otherwise control: a package manager other than npm, or npm without a `packageManager` pin.
|
|
112
|
+
* Publication runs the npm CLI, whose Node-bundled version predates trusted publishing on most
|
|
113
|
+
* runtimes, so the workflow installs a capable npm. A pinned npm is never replaced (it is checked
|
|
114
|
+
* before tagging instead); an unpinned npm project gets the capable client only after `npm ci`, so
|
|
115
|
+
* the client setup-node bundles keeps installing dependencies from the committed lockfile.
|
|
116
|
+
* @param {ResolvedCreateVersionConfig} config - Release hooks and package manager.
|
|
117
|
+
* @param {string | null} [pinnedVersion] - Version pinned by `packageManager`, from {@link extractPinnedPackageManagerVersion}.
|
|
118
|
+
* @returns {boolean} True when the generated workflow must install the npm trusted publishing client.
|
|
119
|
+
*/
|
|
120
|
+
function requiresCiNpmOidcClient(config, pinnedVersion = null) {
|
|
121
|
+
return config.publish === NPM_REGISTRY_PROVIDER && config.publication?.authentication === OIDC_AUTHENTICATION && (config.commands.packageManager !== PACKAGE_MANAGER.npm || pinnedVersion === null);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Tells whether text is wrapped in a pair of the given quote character.
|
|
126
|
+
* @param {string} text - Trimmed text.
|
|
127
|
+
* @param {string} quote - One of {@link NPM_CONFIG_QUOTES}.
|
|
128
|
+
* @returns {boolean} True when the text opens and closes with that quote.
|
|
129
|
+
*/
|
|
130
|
+
function isWrappedInQuote(text, quote) {
|
|
131
|
+
return text.length > 1 && text[0] === quote && text.at(-1) === quote;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reads a raw `.npmrc` key or value the way npm's INI parser does for both: single quotes are removed
|
|
136
|
+
* first, then text left wrapped in double quotes is decoded as a JSON string (Unicode escapes included)
|
|
137
|
+
* or, when it is not valid JSON, is kept as is, double quotes included; unquoted text ends at an inline
|
|
138
|
+
* `;` or `#` comment.
|
|
139
|
+
* @param {string} rawText - Text before or after the first `=` of the line.
|
|
140
|
+
* @returns {string} Effective key or value.
|
|
141
|
+
*/
|
|
142
|
+
function parseNpmConfigText(rawText) {
|
|
143
|
+
const text = rawText.trim();
|
|
144
|
+
const quote = NPM_CONFIG_QUOTES.find((candidate) => isWrappedInQuote(text, candidate));
|
|
145
|
+
if (quote !== undefined) {
|
|
146
|
+
const unquotedText = quote === NPM_CONFIG_JSON_QUOTE ? text : text.slice(1, -1);
|
|
147
|
+
return isWrappedInQuote(unquotedText, NPM_CONFIG_JSON_QUOTE) ? decodeNpmConfigJsonString(unquotedText) : unquotedText;
|
|
148
|
+
}
|
|
149
|
+
const commentIndex = text.search(NPM_CONFIG_INLINE_COMMENT_PATTERN);
|
|
150
|
+
return (commentIndex === -1 ? text : text.slice(0, commentIndex)).trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Decodes a double-quoted `.npmrc` key or value as a JSON string, as npm's INI parser does.
|
|
155
|
+
* @param {string} quotedText - Trimmed text wrapped in {@link NPM_CONFIG_JSON_QUOTE}.
|
|
156
|
+
* @returns {string} Decoded text, or the original text with its double quotes when it is not a valid JSON string.
|
|
157
|
+
*/
|
|
158
|
+
function decodeNpmConfigJsonString(quotedText) {
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(quotedText);
|
|
161
|
+
} catch {
|
|
162
|
+
// Not a valid JSON string (for example an unknown escape): npm keeps the text unchanged, quotes included.
|
|
163
|
+
return quotedText;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tells whether a project `.npmrc` key is bound to the GitHub Packages host, whatever its path, field or
|
|
169
|
+
* value: any of them can change how the install authenticates there, and pnpm ignores repository
|
|
170
|
+
* credentials, so the worker still needs the user-level token.
|
|
171
|
+
* @param {string} key - Effective `.npmrc` key.
|
|
172
|
+
* @returns {boolean} True for a key matching {@link NPM_CONFIG_GITHUB_PACKAGES_KEY_PATTERN}.
|
|
173
|
+
*/
|
|
174
|
+
function isGithubPackagesBoundKey(key) {
|
|
175
|
+
return NPM_CONFIG_GITHUB_PACKAGES_KEY_PATTERN.test(key);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reads the effective top-level settings of a `.npmrc` the way npm's INI parser does: a repeated key
|
|
180
|
+
* keeps its last assignment (compared after unquoting), and every key after an INI `[section]`
|
|
181
|
+
* header ({@link NPM_CONFIG_SECTION_HEADER_PATTERN}, tested on the untrimmed line as npm does) is nested
|
|
182
|
+
* under that section, so it never configures the top-level registries or credentials.
|
|
183
|
+
* A key written without `=` gets {@link NPM_CONFIG_VALUELESS_KEY_VALUE}, as in npm, and lines end at
|
|
184
|
+
* CRLF, LF or a lone CR ({@link NPM_CONFIG_LINE_BREAK_PATTERN}), as npm splits them. npm also drops the
|
|
185
|
+
* {@link NPM_CONFIG_ARRAY_KEY_SUFFIX} of an INI array key longer than the suffix, so `key[]` configures `key`;
|
|
186
|
+
* a key bound to GitHub Packages keeps the suffix so the policy rejects it whatever the other assignments.
|
|
187
|
+
* @param {string} npmConfigText - Raw `.npmrc` contents.
|
|
188
|
+
* @returns {Map<string, string | true>} Effective value of each top-level key, in first-assignment order.
|
|
189
|
+
*/
|
|
190
|
+
function readTopLevelNpmConfigEntries(npmConfigText) {
|
|
191
|
+
/** @type {Map<string, string | true>} */
|
|
192
|
+
const entries = new Map();
|
|
193
|
+
for (const line of npmConfigText.split(NPM_CONFIG_LINE_BREAK_PATTERN)) {
|
|
194
|
+
if (NPM_CONFIG_SECTION_HEADER_PATTERN.test(line)) break;
|
|
195
|
+
const trimmedLine = line.trim();
|
|
196
|
+
if (trimmedLine === "" || NPM_CONFIG_COMMENT_PREFIXES.some((prefix) => trimmedLine.startsWith(prefix))) continue;
|
|
197
|
+
const separatorIndex = trimmedLine.indexOf("=");
|
|
198
|
+
const key = parseNpmConfigText(separatorIndex === -1 ? trimmedLine : trimmedLine.slice(0, separatorIndex));
|
|
199
|
+
const isArrayKey = key.length > NPM_CONFIG_ARRAY_KEY_SUFFIX.length && key.endsWith(NPM_CONFIG_ARRAY_KEY_SUFFIX) && !isGithubPackagesBoundKey(key);
|
|
200
|
+
entries.set(isArrayKey ? key.slice(0, -NPM_CONFIG_ARRAY_KEY_SUFFIX.length) : key, separatorIndex === -1 ? NPM_CONFIG_VALUELESS_KEY_VALUE : parseNpmConfigText(trimmedLine.slice(separatorIndex + 1)));
|
|
201
|
+
}
|
|
202
|
+
return entries;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Tells whether npm would expand a `${NAME}` environment reference inside a `.npmrc` key or value.
|
|
207
|
+
* @param {string | true} text - Effective key or value; a valueless key has no references.
|
|
208
|
+
* @returns {boolean} True when the text contains at least one reference.
|
|
209
|
+
*/
|
|
210
|
+
function containsNpmConfigEnvironmentReference(text) {
|
|
211
|
+
return typeof text === "string" && text.search(NPM_CONFIG_ENVIRONMENT_REFERENCES_PATTERN) !== -1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reads the variable of the only project credential the generated workflow accepts for GitHub
|
|
216
|
+
* Packages: {@link CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY} whose whole value is one `${NAME}` reference.
|
|
217
|
+
* @param {string | true | undefined} value - Effective value of that host token, if the `.npmrc` sets it.
|
|
218
|
+
* @returns {string | null} `NAME`, or `null` when the value has any other form.
|
|
219
|
+
*/
|
|
220
|
+
function readAcceptedGithubPackagesTokenVariable(value) {
|
|
221
|
+
return typeof value === "string" ? NPM_CONFIG_ENVIRONMENT_REFERENCE_PATTERN.exec(value)?.[1] ?? null : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Tells whether a `.npmrc` entry selects GitHub Packages as the default or a scoped registry.
|
|
226
|
+
* @param {string} key - Effective `.npmrc` key.
|
|
227
|
+
* @param {string | true} value - Effective value.
|
|
228
|
+
* @returns {boolean} True for a registry key whose URL host is GitHub Packages.
|
|
229
|
+
*/
|
|
230
|
+
function selectsGithubPackagesRegistry(key, value) {
|
|
231
|
+
return NPM_CONFIG_REGISTRY_KEY_PATTERN.test(key) && typeof value === "string" && URL.canParse(value) && new URL(value).host === GITHUB_PACKAGES_HOST;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @typedef {object} ProjectGithubPackagesConfig
|
|
236
|
+
* @property {boolean} usesGithubPackages - A literal registry or any project credential selects GitHub Packages.
|
|
237
|
+
* @property {string | null} tokenVariable - Variable expanded by the accepted host token (`${NAME}`), or `null`.
|
|
238
|
+
* @property {Array<{ key: string, value: string | true }>} entries - Effective top-level entries, validated by
|
|
239
|
+
* {@link assertProjectNpmConfigEntriesSupported}.
|
|
240
|
+
* @property {string | null} irregularFileMode - Git tree mode of a committed `.npmrc` that is not a regular file
|
|
241
|
+
* (a symlink, `120000`, or another kind of entry), which npm still follows in the worker; `null` otherwise.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @typedef {object} CommittedPackageManagerFile
|
|
246
|
+
* @property {string} filePath - Root-relative path of the file committed at HEAD.
|
|
247
|
+
* @property {string | null} text - Committed content, or `null` when the entry is not a regular file.
|
|
248
|
+
* @property {string | null} irregularFileMode - Git tree mode of an entry that is not a regular file (a symlink,
|
|
249
|
+
* `120000`, or another kind of entry), which the package manager still follows in the worker; `null` otherwise.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Reads the package manager files committed at HEAD. Only HEAD counts because the worker
|
|
254
|
+
* checks out the release commit; an absent path is omitted.
|
|
255
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
256
|
+
* @param {string[]} filePaths - Root-relative paths to read.
|
|
257
|
+
* @returns {Promise<CommittedPackageManagerFile[]>} Committed files found at HEAD.
|
|
258
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD.
|
|
259
|
+
*/
|
|
260
|
+
async function readCommittedPackageManagerFiles(repositoryRoot, filePaths) {
|
|
261
|
+
const entries = await listCommittedTreeEntries(repositoryRoot, filePaths);
|
|
262
|
+
return Promise.all(entries.map(async (entry) => isCommittedRegularFile(entry)
|
|
263
|
+
? { filePath: entry.filePath, text: await readCommittedFileEntry(repositoryRoot, entry), irregularFileMode: null }
|
|
264
|
+
: { filePath: entry.filePath, text: null, irregularFileMode: entry.mode }));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Rejects a committed package manager file that is not a regular file once the workflow hands the package manager
|
|
269
|
+
* the GitHub Packages token: HEAD only stores its target, so its registries cannot be checked before the bump.
|
|
270
|
+
* @param {CommittedPackageManagerFile} file - Committed file description.
|
|
271
|
+
* @returns {void}
|
|
272
|
+
* @throws {ReleaseStepError} When the file is not a regular file.
|
|
273
|
+
*/
|
|
274
|
+
function assertCommittedPackageManagerFileRegular(file) {
|
|
275
|
+
if (file.irregularFileMode === null) return;
|
|
276
|
+
throwCommittedFileError(file.filePath, `no es un archivo regular (modo ${file.irregularFileMode}); el package manager lo seguiría en el worker de CI y antes del bump no se puede comprobar qué registry usa con el token de GitHub Packages.`, "Reemplazalo por un archivo regular");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Reads the uncommented lines of a committed package manager config: a line whose trimmed text starts with
|
|
281
|
+
* {@link PACKAGE_MANAGER_CONFIG_COMMENT_PREFIX} is a comment and configures nothing.
|
|
282
|
+
* @param {CommittedPackageManagerFile} file - Committed file description.
|
|
283
|
+
* @returns {string[]} Uncommented lines, or none when the entry is not a regular file.
|
|
284
|
+
*/
|
|
285
|
+
function readUncommentedConfigLines(file) {
|
|
286
|
+
return (file.text?.split(NPM_CONFIG_LINE_BREAK_PATTERN) ?? []).filter((line) => !line.trim().startsWith(PACKAGE_MANAGER_CONFIG_COMMENT_PREFIX));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Rejects a GitHub Packages setup for a package manager the generated workflow does not authenticate (Yarn of any
|
|
291
|
+
* version or Bun): each reads registries and credentials from its own config files and environment variables, which
|
|
292
|
+
* the workflow cannot validate before handing it the Actions token. Besides the shared signals (see
|
|
293
|
+
* {@link readsGithubPackages}), an uncommented mention of the GitHub Packages host in the package manager's own
|
|
294
|
+
* committed config (see {@link CI_UNAUTHENTICATED_PACKAGE_MANAGER_CONFIG_FILES}) counts as a signal. npm and pnpm
|
|
295
|
+
* never read the config files of other package managers, so those files are not inspected for them.
|
|
296
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
297
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
298
|
+
* @param {boolean} sharedSignal - Whether a declared `GITHUB_TOKEN` or the committed `.npmrc` selects GitHub Packages.
|
|
299
|
+
* @returns {Promise<void>}
|
|
300
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD or such a project uses GitHub Packages.
|
|
301
|
+
*/
|
|
302
|
+
async function assertPackageManagerAuthenticatesGithubPackages(repositoryRoot, config, sharedSignal) {
|
|
303
|
+
const manager = config.commands.packageManager;
|
|
304
|
+
if (CI_GITHUB_PACKAGES_AUTHENTICATED_PACKAGE_MANAGERS.includes(manager)) return;
|
|
305
|
+
const ownConfigFiles = await readCommittedPackageManagerFiles(repositoryRoot, [...(CI_UNAUTHENTICATED_PACKAGE_MANAGER_CONFIG_FILES[manager] ?? [])]);
|
|
306
|
+
const configuredFile = ownConfigFiles.find((file) => readUncommentedConfigLines(file).some((line) => GITHUB_PACKAGES_HOST_MENTION_PATTERN.test(line)));
|
|
307
|
+
if (!sharedSignal && configuredFile === undefined) return;
|
|
308
|
+
const signal = configuredFile === undefined ? `ci.secrets declara ${GITHUB_REGISTRY_TOKEN_VARIABLE} o el ${PROJECT_NPM_CONFIG_FILE} commiteado usa GitHub Packages` : `el ${configuredFile.filePath} commiteado usa GitHub Packages`;
|
|
309
|
+
throw new ReleaseStepError(
|
|
310
|
+
`El workflow generado solo autentica GitHub Packages con npm o pnpm y el proyecto usa ${manager}, pero ${signal}; ${manager} lee registries y credenciales de su propia configuración y el workflow no puede comprobarlos antes de darle el token de Actions.`,
|
|
311
|
+
`Usá un workflow propio en ci.workflow que autentique ${manager} con GitHub Packages, o quitá la configuración de GitHub Packages; no se cambió ningún archivo.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Rejects the package manager files that could send the GitHub Packages token somewhere unchecked once the workflow
|
|
317
|
+
* hands it to the install: any of them that is not a regular file, a `pnpm-workspace.yaml` that defines registries
|
|
318
|
+
* or network settings (see {@link PNPM_WORKSPACE_NETWORK_SETTING_LINE_PATTERN}), and a lockfile that resolves GitHub
|
|
319
|
+
* Packages over plain HTTP.
|
|
320
|
+
* @param {CommittedPackageManagerFile[]} files - Committed package manager files.
|
|
321
|
+
* @returns {void}
|
|
322
|
+
* @throws {ReleaseStepError} When one of those files is found.
|
|
323
|
+
*/
|
|
324
|
+
function assertCommittedPackageManagerFilesSupported(files) {
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
assertCommittedPackageManagerFileRegular(file);
|
|
327
|
+
if (file.filePath === PNPM_WORKSPACE_CONFIG_FILE && readUncommentedConfigLines(file).some((line) => PNPM_WORKSPACE_NETWORK_SETTING_LINE_PATTERN.test(line))) {
|
|
328
|
+
throwCommittedFileError(
|
|
329
|
+
PNPM_WORKSPACE_CONFIG_FILE,
|
|
330
|
+
`define registries o ajustes de red (registry, registries, strictSsl, ca, cert, key o proxies); pnpm los usaría en el worker de CI con el token de GitHub Packages y el workflow generado solo valida los registries del ${PROJECT_NPM_CONFIG_FILE}.`,
|
|
331
|
+
`Mové el registry al ${PROJECT_NPM_CONFIG_FILE} commiteado con una URL literal ${CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL}//, quitá esos ajustes del ${PNPM_WORKSPACE_CONFIG_FILE} sin desactivar la verificación TLS`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (LOCKFILE_NAMES.includes(file.filePath) && LOCKFILE_GITHUB_PACKAGES_HTTP_URL_PATTERN.test(file.text ?? "")) {
|
|
335
|
+
throwCommittedFileError(
|
|
336
|
+
file.filePath,
|
|
337
|
+
`resuelve paquetes desde ${LOCKFILE_GITHUB_PACKAGES_HTTP_URL}; la instalación le enviaría el token de GitHub Packages sin cifrar.`,
|
|
338
|
+
`Regenerá el lockfile con el registry ${GITHUB_PACKAGES_REGISTRY_URL} (${CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL}//)`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Rejects a committed `.npmrc` whose effective `strict-ssl` is `false` once the workflow hands npm the GitHub
|
|
346
|
+
* Packages token: without TLS verification anyone on the network path could read it.
|
|
347
|
+
* @param {ProjectGithubPackagesConfig} projectGithubPackages - Committed `.npmrc` description.
|
|
348
|
+
* @returns {void}
|
|
349
|
+
* @throws {ReleaseStepError} When TLS verification is disabled.
|
|
350
|
+
*/
|
|
351
|
+
function assertProjectNpmConfigVerifiesTls(projectGithubPackages) {
|
|
352
|
+
if (!projectGithubPackages.entries.some(({ key, value }) => key === NPM_CONFIG_STRICT_SSL_KEY && value === NPM_CONFIG_DISABLED_VALUE)) return;
|
|
353
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `define ${NPM_CONFIG_STRICT_SSL_KEY}=${NPM_CONFIG_DISABLED_VALUE}, que desactiva la verificación TLS; el token de GitHub Packages podría quedar expuesto en la instalación.`, `Quitá ${NPM_CONFIG_STRICT_SSL_KEY}=${NPM_CONFIG_DISABLED_VALUE}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Describes how the `.npmrc` committed at HEAD reaches GitHub Packages, which the worker cannot
|
|
358
|
+
* install from anonymously. Only HEAD counts because the worker checks out the release commit: an
|
|
359
|
+
* untracked or ignored `.npmrc` never reaches it. Only the effective top-level values count (see
|
|
360
|
+
* {@link readTopLevelNpmConfigEntries}): a later assignment replaces an earlier one, as in npm.
|
|
361
|
+
* A committed `.npmrc` that is not a regular file is only recorded in `irregularFileMode`, since its
|
|
362
|
+
* content cannot be read from HEAD. It only describes the file; whether the generated workflow supports
|
|
363
|
+
* it is decided by {@link assertGeneratedGithubPackagesSetupSupported}.
|
|
364
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
365
|
+
* @returns {Promise<ProjectGithubPackagesConfig>} How the committed `.npmrc` reaches GitHub Packages.
|
|
366
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD.
|
|
367
|
+
*/
|
|
368
|
+
async function describeProjectGithubPackagesConfig(repositoryRoot) {
|
|
369
|
+
/** @type {ProjectGithubPackagesConfig} */
|
|
370
|
+
const description = { usesGithubPackages: false, tokenVariable: null, entries: [], irregularFileMode: null };
|
|
371
|
+
const [npmConfigFile] = await readCommittedPackageManagerFiles(repositoryRoot, [PROJECT_NPM_CONFIG_FILE]);
|
|
372
|
+
if (npmConfigFile === undefined) return description;
|
|
373
|
+
if (npmConfigFile.text === null) {
|
|
374
|
+
description.irregularFileMode = npmConfigFile.irregularFileMode;
|
|
375
|
+
return description;
|
|
376
|
+
}
|
|
377
|
+
const npmConfigEntries = readTopLevelNpmConfigEntries(npmConfigFile.text);
|
|
378
|
+
description.entries = [...npmConfigEntries].map(([key, value]) => ({ key, value }));
|
|
379
|
+
description.usesGithubPackages = description.entries.some(({ key, value }) => selectsGithubPackagesRegistry(key, value) || isGithubPackagesBoundKey(key));
|
|
380
|
+
description.tokenVariable = readAcceptedGithubPackagesTokenVariable(npmConfigEntries.get(CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY));
|
|
381
|
+
return description;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Applies the closed `.npmrc` policy of the generated workflow, entry by entry, before the bump:
|
|
386
|
+
* - the committed `.npmrc` must be a regular file;
|
|
387
|
+
* - no key may contain `${...}` and no registry may read `${...}`, because npm expands them and their
|
|
388
|
+
* host cannot be known before the bump;
|
|
389
|
+
* - a literal GitHub Packages registry must use {@link CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL};
|
|
390
|
+
* - the only accepted key bound to the GitHub Packages host is {@link CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY}`=${NAME}`,
|
|
391
|
+
* whose `NAME` must be allowed (see {@link assertGithubPackagesTokenVariableAllowed}).
|
|
392
|
+
* @param {ProjectGithubPackagesConfig} projectGithubPackages - Committed `.npmrc` description.
|
|
393
|
+
* @returns {void}
|
|
394
|
+
* @throws {ReleaseStepError} When an entry is outside the policy.
|
|
395
|
+
*/
|
|
396
|
+
function assertProjectNpmConfigEntriesSupported(projectGithubPackages) {
|
|
397
|
+
assertProjectNpmConfigRegularFile(projectGithubPackages.irregularFileMode);
|
|
398
|
+
for (const { key, value } of projectGithubPackages.entries) {
|
|
399
|
+
if (containsNpmConfigEnvironmentReference(key)) throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `define la clave ${key}, que npm completa con variables de entorno; antes del bump no se puede saber a qué host apunta ni si reemplazaría el token del workflow.`, `Escribí la clave literal (por ejemplo ${CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY}) o quitá la línea`);
|
|
400
|
+
const isRegistryKey = NPM_CONFIG_REGISTRY_KEY_PATTERN.test(key);
|
|
401
|
+
if (isRegistryKey && containsNpmConfigEnvironmentReference(value)) throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `define ${key} con una variable de entorno; antes del bump no se puede comprobar si apunta a GitHub Packages ni que use ${CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL}.`, `Escribí la URL literal ${CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL}// del registry`);
|
|
402
|
+
if (selectsGithubPackagesRegistry(key, value) && new URL(/** @type {string} */ (value)).protocol !== CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL) {
|
|
403
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `define ${key} con GitHub Packages sobre ${new URL(/** @type {string} */ (value)).protocol}; el workflow le daría a npm el token de Actions y npm lo enviaría sin cifrar.`, `Usá ${GITHUB_PACKAGES_REGISTRY_URL} como registry`);
|
|
404
|
+
}
|
|
405
|
+
if (isGithubPackagesBoundKey(key) && (key !== CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY || readAcceptedGithubPackagesTokenVariable(value) === null)) {
|
|
406
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `define la credencial ${key} con una forma que el workflow generado no acepta: solo admite ${CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY}=\${NOMBRE}, porque cualquier otra credencial de GitHub Packages podría tener prioridad sobre el token de Actions y la instalación fallaría con 401.`, `Usá ${CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY}=\${${GITHUB_REGISTRY_TOKEN_VARIABLE}} o quitá la línea`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Throws a {@link ReleaseStepError} about a file committed at HEAD with the shared closing instructions.
|
|
413
|
+
* @param {string} filePath - Root-relative path of the committed file.
|
|
414
|
+
* @param {string} problem - Spanish description following "El <archivo> commiteado ".
|
|
415
|
+
* @param {string} action - Spanish instruction the hint starts with.
|
|
416
|
+
* @returns {never}
|
|
417
|
+
* @throws {ReleaseStepError} Always.
|
|
418
|
+
*/
|
|
419
|
+
function throwCommittedFileError(filePath, problem, action) {
|
|
420
|
+
throw new ReleaseStepError(`El ${filePath} commiteado ${problem}`, `${action}, commiteá el cambio y volvé a correr el comando; no se cambió ningún archivo.`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Rejects a committed `.npmrc` that is not a regular file, such as a symlink: HEAD only stores its
|
|
425
|
+
* target, so its settings cannot be checked before the bump, yet npm follows it in the worker.
|
|
426
|
+
* @param {string | null} irregularFileMode - Git tree mode of the committed `.npmrc`, or `null` when it is regular or absent.
|
|
427
|
+
* @returns {void}
|
|
428
|
+
* @throws {ReleaseStepError} When the committed `.npmrc` is not a regular file.
|
|
429
|
+
*/
|
|
430
|
+
function assertProjectNpmConfigRegularFile(irregularFileMode) {
|
|
431
|
+
if (irregularFileMode === null) return;
|
|
432
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `no es un archivo regular (modo ${irregularFileMode}, por ejemplo un enlace simbólico); npm lo seguiría en el worker de CI y antes del bump no se puede comprobar qué registries ni credenciales usa.`, "Reemplazalo por un archivo regular con la configuración de npm");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Rejects bindings the generated workflow cannot combine with GitHub Packages, before anything is
|
|
437
|
+
* written: each would make the dependency install send a wrong credential, none, or send the Actions
|
|
438
|
+
* token somewhere unchecked.
|
|
439
|
+
* - With the token step, a job binding of {@link CI_NPM_USER_CONFIG_LOCATION_VARIABLES} moves the user config
|
|
440
|
+
* away from the runner's `~/.npmrc`, where the step stores the token, and any binding whose name starts, in any
|
|
441
|
+
* casing, with one of {@link CI_PACKAGE_MANAGER_CONFIG_VARIABLE_PREFIXES} overrides the committed `.npmrc`
|
|
442
|
+
* (registry, config files, credentials) with a value that cannot be checked before dispatch.
|
|
443
|
+
* - With the token step, a binding of {@link CI_TLS_VERIFICATION_DISABLING_VARIABLES} turns off TLS verification.
|
|
444
|
+
* - The accepted token must expand an allowed variable (see {@link assertGithubPackagesTokenVariableAllowed}).
|
|
445
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
446
|
+
* @param {{ githubToken: boolean, deploymentSecrets: string[] }} environment - Worker bindings from {@link describeCiEnvironment}.
|
|
447
|
+
* @param {ProjectGithubPackagesConfig} projectGithubPackages - Committed `.npmrc` description.
|
|
448
|
+
* @param {boolean} authenticatesGithubPackages - Whether the workflow adds the token step (see {@link readsGithubPackages}).
|
|
449
|
+
* @returns {void}
|
|
450
|
+
* @throws {ReleaseStepError} When any of those setups is found.
|
|
451
|
+
*/
|
|
452
|
+
function assertGithubPackagesSetupSupported(config, environment, projectGithubPackages, authenticatesGithubPackages) {
|
|
453
|
+
const declaredNames = [...(config.ci?.secrets ?? []), ...(config.ci?.variables ?? [])];
|
|
454
|
+
const configurationVariable = declaredNames.find((name) => CI_NPM_USER_CONFIG_LOCATION_VARIABLES.includes(name) || CI_PACKAGE_MANAGER_CONFIG_VARIABLE_PREFIXES.some((prefix) => name.toUpperCase().startsWith(prefix)));
|
|
455
|
+
if (authenticatesGithubPackages && configurationVariable !== undefined) {
|
|
456
|
+
throw new ReleaseStepError(
|
|
457
|
+
`ci.secrets o ci.variables declara ${configurationVariable}, que cambia la configuración del package manager o dónde la busca; el workflow guarda el token de GitHub Packages en ~/.npmrc del runner y antes del bump no se puede comprobar qué registry ni qué credencial usaría la instalación de dependencias.`,
|
|
458
|
+
`Quitá ${configurationVariable} de ci.secrets y ci.variables (el workflow usa ${CI_RUNNER_NPM_USER_CONFIG_PATH}) y definí registries y credenciales en el ${PROJECT_NPM_CONFIG_FILE} commiteado, con URLs literales ${CI_GITHUB_PACKAGES_REGISTRY_PROTOCOL}//; no se cambió ningún archivo.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const tlsDisablingVariable = declaredNames.find((name) => CI_TLS_VERIFICATION_DISABLING_VARIABLES.includes(name));
|
|
462
|
+
if (authenticatesGithubPackages && tlsDisablingVariable !== undefined) {
|
|
463
|
+
throw new ReleaseStepError(
|
|
464
|
+
`ci.secrets o ci.variables declara ${tlsDisablingVariable}, que desactiva la verificación TLS de Node.js; la instalación de dependencias podría exponer el token de GitHub Packages.`,
|
|
465
|
+
`Quitá ${tlsDisablingVariable} de ci.secrets y ci.variables y volvé a correr el comando; no se cambió ningún archivo.`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (projectGithubPackages.tokenVariable !== null) assertGithubPackagesTokenVariableAllowed(config, projectGithubPackages.tokenVariable, environment.deploymentSecrets);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Applies the allowlist of the variable expanded by the accepted GitHub Packages token (`${NAME}`):
|
|
473
|
+
* - `GITHUB_TOKEN`, which the job binds to the Actions token, unless `ci.variables` declares it (that
|
|
474
|
+
* repository variable would replace the Actions token);
|
|
475
|
+
* - a secret declared in `ci.secrets`, which the job already binds from that secret, unless it is a
|
|
476
|
+
* deployment secret (only the Vercel steps get it, with their own value) or `publication.tokenEnv`
|
|
477
|
+
* (npm would send the publication token to GitHub Packages).
|
|
478
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
479
|
+
* @param {string} tokenVariable - Variable the accepted token expands.
|
|
480
|
+
* @param {readonly string[]} deploymentSecrets - Credentials the deployment steps bind themselves (see {@link describeCiEnvironment}).
|
|
481
|
+
* @returns {void}
|
|
482
|
+
* @throws {ReleaseStepError} When the variable is outside the allowlist.
|
|
483
|
+
*/
|
|
484
|
+
function assertGithubPackagesTokenVariableAllowed(config, tokenVariable, deploymentSecrets) {
|
|
485
|
+
if (tokenVariable === GITHUB_REGISTRY_TOKEN_VARIABLE) {
|
|
486
|
+
if (!(config.ci?.variables ?? []).includes(GITHUB_REGISTRY_TOKEN_VARIABLE)) return;
|
|
487
|
+
throw new ReleaseStepError(
|
|
488
|
+
`El ${PROJECT_NPM_CONFIG_FILE} commiteado usa \${${GITHUB_REGISTRY_TOKEN_VARIABLE}} como token de GitHub Packages y ci.variables declara ${GITHUB_REGISTRY_TOKEN_VARIABLE}; el workflow lo cargaría con esa variable del repositorio en vez del token de Actions y la instalación de GitHub Packages fallaría con 401.`,
|
|
489
|
+
`Quitá ${GITHUB_REGISTRY_TOKEN_VARIABLE} de ci.variables (el workflow le da el token de Actions) y volvé a correr el comando; no se cambió ningún archivo.`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const usesVariable = `usa \${${tokenVariable}} como token de GitHub Packages`;
|
|
493
|
+
const allowedVariableHint = `Usá \${${GITHUB_REGISTRY_TOKEN_VARIABLE}} o declará el secret en ci.secrets`;
|
|
494
|
+
if (!(config.ci?.secrets ?? []).includes(tokenVariable)) {
|
|
495
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `${usesVariable}, que no es ${GITHUB_REGISTRY_TOKEN_VARIABLE} ni un secret declarado en ci.secrets; el workflow no le daría ningún token y la instalación de GitHub Packages fallaría o enviaría otro valor.`, allowedVariableHint);
|
|
496
|
+
}
|
|
497
|
+
if (deploymentSecrets.includes(tokenVariable)) {
|
|
498
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `${usesVariable}, un secret del despliegue de Vercel; solo los pasos de Vercel lo reciben, con su propio valor, así que la instalación de vercel build enviaría ese secret como token de GitHub Packages.`, `${allowedVariableHint} que no sea ${deploymentSecrets.join(", ")}`);
|
|
499
|
+
}
|
|
500
|
+
if (tokenVariable === readPublicationTokenVariable(config)) {
|
|
501
|
+
throwCommittedFileError(PROJECT_NPM_CONFIG_FILE, `${usesVariable}, el mismo nombre que publication.tokenEnv; el workflow lo carga con el secret de publicación y la instalación de dependencias enviaría ese token a GitHub Packages.`, `Usá \${${GITHUB_REGISTRY_TOKEN_VARIABLE}} en el ${PROJECT_NPM_CONFIG_FILE} o renombrá el secret de publicación (publication.tokenEnv)`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Renders the step that stores the GitHub Packages token in the runner's user npm config (`~/.npmrc`),
|
|
507
|
+
* so the dependency install and the later `vercel build` install can read GitHub Packages. pnpm ignores
|
|
508
|
+
* credentials in the repository `.npmrc`, so the job-level binding alone is not enough. The stored token is
|
|
509
|
+
* the one the committed `.npmrc` expands: its own secret when that variable is declared in `ci.secrets`
|
|
510
|
+
* (other than `GITHUB_TOKEN`), otherwise the built-in Actions token. Bindings that move that config are
|
|
511
|
+
* rejected by {@link assertGithubPackagesSetupSupported}.
|
|
512
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
513
|
+
* @param {string | null} tokenVariable - Variable expanded by the accepted host token, or `null`.
|
|
514
|
+
* @returns {string} Workflow step lines.
|
|
515
|
+
*/
|
|
516
|
+
function renderGithubPackagesAuthenticationStep(config, tokenVariable) {
|
|
517
|
+
const usesOwnSecret = tokenVariable !== null && tokenVariable !== GITHUB_REGISTRY_TOKEN_VARIABLE && (config.ci?.secrets ?? []).includes(tokenVariable);
|
|
518
|
+
const tokenExpression = usesOwnSecret ? `secrets.${tokenVariable}` : "github.token";
|
|
519
|
+
return [
|
|
520
|
+
` - name: ${CI_GITHUB_PACKAGES_AUTHENTICATION_STEP}`, " env:", ` ${CI_GITHUB_PACKAGES_TOKEN_VARIABLE}: \${{ ${tokenExpression} }}`,
|
|
521
|
+
` run: printf '${CI_GITHUB_PACKAGES_AUTH_TOKEN_KEY}=%s\\n' "$${CI_GITHUB_PACKAGES_TOKEN_VARIABLE}" >> "${CI_RUNNER_NPM_USER_CONFIG_PATH}"`,
|
|
522
|
+
].join("\n");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Rejects a GitHub Packages setup the generated workflow cannot authenticate: unsupported committed
|
|
527
|
+
* `.npmrc` entries (see {@link assertProjectNpmConfigEntriesSupported}) and bindings or references
|
|
528
|
+
* it cannot resolve (see {@link assertGithubPackagesSetupSupported}). It applies only to the generated
|
|
529
|
+
* workflow; a customized workflow authenticates on its own. Only npm and pnpm are authenticated: Yarn and
|
|
530
|
+
* Bun with any GitHub Packages signal are rejected (see {@link assertPackageManagerAuthenticatesGithubPackages}).
|
|
531
|
+
* With the authentication step, TLS settings, lockfiles and, for pnpm, `pnpm-workspace.yaml` are checked too.
|
|
532
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
533
|
+
* @param {ResolvedCreateVersionConfig} config - Project configuration.
|
|
534
|
+
* @param {ReturnType<typeof describeCiEnvironment>} environment - Worker bindings.
|
|
535
|
+
* @param {ProjectGithubPackagesConfig} projectGithubPackages - Committed `.npmrc` description.
|
|
536
|
+
* @param {boolean} authenticatesGithubPackages - Whether the workflow adds the token step (see {@link readsGithubPackages}).
|
|
537
|
+
* @returns {Promise<void>}
|
|
538
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD or the setup is unsupported.
|
|
539
|
+
*/
|
|
540
|
+
async function assertGeneratedGithubPackagesSetupSupported(repositoryRoot, config, environment, projectGithubPackages, authenticatesGithubPackages) {
|
|
541
|
+
assertProjectNpmConfigEntriesSupported(projectGithubPackages);
|
|
542
|
+
await assertPackageManagerAuthenticatesGithubPackages(repositoryRoot, config, authenticatesGithubPackages);
|
|
543
|
+
assertGithubPackagesSetupSupported(config, environment, projectGithubPackages, authenticatesGithubPackages);
|
|
544
|
+
if (!authenticatesGithubPackages) return;
|
|
545
|
+
assertProjectNpmConfigVerifiesTls(projectGithubPackages);
|
|
546
|
+
const workspaceConfigFiles = config.commands.packageManager === PACKAGE_MANAGER.pnpm ? [PNPM_WORKSPACE_CONFIG_FILE] : [];
|
|
547
|
+
assertCommittedPackageManagerFilesSupported(await readCommittedPackageManagerFiles(repositoryRoot, [...workspaceConfigFiles, ...LOCKFILE_NAMES]));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Tells whether the generated workflow hands the dependency install the GitHub Packages token.
|
|
552
|
+
* @param {{ githubToken: boolean }} environment - Worker bindings from {@link describeCiEnvironment}.
|
|
553
|
+
* @param {ProjectGithubPackagesConfig} projectGithubPackages - Committed `.npmrc` description.
|
|
554
|
+
* @returns {boolean} True when a declared `GITHUB_TOKEN` or the committed `.npmrc` selects GitHub Packages.
|
|
555
|
+
*/
|
|
556
|
+
function readsGithubPackages(environment, projectGithubPackages) {
|
|
557
|
+
return environment.githubToken || projectGithubPackages.usesGithubPackages;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Renders the steps that install the package manager and the project dependencies, before the GitHub
|
|
562
|
+
* Packages authentication is prepended.
|
|
563
|
+
* @param {ResolvedCreateVersionConfig["commands"]["packageManager"]} manager - Package manager of the project.
|
|
564
|
+
* @param {string | null} pinnedVersion - Version pinned by `packageManager`, or `null`.
|
|
565
|
+
* @param {string[]} npmOidcClientSetup - Step lines that install the npm trusted publishing client, or none.
|
|
566
|
+
* @returns {string} Workflow step lines.
|
|
567
|
+
*/
|
|
568
|
+
function renderPackageSetup(manager, pinnedVersion, npmOidcClientSetup) {
|
|
569
|
+
if (manager === PACKAGE_MANAGER.pnpm) {
|
|
570
|
+
return [" - name: Configurar pnpm", " uses: pnpm/action-setup@v4", " with:", ` version: '${pinnedVersion ?? CI_DEFAULT_PNPM_VERSION}'`, " - name: Instalar dependencias", " run: pnpm install --frozen-lockfile"].join("\n");
|
|
571
|
+
}
|
|
572
|
+
if (manager === PACKAGE_MANAGER.bun) {
|
|
573
|
+
return [" - name: Configurar Bun", " uses: oven-sh/setup-bun@v2", ...(pinnedVersion ? [" with:", ` bun-version: '${pinnedVersion}'`] : []), " - name: Instalar dependencias", " run: bun install --frozen-lockfile"].join("\n");
|
|
574
|
+
}
|
|
575
|
+
if (manager === PACKAGE_MANAGER.yarn) {
|
|
576
|
+
// Yarn 2+ (Berry) ships as @yarnpkg/cli-dist and replaced --frozen-lockfile with --immutable.
|
|
577
|
+
const yarnBerry = pinnedVersion !== null && Number(pinnedVersion.split(".")[0]) > 1;
|
|
578
|
+
return [" - name: Configurar Yarn", ` run: npm install --global ${yarnBerry ? "@yarnpkg/cli-dist" : "yarn"}@${pinnedVersion ?? CI_DEFAULT_YARN_VERSION}`, " - name: Instalar dependencias", ` run: yarn install ${yarnBerry ? "--immutable" : "--frozen-lockfile"}`].join("\n");
|
|
579
|
+
}
|
|
580
|
+
return [...(pinnedVersion ? [" - name: Configurar npm", ` run: npm install --global npm@${pinnedVersion}`] : []), " - name: Instalar dependencias", " run: npm ci", ...npmOidcClientSetup].join("\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Renders the Vercel steps of a workflow that deploys after the worker: the CLI setup, and the
|
|
585
|
+
* production pull, build and deploy, each with its own credentials.
|
|
586
|
+
* @param {boolean} deploysToVercel - Whether `ci.deployment` is Vercel.
|
|
587
|
+
* @returns {{ deploymentSetup: string, deployment: string }} Workflow step lines, empty without Vercel.
|
|
588
|
+
*/
|
|
589
|
+
function renderVercelSteps(deploysToVercel) {
|
|
590
|
+
if (!deploysToVercel) return { deploymentSetup: "", deployment: "" };
|
|
591
|
+
const vercelEnvironment = CI_VERCEL_SECRETS.map((name) => ` ${name}: \${{ secrets.${name} }}`).join("\n");
|
|
592
|
+
return {
|
|
593
|
+
deploymentSetup: [" - name: Configurar Vercel", ` run: npm install --global vercel@${CI_VERCEL_CLI_VERSION}`].join("\n"),
|
|
594
|
+
// Production values are pulled only after checks, migrations and hooks finish, so release code never receives them.
|
|
595
|
+
deployment: [" - name: Obtener entorno de producción", " env:", vercelEnvironment, " run: |", ' vercel pull --yes --environment=production --token="$VERCEL_TOKEN"', " cp .vercel/.env.production.local .env"," - name: Construir artefacto de producción", " env:", vercelEnvironment, ' run: vercel build --prod --token="$VERCEL_TOKEN"', " - name: Desplegar producción después de los checks", " env:", vercelEnvironment, ' run: vercel deploy --prebuilt --prod --token="$VERCEL_TOKEN"'].join("\n"),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Renders the release workflow and keeps the GitHub Packages description it was rendered from, so a caller
|
|
601
|
+
* can compare the workflow first and validate that setup afterwards without describing it twice.
|
|
602
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
603
|
+
* @param {ResolvedCreateVersionConfig} config - Release hooks and package manager.
|
|
604
|
+
* @returns {Promise<{ content: string, assertGithubPackagesSetupSupported: () => Promise<void> }>} GitHub Actions YAML,
|
|
605
|
+
* and the check that the generated workflow can authenticate the project's GitHub Packages setup.
|
|
606
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD or the worker bindings are invalid.
|
|
607
|
+
*/
|
|
608
|
+
export async function prepareCiReleaseWorkflow(repositoryRoot, config) {
|
|
609
|
+
const manifest = readJsonManifest(repositoryRoot);
|
|
610
|
+
const manager = config.commands.packageManager;
|
|
611
|
+
const pinnedVersion = extractPinnedPackageManagerVersion(manifest);
|
|
612
|
+
const nodeVersion = await describeCiNodeVersion(repositoryRoot);
|
|
613
|
+
const npmOidcClientSetup = requiresCiNpmOidcClient(config, pinnedVersion) ? [" - name: Configurar npm para trusted publishing", ` run: npm install --global npm@${NPM_OIDC_MINIMUM_VERSION}`] : [];
|
|
614
|
+
const nodeSetup = [
|
|
615
|
+
" - name: Configurar Node.js", " uses: actions/setup-node@v4", " with:",
|
|
616
|
+
nodeVersion.pinnedFile ? ` node-version-file: '${nodeVersion.pinnedFile}'` : ` node-version: '${nodeVersion.version}'`,
|
|
617
|
+
// npm projects install their dependencies with the bundled client and get this one afterwards.
|
|
618
|
+
...(manager === PACKAGE_MANAGER.npm ? [] : npmOidcClientSetup),
|
|
619
|
+
].join("\n");
|
|
620
|
+
const environment = describeCiEnvironment(config);
|
|
621
|
+
const projectGithubPackages = await describeProjectGithubPackagesConfig(repositoryRoot);
|
|
622
|
+
const authenticatesGithubPackages = readsGithubPackages(environment, projectGithubPackages);
|
|
623
|
+
const packageSetup = renderPackageSetup(manager, pinnedVersion, npmOidcClientSetup);
|
|
624
|
+
const authenticatedPackageSetup = authenticatesGithubPackages ? `${renderGithubPackagesAuthenticationStep(config, projectGithubPackages.tokenVariable)}\n${packageSetup}` : packageSetup;
|
|
625
|
+
const readsPublicationHistory = requiresCiPublicationHistory(config);
|
|
626
|
+
// The worker reads its own Actions history, and npm prefers a project `${GITHUB_TOKEN}` host token over ~/.npmrc;
|
|
627
|
+
// any other accepted token variable is a declared secret, already bound from it.
|
|
628
|
+
const bindsGithubToken = environment.githubToken || readsPublicationHistory || projectGithubPackages.tokenVariable === GITHUB_REGISTRY_TOKEN_VARIABLE;
|
|
629
|
+
// Deployment secrets are bound only on their own steps; a project credential expanding one is rejected by the validation.
|
|
630
|
+
const jobSecrets = environment.secrets.filter((name) => !environment.deploymentSecrets.includes(name));
|
|
631
|
+
const bindings = [
|
|
632
|
+
...jobSecrets.map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
633
|
+
...environment.variables.map((name) => ` ${name}: \${{ vars.${name} }}`),
|
|
634
|
+
...(bindsGithubToken ? [CI_GITHUB_TOKEN_JOB_BINDING] : []),
|
|
635
|
+
];
|
|
636
|
+
const permissions = [
|
|
637
|
+
...(readsPublicationHistory ? [" actions: read"] : []),
|
|
638
|
+
...(config.publication.authentication === OIDC_AUTHENTICATION ? [" id-token: write"] : []),
|
|
639
|
+
...(authenticatesGithubPackages ? [` packages: ${environment.githubTokenWrite ? "write" : "read"}`] : []),
|
|
640
|
+
].join("\n");
|
|
641
|
+
const releaseCommand = `${describeCreateVersionWithOptions(config.commands)} --${CREATE_VERSION_FLAG.ciRelease} "$RELEASE_TAG"`;
|
|
642
|
+
const { deploymentSetup, deployment } = renderVercelSteps(config.ci?.deployment === CI_VERCEL_DEPLOYMENT);
|
|
643
|
+
const deno = config.publication.jsrClient === CI_DENO_JSR_CLIENT && config.publish === JSR_REGISTRY_PROVIDER ? [" - name: Configurar Deno para JSR", " uses: denoland/setup-deno@v2", " with:", ` deno-version: '${CI_DENO_VERSION}'`].join("\n") : "";
|
|
644
|
+
const content = readFileSync(new URL(CI_RELEASE_TEMPLATE_FILE, import.meta.url), "utf8")
|
|
645
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.identity, () => renderCiReleaseIdentityStep())
|
|
646
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.node, nodeSetup)
|
|
647
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.install, authenticatedPackageSetup)
|
|
648
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.environment, bindings.join("\n"))
|
|
649
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.permissions, permissions)
|
|
650
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.command, releaseCommand)
|
|
651
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.deploymentSetup, deploymentSetup)
|
|
652
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.deployment, deployment)
|
|
653
|
+
.replace(CI_TEMPLATE_PLACEHOLDER.deno, deno);
|
|
654
|
+
return {
|
|
655
|
+
content,
|
|
656
|
+
assertGithubPackagesSetupSupported: () => assertGeneratedGithubPackagesSetupSupported(repositoryRoot, config, environment, projectGithubPackages, authenticatesGithubPackages),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Renders a workflow that installs the project's package manager and finishes a pinned release.
|
|
662
|
+
* @param {string} repositoryRoot - Project checkout.
|
|
663
|
+
* @param {ResolvedCreateVersionConfig} config - Release hooks and package manager.
|
|
664
|
+
* @param {{ validateGithubPackages?: boolean }} [options] - `validateGithubPackages: false` only renders,
|
|
665
|
+
* for comparisons against a committed workflow that may be customized and authenticate on its own.
|
|
666
|
+
* @returns {Promise<string>} GitHub Actions YAML.
|
|
667
|
+
* @throws {ReleaseStepError} When Git cannot list or read HEAD or, while validating, the GitHub Packages
|
|
668
|
+
* setup is unsupported (see {@link assertGeneratedGithubPackagesSetupSupported}).
|
|
669
|
+
*/
|
|
670
|
+
export async function renderCiReleaseWorkflow(repositoryRoot, config, { validateGithubPackages = true } = {}) {
|
|
671
|
+
const workflow = await prepareCiReleaseWorkflow(repositoryRoot, config);
|
|
672
|
+
if (validateGithubPackages) await workflow.assertGithubPackagesSetupSupported();
|
|
673
|
+
return workflow.content;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Prepares a named CI export and a new workflow without replacing existing project code.
|
|
678
|
+
* Syntax is checked with Node before anything in the project is written.
|
|
679
|
+
* @param {string} repositoryRoot - Checkout whose configuration is already loaded.
|
|
680
|
+
* @param {ResolvedCreateVersionConfig} config - Configuration selecting the default workflow.
|
|
681
|
+
* @param {boolean} [writeConfiguration] - Add a named CI export only when settings were absent.
|
|
682
|
+
* @returns {Promise<ReleaseFileUpdate[]>} Files to include atomically with the bump.
|
|
683
|
+
* @throws {ReleaseStepError} When an existing workflow would be overwritten or configuration cannot be extended.
|
|
684
|
+
*/
|
|
685
|
+
export async function prepareCiSetupFiles(repositoryRoot, config, writeConfiguration = true) {
|
|
686
|
+
const workflowPath = ciWorkflowPath(config.ci?.workflow ?? DEFAULT_CI_WORKFLOW);
|
|
687
|
+
assertSafeCiPath(repositoryRoot, workflowPath);
|
|
688
|
+
if (existsSync(path.join(repositoryRoot, workflowPath))) throw new ReleaseStepError(`${workflowPath} ya existe; no se reemplazó.`, "Configurá ci.workflow en astra-rp.config.(m)js y verificá que reciba version, tag y sha y ejecute --ci-release.");
|
|
689
|
+
const configFile = CREATE_VERSION_CONFIG_FILES.find((fileName) => existsSync(path.join(repositoryRoot, fileName)));
|
|
690
|
+
if (!configFile || !lstatSync(path.join(repositoryRoot, configFile)).isFile()) throw new ReleaseStepError("No se pudo encontrar una configuración regular para agregar CI.", "Configurá ci.workflow manualmente en astra-rp.config.(m)js.");
|
|
691
|
+
const originalBytes = readFileSync(path.join(repositoryRoot, configFile));
|
|
692
|
+
const content = `${originalBytes.toString("utf8").trimEnd()}\n\n/** GitHub Actions workflow used by create-version. */\nexport const ${CI_CONFIG_EXPORT} = ${JSON.stringify(config.ci)};\n`;
|
|
693
|
+
const temporaryDirectory = mkdtempSync(path.join(tmpdir(), "astra-rp-ci-config-"));
|
|
694
|
+
try {
|
|
695
|
+
const candidate = path.join(temporaryDirectory, "config.mjs");
|
|
696
|
+
writeFileSync(candidate, content);
|
|
697
|
+
const checked = await runCaptured(process.execPath, ["--check", candidate]);
|
|
698
|
+
if (writeConfiguration && checked.status !== 0) throw new ReleaseStepError("La configuración existente no admite el export ci automático.", "Agregá ci: { workflow: 'release.yml' } a su export default; no se cambió ningún archivo.");
|
|
699
|
+
} finally {
|
|
700
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
701
|
+
}
|
|
702
|
+
/** @type {ReleaseFileUpdate[]} */
|
|
703
|
+
const updates = [
|
|
704
|
+
...(writeConfiguration ? [{ filePath: configFile, originalBytes, content }] : []),
|
|
705
|
+
{ filePath: workflowPath, originalBytes: null, content: await renderCiReleaseWorkflow(repositoryRoot, config) },
|
|
706
|
+
];
|
|
707
|
+
if (config.ci?.deployment === CI_VERCEL_DEPLOYMENT) {
|
|
708
|
+
for (const fileName of [".gitignore", CI_VERCEL_CONFIG_FILE, CI_VERCEL_GATE_FILE]) assertSafeCiPath(repositoryRoot, fileName);
|
|
709
|
+
await assertVercelWorkerPathsUntracked(repositoryRoot);
|
|
710
|
+
const ignored = path.join(repositoryRoot, ".gitignore");
|
|
711
|
+
const originalIgnoredBytes = existsSync(ignored) ? readFileSync(ignored) : null;
|
|
712
|
+
updates.push({ filePath: ".gitignore", originalBytes: originalIgnoredBytes, content: `${originalIgnoredBytes?.toString("utf8").trimEnd() ?? ""}\n.vercel/\n.env\n` });
|
|
713
|
+
const vercelPath = path.join(repositoryRoot, CI_VERCEL_CONFIG_FILE);
|
|
714
|
+
const originalVercelBytes = readFileSync(vercelPath);
|
|
715
|
+
const vercel = JSON.parse(originalVercelBytes.toString("utf8"));
|
|
716
|
+
if (vercel.ignoreCommand !== undefined && typeof vercel.ignoreCommand !== "string") throw new ReleaseStepError("vercel.json tiene un ignoreCommand que no es un comando válido.", "Corregilo antes de configurar CI; no se cambió ningún archivo.");
|
|
717
|
+
if (existsSync(path.join(repositoryRoot, CI_VERCEL_GATE_FILE))) throw new ReleaseStepError(`${CI_VERCEL_GATE_FILE} ya existe; no se reemplazó.`, "Revisá la configuración de CI existente.");
|
|
718
|
+
const gate = readFileSync(new URL(CI_VERCEL_GATE_TEMPLATE, import.meta.url), "utf8").replace(CI_VERCEL_IGNORE_PLACEHOLDER, () => JSON.stringify(vercel.ignoreCommand ?? null));
|
|
719
|
+
updates.push({ filePath: CI_VERCEL_GATE_FILE, originalBytes: null, content: gate });
|
|
720
|
+
updates.push({ filePath: CI_VERCEL_CONFIG_FILE, originalBytes: originalVercelBytes, content: `${JSON.stringify({ ...vercel, ignoreCommand: `node ${CI_VERCEL_GATE_FILE}` }, null, 2)}\n` });
|
|
721
|
+
}
|
|
722
|
+
if (updates.some((update) => config.versionFiles.includes(update.filePath))) throw new ReleaseStepError("Un archivo de la configuración automática de CI también figura en versionFiles.", "Quitalo de versionFiles antes de configurar CI; no se cambió nada.");
|
|
723
|
+
for (const update of updates.filter((file) => file.originalBytes !== null)) {
|
|
724
|
+
const literalPath = `${GIT_LITERAL_PATHSPEC_PREFIX}${update.filePath}`;
|
|
725
|
+
const tracked = await runCaptured("git", ["ls-files", "--error-unmatch", "--", literalPath], { cwd: repositoryRoot });
|
|
726
|
+
const clean = await runCaptured("git", ["diff", "--quiet", "HEAD", "--", literalPath], { cwd: repositoryRoot });
|
|
727
|
+
if (tracked.status !== 0 || clean.status !== 0) throw new ReleaseStepError(`La configuración automática de CI necesita ${update.filePath} commiteado y limpio.`, "Commiteá esos ajustes o guardalos con git stash antes de configurar CI; --ignore-local-changes no los incluirá en el release.");
|
|
728
|
+
}
|
|
729
|
+
return updates;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Rejects Vercel setup when the worker would overwrite tracked files after checkout: ignoring them
|
|
734
|
+
* afterwards does not untrack them, so `--ci-release` would find a dirty tree on every run of the tag.
|
|
735
|
+
* @param {string} repositoryRoot - Checkout about to receive the Vercel workflow.
|
|
736
|
+
* @returns {Promise<void>}
|
|
737
|
+
* @throws {ReleaseStepError} When a path written by `vercel pull` in the worker is tracked by Git.
|
|
738
|
+
*/
|
|
739
|
+
async function assertVercelWorkerPathsUntracked(repositoryRoot) {
|
|
740
|
+
const literalPaths = CI_VERCEL_WORKER_WRITTEN_PATHS.map((filePath) => `${GIT_LITERAL_PATHSPEC_PREFIX}${filePath}`);
|
|
741
|
+
const listed = await runCaptured("git", ["ls-files", "-z", "--", ...literalPaths], { cwd: repositoryRoot });
|
|
742
|
+
if (listed.status !== 0) throw new ReleaseStepError(`No se pudo comprobar si ${CI_VERCEL_WORKER_WRITTEN_PATHS.join(" o ")} están versionados (git ls-files terminó con status ${listed.status}).`, "Revisá el repositorio Git antes de configurar CI; no se cambió ningún archivo.");
|
|
743
|
+
const trackedPaths = listed.stdout.split("\0").filter(Boolean);
|
|
744
|
+
if (trackedPaths.length > 0) throw new ReleaseStepError(`El worker de Vercel sobrescribe archivos versionados (${trackedPaths.join(", ")}); el release en CI quedaría con cambios locales y se bloquearía.`, `Quitalos del índice con git rm -r --cached -- ${trackedPaths.join(" ")} (la copia local se conserva), commiteá y volvé a configurar CI; no se cambió ningún archivo.`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Records the execution mode for the early Vercel build gate without importing project dependencies.
|
|
749
|
+
* @param {string} repositoryRoot - Project root.
|
|
750
|
+
* @param {string} version - Version of the release being committed.
|
|
751
|
+
* @param {"local" | "ci"} execution - Execution location chosen for this release.
|
|
752
|
+
* @returns {ReleaseFileUpdate} Version-scoped metadata included in the bump.
|
|
753
|
+
*/
|
|
754
|
+
export function prepareCiReleaseMetadata(repositoryRoot, version, execution) {
|
|
755
|
+
assertSafeCiPath(repositoryRoot, CI_RELEASE_METADATA_FILE);
|
|
756
|
+
const metadataPath = path.join(repositoryRoot, CI_RELEASE_METADATA_FILE);
|
|
757
|
+
return { filePath: CI_RELEASE_METADATA_FILE, originalBytes: existsSync(metadataPath) ? readFileSync(metadataPath) : null, content: `${JSON.stringify({ version, execution })}\n` };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Prevents concurrent editor changes from being overwritten after an interactive version prompt.
|
|
762
|
+
* @param {string} repositoryRoot - Checkout about to receive the bump.
|
|
763
|
+
* @param {ReleaseFileUpdate[]} updates - CI files captured before prompting.
|
|
764
|
+
* @returns {void}
|
|
765
|
+
* @throws {ReleaseStepError} When a captured file changed or a new workflow appeared.
|
|
766
|
+
*/
|
|
767
|
+
export function assertCiSetupFilesUnchanged(repositoryRoot, updates) {
|
|
768
|
+
for (const update of updates) {
|
|
769
|
+
assertSafeCiPath(repositoryRoot, update.filePath);
|
|
770
|
+
const target = path.join(repositoryRoot, update.filePath);
|
|
771
|
+
const current = existsSync(target) ? readFileSync(target) : null;
|
|
772
|
+
if (update.originalBytes === null ? current !== null : current === null || !update.originalBytes.equals(current)) throw new ReleaseStepError(`${update.filePath} cambió después de preparar la configuración de CI.`, "Conservá esos cambios y volvé a correr el comando; no se escribió el bump ni se reemplazó la configuración.");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Rejects symlinks anywhere in an owned setup path before writes can leave the checkout.
|
|
778
|
+
* @param {string} repositoryRoot - Trusted checkout root.
|
|
779
|
+
* @param {string} relativePath - Constant or validated relative file path.
|
|
780
|
+
* @returns {void}
|
|
781
|
+
* @throws {ReleaseStepError} When an existing path component is a symlink.
|
|
782
|
+
*/
|
|
783
|
+
export function assertSafeCiPath(repositoryRoot, relativePath) {
|
|
784
|
+
let current = repositoryRoot;
|
|
785
|
+
for (const segment of relativePath.split("/")) {
|
|
786
|
+
current = path.join(current, segment);
|
|
787
|
+
let entry;
|
|
788
|
+
try { entry = lstatSync(current); } catch (error) {
|
|
789
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") break;
|
|
790
|
+
throw new ReleaseStepError(`No se pudo comprobar la ruta ${relativePath} para CI.`, "Revisá los permisos antes de configurar CI; no se escribió nada.", { cause: error });
|
|
791
|
+
}
|
|
792
|
+
if (entry.isSymbolicLink()) throw new ReleaseStepError(`${relativePath} contiene un enlace simbólico.`, "Usá archivos y directorios regulares para configurar CI; no se escribió nada.");
|
|
793
|
+
}
|
|
794
|
+
}
|