ras-stack 0.20.0 → 0.22.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 +17 -0
- package/dist/conformance/index.d.ts +25 -0
- package/dist/conformance/index.js +125 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/policy/cli.js +13 -2
- package/dist/policy/cli.js.map +1 -1
- package/dist/policy/fleet.d.ts +22 -0
- package/dist/policy/fleet.js +80 -0
- package/dist/policy/fleet.js.map +1 -0
- package/dist/policy/index.d.ts +9 -1
- package/dist/policy/index.js +35 -7
- package/dist/policy/index.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -170,6 +170,15 @@ if (target.provider === 'postgres') {
|
|
|
170
170
|
|
|
171
171
|
The package does not make SQLite and PostgreSQL queries look identical. Applications keep driver-specific transaction and compatibility behavior while sharing target validation, pool defaults, numeric parsing, migrations, credential-safe display URLs, and shutdown.
|
|
172
172
|
|
|
173
|
+
Consumer tests can verify the real provider selection and provider-specific safety settings without sharing a schema or repository:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { assertDatabaseTargetConformance, assertSqliteConformance } from 'ras-stack/conformance'
|
|
177
|
+
|
|
178
|
+
await assertDatabaseTargetConformance(databaseTarget)
|
|
179
|
+
await assertSqliteConformance((name) => sqliteClient.pragma(name, { simple: true }))
|
|
180
|
+
```
|
|
181
|
+
|
|
173
182
|
## Realtime updates
|
|
174
183
|
|
|
175
184
|
Applications choose their channel names, authorize subscriptions, and define payloads. `ras-stack` handles Centrifugo's HTTP publication, signed tokens, and repeated browser lifecycle mechanics:
|
|
@@ -350,6 +359,14 @@ pnpm exec ras-stack-policy check
|
|
|
350
359
|
|
|
351
360
|
`changesets` and `dependabot` produce deterministic complete files, with optional deep overrides. The pnpm policy changes only `minimumReleaseAge` in the existing `pnpm-workspace.yaml`, preserving local package layout, build approvals, dependency overrides, exclusions, and comments. Its default is seven days; set `"minimumReleaseAge": 0` only as an explicit repository exception. Commit both the selection and generated files so policy changes remain visible in review.
|
|
352
361
|
|
|
362
|
+
The same adoption policy can produce a read-only fleet report from public repository metadata. Declare each repository's supported versions and required shared config references in `ras-stack.fleet.json`, then run:
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
GITHUB_TOKEN="$(gh auth token)" pnpm exec ras-stack-policy fleet
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The command reads only `package.json`, root toolchain configs, and GitHub workflow metadata. It prints Markdown, exits unsuccessfully when drift exists, and never writes to a consumer repository. Omit an expectation when a repository intentionally does not share that surface. The included scheduled workflow writes the result to the Actions summary and retains it as an artifact.
|
|
369
|
+
|
|
353
370
|
## GitHub Actions
|
|
354
371
|
|
|
355
372
|
The JavaScript setup action reads the Node version from `engines.node` and the pnpm version from `packageManager` in the consuming repository:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ConformanceError extends Error {
|
|
2
|
+
readonly scenario: string;
|
|
3
|
+
constructor(scenario: string, message: string, options?: {
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export declare function assertMutationOriginConformance(guard: (request: Request) => void | Promise<void>, options?: {
|
|
8
|
+
trustForwardedHeaders?: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>;
|
|
11
|
+
export declare function assertHealthHandlerConformance(createHandler: HealthHandlerFactory): Promise<void>;
|
|
12
|
+
export type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown;
|
|
13
|
+
export declare function assertSqliteConformance(readPragma: SqlitePragmaReader): Promise<void>;
|
|
14
|
+
export type DatabaseTarget = {
|
|
15
|
+
provider: 'sqlite';
|
|
16
|
+
file: string;
|
|
17
|
+
} | {
|
|
18
|
+
provider: 'postgres';
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
export type DatabaseTargetResolver = (options: {
|
|
22
|
+
databaseUrl?: string;
|
|
23
|
+
sqliteFile: string;
|
|
24
|
+
}) => DatabaseTarget;
|
|
25
|
+
export declare function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export class ConformanceError extends Error {
|
|
2
|
+
scenario;
|
|
3
|
+
constructor(scenario, message, options = {}) {
|
|
4
|
+
super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause });
|
|
5
|
+
this.scenario = scenario;
|
|
6
|
+
this.name = 'ConformanceError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function assertMutationOriginConformance(guard, options = {}) {
|
|
10
|
+
await accepted('same-origin request', () => guard(new Request('https://app.example/action', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },
|
|
13
|
+
})));
|
|
14
|
+
await rejected('cross-origin request', () => guard(new Request('https://app.example/action', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },
|
|
17
|
+
})));
|
|
18
|
+
await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })));
|
|
19
|
+
if (!options.trustForwardedHeaders) {
|
|
20
|
+
await rejected('spoofed forwarded-origin request', () => guard(new Request('https://app.example/action', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
origin: 'https://attacker.example',
|
|
24
|
+
'x-forwarded-host': 'attacker.example',
|
|
25
|
+
'x-forwarded-proto': 'https',
|
|
26
|
+
},
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function assertHealthHandlerConformance(createHandler) {
|
|
31
|
+
const healthy = await createHandler(() => undefined)();
|
|
32
|
+
if (healthy.status !== 200)
|
|
33
|
+
throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`);
|
|
34
|
+
const healthyBody = await responseBody(healthy, 'healthy dependency');
|
|
35
|
+
if (healthyBody.ok !== true)
|
|
36
|
+
throw new ConformanceError('healthy dependency', 'response body must contain ok: true');
|
|
37
|
+
const privateMessage = 'password=private-diagnostic';
|
|
38
|
+
const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))();
|
|
39
|
+
if (unavailable.status !== 503) {
|
|
40
|
+
throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`);
|
|
41
|
+
}
|
|
42
|
+
const unavailableText = await unavailable.text();
|
|
43
|
+
if (unavailableText.includes(privateMessage)) {
|
|
44
|
+
throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message');
|
|
45
|
+
}
|
|
46
|
+
let unavailableBody;
|
|
47
|
+
try {
|
|
48
|
+
unavailableBody = JSON.parse(unavailableText);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error });
|
|
52
|
+
}
|
|
53
|
+
if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {
|
|
54
|
+
throw new ConformanceError('unavailable dependency', 'response body must contain ok: false');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function assertSqliteConformance(readPragma) {
|
|
58
|
+
const values = {
|
|
59
|
+
journalMode: String(await readPragma('journal_mode')).toLowerCase(),
|
|
60
|
+
synchronous: Number(await readPragma('synchronous')),
|
|
61
|
+
busyTimeout: Number(await readPragma('busy_timeout')),
|
|
62
|
+
foreignKeys: Number(await readPragma('foreign_keys')),
|
|
63
|
+
};
|
|
64
|
+
if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {
|
|
65
|
+
throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`);
|
|
66
|
+
}
|
|
67
|
+
if (values.synchronous !== 2) {
|
|
68
|
+
throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`);
|
|
69
|
+
}
|
|
70
|
+
if (values.busyTimeout !== 5000) {
|
|
71
|
+
throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`);
|
|
72
|
+
}
|
|
73
|
+
if (values.foreignKeys !== 1) {
|
|
74
|
+
throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function assertDatabaseTargetConformance(resolve) {
|
|
78
|
+
const sqliteFile = '/data/application.sqlite';
|
|
79
|
+
const sqlite = resolve({ sqliteFile });
|
|
80
|
+
if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {
|
|
81
|
+
throw new ConformanceError('default database target', 'expected the configured SQLite file');
|
|
82
|
+
}
|
|
83
|
+
for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {
|
|
84
|
+
const target = resolve({ databaseUrl: url, sqliteFile });
|
|
85
|
+
if (target.provider !== 'postgres' || target.url !== url) {
|
|
86
|
+
throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
resolve({ databaseUrl: 'https://database.example/application', sqliteFile });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected');
|
|
96
|
+
}
|
|
97
|
+
async function accepted(scenario, work) {
|
|
98
|
+
try {
|
|
99
|
+
await work();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function rejected(scenario, work) {
|
|
106
|
+
try {
|
|
107
|
+
await work();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw new ConformanceError(scenario, 'expected request to be rejected');
|
|
113
|
+
}
|
|
114
|
+
async function responseBody(response, scenario) {
|
|
115
|
+
try {
|
|
116
|
+
const body = await response.json();
|
|
117
|
+
if (body && typeof body === 'object')
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new ConformanceError(scenario, 'response body must be JSON', { cause: error });
|
|
122
|
+
}
|
|
123
|
+
throw new ConformanceError(scenario, 'response body must be an object');
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"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","sourcesContent":["export class ConformanceError extends Error {\n constructor(\n readonly scenario: string,\n message: string,\n options: { cause?: unknown } = {},\n ) {\n super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'ConformanceError'\n }\n}\n\nexport async function assertMutationOriginConformance(\n guard: (request: Request) => void | Promise<void>,\n options: { trustForwardedHeaders?: boolean } = {},\n) {\n await accepted('same-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },\n }),\n ),\n )\n await rejected('cross-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },\n }),\n ),\n )\n await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })))\n if (!options.trustForwardedHeaders) {\n await rejected('spoofed forwarded-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: {\n origin: 'https://attacker.example',\n 'x-forwarded-host': 'attacker.example',\n 'x-forwarded-proto': 'https',\n },\n }),\n ),\n )\n }\n}\n\nexport type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>\n\nexport async function assertHealthHandlerConformance(createHandler: HealthHandlerFactory) {\n const healthy = await createHandler(() => undefined)()\n if (healthy.status !== 200) throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`)\n const healthyBody = await responseBody(healthy, 'healthy dependency')\n if (healthyBody.ok !== true) throw new ConformanceError('healthy dependency', 'response body must contain ok: true')\n\n const privateMessage = 'password=private-diagnostic'\n const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))()\n if (unavailable.status !== 503) {\n throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`)\n }\n const unavailableText = await unavailable.text()\n if (unavailableText.includes(privateMessage)) {\n throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message')\n }\n let unavailableBody: unknown\n try {\n unavailableBody = JSON.parse(unavailableText)\n } catch (error) {\n throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error })\n }\n if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {\n throw new ConformanceError('unavailable dependency', 'response body must contain ok: false')\n }\n}\n\nexport type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown\n\nexport async function assertSqliteConformance(readPragma: SqlitePragmaReader) {\n const values = {\n journalMode: String(await readPragma('journal_mode')).toLowerCase(),\n synchronous: Number(await readPragma('synchronous')),\n busyTimeout: Number(await readPragma('busy_timeout')),\n foreignKeys: Number(await readPragma('foreign_keys')),\n }\n if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {\n throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`)\n }\n if (values.synchronous !== 2) {\n throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`)\n }\n if (values.busyTimeout !== 5000) {\n throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`)\n }\n if (values.foreignKeys !== 1) {\n throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`)\n }\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport type DatabaseTargetResolver = (options: { databaseUrl?: string; sqliteFile: string }) => DatabaseTarget\n\nexport function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver) {\n const sqliteFile = '/data/application.sqlite'\n const sqlite = resolve({ sqliteFile })\n if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {\n throw new ConformanceError('default database target', 'expected the configured SQLite file')\n }\n\n for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {\n const target = resolve({ databaseUrl: url, sqliteFile })\n if (target.provider !== 'postgres' || target.url !== url) {\n throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`)\n }\n }\n\n try {\n resolve({ databaseUrl: 'https://database.example/application', sqliteFile })\n } catch {\n return\n }\n throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected')\n}\n\nasync function accepted(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch (error) {\n throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error })\n }\n}\n\nasync function rejected(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch {\n return\n }\n throw new ConformanceError(scenario, 'expected request to be rejected')\n}\n\nasync function responseBody(response: Response, scenario: string): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await response.json()\n if (body && typeof body === 'object') return body as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError(scenario, 'response body must be JSON', { cause: error })\n }\n throw new ConformanceError(scenario, 'response body must be an object')\n}\n"]}
|
package/dist/policy/cli.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fleetConfig, fleetMarkdown, inspectFleet } from './fleet.js';
|
|
2
5
|
import { checkRepositoryPolicy, syncRepositoryPolicy } from './index.js';
|
|
3
6
|
const command = process.argv[2];
|
|
4
|
-
if (command !== 'check' && command !== 'sync') {
|
|
5
|
-
console.error('usage: ras-stack-policy <check|sync>');
|
|
7
|
+
if (command !== 'check' && command !== 'sync' && command !== 'fleet') {
|
|
8
|
+
console.error('usage: ras-stack-policy <check|sync|fleet>');
|
|
6
9
|
process.exitCode = 2;
|
|
7
10
|
}
|
|
11
|
+
else if (command === 'fleet') {
|
|
12
|
+
const path = resolve(process.argv[3] ?? 'ras-stack.fleet.json');
|
|
13
|
+
const config = fleetConfig(JSON.parse(await readFile(path, 'utf8')));
|
|
14
|
+
const results = await inspectFleet(config);
|
|
15
|
+
process.stdout.write(fleetMarkdown(results));
|
|
16
|
+
if (results.some((result) => result.drift.length > 0))
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
8
19
|
else {
|
|
9
20
|
const changed = command === 'check' ? await checkRepositoryPolicy(process.cwd()) : await syncRepositoryPolicy(process.cwd(), 'write');
|
|
10
21
|
if (command === 'check' && changed.length > 0) {
|
package/dist/policy/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/policy/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAExE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC/B,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/policy/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAExE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC/B,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;IACrE,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC3D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC;KAAM,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,sBAAsB,CAAC,CAAA;IAC/D,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IACpE,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;IAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IAC5C,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AAC7E,CAAC;KAAM,CAAC;IACN,MAAM,OAAO,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,qBAAqB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,oBAAoB,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CAAA;IACrI,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,KAAK,MAAM,OAAO,IAAI,OAAO;YAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrD,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAA;QAChE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;IACtB,CAAC;SAAM,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;IAC5D,CAAC;AACH,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises'\nimport { resolve } from 'node:path'\nimport { fleetConfig, fleetMarkdown, inspectFleet } from './fleet.js'\nimport { checkRepositoryPolicy, syncRepositoryPolicy } from './index.js'\n\nconst command = process.argv[2]\nif (command !== 'check' && command !== 'sync' && command !== 'fleet') {\n console.error('usage: ras-stack-policy <check|sync|fleet>')\n process.exitCode = 2\n} else if (command === 'fleet') {\n const path = resolve(process.argv[3] ?? 'ras-stack.fleet.json')\n const config = fleetConfig(JSON.parse(await readFile(path, 'utf8')))\n const results = await inspectFleet(config)\n process.stdout.write(fleetMarkdown(results))\n if (results.some((result) => result.drift.length > 0)) process.exitCode = 1\n} else {\n const changed = command === 'check' ? await checkRepositoryPolicy(process.cwd()) : await syncRepositoryPolicy(process.cwd(), 'write')\n if (command === 'check' && changed.length > 0) {\n for (const message of changed) console.error(message)\n console.error('run ras-stack-policy sync and commit the result')\n process.exitCode = 1\n } else if (command === 'sync') {\n for (const path of changed) console.log(`updated ${path}`)\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AdoptionPolicy, AdoptionSnapshot } from './index.js';
|
|
2
|
+
export type FleetRepository = {
|
|
3
|
+
repository: string;
|
|
4
|
+
ref?: string;
|
|
5
|
+
adoption: AdoptionPolicy;
|
|
6
|
+
};
|
|
7
|
+
export type FleetConfig = {
|
|
8
|
+
repositories: FleetRepository[];
|
|
9
|
+
};
|
|
10
|
+
export type FleetResult = {
|
|
11
|
+
repository: string;
|
|
12
|
+
ref: string;
|
|
13
|
+
drift: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function fleetConfig(source: unknown): FleetConfig;
|
|
16
|
+
export declare function inspectFleet(config: FleetConfig, load?: (repository: string, ref: string) => Promise<AdoptionSnapshot>): Promise<{
|
|
17
|
+
repository: string;
|
|
18
|
+
ref: string;
|
|
19
|
+
drift: string[];
|
|
20
|
+
}[]>;
|
|
21
|
+
export declare function fleetMarkdown(results: FleetResult[]): string;
|
|
22
|
+
export declare function loadGitHubAdoptionSnapshot(repository: string, ref: string): Promise<AdoptionSnapshot>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { adoptionSnapshotDrift } from './index.js';
|
|
2
|
+
export function fleetConfig(source) {
|
|
3
|
+
if (!source || typeof source !== 'object' || !('repositories' in source) || !Array.isArray(source.repositories)) {
|
|
4
|
+
throw new Error('fleet configuration must contain a repositories array');
|
|
5
|
+
}
|
|
6
|
+
if (source.repositories.length === 0 || source.repositories.length > 100) {
|
|
7
|
+
throw new Error('fleet configuration must contain between 1 and 100 repositories');
|
|
8
|
+
}
|
|
9
|
+
for (const entry of source.repositories) {
|
|
10
|
+
if (!entry || typeof entry !== 'object')
|
|
11
|
+
throw new Error('each fleet repository must be an object');
|
|
12
|
+
if (!('repository' in entry) || typeof entry.repository !== 'string' || !/^[a-z\d][\w.-]*\/[a-z\d][\w.-]*$/i.test(entry.repository)) {
|
|
13
|
+
throw new Error('each fleet repository must use an owner/name identifier');
|
|
14
|
+
}
|
|
15
|
+
if ('ref' in entry && entry.ref !== undefined && typeof entry.ref !== 'string') {
|
|
16
|
+
throw new Error('fleet repository ref must be a string');
|
|
17
|
+
}
|
|
18
|
+
if (!('adoption' in entry) || !entry.adoption || typeof entry.adoption !== 'object') {
|
|
19
|
+
throw new Error('each fleet repository must declare an adoption policy');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return source;
|
|
23
|
+
}
|
|
24
|
+
export async function inspectFleet(config, load = loadGitHubAdoptionSnapshot) {
|
|
25
|
+
return Promise.all(config.repositories.map(async ({ repository, ref = 'main', adoption }) => ({
|
|
26
|
+
repository,
|
|
27
|
+
ref,
|
|
28
|
+
drift: adoptionSnapshotDrift(await load(repository, ref), adoption),
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
export function fleetMarkdown(results) {
|
|
32
|
+
const healthy = results.filter((result) => result.drift.length === 0).length;
|
|
33
|
+
const lines = [`# ras-stack fleet health`, '', `${healthy}/${results.length} repositories conform to their declared adoption policy.`, ''];
|
|
34
|
+
for (const result of results) {
|
|
35
|
+
lines.push(`## ${result.drift.length === 0 ? '✅' : '❌'} ${result.repository}@${result.ref}`, '');
|
|
36
|
+
if (result.drift.length === 0)
|
|
37
|
+
lines.push('No drift detected.', '');
|
|
38
|
+
else
|
|
39
|
+
for (const message of result.drift)
|
|
40
|
+
lines.push(`- ${message}`);
|
|
41
|
+
if (result.drift.length > 0)
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
45
|
+
}
|
|
46
|
+
export async function loadGitHubAdoptionSnapshot(repository, ref) {
|
|
47
|
+
const headers = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
|
|
48
|
+
const token = process.env.GITHUB_TOKEN;
|
|
49
|
+
if (token)
|
|
50
|
+
headers.Authorization = `Bearer ${token}`;
|
|
51
|
+
const tree = await githubJson(`https://api.github.com/repos/${repository}/git/trees/${encodeURIComponent(ref)}?recursive=1`, headers);
|
|
52
|
+
if (tree.truncated)
|
|
53
|
+
throw new Error(`${repository}@${ref}: GitHub returned a truncated tree`);
|
|
54
|
+
const selected = tree.tree.filter((entry) => entry.type === 'blob' && entry.sha && selectedMetadataPath(entry.path));
|
|
55
|
+
const sources = await Promise.all(selected.map(async (entry) => {
|
|
56
|
+
const blob = await githubJson(`https://api.github.com/repos/${repository}/git/blobs/${entry.sha}`, headers);
|
|
57
|
+
if (blob.encoding !== 'base64')
|
|
58
|
+
throw new Error(`${repository}@${ref}:${entry.path}: unsupported GitHub blob encoding`);
|
|
59
|
+
return [entry.path, Buffer.from(blob.content.replaceAll('\n', ''), 'base64').toString('utf8')];
|
|
60
|
+
}));
|
|
61
|
+
const files = new Map(sources);
|
|
62
|
+
const manifestSource = files.get('package.json');
|
|
63
|
+
if (!manifestSource)
|
|
64
|
+
throw new Error(`${repository}@${ref}: package.json was not found`);
|
|
65
|
+
return { manifest: JSON.parse(manifestSource), files };
|
|
66
|
+
}
|
|
67
|
+
function selectedMetadataPath(path) {
|
|
68
|
+
return (path === 'package.json' ||
|
|
69
|
+
path === 'ras-stack.policy.json' ||
|
|
70
|
+
path === 'oxlint.json' ||
|
|
71
|
+
/^tsconfig(?:\.[^.]+)*\.json$/.test(path) ||
|
|
72
|
+
/^\.github\/.*\.ya?ml$/.test(path));
|
|
73
|
+
}
|
|
74
|
+
async function githubJson(url, headers) {
|
|
75
|
+
const response = await fetch(url, { headers });
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
throw new Error(`GitHub request failed with ${response.status}: ${url}`);
|
|
78
|
+
return (await response.json());
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=fleet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../src/policy/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AdoptionPolicy, AdoptionSnapshot } from './index.js'\nimport { adoptionSnapshotDrift } from './index.js'\n\nexport type FleetRepository = {\n repository: string\n ref?: string\n adoption: AdoptionPolicy\n}\n\nexport type FleetConfig = {\n repositories: FleetRepository[]\n}\n\nexport type FleetResult = {\n repository: string\n ref: string\n drift: string[]\n}\n\nexport function fleetConfig(source: unknown): FleetConfig {\n if (!source || typeof source !== 'object' || !('repositories' in source) || !Array.isArray(source.repositories)) {\n throw new Error('fleet configuration must contain a repositories array')\n }\n if (source.repositories.length === 0 || source.repositories.length > 100) {\n throw new Error('fleet configuration must contain between 1 and 100 repositories')\n }\n for (const entry of source.repositories) {\n if (!entry || typeof entry !== 'object') throw new Error('each fleet repository must be an object')\n if (!('repository' in entry) || typeof entry.repository !== 'string' || !/^[a-z\\d][\\w.-]*\\/[a-z\\d][\\w.-]*$/i.test(entry.repository)) {\n throw new Error('each fleet repository must use an owner/name identifier')\n }\n if ('ref' in entry && entry.ref !== undefined && typeof entry.ref !== 'string') {\n throw new Error('fleet repository ref must be a string')\n }\n if (!('adoption' in entry) || !entry.adoption || typeof entry.adoption !== 'object') {\n throw new Error('each fleet repository must declare an adoption policy')\n }\n }\n return source as FleetConfig\n}\n\nexport async function inspectFleet(\n config: FleetConfig,\n load: (repository: string, ref: string) => Promise<AdoptionSnapshot> = loadGitHubAdoptionSnapshot,\n) {\n return Promise.all(\n config.repositories.map(async ({ repository, ref = 'main', adoption }) => ({\n repository,\n ref,\n drift: adoptionSnapshotDrift(await load(repository, ref), adoption),\n })),\n )\n}\n\nexport function fleetMarkdown(results: FleetResult[]) {\n const healthy = results.filter((result) => result.drift.length === 0).length\n const lines = [`# ras-stack fleet health`, '', `${healthy}/${results.length} repositories conform to their declared adoption policy.`, '']\n for (const result of results) {\n lines.push(`## ${result.drift.length === 0 ? '✅' : '❌'} ${result.repository}@${result.ref}`, '')\n if (result.drift.length === 0) lines.push('No drift detected.', '')\n else for (const message of result.drift) lines.push(`- ${message}`)\n if (result.drift.length > 0) lines.push('')\n }\n return `${lines.join('\\n').trimEnd()}\\n`\n}\n\nexport async function loadGitHubAdoptionSnapshot(repository: string, ref: string): Promise<AdoptionSnapshot> {\n const headers: Record<string, string> = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' }\n const token = process.env.GITHUB_TOKEN\n if (token) headers.Authorization = `Bearer ${token}`\n const tree = await githubJson<GitTree>(\n `https://api.github.com/repos/${repository}/git/trees/${encodeURIComponent(ref)}?recursive=1`,\n headers,\n )\n if (tree.truncated) throw new Error(`${repository}@${ref}: GitHub returned a truncated tree`)\n const selected = tree.tree.filter((entry) => entry.type === 'blob' && entry.sha && selectedMetadataPath(entry.path))\n const sources = await Promise.all(\n selected.map(async (entry) => {\n const blob = await githubJson<GitBlob>(`https://api.github.com/repos/${repository}/git/blobs/${entry.sha}`, headers)\n if (blob.encoding !== 'base64') throw new Error(`${repository}@${ref}:${entry.path}: unsupported GitHub blob encoding`)\n return [entry.path, Buffer.from(blob.content.replaceAll('\\n', ''), 'base64').toString('utf8')] as const\n }),\n )\n const files = new Map(sources)\n const manifestSource = files.get('package.json')\n if (!manifestSource) throw new Error(`${repository}@${ref}: package.json was not found`)\n return { manifest: JSON.parse(manifestSource) as Record<string, unknown>, files }\n}\n\nfunction selectedMetadataPath(path: string) {\n return (\n path === 'package.json' ||\n path === 'ras-stack.policy.json' ||\n path === 'oxlint.json' ||\n /^tsconfig(?:\\.[^.]+)*\\.json$/.test(path) ||\n /^\\.github\\/.*\\.ya?ml$/.test(path)\n )\n}\n\nasync function githubJson<T>(url: string, headers: Record<string, string>): Promise<T> {\n const response = await fetch(url, { headers })\n if (!response.ok) throw new Error(`GitHub request failed with ${response.status}: ${url}`)\n return (await response.json()) as T\n}\n\ntype GitTree = { truncated: boolean; tree: { path: string; type: string; sha?: string }[] }\ntype GitBlob = { encoding: string; content: string }\n"]}
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { fleetConfig, fleetMarkdown, inspectFleet, loadGitHubAdoptionSnapshot } from './fleet.js';
|
|
2
|
+
export type { FleetConfig, FleetRepository, FleetResult } from './fleet.js';
|
|
1
3
|
type PolicySelection = boolean | {
|
|
2
4
|
overrides?: Record<string, unknown>;
|
|
3
5
|
};
|
|
@@ -11,12 +13,18 @@ export type RepositoryPolicy = {
|
|
|
11
13
|
};
|
|
12
14
|
export type AdoptionPolicy = {
|
|
13
15
|
minimumRasStackVersion?: string;
|
|
16
|
+
minimumWorkflowRasStackVersion?: string;
|
|
14
17
|
node?: string;
|
|
15
18
|
pnpm?: string;
|
|
16
19
|
just?: string;
|
|
20
|
+
requiredReferences?: string[];
|
|
21
|
+
};
|
|
22
|
+
export type AdoptionSnapshot = {
|
|
23
|
+
manifest: Record<string, unknown>;
|
|
24
|
+
files: Map<string, string>;
|
|
17
25
|
};
|
|
18
26
|
export declare function syncRepositoryPolicy(root: string, mode: 'check' | 'write'): Promise<string[]>;
|
|
19
27
|
export declare function checkRepositoryPolicy(root: string): Promise<string[]>;
|
|
20
28
|
export declare function adoptionDrift(root: string, policy: AdoptionPolicy): Promise<string[]>;
|
|
29
|
+
export declare function adoptionSnapshotDrift(snapshot: AdoptionSnapshot, policy: AdoptionPolicy): string[];
|
|
21
30
|
export declare function renderedPolicyFiles(root: string, config: RepositoryPolicy): Promise<Map<string, string>>;
|
|
22
|
-
export {};
|
package/dist/policy/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { parseDocument, stringify } from 'yaml';
|
|
4
|
+
export { fleetConfig, fleetMarkdown, inspectFleet, loadGitHubAdoptionSnapshot } from './fleet.js';
|
|
4
5
|
const changesetsPolicy = {
|
|
5
6
|
$schema: 'https://unpkg.com/@changesets/config@3.1.2/schema.json',
|
|
6
7
|
changelog: '@changesets/cli/changelog',
|
|
@@ -52,8 +53,13 @@ export async function checkRepositoryPolicy(root) {
|
|
|
52
53
|
return [...files.map((path) => `policy drift: ${path}`), ...(config.adoption ? await adoptionDrift(root, config.adoption) : [])];
|
|
53
54
|
}
|
|
54
55
|
export async function adoptionDrift(root, policy) {
|
|
55
|
-
const drift = [];
|
|
56
56
|
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
57
|
+
const files = await adoptionFiles(root);
|
|
58
|
+
return adoptionSnapshotDrift({ manifest, files }, policy);
|
|
59
|
+
}
|
|
60
|
+
export function adoptionSnapshotDrift(snapshot, policy) {
|
|
61
|
+
const drift = [];
|
|
62
|
+
const { manifest, files } = snapshot;
|
|
57
63
|
if (policy.node && manifest.engines && plainObject(manifest.engines) && manifest.engines.node !== policy.node) {
|
|
58
64
|
drift.push(`toolchain drift: package.json engines.node must be ${policy.node}`);
|
|
59
65
|
}
|
|
@@ -66,13 +72,14 @@ export async function adoptionDrift(root, policy) {
|
|
|
66
72
|
drift.push(`ras-stack drift: package.json uses ${dependency}, minimum is ${policy.minimumRasStackVersion}`);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
const workflows =
|
|
75
|
+
const workflows = new Map([...files].filter(([path]) => path.startsWith('.github/') && /\.ya?ml$/.test(path)));
|
|
76
|
+
const workflowMinimum = policy.minimumWorkflowRasStackVersion ?? policy.minimumRasStackVersion;
|
|
70
77
|
for (const [path, source] of workflows) {
|
|
71
|
-
if (
|
|
78
|
+
if (workflowMinimum) {
|
|
72
79
|
for (const match of source.matchAll(/richardsolomou\/ras-stack\/[^\s'"}]+@v(\d+\.\d+\.\d+)/g)) {
|
|
73
80
|
const version = match[1];
|
|
74
|
-
if (version && compareVersions(version,
|
|
75
|
-
drift.push(`ras-stack drift: ${path} uses v${version}, minimum is v${
|
|
81
|
+
if (version && compareVersions(version, workflowMinimum) < 0) {
|
|
82
|
+
drift.push(`ras-stack drift: ${path} uses v${version}, minimum is v${workflowMinimum}`);
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
@@ -81,7 +88,12 @@ export async function adoptionDrift(root, policy) {
|
|
|
81
88
|
![...workflows.values()].some((source) => source.includes(`just-version: '${policy.just}'`) || source.includes(`just-version: ${policy.just}`))) {
|
|
82
89
|
drift.push(`toolchain drift: no workflow declares just-version ${policy.just}`);
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
for (const reference of policy.requiredReferences ?? []) {
|
|
92
|
+
if (![...files.values()].some((source) => source.includes(reference))) {
|
|
93
|
+
drift.push(`shared config drift: no configuration references ${reference}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...new Set(drift)];
|
|
85
97
|
}
|
|
86
98
|
export async function renderedPolicyFiles(root, config) {
|
|
87
99
|
const files = new Map();
|
|
@@ -118,10 +130,13 @@ function repositoryPolicy(source) {
|
|
|
118
130
|
if (adoption !== undefined && adoption !== false) {
|
|
119
131
|
if (!plainObject(adoption))
|
|
120
132
|
throw new Error('adoption policy must be false or an object');
|
|
121
|
-
for (const key of ['minimumRasStackVersion', 'node', 'pnpm', 'just']) {
|
|
133
|
+
for (const key of ['minimumRasStackVersion', 'minimumWorkflowRasStackVersion', 'node', 'pnpm', 'just']) {
|
|
122
134
|
if (adoption[key] !== undefined && typeof adoption[key] !== 'string')
|
|
123
135
|
throw new Error(`adoption.${key} must be a string`);
|
|
124
136
|
}
|
|
137
|
+
if (adoption.requiredReferences !== undefined && !stringArray(adoption.requiredReferences)) {
|
|
138
|
+
throw new Error('adoption.requiredReferences must be an array of strings');
|
|
139
|
+
}
|
|
125
140
|
}
|
|
126
141
|
return value;
|
|
127
142
|
}
|
|
@@ -160,6 +175,16 @@ async function textFiles(directory, root = directory) {
|
|
|
160
175
|
}
|
|
161
176
|
return files;
|
|
162
177
|
}
|
|
178
|
+
async function adoptionFiles(root) {
|
|
179
|
+
const files = await textFiles(join(root, '.github'), root);
|
|
180
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
181
|
+
await Promise.all(entries.map(async (entry) => {
|
|
182
|
+
if (!entry.isFile() || !/^(?:ras-stack\.policy|oxlint|tsconfig(?:\.[^.]+)*)\.json$/.test(entry.name))
|
|
183
|
+
return;
|
|
184
|
+
files.set(entry.name, await readFile(join(root, entry.name), 'utf8'));
|
|
185
|
+
}));
|
|
186
|
+
return files;
|
|
187
|
+
}
|
|
163
188
|
function validateSelection(name, value) {
|
|
164
189
|
if (value === undefined || typeof value === 'boolean')
|
|
165
190
|
return;
|
|
@@ -180,4 +205,7 @@ function deepMerge(base, overrides) {
|
|
|
180
205
|
function plainObject(value) {
|
|
181
206
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
182
207
|
}
|
|
208
|
+
function stringArray(value) {
|
|
209
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
210
|
+
}
|
|
183
211
|
//# sourceMappingURL=index.js.map
|
package/dist/policy/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/policy/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { parseDocument, stringify } from 'yaml'\n\nconst changesetsPolicy = {\n $schema: 'https://unpkg.com/@changesets/config@3.1.2/schema.json',\n changelog: '@changesets/cli/changelog',\n commit: false,\n fixed: [],\n linked: [],\n access: 'public',\n baseBranch: 'main',\n updateInternalDependencies: 'patch',\n ignore: [],\n}\n\nconst dependabotPolicy = {\n version: 2,\n updates: [\n {\n 'package-ecosystem': 'npm',\n directory: '/',\n schedule: { interval: 'weekly' },\n cooldown: { 'default-days': 7 },\n groups: { 'javascript-dependencies': { patterns: ['*'] } },\n },\n {\n 'package-ecosystem': 'github-actions',\n directory: '/',\n schedule: { interval: 'weekly' },\n cooldown: { 'default-days': 7 },\n },\n ],\n}\n\ntype PolicySelection = boolean | { overrides?: Record<string, unknown> }\n\nexport type RepositoryPolicy = {\n changesets?: PolicySelection\n dependabot?: PolicySelection\n pnpm?: false | { minimumReleaseAge?: number }\n adoption?: false | AdoptionPolicy\n}\n\nexport type AdoptionPolicy = {\n minimumRasStackVersion?: string\n node?: string\n pnpm?: string\n just?: string\n}\n\nexport async function syncRepositoryPolicy(root: string, mode: 'check' | 'write') {\n const config = repositoryPolicy(await readFile(join(root, 'ras-stack.policy.json'), 'utf8'))\n const files = await renderedPolicyFiles(root, config)\n const compared = await Promise.all(\n [...files].map(async ([path, expected]) => {\n const absolute = join(root, path)\n const actual = await readFile(absolute, 'utf8').catch(() => undefined)\n if (actual === expected) return undefined\n if (mode === 'write') {\n await mkdir(dirname(absolute), { recursive: true })\n await writeFile(absolute, expected)\n }\n return path\n }),\n )\n return compared.filter((path) => path !== undefined)\n}\n\nexport async function checkRepositoryPolicy(root: string) {\n const files = await syncRepositoryPolicy(root, 'check')\n const config = repositoryPolicy(await readFile(join(root, 'ras-stack.policy.json'), 'utf8'))\n return [...files.map((path) => `policy drift: ${path}`), ...(config.adoption ? await adoptionDrift(root, config.adoption) : [])]\n}\n\nexport async function adoptionDrift(root: string, policy: AdoptionPolicy) {\n const drift: string[] = []\n const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as Record<string, unknown>\n if (policy.node && manifest.engines && plainObject(manifest.engines) && manifest.engines.node !== policy.node) {\n drift.push(`toolchain drift: package.json engines.node must be ${policy.node}`)\n }\n if (policy.pnpm && manifest.packageManager !== `pnpm@${policy.pnpm}`) {\n drift.push(`toolchain drift: package.json packageManager must be pnpm@${policy.pnpm}`)\n }\n if (policy.minimumRasStackVersion) {\n const dependency = dependencyVersion(manifest, 'ras-stack')\n if (dependency && compareVersions(dependency, policy.minimumRasStackVersion) < 0) {\n drift.push(`ras-stack drift: package.json uses ${dependency}, minimum is ${policy.minimumRasStackVersion}`)\n }\n }\n\n const workflows = await textFiles(join(root, '.github'))\n for (const [path, source] of workflows) {\n if (policy.minimumRasStackVersion) {\n for (const match of source.matchAll(/richardsolomou\\/ras-stack\\/[^\\s'\"}]+@v(\\d+\\.\\d+\\.\\d+)/g)) {\n const version = match[1]\n if (version && compareVersions(version, policy.minimumRasStackVersion) < 0) {\n drift.push(`ras-stack drift: ${path} uses v${version}, minimum is v${policy.minimumRasStackVersion}`)\n }\n }\n }\n }\n if (\n policy.just &&\n ![...workflows.values()].some(\n (source) => source.includes(`just-version: '${policy.just}'`) || source.includes(`just-version: ${policy.just}`),\n )\n ) {\n drift.push(`toolchain drift: no workflow declares just-version ${policy.just}`)\n }\n return drift\n}\n\nexport async function renderedPolicyFiles(root: string, config: RepositoryPolicy) {\n const files = new Map<string, string>()\n if (config.changesets) {\n files.set('.changeset/config.json', `${JSON.stringify(selectedPolicy(changesetsPolicy, config.changesets), null, 2)}\\n`)\n }\n if (config.dependabot) {\n files.set('.github/dependabot.yml', stringify(selectedPolicy(dependabotPolicy, config.dependabot), { lineWidth: 0, singleQuote: true }))\n }\n if (config.pnpm) {\n const path = join(root, 'pnpm-workspace.yaml')\n const document = parseDocument(await readFile(path, 'utf8').catch(() => ''))\n document.set('minimumReleaseAge', config.pnpm.minimumReleaseAge ?? 10_080)\n files.set('pnpm-workspace.yaml', document.toString({ lineWidth: 0 }))\n }\n return files\n}\n\nfunction repositoryPolicy(source: string): RepositoryPolicy {\n const value = JSON.parse(source) as unknown\n if (!plainObject(value)) throw new Error('ras-stack.policy.json must contain an object')\n for (const name of ['changesets', 'dependabot'] as const) validateSelection(name, value[name])\n const pnpm = value.pnpm\n if (pnpm !== undefined && pnpm !== false) {\n if (!plainObject(pnpm)) throw new Error('pnpm policy must be false or an object')\n const age = pnpm.minimumReleaseAge\n if (age !== undefined && (typeof age !== 'number' || !Number.isInteger(age) || age < 0)) {\n throw new Error('pnpm.minimumReleaseAge must be a non-negative integer')\n }\n }\n const adoption = value.adoption\n if (adoption !== undefined && adoption !== false) {\n if (!plainObject(adoption)) throw new Error('adoption policy must be false or an object')\n for (const key of ['minimumRasStackVersion', 'node', 'pnpm', 'just']) {\n if (adoption[key] !== undefined && typeof adoption[key] !== 'string') throw new Error(`adoption.${key} must be a string`)\n }\n }\n return value\n}\n\nfunction dependencyVersion(manifest: Record<string, unknown>, name: string) {\n for (const field of ['dependencies', 'devDependencies']) {\n const dependencies = manifest[field]\n const value = plainObject(dependencies) ? dependencies[name] : undefined\n if (typeof value === 'string') return /\\d+\\.\\d+\\.\\d+/.exec(value)?.[0]\n }\n return undefined\n}\n\nfunction compareVersions(left: string, right: string) {\n const a = left.split('.').map(Number)\n const b = right.split('.').map(Number)\n for (let index = 0; index < 3; index += 1) {\n if (a[index] !== b[index]) return (a[index] ?? 0) - (b[index] ?? 0)\n }\n return 0\n}\n\nasync function textFiles(directory: string, root = directory): Promise<Map<string, string>> {\n const files = new Map<string, string>()\n const entries = await readdir(directory, { withFileTypes: true }).catch(() => [])\n const results = await Promise.all(\n entries.map(async (entry) => {\n const path = join(directory, entry.name)\n if (entry.isDirectory()) return textFiles(path, root)\n if (/\\.ya?ml$/.test(entry.name)) return new Map([[path.slice(root.length + 1), await readFile(path, 'utf8')]])\n return new Map<string, string>()\n }),\n )\n for (const result of results) {\n for (const [path, source] of result) files.set(path, source)\n }\n return files\n}\n\nfunction validateSelection(name: string, value: unknown) {\n if (value === undefined || typeof value === 'boolean') return\n if (!plainObject(value) || (value.overrides !== undefined && !plainObject(value.overrides))) {\n throw new Error(`${name} policy must be a boolean or an object with overrides`)\n }\n}\n\nfunction selectedPolicy(base: Record<string, unknown>, selection: Exclude<PolicySelection, false>) {\n return selection === true ? base : deepMerge(base, selection.overrides ?? {})\n}\n\nfunction deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n [...new Set([...Object.keys(base), ...Object.keys(overrides)])].map((key) => {\n const left = base[key]\n const right = overrides[key]\n return [key, plainObject(left) && plainObject(right) ? deepMerge(left, right) : right === undefined ? left : right]\n }),\n )\n}\n\nfunction plainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/policy/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { parseDocument, stringify } from 'yaml'\n\nexport { fleetConfig, fleetMarkdown, inspectFleet, loadGitHubAdoptionSnapshot } from './fleet.js'\nexport type { FleetConfig, FleetRepository, FleetResult } from './fleet.js'\n\nconst changesetsPolicy = {\n $schema: 'https://unpkg.com/@changesets/config@3.1.2/schema.json',\n changelog: '@changesets/cli/changelog',\n commit: false,\n fixed: [],\n linked: [],\n access: 'public',\n baseBranch: 'main',\n updateInternalDependencies: 'patch',\n ignore: [],\n}\n\nconst dependabotPolicy = {\n version: 2,\n updates: [\n {\n 'package-ecosystem': 'npm',\n directory: '/',\n schedule: { interval: 'weekly' },\n cooldown: { 'default-days': 7 },\n groups: { 'javascript-dependencies': { patterns: ['*'] } },\n },\n {\n 'package-ecosystem': 'github-actions',\n directory: '/',\n schedule: { interval: 'weekly' },\n cooldown: { 'default-days': 7 },\n },\n ],\n}\n\ntype PolicySelection = boolean | { overrides?: Record<string, unknown> }\n\nexport type RepositoryPolicy = {\n changesets?: PolicySelection\n dependabot?: PolicySelection\n pnpm?: false | { minimumReleaseAge?: number }\n adoption?: false | AdoptionPolicy\n}\n\nexport type AdoptionPolicy = {\n minimumRasStackVersion?: string\n minimumWorkflowRasStackVersion?: string\n node?: string\n pnpm?: string\n just?: string\n requiredReferences?: string[]\n}\n\nexport type AdoptionSnapshot = {\n manifest: Record<string, unknown>\n files: Map<string, string>\n}\n\nexport async function syncRepositoryPolicy(root: string, mode: 'check' | 'write') {\n const config = repositoryPolicy(await readFile(join(root, 'ras-stack.policy.json'), 'utf8'))\n const files = await renderedPolicyFiles(root, config)\n const compared = await Promise.all(\n [...files].map(async ([path, expected]) => {\n const absolute = join(root, path)\n const actual = await readFile(absolute, 'utf8').catch(() => undefined)\n if (actual === expected) return undefined\n if (mode === 'write') {\n await mkdir(dirname(absolute), { recursive: true })\n await writeFile(absolute, expected)\n }\n return path\n }),\n )\n return compared.filter((path) => path !== undefined)\n}\n\nexport async function checkRepositoryPolicy(root: string) {\n const files = await syncRepositoryPolicy(root, 'check')\n const config = repositoryPolicy(await readFile(join(root, 'ras-stack.policy.json'), 'utf8'))\n return [...files.map((path) => `policy drift: ${path}`), ...(config.adoption ? await adoptionDrift(root, config.adoption) : [])]\n}\n\nexport async function adoptionDrift(root: string, policy: AdoptionPolicy) {\n const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as Record<string, unknown>\n const files = await adoptionFiles(root)\n return adoptionSnapshotDrift({ manifest, files }, policy)\n}\n\nexport function adoptionSnapshotDrift(snapshot: AdoptionSnapshot, policy: AdoptionPolicy) {\n const drift: string[] = []\n const { manifest, files } = snapshot\n if (policy.node && manifest.engines && plainObject(manifest.engines) && manifest.engines.node !== policy.node) {\n drift.push(`toolchain drift: package.json engines.node must be ${policy.node}`)\n }\n if (policy.pnpm && manifest.packageManager !== `pnpm@${policy.pnpm}`) {\n drift.push(`toolchain drift: package.json packageManager must be pnpm@${policy.pnpm}`)\n }\n if (policy.minimumRasStackVersion) {\n const dependency = dependencyVersion(manifest, 'ras-stack')\n if (dependency && compareVersions(dependency, policy.minimumRasStackVersion) < 0) {\n drift.push(`ras-stack drift: package.json uses ${dependency}, minimum is ${policy.minimumRasStackVersion}`)\n }\n }\n\n const workflows = new Map([...files].filter(([path]) => path.startsWith('.github/') && /\\.ya?ml$/.test(path)))\n const workflowMinimum = policy.minimumWorkflowRasStackVersion ?? policy.minimumRasStackVersion\n for (const [path, source] of workflows) {\n if (workflowMinimum) {\n for (const match of source.matchAll(/richardsolomou\\/ras-stack\\/[^\\s'\"}]+@v(\\d+\\.\\d+\\.\\d+)/g)) {\n const version = match[1]\n if (version && compareVersions(version, workflowMinimum) < 0) {\n drift.push(`ras-stack drift: ${path} uses v${version}, minimum is v${workflowMinimum}`)\n }\n }\n }\n }\n if (\n policy.just &&\n ![...workflows.values()].some(\n (source) => source.includes(`just-version: '${policy.just}'`) || source.includes(`just-version: ${policy.just}`),\n )\n ) {\n drift.push(`toolchain drift: no workflow declares just-version ${policy.just}`)\n }\n for (const reference of policy.requiredReferences ?? []) {\n if (![...files.values()].some((source) => source.includes(reference))) {\n drift.push(`shared config drift: no configuration references ${reference}`)\n }\n }\n return [...new Set(drift)]\n}\n\nexport async function renderedPolicyFiles(root: string, config: RepositoryPolicy) {\n const files = new Map<string, string>()\n if (config.changesets) {\n files.set('.changeset/config.json', `${JSON.stringify(selectedPolicy(changesetsPolicy, config.changesets), null, 2)}\\n`)\n }\n if (config.dependabot) {\n files.set('.github/dependabot.yml', stringify(selectedPolicy(dependabotPolicy, config.dependabot), { lineWidth: 0, singleQuote: true }))\n }\n if (config.pnpm) {\n const path = join(root, 'pnpm-workspace.yaml')\n const document = parseDocument(await readFile(path, 'utf8').catch(() => ''))\n document.set('minimumReleaseAge', config.pnpm.minimumReleaseAge ?? 10_080)\n files.set('pnpm-workspace.yaml', document.toString({ lineWidth: 0 }))\n }\n return files\n}\n\nfunction repositoryPolicy(source: string): RepositoryPolicy {\n const value = JSON.parse(source) as unknown\n if (!plainObject(value)) throw new Error('ras-stack.policy.json must contain an object')\n for (const name of ['changesets', 'dependabot'] as const) validateSelection(name, value[name])\n const pnpm = value.pnpm\n if (pnpm !== undefined && pnpm !== false) {\n if (!plainObject(pnpm)) throw new Error('pnpm policy must be false or an object')\n const age = pnpm.minimumReleaseAge\n if (age !== undefined && (typeof age !== 'number' || !Number.isInteger(age) || age < 0)) {\n throw new Error('pnpm.minimumReleaseAge must be a non-negative integer')\n }\n }\n const adoption = value.adoption\n if (adoption !== undefined && adoption !== false) {\n if (!plainObject(adoption)) throw new Error('adoption policy must be false or an object')\n for (const key of ['minimumRasStackVersion', 'minimumWorkflowRasStackVersion', 'node', 'pnpm', 'just']) {\n if (adoption[key] !== undefined && typeof adoption[key] !== 'string') throw new Error(`adoption.${key} must be a string`)\n }\n if (adoption.requiredReferences !== undefined && !stringArray(adoption.requiredReferences)) {\n throw new Error('adoption.requiredReferences must be an array of strings')\n }\n }\n return value\n}\n\nfunction dependencyVersion(manifest: Record<string, unknown>, name: string) {\n for (const field of ['dependencies', 'devDependencies']) {\n const dependencies = manifest[field]\n const value = plainObject(dependencies) ? dependencies[name] : undefined\n if (typeof value === 'string') return /\\d+\\.\\d+\\.\\d+/.exec(value)?.[0]\n }\n return undefined\n}\n\nfunction compareVersions(left: string, right: string) {\n const a = left.split('.').map(Number)\n const b = right.split('.').map(Number)\n for (let index = 0; index < 3; index += 1) {\n if (a[index] !== b[index]) return (a[index] ?? 0) - (b[index] ?? 0)\n }\n return 0\n}\n\nasync function textFiles(directory: string, root = directory): Promise<Map<string, string>> {\n const files = new Map<string, string>()\n const entries = await readdir(directory, { withFileTypes: true }).catch(() => [])\n const results = await Promise.all(\n entries.map(async (entry) => {\n const path = join(directory, entry.name)\n if (entry.isDirectory()) return textFiles(path, root)\n if (/\\.ya?ml$/.test(entry.name)) return new Map([[path.slice(root.length + 1), await readFile(path, 'utf8')]])\n return new Map<string, string>()\n }),\n )\n for (const result of results) {\n for (const [path, source] of result) files.set(path, source)\n }\n return files\n}\n\nasync function adoptionFiles(root: string) {\n const files = await textFiles(join(root, '.github'), root)\n const entries = await readdir(root, { withFileTypes: true })\n await Promise.all(\n entries.map(async (entry) => {\n if (!entry.isFile() || !/^(?:ras-stack\\.policy|oxlint|tsconfig(?:\\.[^.]+)*)\\.json$/.test(entry.name)) return\n files.set(entry.name, await readFile(join(root, entry.name), 'utf8'))\n }),\n )\n return files\n}\n\nfunction validateSelection(name: string, value: unknown) {\n if (value === undefined || typeof value === 'boolean') return\n if (!plainObject(value) || (value.overrides !== undefined && !plainObject(value.overrides))) {\n throw new Error(`${name} policy must be a boolean or an object with overrides`)\n }\n}\n\nfunction selectedPolicy(base: Record<string, unknown>, selection: Exclude<PolicySelection, false>) {\n return selection === true ? base : deepMerge(base, selection.overrides ?? {})\n}\n\nfunction deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n [...new Set([...Object.keys(base), ...Object.keys(overrides)])].map((key) => {\n const left = base[key]\n const right = overrides[key]\n return [key, plainObject(left) && plainObject(right) ? deepMerge(left, right) : right === undefined ? left : right]\n }),\n )\n}\n\nfunction plainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction stringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string')\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ras-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Composable full-stack primitives shared across Richard Solomou's applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"types": "./dist/database/postgres.d.ts",
|
|
60
60
|
"default": "./dist/database/postgres.js"
|
|
61
61
|
},
|
|
62
|
+
"./conformance": {
|
|
63
|
+
"types": "./dist/conformance/index.d.ts",
|
|
64
|
+
"default": "./dist/conformance/index.js"
|
|
65
|
+
},
|
|
62
66
|
"./policy": {
|
|
63
67
|
"types": "./dist/policy/index.d.ts",
|
|
64
68
|
"default": "./dist/policy/index.js"
|