@davidvornholt/standards 0.28.0 → 0.29.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/README.md +10 -2
- package/package.json +1 -3
- package/src/cli.ts +28 -4
- package/src/poller-config.ts +0 -2
- package/src/poller-github-pulls.ts +0 -1
- package/src/poller-outcome.ts +6 -2
- package/src/poller-review-plan-parse.ts +4 -2
- package/src/poller-review-thread-write.ts +0 -1
- package/src/structure-check.ts +1 -1
- package/src/structure-profile.ts +11 -0
- package/src/structure-workspace.ts +15 -4
package/README.md
CHANGED
|
@@ -97,7 +97,15 @@ bun standards structure --profile source
|
|
|
97
97
|
|
|
98
98
|
`doctor` checks project-owned integration points such as `biome.jsonc`, `AGENTS.local.md`, package scripts, sync policy, settings overlays, and Dependabot inputs.
|
|
99
99
|
|
|
100
|
-
`structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
|
|
100
|
+
`structure` checks the exact Bun declaration, root and workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
|
|
101
|
+
|
|
102
|
+
Starting with 0.29.0, Biome runs once from the repository root instead of once per workspace. The root `package.json` must declare `lint` running `biome check --error-on-warnings .` and `lint:fix` running `biome check --write --error-on-warnings .`, and any workspace that declares `lint` or `lint:fix` fails. To upgrade an existing repository:
|
|
103
|
+
|
|
104
|
+
1. Install the new CLI with `bun add --dev --exact @davidvornholt/standards@0.29.0`.
|
|
105
|
+
2. Run `bun standards sync` so the canonical `packages/typescript-config` and `packages/a11y-testing` drop their lint scripts. A repository that pins `ref` in `sync-standards.local.json` first moves it to `v0.29.0` or later.
|
|
106
|
+
3. Add the root `lint` and `lint:fix` scripts and remove both scripts from every workspace `package.json`.
|
|
107
|
+
4. In `turbo.json`, replace the `lint` and `lint:fix` tasks with `//#lint` (`"outputs": []`) and `//#lint:fix` (`"cache": false`, `"outputs": []`), and delete any `<package>#lint` overrides. Keep the default inputs for `//#lint`.
|
|
108
|
+
5. Run `bun run lint:fix` and fix anything it still reports. The root run also checks root files that per-workspace runs never covered.
|
|
101
109
|
|
|
102
110
|
## dependabot
|
|
103
111
|
|
|
@@ -240,7 +248,7 @@ Bootstrap credentials and singular tokens still owned by an active repository ar
|
|
|
240
248
|
|
|
241
249
|
### Prepare existing consumers for GitHub App client IDs
|
|
242
250
|
|
|
243
|
-
`standards creds add github --dest ci:ci.broker_app` writes and verifies `app_id`, `client_id`, and `private_key` together.
|
|
251
|
+
`standards creds add github --dest ci:ci.broker_app` writes and verifies `app_id`, `client_id`, and `private_key` together. The canonical Standards sync workflow passes `client-id` to `actions/create-github-app-token@v3` when `ci.broker_app.client_id` exists and falls back to the deprecated `app-id` when it does not. Provision `client_id` in each existing consumer before a future release drops that fallback: re-run `bun standards creds add github --dest ci:ci.broker_app`, or add the App's public client ID at `ci.broker_app.client_id` with `sops`. Mirror the new key with a placeholder in `secrets/ci.example.yaml` and commit both files. Keep the numeric `app_id` until every local and canonical caller has switched to `client-id`.
|
|
244
252
|
|
|
245
253
|
## screenshots
|
|
246
254
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidvornholt/standards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -180,8 +180,6 @@
|
|
|
180
180
|
"src/sops-action-callers.ts"
|
|
181
181
|
],
|
|
182
182
|
"scripts": {
|
|
183
|
-
"lint": "biome check --error-on-warnings .",
|
|
184
|
-
"lint:fix": "biome check --write --error-on-warnings .",
|
|
185
183
|
"check-types": "tsc --noEmit",
|
|
186
184
|
"test": "bun test"
|
|
187
185
|
},
|
package/src/cli.ts
CHANGED
|
@@ -389,11 +389,31 @@ type MirrorResult = {
|
|
|
389
389
|
readonly stale: ReadonlyArray<string>;
|
|
390
390
|
};
|
|
391
391
|
|
|
392
|
+
type MirrorCommand = 'init' | 'sync';
|
|
393
|
+
|
|
392
394
|
type MirrorOptions = {
|
|
393
395
|
readonly upstream: ReadonlyMap<string, ManagedEntry>;
|
|
394
396
|
readonly consumer: string;
|
|
395
397
|
readonly previous: Record<string, string>;
|
|
396
398
|
readonly dryRun: boolean;
|
|
399
|
+
readonly command: MirrorCommand;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// A copy or zip tool that follows links turns a managed link into a directory of
|
|
403
|
+
// copies. Those copies are not in the lock, so the run refuses; for a link the
|
|
404
|
+
// message names what the path should be and the exact way back to it.
|
|
405
|
+
const directoryCollision = (
|
|
406
|
+
rel: string,
|
|
407
|
+
entry: ManagedEntry,
|
|
408
|
+
unlocked: ReadonlyArray<string>,
|
|
409
|
+
command: MirrorCommand,
|
|
410
|
+
): string => {
|
|
411
|
+
const holding = `${unlocked.length} path(s) this repository does not manage (${unlocked.slice(0, COLLISION_SAMPLE).join(', ')})`;
|
|
412
|
+
if (entry.kind !== 'symlink') {
|
|
413
|
+
return `${rel} is a directory holding ${holding}; move or delete them, then re-run`;
|
|
414
|
+
}
|
|
415
|
+
const canonical = posix.join(posix.dirname(rel), entry.target);
|
|
416
|
+
return `${rel} should be a symlink to the canonical ${canonical}, but it is a directory holding ${holding}; copy and zip tools that follow links can cause this. First move any of your own files out of ${rel}, then run \`rm -rf ${rel} && bun standards ${command}\``;
|
|
397
417
|
};
|
|
398
418
|
|
|
399
419
|
// Two things init/sync must settle before their first write: an escaping
|
|
@@ -408,10 +428,11 @@ const mirrorPreconditionProblems = async (
|
|
|
408
428
|
consumer: string,
|
|
409
429
|
upstream: ReadonlyMap<string, ManagedEntry>,
|
|
410
430
|
previous: Record<string, string>,
|
|
431
|
+
command: MirrorCommand,
|
|
411
432
|
): Promise<ReadonlyArray<string>> => {
|
|
412
433
|
const locked = new Set(Object.keys(previous));
|
|
413
434
|
const collisions = await Promise.all(
|
|
414
|
-
[...upstream
|
|
435
|
+
[...upstream].map(async ([rel, entry]) => {
|
|
415
436
|
const ancestor = await interposedAncestor(consumer, rel, upstream);
|
|
416
437
|
if (ancestor !== null) {
|
|
417
438
|
return `${rel} is below the ${ancestor.kind} ${ancestor.rel}; refusing to write through an existing file or symlink ancestor; move it aside before syncing this payload`;
|
|
@@ -423,7 +444,7 @@ const mirrorPreconditionProblems = async (
|
|
|
423
444
|
const unlocked = await unlockedPathsUnder(consumer, rel, locked);
|
|
424
445
|
return unlocked.length === 0
|
|
425
446
|
? null
|
|
426
|
-
:
|
|
447
|
+
: directoryCollision(rel, entry, unlocked, command);
|
|
427
448
|
}),
|
|
428
449
|
);
|
|
429
450
|
return [
|
|
@@ -545,12 +566,13 @@ const mirror = async ({
|
|
|
545
566
|
consumer,
|
|
546
567
|
previous,
|
|
547
568
|
dryRun,
|
|
569
|
+
command,
|
|
548
570
|
}: MirrorOptions): Promise<MirrorResult> => {
|
|
549
571
|
for (const rel of Object.keys(previous)) {
|
|
550
572
|
assertSafeRelativePath(rel, 'sync-standards.lock file');
|
|
551
573
|
}
|
|
552
574
|
assertNoProblems(
|
|
553
|
-
await mirrorPreconditionProblems(consumer, upstream, previous),
|
|
575
|
+
await mirrorPreconditionProblems(consumer, upstream, previous, command),
|
|
554
576
|
'cannot mirror canonical files',
|
|
555
577
|
);
|
|
556
578
|
const next: Record<string, string> = {};
|
|
@@ -682,7 +704,7 @@ const runInit = async (
|
|
|
682
704
|
assertNoProblems(
|
|
683
705
|
[
|
|
684
706
|
...symlinkTargetProblems(seeds),
|
|
685
|
-
...(await mirrorPreconditionProblems(consumer, upstream, {})),
|
|
707
|
+
...(await mirrorPreconditionProblems(consumer, upstream, {}, 'init')),
|
|
686
708
|
],
|
|
687
709
|
'cannot initialize from this source',
|
|
688
710
|
);
|
|
@@ -712,6 +734,7 @@ const runInit = async (
|
|
|
712
734
|
consumer,
|
|
713
735
|
previous: {},
|
|
714
736
|
dryRun: false,
|
|
737
|
+
command: 'init',
|
|
715
738
|
});
|
|
716
739
|
reportMirror(result, false);
|
|
717
740
|
await applyProspectiveDependabot(consumer, prospectiveDependabot, false);
|
|
@@ -745,6 +768,7 @@ const runSync = async (
|
|
|
745
768
|
consumer,
|
|
746
769
|
previous: lock?.files ?? {},
|
|
747
770
|
dryRun,
|
|
771
|
+
command: 'sync',
|
|
748
772
|
});
|
|
749
773
|
reportMirror(result, dryRun);
|
|
750
774
|
const generated = await applyProspectiveDependabot(
|
package/src/poller-config.ts
CHANGED
|
@@ -14,7 +14,6 @@ const CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
|
14
14
|
'repos',
|
|
15
15
|
'model',
|
|
16
16
|
'reasoningEffort',
|
|
17
|
-
// biome-ignore lint/security/noSecrets: a config key name, not a credential.
|
|
18
17
|
'maxJobsPerTick',
|
|
19
18
|
'staleClaimHours',
|
|
20
19
|
'runTimeoutMinutes',
|
|
@@ -167,7 +166,6 @@ export const parsePollerConfig = (
|
|
|
167
166
|
),
|
|
168
167
|
maxJobsPerTick: parsePositiveInteger(
|
|
169
168
|
raw.maxJobsPerTick,
|
|
170
|
-
// biome-ignore lint/security/noSecrets: a config key name, not a credential.
|
|
171
169
|
'maxJobsPerTick',
|
|
172
170
|
DEFAULT_MAX_JOBS_PER_TICK,
|
|
173
171
|
problems,
|
|
@@ -175,7 +175,6 @@ export const markPullRequestReady = async (
|
|
|
175
175
|
): Promise<void> => {
|
|
176
176
|
const response = await request(token, 'POST', '/graphql', {
|
|
177
177
|
query:
|
|
178
|
-
// biome-ignore lint/security/noSecrets: a GraphQL mutation string, not a credential.
|
|
179
178
|
'mutation($id: ID!) { markPullRequestReadyForReview(input: {pullRequestId: $id}) { pullRequest { isDraft } } }',
|
|
180
179
|
variables: { id: pullRequestNodeId },
|
|
181
180
|
});
|
package/src/poller-outcome.ts
CHANGED
|
@@ -19,8 +19,12 @@ const hasExactKeys = (
|
|
|
19
19
|
value: Readonly<Record<string, unknown>>,
|
|
20
20
|
expected: ReadonlyArray<string>,
|
|
21
21
|
): boolean => {
|
|
22
|
-
const actual = Object.keys(value).sort()
|
|
23
|
-
|
|
22
|
+
const actual = Object.keys(value).sort((left, right) =>
|
|
23
|
+
left.localeCompare(right),
|
|
24
|
+
);
|
|
25
|
+
const sortedExpected = [...expected].sort((left, right) =>
|
|
26
|
+
left.localeCompare(right),
|
|
27
|
+
);
|
|
24
28
|
return (
|
|
25
29
|
actual.length === sortedExpected.length &&
|
|
26
30
|
actual.every((key, index) => key === sortedExpected[index])
|
|
@@ -22,8 +22,10 @@ const hasExactKeys = (
|
|
|
22
22
|
value: Readonly<Record<string, unknown>>,
|
|
23
23
|
keys: ReadonlyArray<string>,
|
|
24
24
|
): boolean => {
|
|
25
|
-
const actual = Object.keys(value).sort()
|
|
26
|
-
|
|
25
|
+
const actual = Object.keys(value).sort((left, right) =>
|
|
26
|
+
left.localeCompare(right),
|
|
27
|
+
);
|
|
28
|
+
const expected = [...keys].sort((left, right) => left.localeCompare(right));
|
|
27
29
|
return (
|
|
28
30
|
actual.length === expected.length &&
|
|
29
31
|
actual.every((key, index) => key === expected[index])
|
|
@@ -2,7 +2,6 @@ import { apiError, HTTP_OK, request } from './github-api';
|
|
|
2
2
|
import { isRecord } from './github-settings-parse';
|
|
3
3
|
|
|
4
4
|
const REPLY_MUTATION =
|
|
5
|
-
// biome-ignore lint/security/noSecrets: a GraphQL mutation string, not a credential.
|
|
6
5
|
'mutation($id: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $id, body: $body }) { comment { id } } }';
|
|
7
6
|
|
|
8
7
|
const RESOLVE_MUTATION =
|
package/src/structure-check.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { collectCiSecretsProblems } from './structure-secrets';
|
|
|
16
16
|
import { inspectWorkspace, type Workspace } from './structure-workspace';
|
|
17
17
|
|
|
18
18
|
const ROOT_FIXED_SCRIPTS = new Set(
|
|
19
|
-
'standards,check,check:fix,test:a11y'.split(','),
|
|
19
|
+
'standards,check,check:fix,lint,lint:fix,test:a11y'.split(','),
|
|
20
20
|
);
|
|
21
21
|
const GLOB_SUFFIX = '/*';
|
|
22
22
|
const WORKSPACES_REQUIREMENT =
|
package/src/structure-profile.ts
CHANGED
|
@@ -13,6 +13,8 @@ const CONSUMER_CHECK =
|
|
|
13
13
|
const CONSUMER_CHECK_FIX =
|
|
14
14
|
'turbo run lint:fix check-types test build test:a11y --output-logs=errors-only';
|
|
15
15
|
const ROOT_A11Y = 'turbo run test:a11y';
|
|
16
|
+
const ROOT_LINT = 'biome check --error-on-warnings .';
|
|
17
|
+
const ROOT_LINT_FIX = 'biome check --write --error-on-warnings .';
|
|
16
18
|
|
|
17
19
|
export const PUBLISHED_CLI_WORKSPACE = 'packages/standards-cli';
|
|
18
20
|
const PUBLISHED_CLI_PACKAGE = '@davidvornholt/standards';
|
|
@@ -51,6 +53,14 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
|
51
53
|
},
|
|
52
54
|
];
|
|
53
55
|
|
|
56
|
+
// Biome runs once over the whole repository from the root. With the project
|
|
57
|
+
// domain on, every invocation loads the entire repository, so one root run
|
|
58
|
+
// replaces a full project load per workspace and also covers root files.
|
|
59
|
+
const ROOT_LINT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
60
|
+
{ name: 'lint', commands: [ROOT_LINT], exact: true },
|
|
61
|
+
{ name: 'lint:fix', commands: [ROOT_LINT_FIX], exact: true },
|
|
62
|
+
];
|
|
63
|
+
|
|
54
64
|
export const rootScriptExpectations = (
|
|
55
65
|
profile: StructureProfile,
|
|
56
66
|
requireA11y: boolean,
|
|
@@ -61,6 +71,7 @@ export const rootScriptExpectations = (
|
|
|
61
71
|
{ name: 'check', commands: [CONSUMER_CHECK], exact: false },
|
|
62
72
|
{ name: 'check:fix', commands: [CONSUMER_CHECK_FIX], exact: false },
|
|
63
73
|
]),
|
|
74
|
+
...ROOT_LINT_EXPECTATIONS,
|
|
64
75
|
...(requireA11y
|
|
65
76
|
? [{ name: 'test:a11y', commands: [ROOT_A11Y], exact: false }]
|
|
66
77
|
: []),
|
|
@@ -13,10 +13,12 @@ export type Workspace = {
|
|
|
13
13
|
};
|
|
14
14
|
const WORKSPACE_SCRIPTS: ReadonlyArray<readonly [string, string]> = [
|
|
15
15
|
['check-types', 'tsc --noEmit'],
|
|
16
|
-
['lint', 'biome check --error-on-warnings .'],
|
|
17
|
-
['lint:fix', 'biome check --write --error-on-warnings .'],
|
|
18
16
|
['test', 'bun test'],
|
|
19
17
|
];
|
|
18
|
+
// Biome lints the whole repository once from the root "lint" scripts, and
|
|
19
|
+
// Turbo runs only that root task. A workspace copy would be a stale second
|
|
20
|
+
// entry point, so declaring one fails instead of being silently ignored.
|
|
21
|
+
const ROOT_ONLY_SCRIPTS = ['lint', 'lint:fix'] as const;
|
|
20
22
|
const A11Y_DEPENDENCIES = ['@axe-core/playwright', '@playwright/test'] as const;
|
|
21
23
|
const DEPENDENCY_FIELDS =
|
|
22
24
|
'dependencies devDependencies peerDependencies optionalDependencies'.split(
|
|
@@ -57,8 +59,9 @@ const containsA11ySuite = async (dir: string): Promise<boolean> => {
|
|
|
57
59
|
);
|
|
58
60
|
return nested.includes(true);
|
|
59
61
|
};
|
|
60
|
-
const inspectScripts = (ws: Workspace): ReadonlyArray<string> =>
|
|
61
|
-
|
|
62
|
+
const inspectScripts = (ws: Workspace): ReadonlyArray<string> => {
|
|
63
|
+
const scripts = isRecord(ws.manifest.scripts) ? ws.manifest.scripts : {};
|
|
64
|
+
const required = WORKSPACE_SCRIPTS.flatMap(([name, command]) => {
|
|
62
65
|
const script = scriptOf(ws.manifest, name);
|
|
63
66
|
const accepted =
|
|
64
67
|
hasSafeCommand(script, command) ||
|
|
@@ -67,6 +70,14 @@ const inspectScripts = (ws: Workspace): ReadonlyArray<string> =>
|
|
|
67
70
|
name === 'test' ? 'bun test or bun test --isolate' : command;
|
|
68
71
|
return accepted ? [] : [`${ws.rel}: script "${name}" must run ${expected}`];
|
|
69
72
|
});
|
|
73
|
+
const rootOnly = ROOT_ONLY_SCRIPTS.filter((name) =>
|
|
74
|
+
Object.hasOwn(scripts, name),
|
|
75
|
+
).map(
|
|
76
|
+
(name) =>
|
|
77
|
+
`${ws.rel}: remove script "${name}"; linting runs once at the repository root through the root "lint" and "lint:fix" scripts`,
|
|
78
|
+
);
|
|
79
|
+
return [...required, ...rootOnly];
|
|
80
|
+
};
|
|
70
81
|
const declaredDependencies = (manifest: Record<string, unknown>) =>
|
|
71
82
|
DEPENDENCY_FIELDS.flatMap((field) => {
|
|
72
83
|
const deps = manifest[field];
|