@pellux/goodvibes-toolchain 1.11.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.
Files changed (80) hide show
  1. package/README.md +50 -0
  2. package/dist/bin/build-binaries.d.ts +3 -0
  3. package/dist/bin/build-binaries.d.ts.map +1 -0
  4. package/dist/bin/build-binaries.js +67 -0
  5. package/dist/bin/changelog-gate.d.ts +3 -0
  6. package/dist/bin/changelog-gate.d.ts.map +1 -0
  7. package/dist/bin/changelog-gate.js +13 -0
  8. package/dist/bin/coverage-gate.d.ts +3 -0
  9. package/dist/bin/coverage-gate.d.ts.map +1 -0
  10. package/dist/bin/coverage-gate.js +19 -0
  11. package/dist/bin/package-install-check.d.ts +3 -0
  12. package/dist/bin/package-install-check.d.ts.map +1 -0
  13. package/dist/bin/package-install-check.js +14 -0
  14. package/dist/bin/per-job-green.d.ts +3 -0
  15. package/dist/bin/per-job-green.d.ts.map +1 -0
  16. package/dist/bin/per-job-green.js +54 -0
  17. package/dist/bin/post-build-smoke.d.ts +3 -0
  18. package/dist/bin/post-build-smoke.d.ts.map +1 -0
  19. package/dist/bin/post-build-smoke.js +14 -0
  20. package/dist/bin/publish-package.d.ts +3 -0
  21. package/dist/bin/publish-package.d.ts.map +1 -0
  22. package/dist/bin/publish-package.js +25 -0
  23. package/dist/bin/release-cut.d.ts +3 -0
  24. package/dist/bin/release-cut.d.ts.map +1 -0
  25. package/dist/bin/release-cut.js +27 -0
  26. package/dist/bin/sdk-pin-gate.d.ts +3 -0
  27. package/dist/bin/sdk-pin-gate.d.ts.map +1 -0
  28. package/dist/bin/sdk-pin-gate.js +15 -0
  29. package/dist/bin/sha256sums.d.ts +3 -0
  30. package/dist/bin/sha256sums.d.ts.map +1 -0
  31. package/dist/bin/sha256sums.js +37 -0
  32. package/dist/bin/verification-ledger.d.ts +3 -0
  33. package/dist/bin/verification-ledger.d.ts.map +1 -0
  34. package/dist/bin/verification-ledger.js +28 -0
  35. package/dist/config.d.ts +147 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +62 -0
  38. package/dist/index.d.ts +23 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +22 -0
  41. package/dist/lib/build-binaries.d.ts +45 -0
  42. package/dist/lib/build-binaries.d.ts.map +1 -0
  43. package/dist/lib/build-binaries.js +98 -0
  44. package/dist/lib/changelog-gate.d.ts +18 -0
  45. package/dist/lib/changelog-gate.d.ts.map +1 -0
  46. package/dist/lib/changelog-gate.js +32 -0
  47. package/dist/lib/coverage-gate.d.ts +25 -0
  48. package/dist/lib/coverage-gate.d.ts.map +1 -0
  49. package/dist/lib/coverage-gate.js +46 -0
  50. package/dist/lib/effects.d.ts +53 -0
  51. package/dist/lib/effects.d.ts.map +1 -0
  52. package/dist/lib/effects.js +78 -0
  53. package/dist/lib/load-config.d.ts +8 -0
  54. package/dist/lib/load-config.d.ts.map +1 -0
  55. package/dist/lib/load-config.js +15 -0
  56. package/dist/lib/package-install-check.d.ts +49 -0
  57. package/dist/lib/package-install-check.d.ts.map +1 -0
  58. package/dist/lib/package-install-check.js +66 -0
  59. package/dist/lib/per-job-green.d.ts +54 -0
  60. package/dist/lib/per-job-green.d.ts.map +1 -0
  61. package/dist/lib/per-job-green.js +247 -0
  62. package/dist/lib/post-build-smoke.d.ts +24 -0
  63. package/dist/lib/post-build-smoke.d.ts.map +1 -0
  64. package/dist/lib/post-build-smoke.js +33 -0
  65. package/dist/lib/publish-package.d.ts +45 -0
  66. package/dist/lib/publish-package.d.ts.map +1 -0
  67. package/dist/lib/publish-package.js +58 -0
  68. package/dist/lib/release-cut.d.ts +52 -0
  69. package/dist/lib/release-cut.d.ts.map +1 -0
  70. package/dist/lib/release-cut.js +120 -0
  71. package/dist/lib/sdk-pin-gate.d.ts +25 -0
  72. package/dist/lib/sdk-pin-gate.d.ts.map +1 -0
  73. package/dist/lib/sdk-pin-gate.js +146 -0
  74. package/dist/lib/sha256sums.d.ts +41 -0
  75. package/dist/lib/sha256sums.d.ts.map +1 -0
  76. package/dist/lib/sha256sums.js +53 -0
  77. package/dist/lib/verification-ledger.d.ts +30 -0
  78. package/dist/lib/verification-ledger.d.ts.map +1 -0
  79. package/dist/lib/verification-ledger.js +37 -0
  80. package/package.json +59 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * package-install-check — static verification of a package as it would install.
