@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
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@astra-kit/rp`: dependency-free release process.
|
|
3
|
+
*
|
|
4
|
+
* - `@astra-kit/rp/versions`: stable `X.Y.Z` rules; only the next patch, minor or major is allowed.
|
|
5
|
+
* - `@astra-kit/rp/build-gate`: Vercel `ignoreCommand` decision built on those rules.
|
|
6
|
+
* - `@astra-kit/rp/changelog`: release history generated from Git commits.
|
|
7
|
+
* - `@astra-kit/rp/guard-publish`: `prepublishOnly` guard that blocks pnpm, yarn and bun publications.
|
|
8
|
+
* - `@astra-kit/rp/package-manager`: detection of pnpm, bun, npm or yarn and the commands each one runs.
|
|
9
|
+
* - `@astra-kit/rp/version-files`: rewrites the marked version lines of `versionFiles`.
|
|
10
|
+
* - `@astra-kit/rp/terminal-ui`: boxes, spinners and prompts for release commands.
|
|
11
|
+
* - `@astra-kit/rp/testing`: shared version bump fixtures for consumer test suites.
|
|
12
|
+
* - `@astra-kit/rp/constants`: every constant above, grouped by domain.
|
|
13
|
+
*
|
|
14
|
+
* @module @astra-kit/rp
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export * from "./build-gate.js";
|
|
18
|
+
export * from "./changelog.js";
|
|
19
|
+
export * from "./guard-publish.js";
|
|
20
|
+
export * from "./package-manager.js";
|
|
21
|
+
export * from "./version-files.js";
|
|
22
|
+
export * from "./versions.js";
|
|
23
|
+
export {
|
|
24
|
+
ICON,
|
|
25
|
+
countTerminalRows,
|
|
26
|
+
formatDuration,
|
|
27
|
+
getPromptWaitMs,
|
|
28
|
+
measureActiveMs,
|
|
29
|
+
paint,
|
|
30
|
+
print,
|
|
31
|
+
renderBanner,
|
|
32
|
+
renderBox,
|
|
33
|
+
renderRow,
|
|
34
|
+
renderStepHeader,
|
|
35
|
+
resolveBoxWidth,
|
|
36
|
+
resolveNumberKey,
|
|
37
|
+
select,
|
|
38
|
+
startSpinner,
|
|
39
|
+
visibleWidth,
|
|
40
|
+
wrapStyledLine,
|
|
41
|
+
} from "./terminal-ui.js";
|
|
42
|
+
export * from "./constants/index.js";
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure planning of a monorepo release. Receives the snapshot of `state.js`
|
|
3
|
+
* and decides, without touching Git or npm, what is still missing:
|
|
4
|
+
*
|
|
5
|
+
* - a new release: the packages with commits under their paths are the
|
|
6
|
+
* candidates; the run asks the version of each one (or skips it) before
|
|
7
|
+
* verifying manual changelogs, applying migrations, running the checks and creating one release commit with a tag
|
|
8
|
+
* per package (which closes the `[Unreleased]` section of every released changelog), prepares, pushes and publishes in dependency order;
|
|
9
|
+
* - the resume of a local release commit that never reached `origin`, or of
|
|
10
|
+
* tagged releases on `origin/main` that npm does not have yet;
|
|
11
|
+
* - nothing, when no package changed;
|
|
12
|
+
* - blockers explaining what to fix first.
|
|
13
|
+
*
|
|
14
|
+
* @module monorepo/plan
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
CREATE_VERSION_FLAG,
|
|
19
|
+
MAIN_BRANCH,
|
|
20
|
+
MAX_LISTED_ITEMS,
|
|
21
|
+
NPM_LOOKUP_STATUS,
|
|
22
|
+
RELEASE_MODE,
|
|
23
|
+
RELEASE_REMOTE,
|
|
24
|
+
RELEASE_STEP,
|
|
25
|
+
REMOTE_MAIN_REF,
|
|
26
|
+
} from "../constants/create-version.js";
|
|
27
|
+
import { MONOREPO_RELEASE_STEP } from "../constants/monorepo.js";
|
|
28
|
+
import {
|
|
29
|
+
addMigrationsPlanStep,
|
|
30
|
+
changelogUpdateRequiredBlocker,
|
|
31
|
+
classifyNpmAuth,
|
|
32
|
+
codeChangesToSetAsideBlocker,
|
|
33
|
+
failedNpmLookupBlocker,
|
|
34
|
+
featureBranchBlocker,
|
|
35
|
+
foreignCommitsBlocker,
|
|
36
|
+
hasFailedNpmLookup,
|
|
37
|
+
isCodeChange,
|
|
38
|
+
listPorcelainPaths,
|
|
39
|
+
missingChecksBlocker,
|
|
40
|
+
syncMainPlanStep,
|
|
41
|
+
uncommittedFilesBlocker,
|
|
42
|
+
unreleasedEntriesRequiredBlocker,
|
|
43
|
+
} from "../create-version/plan.js";
|
|
44
|
+
import { DEFAULT_PROJECT_COMMANDS } from "../package-manager.js";
|
|
45
|
+
import { findHighestStableVersion, isStableReleaseVersion, isStableVersionAbove } from "../versions.js";
|
|
46
|
+
import { parseMonorepoReleaseSubject } from "./release-commit.js";
|
|
47
|
+
import { formatPackageTag } from "./workspaces.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import("./state.js").MonorepoSnapshot} MonorepoSnapshot
|
|
51
|
+
* @typedef {import("./state.js").PackageSnapshot} PackageSnapshot
|
|
52
|
+
* @typedef {import("../create-version/plan.js").ReleaseCapabilities} ReleaseCapabilities
|
|
53
|
+
* @typedef {import("../create-version/plan.js").ReleasePlanStep} ReleasePlanStep
|
|
54
|
+
* @typedef {import("../create-version/plan.js").ReleaseBlocker} ReleaseBlocker
|
|
55
|
+
* @typedef {import("../package-manager.js").ProjectCommands} ProjectCommands
|
|
56
|
+
* @typedef {{ name: string, version: string, tag: string, commitSha: string, publish: boolean }} PendingPackageRelease
|
|
57
|
+
* A release to finish: its tag, the release commit it points at and whether it still has to be published.
|
|
58
|
+
* @typedef {{
|
|
59
|
+
* mode: string,
|
|
60
|
+
* steps: ReleasePlanStep[],
|
|
61
|
+
* blockers: ReleaseBlocker[],
|
|
62
|
+
* warnings: string[],
|
|
63
|
+
* candidates: string[],
|
|
64
|
+
* pendingReleases: PendingPackageRelease[],
|
|
65
|
+
* }} MonorepoPlan
|
|
66
|
+
* `candidates` are the packages a new release may include; `pendingReleases` what a resume finishes.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {Partial<MonorepoPlan>} plan - Plan fields.
|
|
71
|
+
* @returns {MonorepoPlan} Plan with every list defaulted.
|
|
72
|
+
*/
|
|
73
|
+
function createPlan(plan) {
|
|
74
|
+
return { mode: RELEASE_MODE.blocked, steps: [], blockers: [], warnings: [], candidates: [], pendingReleases: [], ...plan };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tells whether a `git status --porcelain` line changes one of the given files.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} line - Porcelain line.
|
|
81
|
+
* @param {ReadonlySet<string>} paths - Paths relative to the root.
|
|
82
|
+
* @returns {boolean} Whether the line reports one of them.
|
|
83
|
+
*/
|
|
84
|
+
function changesOneOf(line, paths) {
|
|
85
|
+
return listPorcelainPaths(line).some((changedPath) => paths.has(changedPath));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Lists the uncommitted changes a run with `--ignore-local-changes` sets aside: every change,
|
|
90
|
+
* except the package changelogs in a new release (its bump commits them).
|
|
91
|
+
*
|
|
92
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
93
|
+
* @param {string} mode - Planned mode.
|
|
94
|
+
* @returns {string[]} `git status --porcelain` lines to set aside.
|
|
95
|
+
*/
|
|
96
|
+
export function listMonorepoChangesToSetAside(state, mode) {
|
|
97
|
+
const changelogs = new Set(state.packages.map((packageSnapshot) => packageSnapshot.unit.changelogPath));
|
|
98
|
+
return mode === RELEASE_MODE.newRelease ? state.workingTreeChanges.filter((line) => !changesOneOf(line, changelogs)) : state.workingTreeChanges;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Lists the blockers that must be fixed before any step runs.
|
|
103
|
+
*
|
|
104
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
105
|
+
* @param {boolean} ignoreLocalChanges - Whether uncommitted changes are set aside instead of blocking.
|
|
106
|
+
* @param {ProjectCommands} commands - Project commands quoted by the hints.
|
|
107
|
+
* @returns {ReleaseBlocker[]} Blockers, most urgent first.
|
|
108
|
+
*/
|
|
109
|
+
function findBlockers(state, ignoreLocalChanges, commands) {
|
|
110
|
+
if (!state.currentBranch) {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
title: "HEAD está desacoplado (detached)",
|
|
114
|
+
details: [
|
|
115
|
+
`Hacé git switch ${MAIN_BRANCH} y volvé a correr ${commands.createVersion}.`,
|
|
116
|
+
"En un monorepo las publicaciones pendientes también se retoman desde main: cada paquete se publica desde su tag.",
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @type {ReleaseBlocker[]} */
|
|
123
|
+
const blockers = [];
|
|
124
|
+
|
|
125
|
+
if (state.currentBranch !== MAIN_BRANCH) {
|
|
126
|
+
blockers.push(featureBranchBlocker({ ...state, currentBranch: state.currentBranch }, commands));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Without origin/main no package has commits to compare, so the plan would say everything is up to date.
|
|
130
|
+
if (!state.remoteMainExists) {
|
|
131
|
+
blockers.push({
|
|
132
|
+
title: `No existe ${REMOTE_MAIN_REF}: no se puede saber qué cambió en cada paquete`,
|
|
133
|
+
details: [`Subí ${MAIN_BRANCH} con git push -u ${RELEASE_REMOTE} ${MAIN_BRANCH} (o revisá el remoto ${RELEASE_REMOTE}) y volvé a correr ${commands.createVersion}.`],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const blockingChanges = listMonorepoChangesToSetAside(state, RELEASE_MODE.newRelease);
|
|
138
|
+
|
|
139
|
+
if (blockingChanges.length > 0 && !ignoreLocalChanges) {
|
|
140
|
+
blockers.push(uncommittedFilesBlocker(blockingChanges, commands));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const packageSnapshot of state.packages) {
|
|
144
|
+
if (packageSnapshot.npm && hasFailedNpmLookup(packageSnapshot)) {
|
|
145
|
+
blockers.push(failedNpmLookupBlocker(packageSnapshot.npm, packageSnapshot.npmAuth, commands, { subject: ` para ${packageSnapshot.unit.name}`, defaultReason: "npm no respondió" }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return blockers;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Tells whether npm still lacks a version of a package (always `true` when npm is not tracked).
|
|
154
|
+
*
|
|
155
|
+
* @param {PackageSnapshot | undefined} packageSnapshot - Package.
|
|
156
|
+
* @param {string} version - Version.
|
|
157
|
+
* @returns {boolean} Whether the version still has to be published.
|
|
158
|
+
*/
|
|
159
|
+
function isMissingFromNpm(packageSnapshot, version) {
|
|
160
|
+
return packageSnapshot?.npm ? !packageSnapshot.npm.publishedVersions.includes(version) : true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Plans the resume of local release commits that never reached `origin`.
|
|
165
|
+
*
|
|
166
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
167
|
+
* @param {ReleaseCapabilities} capabilities - Project capabilities.
|
|
168
|
+
* @param {string} tagFormat - Tag format.
|
|
169
|
+
* @returns {MonorepoPlan | null} Resume plan, a blocked plan for foreign commits, or `null` when `main` is not ahead.
|
|
170
|
+
*/
|
|
171
|
+
function planLocalResume(state, capabilities, tagFormat) {
|
|
172
|
+
const { aheadCommits } = state.main;
|
|
173
|
+
|
|
174
|
+
if (aheadCommits.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parsed = aheadCommits.map((commit) => ({ commit, releases: parseMonorepoReleaseSubject(commit.subject) }));
|
|
179
|
+
const foreign = parsed.filter(({ releases }) => releases === null);
|
|
180
|
+
|
|
181
|
+
if (foreign.length > 0) {
|
|
182
|
+
return createPlan({ blockers: [foreignCommitsBlocker(foreign.map(({ commit }) => commit))] });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const byName = new Map(state.packages.map((packageSnapshot) => [packageSnapshot.unit.name, packageSnapshot]));
|
|
186
|
+
/** @type {Map<string, PendingPackageRelease>} */
|
|
187
|
+
const pending = new Map();
|
|
188
|
+
|
|
189
|
+
// Newest first: a package released twice locally keeps its newest release.
|
|
190
|
+
for (const { commit, releases } of parsed) {
|
|
191
|
+
for (const release of releases ?? []) {
|
|
192
|
+
const packageSnapshot = byName.get(release.name);
|
|
193
|
+
if (!packageSnapshot || pending.has(release.name)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
pending.set(release.name, {
|
|
197
|
+
name: release.name,
|
|
198
|
+
version: release.version,
|
|
199
|
+
tag: formatPackageTag(tagFormat, packageSnapshot.unit, release.version),
|
|
200
|
+
commitSha: commit.sha ?? "",
|
|
201
|
+
publish: capabilities.publish && isMissingFromNpm(packageSnapshot, release.version),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const pendingReleases = [...pending.values()];
|
|
207
|
+
const listed = pendingReleases.map(({ name, version }) => `${name}@${version}`).join(", ");
|
|
208
|
+
/** @type {ReleasePlanStep[]} */
|
|
209
|
+
const steps = [];
|
|
210
|
+
|
|
211
|
+
if (capabilities.prepare) {
|
|
212
|
+
steps.push({ id: RELEASE_STEP.prepareRelease, title: "Preparar el release", detail: "El commit de release ya existe: se vuelve a preparar lo necesario." });
|
|
213
|
+
}
|
|
214
|
+
steps.push({ id: MONOREPO_RELEASE_STEP.pushPackages, title: `Subir ${MAIN_BRANCH} y los tags a origin`, detail: `El release quedó creado solo en local: ${listed}.` });
|
|
215
|
+
if (pendingReleases.some((release) => release.publish)) {
|
|
216
|
+
steps.push({ id: MONOREPO_RELEASE_STEP.publishPackages, title: capabilities.publishTitle, detail: "Solo los paquetes que todavía no se publicaron." });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return createPlan({ mode: RELEASE_MODE.resume, steps, pendingReleases });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Plans the publication of tagged releases of `origin/main` that npm does not have yet. A version
|
|
224
|
+
* below the highest one published is only warned about: publishing it would move `latest` back.
|
|
225
|
+
*
|
|
226
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
227
|
+
* @param {ReleaseCapabilities} capabilities - Project capabilities.
|
|
228
|
+
* @returns {{ plan: MonorepoPlan | null, warnings: string[] }} Resume plan (or `null` when nothing tagged is
|
|
229
|
+
* missing from npm), and the warnings about missing versions that cannot be published.
|
|
230
|
+
*/
|
|
231
|
+
function planPendingPublications(state, capabilities) {
|
|
232
|
+
if (!capabilities.publish) {
|
|
233
|
+
return { plan: null, warnings: [] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @type {PendingPackageRelease[]} */
|
|
237
|
+
const pendingReleases = [];
|
|
238
|
+
/** @type {string[]} */
|
|
239
|
+
const warnings = [];
|
|
240
|
+
|
|
241
|
+
for (const packageSnapshot of state.packages) {
|
|
242
|
+
const { npm, lastRelease, releasedVersion, unit } = packageSnapshot;
|
|
243
|
+
const version = lastRelease?.version ?? null;
|
|
244
|
+
|
|
245
|
+
if (npm?.status !== NPM_LOOKUP_STATUS.ok || !lastRelease?.tag || !isStableReleaseVersion(version) || version !== releasedVersion) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!lastRelease.tagged || !lastRelease.tagOnOrigin || npm.publishedVersions.includes(/** @type {string} */ (version))) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const highestPublished = findHighestStableVersion(npm.publishedVersions);
|
|
253
|
+
if (highestPublished && !isStableVersionAbove(/** @type {string} */ (version), highestPublished)) {
|
|
254
|
+
warnings.push(`${unit.name}@${version} (${lastRelease.tag}) no está en ${npm.registryLabel ?? "npm"}, pero ${npm.registryLabel ?? "npm"} ya tiene ${highestPublished}: publicarla movería ${npm.tag ?? "latest"} hacia atrás, así que no se publica.`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pendingReleases.push({ name: unit.name, version: /** @type {string} */ (version), tag: lastRelease.tag, commitSha: lastRelease.sha, publish: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (pendingReleases.length === 0) {
|
|
262
|
+
return { plan: null, warnings };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const listed = pendingReleases.map(({ name, version }) => `${name}@${version}`).join(", ");
|
|
266
|
+
const plan = createPlan({
|
|
267
|
+
mode: RELEASE_MODE.resume,
|
|
268
|
+
steps: [
|
|
269
|
+
{
|
|
270
|
+
id: MONOREPO_RELEASE_STEP.publishPackages,
|
|
271
|
+
title: `${capabilities.publishTitle} (${listed})`,
|
|
272
|
+
detail: "Ya están en origin con su tag: se prepara y publica cada uno desde su commit de release.",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
warnings,
|
|
276
|
+
pendingReleases,
|
|
277
|
+
});
|
|
278
|
+
return { plan, warnings };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Warns that `--skip-unpublished` leaves a tagged release out of npm on purpose.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} name - Package name.
|
|
285
|
+
* @param {string} version - Skipped version.
|
|
286
|
+
* @param {string} tag - Its tag.
|
|
287
|
+
* @param {string} [registryLabel] - Selected registry label.
|
|
288
|
+
* @returns {string} Warning.
|
|
289
|
+
*/
|
|
290
|
+
function describeSkippedPublication(name, version, tag, registryLabel = "npm") {
|
|
291
|
+
return `Se saltea ${name}@${version} (tag ${tag}), que no está en ${registryLabel}: el release nuevo sale sin publicarla (--${CREATE_VERSION_FLAG.skipUnpublished}).`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Plans a new release of the packages that changed.
|
|
296
|
+
*
|
|
297
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
298
|
+
* @param {ReleaseCapabilities} capabilities - Project capabilities.
|
|
299
|
+
* @param {ProjectCommands} commands - Project commands quoted by the hints.
|
|
300
|
+
* @param {string[]} warnings - Warnings gathered so far.
|
|
301
|
+
* @param {boolean} selectAllPackages - Whether the flags select every candidate without allowing skips.
|
|
302
|
+
* @returns {MonorepoPlan} Plan.
|
|
303
|
+
*/
|
|
304
|
+
function planNewRelease(state, capabilities, commands, warnings, selectAllPackages) {
|
|
305
|
+
const candidates = state.packages.filter((packageSnapshot) => packageSnapshot.unreleasedCommits.length > 0);
|
|
306
|
+
const candidateNames = candidates.map((packageSnapshot) => packageSnapshot.unit.name);
|
|
307
|
+
|
|
308
|
+
// Before the up-to-date check: the local checkout may not have the changes (or workspaces) origin already has.
|
|
309
|
+
if (state.main.behindCount > 0) {
|
|
310
|
+
return createPlan({
|
|
311
|
+
mode: RELEASE_MODE.newRelease,
|
|
312
|
+
steps: [syncMainPlanStep(state.main.behindCount, commands)],
|
|
313
|
+
warnings,
|
|
314
|
+
candidates: candidateNames,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (candidates.length === 0) {
|
|
319
|
+
return createPlan({ mode: RELEASE_MODE.upToDate, warnings });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (capabilities.checksMissing) {
|
|
323
|
+
return createPlan({ blockers: [missingChecksBlocker(commands)] });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// A changelog that cannot be released: not updated since the last release, or without entries in `[Unreleased]`.
|
|
327
|
+
const changelogBlockers = candidates.flatMap(({ changelog, unit }) => {
|
|
328
|
+
const blocker = changelog.updated === true ? unreleasedEntriesRequiredBlocker(changelog, unit.changelogPath, commands) : changelogUpdateRequiredBlocker(changelog.reason, unit.changelogPath, commands);
|
|
329
|
+
return blocker ? [{ unit, blocker }] : [];
|
|
330
|
+
});
|
|
331
|
+
if (selectAllPackages && changelogBlockers.length > 0) {
|
|
332
|
+
return createPlan({ blockers: changelogBlockers.map(({ blocker }) => blocker), warnings });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The versions are chosen first: skipping every package must not leave migrations applied.
|
|
336
|
+
/** @type {ReleasePlanStep[]} */
|
|
337
|
+
const steps = [
|
|
338
|
+
{
|
|
339
|
+
id: MONOREPO_RELEASE_STEP.chooseVersions,
|
|
340
|
+
title: `Elegir la versión de cada paquete con cambios (${candidates.length})`,
|
|
341
|
+
detail: candidateNames.join(", "),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: MONOREPO_RELEASE_STEP.verifyChangelogs,
|
|
345
|
+
title: "Verificar los CHANGELOG elegidos y sus entradas en [Unreleased]",
|
|
346
|
+
detail: "Cada uno debe haber cambiado desde su último release y tener entradas en [Unreleased]; el commit de release cierra esa sección.",
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
for (const { unit, blocker } of changelogBlockers) {
|
|
351
|
+
warnings.push(`${blocker.title}; si elegís publicar ${unit.name}, el release se corta. Actualizalo manualmente.`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
addMigrationsPlanStep(state.migrations, steps, warnings);
|
|
355
|
+
|
|
356
|
+
if (capabilities.checks) {
|
|
357
|
+
steps.push({ id: RELEASE_STEP.runChecks, title: "Validar el proyecto", detail: "Corre los checks configurados antes de tocar las versiones." });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
steps.push({
|
|
361
|
+
id: MONOREPO_RELEASE_STEP.bumpPackages,
|
|
362
|
+
title: "Crear el commit de release y un tag por paquete",
|
|
363
|
+
detail: "Actualiza los package.json y cierra [Unreleased] de cada CHANGELOG como ## [X.Y.Z] - AAAA-MM-DD, con un [Unreleased] vacío arriba.",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (capabilities.prepare) {
|
|
367
|
+
steps.push({ id: RELEASE_STEP.prepareRelease, title: "Preparar el release", detail: "Corre la preparación configurada sobre el commit de release." });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
steps.push({ id: MONOREPO_RELEASE_STEP.pushPackages, title: `Subir ${MAIN_BRANCH} y los tags a origin` });
|
|
371
|
+
|
|
372
|
+
if (capabilities.publish) {
|
|
373
|
+
steps.push({ id: MONOREPO_RELEASE_STEP.publishPackages, title: capabilities.publishTitle, detail: "En orden de dependencias entre los paquetes publicados." });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return createPlan({ mode: RELEASE_MODE.newRelease, steps, warnings, candidates: candidateNames });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Stops a plan whose publications would fail on the npm credentials, or warns when they could not
|
|
381
|
+
* be verified or a first publication cannot be told apart from a hidden private package.
|
|
382
|
+
*
|
|
383
|
+
* @param {MonorepoPlan} plan - Plan.
|
|
384
|
+
* @param {MonorepoSnapshot} state - Snapshot, with the credentials of the packages to publish.
|
|
385
|
+
* @param {ProjectCommands} commands - Project commands quoted by the fix.
|
|
386
|
+
* @returns {MonorepoPlan} The same plan, blocked or with warnings when needed.
|
|
387
|
+
*/
|
|
388
|
+
function applyNpmAuth(plan, state, commands) {
|
|
389
|
+
if (!plan.steps.some((planStep) => planStep.id === MONOREPO_RELEASE_STEP.publishPackages)) {
|
|
390
|
+
return plan;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const blockers = [];
|
|
394
|
+
const warnings = [...plan.warnings];
|
|
395
|
+
|
|
396
|
+
for (const packageSnapshot of state.packages) {
|
|
397
|
+
const { npmAuth, unit } = packageSnapshot;
|
|
398
|
+
if (!npmAuth) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const { problem, warning } = classifyNpmAuth(npmAuth, commands, ` de ${unit.name}`);
|
|
402
|
+
if (problem) {
|
|
403
|
+
blockers.push(problem);
|
|
404
|
+
} else if (warning) {
|
|
405
|
+
warnings.push(warning);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return blockers.length > 0 ? createPlan({ blockers }) : { ...plan, warnings };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Names the packages whose npm credentials the diagnosis checks: the ones the plan could publish.
|
|
414
|
+
*
|
|
415
|
+
* @param {MonorepoPlan} plan - Plan built without credentials.
|
|
416
|
+
* @returns {string[]} Package names.
|
|
417
|
+
*/
|
|
418
|
+
export function listPackagesToAuthenticate(plan) {
|
|
419
|
+
if (!plan.steps.some((planStep) => planStep.id === MONOREPO_RELEASE_STEP.publishPackages)) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
return plan.mode === RELEASE_MODE.resume ? plan.pendingReleases.filter((release) => release.publish).map((release) => release.name) : plan.candidates;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Decides what is still missing to release the monorepo from `main`.
|
|
427
|
+
*
|
|
428
|
+
* @param {MonorepoSnapshot} state - Snapshot gathered by `collectMonorepoState`.
|
|
429
|
+
* @param {ReleaseCapabilities} capabilities - Steps the project configured.
|
|
430
|
+
* @param {{ tagFormat: string, ignoreLocalChanges?: boolean, skipUnpublished?: boolean, selectAllPackages?: boolean }} options -
|
|
431
|
+
* Tag format and command-line options. `skipUnpublished` allows a new release despite missing publications;
|
|
432
|
+
* `selectAllPackages` requires updated changelogs for every candidate because the flags do not allow skips.
|
|
433
|
+
* @returns {MonorepoPlan} Ordered plan.
|
|
434
|
+
*/
|
|
435
|
+
export function buildMonorepoPlan(state, capabilities, { tagFormat, ignoreLocalChanges = false, skipUnpublished = false, selectAllPackages = false }) {
|
|
436
|
+
const commands = capabilities.commands ?? DEFAULT_PROJECT_COMMANDS;
|
|
437
|
+
const blockers = findBlockers(state, ignoreLocalChanges, commands);
|
|
438
|
+
|
|
439
|
+
if (blockers.length > 0) {
|
|
440
|
+
return createPlan({ blockers });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const localResume = planLocalResume(state, capabilities, tagFormat);
|
|
444
|
+
const pending = localResume ? { plan: null, warnings: [] } : planPendingPublications(state, capabilities);
|
|
445
|
+
const skipped = skipUnpublished && pending.plan ? pending.plan.pendingReleases.map(({ name, version, tag }) => describeSkippedPublication(name, version, tag, state.packages.find(({ unit }) => unit.name === name)?.npm?.registryLabel)) : [];
|
|
446
|
+
const plan = localResume ?? (skipUnpublished ? null : pending.plan) ?? planNewRelease(state, capabilities, commands, [...pending.warnings, ...skipped], selectAllPackages);
|
|
447
|
+
const withAuth = applyNpmAuth(plan, state, commands);
|
|
448
|
+
// As in single-package mode: the configuration and the workspace manifests were already read from the
|
|
449
|
+
// working tree, so code and data changes cannot be set aside.
|
|
450
|
+
const codeChanges = ignoreLocalChanges && withAuth.steps.length > 0 ? listMonorepoChangesToSetAside(state, withAuth.mode).filter(isCodeChange) : [];
|
|
451
|
+
|
|
452
|
+
if (codeChanges.length > 0) {
|
|
453
|
+
return createPlan({ blockers: [codeChangesToSetAsideBlocker(codeChanges, commands)] });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const changelogPaths = new Set(state.packages.map(({ unit }) => unit.changelogPath));
|
|
457
|
+
const dirtyChangelogs = state.workingTreeChanges.filter((line) => changesOneOf(line, changelogPaths));
|
|
458
|
+
if (withAuth.mode === RELEASE_MODE.resume && ((!ignoreLocalChanges && state.workingTreeChanges.length > 0) || dirtyChangelogs.length > 0)) {
|
|
459
|
+
return createPlan({
|
|
460
|
+
blockers: [
|
|
461
|
+
{
|
|
462
|
+
title: "Hay cambios sin commitear y el release ya está commiteado",
|
|
463
|
+
details: [
|
|
464
|
+
...state.workingTreeChanges.slice(0, MAX_LISTED_ITEMS),
|
|
465
|
+
dirtyChangelogs.length > 0
|
|
466
|
+
? `Guardá manualmente los cambios del CHANGELOG (git stash) y volvé a correr ${commands.createVersion}; --${CREATE_VERSION_FLAG.ignoreLocalChanges} no lo aparta ni restaura.`
|
|
467
|
+
: `Retomar un release usa los archivos de su commit: guardalos (git stash) y volvé a correr ${commands.createVersion}, o usá --${CREATE_VERSION_FLAG.ignoreLocalChanges}.`,
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return withAuth;
|
|
475
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subject of the single release commit of a monorepo release:
|
|
3
|
+
* `release: @scope/a@1.2.0, @scope/b@0.3.1`. It records which packages the
|
|
4
|
+
* commit released, so a run can resume a local release commit that never
|
|
5
|
+
* reached `origin`.
|
|
6
|
+
*
|
|
7
|
+
* @module monorepo/release-commit
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { MONOREPO_RELEASE_ENTRY_PATTERN, MONOREPO_RELEASE_ENTRY_SEPARATOR, MONOREPO_RELEASE_SUBJECT_PREFIX } from "../constants/monorepo.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ name: string, version: string }} PackageRelease
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds the subject of a release commit.
|
|
18
|
+
*
|
|
19
|
+
* @param {readonly PackageRelease[]} releases - Released packages, in publication order.
|
|
20
|
+
* @returns {string} Subject.
|
|
21
|
+
*/
|
|
22
|
+
export function buildMonorepoReleaseSubject(releases) {
|
|
23
|
+
return `${MONOREPO_RELEASE_SUBJECT_PREFIX}${releases.map(({ name, version }) => `${name}@${version}`).join(MONOREPO_RELEASE_ENTRY_SEPARATOR)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the packages a release commit subject lists.
|
|
28
|
+
*
|
|
29
|
+
* @param {string | null | undefined} subject - Commit subject.
|
|
30
|
+
* @returns {PackageRelease[] | null} Released packages, or `null` when the subject is not a release commit.
|
|
31
|
+
*/
|
|
32
|
+
export function parseMonorepoReleaseSubject(subject) {
|
|
33
|
+
const trimmed = subject?.trim() ?? "";
|
|
34
|
+
if (!trimmed.startsWith(MONOREPO_RELEASE_SUBJECT_PREFIX)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const entries = trimmed.slice(MONOREPO_RELEASE_SUBJECT_PREFIX.length).split(MONOREPO_RELEASE_ENTRY_SEPARATOR);
|
|
39
|
+
const releases = entries.map((entry) => MONOREPO_RELEASE_ENTRY_PATTERN.exec(entry.trim())?.groups);
|
|
40
|
+
|
|
41
|
+
if (releases.length === 0 || releases.some((groups) => !groups)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return releases.map((groups) => ({ name: /** @type {Record<string, string>} */ (groups).name, version: /** @type {Record<string, string>} */ (groups).version }));
|
|
46
|
+
}
|