@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,927 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monorepo mode of `astra-rp create-version`: diagnoses every released
|
|
3
|
+
* workspace package, shows the plan and ships one release commit with a tag
|
|
4
|
+
* per package, then publishes each package in dependency order.
|
|
5
|
+
*
|
|
6
|
+
* It reuses the single-package steps that do not depend on the package
|
|
7
|
+
* (sync of `main`, migrations, checks, local changes, npm credentials,
|
|
8
|
+
* verified artifacts) and adds the per-package ones. Every step is derived
|
|
9
|
+
* from the current state, so running the command again after a failure
|
|
10
|
+
* resumes from the first missing step, including publications that npm never
|
|
11
|
+
* received: each one is prepared and published from its own release commit.
|
|
12
|
+
*
|
|
13
|
+
* @module monorepo/run
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { CHANGELOG_UPDATE_REQUIRED_CODE } from "../constants/changelog.js";
|
|
21
|
+
import {
|
|
22
|
+
CREATE_VERSION_FLAG,
|
|
23
|
+
FAILURE_EXIT_CODE,
|
|
24
|
+
GITHUB_REPOSITORY_PATTERN,
|
|
25
|
+
MAIN_BRANCH,
|
|
26
|
+
MAX_LISTED_ITEMS,
|
|
27
|
+
NPM_LOOKUP_STATUS,
|
|
28
|
+
RELEASE_MODE,
|
|
29
|
+
RELEASE_REGISTRY,
|
|
30
|
+
RELEASE_REMOTE,
|
|
31
|
+
RELEASE_STEP,
|
|
32
|
+
SHORT_SHA_LENGTH,
|
|
33
|
+
SUMMARY_VERSION_PLACEHOLDER,
|
|
34
|
+
} from "../constants/create-version.js";
|
|
35
|
+
import { MONOREPO_RELEASE_STEP, SKIP_PACKAGE_CHOICE, SUMMARY_PACKAGE_PLACEHOLDER } from "../constants/monorepo.js";
|
|
36
|
+
import { RELEASE_TYPE_ORDER } from "../constants/versions.js";
|
|
37
|
+
import { findPnpmPackRewrites, findPreparedArtifact, expandArtifactPattern, isSafeArtifactPath, verifyPreparedArtifact, withArtifactOutsidePackageRoot } from "../create-version/artifact.js";
|
|
38
|
+
import { describeReleaseTypes } from "../create-version/config.js";
|
|
39
|
+
import { isRegistryProvider } from "../create-version/registry-config.js";
|
|
40
|
+
import { checkRegistryAccess, publishRegistryRelease, resolveRegistry, selectProjectRegistry } from "../create-version/registry.js";
|
|
41
|
+
import { prepareJsrVersionUpdates } from "../create-version/jsr.js";
|
|
42
|
+
import { JSR_REGISTRY_PROVIDER } from "../constants/registry.js";
|
|
43
|
+
import { assertUnreleasedEntries, describeMissingUnreleasedEntries, formatReleaseDate, prepareChangelogClosing, verifyChangelogUpdate } from "../create-version/changelog.js";
|
|
44
|
+
import { ReleaseStepError, describeError } from "../create-version/errors.js";
|
|
45
|
+
import { readJsonManifest } from "../create-version/json.js";
|
|
46
|
+
import { describePublishedRelease, readNpmPackIntegrity } from "../create-version/npm.js";
|
|
47
|
+
import { describeNpmPublishFailure } from "../create-version/npm-auth.js";
|
|
48
|
+
import { createGitReader } from "../create-version/process.js";
|
|
49
|
+
import {
|
|
50
|
+
ReleaseCancelledError,
|
|
51
|
+
applyMigrationsStep,
|
|
52
|
+
askToIgnoreLocalChanges,
|
|
53
|
+
assertNoTrackedChanges,
|
|
54
|
+
createAnnotatedTag,
|
|
55
|
+
createHookContext,
|
|
56
|
+
describeReleaseCapabilities,
|
|
57
|
+
assertReleaseFilesMatchHead,
|
|
58
|
+
commitReleaseFiles,
|
|
59
|
+
prepareVersionFileUpdates,
|
|
60
|
+
readReleaseFile,
|
|
61
|
+
renderCommitList,
|
|
62
|
+
renderNodeVersionRow,
|
|
63
|
+
renderNpmAuthRow,
|
|
64
|
+
renderRepositoryRows,
|
|
65
|
+
rewriteManifestVersion,
|
|
66
|
+
renderPlan,
|
|
67
|
+
runChecksStep,
|
|
68
|
+
runConfiguredCommands,
|
|
69
|
+
runGitStep,
|
|
70
|
+
runPlanSteps,
|
|
71
|
+
syncMainStep,
|
|
72
|
+
withLocalChangesSetAside,
|
|
73
|
+
} from "../create-version/run.js";
|
|
74
|
+
import { BOX_TONE, ICON, formatDuration, measureActiveMs, paint, print, renderBanner, renderBox, renderRow, select, startSpinner } from "../terminal-ui.js";
|
|
75
|
+
import { bumpReleaseVersion, compareReleaseVersions, findHighestStableVersion, isStableReleaseVersion, suggestNextReleaseType } from "../versions.js";
|
|
76
|
+
import { buildMonorepoPlan, listMonorepoChangesToSetAside, listPackagesToAuthenticate } from "./plan.js";
|
|
77
|
+
import { buildMonorepoReleaseSubject } from "./release-commit.js";
|
|
78
|
+
import { collectMonorepoState } from "./state.js";
|
|
79
|
+
import { discoverWorkspacePackages, formatPackageTag, resolveReleaseUnits, sortByPublicationOrder } from "./workspaces.js";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {import("./workspaces.js").ReleaseUnit} ReleaseUnit
|
|
83
|
+
* @typedef {import("./state.js").MonorepoSnapshot} MonorepoSnapshot
|
|
84
|
+
* @typedef {import("./state.js").PackageSnapshot} PackageSnapshot
|
|
85
|
+
* @typedef {import("./plan.js").MonorepoPlan} MonorepoPlan
|
|
86
|
+
* @typedef {import("./plan.js").PendingPackageRelease} PendingPackageRelease
|
|
87
|
+
* @typedef {import("../create-version/config.js").ResolvedCreateVersionConfig} ResolvedCreateVersionConfig
|
|
88
|
+
* @typedef {import("../create-version/plan.js").ReleaseOptions} ReleaseOptions
|
|
89
|
+
* @typedef {import("../create-version/process.js").GitReader} GitReader
|
|
90
|
+
* @typedef {{ unit: ReleaseUnit, snapshot: PackageSnapshot, version: string, tag: string, commitSha: string | null }} ChosenRelease
|
|
91
|
+
* A package going out in this run; `commitSha` is set once its release commit exists.
|
|
92
|
+
* @typedef {{
|
|
93
|
+
* repositoryRoot: string,
|
|
94
|
+
* config: ResolvedCreateVersionConfig,
|
|
95
|
+
* state: MonorepoSnapshot,
|
|
96
|
+
* options: ReleaseOptions,
|
|
97
|
+
* reader: GitReader,
|
|
98
|
+
* commands: import("../package-manager.js").ProjectCommands,
|
|
99
|
+
* tagFormat: string,
|
|
100
|
+
* units: ReleaseUnit[],
|
|
101
|
+
* plan: MonorepoPlan,
|
|
102
|
+
* chosen: ChosenRelease[],
|
|
103
|
+
* changelogFiles: import("../create-version/changelog.js").PreservedReleaseFile[],
|
|
104
|
+
* preparedRoots: Set<string>,
|
|
105
|
+
* pushed: boolean,
|
|
106
|
+
* published: { name: string, version: string, registryUrl: string | null, registryLabel?: string, packageName?: string }[],
|
|
107
|
+
* }} MonorepoContext
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/** Prefix of the temporary checkouts a publication prepares an older release commit in. */
|
|
111
|
+
const RELEASE_CHECKOUT_PREFIX = "astra-rp-release-";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Renders the diagnosis: the repository rows plus one row per released package.
|
|
115
|
+
*
|
|
116
|
+
* @param {MonorepoSnapshot} state - Snapshot.
|
|
117
|
+
* @param {string} repositoryRoot - Repository root.
|
|
118
|
+
* @returns {string} Box.
|
|
119
|
+
*/
|
|
120
|
+
function renderMonorepoDiagnosis(state, repositoryRoot) {
|
|
121
|
+
const rows = [...renderRepositoryRows(state), ""];
|
|
122
|
+
|
|
123
|
+
for (const packageSnapshot of state.packages) {
|
|
124
|
+
const { unit, lastRelease, unreleasedCommits, npm, npmAuth, changelog } = packageSnapshot;
|
|
125
|
+
const released = lastRelease?.version ? paint("cyan", lastRelease.tag ?? lastRelease.version) : paint("gray", "sin releases");
|
|
126
|
+
const pending = unreleasedCommits.length > 0 ? paint("yellow", `${unreleasedCommits.length} commit(s) sin publicar`) : "al día";
|
|
127
|
+
const published = npm ? (npm.status === NPM_LOOKUP_STATUS.ok ? `${npm.registryLabel ?? "npm"} ${npm.latestVersion ?? npm.publishedVersions.at(-1) ?? "—"}` : paint("red", `${npm.registryLabel ?? "npm"} no respondió`)) : null;
|
|
128
|
+
const missingEntries = changelog.updated ? describeMissingUnreleasedEntries(changelog, unit.changelogPath) : null;
|
|
129
|
+
const changelogReady = changelog.updated && !missingEntries;
|
|
130
|
+
const changelogNote =
|
|
131
|
+
unreleasedCommits.length > 0
|
|
132
|
+
? paint(changelogReady ? "gray" : "yellow", changelogReady ? "CHANGELOG actualizado manualmente" : missingEntries ? "CHANGELOG sin entradas en [Unreleased]; bloquea si se elige publicar" : "CHANGELOG sin actualización manual; bloquea si se elige publicar")
|
|
133
|
+
: null;
|
|
134
|
+
rows.push(
|
|
135
|
+
renderRow(
|
|
136
|
+
unreleasedCommits.length > 0 ? ICON.warning : ICON.success,
|
|
137
|
+
unit.name,
|
|
138
|
+
[released, pending, published, changelogNote].filter(Boolean).join(paint("gray", " · "))
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
if (npmAuth) {
|
|
142
|
+
rows.push(renderNpmAuthRow(npmAuth));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (state.migrations) {
|
|
147
|
+
rows.push("", renderRow(state.migrations.status === "pending" ? ICON.warning : ICON.success, "Migraciones", state.migrations.status));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const nodeRow = renderNodeVersionRow(repositoryRoot);
|
|
151
|
+
if (nodeRow) {
|
|
152
|
+
rows.push(nodeRow);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return renderBox({ title: "Diagnóstico · monorepo", lines: rows, tone: BOX_TONE.info });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {MonorepoContext} context - Context.
|
|
160
|
+
* @param {string} name - Package name.
|
|
161
|
+
* @returns {PackageSnapshot} Snapshot of a released package.
|
|
162
|
+
*/
|
|
163
|
+
function requirePackage(context, name) {
|
|
164
|
+
const packageSnapshot = context.state.packages.find((candidate) => candidate.unit.name === name);
|
|
165
|
+
if (!packageSnapshot) {
|
|
166
|
+
throw new ReleaseStepError(`${name} no es un paquete publicado de este monorepo.`, "Revisá packages en astra-rp.config.(m)js.");
|
|
167
|
+
}
|
|
168
|
+
return packageSnapshot;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads the working-tree version of a package.
|
|
173
|
+
*
|
|
174
|
+
* @param {MonorepoContext} context - Context.
|
|
175
|
+
* @param {ReleaseUnit} unit - Package.
|
|
176
|
+
* @returns {string} Version.
|
|
177
|
+
* @throws {ReleaseStepError} When it is not a stable `X.Y.Z` version.
|
|
178
|
+
*/
|
|
179
|
+
function readWorkingVersion(context, unit) {
|
|
180
|
+
const version = readJsonManifest(context.repositoryRoot, unit.manifestPath).version;
|
|
181
|
+
if (!isStableReleaseVersion(version)) {
|
|
182
|
+
throw new ReleaseStepError(`${unit.manifestPath} tiene la versión ${String(version)}, que no es X.Y.Z.`, `Corregila y volvé a correr ${context.commands.createVersion}.`);
|
|
183
|
+
}
|
|
184
|
+
return version;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Checks, before anything is written, that the release tag of a package is a valid Git ref name and
|
|
189
|
+
* does not exist locally, so `git tag` cannot fail after the release commit.
|
|
190
|
+
*
|
|
191
|
+
* @param {MonorepoContext} context - Context.
|
|
192
|
+
* @param {string} tag - Tag the release would create.
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
* @throws {ReleaseStepError} When the tag is invalid or already exists.
|
|
195
|
+
*/
|
|
196
|
+
async function assertTagCanBeCreated(context, tag) {
|
|
197
|
+
if ((await context.reader.tryGit(["check-ref-format", `refs/tags/${tag}`])) === null) {
|
|
198
|
+
throw new ReleaseStepError(
|
|
199
|
+
`El tag ${tag} no es un nombre de tag válido para Git (tagFormat "${context.tagFormat}").`,
|
|
200
|
+
`No se escribió nada. Ajustá tagFormat o el nombre del paquete y volvé a correr ${context.commands.createVersion}.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (await context.reader.tryGit(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`])) {
|
|
204
|
+
throw new ReleaseStepError(
|
|
205
|
+
`El tag ${tag} ya existe en local.`,
|
|
206
|
+
`No se escribió nada. Revisalo con git show ${tag}; si sobra, borralo con git tag -d ${tag} y volvé a correr ${context.commands.createVersion}.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Asks (or takes from the flags) the version of every package with changes; a package can be left
|
|
213
|
+
* out of this release.
|
|
214
|
+
*
|
|
215
|
+
* @param {MonorepoContext} context - Context.
|
|
216
|
+
* @returns {Promise<void>}
|
|
217
|
+
*/
|
|
218
|
+
async function chooseVersionsStep(context) {
|
|
219
|
+
for (const name of context.plan.candidates) {
|
|
220
|
+
const packageSnapshot = requirePackage(context, name);
|
|
221
|
+
const { unit, unreleasedCommits } = packageSnapshot;
|
|
222
|
+
const currentVersion = readWorkingVersion(context, unit);
|
|
223
|
+
const suggestion = suggestNextReleaseType(unreleasedCommits, currentVersion, { preMajorShift: context.config.preMajorShift });
|
|
224
|
+
print(renderCommitList(unreleasedCommits, `${unit.name} · ${unreleasedCommits.length} commit(s) sin publicar`));
|
|
225
|
+
|
|
226
|
+
/** @type {string} */
|
|
227
|
+
let choice;
|
|
228
|
+
if (context.options.bump) {
|
|
229
|
+
choice = context.options.bump;
|
|
230
|
+
} else if (context.options.acceptSuggested) {
|
|
231
|
+
choice = suggestion.releaseType;
|
|
232
|
+
} else {
|
|
233
|
+
const releaseTypeDescriptions = describeReleaseTypes(context.config, currentVersion);
|
|
234
|
+
choice = await select({
|
|
235
|
+
message: `¿Qué versión de ${unit.name}? (actual ${currentVersion})`,
|
|
236
|
+
options: [
|
|
237
|
+
...RELEASE_TYPE_ORDER.map((releaseType) => ({
|
|
238
|
+
label: `${releaseType.padEnd(5)} ${currentVersion} → ${bumpReleaseVersion(currentVersion, releaseType)}`,
|
|
239
|
+
hint: releaseType === suggestion.releaseType ? `${ICON.star} sugerida: ${suggestion.reason}` : undefined,
|
|
240
|
+
description: releaseTypeDescriptions[releaseType],
|
|
241
|
+
value: releaseType,
|
|
242
|
+
})),
|
|
243
|
+
{ label: "No publicar ahora", hint: "queda para un release siguiente", value: SKIP_PACKAGE_CHOICE },
|
|
244
|
+
],
|
|
245
|
+
// Nothing is preselected so a stray Enter never ships a version.
|
|
246
|
+
defaultIndex: null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (choice === SKIP_PACKAGE_CHOICE) {
|
|
251
|
+
print(`${ICON.info} ${unit.name} queda para otro release.`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const version = bumpReleaseVersion(currentVersion, /** @type {"patch" | "minor" | "major"} */ (choice));
|
|
256
|
+
// As in single-package mode: a version not above the highest one on npm is either already
|
|
257
|
+
// published or would move `latest` back.
|
|
258
|
+
const highestPublished = findHighestStableVersion(packageSnapshot.npm?.publishedVersions ?? []);
|
|
259
|
+
if (highestPublished && compareReleaseVersions(version, highestPublished) <= 0) {
|
|
260
|
+
throw new ReleaseStepError(
|
|
261
|
+
`${unit.name}@${version} no es mayor que ${highestPublished}, la versión más alta publicada en ${packageSnapshot.npm?.registryLabel ?? "npm"}.`,
|
|
262
|
+
`No se escribió nada. Llevá la versión de ${unit.manifestPath} a ${highestPublished} o elegí otro tipo de versión, y volvé a correr ${context.commands.createVersion}.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
context.chosen.push({ unit, snapshot: packageSnapshot, version, tag: formatPackageTag(context.tagFormat, unit, version), commitSha: null });
|
|
266
|
+
print(`${ICON.success} ${unit.name}: ${currentVersion} → ${paint(["bold", "cyan"], version)} (${choice})`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const duplicatedTag = context.chosen.find((release, index) => context.chosen.findIndex(({ tag }) => tag === release.tag) !== index);
|
|
270
|
+
if (duplicatedTag) {
|
|
271
|
+
throw new ReleaseStepError(
|
|
272
|
+
`Dos paquetes del release generan el mismo tag ${duplicatedTag.tag} (tagFormat "${context.tagFormat}").`,
|
|
273
|
+
`No se escribió nada. Usá {name} en tagFormat, o carpetas de paquete con nombres distintos, y volvé a correr ${context.commands.createVersion}.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const { tag } of context.chosen) {
|
|
278
|
+
await assertTagCanBeCreated(context, tag);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (context.chosen.length === 0) {
|
|
282
|
+
print(renderBox({ title: "Release cancelado", lines: [`${ICON.info} No elegiste ningún paquete: no se tocó nada.`], tone: BOX_TONE.info }));
|
|
283
|
+
throw new ReleaseCancelledError();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Checked again before the commit; here it stops the release before migrations are applied.
|
|
287
|
+
const versionFilesByPackage = await assertReleaseScope(context);
|
|
288
|
+
for (const { unit, version } of context.chosen) {
|
|
289
|
+
// Only validates: a missing, untracked or unmarked versionFiles entry stops the release before migrations.
|
|
290
|
+
await prepareVersionFileUpdates(context, versionFilesByPackage.get(unit.name) ?? [], version);
|
|
291
|
+
if (context.config.publish === JSR_REGISTRY_PROVIDER) await prepareJsrVersionUpdates(context, path.join(context.repositoryRoot, unit.directory), context.config.publication, version);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Verifies the chosen packages' manual changelogs and their `[Unreleased]` entries before migrations,
|
|
297
|
+
* capturing their bytes for the release commit, which closes that section.
|
|
298
|
+
*
|
|
299
|
+
* @param {MonorepoContext} context - Context.
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
async function verifyChangelogsStep(context) {
|
|
303
|
+
for (const { unit, snapshot } of context.chosen) {
|
|
304
|
+
const changelog = await verifyChangelogUpdate(context, unit.changelogPath, snapshot.lastRelease?.sha ?? null);
|
|
305
|
+
assertUnreleasedEntries(context, changelog);
|
|
306
|
+
context.changelogFiles.push(changelog);
|
|
307
|
+
print(`${ICON.success} ${unit.changelogPath} actualizado manualmente; el commit de release cierra su sección [Unreleased].`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Assigns every `versionFiles` entry to the chosen package that contains it.
|
|
313
|
+
*
|
|
314
|
+
* @param {MonorepoContext} context - Context.
|
|
315
|
+
* @returns {Map<string, string[]>} Files per package name.
|
|
316
|
+
* @throws {ReleaseStepError} When an entry is outside every released package, or is the manifest or
|
|
317
|
+
* changelog of its package (the release commit writes them: it bumps the manifest and closes the changelog).
|
|
318
|
+
*/
|
|
319
|
+
function groupVersionFilesByPackage(context) {
|
|
320
|
+
/** @type {Map<string, string[]>} */
|
|
321
|
+
const grouped = new Map();
|
|
322
|
+
|
|
323
|
+
for (const filePath of context.config.versionFiles) {
|
|
324
|
+
// With nested workspaces the file belongs to the deepest package that contains it.
|
|
325
|
+
const owner = context.units
|
|
326
|
+
.filter((unit) => filePath.startsWith(`${unit.directory}/`))
|
|
327
|
+
.reduce((/** @type {typeof context.units[number] | undefined} */ closest, unit) => (closest && closest.directory.length >= unit.directory.length ? closest : unit), undefined);
|
|
328
|
+
if (!owner) {
|
|
329
|
+
throw new ReleaseStepError(
|
|
330
|
+
`${filePath} (versionFiles) no está dentro de ningún paquete publicado.`,
|
|
331
|
+
`En un monorepo cada archivo toma la versión del paquete que lo contiene: movelo o sacalo de versionFiles y volvé a correr ${context.commands.createVersion}.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (filePath === owner.manifestPath || filePath.toLowerCase() === owner.changelogPath.toLowerCase()) {
|
|
335
|
+
throw new ReleaseStepError(
|
|
336
|
+
`${filePath} (versionFiles) es el package.json o el CHANGELOG.md de ${owner.name}: el commit de release los incluye y cierra la sección [Unreleased] del CHANGELOG.`,
|
|
337
|
+
`Sacalo de versionFiles en astra-rp.config.(m)js y volvé a correr ${context.commands.createVersion}; no se tocó la versión.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
grouped.set(owner.name, [...(grouped.get(owner.name) ?? []), filePath]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return grouped;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Lists the paths of a `git diff --name-only --no-renames -z` run: both sides of a rename count.
|
|
348
|
+
*
|
|
349
|
+
* @param {MonorepoContext} context - Context.
|
|
350
|
+
* @param {string[]} diffArguments - Arguments after `git diff --name-only --no-renames -z`.
|
|
351
|
+
* @returns {Promise<string[]>} Paths relative to the root.
|
|
352
|
+
*/
|
|
353
|
+
async function listDiffPaths(context, diffArguments) {
|
|
354
|
+
return (await context.reader.git(["diff", "--name-only", "--no-renames", "-z", ...diffArguments])).split("\0").filter(Boolean);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Stops the release before writing anything when the release commit would carry changes that are
|
|
359
|
+
* not of the chosen packages: `git commit` takes the whole index, and the changelogs of every package
|
|
360
|
+
* are allowed to stay uncommitted during the plan.
|
|
361
|
+
*
|
|
362
|
+
* @param {MonorepoContext} context - Context.
|
|
363
|
+
* @param {ReadonlySet<string>} releasePaths - Manifests, changelogs and `versionFiles` of the chosen packages.
|
|
364
|
+
* @returns {Promise<void>}
|
|
365
|
+
* @throws {ReleaseStepError} When something outside those files is staged, differs from `HEAD` or is untracked (and not ignored), or the changelog of a package left out has changes.
|
|
366
|
+
*/
|
|
367
|
+
async function assertOnlyReleaseChanges(context, releasePaths) {
|
|
368
|
+
const foreignStaged = (await listDiffPaths(context, ["--cached"])).filter((filePath) => !releasePaths.has(filePath));
|
|
369
|
+
if (foreignStaged.length > 0) {
|
|
370
|
+
throw new ReleaseStepError(
|
|
371
|
+
`Hay cambios staged que no son del release y entrarían en su commit: ${foreignStaged.slice(0, MAX_LISTED_ITEMS).join(", ")}.`,
|
|
372
|
+
`No se escribió nada. Sacalos del índice con git restore --staged <archivo> (o commitealos aparte) y volvé a correr ${context.commands.createVersion}.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const chosenNames = new Set(context.chosen.map(({ unit }) => unit.name));
|
|
377
|
+
const skippedChangelogs = new Set(context.units.filter((unit) => !chosenNames.has(unit.name)).map((unit) => unit.changelogPath));
|
|
378
|
+
// `git diff HEAD` does not list untracked files, yet `npm publish` packs them from the package directory.
|
|
379
|
+
const untrackedPaths = (await context.reader.git(["ls-files", "--others", "--exclude-standard", "-z"])).split("\0").filter(Boolean);
|
|
380
|
+
const changedPaths = [...(await listDiffPaths(context, ["HEAD"])), ...untrackedPaths];
|
|
381
|
+
const dirtySkippedChangelogs = changedPaths.filter((filePath) => skippedChangelogs.has(filePath));
|
|
382
|
+
if (dirtySkippedChangelogs.length > 0) {
|
|
383
|
+
throw new ReleaseStepError(
|
|
384
|
+
`Hay cambios sin commitear en el CHANGELOG de paquetes que no salen en este release: ${dirtySkippedChangelogs.join(", ")}.`,
|
|
385
|
+
`No se escribió nada. Commitealos aparte o guardalos con git stash, o elegí también esos paquetes, y volvé a correr ${context.commands.createVersion}.`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Unstaged changes (for example, a check that rewrites a file) stay out of the commit but not out of what gets published.
|
|
390
|
+
// The plan blocks untracked files (or sets them aside with --ignore-local-changes), so an earlier step created these.
|
|
391
|
+
const foreignUntracked = untrackedPaths.filter((filePath) => !releasePaths.has(filePath));
|
|
392
|
+
if (foreignUntracked.length > 0) {
|
|
393
|
+
throw new ReleaseStepError(
|
|
394
|
+
`Un paso anterior (por ejemplo, un check) creó ${foreignUntracked.slice(0, MAX_LISTED_ITEMS).join(", ")}, que no son del release: quedarían fuera del commit pero se publicarían.`,
|
|
395
|
+
`No se escribió nada. Revisá el check, borralos o agregalos a .gitignore, y volvé a correr ${context.commands.createVersion}.`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const foreignChanged = changedPaths.filter((filePath) => !releasePaths.has(filePath));
|
|
400
|
+
if (foreignChanged.length > 0) {
|
|
401
|
+
throw new ReleaseStepError(
|
|
402
|
+
`Un paso anterior (por ejemplo, un check) modificó ${foreignChanged.slice(0, MAX_LISTED_ITEMS).join(", ")}, que no son del release: se publicarían sin estar commiteados.`,
|
|
403
|
+
`No se escribió nada. Revisá el check o commiteá esos cambios aparte, y volvé a correr ${context.commands.createVersion}.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Checks that only the files of the chosen packages would enter the release commit.
|
|
410
|
+
*
|
|
411
|
+
* @param {MonorepoContext} context - Context.
|
|
412
|
+
* @returns {Promise<Map<string, string[]>>} `versionFiles` per package name.
|
|
413
|
+
* @throws {ReleaseStepError} When a `versionFiles` entry has no package, or other changes would enter the commit.
|
|
414
|
+
*/
|
|
415
|
+
async function assertReleaseScope(context) {
|
|
416
|
+
const versionFilesByPackage = groupVersionFilesByPackage(context);
|
|
417
|
+
await assertOnlyReleaseChanges(
|
|
418
|
+
context,
|
|
419
|
+
new Set(context.chosen.flatMap(({ unit }) => [unit.manifestPath, unit.changelogPath, ...(versionFilesByPackage.get(unit.name) ?? [])]))
|
|
420
|
+
);
|
|
421
|
+
return versionFilesByPackage;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Writes the new versions, closes the `[Unreleased]` section of every released changelog and creates
|
|
426
|
+
* the release commit with an annotated tag per package. Every file is computed before anything is written, and the commit
|
|
427
|
+
* reuses the single-package one: literal staging, rollback on failure and the prepared-tree check.
|
|
428
|
+
*
|
|
429
|
+
* @param {MonorepoContext} context - Context.
|
|
430
|
+
* @returns {Promise<void>}
|
|
431
|
+
*/
|
|
432
|
+
async function bumpPackagesStep(context) {
|
|
433
|
+
const versionFilesByPackage = await assertReleaseScope(context);
|
|
434
|
+
await assertReleaseFilesMatchHead(context, [...context.chosen.map(({ unit }) => unit.manifestPath), ...context.config.versionFiles]);
|
|
435
|
+
/** @type {import("../create-version/run.js").ReleaseFileUpdate[]} */
|
|
436
|
+
const releaseFiles = [];
|
|
437
|
+
|
|
438
|
+
for (const { unit, version } of context.chosen) {
|
|
439
|
+
const manifest = readReleaseFile(context, unit.manifestPath, unit.manifestPath);
|
|
440
|
+
releaseFiles.push(
|
|
441
|
+
{ filePath: unit.manifestPath, originalBytes: manifest.originalBytes, content: rewriteManifestVersion(context, unit.manifestPath, manifest.text, version) },
|
|
442
|
+
...(await prepareVersionFileUpdates(context, versionFilesByPackage.get(unit.name) ?? [], version))
|
|
443
|
+
);
|
|
444
|
+
if (context.config.publish === JSR_REGISTRY_PROVIDER) releaseFiles.push(...await prepareJsrVersionUpdates(context, path.join(context.repositoryRoot, unit.directory), context.config.publication, version));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const releaseDate = formatReleaseDate(new Date(), context.config.releaseTimeZone);
|
|
448
|
+
const closedChangelogs = context.chosen.map(({ unit, version }) => {
|
|
449
|
+
const changelog = context.changelogFiles.find(({ filePath }) => filePath === unit.changelogPath);
|
|
450
|
+
if (!changelog) {
|
|
451
|
+
throw new ReleaseStepError(`No se verificó ${unit.changelogPath} antes del commit de release.`, `Volvé a correr ${context.commands.createVersion}; no se tocó la versión.`);
|
|
452
|
+
}
|
|
453
|
+
return prepareChangelogClosing(context, changelog, version, releaseDate);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const ordered = sortByPublicationOrder(context.chosen.map((release) => ({ ...release, name: release.unit.name, publishedDependencies: release.unit.publishedDependencies })));
|
|
457
|
+
const subject = buildMonorepoReleaseSubject(ordered.map(({ name, version }) => ({ name, version })));
|
|
458
|
+
await commitReleaseFiles(context, [...releaseFiles, ...closedChangelogs], subject, closedChangelogs);
|
|
459
|
+
|
|
460
|
+
const commitSha = await context.reader.git(["rev-parse", "HEAD"]);
|
|
461
|
+
for (const release of context.chosen) {
|
|
462
|
+
await createAnnotatedTag(context, release.tag, `${release.unit.name}@${release.version}`, `Si ya existe, revisalo con git show ${release.tag}.`);
|
|
463
|
+
release.commitSha = commitSha;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
print(`${ICON.success} Commit ${paint("bold", subject)} y ${context.chosen.length} tag(s) creados en local.`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Releases the run is finishing: the ones just created, or the pending ones of a resume.
|
|
471
|
+
*
|
|
472
|
+
* @param {MonorepoContext} context - Context.
|
|
473
|
+
* @returns {PendingPackageRelease[]} Releases with their commit.
|
|
474
|
+
*/
|
|
475
|
+
function listRunReleases(context) {
|
|
476
|
+
if (context.chosen.length > 0) {
|
|
477
|
+
return context.chosen.map((release) => ({ name: release.unit.name, version: release.version, tag: release.tag, commitSha: release.commitSha ?? "", publish: true }));
|
|
478
|
+
}
|
|
479
|
+
return context.plan.pendingReleases;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Runs the configured preparation once in a checkout, with the releases it covers.
|
|
484
|
+
*
|
|
485
|
+
* @param {MonorepoContext} context - Context.
|
|
486
|
+
* @param {string} checkoutRoot - Directory to prepare (the repository, or a temporary release checkout).
|
|
487
|
+
* @param {PendingPackageRelease[]} releases - Releases prepared there.
|
|
488
|
+
* @returns {Promise<void>}
|
|
489
|
+
*/
|
|
490
|
+
async function prepareCheckout(context, checkoutRoot, releases) {
|
|
491
|
+
const { prepare } = context.config;
|
|
492
|
+
if (!prepare || context.preparedRoots.has(checkoutRoot)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const hint = `Corregí el error y volvé a correr ${context.commands.createVersion}, que retoma desde acá.`;
|
|
497
|
+
if (Array.isArray(prepare)) {
|
|
498
|
+
await runConfiguredCommands({ ...context, repositoryRoot: checkoutRoot }, prepare, hint);
|
|
499
|
+
} else {
|
|
500
|
+
const units = new Map(context.units.map((unit) => [unit.name, unit]));
|
|
501
|
+
await prepare({
|
|
502
|
+
...createHookContext(checkoutRoot, createGitReader(checkoutRoot), null),
|
|
503
|
+
releases: releases.map(({ name, version }) => ({ name, version, directory: units.get(name)?.directory ?? "" })),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
context.preparedRoots.add(checkoutRoot);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Runs the preparation on the release commit (the plan step).
|
|
511
|
+
*
|
|
512
|
+
* @param {MonorepoContext} context - Context.
|
|
513
|
+
* @returns {Promise<void>}
|
|
514
|
+
*/
|
|
515
|
+
async function prepareReleaseStep(context) {
|
|
516
|
+
await prepareCheckout(context, context.repositoryRoot, listRunReleases(context));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Pushes `main` and every release tag atomically, creating the tags a resumed release lacks.
|
|
521
|
+
*
|
|
522
|
+
* @param {MonorepoContext} context - Context.
|
|
523
|
+
* @returns {Promise<void>}
|
|
524
|
+
*/
|
|
525
|
+
async function pushPackagesStep(context) {
|
|
526
|
+
const releases = listRunReleases(context);
|
|
527
|
+
|
|
528
|
+
for (const release of releases) {
|
|
529
|
+
const taggedSha = await context.reader.tryGit(["rev-parse", "--verify", "--quiet", `refs/tags/${release.tag}^{commit}`]);
|
|
530
|
+
if (taggedSha === null) {
|
|
531
|
+
await createAnnotatedTag(context, release.tag, `${release.name}@${release.version}`, `Revisá git tag --list ${release.tag}.`, release.commitSha);
|
|
532
|
+
} else if (taggedSha !== release.commitSha) {
|
|
533
|
+
throw new ReleaseStepError(
|
|
534
|
+
`El tag local ${release.tag} apunta a ${taggedSha.slice(0, SHORT_SHA_LENGTH)} y el commit de release de ${release.name}@${release.version} es ${release.commitSha.slice(0, SHORT_SHA_LENGTH)}.`,
|
|
535
|
+
`No se subió nada. Revisalo con git show ${release.tag}; si sobra, borralo con git tag -d ${release.tag} y volvé a correr ${context.commands.createVersion}.`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
await runGitStep(
|
|
541
|
+
context,
|
|
542
|
+
["push", "--atomic", RELEASE_REMOTE, MAIN_BRANCH, ...releases.map((release) => `refs/tags/${release.tag}`)],
|
|
543
|
+
`El push de ${MAIN_BRANCH} y los tags falló`,
|
|
544
|
+
`El release quedó en local: corregí el error y corré ${context.commands.createVersion}, que retoma el push.`
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
for (const release of releases) {
|
|
548
|
+
if (!(await context.reader.tryGit(["ls-remote", "--tags", RELEASE_REMOTE, `refs/tags/${release.tag}`]))) {
|
|
549
|
+
throw new ReleaseStepError(`${MAIN_BRANCH} se subió pero ${release.tag} no aparece en ${RELEASE_REMOTE}.`, `Subilo con git push ${RELEASE_REMOTE} ${release.tag}.`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
context.pushed = true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Finds and verifies the prepared archive of a package when the project configured `artifact`
|
|
558
|
+
* (the pattern is relative to the package directory).
|
|
559
|
+
*
|
|
560
|
+
* @param {MonorepoContext} context - Context.
|
|
561
|
+
* @param {string} packageRoot - Package directory in the checkout.
|
|
562
|
+
* @param {Record<string, unknown>} manifest - Package manifest.
|
|
563
|
+
* @param {string} version - Version being published.
|
|
564
|
+
* @returns {Promise<string | null>} Archive relative to the package directory, or `null` to publish the directory.
|
|
565
|
+
*/
|
|
566
|
+
async function resolvePackageArtifact(context, packageRoot, manifest, version) {
|
|
567
|
+
const { artifact } = context.config;
|
|
568
|
+
if (!artifact) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const release = { version, packageName: String(manifest.name) };
|
|
573
|
+
const prepared = findPreparedArtifact(packageRoot, artifact, release);
|
|
574
|
+
if (!prepared || !isSafeArtifactPath(prepared.path)) {
|
|
575
|
+
throw new ReleaseStepError(`No hay un artefacto preparado de ${release.packageName}@${version} que coincida con ${expandArtifactPattern(artifact, release)}.`, `Revisá la preparación y volvé a correr ${context.commands.createVersion}.`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const npmPack = await withArtifactOutsidePackageRoot(packageRoot, prepared.path, () => readNpmPackIntegrity(packageRoot));
|
|
579
|
+
if (!npmPack.pack || npmPack.pack.version !== version) {
|
|
580
|
+
throw new ReleaseStepError(`No se pudo verificar ${prepared.path} de ${release.packageName}: ${npmPack.problem ?? "npm pack describe otra versión"}.`, `No se publicó nada. Volvé a correr ${context.commands.createVersion}.`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const problems = verifyPreparedArtifact(packageRoot, prepared, npmPack.pack.integrity);
|
|
584
|
+
if (problems.length > 0) {
|
|
585
|
+
throw new ReleaseStepError(`El artefacto ${prepared.path} de ${release.packageName} no se puede publicar: ${problems.join("; ")}.`, `No se publicó nada. Rearmalo con npm pack y volvé a correr ${context.commands.createVersion}.`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return prepared.path;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Publishes one package from a checkout, with npm or the project hook.
|
|
593
|
+
*
|
|
594
|
+
* @param {MonorepoContext} context - Context.
|
|
595
|
+
* @param {string} checkoutRoot - Checkout holding the release commit.
|
|
596
|
+
* @param {PendingPackageRelease} release - Release to publish.
|
|
597
|
+
* @returns {Promise<void>}
|
|
598
|
+
*/
|
|
599
|
+
async function publishPackage(context, checkoutRoot, release) {
|
|
600
|
+
const { unit } = requirePackage(context, release.name);
|
|
601
|
+
const packageRoot = path.join(checkoutRoot, unit.directory);
|
|
602
|
+
|
|
603
|
+
if (!isRegistryProvider(context.config.publish)) {
|
|
604
|
+
await /** @type {import("../create-version/config.js").ReleaseHook} */ (context.config.publish)({
|
|
605
|
+
...createHookContext(checkoutRoot, createGitReader(checkoutRoot), release.version),
|
|
606
|
+
releases: [{ name: release.name, version: release.version, directory: unit.directory }],
|
|
607
|
+
});
|
|
608
|
+
context.published.push({ name: release.name, version: release.version, registryUrl: null });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// npm publishes the checkout of the release commit: no tracked file may differ from it, and npm
|
|
613
|
+
// would upload as they are the specifiers only pnpm rewrites when packing.
|
|
614
|
+
await assertNoTrackedChanges({ reader: createGitReader(checkoutRoot), commands: context.commands });
|
|
615
|
+
const manifest = readJsonManifest(packageRoot);
|
|
616
|
+
if (manifest.name !== release.name) {
|
|
617
|
+
throw new ReleaseStepError(
|
|
618
|
+
`El commit de release ${release.tag} publica ${String(manifest.name)} desde ${unit.manifestPath}, pero el paquete ahora se llama ${release.name}.`,
|
|
619
|
+
`No se publicó nada. Publicá ese release a mano con su nombre original, o dalo por descartado con --${CREATE_VERSION_FLAG.skipUnpublished}.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const rewrites = context.config.publish === JSR_REGISTRY_PROVIDER ? [] : findPnpmPackRewrites(manifest);
|
|
623
|
+
if (rewrites.length > 0) {
|
|
624
|
+
throw new ReleaseStepError(`${release.name} depende de reescrituras del package manager al empaquetar: ${rewrites.join("; ")}.`, "No se publicó nada. Reemplazá workspace:/catalog:/jsr: por rangos de versión.");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const selection = selectProjectRegistry(context.config);
|
|
628
|
+
let registry;
|
|
629
|
+
try {
|
|
630
|
+
// Resolved in the repository, as the diagnosis did: a temporary release checkout lacks an untracked project .npmrc.
|
|
631
|
+
registry = await resolveRegistry(selection, manifest, context.repositoryRoot, packageRoot);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
throw new ReleaseStepError(`No se puede publicar ${release.name}: ${describeError(error)}.`, "Corregí el registry (publishConfig) y volvé a correr el comando.");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const registryUrl = registry.registryUrl;
|
|
637
|
+
const artifactPath = registry.provider === JSR_REGISTRY_PROVIDER ? null : await resolvePackageArtifact(context, packageRoot, manifest, release.version);
|
|
638
|
+
const result = await publishRegistryRelease(selection, manifest, context.repositoryRoot, packageRoot, release.version, artifactPath);
|
|
639
|
+
|
|
640
|
+
if (result.missingToken) {
|
|
641
|
+
throw new ReleaseStepError(`Falta ${registry.options.tokenEnv} para publicar ${release.name}@${release.version} en ${registry.label}.`, `Definilo en el entorno, .env del repo o ~/.config/astra-rp/.env y corré ${context.commands.createVersion}: retoma solo lo que falta publicar.`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!result.confirmed) {
|
|
645
|
+
const npmAuth = await checkRegistryAccess(selection, manifest, context.repositoryRoot, packageRoot);
|
|
646
|
+
const failure = registry.provider === RELEASE_REGISTRY.npm ? describeNpmPublishFailure(npmAuth, { exitCode: result.exitCode, version: `${release.name}@${release.version}` }, context.commands) : {
|
|
647
|
+
message: result.exitCode === 0
|
|
648
|
+
? `El cliente de ${registry.label} terminó con código 0, pero la metadata no confirmó ${registry.packageName}@${release.version}.`
|
|
649
|
+
: `No se confirmó la publicación de ${registry.packageName}@${release.version} en ${registry.label} (código ${result.exitCode}).`,
|
|
650
|
+
hint: `Consultá ${registryUrl} antes de reintentar; ${context.commands.createVersion} retoma lo pendiente. ${npmAuth.reason ?? "Revisá las credenciales y el permiso de escritura."}`,
|
|
651
|
+
};
|
|
652
|
+
throw new ReleaseStepError(failure.message, failure.hint);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
context.published.push({ name: release.name, version: release.version, registryUrl, registryLabel: registry.label, packageName: registry.packageName });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Checks out a release commit in a temporary worktree (or uses the repository when `HEAD` is that
|
|
660
|
+
* commit), runs an operation there and removes the worktree.
|
|
661
|
+
*
|
|
662
|
+
* @template T
|
|
663
|
+
* @param {MonorepoContext} context - Context.
|
|
664
|
+
* @param {string} commitSha - Release commit.
|
|
665
|
+
* @param {(checkoutRoot: string) => Promise<T>} operation - Work done in the checkout.
|
|
666
|
+
* @returns {Promise<T>} Operation result.
|
|
667
|
+
*/
|
|
668
|
+
async function withReleaseCheckout(context, commitSha, operation) {
|
|
669
|
+
const headSha = await context.reader.git(["rev-parse", "HEAD"]);
|
|
670
|
+
if (headSha === commitSha) {
|
|
671
|
+
return operation(context.repositoryRoot);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const parentDirectory = mkdtempSync(path.join(os.tmpdir(), RELEASE_CHECKOUT_PREFIX));
|
|
675
|
+
const checkoutRoot = path.join(parentDirectory, "checkout");
|
|
676
|
+
await runGitStep(context, ["worktree", "add", "--quiet", "--detach", checkoutRoot, commitSha], `No se pudo preparar el commit ${commitSha.slice(0, SHORT_SHA_LENGTH)} en un worktree temporal`, "Revisá git worktree list.");
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
return await operation(checkoutRoot);
|
|
680
|
+
} finally {
|
|
681
|
+
await context.reader.tryGit(["worktree", "remove", "--force", checkoutRoot]);
|
|
682
|
+
rmSync(parentDirectory, { recursive: true, force: true });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Publishes every pending package in dependency order, each from its release commit.
|
|
688
|
+
*
|
|
689
|
+
* @param {MonorepoContext} context - Context.
|
|
690
|
+
* @returns {Promise<void>}
|
|
691
|
+
*/
|
|
692
|
+
async function publishPackagesStep(context) {
|
|
693
|
+
const releases = listRunReleases(context).filter((release) => release.publish);
|
|
694
|
+
const units = new Map(context.units.map((unit) => [unit.name, unit]));
|
|
695
|
+
const ordered = sortByPublicationOrder(releases.map((release) => ({ ...release, publishedDependencies: units.get(release.name)?.publishedDependencies ?? [] })));
|
|
696
|
+
/** @type {Map<string, typeof ordered>} */
|
|
697
|
+
const byCommit = new Map();
|
|
698
|
+
for (const release of ordered) {
|
|
699
|
+
byCommit.set(release.commitSha, [...(byCommit.get(release.commitSha) ?? []), release]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const [commitSha, commitReleases] of byCommit) {
|
|
703
|
+
await withReleaseCheckout(context, commitSha, async (checkoutRoot) => {
|
|
704
|
+
await prepareCheckout(context, checkoutRoot, commitReleases);
|
|
705
|
+
for (const release of commitReleases) {
|
|
706
|
+
print(paint("gray", `Publicando ${release.name}@${release.version}…`));
|
|
707
|
+
await publishPackage(context, checkoutRoot, release);
|
|
708
|
+
print(`${ICON.success} ${release.name}@${release.version} publicado.`);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Executors of each plan step.
|
|
716
|
+
*
|
|
717
|
+
* @type {Record<string, (context: MonorepoContext) => Promise<void>>}
|
|
718
|
+
*/
|
|
719
|
+
const STEP_EXECUTORS = {
|
|
720
|
+
[RELEASE_STEP.syncMain]: syncMainStep,
|
|
721
|
+
[RELEASE_STEP.applyMigrations]: applyMigrationsStep,
|
|
722
|
+
[RELEASE_STEP.runChecks]: runChecksStep,
|
|
723
|
+
[RELEASE_STEP.prepareRelease]: prepareReleaseStep,
|
|
724
|
+
[MONOREPO_RELEASE_STEP.chooseVersions]: chooseVersionsStep,
|
|
725
|
+
[MONOREPO_RELEASE_STEP.verifyChangelogs]: verifyChangelogsStep,
|
|
726
|
+
[MONOREPO_RELEASE_STEP.bumpPackages]: bumpPackagesStep,
|
|
727
|
+
[MONOREPO_RELEASE_STEP.pushPackages]: pushPackagesStep,
|
|
728
|
+
[MONOREPO_RELEASE_STEP.publishPackages]: publishPackagesStep,
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Renders the closing summary.
|
|
733
|
+
*
|
|
734
|
+
* @param {MonorepoContext} context - Context.
|
|
735
|
+
* @param {string} remoteUrl - `origin` URL.
|
|
736
|
+
* @param {number} startedAt - Start timestamp.
|
|
737
|
+
* @returns {string} Box.
|
|
738
|
+
*/
|
|
739
|
+
function renderMonorepoSummary(context, remoteUrl, startedAt) {
|
|
740
|
+
const releases = listRunReleases(context);
|
|
741
|
+
const githubRepository = GITHUB_REPOSITORY_PATTERN.exec(remoteUrl)?.[1];
|
|
742
|
+
const lines = releases.map((release) => {
|
|
743
|
+
const published = context.published.find((entry) => entry.name === release.name);
|
|
744
|
+
const npmNote = published?.registryUrl ? paint("gray", ` · ${published.registryLabel ?? "Registro"}: ${describePublishedRelease({ registryUrl: published.registryUrl, packageName: published.packageName ?? release.name, version: release.version })}`) : "";
|
|
745
|
+
return `${ICON.success} ${paint("bold", release.name)} ${paint(["bold", "greenBright"], release.version)} ${paint("gray", `(${release.tag})`)}${npmNote}`;
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (context.pushed) {
|
|
749
|
+
lines.push("", `${ICON.success} ${paint("bold", "Git")} ${MAIN_BRANCH} + ${releases.length} tag(s) en ${RELEASE_REMOTE}`);
|
|
750
|
+
}
|
|
751
|
+
if (githubRepository) {
|
|
752
|
+
lines.push(`${ICON.info} ${paint("bold", "Tags")} https://github.com/${githubRepository}/tags`);
|
|
753
|
+
}
|
|
754
|
+
for (const line of context.config.summary) {
|
|
755
|
+
const perPackage = line.includes(SUMMARY_VERSION_PLACEHOLDER) || line.includes(SUMMARY_PACKAGE_PLACEHOLDER);
|
|
756
|
+
const expanded = perPackage ? releases.map(({ name, version }) => line.replaceAll(SUMMARY_VERSION_PLACEHOLDER, version).replaceAll(SUMMARY_PACKAGE_PLACEHOLDER, name)) : [line];
|
|
757
|
+
lines.push(...expanded.map((expandedLine) => `${ICON.info} ${expandedLine}`));
|
|
758
|
+
}
|
|
759
|
+
lines.push("", paint("gray", `Tiempo total: ${formatDuration(measureActiveMs(startedAt))} (sin contar la espera de tus respuestas)`));
|
|
760
|
+
|
|
761
|
+
const outcome = context.published.length > 0 ? "publicados" : "releaseados (sin publicar)";
|
|
762
|
+
return renderBox({ title: `${ICON.rocket} ${releases.length} paquete(s) ${outcome}`, lines, tone: BOX_TONE.success });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Closes the failure box of a step: the packages already published and how to resume.
|
|
767
|
+
*
|
|
768
|
+
* @param {MonorepoContext} context - Context.
|
|
769
|
+
* @returns {Promise<string[]>} Lines appended after the failure and its hint.
|
|
770
|
+
*/
|
|
771
|
+
async function describeMonorepoFailureFollowUp(context) {
|
|
772
|
+
const publishedLines = context.published.length > 0 ? ["", `${ICON.warning} Ya se publicaron: ${context.published.map(({ name, version }) => `${name}@${version}`).join(", ")}.`] : [];
|
|
773
|
+
return [...publishedLines, "", paint("gray", `${context.commands.createVersion} retoma desde el primer paso que falte.`)];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Runs `create-version` in monorepo mode (`packages` in the configuration).
|
|
778
|
+
*
|
|
779
|
+
* @param {{
|
|
780
|
+
* repositoryRoot: string,
|
|
781
|
+
* config: ResolvedCreateVersionConfig,
|
|
782
|
+
* options: ReleaseOptions,
|
|
783
|
+
* startedAt: number,
|
|
784
|
+
* }} run - Repository, resolved configuration, command-line options and start time.
|
|
785
|
+
* @returns {Promise<number>} Exit code.
|
|
786
|
+
*/
|
|
787
|
+
export async function runMonorepoCreateVersion({ repositoryRoot, config, options: initialOptions, startedAt }) {
|
|
788
|
+
let options = initialOptions;
|
|
789
|
+
const commands = config.commands;
|
|
790
|
+
const tagFormat = /** @type {string} */ (config.tagFormat);
|
|
791
|
+
|
|
792
|
+
if (options.setVersion) {
|
|
793
|
+
print(`${ICON.failure} ${paint("red", `En un monorepo cada paquete tiene su versión: usá --${CREATE_VERSION_FLAG.bump} (para todos) o --${CREATE_VERSION_FLAG.acceptSuggested}.`)}`);
|
|
794
|
+
return FAILURE_EXIT_CODE;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const selectAllPackages = Boolean(options.bump || options.acceptSuggested);
|
|
798
|
+
|
|
799
|
+
let units;
|
|
800
|
+
try {
|
|
801
|
+
units = resolveReleaseUnits(discoverWorkspacePackages(repositoryRoot, /** @type {"workspaces" | string[]} */ (config.packages)));
|
|
802
|
+
} catch (error) {
|
|
803
|
+
print(`${ICON.failure} ${paint("red", describeError(error))}`);
|
|
804
|
+
return FAILURE_EXIT_CODE;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (units.length === 0) {
|
|
808
|
+
print(`${ICON.failure} ${paint("red", "No hay paquetes para publicar: todos los workspaces son private.")}`);
|
|
809
|
+
return FAILURE_EXIT_CODE;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const reader = createGitReader(repositoryRoot);
|
|
813
|
+
const remoteUrl = (await reader.tryGit(["remote", "get-url", RELEASE_REMOTE])) ?? "";
|
|
814
|
+
let capabilities = describeReleaseCapabilities(config);
|
|
815
|
+
const { migrations } = config;
|
|
816
|
+
const spinner = startSpinner("Diagnosticando el monorepo");
|
|
817
|
+
let state;
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
state = await collectMonorepoState({
|
|
821
|
+
repositoryRoot,
|
|
822
|
+
units,
|
|
823
|
+
tagFormat,
|
|
824
|
+
trackNpm: config.registry !== null,
|
|
825
|
+
registrySelection: selectProjectRegistry(config),
|
|
826
|
+
checkMigrations: migrations ? () => migrations.check(createHookContext(repositoryRoot, reader, null)) : null,
|
|
827
|
+
checkNpmAuthFor: (snapshot) =>
|
|
828
|
+
isRegistryProvider(config.publish) ? listPackagesToAuthenticate(buildMonorepoPlan(snapshot, capabilities, { tagFormat, ignoreLocalChanges: true, skipUnpublished: options.skipUnpublished, selectAllPackages })) : [],
|
|
829
|
+
onProgress: (label) => spinner.update(label),
|
|
830
|
+
});
|
|
831
|
+
spinner.succeed("Diagnóstico completo");
|
|
832
|
+
} catch (error) {
|
|
833
|
+
spinner.fail("No se pudo diagnosticar el monorepo");
|
|
834
|
+
print(paint("red", describeError(error)));
|
|
835
|
+
return FAILURE_EXIT_CODE;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const rootName = (() => {
|
|
839
|
+
try {
|
|
840
|
+
return String(readJsonManifest(repositoryRoot).name ?? "monorepo");
|
|
841
|
+
} catch {
|
|
842
|
+
return "monorepo";
|
|
843
|
+
}
|
|
844
|
+
})();
|
|
845
|
+
print(renderBanner({ projectName: config.projectName ?? rootName, publishedLabel: `${units.length} paquete(s)` }));
|
|
846
|
+
capabilities = describeReleaseCapabilities(config, state.packages.find((packageSnapshot) => packageSnapshot.npm?.registryLabel)?.npm?.registryLabel);
|
|
847
|
+
print(renderMonorepoDiagnosis(state, repositoryRoot));
|
|
848
|
+
|
|
849
|
+
let plan = buildMonorepoPlan(state, capabilities, { tagFormat, ignoreLocalChanges: options.ignoreLocalChanges, skipUnpublished: options.skipUnpublished, selectAllPackages });
|
|
850
|
+
|
|
851
|
+
if (plan.blockers.length > 0 && !options.ignoreLocalChanges && !options.dryRun && process.stdin.isTTY) {
|
|
852
|
+
const planIgnoringChanges = buildMonorepoPlan(state, capabilities, { tagFormat, ignoreLocalChanges: true, skipUnpublished: options.skipUnpublished, selectAllPackages });
|
|
853
|
+
if (planIgnoringChanges.blockers.length === 0 && planIgnoringChanges.steps.length > 0) {
|
|
854
|
+
if (!(await askToIgnoreLocalChanges(listMonorepoChangesToSetAside(state, planIgnoringChanges.mode)))) {
|
|
855
|
+
print(`${ICON.info} Release cancelado: no se tocó nada. Commiteá o guardá los cambios y volvé a correr ${commands.createVersion}.`);
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
options = { ...options, ignoreLocalChanges: true };
|
|
859
|
+
plan = planIgnoringChanges;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (plan.mode === RELEASE_MODE.upToDate) {
|
|
864
|
+
print(renderBox({ title: "Todo al día", lines: [`${ICON.success} Ningún paquete tiene cambios sin publicar.`, ...plan.warnings.map((warning) => `${ICON.warning} ${warning}`)], tone: BOX_TONE.success }));
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
print(renderPlan(plan));
|
|
869
|
+
|
|
870
|
+
if (plan.blockers.length > 0) {
|
|
871
|
+
return plan.blockers.some((blocker) => blocker.code === CHANGELOG_UPDATE_REQUIRED_CODE) ? FAILURE_EXIT_CODE : 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (options.dryRun) {
|
|
875
|
+
print(`${ICON.info} ${paint("cyan", `--dry-run: no se cambió nada. Corré ${commands.createVersion} para ejecutar el plan.`)}`);
|
|
876
|
+
return 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (plan.steps.some((planStep) => planStep.id === MONOREPO_RELEASE_STEP.chooseVersions) && !options.bump && !options.acceptSuggested && !process.stdin.isTTY) {
|
|
880
|
+
print(`${ICON.failure} ${paint("red", `Sin terminal interactiva no se puede elegir la versión de cada paquete: usá --${CREATE_VERSION_FLAG.bump} patch|minor|major o --${CREATE_VERSION_FLAG.acceptSuggested}.`)}`);
|
|
881
|
+
return FAILURE_EXIT_CODE;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** @type {MonorepoContext} */
|
|
885
|
+
const context = {
|
|
886
|
+
repositoryRoot,
|
|
887
|
+
config,
|
|
888
|
+
state,
|
|
889
|
+
options,
|
|
890
|
+
reader,
|
|
891
|
+
commands,
|
|
892
|
+
tagFormat,
|
|
893
|
+
units,
|
|
894
|
+
plan,
|
|
895
|
+
chosen: [],
|
|
896
|
+
changelogFiles: [],
|
|
897
|
+
preparedRoots: new Set(),
|
|
898
|
+
pushed: false,
|
|
899
|
+
published: [],
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const changesToSetAside = options.ignoreLocalChanges ? listMonorepoChangesToSetAside(state, plan.mode) : [];
|
|
903
|
+
|
|
904
|
+
return withLocalChangesSetAside(
|
|
905
|
+
{
|
|
906
|
+
reader,
|
|
907
|
+
repositoryRoot,
|
|
908
|
+
changeCount: changesToSetAside.length,
|
|
909
|
+
setAsideOptions: {
|
|
910
|
+
keptPaths: plan.mode === RELEASE_MODE.newRelease ? units.map((unit) => unit.changelogPath) : [],
|
|
911
|
+
createVersionCommand: commands.createVersion,
|
|
912
|
+
},
|
|
913
|
+
messages: {
|
|
914
|
+
setAside: "cambio(s) sin commitear apartados con git stash; al terminar vuelven igual.",
|
|
915
|
+
restored: "Cambios sin commitear restaurados como estaban.",
|
|
916
|
+
recovery: "recuperalos con git stash pop --index.",
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
() => runPlanSteps(context, {
|
|
920
|
+
steps: plan.steps,
|
|
921
|
+
executors: STEP_EXECUTORS,
|
|
922
|
+
untouchedNote: "No se tocaron versiones ni tags.",
|
|
923
|
+
describeFailureFollowUp: describeMonorepoFailureFollowUp,
|
|
924
|
+
renderSummary: (finishedContext) => (finishedContext.pushed || finishedContext.published.length > 0 ? renderMonorepoSummary(finishedContext, remoteUrl, startedAt) : null),
|
|
925
|
+
}, startedAt)
|
|
926
|
+
);
|
|
927
|
+
}
|