@clossys/launcher 0.3.0 → 0.4.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 +1328 -60
- package/contracts/conversation-contract.md +2 -1
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +453 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +799 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +157 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +2840 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +198 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +840 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +493 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -5
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +20 -16
- package/skill-catalogue/advisor/SKILL.md +100 -11
- package/skill-catalogue/architect/SKILL.md +2 -13
- package/skill-catalogue/bouncer/SKILL.md +2 -13
- package/skill-catalogue/builder/SKILL.md +2 -13
- package/skill-catalogue/butler/SKILL.md +2 -13
- package/skill-catalogue/controller/SKILL.md +2 -13
- package/skill-catalogue/customer/SKILL.md +4 -13
- package/skill-catalogue/designer/SKILL.md +9 -14
- package/skill-catalogue/giver/SKILL.md +2 -13
- package/skill-catalogue/influencer/SKILL.md +2 -13
- package/skill-catalogue/inspector/SKILL.md +2 -13
- package/skill-catalogue/integrator/SKILL.md +2 -13
- package/skill-catalogue/keeper/SKILL.md +2 -13
- package/skill-catalogue/launcher/SKILL.md +20 -16
- package/skill-catalogue/locksmith/SKILL.md +2 -13
- package/skill-catalogue/messenger/SKILL.md +2 -13
- package/skill-catalogue/observer/SKILL.md +2 -13
- package/skill-catalogue/publisher/SKILL.md +11 -17
- package/skill-catalogue/starter/SKILL.md +3 -13
- package/skill-catalogue/strategist/SKILL.md +14 -17
- package/skill-catalogue/writer/SKILL.md +7 -14
- package/src/admission-fixture.ts +572 -0
- package/src/admission.ts +816 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +637 -0
- package/src/ledger-trust.ts +267 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
- package/CHANGELOG.md +0 -131
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The bytes of the setup template files: the Starter request, the trusted
|
|
3
|
+
* adoption decision workflow, the product CI workflow, and the dispatcher that
|
|
4
|
+
* names which files an act writes.
|
|
5
|
+
*
|
|
6
|
+
* These are pure renderers. They do no I/O and read no clock, environment, or
|
|
7
|
+
* randomness, so the bytes are a function of the input and of the constants
|
|
8
|
+
* below. The only imports are the act-to-path table and the sibling script
|
|
9
|
+
* renderers, which are themselves pure. Every ambiguity refuses: a refusal names
|
|
10
|
+
* a position (`starter.version`), never the offending value, so a refusal can be
|
|
11
|
+
* shown or logged without carrying what was refused.
|
|
12
|
+
*
|
|
13
|
+
* Template text is built only from the string constants below. The one choice
|
|
14
|
+
* an input makes is between the two constant install variants; no caller text
|
|
15
|
+
* is ever spliced into a workflow.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { TEMPLATE_PATHS, type TemplateAct } from "./change-set-contract.js";
|
|
19
|
+
import { renderAdoptionEvidenceWorkflow, renderPathScopeWorkflow, renderSnapshotCollector } from "./setup-template-scripts.js";
|
|
20
|
+
|
|
21
|
+
export type SetupPackageManager = "npm" | "pnpm";
|
|
22
|
+
|
|
23
|
+
export interface StarterPinInput {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly integrity: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StarterRequestInput {
|
|
30
|
+
readonly packageManager: unknown;
|
|
31
|
+
readonly repository: string;
|
|
32
|
+
readonly starter: StarterPinInput;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TemplateRefusal {
|
|
36
|
+
readonly reason: "starter-pin-unsupported" | "package-manager-unsupported" | "input-invalid";
|
|
37
|
+
/** A position such as `starter.version`. Never the offending value. */
|
|
38
|
+
readonly at: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TemplateFile {
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly bytes: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TemplateResult =
|
|
47
|
+
| { readonly ok: true; readonly files: readonly TemplateFile[] }
|
|
48
|
+
| { readonly ok: false; readonly refusal: TemplateRefusal };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The Starter versions a setup may pin, for documentation. The check below is
|
|
52
|
+
* the rule: an exact `0.2.PATCH`, with no range, prerelease, build metadata, or
|
|
53
|
+
* leading zero.
|
|
54
|
+
*/
|
|
55
|
+
export const STARTER_PIN_RANGE = ">=0.2.0 <0.3.0";
|
|
56
|
+
|
|
57
|
+
const REQUEST_PATH = ".starter/request.json";
|
|
58
|
+
const DECISION_WORKFLOW_PATH = ".github/workflows/clossys-adoption-decision.yml";
|
|
59
|
+
|
|
60
|
+
const STARTER_NAME = "@clossys/starter";
|
|
61
|
+
const STARTER_BIN = "foundry-starter";
|
|
62
|
+
const SNAPSHOT_MAX_AGE_MS = 3_600_000;
|
|
63
|
+
|
|
64
|
+
const STARTER_VERSION = /^0\.2\.(?:0|[1-9][0-9]{0,8})$/u;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether `version` is a Starter version the templates support: exactly the
|
|
68
|
+
* rule renderStarterRequest() applies, so a repository is read as past its
|
|
69
|
+
* setup only at a pin these templates would themselves have written.
|
|
70
|
+
*/
|
|
71
|
+
export function isSupportedStarterVersion(version: unknown): boolean {
|
|
72
|
+
return typeof version === "string" && STARTER_VERSION.test(version);
|
|
73
|
+
}
|
|
74
|
+
/** A SHA-512 SRI: 86 base64 symbols and `==`. */
|
|
75
|
+
const SHA512_SRI = /^sha512-([A-Za-z0-9+/]{86})==$/u;
|
|
76
|
+
/** The last of 86 symbols carries 4 payload bits and 2 zero bits; only these four symbols end a canonical 64-byte value. */
|
|
77
|
+
const CANONICAL_LAST_SYMBOL = "AQgw";
|
|
78
|
+
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
|
|
79
|
+
const REPOSITORY_NAME = /^[A-Za-z0-9._-]{1,100}$/u;
|
|
80
|
+
|
|
81
|
+
const INPUT_KEYS: ReadonlySet<string> = new Set(["packageManager", "repository", "starter"]);
|
|
82
|
+
const STARTER_KEYS: ReadonlySet<string> = new Set(["name", "version", "integrity"]);
|
|
83
|
+
|
|
84
|
+
function refuse(reason: TemplateRefusal["reason"], at: string): TemplateResult {
|
|
85
|
+
return { ok: false, refusal: { reason, at } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether `value` is a plain object: an ordinary or null-prototype object whose
|
|
90
|
+
* every own key is a string in `allowed`, spelled as an enumerable data
|
|
91
|
+
* property. A getter, a symbol key, or a key named anything else (an inherited
|
|
92
|
+
* shape, `__proto__` from a JSON parse) is not plain.
|
|
93
|
+
*/
|
|
94
|
+
function isPlainRecord(value: unknown, allowed: ReadonlySet<string>): value is Record<string, unknown> {
|
|
95
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
96
|
+
const prototype = Object.getPrototypeOf(value);
|
|
97
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
98
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
99
|
+
if (typeof key !== "string" || !allowed.has(key)) return false;
|
|
100
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
101
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.enumerable !== true) return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ownValue(record: Record<string, unknown>, key: string): unknown {
|
|
107
|
+
return Object.getOwnPropertyDescriptor(record, key)?.value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isPackageManager(value: unknown): value is SetupPackageManager {
|
|
111
|
+
return value === "npm" || value === "pnpm";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isRepositorySlug(value: unknown): value is string {
|
|
115
|
+
if (typeof value !== "string") return false;
|
|
116
|
+
const parts = value.split("/");
|
|
117
|
+
if (parts.length !== 2) return false;
|
|
118
|
+
const [owner, name] = parts as [string, string];
|
|
119
|
+
return OWNER.test(owner) && REPOSITORY_NAME.test(name) && name !== "." && name !== "..";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isSha512Sri(value: unknown): value is string {
|
|
123
|
+
if (typeof value !== "string") return false;
|
|
124
|
+
const match = SHA512_SRI.exec(value);
|
|
125
|
+
if (match === null) return false;
|
|
126
|
+
return CANONICAL_LAST_SYMBOL.includes((match[1] as string).charAt(85));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Render `.starter/request.json`, or refuse at the first position, in the fixed order, that is not acceptable. */
|
|
130
|
+
export function renderStarterRequest(input: unknown): TemplateResult {
|
|
131
|
+
if (!isPlainRecord(input, INPUT_KEYS)) return refuse("input-invalid", "input");
|
|
132
|
+
const packageManager = ownValue(input, "packageManager");
|
|
133
|
+
const repository = ownValue(input, "repository");
|
|
134
|
+
const starter = ownValue(input, "starter");
|
|
135
|
+
if (!isPlainRecord(starter, STARTER_KEYS)) return refuse("input-invalid", "starter");
|
|
136
|
+
|
|
137
|
+
if (!isPackageManager(packageManager)) return refuse("package-manager-unsupported", "packageManager");
|
|
138
|
+
if (!isRepositorySlug(repository)) return refuse("input-invalid", "repository");
|
|
139
|
+
const name = ownValue(starter, "name");
|
|
140
|
+
if (name !== STARTER_NAME) return refuse("input-invalid", "starter.name");
|
|
141
|
+
const version = ownValue(starter, "version");
|
|
142
|
+
if (!isSupportedStarterVersion(version)) return refuse("starter-pin-unsupported", "starter.version");
|
|
143
|
+
const integrity = ownValue(starter, "integrity");
|
|
144
|
+
if (!isSha512Sri(integrity)) return refuse("input-invalid", "starter.integrity");
|
|
145
|
+
|
|
146
|
+
// Every member is written out, in this order, from the checked values above.
|
|
147
|
+
// The request never carries an advisor or a hub.
|
|
148
|
+
const request = {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
phase: "admission",
|
|
151
|
+
packageManager,
|
|
152
|
+
snapshot: {
|
|
153
|
+
repository,
|
|
154
|
+
maxAgeMs: SNAPSHOT_MAX_AGE_MS,
|
|
155
|
+
},
|
|
156
|
+
starter: {
|
|
157
|
+
name,
|
|
158
|
+
version,
|
|
159
|
+
integrity,
|
|
160
|
+
bin: STARTER_BIN,
|
|
161
|
+
},
|
|
162
|
+
target: {
|
|
163
|
+
name,
|
|
164
|
+
version,
|
|
165
|
+
integrity,
|
|
166
|
+
bin: STARTER_BIN,
|
|
167
|
+
invocation: "single-json-input",
|
|
168
|
+
},
|
|
169
|
+
evidence: {
|
|
170
|
+
assessment: "evidence/assessment.json",
|
|
171
|
+
targetInput: "evidence/target-input.json",
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
return { ok: true, files: [{ path: REQUEST_PATH, bytes: `${JSON.stringify(request, null, 2)}\n` }] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// The trusted adoption decision workflow
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
const CHECKOUT = "actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0";
|
|
182
|
+
const SETUP_NODE = "actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0";
|
|
183
|
+
|
|
184
|
+
const HEADER: readonly string[] = [
|
|
185
|
+
"# Clossys adoption decision, written by Clossys Launcher.",
|
|
186
|
+
"#",
|
|
187
|
+
"# This is the trusted half of the adoption check. It runs when the",
|
|
188
|
+
'# "Clossys adoption evidence" workflow completes, whatever its conclusion,',
|
|
189
|
+
"# and its job has no condition: a skipped required job reads as a green check,",
|
|
190
|
+
"# so this job starts for every conclusion.",
|
|
191
|
+
"#",
|
|
192
|
+
"# The trusted job executes only the installed base. It checks out the pull",
|
|
193
|
+
"# request base commit, runs one fixed frozen install with lifecycle scripts",
|
|
194
|
+
"# off and no registry credential, and runs the installed Starter's admit",
|
|
195
|
+
"# command. It reads the pull request head's installed ledger as data, from a",
|
|
196
|
+
"# sparse checkout of that one file, and executes nothing from the head.",
|
|
197
|
+
"#",
|
|
198
|
+
"# The snapshot artifact a pull request produces is never read by this job. No",
|
|
199
|
+
"# artifact of any kind is fetched or trusted here.",
|
|
200
|
+
"#",
|
|
201
|
+
"# One-merge lag: this job runs from the base, so a change to this workflow, to",
|
|
202
|
+
"# the evidence workflow, or to .starter/request.json is proved only by the",
|
|
203
|
+
"# first pull request after it merges.",
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
function workflowTop(manager: SetupPackageManager): readonly string[] {
|
|
207
|
+
return [
|
|
208
|
+
...HEADER,
|
|
209
|
+
"name: Clossys adoption decision (" + manager + ")",
|
|
210
|
+
"",
|
|
211
|
+
"on:",
|
|
212
|
+
" workflow_run:",
|
|
213
|
+
" workflows: [Clossys adoption evidence]",
|
|
214
|
+
" types: [completed]",
|
|
215
|
+
"",
|
|
216
|
+
"permissions:",
|
|
217
|
+
" contents: read",
|
|
218
|
+
"",
|
|
219
|
+
"jobs:",
|
|
220
|
+
" decide-adoption:",
|
|
221
|
+
" runs-on: ubuntu-latest",
|
|
222
|
+
" timeout-minutes: 15",
|
|
223
|
+
" steps:",
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const FIRST_STEPS: readonly string[] = [
|
|
228
|
+
" # Fail closed unless this run names a pull request base and a head commit.",
|
|
229
|
+
" # A run with no pull request, such as one from a fork, has neither. Values",
|
|
230
|
+
" # reach the script only through the environment, never through its text.",
|
|
231
|
+
" - name: Require the pull request base and head",
|
|
232
|
+
" env:",
|
|
233
|
+
" BASE_SHA: ${{ github.event.workflow_run.pull_requests[0].base.sha }}",
|
|
234
|
+
" HEAD_SHA: ${{ github.event.workflow_run.head_sha }}",
|
|
235
|
+
" run: |",
|
|
236
|
+
" sha='^[0-9a-f]{40}$'",
|
|
237
|
+
' if [[ ! "$BASE_SHA" =~ $sha || ! "$HEAD_SHA" =~ $sha ]]; then',
|
|
238
|
+
' echo "::error::This run has no pull request base and head commit to admit."',
|
|
239
|
+
" exit 1",
|
|
240
|
+
" fi",
|
|
241
|
+
"",
|
|
242
|
+
" # The protected pull request base, never the source run's head. It is the",
|
|
243
|
+
" # only checkout this job executes.",
|
|
244
|
+
" - name: Check out the protected base",
|
|
245
|
+
" uses: " + CHECKOUT,
|
|
246
|
+
" with:",
|
|
247
|
+
" ref: ${{ github.event.workflow_run.pull_requests[0].base.sha }}",
|
|
248
|
+
" persist-credentials: false",
|
|
249
|
+
"",
|
|
250
|
+
" - name: Set up Node",
|
|
251
|
+
" uses: " + SETUP_NODE,
|
|
252
|
+
" with:",
|
|
253
|
+
" node-version: 20",
|
|
254
|
+
" registry-url: https://registry.npmjs.org",
|
|
255
|
+
' scope: "@clossys"',
|
|
256
|
+
"",
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const NPM_INSTALL: readonly string[] = [
|
|
260
|
+
" # Fixed and frozen: lifecycle scripts stay off and no registry credential is",
|
|
261
|
+
" # attached. A failure here stops the job before Starter runs.",
|
|
262
|
+
" - name: Fixed npm install",
|
|
263
|
+
" run: npm ci --ignore-scripts",
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const PNPM_INSTALL: readonly string[] = [
|
|
267
|
+
" - name: Enable pnpm",
|
|
268
|
+
" run: corepack enable",
|
|
269
|
+
"",
|
|
270
|
+
" # Fixed and frozen: lifecycle scripts stay off and no registry credential is",
|
|
271
|
+
" # attached. A failure here stops the job before Starter runs.",
|
|
272
|
+
" - name: Fixed pnpm install",
|
|
273
|
+
" run: pnpm install --frozen-lockfile --ignore-scripts",
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const LAST_STEPS: readonly string[] = [
|
|
277
|
+
"",
|
|
278
|
+
" # The pull request head as data only: one file, no persisted credential,",
|
|
279
|
+
" # nothing executed.",
|
|
280
|
+
" - name: Check out the pull request head ledger as data",
|
|
281
|
+
" uses: " + CHECKOUT,
|
|
282
|
+
" with:",
|
|
283
|
+
" ref: ${{ github.event.workflow_run.head_sha }}",
|
|
284
|
+
" path: .starter-head",
|
|
285
|
+
" persist-credentials: false",
|
|
286
|
+
" sparse-checkout-cone-mode: false",
|
|
287
|
+
" sparse-checkout: |",
|
|
288
|
+
" /clossys/.state/installed.json",
|
|
289
|
+
"",
|
|
290
|
+
" # No credential environment is attached to this step. Its captured status",
|
|
291
|
+
" # is printed and re-raised exactly.",
|
|
292
|
+
" - name: Admit the pull request head",
|
|
293
|
+
" run: |",
|
|
294
|
+
' report="$RUNNER_TEMP/adoption-admission-report.json"',
|
|
295
|
+
' output="$RUNNER_TEMP/adoption-admission.txt"',
|
|
296
|
+
" status=0",
|
|
297
|
+
" node node_modules/@clossys/starter/dist/cli.js admit \\",
|
|
298
|
+
" .starter/request.json \\",
|
|
299
|
+
" . \\",
|
|
300
|
+
" .starter-head \\",
|
|
301
|
+
' --report "$report" > "$output" || status=$?',
|
|
302
|
+
' cat "$output"',
|
|
303
|
+
' cat "$output" >> "$GITHUB_STEP_SUMMARY"',
|
|
304
|
+
' exit "$status"',
|
|
305
|
+
"",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
function decisionWorkflow(manager: SetupPackageManager): string {
|
|
309
|
+
const install = manager === "npm" ? NPM_INSTALL : PNPM_INSTALL;
|
|
310
|
+
return [...workflowTop(manager), ...FIRST_STEPS, ...install, ...LAST_STEPS].join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Render `.github/workflows/clossys-adoption-decision.yml` for npm or pnpm, or refuse at `packageManager`. */
|
|
314
|
+
export function renderAdoptionDecisionWorkflow(packageManager: unknown): TemplateResult {
|
|
315
|
+
if (!isPackageManager(packageManager)) return refuse("package-manager-unsupported", "packageManager");
|
|
316
|
+
return { ok: true, files: [{ path: DECISION_WORKFLOW_PATH, bytes: decisionWorkflow(packageManager) }] };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// The product CI workflow
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
const CI_WORKFLOW_PATH = ".github/workflows/clossys-ci.yml";
|
|
324
|
+
const PATH_SCOPE_WORKFLOW_PATH = ".github/workflows/clossys-path-scope.yml";
|
|
325
|
+
const EVIDENCE_WORKFLOW_PATH = ".github/workflows/clossys-adoption-evidence.yml";
|
|
326
|
+
const SNAPSHOT_COLLECTOR_PATH = ".github/scripts/clossys-collect-adoption-snapshot.mjs";
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The lines of the product CI workflow, one string per line. The workflow
|
|
330
|
+
* carries `${{` expressions, which is why these are quoted strings and not a
|
|
331
|
+
* template literal. A test holds these bytes equal to the copy Controller ships.
|
|
332
|
+
*/
|
|
333
|
+
const PRODUCT_CI_WORKFLOW: readonly string[] = [
|
|
334
|
+
"# CI workflow for a client product repository. The setup pull request of a",
|
|
335
|
+
"# product repository writes this file as .github/workflows/clossys-ci.yml,",
|
|
336
|
+
"# only when that path is absent -- an existing file is left as the",
|
|
337
|
+
"# repository has it.",
|
|
338
|
+
"#",
|
|
339
|
+
"# Passes ci-conventions-check with `verify-product` declared as the required",
|
|
340
|
+
"# context mapped to that installed path; a test in this package runs the",
|
|
341
|
+
"# shipped bytes through the checker. One job means no fan-in job is needed:",
|
|
342
|
+
"# `verify-product` is itself the required-status-check context.",
|
|
343
|
+
"#",
|
|
344
|
+
"# The package manager is chosen by lockfile, and anything else fails",
|
|
345
|
+
"# closed: a repository with exactly one of package-lock.json or",
|
|
346
|
+
"# pnpm-lock.yaml installs with that manager, and a repository with both, with",
|
|
347
|
+
"# neither, or with a yarn.lock beside either is refused by the install step",
|
|
348
|
+
"# with an error rather than guessed at. The install step",
|
|
349
|
+
"# passes --ignore-scripts, and no cache is configured because cache selection",
|
|
350
|
+
"# would need the same lockfile decision made before the install step runs.",
|
|
351
|
+
"# Build and test run the repository's own `build` and `test` scripts when",
|
|
352
|
+
"# present and skip a missing one.",
|
|
353
|
+
"",
|
|
354
|
+
"name: Clossys CI",
|
|
355
|
+
"",
|
|
356
|
+
"on:",
|
|
357
|
+
" pull_request:",
|
|
358
|
+
" push:",
|
|
359
|
+
" branches: [main]",
|
|
360
|
+
" merge_group:",
|
|
361
|
+
"",
|
|
362
|
+
"# Least-privilege by default (ci-conventions.md, Security). A job that",
|
|
363
|
+
"# genuinely needs more declares exactly that job-level exception.",
|
|
364
|
+
"permissions:",
|
|
365
|
+
" contents: read",
|
|
366
|
+
"",
|
|
367
|
+
"# cancel-in-progress only for pull_request: the group key includes the",
|
|
368
|
+
"# event name, so a push (to main) or a merge_group run is never cancelled",
|
|
369
|
+
"# by a later pull_request run on the same ref, and a pull_request run IS",
|
|
370
|
+
"# cancelled by its own next push -- ci-conventions.md, Cost.",
|
|
371
|
+
"concurrency:",
|
|
372
|
+
" group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}",
|
|
373
|
+
" cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
|
|
374
|
+
"",
|
|
375
|
+
"jobs:",
|
|
376
|
+
" verify-product:",
|
|
377
|
+
" runs-on: ubuntu-latest",
|
|
378
|
+
" timeout-minutes: 15",
|
|
379
|
+
" steps:",
|
|
380
|
+
" - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0",
|
|
381
|
+
" with:",
|
|
382
|
+
" persist-credentials: false",
|
|
383
|
+
" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0",
|
|
384
|
+
" with:",
|
|
385
|
+
" node-version: 20",
|
|
386
|
+
" # Selects the manager once and records it as PM for the build and",
|
|
387
|
+
" # test steps below, so the lockfile decision is not repeated.",
|
|
388
|
+
" - name: Install dependencies",
|
|
389
|
+
" run: |",
|
|
390
|
+
" if [ -f yarn.lock ]; then",
|
|
391
|
+
" echo \"::error::yarn.lock is present and Yarn is not supported\"",
|
|
392
|
+
" exit 1",
|
|
393
|
+
" elif [ -f pnpm-lock.yaml ] && [ ! -f package-lock.json ]; then",
|
|
394
|
+
" echo \"PM=pnpm\" >> \"$GITHUB_ENV\"",
|
|
395
|
+
" corepack enable",
|
|
396
|
+
" pnpm install --frozen-lockfile --ignore-scripts",
|
|
397
|
+
" elif [ -f package-lock.json ] && [ ! -f pnpm-lock.yaml ]; then",
|
|
398
|
+
" echo \"PM=npm\" >> \"$GITHUB_ENV\"",
|
|
399
|
+
" npm ci --ignore-scripts",
|
|
400
|
+
" else",
|
|
401
|
+
" echo \"::error::exactly one of package-lock.json or pnpm-lock.yaml is required\"",
|
|
402
|
+
" exit 1",
|
|
403
|
+
" fi",
|
|
404
|
+
" - name: Build",
|
|
405
|
+
" run: '\"${PM:?}\" run --if-present build'",
|
|
406
|
+
" - name: Test",
|
|
407
|
+
" run: '\"${PM:?}\" run --if-present test'",
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
/** Render `.github/workflows/clossys-ci.yml`. */
|
|
411
|
+
export function renderProductCiWorkflow(): string {
|
|
412
|
+
return `${PRODUCT_CI_WORKFLOW.join("\n")}\n`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// The dispatcher
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
const CALLER_KEYS: ReadonlySet<string> = new Set(["packageManager"]);
|
|
420
|
+
|
|
421
|
+
function isTemplateAct(value: unknown): value is TemplateAct {
|
|
422
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(TEMPLATE_PATHS, value);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Hand back `files` only when their paths are exactly the act's, in order; anything else is a refusal. */
|
|
426
|
+
function checked(act: TemplateAct, result: TemplateResult): TemplateResult {
|
|
427
|
+
if (!result.ok) return result;
|
|
428
|
+
const expected = TEMPLATE_PATHS[act];
|
|
429
|
+
const same = result.files.length === expected.length && result.files.every((file, index) => file.path === expected[index]);
|
|
430
|
+
return same ? result : refuse("input-invalid", "act");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function renderCallerWorkflows(input: unknown): TemplateResult {
|
|
434
|
+
if (!isPlainRecord(input, CALLER_KEYS) || Reflect.ownKeys(input).length !== 1) return refuse("input-invalid", "input");
|
|
435
|
+
const decision = renderAdoptionDecisionWorkflow(ownValue(input, "packageManager"));
|
|
436
|
+
if (!decision.ok) return decision;
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
files: [
|
|
440
|
+
{ path: EVIDENCE_WORKFLOW_PATH, bytes: renderAdoptionEvidenceWorkflow() },
|
|
441
|
+
...decision.files,
|
|
442
|
+
{ path: SNAPSHOT_COLLECTOR_PATH, bytes: renderSnapshotCollector() },
|
|
443
|
+
],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderFixedFile(input: unknown, path: string, bytes: () => string): TemplateResult {
|
|
448
|
+
if (input !== undefined) return refuse("input-invalid", "input");
|
|
449
|
+
return { ok: true, files: [{ path, bytes: bytes() }] };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Render the files one setup act writes, or refuse. `add-caller-workflow` takes
|
|
454
|
+
* `{ packageManager }`, `write-starter-request` takes a `StarterRequestInput`,
|
|
455
|
+
* and `add-ci-template` and `add-path-scope-job` take no input. The returned
|
|
456
|
+
* paths are always `TEMPLATE_PATHS[act]`, in that order.
|
|
457
|
+
*/
|
|
458
|
+
export function renderSetupTemplate(act: TemplateAct, input?: unknown): TemplateResult {
|
|
459
|
+
const name: unknown = act;
|
|
460
|
+
if (!isTemplateAct(name)) return refuse("input-invalid", "act");
|
|
461
|
+
switch (name) {
|
|
462
|
+
case "add-caller-workflow":
|
|
463
|
+
return checked(name, renderCallerWorkflows(input));
|
|
464
|
+
case "write-starter-request":
|
|
465
|
+
return checked(name, renderStarterRequest(input));
|
|
466
|
+
case "add-ci-template":
|
|
467
|
+
return checked(name, renderFixedFile(input, CI_WORKFLOW_PATH, renderProductCiWorkflow));
|
|
468
|
+
case "add-path-scope-job":
|
|
469
|
+
return checked(name, renderFixedFile(input, PATH_SCOPE_WORKFLOW_PATH, renderPathScopeWorkflow));
|
|
470
|
+
}
|
|
471
|
+
}
|