@mui/internal-code-infra 0.1.1-canary.20 → 0.1.1-canary.22
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/README.md +4 -1
- package/build/utils/pnpm.d.mts +67 -3
- package/package.json +2 -2
- package/src/cli/cmdPublish.mjs +17 -3
- package/src/cli/cmdPublishNewPackage.mjs +11 -4
- package/src/utils/pnpm.mjs +126 -25
package/README.md
CHANGED
|
@@ -24,12 +24,15 @@ This is stored in the `docs` top-level directory.
|
|
|
24
24
|
> - **Commit SHA to release from:** the commit that contains the merged release on master. This commit is linked to the GitHub release.
|
|
25
25
|
> - **Run in dry-run mode:** Used for debugging.
|
|
26
26
|
> - **Create GitHub release:** Keep selected if you want a GitHub release to be automatically created from the changelog.
|
|
27
|
-
> - **npm dist tag to publish to** Use to publish legacy or canary versions.
|
|
27
|
+
> - **npm dist tag to publish to** Use to publish legacy or canary versions. The publish fails before it publishes anything when a dist-tag would move to a lower version. `latest` and `latest-*` also cannot move to a prerelease unless they already point to one, and `latest-*` cannot change its major.
|
|
28
28
|
|
|
29
29
|
3. Click "Run workflow"
|
|
30
30
|
4. Refresh the page to see the newly created workflow, and click it.
|
|
31
31
|
5. The next screen shows "@username requested your review to deploy to npm-publish", click "Review deployments" and authorize your workflow run. **Never approve workflow runs you didn't initiaite.**
|
|
32
32
|
|
|
33
|
+
> [!NOTE]
|
|
34
|
+
> A CI dry run (`code-infra publish --ci --dry-run`) runs the same dist-tag check. On a branch that does not publish to `latest`, pass the dist-tag of the release to it, for example `--tag next` for prereleases or `--tag latest-v8` for a previous major.
|
|
35
|
+
|
|
33
36
|
> [!IMPORTANT]
|
|
34
37
|
> Go through the below steps if there is an error that says `The following packages are new and need to be published manually first` in the publish flow.
|
|
35
38
|
|
package/build/utils/pnpm.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/** Version that `publish-new-package` publishes to reserve the name of a new package. */
|
|
3
|
+
export declare const PLACEHOLDER_VERSION = "0.0.1";
|
|
2
4
|
export type PrivatePackage = {
|
|
3
5
|
/**
|
|
4
6
|
* - Package name
|
|
@@ -122,6 +124,43 @@ export declare function getPublishRegistry(packagePath: string): Promise<string>
|
|
|
122
124
|
* @returns {Promise<string | null>} Version string, or null when absent/invalid
|
|
123
125
|
*/
|
|
124
126
|
export declare function getReleaseVersion(cwd?: string): Promise<string | null>;
|
|
127
|
+
export type RegistryPackage = {
|
|
128
|
+
/**
|
|
129
|
+
* - Registry that the package publishes to
|
|
130
|
+
*/
|
|
131
|
+
registry: string;
|
|
132
|
+
/**
|
|
133
|
+
* - Whether the package is on the registry
|
|
134
|
+
*/
|
|
135
|
+
exists: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* - Dist-tags of the package
|
|
138
|
+
*/
|
|
139
|
+
distTags: Record<string, string>;
|
|
140
|
+
/**
|
|
141
|
+
* - Published versions of the package
|
|
142
|
+
*/
|
|
143
|
+
versions: string[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* @typedef {Object} RegistryPackage
|
|
147
|
+
* @property {string} registry - Registry that the package publishes to
|
|
148
|
+
* @property {boolean} exists - Whether the package is on the registry
|
|
149
|
+
* @property {Record<string, string>} distTags - Dist-tags of the package
|
|
150
|
+
* @property {string[]} versions - Published versions of the package
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* Read each package from its publish registry with `pnpm view`, so the lookup uses the
|
|
154
|
+
* same registry configuration and credentials as `pnpm publish`.
|
|
155
|
+
*
|
|
156
|
+
* @param {PublicPackage[]} packages - Packages to read
|
|
157
|
+
* @param {{ npmOnly?: boolean }} [options] - `npmOnly` reads only the packages that publish to npm.
|
|
158
|
+
* Checks that only concern npm use it, so a missing credential for another registry does not block them.
|
|
159
|
+
* @returns {Promise<Map<string, RegistryPackage>>} By package name
|
|
160
|
+
*/
|
|
161
|
+
export declare function getRegistryPackages(packages: PublicPackage[], { npmOnly }?: {
|
|
162
|
+
npmOnly?: boolean;
|
|
163
|
+
}): Promise<Map<string, RegistryPackage>>;
|
|
125
164
|
/**
|
|
126
165
|
* Filter to the packages that must be published by hand before CI can take over.
|
|
127
166
|
*
|
|
@@ -131,14 +170,39 @@ export declare function getReleaseVersion(cwd?: string): Promise<string | null>;
|
|
|
131
170
|
* midway through. See {@link getPackageVersionInfo} for the version-level check.
|
|
132
171
|
*
|
|
133
172
|
* @param {PublicPackage[]} packages - Packages to check
|
|
134
|
-
* @
|
|
173
|
+
* @param {Map<string, RegistryPackage>} registryPackages - Registry data from {@link getRegistryPackages}
|
|
174
|
+
* @returns {PublicPackage[]} The subset needing a manual first publish
|
|
175
|
+
*/
|
|
176
|
+
export declare function getPackagesNeedingManualPublish(packages: PublicPackage[], registryPackages: Map<string, RegistryPackage>): PublicPackage[];
|
|
177
|
+
export type DistTagConflict = {
|
|
178
|
+
/**
|
|
179
|
+
* - Package to publish
|
|
180
|
+
*/
|
|
181
|
+
pkg: PublicPackage;
|
|
182
|
+
/**
|
|
183
|
+
* - Why the dist-tag would move to the wrong release line
|
|
184
|
+
*/
|
|
185
|
+
reason: string;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* @typedef {Object} DistTagConflict
|
|
189
|
+
* @property {PublicPackage} pkg - Package to publish
|
|
190
|
+
* @property {string} reason - Why the dist-tag would move to the wrong release line
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* Find the packages whose publish with `tag` would move the dist-tag to the wrong release line.
|
|
194
|
+
*
|
|
195
|
+
* @param {PublicPackage[]} packages - Packages to publish
|
|
196
|
+
* @param {string} tag - Dist-tag to publish with
|
|
197
|
+
* @param {Map<string, RegistryPackage>} registryPackages - Registry data from {@link getRegistryPackages}
|
|
198
|
+
* @returns {DistTagConflict[]}
|
|
135
199
|
*/
|
|
136
|
-
export declare function
|
|
200
|
+
export declare function getDistTagConflicts(packages: PublicPackage[], tag: string, registryPackages: Map<string, RegistryPackage>): DistTagConflict[];
|
|
137
201
|
/**
|
|
138
202
|
* Get package version info from registry.
|
|
139
203
|
*
|
|
140
204
|
* Resolves through `pnpm view`, which reports a lookup failure the same way it
|
|
141
|
-
* reports a missing version. Use {@link
|
|
205
|
+
* reports a missing version. Use {@link getRegistryPackages} where
|
|
142
206
|
* absence has to be told apart from an error.
|
|
143
207
|
*
|
|
144
208
|
* @param {string} packageName - Name of the package
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.1.1-canary.
|
|
3
|
+
"version": "0.1.1-canary.22",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"publishConfig": {
|
|
191
191
|
"access": "public"
|
|
192
192
|
},
|
|
193
|
-
"gitSha": "
|
|
193
|
+
"gitSha": "7be938414716e74df30e861271c65d7885da8c98",
|
|
194
194
|
"scripts": {
|
|
195
195
|
"build": "tsc -p tsconfig.build.json",
|
|
196
196
|
"typescript": "tsc -noEmit",
|
package/src/cli/cmdPublish.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import * as semver from 'semver';
|
|
|
19
19
|
|
|
20
20
|
import { persistentAuthStrategy } from '../utils/github.mjs';
|
|
21
21
|
import {
|
|
22
|
+
getDistTagConflicts,
|
|
22
23
|
getPackagesNeedingManualPublish,
|
|
24
|
+
getRegistryPackages,
|
|
23
25
|
getReleaseVersion,
|
|
24
26
|
getWorkspacePackages,
|
|
25
27
|
publishPackages,
|
|
@@ -364,7 +366,8 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
364
366
|
githubReleaseData = await validateGitHubRelease(version);
|
|
365
367
|
}
|
|
366
368
|
|
|
367
|
-
const
|
|
369
|
+
const registryPackages = await getRegistryPackages(filteredPackages);
|
|
370
|
+
const newPackages = getPackagesNeedingManualPublish(filteredPackages, registryPackages);
|
|
368
371
|
|
|
369
372
|
if (newPackages.length > 0) {
|
|
370
373
|
const newPackageNames = newPackages.map((pkg) => pkg.name).join(', ');
|
|
@@ -373,6 +376,15 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
373
376
|
);
|
|
374
377
|
}
|
|
375
378
|
|
|
379
|
+
const distTagConflicts = getDistTagConflicts(filteredPackages, tag, registryPackages);
|
|
380
|
+
if (distTagConflicts.length > 0) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Publishing with the "${tag}" dist-tag would move it to the wrong release line:
|
|
383
|
+
${distTagConflicts.map(({ pkg, reason }) => ` * ${pkg.name}: ${reason}`).join('\n')}
|
|
384
|
+
Publish a previous major with its "latest-vX" dist-tag and a prerelease of the next major with "next". On a release branch, also make that dist-tag the "dist-tag" default of the publish workflow. If the dist-tag on npm is outdated, fix it with "npm dist-tag" first.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
376
388
|
// Publish to npm (pnpm handles duplicate checking automatically)
|
|
377
389
|
// No git checks, we'll do our own
|
|
378
390
|
console.log('\n📦 Publishing packages to npm...');
|
|
@@ -426,8 +438,10 @@ const PUBLISH_WORKFLOW_ID = `.github/${WORKFLOW_PATH}`;
|
|
|
426
438
|
*/
|
|
427
439
|
async function triggerLocalGithubPublishWorkflow(opts) {
|
|
428
440
|
console.log(`🔍 Checking if there are new packages to publish in the workspace...`);
|
|
429
|
-
const
|
|
430
|
-
|
|
441
|
+
const packages = await getWorkspacePackages({ publicOnly: true });
|
|
442
|
+
const newPackages = getPackagesNeedingManualPublish(
|
|
443
|
+
packages,
|
|
444
|
+
await getRegistryPackages(packages, { npmOnly: true }),
|
|
431
445
|
);
|
|
432
446
|
if (newPackages.length) {
|
|
433
447
|
console.warn(
|
|
@@ -11,7 +11,12 @@ import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
|
|
|
11
11
|
|
|
12
12
|
import { getRepositoryInfo } from '../utils/git.mjs';
|
|
13
13
|
import { toPosixPath } from '../utils/path.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPackagesNeedingManualPublish,
|
|
16
|
+
getRegistryPackages,
|
|
17
|
+
getWorkspacePackages,
|
|
18
|
+
PLACEHOLDER_VERSION,
|
|
19
|
+
} from '../utils/pnpm.mjs';
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* @typedef {Object} Args
|
|
@@ -46,8 +51,10 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
46
51
|
}),
|
|
47
52
|
async handler(args) {
|
|
48
53
|
console.log(`🔍 Detecting new packages to publish in workspace...`);
|
|
49
|
-
const
|
|
50
|
-
|
|
54
|
+
const packages = await getWorkspacePackages({ publicOnly: true });
|
|
55
|
+
const newPackages = getPackagesNeedingManualPublish(
|
|
56
|
+
packages,
|
|
57
|
+
await getRegistryPackages(packages, { npmOnly: true }),
|
|
51
58
|
);
|
|
52
59
|
|
|
53
60
|
if (!newPackages.length) {
|
|
@@ -78,7 +85,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
78
85
|
const repo = await getRepositoryInfo();
|
|
79
86
|
const packageJson = {
|
|
80
87
|
name: pkg.name,
|
|
81
|
-
version:
|
|
88
|
+
version: PLACEHOLDER_VERSION,
|
|
82
89
|
repository: {
|
|
83
90
|
type: 'git',
|
|
84
91
|
url: `git+https://github.com/${repo.owner}/${repo.repo}.git`,
|
package/src/utils/pnpm.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import { parseDocument, isMap } from 'yaml';
|
|
|
12
12
|
/** Canonical npm registry URL, in the form `getPublishRegistry` returns. */
|
|
13
13
|
const NPMJS_REGISTRY = 'https://registry.npmjs.org/';
|
|
14
14
|
|
|
15
|
+
/** Version that `publish-new-package` publishes to reserve the name of a new package. */
|
|
16
|
+
export const PLACEHOLDER_VERSION = '0.0.1';
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* @typedef {Object} PrivatePackage
|
|
17
20
|
* @property {string} [name] - Package name
|
|
@@ -189,49 +192,147 @@ function requiresTrustedPublisherBootstrap(registry) {
|
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
|
|
195
|
+
* @typedef {Object} RegistryPackage
|
|
196
|
+
* @property {string} registry - Registry that the package publishes to
|
|
197
|
+
* @property {boolean} exists - Whether the package is on the registry
|
|
198
|
+
* @property {Record<string, string>} distTags - Dist-tags of the package
|
|
199
|
+
* @property {string[]} versions - Published versions of the package
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read each package from its publish registry with `pnpm view`, so the lookup uses the
|
|
204
|
+
* same registry configuration and credentials as `pnpm publish`.
|
|
198
205
|
*
|
|
199
|
-
* @param {PublicPackage[]} packages - Packages to
|
|
200
|
-
* @
|
|
206
|
+
* @param {PublicPackage[]} packages - Packages to read
|
|
207
|
+
* @param {{ npmOnly?: boolean }} [options] - `npmOnly` reads only the packages that publish to npm.
|
|
208
|
+
* Checks that only concern npm use it, so a missing credential for another registry does not block them.
|
|
209
|
+
* @returns {Promise<Map<string, RegistryPackage>>} By package name
|
|
201
210
|
*/
|
|
202
|
-
export async function
|
|
203
|
-
const
|
|
211
|
+
export async function getRegistryPackages(packages, { npmOnly = false } = {}) {
|
|
212
|
+
const entries = await Promise.all(
|
|
204
213
|
packages.map(async (pkg) => {
|
|
205
214
|
const registry = await getPublishRegistry(pkg.path);
|
|
206
|
-
if (
|
|
207
|
-
return
|
|
215
|
+
if (npmOnly && registry !== NPMJS_REGISTRY) {
|
|
216
|
+
return null;
|
|
208
217
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
218
|
+
const { exitCode, stdout, stderr } = await $({
|
|
219
|
+
cwd: pkg.path,
|
|
220
|
+
reject: false,
|
|
221
|
+
// pnpm resolves its own `packageManager` version from `--registry`, which may not serve it.
|
|
222
|
+
env: { PNPM_CONFIG_PM_ON_FAIL: 'ignore' },
|
|
223
|
+
})`pnpm view ${pkg.name} dist-tags versions --json --registry ${registry}`;
|
|
224
|
+
let info;
|
|
225
|
+
try {
|
|
226
|
+
info = JSON.parse(stdout || '{}');
|
|
227
|
+
} catch {
|
|
228
|
+
info = {};
|
|
214
229
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// silently disable the check, so fail loudly instead.
|
|
230
|
+
// Anything but a 404 tells nothing about whether the package exists. Treating it as
|
|
231
|
+
// "not published" would silently disable the checks, so fail loudly instead.
|
|
232
|
+
if (exitCode !== 0 && info.error?.code !== 'ERR_PNPM_FETCH_404') {
|
|
219
233
|
throw new Error(
|
|
220
|
-
`Failed to
|
|
234
|
+
`Failed to read ${pkg.name} from ${registry}: ${info.error?.message ?? stderr}`,
|
|
221
235
|
);
|
|
222
236
|
}
|
|
223
|
-
|
|
237
|
+
/** @type {RegistryPackage} */
|
|
238
|
+
const registryPackage = {
|
|
239
|
+
registry,
|
|
240
|
+
exists: exitCode === 0,
|
|
241
|
+
distTags: info['dist-tags'] ?? {},
|
|
242
|
+
versions: [info.versions ?? []].flat(),
|
|
243
|
+
};
|
|
244
|
+
return /** @type {const} */ ([pkg.name, registryPackage]);
|
|
224
245
|
}),
|
|
225
246
|
);
|
|
247
|
+
return new Map(entries.filter((entry) => entry !== null));
|
|
248
|
+
}
|
|
226
249
|
|
|
227
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Filter to the packages that must be published by hand before CI can take over.
|
|
252
|
+
*
|
|
253
|
+
* A brand new package has to be pushed once manually (see
|
|
254
|
+
* `code-infra publish-new-package`) before the OIDC-based workflow can publish
|
|
255
|
+
* it, so the release fails with a clear message instead of a confusing 404
|
|
256
|
+
* midway through. See {@link getPackageVersionInfo} for the version-level check.
|
|
257
|
+
*
|
|
258
|
+
* @param {PublicPackage[]} packages - Packages to check
|
|
259
|
+
* @param {Map<string, RegistryPackage>} registryPackages - Registry data from {@link getRegistryPackages}
|
|
260
|
+
* @returns {PublicPackage[]} The subset needing a manual first publish
|
|
261
|
+
*/
|
|
262
|
+
export function getPackagesNeedingManualPublish(packages, registryPackages) {
|
|
263
|
+
return packages.filter((pkg) => {
|
|
264
|
+
const registryPackage = registryPackages.get(pkg.name);
|
|
265
|
+
return (
|
|
266
|
+
registryPackage !== undefined &&
|
|
267
|
+
!registryPackage.exists &&
|
|
268
|
+
requiresTrustedPublisherBootstrap(registryPackage.registry)
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Why publishing `version` with `tag` would move the dist-tag to the wrong release line.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} tag - Dist-tag to publish with
|
|
277
|
+
* @param {string} version - Version to publish
|
|
278
|
+
* @param {string} current - Version that the dist-tag points to on the registry
|
|
279
|
+
* @returns {string | null} The reason, or null when the publish is safe
|
|
280
|
+
*/
|
|
281
|
+
function getDistTagConflict(tag, version, current) {
|
|
282
|
+
if (semver.lt(version, current)) {
|
|
283
|
+
return `"${tag}" can only move to a newer version, but ${version} is lower than the current ${current}`;
|
|
284
|
+
}
|
|
285
|
+
// A tag that already points to a prerelease can move to the next one, for packages still in beta.
|
|
286
|
+
if (
|
|
287
|
+
(tag === 'latest' || tag.startsWith('latest-')) &&
|
|
288
|
+
semver.prerelease(version) &&
|
|
289
|
+
!semver.prerelease(current)
|
|
290
|
+
) {
|
|
291
|
+
return `"${tag}" cannot point to a prerelease (${version})`;
|
|
292
|
+
}
|
|
293
|
+
if (tag.startsWith('latest-') && semver.major(version) !== semver.major(current)) {
|
|
294
|
+
return `"${tag}" cannot cross a major (${current} to ${version})`;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @typedef {Object} DistTagConflict
|
|
301
|
+
* @property {PublicPackage} pkg - Package to publish
|
|
302
|
+
* @property {string} reason - Why the dist-tag would move to the wrong release line
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Find the packages whose publish with `tag` would move the dist-tag to the wrong release line.
|
|
307
|
+
*
|
|
308
|
+
* @param {PublicPackage[]} packages - Packages to publish
|
|
309
|
+
* @param {string} tag - Dist-tag to publish with
|
|
310
|
+
* @param {Map<string, RegistryPackage>} registryPackages - Registry data from {@link getRegistryPackages}
|
|
311
|
+
* @returns {DistTagConflict[]}
|
|
312
|
+
*/
|
|
313
|
+
export function getDistTagConflicts(packages, tag, registryPackages) {
|
|
314
|
+
return packages.flatMap((pkg) => {
|
|
315
|
+
const registryPackage = registryPackages.get(pkg.name);
|
|
316
|
+
// pnpm skips versions that are already published, such as unchanged packages on a legacy branch.
|
|
317
|
+
if (!registryPackage || registryPackage.versions.includes(pkg.version)) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
// A package that is not on the registry yet has no dist-tags to protect, and npm points
|
|
321
|
+
// `latest` at the first version, which is the placeholder for a new package.
|
|
322
|
+
const current = semver.valid(registryPackage.distTags[tag]);
|
|
323
|
+
if (!current || current === PLACEHOLDER_VERSION) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
const reason = getDistTagConflict(tag, pkg.version, current);
|
|
327
|
+
return reason ? [{ pkg, reason }] : [];
|
|
328
|
+
});
|
|
228
329
|
}
|
|
229
330
|
|
|
230
331
|
/**
|
|
231
332
|
* Get package version info from registry.
|
|
232
333
|
*
|
|
233
334
|
* Resolves through `pnpm view`, which reports a lookup failure the same way it
|
|
234
|
-
* reports a missing version. Use {@link
|
|
335
|
+
* reports a missing version. Use {@link getRegistryPackages} where
|
|
235
336
|
* absence has to be told apart from an error.
|
|
236
337
|
*
|
|
237
338
|
* @param {string} packageName - Name of the package
|