3
+ *
4
+ * Inspects the `npm pack --json --dry-run` tarball for required and forbidden
5
+ * paths and a size cap, and verifies each bin shim exists, is executable, and
6
+ * carries the expected shebang. Static — the actual install-into-temp smoke is
7
+ * a separate tool/CI job.
8
+ */
9
+ import { realExec, realFsReader, consoleLogger } from './effects.js';
10
+ /** Check a packed file list + unpacked size against the tarball policy. Pure. */
11
+ export function evaluateTarballPaths(files, unpackedBytes, config) {
12
+ const present = new Set(files);
13
+ const missing = config.requiredTarballPaths.filter((p) => !present.has(p));
14
+ const forbidden = files.filter((f) => config.forbiddenTarballPrefixes.some((prefix) => f.startsWith(prefix)));
15
+ const oversize = unpackedBytes > config.maxTarballBytes;
16
+ return { ok: missing.length === 0 && forbidden.length === 0 && !oversize, missing, forbidden, oversize };
17
+ }
18
+ /** Verify one bin shim: present, executable, and has the expected shebang. */
19
+ export function evaluateBinShim(fs, path, name, shebang) {
20
+ const issues = [];
21
+ if (!fs.exists(path)) {
22
+ return { name, ok: false, issues: [`bin ${name} missing at ${path}`] };
23
+ }
24
+ if (!fs.isExecutable(path))
25
+ issues.push(`bin ${name} is not executable`);
26
+ const text = fs.readText(path);
27
+ if (!text.startsWith(shebang))
28
+ issues.push(`bin ${name} does not start with "${shebang}"`);
29
+ return { name, ok: issues.length === 0, issues };
30
+ }
31
+ /** Parse `npm pack --json` output into a file list and unpacked size. */
32
+ export function parseNpmPack(output) {
33
+ const parsed = JSON.parse(output);
34
+ const first = parsed[0];
35
+ return {
36
+ files: (first?.files ?? []).map((f) => f.path),
37
+ unpackedBytes: first?.unpackedSize ?? first?.size ?? 0,
38
+ };
39
+ }
40
+ /** Run the full static install check. */
41
+ export function runPackageInstallCheck(options) {
42
+ const exec = options.exec ?? realExec;
43
+ const fs = options.fs ?? realFsReader(options.cwd);
44
+ const logger = options.logger ?? consoleLogger;
45
+ const issues = [];
46
+ for (const bin of options.bins ?? []) {
47
+ const result = evaluateBinShim(fs, bin.path, bin.name, bin.shebang);
48
+ issues.push(...result.issues);
49
+ }
50
+ const pack = exec('npm', ['pack', '--json', '--dry-run'], { cwd: options.cwd });
51
+ if (pack.status !== 0) {
52
+ issues.push(`npm pack --dry-run failed: ${pack.stderr.trim().slice(0, 300)}`);
53
+ return { ok: false, issues };
54
+ }
55
+ const { files, unpackedBytes } = parseNpmPack(pack.stdout);
56
+ const policy = evaluateTarballPaths(files, unpackedBytes, options.config);
57
+ if (policy.missing.length > 0)
58
+ issues.push(`missing required tarball path(s): ${policy.missing.join(', ')}`);
59
+ if (policy.forbidden.length > 0)
60
+ issues.push(`forbidden tarball path(s): ${policy.forbidden.slice(0, 5).join(', ')}`);
61
+ if (policy.oversize)
62
+ issues.push(`tarball exceeds ${options.config.maxTarballBytes} bytes`);
63
+ const ok = issues.length === 0;
64
+ logger.info(ok ? `[package-install-check] OK (${files.length} files)` : `[package-install-check] ${issues.length} issue(s)`);
65
+ return { ok, issues };
66
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * per-job-green — the by-reference validation primitive.
3
+ *
4
+ * Given a commit SHA, find that commit's push-CI run for a named workflow, wait
5
+ * for it to complete, and assert EVERY job concluded `success` (per-job green,
6
+ * never the rollup). Generalizes the agent repo's bespoke poll, which asserted
7
+ * only the rollup plus one hard-coded job name.
8
+ *
9
+ * Transient-error posture: EVERY GitHub API call (runs listing, per-run jobs,
10
+ * check-suites, check-runs) goes through a bounded retry loop (default 8
11
+ * attempts with 7s sleeps, configurable) before its status is treated as
12
+ * final — the API's known flaky mode makes single-shot calls fail a meaningful
13
+ * fraction of the time. When the runs listing stays unavailable after retries,
14
+ * the Checks API (`check-suites` / `check-runs` on the commit) is the fallback.
15
+ * "No run found yet" is never a failure — it waits and polls. Only an exhausted
16
+ * retry budget on a needed endpoint, a non-green job, or the deadline produces
17
+ * a failure, and each names its source honestly.
18
+ */
19
+ import type { HttpGetJson, Logger, Sleep } from './effects.js';
20
+ import type { PerJobGreenConfig } from '../config.js';
21
+ export interface PerJobGreenResult {
22
+ readonly ok: boolean;
23
+ /** The resolved Actions run id, when one was found. */
24
+ readonly runId: number | null;
25
+ /** Head SHA the resolved run actually built (for the artifact-integrity handoff). */
26
+ readonly headSha: string | null;
27
+ /** Names of jobs/checks that did not conclude `success`. */
28
+ readonly failures: readonly string[];
29
+ /** Which source produced the verdict. */
30
+ readonly source: 'actions' | 'check-suites' | 'none';
31
+ readonly reason: string;
32
+ }
33
+ export interface PerJobGreenDeps {
34
+ readonly http: HttpGetJson;
35
+ readonly sleep: Sleep;
36
+ readonly logger: Logger;
37
+ /** Milliseconds since epoch; injectable so the deadline is deterministic in tests. */
38
+ readonly now: () => number;
39
+ /** GitHub API origin. Defaults to https://api.github.com. */
40
+ readonly apiBase?: string;
41
+ /** Bearer token for the API. */
42
+ readonly token?: string;
43
+ }
44
+ /**
45
+ * Extract the Actions run id from a check-run's details_url. Actions-created
46
+ * check runs point at `https://github.com/{owner}/{repo}/actions/runs/{run_id}/job/{job_id}`.
47
+ */
48
+ export declare function runIdFromDetailsUrl(detailsUrl: string | undefined): number | null;
49
+ /**
50
+ * Verify `sha`'s push-CI run is per-job green. Polls until the run completes or
51
+ * the deadline passes. Deterministic under injected `http`/`sleep`/`now`.
52
+ */
53
+ export declare function verifyPerJobGreen(deps: PerJobGreenDeps, config: PerJobGreenConfig, sha: string): Promise<PerJobGreenResult>;
54
+ //# sourceMappingURL=per-job-green.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"per-job-green.d.ts","sourceRoot":"","sources":["../../src/lib/per-job-green.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,cAAc,GAAG,MAAM,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAiGD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIjF;AAgFD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAyDjI"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * per-job-green — the by-reference validation primitive.
3
+ *
4
+ * Given a commit SHA, find that commit's push-CI run for a named workflow, wait
5
+ * for it to complete, and assert EVERY job concluded `success` (per-job green,
6
+ * never the rollup). Generalizes the agent repo's bespoke poll, which asserted
7
+ * only the rollup plus one hard-coded job name.
8
+ *
9
+ * Transient-error posture: EVERY GitHub API call (runs listing, per-run jobs,
10
+ * check-suites, check-runs) goes through a bounded retry loop (default 8
11
+ * attempts with 7s sleeps, configurable) before its status is treated as
12
+ * final — the API's known flaky mode makes single-shot calls fail a meaningful
13
+ * fraction of the time. When the runs listing stays unavailable after retries,
14
+ * the Checks API (`check-suites` / `check-runs` on the commit) is the fallback.
15
+ * "No run found yet" is never a failure — it waits and polls. Only an exhausted
16
+ * retry budget on a needed endpoint, a non-green job, or the deadline produces
17
+ * a failure, and each names its source honestly.
18
+ */
19
+ function headers(token) {
20
+ const base = {
21
+ Accept: 'application/vnd.github+json',
22
+ 'X-GitHub-Api-Version': '2022-11-28',
23
+ 'User-Agent': 'goodvibes-toolchain-per-job-green',
24
+ };
25
+ if (token)
26
+ base.Authorization = `Bearer ${token}`;
27
+ return base;
28
+ }
29
+ function asArray(value, key) {
30
+ if (value && typeof value === 'object') {
31
+ const field = value[key];
32
+ if (Array.isArray(field))
33
+ return field;
34
+ }
35
+ return [];
36
+ }
37
+ /** Statuses worth retrying: server errors, rate limiting, and transport failures. */
38
+ function isTransientStatus(status) {
39
+ return status === 0 || status === 429 || status >= 500;
40
+ }
41
+ /**
42
+ * GET with the tool's bounded transient-error retry. Every API call in this
43
+ * module goes through here; a thrown transport error counts as status 0 and is
44
+ * retried like a 5xx. Returns the last response once a definitive (non-
45
+ * transient) status arrives or the retry budget is exhausted.
46
+ */
47
+ async function getWithRetry(deps, config, url, label) {
48
+ let last = { status: 0, body: null };
49
+ for (let attempt = 1; attempt <= config.retryAttempts; attempt++) {
50
+ try {
51
+ last = await deps.http(url, headers(deps.token));
52
+ }
53
+ catch (error) {
54
+ last = { status: 0, body: error instanceof Error ? error.message : String(error) };
55
+ }
56
+ if (!isTransientStatus(last.status))
57
+ return last;
58
+ if (attempt < config.retryAttempts) {
59
+ deps.logger.warn(`[per-job-green] ${label}: transient ${last.status} (attempt ${attempt}/${config.retryAttempts}); retrying in ${config.retryDelayMs}ms`);
60
+ await deps.sleep(config.retryDelayMs);
61
+ }
62
+ }
63
+ deps.logger.warn(`[per-job-green] ${label}: still ${last.status} after ${config.retryAttempts} attempt(s)`);
64
+ return last;
65
+ }
66
+ /** Find the newest push run of the configured workflow for `sha`. Returns null if none exists yet. */
67
+ async function resolveRun(deps, config, sha) {
68
+ const base = deps.apiBase ?? 'https://api.github.com';
69
+ const url = `${base}/repos/${config.owner}/${config.repo}/actions/runs?head_sha=${sha}&event=${config.event}&per_page=50`;
70
+ const res = await getWithRetry(deps, config, url, 'actions runs listing');
71
+ if (res.status !== 200)
72
+ return { run: null, status: res.status };
73
+ const runs = asArray(res.body, 'workflow_runs')
74
+ .filter((r) => r.path?.endsWith(`/${config.workflow}`) || r.name === config.workflow);
75
+ if (runs.length === 0)
76
+ return { run: null, status: res.status };
77
+ const newest = [...runs].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))[0] ?? null;
78
+ return { run: newest, status: res.status };
79
+ }
80
+ /** Enumerate every job of a completed run and collect the ones that are not `success`. */
81
+ async function evaluateRunJobs(deps, config, runId) {
82
+ const base = deps.apiBase ?? 'https://api.github.com';
83
+ const url = `${base}/repos/${config.owner}/${config.repo}/actions/runs/${runId}/jobs?per_page=100`;
84
+ const res = await getWithRetry(deps, config, url, `actions run ${runId} jobs`);
85
+ if (res.status !== 200) {
86
+ // Honest failure naming the endpoint: the retry budget is exhausted (or the
87
+ // status is definitively non-200), so the per-job conclusions are unknowable.
88
+ return [`actions run ${runId} jobs endpoint returned ${res.status} after ${config.retryAttempts} attempt(s)`];
89
+ }
90
+ const jobs = asArray(res.body, 'jobs');
91
+ if (jobs.length === 0)
92
+ return ['no-jobs-reported'];
93
+ return jobs.filter((j) => j.conclusion !== 'success').map((j) => `${j.name} (${j.conclusion ?? j.status ?? 'unknown'})`);
94
+ }
95
+ /**
96
+ * Extract the Actions run id from a check-run's details_url. Actions-created
97
+ * check runs point at `https://github.com/{owner}/{repo}/actions/runs/{run_id}/job/{job_id}`.
98
+ */
99
+ export function runIdFromDetailsUrl(detailsUrl) {
100
+ if (!detailsUrl)
101
+ return null;
102
+ const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(detailsUrl);
103
+ return match?.[1] ? Number.parseInt(match[1], 10) : null;
104
+ }
105
+ /**
106
+ * Resolve the fallback run id to the TARGET workflow's run.
107
+ *
108
+ * Discriminator: neither the check-run nor the check-suite REST payload carries
109
+ * a workflow-file field, so each candidate run id (parsed from Actions
110
+ * details_urls) is confirmed against the single-run endpoint
111
+ * `GET /actions/runs/{id}`, whose `path` names the workflow file. A candidate
112
+ * is accepted only when its confirmed path matches `config.workflow`.
113
+ *
114
+ * Availability compromise: when the commit carries exactly ONE distinct
115
+ * candidate and its confirmation is UNAVAILABLE (any non-200 — i.e. not a
116
+ * CONFIRMED mismatch), the sole candidate is accepted: no ambiguity exists on
117
+ * the commit, and demanding confirmation during the very Actions-API outage the
118
+ * fallback exists for would gut it. With MULTIPLE candidates, confirmation is
119
+ * required — an unconfirmable set resolves to null (UNRESOLVED), never a guess.
120
+ */
121
+ async function resolveFallbackRunId(deps, config, candidateIds) {
122
+ const distinct = [...new Set(candidateIds)];
123
+ if (distinct.length === 0)
124
+ return null;
125
+ const base = deps.apiBase ?? 'https://api.github.com';
126
+ let unavailable = 0;
127
+ for (const id of distinct) {
128
+ const res = await getWithRetry(deps, config, `${base}/repos/${config.owner}/${config.repo}/actions/runs/${id}`, `actions run ${id} workflow confirmation`);
129
+ if (res.status !== 200) {
130
+ unavailable += 1;
131
+ continue;
132
+ }
133
+ const path = res.body?.path;
134
+ if (typeof path === 'string' && (path === config.workflow || path.endsWith(`/${config.workflow}`))) {
135
+ return id;
136
+ }
137
+ }
138
+ if (distinct.length === 1 && unavailable === 1) {
139
+ deps.logger.warn(`[per-job-green] sole fallback run candidate ${distinct[0]} accepted without workflow confirmation (confirmation endpoint unavailable; no ambiguity on this commit)`);
140
+ return distinct[0] ?? null;
141
+ }
142
+ return null;
143
+ }
144
+ /** Checks-API fallback: assess per-check conclusions for the commit when the Actions API is 503-ing. */
145
+ async function evaluateCheckSuites(deps, config, sha) {
146
+ const base = deps.apiBase ?? 'https://api.github.com';
147
+ const suitesUrl = `${base}/repos/${config.owner}/${config.repo}/commits/${sha}/check-suites`;
148
+ const suitesRes = await getWithRetry(deps, config, suitesUrl, 'check-suites');
149
+ if (suitesRes.status !== 200)
150
+ return null;
151
+ const suites = asArray(suitesRes.body, 'check_suites');
152
+ if (suites.length === 0)
153
+ return null;
154
+ if (suites.some((s) => s.status !== 'completed'))
155
+ return null; // still running — let the caller keep polling
156
+ const runsUrl = `${base}/repos/${config.owner}/${config.repo}/commits/${sha}/check-runs?per_page=100`;
157
+ const runsRes = await getWithRetry(deps, config, runsUrl, 'check-runs');
158
+ if (runsRes.status !== 200)
159
+ return null;
160
+ const checks = asArray(runsRes.body, 'check_runs');
161
+ const failures = checks
162
+ .filter((c) => c.conclusion !== 'success' && c.conclusion !== 'neutral' && c.conclusion !== 'skipped')
163
+ .map((c) => `${c.name} (${c.conclusion ?? 'unknown'})`);
164
+ // Downstream artifact restores need the Actions run id even on this path.
165
+ // Actions-created check runs carry it in details_url. Resolution must be
166
+ // workflow-filtered (a commit can carry check runs from several push
167
+ // workflows); see resolveFallbackRunId for the exact discriminator. When no
168
+ // candidate resolves to the target workflow, runId stays null and the caller
169
+ // must treat the handoff as unresolved (the bin surfaces run_id= empty;
170
+ // release pipelines hard-fail their artifact restore honestly instead of
171
+ // misdirecting it).
172
+ const candidates = checks.map((c) => runIdFromDetailsUrl(c.details_url)).filter((id) => id !== null);
173
+ const runId = await resolveFallbackRunId(deps, config, candidates);
174
+ const runIdNote = runId === null ? '; run id UNRESOLVED from check-run details' : `; run id ${runId} resolved from check-run details`;
175
+ return {
176
+ ok: failures.length === 0,
177
+ runId,
178
+ headSha: sha,
179
+ failures,
180
+ source: 'check-suites',
181
+ reason: failures.length === 0
182
+ ? `check-suites fallback: all ${checks.length} checks green for ${sha}${runIdNote}`
183
+ : `check-suites fallback: ${failures.length} check(s) not green`,
184
+ };
185
+ }
186
+ /**
187
+ * Verify `sha`'s push-CI run is per-job green. Polls until the run completes or
188
+ * the deadline passes. Deterministic under injected `http`/`sleep`/`now`.
189
+ */
190
+ export async function verifyPerJobGreen(deps, config, sha) {
191
+ const start = deps.now();
192
+ const deadline = start + config.deadlineMs;
193
+ while (true) {
194
+ const { run, status } = await resolveRun(deps, config, sha);
195
+ if (isTransientStatus(status)) {
196
+ // The runs listing stayed unavailable through its whole retry budget —
197
+ // try the Checks API before falling back to another poll cycle.
198
+ deps.logger.warn(`[per-job-green] Actions API unavailable (${status}) after retries — trying check-suites fallback for ${sha}`);
199
+ const fallback = await evaluateCheckSuites(deps, config, sha);
200
+ if (fallback)
201
+ return fallback;
202
+ deps.logger.warn('[per-job-green] check-suites fallback inconclusive; will retry');
203
+ }
204
+ else if (run) {
205
+ if (run.status === 'completed') {
206
+ if (run.conclusion !== 'success') {
207
+ // Rollup already failed — still enumerate jobs for an actionable message.
208
+ const failures = await evaluateRunJobs(deps, config, run.id);
209
+ return {
210
+ ok: false,
211
+ runId: run.id,
212
+ headSha: run.head_sha ?? sha,
213
+ failures: failures.length > 0 ? failures : [`run conclusion ${run.conclusion ?? 'unknown'}`],
214
+ source: 'actions',
215
+ reason: `run ${run.id} concluded ${run.conclusion ?? 'unknown'}`,
216
+ };
217
+ }
218
+ const failures = await evaluateRunJobs(deps, config, run.id);
219
+ return {
220
+ ok: failures.length === 0,
221
+ runId: run.id,
222
+ headSha: run.head_sha ?? sha,
223
+ failures,
224
+ source: 'actions',
225
+ reason: failures.length === 0
226
+ ? `run ${run.id} is per-job green for ${run.head_sha ?? sha}`
227
+ : `run ${run.id} has ${failures.length} non-green job(s)`,
228
+ };
229
+ }
230
+ deps.logger.info(`[per-job-green] run ${run.id} is ${run.status ?? 'pending'}; waiting`);
231
+ }
232
+ else {
233
+ deps.logger.info(`[per-job-green] no ${config.workflow} push run found yet for ${sha}; waiting`);
234
+ }
235
+ if (deps.now() >= deadline) {
236
+ return {
237
+ ok: false,
238
+ runId: run?.id ?? null,
239
+ headSha: run?.head_sha ?? null,
240
+ failures: ['deadline-exceeded'],
241
+ source: run ? 'actions' : 'none',
242
+ reason: `timed out after ${config.deadlineMs}ms waiting for ${config.workflow} to complete on ${sha}`,
243
+ };
244
+ }
245
+ await deps.sleep(config.pollIntervalMs);
246
+ }
247
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * post-build-smoke — proves a freshly compiled binary boots.
3
+ *
4
+ * Runs `<binary> --version`, asserts the expected banner prefix, and rejects
5
+ * output containing packaging-failure sentinels (e.g. `sqlite-vec`,
6
+ * `$bunfs/root`) that mean a native addon or module failed to bundle.
7
+ */
8
+ import type { Exec, ExecResult, Logger } from './effects.js';
9
+ import type { SmokeConfig } from '../config.js';
10
+ export interface SmokeResult {
11
+ readonly ok: boolean;
12
+ readonly detail: string;
13
+ }
14
+ /** Evaluate a captured `--version` run against the smoke policy. Pure. */
15
+ export declare function evaluateSmokeOutput(result: ExecResult, config: Pick<SmokeConfig, 'bannerPrefix' | 'forbiddenStrings'>): SmokeResult;
16
+ export interface RunSmokeOptions {
17
+ readonly binary: string;
18
+ readonly config: SmokeConfig;
19
+ readonly exec?: Exec;
20
+ readonly logger?: Logger;
21
+ }
22
+ /** Run the version smoke against a binary path. */
23
+ export declare function runPostBuildSmoke(options: RunSmokeOptions): SmokeResult;
24
+ //# sourceMappingURL=post-build-smoke.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-build-smoke.d.ts","sourceRoot":"","sources":["../../src/lib/post-build-smoke.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,GAAG,kBAAkB,CAAC,GAAG,WAAW,CAanI;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CAQvE"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * post-build-smoke — proves a freshly compiled binary boots.
3
+ *
4
+ * Runs `<binary> --version`, asserts the expected banner prefix, and rejects
5
+ * output containing packaging-failure sentinels (e.g. `sqlite-vec`,
6
+ * `$bunfs/root`) that mean a native addon or module failed to bundle.
7
+ */
8
+ import { realExec, consoleLogger } from './effects.js';
9
+ /** Evaluate a captured `--version` run against the smoke policy. Pure. */
10
+ export function evaluateSmokeOutput(result, config) {
11
+ const combined = `${result.stdout}\n${result.stderr}`;
12
+ if (result.status !== 0) {
13
+ return { ok: false, detail: `binary exited ${result.status}: ${combined.trim().slice(0, 400)}` };
14
+ }
15
+ const forbidden = config.forbiddenStrings.find((s) => combined.includes(s));
16
+ if (forbidden) {
17
+ return { ok: false, detail: `output contains packaging-failure sentinel "${forbidden}"` };
18
+ }
19
+ if (!result.stdout.trimStart().startsWith(config.bannerPrefix)) {
20
+ return { ok: false, detail: `version banner does not start with "${config.bannerPrefix}": ${result.stdout.trim().slice(0, 200)}` };
21
+ }
22
+ return { ok: true, detail: `version banner OK: ${result.stdout.trim().slice(0, 120)}` };
23
+ }
24
+ /** Run the version smoke against a binary path. */
25
+ export function runPostBuildSmoke(options) {
26
+ const exec = options.exec ?? realExec;
27
+ const logger = options.logger ?? consoleLogger;
28
+ logger.info(`[post-build-smoke] ${options.binary} --version`);
29
+ const result = exec(options.binary, ['--version']);
30
+ const evaluated = evaluateSmokeOutput(result, options.config);
31
+ (evaluated.ok ? logger.info : logger.error)(`[post-build-smoke] ${evaluated.detail}`);
32
+ return evaluated;
33
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * publish-package — idempotent npm publish + post-publish propagation poll.
3
+ *
4
+ * One implementation of the publish concern with a single set of constants.
5
+ * Idempotency: if the exact name@version is already on the registry, skip
6
+ * (re-runs are safe). Propagation poll: after publish, wait until the registry
7
+ * actually serves the new version before downstream jobs depend on it.
8
+ */
9
+ import type { Exec, Logger, Sleep } from './effects.js';
10
+ /** Query the published version of name@version; null when absent (or on error). */
11
+ export declare function getPublishedVersion(exec: Exec, name: string, version: string, registry: string): string | null;
12
+ export interface PublishOptions {
13
+ readonly cwd: string;
14
+ readonly name: string;
15
+ readonly version: string;
16
+ readonly registry?: string;
17
+ readonly dryRun?: boolean;
18
+ readonly exec?: Exec;
19
+ readonly logger?: Logger;
20
+ }
21
+ export interface PublishResult {
22
+ readonly ok: boolean;
23
+ readonly skipped: boolean;
24
+ readonly detail: string;
25
+ }
26
+ /** Publish the package in `cwd`, skipping if already published. */
27
+ export declare function runPublishPackage(options: PublishOptions): PublishResult;
28
+ export interface PropagationOptions {
29
+ readonly name: string;
30
+ readonly version: string;
31
+ readonly registry?: string;
32
+ readonly attempts?: number;
33
+ readonly delayMs?: number;
34
+ readonly exec?: Exec;
35
+ readonly sleep?: Sleep;
36
+ readonly logger?: Logger;
37
+ }
38
+ export interface PropagationResult {
39
+ readonly ok: boolean;
40
+ readonly attempts: number;
41
+ readonly detail: string;
42
+ }
43
+ /** Poll the registry until name@version resolves, or attempts are exhausted. */
44
+ export declare function pollPropagation(options: PropagationOptions): Promise<PropagationResult>;
45
+ //# sourceMappingURL=publish-package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-package.d.ts","sourceRoot":"","sources":["../../src/lib/publish-package.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAIxD,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9G;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CAsBxE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,gFAAgF;AAChF,wBAAsB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiB7F"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * publish-package — idempotent npm publish + post-publish propagation poll.
3
+ *
4
+ * One implementation of the publish concern with a single set of constants.
5
+ * Idempotency: if the exact name@version is already on the registry, skip
6
+ * (re-runs are safe). Propagation poll: after publish, wait until the registry
7
+ * actually serves the new version before downstream jobs depend on it.
8
+ */
9
+ import { realExec, consoleLogger, realSleep } from './effects.js';
10
+ import { DEFAULT_REGISTRY } from '../config.js';
11
+ /** Query the published version of name@version; null when absent (or on error). */
12
+ export function getPublishedVersion(exec, name, version, registry) {
13
+ const res = exec('npm', ['view', `${name}@${version}`, 'version', '--registry', registry]);
14
+ if (res.status !== 0)
15
+ return null;
16
+ const out = res.stdout.trim();
17
+ return out.length > 0 ? out : null;
18
+ }
19
+ /** Publish the package in `cwd`, skipping if already published. */
20
+ export function runPublishPackage(options) {
21
+ const exec = options.exec ?? realExec;
22
+ const logger = options.logger ?? consoleLogger;
23
+ const registry = options.registry ?? DEFAULT_REGISTRY;
24
+ if (options.dryRun) {
25
+ const res = exec('npm', ['pack', '--json'], { cwd: options.cwd });
26
+ return { ok: res.status === 0, skipped: false, detail: res.status === 0 ? 'dry-run pack ok' : `dry-run pack failed: ${res.stderr.trim()}` };
27
+ }
28
+ const already = getPublishedVersion(exec, options.name, options.version, registry);
29
+ if (already === options.version) {
30
+ logger.info(`[publish-package] ${options.name}@${options.version} already published — skipping`);
31
+ return { ok: true, skipped: true, detail: 'already published' };
32
+ }
33
+ const res = exec('npm', ['publish', '--access', 'public', '--registry', registry], { cwd: options.cwd });
34
+ if (res.status !== 0) {
35
+ return { ok: false, skipped: false, detail: `npm publish failed: ${res.stderr.trim().slice(0, 400)}` };
36
+ }
37
+ logger.info(`[publish-package] published ${options.name}@${options.version}`);
38
+ return { ok: true, skipped: false, detail: 'published' };
39
+ }
40
+ /** Poll the registry until name@version resolves, or attempts are exhausted. */
41
+ export async function pollPropagation(options) {
42
+ const exec = options.exec ?? realExec;
43
+ const sleep = options.sleep ?? realSleep;
44
+ const logger = options.logger ?? consoleLogger;
45
+ const registry = options.registry ?? DEFAULT_REGISTRY;
46
+ const attempts = options.attempts ?? 48;
47
+ const delayMs = options.delayMs ?? 5000;
48
+ for (let attempt = 1; attempt <= attempts; attempt++) {
49
+ const published = getPublishedVersion(exec, options.name, options.version, registry);
50
+ if (published === options.version) {
51
+ return { ok: true, attempts: attempt, detail: `${options.name}@${options.version} resolvable after ${attempt} attempt(s)` };
52
+ }
53
+ logger.info(`[publish-package] propagation ${attempt}/${attempts}: ${options.name}@${options.version} not yet resolvable`);
54
+ if (attempt < attempts)
55
+ await sleep(delayMs);
56
+ }
57
+ return { ok: false, attempts, detail: `${options.name}@${options.version} did not propagate within ${attempts} attempt(s)` };
58
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * release-cut — local release preparation ONLY.
3
+ *
4
+ * Per the CI/CD design's principle 4 (CI owns validation), this tool never
5
+ * re-runs gates. It: guards a clean tree on the release branch, bumps the
6
+ * version across the root and configured manifests, runs the repo's version-
7
+ * sync commands, prepends a CHANGELOG section, commits, and creates an
8
+ * annotated tag. Validation happened on the push CI run; the tag is cut from an
9
+ * already-green tree and verified by-reference downstream.
10
+ */
11
+ import type { Exec, Logger } from './effects.js';
12
+ import type { ReleaseCutConfig } from '../config.js';
13
+ export type BumpKind = 'patch' | 'minor' | 'major';
14
+ export interface Semver {
15
+ readonly major: number;
16
+ readonly minor: number;
17
+ readonly patch: number;
18
+ }
19
+ /** Parse an exact X.Y.Z string; throws on anything else. */
20
+ export declare function parseSemver(version: string): Semver;
21
+ /** Compute the next version for a bump kind. */
22
+ export declare function nextVersion(current: string, kind: BumpKind): string;
23
+ /** Render a CHANGELOG section heading for a version. */
24
+ export declare function changelogHeading(version: string, style: 'bracket' | 'plain', date: string): string;
25
+ /** Build a full CHANGELOG section body from a heading and notes/commit lines. */
26
+ export declare function buildChangelogSection(version: string, style: 'bracket' | 'plain', date: string, bodyLines: readonly string[]): string;
27
+ /**
28
+ * Insert a new section into changelog text. `top` prepends above the first
29
+ * `## ` heading; `first-separator` inserts after the first `---` line.
30
+ */
31
+ export declare function insertChangelogSection(changelog: string, section: string, marker: 'first-separator' | 'top'): string;
32
+ export interface ReleaseCutOptions {
33
+ readonly cwd: string;
34
+ readonly bump: BumpKind;
35
+ readonly config: ReleaseCutConfig;
36
+ readonly notes?: readonly string[];
37
+ readonly date?: string;
38
+ readonly dryRun?: boolean;
39
+ readonly exec?: Exec;
40
+ readonly logger?: Logger;
41
+ }
42
+ export interface ReleaseCutResult {
43
+ readonly version: string;
44
+ readonly tag: string;
45
+ readonly committed: boolean;
46
+ }
47
+ /**
48
+ * Perform a release cut. Uses real fs writes + the injected exec (git) against
49
+ * `cwd`; tests drive it against a temp-dir git fixture.
50
+ */
51
+ export declare function runReleaseCut(options: ReleaseCutOptions): ReleaseCutResult;
52
+ //# sourceMappingURL=release-cut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-cut.d.ts","sourceRoot":"","sources":["../../src/lib/release-cut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAInD;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAKnE;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElG;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAGrI;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAUpH;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAqBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA6C1E"}