@rtorcato/repo-tooling 3.0.0 → 3.2.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.
|
@@ -5,10 +5,11 @@ import { resolveLanguageModule } from '../../languages/registry.js';
|
|
|
5
5
|
import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
|
|
6
6
|
import { detectLanguage } from '../utils/detect-language.js';
|
|
7
7
|
import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
8
|
+
import { checkGitIdentity } from '../../base/git-identity.js';
|
|
8
9
|
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
9
10
|
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
10
11
|
import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
|
|
11
|
-
import { allDeps, checkAreTheTypesWrong, checkDocsSite, checkEnginesNode, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
12
|
+
import { allDeps, checkAreTheTypesWrong, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
12
13
|
export { evaluateNodeVersion };
|
|
13
14
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
14
15
|
// Detects the broken-release-on-protected-main footgun: a workflow that runs
|
|
@@ -140,6 +141,7 @@ function demoteDeclined(results, lock) {
|
|
|
140
141
|
async function runBaseChecks(dir, lock, opts) {
|
|
141
142
|
const results = [];
|
|
142
143
|
results.push(checkLockfile(lock));
|
|
144
|
+
results.push(await checkGitIdentity(dir));
|
|
143
145
|
results.push(await checkEditorConfig(dir));
|
|
144
146
|
results.push(await checkFile(dir, COMMITLINT_FILE_CHECK));
|
|
145
147
|
if (opts.hooks) {
|
|
@@ -216,6 +218,8 @@ export async function runDoctor(dir) {
|
|
|
216
218
|
results.push(evaluateNodeVersion(process.version));
|
|
217
219
|
results.push(checkPackageJson(pkg));
|
|
218
220
|
results.push(checkEnginesNode(pkg));
|
|
221
|
+
results.push(await checkConfigSchemaVersions(targetDir, pkg));
|
|
222
|
+
results.push(checkGitDependencies(pkg));
|
|
219
223
|
results.push(await checkVscodeExtensions(targetDir));
|
|
220
224
|
results.push(await checkNodeVersionPin(targetDir));
|
|
221
225
|
results.push(await checkNodeVersionConsistency(targetDir, pkg));
|
|
@@ -947,3 +947,161 @@ export async function checkTailwind(dir, pkg) {
|
|
|
947
947
|
hint: 'Run `npx @rtorcato/repo-tooling fix tailwind` to scaffold the v4 PostCSS wiring',
|
|
948
948
|
};
|
|
949
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Both checks below read `package.json` alone — no network, no lockfile. They
|
|
952
|
+
* report `optional-missing` rather than `drift`: each is a policy call a repo
|
|
953
|
+
* can legitimately decide against, so they surface in doctor's output and can
|
|
954
|
+
* be declined in `.repo-tooling.json`, but neither fails a build.
|
|
955
|
+
*/
|
|
956
|
+
/** `[major, minor, patch]` floor of a range, or null when it hasn't got one. */
|
|
957
|
+
export function rangeFloor(range) {
|
|
958
|
+
const m = /^[\s^~>=<v]*(\d+)\.(\d+)\.(\d+)/.exec(range);
|
|
959
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Major.minor only — patch is deliberately ignored. New config keys and new
|
|
963
|
+
* behaviour arrive in minors; a dev floor five patches above the peer floor
|
|
964
|
+
* (`^2.5.0` vs `^2.5.5`) is just a routine bump and flagging it would make
|
|
965
|
+
* this check fire on almost every package.
|
|
966
|
+
*/
|
|
967
|
+
function compareFloor(a, b) {
|
|
968
|
+
return a[0] - b[0] || a[1] - b[1];
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Config files whose `$schema` URL carries the tool version the config is
|
|
972
|
+
* written for, and the package that reads them. One entry per tool; adding
|
|
973
|
+
* another is a line here.
|
|
974
|
+
*/
|
|
975
|
+
const SCHEMA_CONFIGS = [
|
|
976
|
+
{ files: ['biome.json', 'biome.jsonc'], host: 'biomejs.dev', pkg: '@biomejs/biome' },
|
|
977
|
+
];
|
|
978
|
+
/** The version out of `https://biomejs.dev/schemas/2.5.0/schema.json`. */
|
|
979
|
+
export function schemaUrlVersion(url) {
|
|
980
|
+
const m = /\/(\d+\.\d+\.\d+)\//.exec(url);
|
|
981
|
+
return m?.[1] ? rangeFloor(m[1]) : null;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* `"$schema": "..."` without parsing the file. Biome configs may be JSONC, and
|
|
985
|
+
* a comment is enough to break `JSON.parse` — the one field this needs is
|
|
986
|
+
* cheaper and safer to read directly.
|
|
987
|
+
*/
|
|
988
|
+
function readSchemaUrl(contents) {
|
|
989
|
+
return /"\$schema"\s*:\s*"([^"]+)"/.exec(contents)?.[1] ?? null;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* A shipped config written for a newer tool version than the package claims to
|
|
993
|
+
* support (#330). The `$schema` URL is an explicit, machine-readable statement
|
|
994
|
+
* of which version the config targets, so comparing it against the declared
|
|
995
|
+
* dependency floor is exact — no heuristics, no false positives.
|
|
996
|
+
*
|
|
997
|
+
* This is the #330 defect precisely: `tooling/biome/biome.json` carries
|
|
998
|
+
* `$schema` 2.5.0 and uses `linter.rules.preset`, a 2.5 key, while
|
|
999
|
+
* `peerDependencies` advertised `@biomejs/biome: ^2.0.0`. Consumers on 2.0–2.4
|
|
1000
|
+
* got `Found an unknown key \`preset\`` with nothing pointing at the range.
|
|
1001
|
+
*
|
|
1002
|
+
* It reads the same way in a consuming repo: a `biome.json` targeting 2.5.0
|
|
1003
|
+
* with `@biomejs/biome: ^2.3.0` in devDependencies is the identical failure,
|
|
1004
|
+
* one level down.
|
|
1005
|
+
*
|
|
1006
|
+
* The floor is what matters, not the ceiling — `^2.0.0` resolves to the newest
|
|
1007
|
+
* 2.x today, so the repo's own install works and only consumers pinned lower
|
|
1008
|
+
* break. That is exactly why this goes unnoticed.
|
|
1009
|
+
*/
|
|
1010
|
+
export async function checkConfigSchemaVersions(dir, pkg) {
|
|
1011
|
+
const check = 'Config schema versions';
|
|
1012
|
+
// peerDependencies is the contract a publisher offers; devDependencies is
|
|
1013
|
+
// what a leaf repo actually installs. Prefer the former when present.
|
|
1014
|
+
const declared = {
|
|
1015
|
+
...(pkg?.dependencies ?? {}),
|
|
1016
|
+
...(pkg?.devDependencies ?? {}),
|
|
1017
|
+
...(pkg?.peerDependencies ?? {}),
|
|
1018
|
+
};
|
|
1019
|
+
const mismatches = [];
|
|
1020
|
+
let checked = 0;
|
|
1021
|
+
for (const spec of SCHEMA_CONFIGS) {
|
|
1022
|
+
for (const file of spec.files) {
|
|
1023
|
+
const filepath = path.join(dir, file);
|
|
1024
|
+
if (!(await fs.pathExists(filepath)))
|
|
1025
|
+
continue;
|
|
1026
|
+
const url = readSchemaUrl(await fs.readFile(filepath, 'utf8'));
|
|
1027
|
+
if (!url?.includes(spec.host))
|
|
1028
|
+
continue;
|
|
1029
|
+
const schemaVersion = schemaUrlVersion(url);
|
|
1030
|
+
const range = declared[spec.pkg];
|
|
1031
|
+
if (!schemaVersion || !range)
|
|
1032
|
+
continue;
|
|
1033
|
+
const floor = rangeFloor(range);
|
|
1034
|
+
if (!floor)
|
|
1035
|
+
continue;
|
|
1036
|
+
checked++;
|
|
1037
|
+
if (compareFloor(schemaVersion, floor) > 0) {
|
|
1038
|
+
const v = schemaVersion.join('.');
|
|
1039
|
+
mismatches.push(`${file} targets ${spec.pkg} ${v} but the range is ${range}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (checked === 0) {
|
|
1044
|
+
return { check, status: 'ok', detail: 'no versioned config schemas to compare' };
|
|
1045
|
+
}
|
|
1046
|
+
if (mismatches.length === 0) {
|
|
1047
|
+
return {
|
|
1048
|
+
check,
|
|
1049
|
+
status: 'ok',
|
|
1050
|
+
detail: `${checked} config schema${checked === 1 ? '' : 's'} within the declared version range`,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
check,
|
|
1055
|
+
status: 'optional-missing',
|
|
1056
|
+
detail: `${mismatches.length} config${mismatches.length === 1 ? '' : 's'} written for a newer tool than declared: ${mismatches.join('; ')}`,
|
|
1057
|
+
hint: 'Raise the dependency floor to the version the config targets, or rewrite the config for the oldest version supported. Anyone resolving below the schema version gets a config-parse error that never mentions the version range.',
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/** npm git specifiers, including the bare `owner/repo` GitHub shorthand. */
|
|
1061
|
+
const GIT_PROTOCOL = /^(?:github|gitlab|bitbucket|gist):|^git\+|^git:\/\//;
|
|
1062
|
+
const GITHUB_SHORTHAND = /^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*(?:#.*)?$/;
|
|
1063
|
+
export function isGitSpecifier(spec) {
|
|
1064
|
+
return GIT_PROTOCOL.test(spec) || GITHUB_SHORTHAND.test(spec);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* A git dependency with no `#ref` (#332). The package manager resolves it once
|
|
1068
|
+
* and pins the commit in the lockfile, so it never moves again until somebody
|
|
1069
|
+
* re-resolves by hand — with no version mismatch, no Dependabot PR, and no
|
|
1070
|
+
* signal of any kind that it has gone stale.
|
|
1071
|
+
*
|
|
1072
|
+
* This is how the docs homepage kept advertising `@rtorcato/js-tooling` for
|
|
1073
|
+
* days after the rename: `github:rtorcato/shared-docs` had been pinned to a
|
|
1074
|
+
* pre-rename commit and nothing could tell.
|
|
1075
|
+
*/
|
|
1076
|
+
export function checkGitDependencies(pkg) {
|
|
1077
|
+
const check = 'Git dependencies';
|
|
1078
|
+
const fields = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
1079
|
+
const refless = [];
|
|
1080
|
+
let total = 0;
|
|
1081
|
+
for (const field of fields) {
|
|
1082
|
+
const deps = pkg?.[field] ?? {};
|
|
1083
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
1084
|
+
if (typeof spec !== 'string' || !isGitSpecifier(spec))
|
|
1085
|
+
continue;
|
|
1086
|
+
total++;
|
|
1087
|
+
if (!spec.includes('#'))
|
|
1088
|
+
refless.push(`${name} (${spec})`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (total === 0) {
|
|
1092
|
+
return { check, status: 'ok', detail: 'no git dependencies' };
|
|
1093
|
+
}
|
|
1094
|
+
if (refless.length === 0) {
|
|
1095
|
+
return {
|
|
1096
|
+
check,
|
|
1097
|
+
status: 'ok',
|
|
1098
|
+
detail: `${total} git dependenc${total === 1 ? 'y' : 'ies'}, all with an explicit ref`,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
check,
|
|
1103
|
+
status: 'optional-missing',
|
|
1104
|
+
detail: `${refless.length} git dependenc${refless.length === 1 ? 'y' : 'ies'} with no ref — pinned to whatever commit was current at install time: ${refless.join('; ')}`,
|
|
1105
|
+
hint: 'Prefer publishing to a registry and depending on a semver range. Failing that, add an explicit ref — `#semver:^1.2.0` is strongest, `#main` at least makes the intent legible and `pnpm update` meaningful.',
|
|
1106
|
+
};
|
|
1107
|
+
}
|
package/package.json
CHANGED
|
@@ -43,6 +43,13 @@ export default {
|
|
|
43
43
|
{
|
|
44
44
|
assets: ['CHANGELOG.md', 'package.json', 'README.md'],
|
|
45
45
|
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
|
46
|
+
// Don't label the "release is failing" issue. The default is
|
|
47
|
+
// ['semantic-release'], and GitHub rejects issue creation outright when
|
|
48
|
+
// the label doesn't exist on the repo — so a failed release fails *again*
|
|
49
|
+
// with a 422 that buries the real error under an octokit stack trace.
|
|
50
|
+
// false keeps the issue (and its post-mortem) without requiring every
|
|
51
|
+
// consuming repo to pre-create a label.
|
|
52
|
+
labels: false,
|
|
46
53
|
},
|
|
47
54
|
],
|
|
48
55
|
[
|