@rtorcato/repo-tooling 4.5.0 → 4.7.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/dist/base/checks.js +24 -1
- package/dist/base/ci.js +6 -2
- package/dist/base/fixers.js +2 -1
- package/dist/cli/commands/doctor.js +2 -2
- package/dist/cli/generators/brand.js +23 -0
- package/dist/cli/generators/docs-site.js +15 -5
- package/dist/cli/generators/github-actions.js +2 -2
- package/dist/languages/js/ci.js +38 -3
- package/dist/languages/js/fixers.js +2 -1
- package/package.json +2 -2
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
|
}
|
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
|
`;
|
package/dist/base/fixers.js
CHANGED
|
@@ -15,7 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
17
|
import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
|
|
18
|
-
import { addReadmeBanner, generateBrand, renderBrand, resolveBrandMeta, } from '../cli/generators/brand.js';
|
|
18
|
+
import { addReadmeBanner, generateBrand, renderBrand, resolveBrandMeta, syncBrandToDocs, } from '../cli/generators/brand.js';
|
|
19
19
|
import { generateCommunityHealth } from '../cli/generators/community-health.js';
|
|
20
20
|
import { generateCommitlintConfig } from '../cli/generators/git.js';
|
|
21
21
|
import { generateCodeowners, generateEditorConfig } from '../cli/generators/misc.js';
|
|
@@ -274,6 +274,7 @@ export const BASE_FIXERS = [
|
|
|
274
274
|
const tagline = lock?.rules?.brand?.tagline;
|
|
275
275
|
const filesWritten = await generateBrand(pkg, targetDir, tagline);
|
|
276
276
|
filesWritten.push(...((await renderBrand(targetDir)) ?? []));
|
|
277
|
+
filesWritten.push(...(await syncBrandToDocs(targetDir)));
|
|
277
278
|
const { name } = await resolveBrandMeta(pkg, targetDir, tagline);
|
|
278
279
|
const readme = await addReadmeBanner(targetDir, name);
|
|
279
280
|
if (readme && !filesWritten.includes(readme))
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { renderGitHubWorkflow } from '../../base/ci.js';
|
|
5
|
-
import { githubJobs, scriptsOf } from '../../languages/js/ci.js';
|
|
5
|
+
import { githubJobs, hasBin, scriptsOf } from '../../languages/js/ci.js';
|
|
6
6
|
import { inferProjectConfig } from '../../languages/js/fixers.js';
|
|
7
7
|
import { checkNpmTrustedPublisher, checkPublishJob, findNpmPublishJob, NPM_OIDC_CHECK, } from '../../languages/js/npm-trust.js';
|
|
8
8
|
import { PERL_GIT_HOOKS, runPerlChecks } from '../../languages/perl/checks.js';
|
|
@@ -391,7 +391,7 @@ export async function runDoctor(dir) {
|
|
|
391
391
|
},
|
|
392
392
|
// Same `scripts` gating the fixer applies, or the workflow doctor
|
|
393
393
|
// compares against would reference steps the fixer never writes (#364).
|
|
394
|
-
presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
|
|
394
|
+
presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg), bin: hasBin(pkg) })),
|
|
395
395
|
language,
|
|
396
396
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
397
397
|
})));
|
|
@@ -402,6 +402,29 @@ export async function renderBrand(targetDir) {
|
|
|
402
402
|
}
|
|
403
403
|
return written;
|
|
404
404
|
}
|
|
405
|
+
/** brand/ file → docs-site static/img file. The ico and card PNG exist only once rendered. */
|
|
406
|
+
const DOCS_ASSETS = ['favicon.svg', 'favicon.ico', 'social-card.png'];
|
|
407
|
+
/**
|
|
408
|
+
* Copy the brand favicon and social card into the docs site's `static/img`
|
|
409
|
+
* (#680). Copy-if-missing, and a no-op without `apps/docs`, so `fix brand` and
|
|
410
|
+
* `fix docs-site` reach the same tree in either order — each calls it.
|
|
411
|
+
*/
|
|
412
|
+
export async function syncBrandToDocs(targetDir) {
|
|
413
|
+
if (!(await fs.pathExists(path.join(targetDir, 'apps', 'docs'))))
|
|
414
|
+
return [];
|
|
415
|
+
const img = path.join('apps', 'docs', 'static', 'img');
|
|
416
|
+
const written = [];
|
|
417
|
+
for (const name of DOCS_ASSETS) {
|
|
418
|
+
const src = path.join(targetDir, 'brand', name);
|
|
419
|
+
const dest = path.join(targetDir, img, name);
|
|
420
|
+
if (!(await fs.pathExists(src)) || (await fs.pathExists(dest)))
|
|
421
|
+
continue;
|
|
422
|
+
await fs.ensureDir(path.dirname(dest));
|
|
423
|
+
await fs.copyFile(src, dest);
|
|
424
|
+
written.push(path.join(img, name));
|
|
425
|
+
}
|
|
426
|
+
return written;
|
|
427
|
+
}
|
|
405
428
|
export const BANNER_START = '<!-- js-tooling:banner:start -->';
|
|
406
429
|
export const BANNER_END = '<!-- js-tooling:banner:end -->';
|
|
407
430
|
/** The README `<picture>` banner, mobile variant under 640px, as a delimited block. */
|
|
@@ -1,10 +1,12 @@
|
|
|
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';
|
|
4
5
|
import { coverageUploadWorkflow } from '../../base/checks.js';
|
|
5
6
|
import { jsBadgeAudience } from '../../languages/js/checks.js';
|
|
6
7
|
import { copyPreset, PRESETS } from '../utils/copy-preset.js';
|
|
7
8
|
import { buildBadgeRow, parseRepository } from './badges.js';
|
|
9
|
+
import { syncBrandToDocs } from './brand.js';
|
|
8
10
|
import { DOCS_SITE_BUILDS, mergeAllowBuilds } from './pnpm-workspace.js';
|
|
9
11
|
import { inferSubpathsFromExports } from './treeshake.js';
|
|
10
12
|
/**
|
|
@@ -175,6 +177,7 @@ ${typedocPlugins}\t\t[
|
|
|
175
177
|
\t],
|
|
176
178
|
|
|
177
179
|
\tthemeConfig: {
|
|
180
|
+
\t\timage: 'img/social-card.png',
|
|
178
181
|
\t\tcolorMode: {
|
|
179
182
|
\t\t\tdefaultMode: 'dark',
|
|
180
183
|
\t\t\trespectPrefersColorScheme: true,
|
|
@@ -344,15 +347,18 @@ on:
|
|
|
344
347
|
paths:
|
|
345
348
|
- 'apps/docs/**'
|
|
346
349
|
- '.github/workflows/docs.yml'
|
|
347
|
-
# The changelog page is built from GitHub Releases
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
# The changelog page is built from GitHub Releases. A release created with
|
|
351
|
+
# GITHUB_TOKEN never fires \`release: published\`, so rebuild once CI (which
|
|
352
|
+
# runs semantic-release) succeeds on main instead — no PAT needed (#691).
|
|
353
|
+
workflow_run:
|
|
354
|
+
workflows: ['${CI_WORKFLOW_NAME}']
|
|
355
|
+
types: [completed]
|
|
356
|
+
branches: [main]
|
|
352
357
|
workflow_dispatch:
|
|
353
358
|
|
|
354
359
|
jobs:
|
|
355
360
|
docs:
|
|
361
|
+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
|
356
362
|
permissions:
|
|
357
363
|
contents: read
|
|
358
364
|
pages: write
|
|
@@ -423,6 +429,10 @@ export async function generateDocsSite(pkg, targetDir, options = {}) {
|
|
|
423
429
|
if (w)
|
|
424
430
|
written.push(w);
|
|
425
431
|
}
|
|
432
|
+
// static/img always exists (the config points at img/favicon.ico); the brand
|
|
433
|
+
// assets go in when brand/ has them (#680).
|
|
434
|
+
await fs.ensureDir(path.join(targetDir, DOCS_APP, 'static', 'img'));
|
|
435
|
+
written.push(...(await syncBrandToDocs(targetDir)));
|
|
426
436
|
const ws = await ensureWorkspace(targetDir);
|
|
427
437
|
if (ws)
|
|
428
438
|
written.push(ws);
|
|
@@ -31,7 +31,7 @@ export const CI_WORKFLOW = '.github/workflows/ci.yml';
|
|
|
31
31
|
* scripts itself as part of the same scaffold.
|
|
32
32
|
* @returns the files actually written, relative to targetDir.
|
|
33
33
|
*/
|
|
34
|
-
export async function generateGitHubActions(config, targetDir, { overwrite = false, scripts } = {}) {
|
|
34
|
+
export async function generateGitHubActions(config, targetDir, { overwrite = false, scripts, bin, } = {}) {
|
|
35
35
|
const workflowsDir = path.join(targetDir, '.github', 'workflows');
|
|
36
36
|
await fs.ensureDir(workflowsDir);
|
|
37
37
|
// This is the JS path specifically. Swift (#287) renders its own workflow
|
|
@@ -40,7 +40,7 @@ export async function generateGitHubActions(config, targetDir, { overwrite = fal
|
|
|
40
40
|
// shared entry point would mean inventing a fake config to pass in. Both
|
|
41
41
|
// paths meet at renderGitHubWorkflow() in src/base/ci.ts, which is the seam
|
|
42
42
|
// that actually matters.
|
|
43
|
-
const workflow = renderGitHubWorkflow(githubJobs(config, { scripts }));
|
|
43
|
+
const workflow = renderGitHubWorkflow(githubJobs(config, { scripts, bin }));
|
|
44
44
|
const ciPath = path.join(workflowsDir, 'ci.yml');
|
|
45
45
|
const existing = (await fs.pathExists(ciPath)) ? await fs.readFile(ciPath, 'utf-8') : null;
|
|
46
46
|
const filesWritten = [];
|
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';
|
|
@@ -64,6 +73,10 @@ const DEPENDENCIES_JOB = {
|
|
|
64
73
|
- name: 📦 Install dependencies
|
|
65
74
|
run: pnpm install --frozen-lockfile`,
|
|
66
75
|
};
|
|
76
|
+
/** `JobOptions.bin` for a real repo. */
|
|
77
|
+
export function hasBin(pkg) {
|
|
78
|
+
return Boolean(pkg?.bin);
|
|
79
|
+
}
|
|
67
80
|
/**
|
|
68
81
|
* `JobOptions.scripts` for a real repo. A package.json with no `scripts` block
|
|
69
82
|
* yields `{}` — "this repo has no scripts", which gates every step off —
|
|
@@ -151,6 +164,21 @@ export function githubJobs(config, opts = {}) {
|
|
|
151
164
|
const publint = config.publint
|
|
152
165
|
? '\n - name: 🔍 Validate package with publint\n run: pnpm exec publint --strict\n'
|
|
153
166
|
: '';
|
|
167
|
+
// npm's own `bin[…] invalid and removed` warning and a bin pnpm never
|
|
168
|
+
// links both hide until the tarball is installed somewhere else (#693).
|
|
169
|
+
const packSmoke = opts.bin
|
|
170
|
+
? `
|
|
171
|
+
- name: 📦 Smoke-test the packed tarball
|
|
172
|
+
run: |
|
|
173
|
+
set -e
|
|
174
|
+
BIN=$(node -p "const p=require('./package.json');typeof p.bin==='string'?p.name.split('/').pop():Object.keys(p.bin)[0]")
|
|
175
|
+
TARBALL="$PWD/$(npm pack --silent | tail -n1)"
|
|
176
|
+
tar -tzf "$TARBALL" | grep -q . || { echo "::error::npm pack produced an empty tarball"; exit 1; }
|
|
177
|
+
SMOKE=$(mktemp -d)
|
|
178
|
+
cd "$SMOKE" && npm init -y >/dev/null && npm install "$TARBALL"
|
|
179
|
+
./node_modules/.bin/"$BIN" --version
|
|
180
|
+
`
|
|
181
|
+
: '';
|
|
154
182
|
jobs.push({
|
|
155
183
|
id: 'build',
|
|
156
184
|
needs: ['dependencies'],
|
|
@@ -158,7 +186,7 @@ export function githubJobs(config, opts = {}) {
|
|
|
158
186
|
|
|
159
187
|
- name: 🏗️ Build project
|
|
160
188
|
run: pnpm build
|
|
161
|
-
${attw}${publint}
|
|
189
|
+
${attw}${publint}${packSmoke}
|
|
162
190
|
- name: 📦 Upload build artifacts
|
|
163
191
|
uses: actions/upload-artifact@v7
|
|
164
192
|
with:
|
|
@@ -175,7 +203,7 @@ ${attw}${publint}
|
|
|
175
203
|
id: 'release',
|
|
176
204
|
// Gate the publish on everything that ran before it.
|
|
177
205
|
needs: jobs.map((job) => job.id),
|
|
178
|
-
if:
|
|
206
|
+
if: RELEASE_IF,
|
|
179
207
|
extra: ` permissions:
|
|
180
208
|
contents: write
|
|
181
209
|
issues: write
|
|
@@ -224,7 +252,14 @@ ${attw}${publint}
|
|
|
224
252
|
# for the package on npmjs.com (Settings → Trusted Publisher).
|
|
225
253
|
env:
|
|
226
254
|
GITHUB_TOKEN: \${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
227
|
-
run:
|
|
255
|
+
run: |
|
|
256
|
+
set -o pipefail
|
|
257
|
+
npx semantic-release 2>&1 | tee release.log
|
|
258
|
+
# semantic-release exits 0 when main moved on since this run started,
|
|
259
|
+
# publishing nothing (#690). Say so instead of going quietly green.
|
|
260
|
+
if grep -q "is behind the remote one" release.log; then
|
|
261
|
+
echo "::warning::main moved on; nothing was published. Re-run via workflow_dispatch."
|
|
262
|
+
fi`,
|
|
228
263
|
});
|
|
229
264
|
}
|
|
230
265
|
return jobs;
|
|
@@ -9,7 +9,7 @@ import { generateGitLabCI } from '../../cli/generators/gitlab-ci.js';
|
|
|
9
9
|
import { GH_WORKFLOWS, generateGhWorkflow } from '../../cli/generators/github-workflows.js';
|
|
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
|
-
import { scriptsOf } from './ci.js';
|
|
12
|
+
import { hasBin, scriptsOf } from './ci.js';
|
|
13
13
|
import { NPM_OIDC_CHECK, NPM_TRUST_FIXER } from './npm-trust.js';
|
|
14
14
|
import { usesPnpm } from './checks.js';
|
|
15
15
|
import { SIZE_LIMIT_SCRIPTS, SIZE_LIMIT_VERSION, composeVerifyScriptFromPkg, ensureScripts, } from '../../cli/generators/package-json.js';
|
|
@@ -451,6 +451,7 @@ export const FIXERS = [
|
|
|
451
451
|
overwrite: result.check === 'GitHub Actions',
|
|
452
452
|
// Only reference scripts this repo actually has (#364).
|
|
453
453
|
scripts: scriptsOf(pkg),
|
|
454
|
+
bin: hasBin(pkg),
|
|
454
455
|
});
|
|
455
456
|
// `pnpm/action-setup` is emitted without a `version:` input, so without
|
|
456
457
|
// this every job dies at setup with "No pnpm version is specified".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
"@next/eslint-plugin-next": "^16.3.2",
|
|
249
249
|
"@playwright/test": "^1.62.1",
|
|
250
250
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
251
|
-
"@rtorcato/repo-ai": "^3.1.
|
|
251
|
+
"@rtorcato/repo-ai": "^3.1.2",
|
|
252
252
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
253
253
|
"@semantic-release/exec": "^7.1.0",
|
|
254
254
|
"@semantic-release/github": "^12.0.9",
|