@davidvornholt/standards 0.6.0 → 0.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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli.ts +84 -2
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ standards github --apply
|
|
|
18
18
|
|
|
19
19
|
## Configuration
|
|
20
20
|
|
|
21
|
+
- **`sync-standards.local.json`** (optional) — consumer-owned sync policy at the repo root, validated by `doctor`/`check` and every `init`/`sync` even when an explicit ref or local source makes its `"ref"` irrelevant. All fields optional; a missing file means the defaults. `"ref"` is a non-empty single-line string that pins a tag, branch, or full commit sha to sync from instead of `main` (an explicit `--ref` overrides it; a local-path `--from` source is used as-is and ignores only the validated pin). `"autoSync": false` is read by the standards-sync workflow, not the CLI, and skips the weekly scheduled run. Version 0.7.0 removes the legacy `STANDARDS_AUTO_SYNC` and `STANDARDS_SYNC_REF` variable behavior; consumers must upgrade the package and lockfile before adopting a policy file.
|
|
21
22
|
- **`GH_TOKEN` / `GITHUB_TOKEN`** (optional) — GitHub API token for the `github` command and the GitHub portion of `check`. When neither is set, the token from the local `gh` CLI is used; with no token at all, reads still work on public repositories. `--apply` and private-repo reads need an authenticated token, and repo merge settings are only visible (and thus verifiable) to admin tokens — non-admin runs report them as unverifiable.
|
|
22
23
|
|
|
23
24
|
See the standards repository README for the ownership model and adoption workflow.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -755,6 +755,8 @@ const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
|
755
755
|
);
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
problems.push(...(await inspectPolicy(consumer)));
|
|
759
|
+
|
|
758
760
|
if (problems.length > 0) {
|
|
759
761
|
console.error(
|
|
760
762
|
`standards doctor: ${problems.length} integration problem(s):`,
|
|
@@ -915,6 +917,82 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
|
|
|
915
917
|
);
|
|
916
918
|
};
|
|
917
919
|
|
|
920
|
+
// Consumer-owned sync policy, checked in next to the canonical (read-only)
|
|
921
|
+
// standards-sync workflow it configures — versioned and reviewable, unlike
|
|
922
|
+
// repository Actions variables. All fields are optional; a missing file means
|
|
923
|
+
// the defaults (track main, weekly auto-sync on).
|
|
924
|
+
// autoSync false skips the scheduled workflow run; manual dispatch and
|
|
925
|
+
// local CLI runs are deliberate acts and always proceed.
|
|
926
|
+
// ref tag, branch, or full commit sha to sync from instead of main.
|
|
927
|
+
const POLICY_FILE = 'sync-standards.local.json';
|
|
928
|
+
const LINE_BREAK = /[\r\n]/u;
|
|
929
|
+
|
|
930
|
+
type Policy = {
|
|
931
|
+
readonly autoSync?: boolean;
|
|
932
|
+
readonly ref?: string;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const parsePolicy = (parsed: unknown): Policy => {
|
|
936
|
+
if (!isRecord(parsed)) {
|
|
937
|
+
throw new Error(`${POLICY_FILE} must be a JSON object`);
|
|
938
|
+
}
|
|
939
|
+
const unsupportedFields = Object.keys(parsed).filter(
|
|
940
|
+
(field) => field !== 'autoSync' && field !== 'ref',
|
|
941
|
+
);
|
|
942
|
+
if (unsupportedFields.length > 0) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
`${POLICY_FILE} contains unsupported field(s): ${unsupportedFields.join(', ')}`,
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
if (parsed.autoSync !== undefined && typeof parsed.autoSync !== 'boolean') {
|
|
948
|
+
throw new Error(`${POLICY_FILE} "autoSync" must be a boolean`);
|
|
949
|
+
}
|
|
950
|
+
if (
|
|
951
|
+
parsed.ref !== undefined &&
|
|
952
|
+
(!isNonEmptyString(parsed.ref) || LINE_BREAK.test(parsed.ref))
|
|
953
|
+
) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`${POLICY_FILE} "ref" must be a non-empty single-line string`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
return { autoSync: parsed.autoSync, ref: parsed.ref };
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const readPolicy = async (consumer: string): Promise<Policy> => {
|
|
962
|
+
const raw = await readTextIfPresent(join(consumer, POLICY_FILE));
|
|
963
|
+
if (raw === null) {
|
|
964
|
+
return {};
|
|
965
|
+
}
|
|
966
|
+
let parsed: unknown;
|
|
967
|
+
try {
|
|
968
|
+
parsed = JSON.parse(raw);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
throw new Error(`${POLICY_FILE} must contain valid JSON`, { cause: error });
|
|
971
|
+
}
|
|
972
|
+
return parsePolicy(parsed);
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const inspectPolicy = async (
|
|
976
|
+
consumer: string,
|
|
977
|
+
): Promise<ReadonlyArray<string>> => {
|
|
978
|
+
try {
|
|
979
|
+
await readPolicy(consumer);
|
|
980
|
+
return [];
|
|
981
|
+
} catch (error) {
|
|
982
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// Policy validation is unconditional once the file exists. Selection happens
|
|
987
|
+
// afterward: explicit refs win for remote sources, while local paths are used
|
|
988
|
+
// as-is and ignore only the already-validated policy ref.
|
|
989
|
+
const selectedRef = (
|
|
990
|
+
src: string,
|
|
991
|
+
explicitRef: string | undefined,
|
|
992
|
+
policy: Policy,
|
|
993
|
+
): string | undefined =>
|
|
994
|
+
existsSync(src) ? explicitRef : (explicitRef ?? policy.ref);
|
|
995
|
+
|
|
918
996
|
const runInitCommand = async (
|
|
919
997
|
consumer: string,
|
|
920
998
|
from: string | undefined,
|
|
@@ -930,7 +1008,9 @@ const runInitCommand = async (
|
|
|
930
1008
|
process.exitCode = 1;
|
|
931
1009
|
return;
|
|
932
1010
|
}
|
|
933
|
-
const
|
|
1011
|
+
const src = from ?? DEFAULT_UPSTREAM;
|
|
1012
|
+
const policy = await readPolicy(consumer);
|
|
1013
|
+
const source = resolveSource(src, selectedRef(src, ref, policy));
|
|
934
1014
|
try {
|
|
935
1015
|
const manifest = await loadManifest(
|
|
936
1016
|
join(source.dir, 'sync-standards.json'),
|
|
@@ -950,7 +1030,9 @@ const runSyncCommand = async (
|
|
|
950
1030
|
const consumerManifest = await loadManifest(
|
|
951
1031
|
join(consumer, 'sync-standards.json'),
|
|
952
1032
|
);
|
|
953
|
-
const
|
|
1033
|
+
const src = from ?? consumerManifest.upstream;
|
|
1034
|
+
const policy = await readPolicy(consumer);
|
|
1035
|
+
const source = resolveSource(src, selectedRef(src, ref, policy));
|
|
954
1036
|
try {
|
|
955
1037
|
const manifest = await loadManifest(
|
|
956
1038
|
join(source.dir, 'sync-standards.json'),
|