@davidvornholt/standards 0.1.0 → 0.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.
- package/package.json +11 -2
- package/src/cli.ts +184 -0
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidvornholt/standards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/davidvornholt/standards.git",
|
|
9
|
+
"directory": "packages/standards-cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/davidvornholt/standards#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/davidvornholt/standards/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"bin": {
|
|
8
17
|
"standards": "src/cli.ts"
|
|
@@ -19,7 +28,7 @@
|
|
|
19
28
|
"test": "bun test"
|
|
20
29
|
},
|
|
21
30
|
"devDependencies": {
|
|
22
|
-
"@davidvornholt/typescript-config": "
|
|
31
|
+
"@davidvornholt/typescript-config": "0.0.0",
|
|
23
32
|
"@types/bun": "^1.3.14",
|
|
24
33
|
"typescript": "7.0.2"
|
|
25
34
|
},
|
package/src/cli.ts
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
} from 'node:path';
|
|
34
34
|
import process from 'node:process';
|
|
35
35
|
|
|
36
|
+
const { YAML: BunYaml } = await import('bun');
|
|
37
|
+
|
|
36
38
|
const DEFAULT_UPSTREAM = 'github:davidvornholt/standards';
|
|
37
39
|
|
|
38
40
|
// Characters of a sha256 hex digest shown in drift reports; enough to identify.
|
|
@@ -477,6 +479,179 @@ const hasStandardsImport = (justfile: string): boolean =>
|
|
|
477
479
|
);
|
|
478
480
|
});
|
|
479
481
|
|
|
482
|
+
const DEPENDABOT_BASELINE_ECOSYSTEMS = ['bun', 'github-actions'] as const;
|
|
483
|
+
const DEPENDABOT_SCHEDULE_INTERVALS = new Set([
|
|
484
|
+
'daily',
|
|
485
|
+
'weekly',
|
|
486
|
+
'monthly',
|
|
487
|
+
'quarterly',
|
|
488
|
+
'semiannually',
|
|
489
|
+
'yearly',
|
|
490
|
+
'cron',
|
|
491
|
+
]);
|
|
492
|
+
|
|
493
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
494
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
495
|
+
|
|
496
|
+
const isNonEmptyString = (value: unknown): value is string =>
|
|
497
|
+
typeof value === 'string' && value.length > 0;
|
|
498
|
+
|
|
499
|
+
type DependabotUpdateInspection = {
|
|
500
|
+
readonly problems: ReadonlyArray<string>;
|
|
501
|
+
readonly rootEcosystem: string | null;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const inspectDependabotSchedule = (
|
|
505
|
+
schedule: unknown,
|
|
506
|
+
label: string,
|
|
507
|
+
): ReadonlyArray<string> => {
|
|
508
|
+
if (!(isRecord(schedule) && isNonEmptyString(schedule.interval))) {
|
|
509
|
+
return [`${label} must define schedule.interval`];
|
|
510
|
+
}
|
|
511
|
+
if (!DEPENDABOT_SCHEDULE_INTERVALS.has(schedule.interval)) {
|
|
512
|
+
return [`${label} has an unsupported schedule.interval`];
|
|
513
|
+
}
|
|
514
|
+
if (schedule.interval === 'cron' && !isNonEmptyString(schedule.cronjob)) {
|
|
515
|
+
return [`${label} must define schedule.cronjob for a cron interval`];
|
|
516
|
+
}
|
|
517
|
+
return [];
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
type DependabotGroupInspection = {
|
|
521
|
+
readonly problems: ReadonlyArray<string>;
|
|
522
|
+
readonly scheduledGroups: ReadonlySet<string>;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const inspectDependabotGroups = (
|
|
526
|
+
groups: unknown,
|
|
527
|
+
): DependabotGroupInspection => {
|
|
528
|
+
if (groups === undefined) {
|
|
529
|
+
return { problems: [], scheduledGroups: new Set() };
|
|
530
|
+
}
|
|
531
|
+
if (!isRecord(groups)) {
|
|
532
|
+
return {
|
|
533
|
+
problems: [
|
|
534
|
+
'.github/dependabot.yml multi-ecosystem-groups must be a mapping',
|
|
535
|
+
],
|
|
536
|
+
scheduledGroups: new Set(),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const problems: Array<string> = [];
|
|
541
|
+
const scheduledGroups = new Set<string>();
|
|
542
|
+
for (const [name, group] of Object.entries(groups)) {
|
|
543
|
+
const label = `.github/dependabot.yml multi-ecosystem-groups.${name}`;
|
|
544
|
+
const groupProblems = isRecord(group)
|
|
545
|
+
? inspectDependabotSchedule(group.schedule, label)
|
|
546
|
+
: [`${label} must be a mapping`];
|
|
547
|
+
problems.push(...groupProblems);
|
|
548
|
+
if (groupProblems.length === 0) {
|
|
549
|
+
scheduledGroups.add(name);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return { problems, scheduledGroups };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const inspectDependabotUpdate = (
|
|
556
|
+
update: unknown,
|
|
557
|
+
index: number,
|
|
558
|
+
scheduledGroups: ReadonlySet<string>,
|
|
559
|
+
): DependabotUpdateInspection => {
|
|
560
|
+
const label = `.github/dependabot.yml updates[${index}]`;
|
|
561
|
+
if (!isRecord(update)) {
|
|
562
|
+
return { problems: [`${label} must be a mapping`], rootEcosystem: null };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const {
|
|
566
|
+
directory,
|
|
567
|
+
directories,
|
|
568
|
+
schedule,
|
|
569
|
+
'multi-ecosystem-group': multiEcosystemGroup,
|
|
570
|
+
'package-ecosystem': ecosystem,
|
|
571
|
+
} = update;
|
|
572
|
+
const problems: Array<string> = [];
|
|
573
|
+
if (!isNonEmptyString(ecosystem)) {
|
|
574
|
+
problems.push(`${label} must define package-ecosystem`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const hasDirectory = isNonEmptyString(directory);
|
|
578
|
+
const hasDirectories =
|
|
579
|
+
Array.isArray(directories) &&
|
|
580
|
+
directories.length > 0 &&
|
|
581
|
+
directories.every(isNonEmptyString);
|
|
582
|
+
if (hasDirectory === hasDirectories) {
|
|
583
|
+
problems.push(
|
|
584
|
+
`${label} must define exactly one of directory or directories`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (schedule === undefined && isNonEmptyString(multiEcosystemGroup)) {
|
|
589
|
+
if (!scheduledGroups.has(multiEcosystemGroup)) {
|
|
590
|
+
problems.push(
|
|
591
|
+
`${label} must reference a scheduled multi-ecosystem group`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
problems.push(...inspectDependabotSchedule(schedule, label));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const targetsRoot =
|
|
599
|
+
directory === '/' ||
|
|
600
|
+
(Array.isArray(directories) && directories.includes('/'));
|
|
601
|
+
return {
|
|
602
|
+
problems,
|
|
603
|
+
rootEcosystem:
|
|
604
|
+
isNonEmptyString(ecosystem) && targetsRoot ? ecosystem : null,
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const inspectDependabot = (raw: string): ReadonlyArray<string> => {
|
|
609
|
+
const problems: Array<string> = [];
|
|
610
|
+
let config: unknown;
|
|
611
|
+
try {
|
|
612
|
+
config = BunYaml.parse(raw);
|
|
613
|
+
} catch {
|
|
614
|
+
return ['.github/dependabot.yml must contain valid YAML'];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!isRecord(config)) {
|
|
618
|
+
return ['.github/dependabot.yml must contain a YAML mapping'];
|
|
619
|
+
}
|
|
620
|
+
if (config.version !== 2) {
|
|
621
|
+
problems.push('.github/dependabot.yml must use version: 2');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const { updates, 'multi-ecosystem-groups': multiEcosystemGroups } = config;
|
|
625
|
+
if (!Array.isArray(updates)) {
|
|
626
|
+
problems.push('.github/dependabot.yml must define an updates list');
|
|
627
|
+
return problems;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const groupInspection = inspectDependabotGroups(multiEcosystemGroups);
|
|
631
|
+
problems.push(...groupInspection.problems);
|
|
632
|
+
const rootEcosystems = new Set<string>();
|
|
633
|
+
for (const [index, update] of updates.entries()) {
|
|
634
|
+
const inspection = inspectDependabotUpdate(
|
|
635
|
+
update,
|
|
636
|
+
index,
|
|
637
|
+
groupInspection.scheduledGroups,
|
|
638
|
+
);
|
|
639
|
+
problems.push(...inspection.problems);
|
|
640
|
+
if (inspection.rootEcosystem !== null) {
|
|
641
|
+
rootEcosystems.add(inspection.rootEcosystem);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const ecosystem of DEPENDABOT_BASELINE_ECOSYSTEMS) {
|
|
646
|
+
if (!rootEcosystems.has(ecosystem)) {
|
|
647
|
+
problems.push(
|
|
648
|
+
`.github/dependabot.yml must include a root-directory ${ecosystem} ecosystem`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return problems;
|
|
653
|
+
};
|
|
654
|
+
|
|
480
655
|
const inspectPackageJson = (packageRaw: string): ReadonlyArray<string> => {
|
|
481
656
|
const problems: Array<string> = [];
|
|
482
657
|
const packageJson = JSON.parse(packageRaw) as Record<string, unknown>;
|
|
@@ -514,6 +689,15 @@ const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
|
514
689
|
problems.push('AGENTS.local.md must exist for project-specific guidance');
|
|
515
690
|
}
|
|
516
691
|
|
|
692
|
+
const dependabot = await readTextIfPresent(
|
|
693
|
+
join(consumer, '.github/dependabot.yml'),
|
|
694
|
+
);
|
|
695
|
+
if (dependabot === null) {
|
|
696
|
+
problems.push('.github/dependabot.yml must exist');
|
|
697
|
+
} else {
|
|
698
|
+
problems.push(...inspectDependabot(dependabot));
|
|
699
|
+
}
|
|
700
|
+
|
|
517
701
|
const packagePath = join(consumer, 'package.json');
|
|
518
702
|
const packageRaw = await readTextIfPresent(packagePath);
|
|
519
703
|
if (packageRaw === null) {
|