@rtorcato/repo-tooling 4.4.0 → 4.6.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/AGENTS.md +1 -1
- package/dist/base/checks.js +47 -19
- package/dist/base/ci.js +6 -2
- package/dist/cli/commands/doctor.js +10 -27
- package/dist/cli/commands/fix-targets.js +2 -0
- package/dist/cli/commands/setup.js +27 -1
- package/dist/cli/generators/badges.js +4 -2
- package/dist/cli/generators/docs-site.js +15 -5
- package/dist/languages/js/ci.js +24 -2
- package/dist/languages/js/fixers.js +3 -1
- package/dist/languages/js/npm-trust.js +351 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -92,7 +92,7 @@ npx @rtorcato/repo-tooling fix bun --yes --json # Bun runtime/test confi
|
|
|
92
92
|
|
|
93
93
|
`fix` defaults the confirm prompt to **No** for drift cases (existing file that doesn't extend our preset). The `--yes` flag is required to overwrite drift. Safe-merge fixers (`biome`, `engines`, `husky`, `package-json`) never overwrite — they add/merge — and use friendlier prompt wording. `fix --json` implies `--yes` (prompts would corrupt JSON output).
|
|
94
94
|
|
|
95
|
-
Fixers marked `explicitOnly` are exempt from `fix` all *and* from `fix --yes` — they only run when named as the target. Today that is `release-environment` (changes what a merge does).
|
|
95
|
+
Fixers marked `explicitOnly` are exempt from `fix` all *and* from `fix --yes` — they only run when named as the target. Today that is `release-environment` (changes what a merge does) and `npm-trusted-publisher` (registers an OIDC trusted publisher on npm via `npm trust github`; refuses when the package isn't on npm yet, local npm < 11.15.0, or not logged in).
|
|
96
96
|
|
|
97
97
|
The same goes for every `optional-missing` finding: a bulk `fix` records it `skipped`, because optional tools are often mutually exclusive (Biome / ESLint / Prettier / Oxlint, semantic-release / Changesets / Release Please) and installing them all is never the intent (#630). Name the one you want — `fix editorconfig --yes`.
|
|
98
98
|
|
package/dist/base/checks.js
CHANGED
|
@@ -142,11 +142,34 @@ export async function checkBrand(dir) {
|
|
|
142
142
|
if (problems.length > 0) {
|
|
143
143
|
return { check, status: 'drift', detail: problems.join('; '), hint: BRAND_HINT };
|
|
144
144
|
}
|
|
145
|
+
// Name the pieces still missing (#679). Severity is unchanged: brand is optional.
|
|
146
|
+
const missing = [];
|
|
147
|
+
if (!(await has('brand/favicon.svg')))
|
|
148
|
+
missing.push('favicon (brand/favicon.svg)');
|
|
149
|
+
const unrendered = [];
|
|
150
|
+
for (const n of ['banner', 'banner-mobile', 'social-card', 'favicon-512']) {
|
|
151
|
+
if (!(await has(`brand/${n}.png`)))
|
|
152
|
+
unrendered.push(`${n}.png`);
|
|
153
|
+
}
|
|
154
|
+
if (unrendered.length > 0)
|
|
155
|
+
missing.push(`rendered PNGs (brand/${unrendered.join(', ')})`);
|
|
156
|
+
const readmeFile = path.join(dir, 'README.md');
|
|
157
|
+
const readmeText = (await fs.pathExists(readmeFile)) ? await fs.readFile(readmeFile, 'utf-8') : '';
|
|
158
|
+
if (!/brand\/banner(?:-mobile)?\.png/.test(readmeText))
|
|
159
|
+
missing.push('README banner');
|
|
145
160
|
if (!brandDir) {
|
|
146
161
|
return {
|
|
147
162
|
check,
|
|
148
163
|
status: 'optional-missing',
|
|
149
|
-
detail:
|
|
164
|
+
detail: `no brand/ folder — missing: ${missing.join('; ')}`,
|
|
165
|
+
hint: BRAND_HINT,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (missing.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
check,
|
|
171
|
+
status: 'ok',
|
|
172
|
+
detail: `brand/ holds the SVG sources and render.sh; missing: ${missing.join('; ')}`,
|
|
150
173
|
hint: BRAND_HINT,
|
|
151
174
|
};
|
|
152
175
|
}
|
|
@@ -380,6 +403,22 @@ export async function checkCodeQL(dir, languages) {
|
|
|
380
403
|
hint: 'Run `npx @rtorcato/repo-tooling fix codeql` to scaffold CodeQL security scanning',
|
|
381
404
|
};
|
|
382
405
|
}
|
|
406
|
+
/** The workflow file that uploads coverage via codecov-action, or null when none does. */
|
|
407
|
+
export async function coverageUploadWorkflow(dir) {
|
|
408
|
+
const workflowsDir = path.join(dir, '.github', 'workflows');
|
|
409
|
+
try {
|
|
410
|
+
const files = (await fs.readdir(workflowsDir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
411
|
+
for (const f of files) {
|
|
412
|
+
const content = await fs.readFile(path.join(workflowsDir, f), 'utf-8');
|
|
413
|
+
if (/codecov\/codecov-action/.test(content))
|
|
414
|
+
return f;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// no workflows dir
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
383
422
|
// A README that advertises a Codecov badge but a CI that never uploads coverage
|
|
384
423
|
// leaves the badge permanently red. Only flags when the badge is actually present
|
|
385
424
|
// (no badge → nothing to back, so it's not applicable).
|
|
@@ -393,24 +432,13 @@ export async function checkCoverageUpload(dir) {
|
|
|
393
432
|
detail: 'no coverage badge in README (nothing to back)',
|
|
394
433
|
};
|
|
395
434
|
}
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
check: 'Coverage upload',
|
|
405
|
-
status: 'ok',
|
|
406
|
-
detail: `coverage badge backed by codecov-action in .github/workflows/${f}`,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
// fall through to drift
|
|
413
|
-
}
|
|
435
|
+
const workflow = await coverageUploadWorkflow(dir);
|
|
436
|
+
if (workflow) {
|
|
437
|
+
return {
|
|
438
|
+
check: 'Coverage upload',
|
|
439
|
+
status: 'ok',
|
|
440
|
+
detail: `coverage badge backed by codecov-action in .github/workflows/${workflow}`,
|
|
441
|
+
};
|
|
414
442
|
}
|
|
415
443
|
return {
|
|
416
444
|
check: 'Coverage upload',
|
package/dist/base/ci.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
/** Every job is gated on the skip-CI check; extra conditions are ANDed on. */
|
|
14
14
|
const SKIP_GUARD = "needs.check-skip.outputs.should-skip != 'true'";
|
|
15
15
|
/** Header through `jobs:` — triggers and concurrency are language-independent. */
|
|
16
|
-
|
|
16
|
+
/** The generated CI workflow's `name:` — the docs workflow's `workflow_run` must match it. */
|
|
17
|
+
export const CI_WORKFLOW_NAME = '🚀 CI/CD Pipeline';
|
|
18
|
+
const WORKFLOW_HEADER = `name: ${CI_WORKFLOW_NAME}
|
|
17
19
|
|
|
18
20
|
on:
|
|
19
21
|
push:
|
|
@@ -24,7 +26,9 @@ on:
|
|
|
24
26
|
|
|
25
27
|
concurrency:
|
|
26
28
|
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
27
|
-
cancel
|
|
29
|
+
# Never cancel on main: a newer push would kill a release that is waiting on
|
|
30
|
+
# approval or halfway through publishing.
|
|
31
|
+
cancel-in-progress: \${{ github.event_name == 'pull_request' }}
|
|
28
32
|
|
|
29
33
|
jobs:
|
|
30
34
|
`;
|
|
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import { renderGitHubWorkflow } from '../../base/ci.js';
|
|
5
5
|
import { githubJobs, scriptsOf } from '../../languages/js/ci.js';
|
|
6
6
|
import { inferProjectConfig } from '../../languages/js/fixers.js';
|
|
7
|
+
import { checkNpmTrustedPublisher, checkPublishJob, findNpmPublishJob, NPM_OIDC_CHECK, } from '../../languages/js/npm-trust.js';
|
|
7
8
|
import { PERL_GIT_HOOKS, runPerlChecks } from '../../languages/perl/checks.js';
|
|
8
9
|
import { readPerlProject, renderPerlWorkflow } from '../../languages/perl/ci.js';
|
|
9
10
|
import { PYTHON_GIT_HOOKS, runPythonChecks } from '../../languages/python/checks.js';
|
|
@@ -68,38 +69,19 @@ async function checkReleaseToken(dir) {
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
//
|
|
72
|
-
// NPM_TOKEN secret
|
|
73
|
-
//
|
|
74
|
-
// relevant for public packages that actually publish to npm.
|
|
72
|
+
// OIDC trusted publishing (#201, #687): the publishing job must not use an
|
|
73
|
+
// NPM_TOKEN secret, must carry `id-token: write`, and must run npm ≥ 11.5.1.
|
|
74
|
+
// Only relevant for public packages that actually publish to npm.
|
|
75
75
|
async function checkNpmOidcPublish(dir, pkg) {
|
|
76
|
-
const check =
|
|
76
|
+
const check = NPM_OIDC_CHECK;
|
|
77
77
|
if (!pkg || pkg.private === true) {
|
|
78
78
|
return { check, status: 'optional-missing', detail: 'private package — no npm publish' };
|
|
79
79
|
}
|
|
80
|
-
const workflowsDir = path.join(dir, '.github', 'workflows');
|
|
81
|
-
if (!(await fs.pathExists(workflowsDir))) {
|
|
82
|
-
return { check, status: 'optional-missing', detail: 'no .github/workflows/' };
|
|
83
|
-
}
|
|
84
80
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const content = await fs.readFile(path.join(workflowsDir, f), 'utf-8');
|
|
90
|
-
if (!/semantic-release/.test(content))
|
|
91
|
-
continue;
|
|
92
|
-
if (/secrets\.NPM_TOKEN/.test(content)) {
|
|
93
|
-
return {
|
|
94
|
-
check,
|
|
95
|
-
status: 'drift',
|
|
96
|
-
detail: `${f} authenticates npm publish with NPM_TOKEN`,
|
|
97
|
-
hint: 'Migrate to OIDC trusted publishing: add a Trusted Publisher for each published package on npmjs.com (Settings → Trusted Publisher), then run `fix github-actions` to drop NPM_TOKEN (the release job keeps `id-token: write`). npm is deprecating 2FA-bypass tokens.',
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
return { check, status: 'ok', detail: `${f} publishes via OIDC (no NPM_TOKEN)` };
|
|
101
|
-
}
|
|
102
|
-
return { check, status: 'optional-missing', detail: 'no semantic-release workflow found' };
|
|
81
|
+
const job = await findNpmPublishJob(dir);
|
|
82
|
+
if (!job)
|
|
83
|
+
return { check, status: 'optional-missing', detail: 'no semantic-release workflow found' };
|
|
84
|
+
return checkPublishJob(job);
|
|
103
85
|
}
|
|
104
86
|
catch {
|
|
105
87
|
return { check, status: 'optional-missing', detail: 'unable to read .github/workflows/' };
|
|
@@ -377,6 +359,7 @@ export async function runDoctor(dir) {
|
|
|
377
359
|
results.push(await checkSizeLimit(targetDir, pkg));
|
|
378
360
|
results.push(await checkReleaseToken(targetDir));
|
|
379
361
|
results.push(await checkNpmOidcPublish(targetDir, pkg));
|
|
362
|
+
results.push(await checkNpmTrustedPublisher(targetDir, pkg));
|
|
380
363
|
results.push(await checkTypedoc(targetDir, pkg));
|
|
381
364
|
results.push(await checkAreTheTypesWrong(targetDir, pkg));
|
|
382
365
|
results.push(await checkPublint(targetDir, pkg));
|
|
@@ -22,6 +22,8 @@ export const FIX_TARGETS = {
|
|
|
22
22
|
'Tree-shake check': 'treeshake-check',
|
|
23
23
|
'GitHub Actions': 'github-actions',
|
|
24
24
|
'Coverage upload': 'github-actions',
|
|
25
|
+
'npm OIDC publish': 'github-actions',
|
|
26
|
+
'npm trusted publisher': 'npm-trusted-publisher',
|
|
25
27
|
Dependabot: 'dependabot',
|
|
26
28
|
CodeQL: 'codeql',
|
|
27
29
|
'Branch protection': 'github-settings',
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
+
import { formatNpmPublishGuide, npmPublishGuide, } from '../../languages/js/npm-trust.js';
|
|
5
6
|
import { LANGUAGES } from '../../languages/registry.js';
|
|
6
7
|
import { generateConfigs } from '../generators/index.js';
|
|
7
8
|
import { detectLanguage } from '../utils/detect-language.js';
|
|
@@ -157,7 +158,8 @@ export async function setupProject(options) {
|
|
|
157
158
|
return;
|
|
158
159
|
if (dryRun) {
|
|
159
160
|
const files = computeFileList(config);
|
|
160
|
-
|
|
161
|
+
const npmPublish = npmPublishFor(config);
|
|
162
|
+
console.log(JSON.stringify({ directory: targetDir, config, files, ...(npmPublish && { npmPublish }) }, null, 2));
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
163
165
|
if (!interactive) {
|
|
@@ -561,6 +563,23 @@ async function promptForConfig(targetDir, seed) {
|
|
|
561
563
|
bun: answers.bun ?? false,
|
|
562
564
|
};
|
|
563
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* The npmjs.com trusted-publisher values for a scaffold whose release job
|
|
568
|
+
* publishes (#687). Owner/repo aren't known yet at setup time, so they stay
|
|
569
|
+
* placeholders; `fix npm-trusted-publisher` derives them once the repo exists.
|
|
570
|
+
* The generated job declares no environment until `fix release-environment`.
|
|
571
|
+
*/
|
|
572
|
+
export function npmPublishFor(config) {
|
|
573
|
+
if (config.language === 'swift' || config.projectType !== 'library' || !config.semanticRelease)
|
|
574
|
+
return null;
|
|
575
|
+
return npmPublishGuide({
|
|
576
|
+
name: config.projectName,
|
|
577
|
+
owner: '<owner>',
|
|
578
|
+
repo: '<repo>',
|
|
579
|
+
file: 'ci.yml',
|
|
580
|
+
environment: null,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
564
583
|
function showNextSteps(config, _targetDir) {
|
|
565
584
|
console.log(chalk.bold('\n📋 Next Steps:\n'));
|
|
566
585
|
const steps = [];
|
|
@@ -589,6 +608,13 @@ function showNextSteps(config, _targetDir) {
|
|
|
589
608
|
steps.forEach((step, index) => {
|
|
590
609
|
console.log(` ${index + 1}. ${step}`);
|
|
591
610
|
});
|
|
611
|
+
// A brand-new package isn't on npm yet, so the bootstrap order always applies.
|
|
612
|
+
const npmPublish = npmPublishFor(config);
|
|
613
|
+
if (npmPublish) {
|
|
614
|
+
console.log(chalk.bold('\n📦 Publish to npm (OIDC trusted publishing — no NPM_TOKEN):\n'));
|
|
615
|
+
for (const line of formatNpmPublishGuide(npmPublish, true))
|
|
616
|
+
console.log(` ${line}`);
|
|
617
|
+
}
|
|
592
618
|
const skipped = collectSkippedFixSuggestions(config);
|
|
593
619
|
if (skipped.length > 0) {
|
|
594
620
|
console.log(chalk.bold('\n💡 Want to add something you skipped?\n'));
|
|
@@ -31,7 +31,7 @@ export function parseRepository(repository) {
|
|
|
31
31
|
* repo-specific is derivable, so a badge row isn't worth adding.
|
|
32
32
|
*/
|
|
33
33
|
export function buildBadgeRow(inputs) {
|
|
34
|
-
const { name, owner, repo, branch = 'main', isPrivate = false } = inputs;
|
|
34
|
+
const { name, owner, repo, branch = 'main', isPrivate = false, bundled = true, uploadsCoverage = true, } = inputs;
|
|
35
35
|
const slug = owner && repo ? `${owner}/${repo}` : null;
|
|
36
36
|
const badges = [];
|
|
37
37
|
let specific = false;
|
|
@@ -43,9 +43,11 @@ export function buildBadgeRow(inputs) {
|
|
|
43
43
|
specific = true;
|
|
44
44
|
badges.push(`[](https://www.npmjs.com/package/${name})`);
|
|
45
45
|
badges.push(`[](https://www.npmjs.com/package/${name})`);
|
|
46
|
+
}
|
|
47
|
+
if (!isPrivate && name && bundled) {
|
|
46
48
|
badges.push(`[](https://bundlephobia.com/package/${name})`);
|
|
47
49
|
}
|
|
48
|
-
if (!isPrivate && slug) {
|
|
50
|
+
if (!isPrivate && slug && uploadsCoverage) {
|
|
49
51
|
badges.push(`[](https://codecov.io/gh/${slug})`);
|
|
50
52
|
}
|
|
51
53
|
badges.push('[](https://opensource.org/licenses/MIT)');
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import selfPackageJson from '../../../package.json' with { type: 'json' };
|
|
4
|
+
import { CI_WORKFLOW_NAME } from '../../base/ci.js';
|
|
5
|
+
import { coverageUploadWorkflow } from '../../base/checks.js';
|
|
6
|
+
import { jsBadgeAudience } from '../../languages/js/checks.js';
|
|
4
7
|
import { copyPreset, PRESETS } from '../utils/copy-preset.js';
|
|
5
8
|
import { buildBadgeRow, parseRepository } from './badges.js';
|
|
6
9
|
import { DOCS_SITE_BUILDS, mergeAllowBuilds } from './pnpm-workspace.js';
|
|
@@ -342,15 +345,18 @@ on:
|
|
|
342
345
|
paths:
|
|
343
346
|
- 'apps/docs/**'
|
|
344
347
|
- '.github/workflows/docs.yml'
|
|
345
|
-
# The changelog page is built from GitHub Releases
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
# The changelog page is built from GitHub Releases. A release created with
|
|
349
|
+
# GITHUB_TOKEN never fires \`release: published\`, so rebuild once CI (which
|
|
350
|
+
# runs semantic-release) succeeds on main instead — no PAT needed (#691).
|
|
351
|
+
workflow_run:
|
|
352
|
+
workflows: ['${CI_WORKFLOW_NAME}']
|
|
353
|
+
types: [completed]
|
|
354
|
+
branches: [main]
|
|
350
355
|
workflow_dispatch:
|
|
351
356
|
|
|
352
357
|
jobs:
|
|
353
358
|
docs:
|
|
359
|
+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
|
354
360
|
permissions:
|
|
355
361
|
contents: read
|
|
356
362
|
pages: write
|
|
@@ -385,11 +391,15 @@ export async function generateDocsSite(pkg, targetDir, options = {}) {
|
|
|
385
391
|
// The docs homepage carries the same badge set as the README (#169), derived
|
|
386
392
|
// from package.json + repo; visibility-aware (private repos drop npm/coverage).
|
|
387
393
|
// Plain row (no upsert delimiters) to stay MDX-safe in the generated intro.
|
|
394
|
+
// Bundlephobia only for a published library, Codecov only when CI uploads
|
|
395
|
+
// coverage — the same rules doctor's badge/coverage checks use (#675).
|
|
388
396
|
const badges = buildBadgeRow({
|
|
389
397
|
name: pkg?.name,
|
|
390
398
|
owner: meta.owner ?? undefined,
|
|
391
399
|
repo: meta.repo ?? undefined,
|
|
392
400
|
isPrivate: pkg?.private === true,
|
|
401
|
+
bundled: jsBadgeAudience(pkg) === 'public',
|
|
402
|
+
uploadsCoverage: (await coverageUploadWorkflow(targetDir)) !== null,
|
|
393
403
|
});
|
|
394
404
|
// Opt-in TypeDoc API section (#229): only wire it when enabled AND the
|
|
395
405
|
// package actually exposes source modules to document.
|
package/dist/languages/js/ci.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conventional-commit types that cut a release, mirroring the `releaseRules` in
|
|
3
|
+
* tooling/semantic-release (a test keeps the two in step). `feat!:` and `fix!:`
|
|
4
|
+
* start with these too, so breaking changes are covered.
|
|
5
|
+
*/
|
|
6
|
+
export const RELEASE_TYPES = ['feat', 'fix', 'perf', 'refactor', 'revert', 'update'];
|
|
7
|
+
// Match only the START of the squash subject: the message carries the whole PR
|
|
8
|
+
// body, so searching it matched PRs that merely mentioned `BREAKING CHANGE`.
|
|
9
|
+
const RELEASE_IF = `github.ref == 'refs/heads/main' && (github.event_name == 'workflow_dispatch' || github.event_name == 'push' && (${RELEASE_TYPES.map((t) => `startsWith(github.event.head_commit.message, '${t}')`).join(' || ')}))`;
|
|
1
10
|
/** Coverage is uploaded when Vitest is the test runner (it emits an lcov report). */
|
|
2
11
|
export function usesCoverage(config) {
|
|
3
12
|
return config.testing.framework === 'vitest';
|
|
@@ -175,7 +184,7 @@ ${attw}${publint}
|
|
|
175
184
|
id: 'release',
|
|
176
185
|
// Gate the publish on everything that ran before it.
|
|
177
186
|
needs: jobs.map((job) => job.id),
|
|
178
|
-
if:
|
|
187
|
+
if: RELEASE_IF,
|
|
179
188
|
extra: ` permissions:
|
|
180
189
|
contents: write
|
|
181
190
|
issues: write
|
|
@@ -212,13 +221,26 @@ ${attw}${publint}
|
|
|
212
221
|
git config --global user.name "github-actions[bot]"
|
|
213
222
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
214
223
|
|
|
224
|
+
# OIDC trusted publishing needs npm >= 11.5.1; Node 22 bundles npm 10, whose
|
|
225
|
+
# \`npm publish\` fails with ENEEDAUTH. Pinned to a range, not \`latest\`, so a
|
|
226
|
+
# future npm major can't change publishing unannounced.
|
|
227
|
+
- name: 📦 Upgrade npm for OIDC trusted publishing
|
|
228
|
+
run: npm install -g npm@^11.5.1
|
|
229
|
+
|
|
215
230
|
- name: 🚀 Run semantic-release
|
|
216
231
|
# Publishes to npm via OIDC trusted publishing — no NPM_TOKEN. Requires
|
|
217
232
|
# the \`id-token: write\` permission above + a trusted publisher configured
|
|
218
233
|
# for the package on npmjs.com (Settings → Trusted Publisher).
|
|
219
234
|
env:
|
|
220
235
|
GITHUB_TOKEN: \${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
221
|
-
run:
|
|
236
|
+
run: |
|
|
237
|
+
set -o pipefail
|
|
238
|
+
npx semantic-release 2>&1 | tee release.log
|
|
239
|
+
# semantic-release exits 0 when main moved on since this run started,
|
|
240
|
+
# publishing nothing (#690). Say so instead of going quietly green.
|
|
241
|
+
if grep -q "is behind the remote one" release.log; then
|
|
242
|
+
echo "::warning::main moved on; nothing was published. Re-run via workflow_dispatch."
|
|
243
|
+
fi`,
|
|
222
244
|
});
|
|
223
245
|
}
|
|
224
246
|
return jobs;
|
|
@@ -10,6 +10,7 @@ import { GH_WORKFLOWS, generateGhWorkflow } from '../../cli/generators/github-wo
|
|
|
10
10
|
import { BIOME_CONFIG, generateBiomeConfig, generateESLintConfig, generatePrettierConfig, } from '../../cli/generators/linting.js';
|
|
11
11
|
import { alignNodeVersion, ensureEnginesNode, ensurePackageManager, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, generateVscodeExtensions, } from '../../cli/generators/misc.js';
|
|
12
12
|
import { scriptsOf } from './ci.js';
|
|
13
|
+
import { NPM_OIDC_CHECK, NPM_TRUST_FIXER } from './npm-trust.js';
|
|
13
14
|
import { usesPnpm } from './checks.js';
|
|
14
15
|
import { SIZE_LIMIT_SCRIPTS, SIZE_LIMIT_VERSION, composeVerifyScriptFromPkg, ensureScripts, } from '../../cli/generators/package-json.js';
|
|
15
16
|
/** Kept in step with the biome branch of getScripts() in package-json.ts. */
|
|
@@ -438,7 +439,7 @@ export const FIXERS = [
|
|
|
438
439
|
{
|
|
439
440
|
target: 'github-actions',
|
|
440
441
|
description: 'Scaffold .github/workflows/ci.yml (+ codecov.yml when tests run)',
|
|
441
|
-
appliesTo: ['GitHub Actions', 'Coverage upload',
|
|
442
|
+
appliesTo: ['GitHub Actions', 'Coverage upload', NPM_OIDC_CHECK],
|
|
442
443
|
outputs: [CI_WORKFLOW, 'codecov.yml', 'package.json (packageManager field)'],
|
|
443
444
|
canFixDrift: true,
|
|
444
445
|
async run({ targetDir, pkg, result }) {
|
|
@@ -462,6 +463,7 @@ export const FIXERS = [
|
|
|
462
463
|
return { filesWritten };
|
|
463
464
|
},
|
|
464
465
|
},
|
|
466
|
+
NPM_TRUST_FIXER,
|
|
465
467
|
{
|
|
466
468
|
target: 'gitlab-ci',
|
|
467
469
|
description: 'Scaffold .gitlab-ci.yml (lint/typecheck/test/build mirrored from GitHub Actions)',
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm OIDC trusted publishing (#687): what the publishing job needs, whether the
|
|
3
|
+
* registry has a trusted publisher wired to it, and the fixer that registers one.
|
|
4
|
+
*
|
|
5
|
+
* Everything that talks to npm goes through `NpmExec`, the same resolve-never-
|
|
6
|
+
* reject seam as `GhExec`, so tests never touch the registry and a network or
|
|
7
|
+
* auth failure degrades to "couldn't check" rather than a failed doctor run.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
import { FixerAbort } from '../../base/fixers.js';
|
|
15
|
+
import { jobEnvironment, workflowJobs } from '../../base/github-settings.js';
|
|
16
|
+
import { parseRepository } from '../../cli/generators/badges.js';
|
|
17
|
+
const NPM_TIMEOUT_MS = 15_000;
|
|
18
|
+
/** Real `npm` runner — never rejects. Args are derived, never free text, and execFile runs no shell. */
|
|
19
|
+
export const realNpmExec = (args) => new Promise((resolve) => {
|
|
20
|
+
execFile('npm', args, { timeout: NPM_TIMEOUT_MS }, (err, stdout, stderr) => {
|
|
21
|
+
const code = err ? (typeof err.code === 'number' ? err.code : null) : 0;
|
|
22
|
+
resolve({ ok: !err, stdout: String(stdout), stderr: String(stderr), code });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
export const NPM_OIDC_CHECK = 'npm OIDC publish';
|
|
26
|
+
export const NPM_TRUST_CHECK = 'npm trusted publisher';
|
|
27
|
+
export const NPM_TRUST_FIX = 'npm-trusted-publisher';
|
|
28
|
+
/** OIDC publish needs npm ≥ 11.5.1 — Node 22 bundles npm 10. */
|
|
29
|
+
export const NPM_OIDC_MIN = '11.5.1';
|
|
30
|
+
/** `npm trust` landed in npm 11.15.0. */
|
|
31
|
+
export const NPM_TRUST_MIN = '11.15.0';
|
|
32
|
+
export const NPM_UPGRADE_CMD = `npm install -g npm@^${NPM_OIDC_MIN}`;
|
|
33
|
+
const NPM_UPGRADE = /\bnpm\s+(?:install|i)\s+(?:-g|--global)\s+npm@/;
|
|
34
|
+
const PUBLISHES = /semantic-release|\bnpm\s+publish\b/;
|
|
35
|
+
/** a ≥ b for dotted numeric versions (prerelease tags ignored). */
|
|
36
|
+
export function versionAtLeast(a, b) {
|
|
37
|
+
const pa = a.trim().split(/[.-]/).map(Number);
|
|
38
|
+
const pb = b.split('.').map(Number);
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
const x = pa[i] ?? 0;
|
|
41
|
+
const y = pb[i] ?? 0;
|
|
42
|
+
if (Number.isNaN(x))
|
|
43
|
+
return false;
|
|
44
|
+
if (x !== y)
|
|
45
|
+
return x > y;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const uncommented = (s) => s
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter((l) => !l.trimStart().startsWith('#'))
|
|
52
|
+
.join('\n');
|
|
53
|
+
/** The first workflow job that publishes to npm, or null. Throws when the directory can't be read. */
|
|
54
|
+
export async function findNpmPublishJob(dir) {
|
|
55
|
+
const workflowsDir = path.join(dir, '.github', 'workflows');
|
|
56
|
+
if (!(await fs.pathExists(workflowsDir)))
|
|
57
|
+
return null;
|
|
58
|
+
for (const file of (await fs.readdir(workflowsDir)).sort()) {
|
|
59
|
+
if (!/\.ya?ml$/.test(file))
|
|
60
|
+
continue;
|
|
61
|
+
const content = await fs.readFile(path.join(workflowsDir, file), 'utf-8');
|
|
62
|
+
if (!PUBLISHES.test(uncommented(content)))
|
|
63
|
+
continue;
|
|
64
|
+
const jobs = [...workflowJobs(content).values()].map(uncommented);
|
|
65
|
+
const body = jobs.find((b) => PUBLISHES.test(b)) ?? uncommented(content);
|
|
66
|
+
return { file, body, content, environment: jobEnvironment(body) };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* What the publishing job itself needs for OIDC: no NPM_TOKEN, `id-token: write`,
|
|
72
|
+
* and an npm new enough to use the token.
|
|
73
|
+
*/
|
|
74
|
+
export function checkPublishJob(job) {
|
|
75
|
+
const check = NPM_OIDC_CHECK;
|
|
76
|
+
const f = job.file;
|
|
77
|
+
if (/secrets\.NPM_TOKEN/.test(job.content)) {
|
|
78
|
+
return {
|
|
79
|
+
check,
|
|
80
|
+
status: 'drift',
|
|
81
|
+
detail: `${f} authenticates npm publish with NPM_TOKEN`,
|
|
82
|
+
hint: 'Migrate to OIDC trusted publishing: add a Trusted Publisher for each published package on npmjs.com (Settings → Trusted Publisher), then run `fix github-actions` to drop NPM_TOKEN (the release job keeps `id-token: write`). npm is deprecating 2FA-bypass tokens.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// The grant may sit on the job or at workflow level (everything before `jobs:`).
|
|
86
|
+
const workflowLevel = uncommented(job.content.split(/^jobs:/m)[0] ?? '');
|
|
87
|
+
if (!/id-token:\s*write/.test(job.body) && !/id-token:\s*write/.test(workflowLevel)) {
|
|
88
|
+
return {
|
|
89
|
+
check,
|
|
90
|
+
status: 'drift',
|
|
91
|
+
detail: `${f}: the publishing job lacks \`id-token: write\` — npm can't mint an OIDC token`,
|
|
92
|
+
hint: 'Add `permissions: { id-token: write }` to the publishing job (or run `fix github-actions`)',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (!NPM_UPGRADE.test(job.body)) {
|
|
96
|
+
return {
|
|
97
|
+
check,
|
|
98
|
+
status: 'drift',
|
|
99
|
+
detail: `${f}: the publishing job runs the npm bundled with Node — OIDC publish needs npm ≥ ${NPM_OIDC_MIN} and fails with ENEEDAUTH below it`,
|
|
100
|
+
hint: `add \`${NPM_UPGRADE_CMD}\` before semantic-release`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { check, status: 'ok', detail: `${f} publishes via OIDC (no NPM_TOKEN)` };
|
|
104
|
+
}
|
|
105
|
+
export function npmPublishGuide(opts) {
|
|
106
|
+
const env = opts.environment ? ` --env ${opts.environment}` : '';
|
|
107
|
+
return {
|
|
108
|
+
package: opts.name,
|
|
109
|
+
where: 'npmjs.com → package → Settings → Trusted Publisher → GitHub Actions',
|
|
110
|
+
organizationOrUser: opts.owner,
|
|
111
|
+
repository: opts.repo,
|
|
112
|
+
workflowFilename: opts.file,
|
|
113
|
+
environment: opts.environment,
|
|
114
|
+
command: `npm trust github ${opts.name} --file ${opts.file} --repo ${opts.owner}/${opts.repo}${env} --allow-publish`,
|
|
115
|
+
bootstrap: [
|
|
116
|
+
"Publish one version BELOW semantic-release's next one by hand: `npm publish --access public --provenance=false --otp=<code>`",
|
|
117
|
+
`Register the trusted publisher: \`npx @rtorcato/repo-tooling fix ${NPM_TRUST_FIX}\` (or on npmjs.com)`,
|
|
118
|
+
'Let CI take over — every later release publishes via OIDC',
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** The guide as text lines, for Next Steps and doctor hints. Never mentions an NPM_TOKEN secret. */
|
|
123
|
+
export function formatNpmPublishGuide(g, unpublished) {
|
|
124
|
+
const lines = [
|
|
125
|
+
`On ${g.where}:`,
|
|
126
|
+
` Organization or user: ${g.organizationOrUser}`,
|
|
127
|
+
` Repository: ${g.repository}`,
|
|
128
|
+
` Workflow filename: ${g.workflowFilename}`,
|
|
129
|
+
];
|
|
130
|
+
if (g.environment)
|
|
131
|
+
lines.push(` Environment: ${g.environment}`);
|
|
132
|
+
lines.push(`Or from a logged-in npm ≥ ${NPM_TRUST_MIN}: ${g.command}`);
|
|
133
|
+
if (unpublished) {
|
|
134
|
+
lines.push(`${g.package} isn't on npm yet — npm only accepts a trusted publisher for an existing package:`);
|
|
135
|
+
for (const [i, s] of g.bootstrap.entries())
|
|
136
|
+
lines.push(` ${i + 1}. ${s}`);
|
|
137
|
+
}
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
/** A trusted-publisher entry's string leaves, lowercased — npm's JSON shape is matched loosely. */
|
|
141
|
+
function stringsOf(v, out = []) {
|
|
142
|
+
if (typeof v === 'string')
|
|
143
|
+
out.push(v.toLowerCase());
|
|
144
|
+
else if (Array.isArray(v))
|
|
145
|
+
for (const x of v)
|
|
146
|
+
stringsOf(x, out);
|
|
147
|
+
else if (v && typeof v === 'object')
|
|
148
|
+
for (const x of Object.values(v))
|
|
149
|
+
stringsOf(x, out);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** `npm trust list --json` → its entries, whether npm wraps them or not. */
|
|
153
|
+
function trustEntries(json) {
|
|
154
|
+
if (Array.isArray(json))
|
|
155
|
+
return json;
|
|
156
|
+
if (json && typeof json === 'object') {
|
|
157
|
+
const arr = Object.values(json).find(Array.isArray);
|
|
158
|
+
if (arr)
|
|
159
|
+
return arr;
|
|
160
|
+
return Object.keys(json).length > 0 ? [json] : [];
|
|
161
|
+
}
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Compare `npm trust list` entries against the publishing job. Returns null on a
|
|
166
|
+
* match, else why nothing matches.
|
|
167
|
+
* ponytail: matches on string leaves rather than a fixed schema — npm's JSON
|
|
168
|
+
* shape isn't pinned in its docs; tighten once it is.
|
|
169
|
+
*/
|
|
170
|
+
export function trustMismatch(entries, want) {
|
|
171
|
+
if (entries.length === 0)
|
|
172
|
+
return 'no trusted publisher is registered';
|
|
173
|
+
const nwo = want.nwo.toLowerCase();
|
|
174
|
+
const file = want.file.toLowerCase();
|
|
175
|
+
const env = want.environment?.toLowerCase() ?? null;
|
|
176
|
+
const leaves = entries.map((e) => stringsOf(e));
|
|
177
|
+
const forRepo = leaves.filter((s) => s.some((x) => x === nwo || x.endsWith(`/${nwo}`)));
|
|
178
|
+
if (forRepo.length === 0)
|
|
179
|
+
return `no trusted publisher for ${want.nwo}`;
|
|
180
|
+
const forFile = forRepo.filter((s) => s.some((x) => x === file || x.endsWith(`/${file}`) || x.includes(`/${file}@`)));
|
|
181
|
+
if (forFile.length === 0)
|
|
182
|
+
return `the trusted publisher for ${want.nwo} names a different workflow (want ${want.file})`;
|
|
183
|
+
if (env && !forFile.some((s) => s.includes(env))) {
|
|
184
|
+
return `the trusted publisher for ${want.nwo} has no \`${want.environment}\` environment, but the job declares one`;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const NPM_NAME = /^(@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
|
|
189
|
+
const ENV_NAME = /^[\w.-]+$/;
|
|
190
|
+
/** What the check and the fixer both need, or why it can't be had. Network only past the offline gates. */
|
|
191
|
+
async function trustContext(dir, pkg, npm, opts) {
|
|
192
|
+
if (!pkg || pkg.private === true)
|
|
193
|
+
return { skip: 'private package — no npm publish' };
|
|
194
|
+
const name = typeof pkg.name === 'string' ? pkg.name : null;
|
|
195
|
+
if (!name)
|
|
196
|
+
return { skip: 'package.json has no name' };
|
|
197
|
+
// Untrusted (audited repo's package.json): reject anything npm could parse as a flag.
|
|
198
|
+
if (!NPM_NAME.test(name))
|
|
199
|
+
return { skip: 'package.json `name` is not a valid npm package name' };
|
|
200
|
+
let job;
|
|
201
|
+
try {
|
|
202
|
+
job = await findNpmPublishJob(dir);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return { skip: 'unable to read .github/workflows/' };
|
|
206
|
+
}
|
|
207
|
+
if (!job)
|
|
208
|
+
return { skip: 'no workflow publishes to npm' };
|
|
209
|
+
if (job.environment && !ENV_NAME.test(job.environment)) {
|
|
210
|
+
return { skip: 'the publish job declares an environment with unsupported characters' };
|
|
211
|
+
}
|
|
212
|
+
const repo = parseRepository(pkg.repository);
|
|
213
|
+
if (!repo)
|
|
214
|
+
return { skip: 'package.json `repository` names no GitHub repo' };
|
|
215
|
+
const nwo = `${repo.owner}/${repo.repo}`;
|
|
216
|
+
const guide = npmPublishGuide({ name, ...repo, file: job.file, environment: job.environment });
|
|
217
|
+
const view = await npm(['view', '--', name, 'version']);
|
|
218
|
+
if (!view.ok) {
|
|
219
|
+
if (/E404|404 Not Found/i.test(view.stderr)) {
|
|
220
|
+
return { skip: `${name} is not on npm yet`, unpublished: true, guide };
|
|
221
|
+
}
|
|
222
|
+
return { skip: 'could not reach the npm registry', guide };
|
|
223
|
+
}
|
|
224
|
+
if (opts.needLogin && process.env.CI)
|
|
225
|
+
return { skip: 'running in CI — no npm login', guide };
|
|
226
|
+
const ver = await npm(['--version']);
|
|
227
|
+
if (!ver.ok || !versionAtLeast(ver.stdout, NPM_TRUST_MIN)) {
|
|
228
|
+
return {
|
|
229
|
+
skip: `local npm ${ver.stdout.trim() || '?'} is older than ${NPM_TRUST_MIN} (\`npm install -g npm@^${NPM_TRUST_MIN}\`)`,
|
|
230
|
+
guide,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const who = await npm(['whoami']);
|
|
234
|
+
if (!who.ok)
|
|
235
|
+
return { skip: 'not logged in to npm (`npm login`)', guide };
|
|
236
|
+
return { guide, nwo, job };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Is a trusted publisher registered on npm, matching the publishing job? Every
|
|
240
|
+
* "couldn't check" is `optional-missing` with the manual steps — never a failure.
|
|
241
|
+
*/
|
|
242
|
+
export async function checkNpmTrustedPublisher(dir, pkg, npm = realNpmExec) {
|
|
243
|
+
const check = NPM_TRUST_CHECK;
|
|
244
|
+
const ctx = await trustContext(dir, pkg, npm, { needLogin: true });
|
|
245
|
+
if ('skip' in ctx) {
|
|
246
|
+
return {
|
|
247
|
+
check,
|
|
248
|
+
status: 'optional-missing',
|
|
249
|
+
detail: ctx.skip,
|
|
250
|
+
...(ctx.guide && {
|
|
251
|
+
hint: formatNpmPublishGuide(ctx.guide, ctx.unpublished === true).join('\n'),
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const list = await npm(['trust', 'list', '--json', '--', ctx.guide.package]);
|
|
256
|
+
let entries = null;
|
|
257
|
+
if (list.ok) {
|
|
258
|
+
try {
|
|
259
|
+
entries = trustEntries(JSON.parse(list.stdout || '[]'));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
entries = null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!entries) {
|
|
266
|
+
return {
|
|
267
|
+
check,
|
|
268
|
+
status: 'optional-missing',
|
|
269
|
+
detail: 'could not read `npm trust list`',
|
|
270
|
+
hint: formatNpmPublishGuide(ctx.guide, false).join('\n'),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const why = trustMismatch(entries, {
|
|
274
|
+
nwo: ctx.nwo,
|
|
275
|
+
file: ctx.job.file,
|
|
276
|
+
environment: ctx.job.environment,
|
|
277
|
+
});
|
|
278
|
+
if (why) {
|
|
279
|
+
return {
|
|
280
|
+
check,
|
|
281
|
+
status: 'drift',
|
|
282
|
+
detail: `${ctx.guide.package}: ${why}`,
|
|
283
|
+
hint: `Run \`npx @rtorcato/repo-tooling fix ${NPM_TRUST_FIX}\`, or: ${ctx.guide.command}`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
check,
|
|
288
|
+
status: 'ok',
|
|
289
|
+
detail: `${ctx.guide.package} trusts ${ctx.nwo} ${ctx.job.file}${ctx.job.environment ? ` (${ctx.job.environment})` : ''}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Register the trusted publisher with `npm trust github`, flags derived from the
|
|
294
|
+
* publishing job. Dry-runs first and asks before the real call.
|
|
295
|
+
*/
|
|
296
|
+
export async function applyNpmTrustedPublisher(dir, pkg, assumeYes, npm = realNpmExec) {
|
|
297
|
+
const ctx = await trustContext(dir, pkg, npm, { needLogin: true });
|
|
298
|
+
if ('skip' in ctx) {
|
|
299
|
+
const hint = ctx.unpublished && ctx.guide ? ctx.guide.bootstrap.join(' → ') : undefined;
|
|
300
|
+
throw new FixerAbort('npm-trust-unavailable', ctx.skip, hint);
|
|
301
|
+
}
|
|
302
|
+
const args = [
|
|
303
|
+
'trust',
|
|
304
|
+
'github',
|
|
305
|
+
'--file',
|
|
306
|
+
ctx.job.file,
|
|
307
|
+
'--repo',
|
|
308
|
+
ctx.nwo,
|
|
309
|
+
...(ctx.job.environment ? ['--env', ctx.job.environment] : []),
|
|
310
|
+
'--allow-publish',
|
|
311
|
+
];
|
|
312
|
+
// `--` last so the name can never be read as a flag.
|
|
313
|
+
const tail = ['--', ctx.guide.package];
|
|
314
|
+
const dry = await npm([...args, '--dry-run', ...tail]);
|
|
315
|
+
if (!dry.ok) {
|
|
316
|
+
throw new FixerAbort('npm-trust-failed', `npm trust --dry-run failed: ${dry.stderr.trim()}`);
|
|
317
|
+
}
|
|
318
|
+
console.error(chalk.gray(dry.stdout.trim()));
|
|
319
|
+
if (!assumeYes) {
|
|
320
|
+
if (!process.stdin.isTTY)
|
|
321
|
+
return [];
|
|
322
|
+
const { confirm } = await inquirer.prompt([
|
|
323
|
+
{
|
|
324
|
+
type: 'confirm',
|
|
325
|
+
name: 'confirm',
|
|
326
|
+
message: 'Register this trusted publisher on npm?',
|
|
327
|
+
default: false,
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
if (confirm !== true)
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const real = await npm([...args, '--yes', ...tail]);
|
|
334
|
+
if (!real.ok)
|
|
335
|
+
throw new FixerAbort('npm-trust-failed', `npm trust failed: ${real.stderr.trim()}`);
|
|
336
|
+
return [`npm trusted publisher for ${ctx.guide.package} (remote, via npm trust)`];
|
|
337
|
+
}
|
|
338
|
+
export const NPM_TRUST_FIXER = {
|
|
339
|
+
target: NPM_TRUST_FIX,
|
|
340
|
+
description: 'Register the npm trusted publisher (OIDC) for this package with `npm trust github`, derived from the publishing job (#687)',
|
|
341
|
+
appliesTo: [NPM_TRUST_CHECK],
|
|
342
|
+
outputs: ['npm trusted publisher (remote, via npm trust)'],
|
|
343
|
+
// safe-add keeps `--diff` from shadow-running it (that would call npm for real);
|
|
344
|
+
// explicitOnly because it changes registry state, like release-environment.
|
|
345
|
+
riskLevel: 'safe-add',
|
|
346
|
+
explicitOnly: true,
|
|
347
|
+
canFixDrift: true,
|
|
348
|
+
async run({ targetDir, pkg, assumeYes }) {
|
|
349
|
+
return { filesWritten: await applyNpmTrustedPublisher(targetDir, pkg, assumeYes) };
|
|
350
|
+
},
|
|
351
|
+
};
|
package/package.json
CHANGED