@ever-works/github-actions-build-plugin 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,2329 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var contracts = require('@ever-works/contracts');
6
+ var octokit = require('octokit');
7
+ var fflate = require('fflate');
8
+ var module$1 = require('module');
9
+
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ // src/github-actions-build.plugin.ts
12
+
13
+ // src/runs/actions-runs.port.ts
14
+ function octokitActionsRunsPort(octokit) {
15
+ const asArray = (data, key) => {
16
+ const bag = data;
17
+ const list = bag?.[key];
18
+ return Array.isArray(list) ? list : [];
19
+ };
20
+ return {
21
+ async dispatchWorkflow({ repository, workflowFile, ref, inputs }) {
22
+ await octokit.request("POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", {
23
+ owner: repository.owner,
24
+ repo: repository.repo,
25
+ workflow_id: workflowFile,
26
+ ref,
27
+ inputs
28
+ });
29
+ },
30
+ async listWorkflowRuns({ repository, workflowFile, perPage, headSha, event }) {
31
+ const params = {
32
+ owner: repository.owner,
33
+ repo: repository.repo,
34
+ workflow_id: workflowFile,
35
+ per_page: perPage
36
+ };
37
+ if (headSha) params.head_sha = headSha;
38
+ if (event) params.event = event;
39
+ const { data } = await octokit.request(
40
+ "GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs",
41
+ params
42
+ );
43
+ return asArray(data, "workflow_runs");
44
+ },
45
+ async getWorkflowRun({ repository, runId }) {
46
+ try {
47
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
48
+ owner: repository.owner,
49
+ repo: repository.repo,
50
+ run_id: runId
51
+ });
52
+ return data ?? null;
53
+ } catch (error) {
54
+ if (error.status === 404) return null;
55
+ throw error;
56
+ }
57
+ },
58
+ async listRunJobs({ repository, runId }) {
59
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs", {
60
+ owner: repository.owner,
61
+ repo: repository.repo,
62
+ run_id: runId,
63
+ per_page: 100
64
+ });
65
+ return asArray(data, "jobs");
66
+ },
67
+ async cancelWorkflowRun({ repository, runId }) {
68
+ try {
69
+ await octokit.request("POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel", {
70
+ owner: repository.owner,
71
+ repo: repository.repo,
72
+ run_id: runId
73
+ });
74
+ } catch (error) {
75
+ if (error.status === 409) return;
76
+ throw error;
77
+ }
78
+ },
79
+ async listRunArtifacts({ repository, runId }) {
80
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts", {
81
+ owner: repository.owner,
82
+ repo: repository.repo,
83
+ run_id: runId,
84
+ per_page: 100
85
+ });
86
+ return asArray(data, "artifacts");
87
+ },
88
+ async downloadJobLogTail({ repository, jobId, maxBytes }) {
89
+ const response = await octokit.request("GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", {
90
+ owner: repository.owner,
91
+ repo: repository.repo,
92
+ job_id: jobId,
93
+ // A request, not a guarantee — the reader cuts again on arrival.
94
+ headers: { range: `bytes=-${maxBytes}` }
95
+ });
96
+ const data = response.data;
97
+ if (data === null || data === void 0) return null;
98
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
99
+ return { bytes, partial: response.status === 206 };
100
+ },
101
+ async downloadArtifactZip({ repository, artifactId }) {
102
+ const { data } = await octokit.request(
103
+ "GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}",
104
+ {
105
+ owner: repository.owner,
106
+ repo: repository.repo,
107
+ artifact_id: artifactId,
108
+ archive_format: "zip"
109
+ }
110
+ );
111
+ return new Uint8Array(data);
112
+ }
113
+ };
114
+ }
115
+
116
+ // src/runs/run-correlator.ts
117
+ var DISPATCH_CLOCK_SKEW_MS = 5e3;
118
+ var ADOPTION_WINDOW_MS = 3e5;
119
+ var CORRELATION_PAGE_SIZE = 20;
120
+ function titleNamesBuild(run, buildId) {
121
+ const title = (run.display_title ?? "").trim();
122
+ return title.length > 0 && buildId.length > 0 && title.includes(buildId);
123
+ }
124
+ function createdAtMs(run) {
125
+ const raw = run.created_at ?? run.run_started_at ?? null;
126
+ if (!raw) return null;
127
+ const at = Date.parse(raw);
128
+ return Number.isNaN(at) ? null : at;
129
+ }
130
+ async function correlateDispatchedRun(port, input) {
131
+ const runs = await port.listWorkflowRuns({
132
+ repository: input.repository,
133
+ workflowFile: input.workflowFile,
134
+ perPage: CORRELATION_PAGE_SIZE
135
+ });
136
+ const named = runs.filter((run) => titleNamesBuild(run, input.buildId));
137
+ if (named.length === 0) {
138
+ return { providerRunId: null, reason: "noMatch" };
139
+ }
140
+ const earliest = input.dispatchedAtMs - DISPATCH_CLOCK_SKEW_MS;
141
+ const latest = input.dispatchedAtMs + ADOPTION_WINDOW_MS;
142
+ for (const run of named) {
143
+ const created = createdAtMs(run);
144
+ if (created === null) continue;
145
+ if (created >= earliest && created <= latest) {
146
+ return { providerRunId: String(run.id) };
147
+ }
148
+ }
149
+ return { providerRunId: null, reason: "outsideWindow" };
150
+ }
151
+ var BUILD_JOB_NAME = "build";
152
+ function isCheckJob(job) {
153
+ return (job.name ?? "").startsWith(contracts.APP_BUILD_CHECK_NAME_PREFIX);
154
+ }
155
+ function jobBillableMinutes(job) {
156
+ if (!job.started_at || !job.completed_at) return 0;
157
+ const started = Date.parse(job.started_at);
158
+ const completed = Date.parse(job.completed_at);
159
+ if (Number.isNaN(started) || Number.isNaN(completed) || completed <= started) return 0;
160
+ return Math.ceil((completed - started) / 6e4);
161
+ }
162
+ function toBuildRunStatus(status, conclusion) {
163
+ if (status !== "completed") {
164
+ return status === "in_progress" ? "running" : "queued";
165
+ }
166
+ switch (conclusion) {
167
+ case "success":
168
+ return "succeeded";
169
+ case "cancelled":
170
+ return "cancelled";
171
+ case null:
172
+ case void 0:
173
+ return "running";
174
+ default:
175
+ return "failed";
176
+ }
177
+ }
178
+ function toTrigger(run, mode) {
179
+ if (run.event === "push") return "push";
180
+ if (run.event === "pull_request") return "pull_request";
181
+ return mode === "verify" ? "verification" : "manual";
182
+ }
183
+ function failingStep(job) {
184
+ const step = (job?.steps ?? []).find(
185
+ (candidate) => candidate.conclusion === "failure" || candidate.conclusion === "timed_out"
186
+ );
187
+ if (!step) return null;
188
+ return { name: (step.name ?? "").trim(), number: Number(step.number ?? 0) };
189
+ }
190
+ function observeRun({ run, jobs, mode }) {
191
+ const buildJob2 = jobs.find((job) => job.name === BUILD_JOB_NAME);
192
+ const checkJobs = jobs.filter(isCheckJob);
193
+ const status = buildJob2 ? toBuildRunStatus(buildJob2.status ?? null, buildJob2.conclusion ?? null) : toBuildRunStatus(run.status ?? null, run.conclusion ?? null);
194
+ const billableMinutes = jobs.reduce((total, job) => total + jobBillableMinutes(job), 0);
195
+ const checksBillableMinutes = checkJobs.reduce((total, job) => total + jobBillableMinutes(job), 0);
196
+ const pullRequestNumber = (run.pull_requests ?? []).find((pr) => typeof pr?.number === "number")?.number;
197
+ const snapshot = {
198
+ providerRunId: String(run.id),
199
+ runAttempt: Number(run.run_attempt ?? 1),
200
+ status,
201
+ trigger: toTrigger(run, mode),
202
+ branch: run.head_branch ?? "",
203
+ // A pull-request run's `head_sha` IS the PR head — the commit the member
204
+ // pushed, and the one the image is built from.
205
+ commitSha: run.head_sha ?? ""
206
+ };
207
+ const conclusion = buildJob2 ? buildJob2.conclusion : run.conclusion;
208
+ if (conclusion) snapshot.conclusion = conclusion;
209
+ if (typeof pullRequestNumber === "number") snapshot.pullRequestNumber = pullRequestNumber;
210
+ if (run.run_started_at) snapshot.startedAt = run.run_started_at;
211
+ if (run.status === "completed" && run.updated_at) snapshot.completedAt = run.updated_at;
212
+ if (billableMinutes > 0) snapshot.billableMinutes = billableMinutes;
213
+ if (checkJobs.length > 0) snapshot.checksBillableMinutes = checksBillableMinutes;
214
+ const runnerLabel = (buildJob2?.labels ?? []).find((label) => label.trim().length > 0);
215
+ if (runnerLabel) snapshot.runnerLabel = runnerLabel;
216
+ if (run.html_url) snapshot.logsUrl = run.html_url;
217
+ return snapshot;
218
+ }
219
+ var RESULT_ARTIFACT_MAX_ZIP_BYTES = 65536;
220
+ var RESULT_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
221
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set(["digest", "imageRepository", "tags", "secretCheck", "smoke"]);
222
+ function parseResultArtifact(value) {
223
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
224
+ return { ok: false, refusal: "malformedJson" };
225
+ }
226
+ const bag = value;
227
+ const unknown = Object.keys(bag).filter((key) => !ALLOWED_KEYS.has(key));
228
+ if (unknown.length > 0) {
229
+ return { ok: false, refusal: "unknownKeys" };
230
+ }
231
+ const digest = typeof bag.digest === "string" ? bag.digest.trim() : "";
232
+ if (!RESULT_DIGEST_PATTERN.test(digest)) {
233
+ return { ok: false, refusal: "badDigest" };
234
+ }
235
+ const result = { digest };
236
+ if (typeof bag.imageRepository === "string" && bag.imageRepository.trim().length > 0) {
237
+ result.imageRepository = bag.imageRepository.trim();
238
+ }
239
+ if (Array.isArray(bag.tags)) {
240
+ result.tags = bag.tags.filter((tag) => typeof tag === "string");
241
+ }
242
+ if (bag.secretCheck === "passed" || bag.secretCheck === "failed" || bag.secretCheck === "not_needed") {
243
+ result.secretCheck = bag.secretCheck;
244
+ }
245
+ if (Array.isArray(bag.smoke)) {
246
+ result.smoke = bag.smoke.filter((row) => typeof row === "object" && row !== null).map((row) => ({ name: String(row.name ?? ""), passed: row.passed === true }));
247
+ }
248
+ return { ok: true, result };
249
+ }
250
+ async function readResultArtifact(port, input) {
251
+ const artifacts = await port.listRunArtifacts(input);
252
+ const artifact = artifacts.find((candidate) => candidate.name === contracts.APP_BUILD_RESULT_ARTIFACT_NAME);
253
+ if (!artifact) {
254
+ return { ok: false, refusal: "absent" };
255
+ }
256
+ if (artifact.expired === true) {
257
+ return { ok: false, refusal: "expired" };
258
+ }
259
+ if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes > RESULT_ARTIFACT_MAX_ZIP_BYTES) {
260
+ return { ok: false, refusal: "zipTooLarge" };
261
+ }
262
+ const zip = await port.downloadArtifactZip({
263
+ repository: input.repository,
264
+ artifactId: artifact.id
265
+ });
266
+ if (zip.byteLength > RESULT_ARTIFACT_MAX_ZIP_BYTES) {
267
+ return { ok: false, refusal: "zipTooLarge" };
268
+ }
269
+ let entries;
270
+ try {
271
+ entries = fflate.unzipSync(zip);
272
+ } catch {
273
+ return { ok: false, refusal: "malformedJson" };
274
+ }
275
+ const entry = entries[contracts.APP_BUILD_RESULT_ARTIFACT_FILE];
276
+ if (!entry) {
277
+ return { ok: false, refusal: "entryMissing" };
278
+ }
279
+ if (entry.byteLength > contracts.APP_BUILD_RESULT_MAX_BYTES) {
280
+ return { ok: false, refusal: "entryTooLarge" };
281
+ }
282
+ let decoded;
283
+ try {
284
+ decoded = JSON.parse(new TextDecoder().decode(entry));
285
+ } catch {
286
+ return { ok: false, refusal: "malformedJson" };
287
+ }
288
+ return parseResultArtifact(decoded);
289
+ }
290
+
291
+ // src/runs/failure-classifier.ts
292
+ var EXCERPT_MAX_LINES = 20;
293
+ var EXCERPT_MAX_LINE_CHARS = 300;
294
+ var MISSING_VALUE = /EW_MISSING:([A-Za-z0-9_]+)/g;
295
+ var SECRET_IN_IMAGE = /EW_SECRET_IN_IMAGE:([A-Za-z0-9_]+)/g;
296
+ var GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
297
+ var BUILDKIT_STEP = /^#\d+\s+\[([^\]]*?)\s+(\d+)\/(\d+)\]\s+(.*)$/;
298
+ var SIGNALS = [
299
+ { cls: "timeout", patterns: [/exceeded the maximum execution time/i] },
300
+ {
301
+ cls: "outOfMemory",
302
+ patterns: [
303
+ /exit code:\s*137/i,
304
+ /^\s*Killed\s*$/im,
305
+ /JavaScript heap out of memory/i,
306
+ /Reached heap limit/i,
307
+ /\bENOMEM\b/i
308
+ ]
309
+ },
310
+ { cls: "diskFull", patterns: [/No space left on device/i, /\bENOSPC\b/i] },
311
+ {
312
+ cls: "dependencyDownloadFailed",
313
+ patterns: [
314
+ /\bETIMEDOUT\b/i,
315
+ /\bECONNRESET\b/i,
316
+ /\bEAI_AGAIN\b/i,
317
+ /TLS handshake timeout/i,
318
+ /429 Too Many Requests/i,
319
+ /\btoomanyrequests\b/i
320
+ ]
321
+ },
322
+ { cls: "dockerfileError", patterns: [/ERROR: failed to solve/i, /dockerfile parse error/i] }
323
+ ];
324
+ function namesIn(log, pattern) {
325
+ const names = /* @__PURE__ */ new Set();
326
+ for (const match of log.matchAll(pattern)) {
327
+ if (match[1]) names.add(match[1]);
328
+ }
329
+ return [...names];
330
+ }
331
+ function lineIndexOf(lines, patterns) {
332
+ for (let index = 0; index < lines.length; index += 1) {
333
+ if (patterns.some((pattern) => pattern.test(lines[index]))) return index;
334
+ }
335
+ return -1;
336
+ }
337
+ function buildExcerpt(lines, matchedIndex, redact) {
338
+ const end = matchedIndex >= 0 ? matchedIndex + 1 : lines.length;
339
+ const start = Math.max(0, end - EXCERPT_MAX_LINES);
340
+ return lines.slice(start, end).map((line) => line.slice(0, EXCERPT_MAX_LINE_CHARS)).map((line) => maskSecretShapes(redact(line)));
341
+ }
342
+ function maskSecretShapes(line) {
343
+ return line.replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "***").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "***").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi, "$1 ***").replace(/\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|APIKEY|API_KEY)[A-Z0-9_]*)\s*[=:]\s*\S+/gi, "$1=***").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s:/@]+:[^\s:/@]+@/gi, "$1***:***@").replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, (run) => GIT_OBJECT_ID.test(run) ? run : "***");
344
+ }
345
+ function classifyFailure(input, redact = (text) => text) {
346
+ const log = input.log ?? "";
347
+ const lines = log.split(/\r?\n/);
348
+ const missing = namesIn(log, MISSING_VALUE);
349
+ if (missing.length > 0) {
350
+ return {
351
+ class: "missingBuildValue",
352
+ detail: { names: missing },
353
+ excerpt: buildExcerpt(lines, lineIndexOf(lines, [/EW_MISSING:/]), redact)
354
+ };
355
+ }
356
+ const leaked = namesIn(log, SECRET_IN_IMAGE);
357
+ if (leaked.length > 0) {
358
+ return {
359
+ class: "secretInImage",
360
+ detail: { names: leaked },
361
+ excerpt: buildExcerpt(lines, lineIndexOf(lines, [/EW_SECRET_IN_IMAGE:/]), redact)
362
+ };
363
+ }
364
+ const timedOutByConclusion = input.jobConclusion === "timed_out";
365
+ const timedOutByMessage = lineIndexOf(lines, [/exceeded the maximum execution time/i]);
366
+ if (timedOutByConclusion || timedOutByMessage >= 0) {
367
+ return {
368
+ class: "timeout",
369
+ ...typeof input.minutes === "number" ? { detail: { minutes: input.minutes } } : {},
370
+ excerpt: buildExcerpt(lines, timedOutByMessage, redact)
371
+ };
372
+ }
373
+ if (input.runConclusion === "startup_failure" || input.jobCount === 0) {
374
+ return { class: "workflowInvalid", excerpt: buildExcerpt(lines, -1, redact) };
375
+ }
376
+ for (const { cls, patterns } of SIGNALS) {
377
+ if (cls === "timeout") continue;
378
+ const index = lineIndexOf(lines, patterns);
379
+ if (index < 0) continue;
380
+ if (cls === "dockerfileError") {
381
+ return {
382
+ class: "dockerfileError",
383
+ detail: dockerfileDetail(lines),
384
+ excerpt: buildExcerpt(lines, index, redact)
385
+ };
386
+ }
387
+ return { class: cls, excerpt: buildExcerpt(lines, index, redact) };
388
+ }
389
+ if ((input.failingStepName ?? "").toLowerCase().includes("verify in the runner") && input.verification && input.verification.failed > 0) {
390
+ return {
391
+ class: "verificationFailed",
392
+ detail: { failed: input.verification.failed, total: input.verification.total },
393
+ excerpt: buildExcerpt(lines, -1, redact)
394
+ };
395
+ }
396
+ return { class: "unknown", excerpt: buildExcerpt(lines, -1, redact) };
397
+ }
398
+ function dockerfileDetail(lines) {
399
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
400
+ const match = BUILDKIT_STEP.exec(lines[index].trim());
401
+ if (!match) continue;
402
+ return {
403
+ dockerfile: match[1],
404
+ step: Number(match[2]),
405
+ total: Number(match[3]),
406
+ command: match[4].slice(0, 120)
407
+ };
408
+ }
409
+ return {};
410
+ }
411
+
412
+ // src/runs/log-tail.ts
413
+ var LOG_TAIL_MAX_BYTES = 2 * 1024 * 1024;
414
+ async function readJobLogTail(port, input) {
415
+ if (typeof port.downloadJobLogTail !== "function") {
416
+ return "";
417
+ }
418
+ let tail;
419
+ try {
420
+ tail = await port.downloadJobLogTail({ ...input, maxBytes: LOG_TAIL_MAX_BYTES });
421
+ } catch {
422
+ return "";
423
+ }
424
+ if (!tail || tail.bytes.byteLength === 0) return "";
425
+ const bounded = tail.bytes.byteLength > LOG_TAIL_MAX_BYTES ? tail.bytes.slice(tail.bytes.byteLength - LOG_TAIL_MAX_BYTES) : tail.bytes;
426
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bounded);
427
+ if (!tail.partial) return text;
428
+ const firstBreak = text.indexOf("\n");
429
+ return firstBreak === -1 ? "" : text.slice(firstBreak + 1);
430
+ }
431
+
432
+ // src/runs/push-digest.ts
433
+ var PUSH_STEP_SCRIPT_FIRST_LINE = 'docker push --all-tags "$EW_IMAGE"';
434
+ var GROUP_PREFIX = "##[group]";
435
+ var PUSH_STEP_HEADER = `${GROUP_PREFIX}Run ${PUSH_STEP_SCRIPT_FIRST_LINE}`;
436
+ var RUNNER_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z /;
437
+ var PUSH_DIGEST_LINE = /^(\S+): digest: (sha256:[a-f0-9]{64}) size: \d+$/;
438
+ var FULL_SHA = /^[a-f0-9]{40}$/;
439
+ function extractPushLogDigest(log, sha) {
440
+ if (!FULL_SHA.test(sha) || log.length === 0) return void 0;
441
+ const tag = `sha-${sha}`;
442
+ const digests = /* @__PURE__ */ new Set();
443
+ let inPushSection = false;
444
+ for (const line of log.split(/\r?\n/).map(contentOf)) {
445
+ if (line.startsWith(GROUP_PREFIX)) {
446
+ inPushSection = line === PUSH_STEP_HEADER;
447
+ continue;
448
+ }
449
+ if (!inPushSection) continue;
450
+ const match = PUSH_DIGEST_LINE.exec(line);
451
+ if (match && match[1] === tag) digests.add(match[2]);
452
+ }
453
+ return digests.size === 1 ? [...digests][0] : void 0;
454
+ }
455
+ function contentOf(raw) {
456
+ return raw.replace(/^\uFEFF/, "").replace(RUNNER_TIMESTAMP, "").trimEnd();
457
+ }
458
+
459
+ // src/registry/ghcr-access.ts
460
+ var GITHUB_API_HOST = "https://api.github.com";
461
+ var GHCR_HOST = "https://ghcr.io";
462
+ var REQUIRED_PULL_TOKEN_SCOPE = "read:packages";
463
+ var GHCR_TOKEN_USERNAME = "x-access-token";
464
+ var MANIFEST_ACCEPT = [
465
+ "application/vnd.oci.image.index.v1+json",
466
+ "application/vnd.oci.image.manifest.v1+json",
467
+ "application/vnd.docker.distribution.manifest.list.v2+json",
468
+ "application/vnd.docker.distribution.manifest.v2+json"
469
+ ].join(", ");
470
+ async function checkPullToken(token, fetchImpl) {
471
+ const response = await fetchImpl(`${GITHUB_API_HOST}/user`, {
472
+ method: "GET",
473
+ headers: {
474
+ authorization: `Bearer ${token}`,
475
+ accept: "application/vnd.github+json",
476
+ "x-github-api-version": "2022-11-28"
477
+ },
478
+ redirect: "manual"
479
+ });
480
+ if (response.status === 401 || response.status === 403) {
481
+ return { ok: false, refusal: "pullTokenCannotRead" };
482
+ }
483
+ const raw = response.headers.get("x-oauth-scopes");
484
+ if (raw === null) {
485
+ return { ok: false, refusal: "pullTokenFineGrained" };
486
+ }
487
+ const scopes = raw.split(",").map((scope) => scope.trim()).filter((scope) => scope.length > 0);
488
+ if (scopes.length !== 1 || scopes[0] !== REQUIRED_PULL_TOKEN_SCOPE) {
489
+ return { ok: false, refusal: "pullTokenTooBroad" };
490
+ }
491
+ return { ok: true, expiresAt: response.headers.get("github-authentication-token-expiration") };
492
+ }
493
+ async function requestRegistryToken(name, fetchImpl, pullToken) {
494
+ const query = new URLSearchParams({ service: "ghcr.io", scope: `repository:${name}:pull` });
495
+ const headers = { accept: "application/json" };
496
+ if (pullToken) {
497
+ headers.authorization = `Basic ${Buffer.from(`${GHCR_TOKEN_USERNAME}:${pullToken}`, "utf-8").toString("base64")}`;
498
+ }
499
+ const response = await fetchImpl(`${GHCR_HOST}/token?${query.toString()}`, {
500
+ method: "GET",
501
+ headers,
502
+ redirect: "manual"
503
+ });
504
+ if (response.status !== 200) return null;
505
+ let body;
506
+ try {
507
+ body = await response.json();
508
+ } catch {
509
+ return null;
510
+ }
511
+ const candidate = body;
512
+ const token = candidate?.token ?? candidate?.access_token;
513
+ return typeof token === "string" && token.length > 0 ? token : null;
514
+ }
515
+ function headManifest(path, registryToken, fetchImpl) {
516
+ return fetchImpl(`${GHCR_HOST}${path}`, {
517
+ method: "HEAD",
518
+ headers: { accept: MANIFEST_ACCEPT, authorization: `Bearer ${registryToken}` },
519
+ redirect: "manual"
520
+ });
521
+ }
522
+ async function checkImageAccess(input, fetchImpl) {
523
+ const reference = parseGhcrReference(input.imageRepository, input.tag);
524
+ if (!reference) {
525
+ return { visibility: "unknown", readable: false };
526
+ }
527
+ const path = registryManifestPath(reference);
528
+ const anonymousToken = await requestRegistryToken(reference.name, fetchImpl);
529
+ const anonymous = anonymousToken ? await headManifest(path, anonymousToken, fetchImpl) : null;
530
+ if (anonymous?.status === 200) {
531
+ const digest2 = anonymous.headers.get("docker-content-digest");
532
+ return {
533
+ visibility: "public",
534
+ readable: true,
535
+ ...digest2 ? { digest: digest2 } : {}
536
+ };
537
+ }
538
+ if (!input.pullToken) {
539
+ return { visibility: "private", readable: false };
540
+ }
541
+ const tokenCheck = await checkPullToken(input.pullToken, fetchImpl);
542
+ const registryToken = await requestRegistryToken(reference.name, fetchImpl, input.pullToken);
543
+ const authenticated = registryToken ? await headManifest(path, registryToken, fetchImpl) : null;
544
+ const readable = authenticated?.status === 200;
545
+ const digest = readable ? authenticated.headers.get("docker-content-digest") : null;
546
+ return {
547
+ visibility: "private",
548
+ readable,
549
+ tokenScopesOk: tokenCheck.ok,
550
+ tokenExpiresAt: tokenCheck.ok ? tokenCheck.expiresAt : null,
551
+ ...digest ? { digest } : {}
552
+ };
553
+ }
554
+ function parseGhcrReference(imageRepository, tag) {
555
+ const repository = (imageRepository ?? "").trim().toLowerCase();
556
+ const reference = (tag ?? "").trim();
557
+ if (repository.length === 0 || reference.length === 0) return null;
558
+ const prefix = "ghcr.io/";
559
+ if (!repository.startsWith(prefix)) return null;
560
+ const name = repository.slice(prefix.length);
561
+ if (!/^[a-z0-9._-]+(\/[a-z0-9._-]+)+$/.test(name)) return null;
562
+ if (!/^[A-Za-z0-9._-]+$/.test(reference)) return null;
563
+ return { name, tag: reference };
564
+ }
565
+ function registryManifestPath(reference) {
566
+ return `/v2/${reference.name}/manifests/${reference.tag}`;
567
+ }
568
+ var RUNNER_TOO_SMALL = "runnerTooSmall";
569
+ function largerRunnerOf(input) {
570
+ const label = (input.settings?.largerRunnerLabel ?? "").trim();
571
+ const memoryGiB = Number(input.settings?.largerRunnerMemoryGiB ?? 0);
572
+ if (label.length === 0 || !Number.isFinite(memoryGiB) || memoryGiB <= 0) {
573
+ return null;
574
+ }
575
+ const vcpu = Number(input.settings?.largerRunnerVcpu ?? 0);
576
+ return {
577
+ label,
578
+ runnerClass: "github-larger",
579
+ vcpu: Number.isFinite(vcpu) && vcpu > 0 ? vcpu : 0,
580
+ memoryGiB,
581
+ usableMemoryGiB: memoryGiB - contracts.APP_BUILD_RUNNER_HEADROOM_GIB
582
+ };
583
+ }
584
+ function hostedRunner(key) {
585
+ const runner = contracts.APP_BUILD_RUNNERS[key];
586
+ return {
587
+ label: runner.label,
588
+ runnerClass: runner.runnerClass,
589
+ vcpu: runner.vcpu,
590
+ memoryGiB: runner.memoryGiB,
591
+ usableMemoryGiB: contracts.appBuildRunnerCapacity(runner).memoryGiB
592
+ };
593
+ }
594
+ function selectRunner(input) {
595
+ const runner = input.visibility === "public" ? hostedRunner("githubPublic") : largerRunnerOf(input) ?? hostedRunner("githubPrivate");
596
+ const declaredMemory = input.resources?.memoryGiB;
597
+ const needsMemory = typeof declaredMemory === "number" && Number.isFinite(declaredMemory) && declaredMemory > 0;
598
+ if (needsMemory && declaredMemory > runner.usableMemoryGiB) {
599
+ return {
600
+ ok: false,
601
+ blocked: { reason: RUNNER_TOO_SMALL, needed: declaredMemory, max: runner.usableMemoryGiB }
602
+ };
603
+ }
604
+ const declaredVcpu = input.resources?.vcpu;
605
+ const overDeclaresCpu = typeof declaredVcpu === "number" && Number.isFinite(declaredVcpu) && runner.vcpu > 0 && declaredVcpu > runner.vcpu;
606
+ return {
607
+ ok: true,
608
+ runner: overDeclaresCpu ? { ...runner, cpuOverDeclared: { needed: declaredVcpu, max: runner.vcpu } } : runner
609
+ };
610
+ }
611
+ var RESOLVED_ON = "2026-09-18";
612
+ var ACTION_PINS = {
613
+ /** `actions/checkout@v7.0.1`. */
614
+ checkout: {
615
+ action: "actions/checkout",
616
+ tag: "v7.0.1",
617
+ sha: "3d3c42e5aac5ba805825da76410c181273ba90b1",
618
+ resolvedOn: RESOLVED_ON
619
+ },
620
+ /** `docker/setup-buildx-action@v4.4.1`. */
621
+ setupBuildx: {
622
+ action: "docker/setup-buildx-action",
623
+ tag: "v4.4.1",
624
+ sha: "f87e5991a6d7451dcb8d9637bfbc97413f497069",
625
+ resolvedOn: RESOLVED_ON
626
+ },
627
+ /** `docker/login-action@v4.6.0`. */
628
+ login: {
629
+ action: "docker/login-action",
630
+ tag: "v4.6.0",
631
+ sha: "dbcb813823bdd20940b903addbd779551569679f",
632
+ resolvedOn: RESOLVED_ON
633
+ },
634
+ /** `docker/build-push-action@v7.4.0`. */
635
+ buildPush: {
636
+ action: "docker/build-push-action",
637
+ tag: "v7.4.0",
638
+ sha: "c3c9e263c25d99ce0380d002d59b67737d91b0dc",
639
+ resolvedOn: RESOLVED_ON
640
+ },
641
+ /** `actions/upload-artifact@v7.0.1`. */
642
+ uploadArtifact: {
643
+ action: "actions/upload-artifact",
644
+ tag: "v7.0.1",
645
+ sha: "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a",
646
+ resolvedOn: RESOLVED_ON
647
+ }
648
+ };
649
+ var ACTION_PIN_SHA_PATTERN = /^[0-9a-f]{40}$/;
650
+ function actionPin(pin) {
651
+ return `${pin.action}@${pin.sha} # ${pin.tag}`;
652
+ }
653
+ function actionPinReference(pin) {
654
+ return `${pin.action}@${pin.sha}`;
655
+ }
656
+ function canonicalActionPins() {
657
+ const keys = Object.keys(ACTION_PINS).sort();
658
+ const canonical = {};
659
+ for (const key of keys) canonical[key] = actionPinReference(ACTION_PINS[key]);
660
+ return canonical;
661
+ }
662
+ var GENERATOR_RESERVED_LITERALS = [contracts.APP_BUILD_RESTRICTED_VALUE_LITERAL, contracts.APP_BUILD_CHECK_NAME_PREFIX];
663
+ var CANONICAL_INPUT_FIELDS = [
664
+ "generator",
665
+ "bootstrap",
666
+ "trackedBranch",
667
+ "repository",
668
+ "appSpecHash",
669
+ "build",
670
+ "values",
671
+ "runner",
672
+ "settings",
673
+ "pins",
674
+ "verifyEnabled",
675
+ "checks",
676
+ "verifyRunnerScriptSha256"
677
+ ];
678
+ function checkTimeoutMinutes(timeoutSeconds) {
679
+ return Math.max(1, Math.ceil(timeoutSeconds / 60));
680
+ }
681
+ function canonicalCheck(check) {
682
+ return {
683
+ name: check.name,
684
+ required: check.required,
685
+ timeoutMinutes: checkTimeoutMinutes(check.timeoutSeconds),
686
+ commandSha256: contracts.sha256Hex(check.command)
687
+ };
688
+ }
689
+ function canonicaliseWorkflowInputs(inputs) {
690
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
691
+ return {
692
+ ...inputs,
693
+ build: inputs.build === null ? null : {
694
+ ...inputs.build,
695
+ args: [...inputs.build.args].sort(
696
+ (left, right) => compare(left.name, right.name) || compare(left.value ?? "", right.value ?? "") || compare(left.fromEnv ?? "", right.fromEnv ?? "")
697
+ ),
698
+ services: [...inputs.build.services].map((service) => ({
699
+ ...service,
700
+ env: [...service.env].sort((left, right) => compare(left.name, right.name))
701
+ })).sort((left, right) => compare(left.name, right.name))
702
+ },
703
+ values: [...inputs.values].sort(
704
+ (left, right) => compare(left.name, right.name) || Number(left.secret) - Number(right.secret) || Number(left.fromBuildService) - Number(right.fromBuildService)
705
+ ),
706
+ checks: [...inputs.checks].sort((left, right) => compare(left.name, right.name)),
707
+ pins: canonicalActionPins()
708
+ };
709
+ }
710
+ function canonicalWorkflowJson(inputs) {
711
+ const canonical = canonicaliseWorkflowInputs(inputs);
712
+ const ordered = {};
713
+ for (const field of CANONICAL_INPUT_FIELDS) ordered[field] = canonical[field];
714
+ return JSON.stringify(ordered);
715
+ }
716
+ function computeWorkflowInputsHash(inputs) {
717
+ return `sha256:${contracts.sha256Hex(canonicalWorkflowJson(inputs))}`;
718
+ }
719
+
720
+ // src/workflow/checks-job.ts
721
+ var CHECK_COMMAND_B64_ENV = "EW_CHECK_COMMAND_B64";
722
+ function checkCommandB64(command) {
723
+ return Buffer.from(command, "utf8").toString("base64");
724
+ }
725
+ function checksJobName(name) {
726
+ return `${contracts.APP_BUILD_CHECK_NAME_PREFIX}${name}`;
727
+ }
728
+ function expressionStringLiteral(value) {
729
+ return `'${value.replace(/'/g, "''")}'`;
730
+ }
731
+ function checksJobCondition(trackedBranch) {
732
+ return [
733
+ `github.event_name == 'pull_request'`,
734
+ `github.event.pull_request.head.repo.full_name == github.repository`,
735
+ `github.event.pull_request.base.ref == ${expressionStringLiteral(trackedBranch)}`
736
+ ].join(" && ");
737
+ }
738
+ function checkMatrixRow(check) {
739
+ return {
740
+ name: check.name,
741
+ required: check.required,
742
+ timeoutMinutes: checkTimeoutMinutes(check.timeoutSeconds),
743
+ commandB64: checkCommandB64(check.command)
744
+ };
745
+ }
746
+ function checksMatrix(checks) {
747
+ return checks.slice(0, contracts.APP_BUILD_CHECKS_MAX).map(checkMatrixRow);
748
+ }
749
+ function checksJob(input) {
750
+ const rows = checksMatrix(input.checks);
751
+ const lines = [];
752
+ lines.push(` ${contracts.APP_BUILD_CHECKS_JOB_ID}:`);
753
+ lines.push(` if: ${checksJobCondition(input.trackedBranch)}`);
754
+ lines.push(` name: ${yamlString(checksJobName("${{ matrix.check.name }}"))}`);
755
+ lines.push(` runs-on: ${yamlString(input.runnerLabel)}`);
756
+ lines.push(` timeout-minutes: \${{ matrix.check.timeoutMinutes }}`);
757
+ lines.push(` continue-on-error: \${{ !matrix.check.required }}`);
758
+ lines.push(" permissions: { contents: read }");
759
+ lines.push(" strategy:");
760
+ lines.push(" fail-fast: false");
761
+ lines.push(` max-parallel: ${contracts.APP_BUILD_CHECKS_MAX_PARALLEL}`);
762
+ lines.push(" matrix:");
763
+ lines.push(" check:");
764
+ for (const row of rows) {
765
+ lines.push(
766
+ ` - { name: ${yamlString(row.name)}, required: ${row.required}, timeoutMinutes: ${row.timeoutMinutes}, commandB64: ${yamlString(row.commandB64)} }`
767
+ );
768
+ }
769
+ lines.push(" steps:");
770
+ lines.push(` - uses: ${actionPin(ACTION_PINS.checkout)}`);
771
+ lines.push(" with:");
772
+ lines.push(
773
+ ` ref: ${yamlString("${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}")}`
774
+ );
775
+ lines.push(" fetch-depth: 1");
776
+ lines.push(" persist-credentials: false");
777
+ lines.push(" lfs: false");
778
+ lines.push(" - name: Run check");
779
+ lines.push(" env:");
780
+ lines.push(` ${CHECK_COMMAND_B64_ENV}: ${yamlString("${{ matrix.check.commandB64 }}")}`);
781
+ lines.push(
782
+ ` run: printf '%s' "$${CHECK_COMMAND_B64_ENV}" | base64 -d > "$RUNNER_TEMP/ew-check.sh" && bash -e "$RUNNER_TEMP/ew-check.sh"`
783
+ );
784
+ return lines;
785
+ }
786
+
787
+ // src/workflow/verify-runner.sh.ts
788
+ var EMBEDDED_VERIFY_RUNNER_SCRIPT = [
789
+ "set -euo pipefail",
790
+ 'echo "EW_VERIFY_NOT_IMPLEMENTED: the runner verification script is not embedded yet (APW-05 T15, plan \xA74.10)." >&2',
791
+ "exit 70"
792
+ ].join("\n");
793
+
794
+ // src/workflow/generator.ts
795
+ function yamlString(value) {
796
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(
797
+ /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g,
798
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
799
+ );
800
+ return `"${escaped}"`;
801
+ }
802
+ function scalarBlock(lines, indent) {
803
+ const pad = " ".repeat(indent);
804
+ return lines.map((line) => line.length === 0 ? "" : `${pad}${line}`);
805
+ }
806
+ function branchSlug(branch) {
807
+ return branch.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-{2,}/g, "-").slice(0, contracts.APP_BUILD_BRANCH_SLUG_MAX_CHARS);
808
+ }
809
+ function buildImageRepository(repository) {
810
+ return `ghcr.io/${repository.owner.toLowerCase()}/${repository.repo.toLowerCase()}/${contracts.APP_BUILD_IMAGE_NAME}`;
811
+ }
812
+ function secretNamesFor(values) {
813
+ return values.filter((value) => value.secret && !value.fromBuildService).map((value) => `${contracts.APP_BUILD_SECRET_PREFIX}${value.name}`).sort();
814
+ }
815
+ function fromEnvNames(build) {
816
+ return build.args.map((arg) => arg.fromEnv).filter((name) => typeof name === "string");
817
+ }
818
+ function normaliseBuildBlock(build) {
819
+ return {
820
+ strategy: build.strategy,
821
+ context: build.context?.trim() || ".",
822
+ dockerfile: build.dockerfile?.trim() || "Dockerfile",
823
+ ...build.target?.trim() ? { target: build.target.trim() } : {},
824
+ args: build.args.map((arg) => ({
825
+ name: arg.name,
826
+ ...arg.value === void 0 ? {} : { value: arg.value },
827
+ ...arg.fromEnv === void 0 ? {} : { fromEnv: arg.fromEnv }
828
+ })),
829
+ services: build.services.map((service) => ({
830
+ name: service.name,
831
+ image: service.image,
832
+ ...service.port === void 0 ? {} : { port: service.port },
833
+ env: (service.env ?? []).map((entry) => ({ name: entry.name, value: entry.value }))
834
+ })),
835
+ resources: {
836
+ cpu: build.resources.cpu,
837
+ ...build.resources.memoryGiB === void 0 ? {} : { memoryGiB: build.resources.memoryGiB },
838
+ timeoutMinutes: build.resources.timeoutMinutes
839
+ }
840
+ };
841
+ }
842
+ function canonicalInputsFor(input, verifyRunnerScript) {
843
+ const bootstrap = input.bootstrap === true;
844
+ const build = input.build ? normaliseBuildBlock(input.build) : null;
845
+ return {
846
+ generator: 1,
847
+ bootstrap,
848
+ trackedBranch: input.trackedBranch,
849
+ repository: input.repository,
850
+ appSpecHash: input.appSpecHash,
851
+ build: bootstrap ? null : build,
852
+ values: input.values.map((value) => ({
853
+ name: value.name,
854
+ secret: value.secret,
855
+ fromBuildService: value.fromBuildService
856
+ })),
857
+ runner: input.runner,
858
+ settings: {
859
+ reclaimDisk: input.settings?.reclaimDisk !== false,
860
+ // Plan §4.5/§4.4: attestations is **effective** — a private repository
861
+ // never gets the permissions, whatever the setting says (`APW05-G07`).
862
+ attestations: input.settings?.attestations === true && input.repository.visibility === "public",
863
+ allowBuildValuesOnPullRequests: input.settings?.allowBuildValuesOnPullRequests === true,
864
+ verificationPromptedValuesRequireApproval: input.settings?.verificationPromptedValuesRequireApproval !== false
865
+ },
866
+ pins: {},
867
+ verifyEnabled: input.verifyEnabled,
868
+ checks: (input.checks ?? []).slice(0, contracts.APP_BUILD_CHECKS_MAX).map(canonicalCheck),
869
+ verifyRunnerScriptSha256: contracts.sha256Hex(verifyRunnerScript)
870
+ };
871
+ }
872
+ function workflowHeader(inputsHash) {
873
+ return [
874
+ "# Generated by Ever Works from .works/works.yml. Do not edit: hand edits are never overwritten;",
875
+ "# Ever Works proposes changes to this file as pull requests.",
876
+ `# ever-works-build generator=1 inputs=${inputsHash}`
877
+ ];
878
+ }
879
+ function checksOnlyFile(input) {
880
+ if (input.bootstrap === true) return false;
881
+ const strategy = input.build?.strategy ?? null;
882
+ return strategy === null || strategy === "image" || strategy === "none";
883
+ }
884
+ function generateWorkflow(input) {
885
+ const verifyRunnerScript = input.verifyRunnerScript ?? EMBEDDED_VERIFY_RUNNER_SCRIPT;
886
+ const canonical = canonicalInputsFor(input, verifyRunnerScript);
887
+ const inputHash = computeWorkflowInputsHash(canonical);
888
+ const build = canonical.build;
889
+ const bootstrap = canonical.bootstrap;
890
+ const slug = branchSlug(canonical.trackedBranch);
891
+ const image = buildImageRepository(canonical.repository);
892
+ const runnerLabel = canonical.runner.label;
893
+ const secretNames = secretNamesFor(input.values);
894
+ const checks = bootstrap ? [] : input.checks ?? [];
895
+ if (checksOnlyFile(input)) {
896
+ if (checks.length === 0) return "";
897
+ return checksOnlyWorkflow({ inputHash, trackedBranch: canonical.trackedBranch, checks, runnerLabel });
898
+ }
899
+ const lines = [...workflowHeader(inputHash)];
900
+ lines.push("name: Ever Works build");
901
+ lines.push(
902
+ `run-name: ${yamlString("Ever Works build ${{ inputs.ew_build_id || github.event.pull_request.head.sha || github.sha }}")}`
903
+ );
904
+ lines.push("on:");
905
+ if (!bootstrap) {
906
+ lines.push(" push:");
907
+ lines.push(` branches: [${yamlString(canonical.trackedBranch)}]`);
908
+ lines.push(" pull_request:");
909
+ lines.push(` branches: [${yamlString(canonical.trackedBranch)}]`);
910
+ lines.push(" types: [opened, synchronize, reopened]");
911
+ }
912
+ lines.push(" workflow_dispatch:");
913
+ lines.push(" inputs:");
914
+ lines.push(" ew_build_id:");
915
+ lines.push(" type: string");
916
+ lines.push(" required: true");
917
+ lines.push(" ew_sha:");
918
+ lines.push(" type: string");
919
+ lines.push(" required: true");
920
+ lines.push(" ew_mode:");
921
+ lines.push(" type: choice");
922
+ lines.push(" options: [build, verify]");
923
+ lines.push(" default: build");
924
+ lines.push(` ${contracts.APP_BUILD_VERIFY_PLAN_INPUT}:`);
925
+ lines.push(" type: string");
926
+ lines.push(" required: false");
927
+ lines.push(` default: ${yamlString("")}`);
928
+ lines.push(" ew_reuse_digest:");
929
+ lines.push(" type: string");
930
+ lines.push(" required: false");
931
+ lines.push(` default: ${yamlString("")}`);
932
+ lines.push("permissions: {}");
933
+ lines.push(...concurrencyLines("dispatch"));
934
+ lines.push("jobs:");
935
+ if (!bootstrap && build) {
936
+ lines.push(...buildJob({ canonical, build, image, slug, runnerLabel, secretNames, verifyRunnerScript }));
937
+ }
938
+ lines.push(...verifyJob({ build, image, runnerLabel, verifyRunnerScript }));
939
+ if (checks.length > 0) {
940
+ lines.push(...checksJob({ checks, trackedBranch: canonical.trackedBranch, runnerLabel }));
941
+ }
942
+ return `${lines.join("\n")}
943
+ `;
944
+ }
945
+ function concurrencyLines(shape) {
946
+ if (shape === "checksOnly") {
947
+ return [
948
+ "concurrency:",
949
+ ` group: ${yamlString(
950
+ "ever-works-build-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('push-{0}', github.sha) }}"
951
+ )}`,
952
+ // `true` as the draft has it: a checks-only file runs for pull requests
953
+ // only, so a newer run of the same pull request always supersedes the one
954
+ // in flight — which is what the full file's `github.event_name ==
955
+ // 'pull_request'` expression amounts to for that trigger set.
956
+ " cancel-in-progress: true"
957
+ ];
958
+ }
959
+ return [
960
+ "concurrency:",
961
+ ` group: ${yamlString(
962
+ "${{ inputs.ew_mode == 'verify' && format('verify-{0}', inputs.ew_build_id) || format('ever-works-build-{0}', github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name) }}"
963
+ )}`,
964
+ ` cancel-in-progress: \${{ github.event_name == 'pull_request' }}`
965
+ ];
966
+ }
967
+ function checksOnlyWorkflow(input) {
968
+ const lines = [...workflowHeader(input.inputHash)];
969
+ lines.push("name: Ever Works build");
970
+ lines.push("on:");
971
+ lines.push(" pull_request:");
972
+ lines.push(` branches: [${yamlString(input.trackedBranch)}]`);
973
+ lines.push(" types: [opened, synchronize, reopened]");
974
+ lines.push("permissions: {}");
975
+ lines.push(...concurrencyLines("checksOnly"));
976
+ lines.push("jobs:");
977
+ lines.push(
978
+ ...checksJob({
979
+ checks: input.checks,
980
+ trackedBranch: input.trackedBranch,
981
+ runnerLabel: input.runnerLabel
982
+ })
983
+ );
984
+ return `${lines.join("\n")}
985
+ `;
986
+ }
987
+ function buildJob(job) {
988
+ const { canonical, build, image, slug, runnerLabel, secretNames } = job;
989
+ const lines = [];
990
+ const attestations = canonical.settings.attestations;
991
+ const fromEnv = build.args.map((arg) => arg.fromEnv).filter((name) => typeof name === "string");
992
+ const restricted = !canonical.settings.allowBuildValuesOnPullRequests;
993
+ lines.push(" build:");
994
+ lines.push(
995
+ ` if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && inputs.ew_mode != 'verify'`
996
+ );
997
+ lines.push(` runs-on: ${yamlString(runnerLabel)}`);
998
+ lines.push(` timeout-minutes: ${build.resources.timeoutMinutes}`);
999
+ lines.push(
1000
+ ` permissions: { contents: read, packages: write${attestations ? ", attestations: write, id-token: write" : ""} }`
1001
+ );
1002
+ if (build.services.length > 0) {
1003
+ lines.push(" services:");
1004
+ for (const service of build.services) lines.push(...serviceLines(service));
1005
+ }
1006
+ lines.push(" env:");
1007
+ lines.push(` EW_IMAGE: ${yamlString(image)}`);
1008
+ lines.push(
1009
+ ` EW_SHA: ${yamlString("${{ inputs.ew_sha || github.event.pull_request.head.sha || github.sha }}")}`
1010
+ );
1011
+ lines.push(` EW_SECRET_NAMES: ${yamlString(secretNames.join(" "))}`);
1012
+ lines.push(" steps:");
1013
+ if (fromEnv.length > 0) {
1014
+ lines.push(" - name: Check build values");
1015
+ if (restricted) lines.push(` if: github.event_name != 'pull_request'`);
1016
+ lines.push(" env:");
1017
+ for (const name of fromEnv) {
1018
+ lines.push(
1019
+ ` ${contracts.APP_BUILD_SECRET_PREFIX}${name}: ${yamlString(`\${{ secrets.${contracts.APP_BUILD_SECRET_PREFIX}${name} }}`)}`
1020
+ );
1021
+ }
1022
+ lines.push(" run: |");
1023
+ const checkNames = fromEnv.map((name) => `${contracts.APP_BUILD_SECRET_PREFIX}${name}`).join(" ");
1024
+ lines.push(
1025
+ ...scalarBlock(
1026
+ [
1027
+ "set -euo pipefail",
1028
+ `for name in ${checkNames}; do`,
1029
+ ' [ -n "${!name:-}" ] || { echo "EW_MISSING:${name#EW_}"; exit 78; }',
1030
+ "done"
1031
+ ],
1032
+ 10
1033
+ )
1034
+ );
1035
+ }
1036
+ lines.push(` - uses: ${actionPin(ACTION_PINS.checkout)}`);
1037
+ lines.push(" with:");
1038
+ lines.push(` ref: ${yamlString("${{ env.EW_SHA }}")}`);
1039
+ lines.push(" fetch-depth: 1");
1040
+ lines.push(" persist-credentials: false");
1041
+ lines.push(" lfs: false");
1042
+ if (canonical.settings.reclaimDisk) {
1043
+ lines.push(" - name: Reclaim runner disk");
1044
+ lines.push(
1045
+ " run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL"
1046
+ );
1047
+ }
1048
+ const waitPorts = build.services.filter((service) => contracts.resolveBuildServiceKind(service.image) === null).map((service) => service.port).filter((port) => typeof port === "number");
1049
+ if (waitPorts.length > 0) {
1050
+ lines.push(" - name: Wait for the build services");
1051
+ lines.push(" run: |");
1052
+ lines.push(
1053
+ ...scalarBlock(
1054
+ [
1055
+ "set -euo pipefail",
1056
+ `for port in ${waitPorts.join(" ")}; do`,
1057
+ " deadline=$((SECONDS + 60))",
1058
+ ' until (echo > "/dev/tcp/127.0.0.1/$port") 2>/dev/null; do',
1059
+ ' [ "$SECONDS" -lt "$deadline" ] || { echo "EW_SERVICE_TIMEOUT:$port"; exit 78; }',
1060
+ " done",
1061
+ "done"
1062
+ ],
1063
+ 10
1064
+ )
1065
+ );
1066
+ }
1067
+ lines.push(` - uses: ${actionPin(ACTION_PINS.setupBuildx)}`);
1068
+ if (build.services.length > 0) {
1069
+ lines.push(" with:");
1070
+ lines.push(` buildkitd-flags: ${yamlString("--allow-insecure-entitlement network.host")}`);
1071
+ }
1072
+ lines.push(` - uses: ${actionPin(ACTION_PINS.login)}`);
1073
+ lines.push(" with:");
1074
+ lines.push(" registry: ghcr.io");
1075
+ lines.push(` username: ${yamlString("${{ github.actor }}")}`);
1076
+ lines.push(` password: ${yamlString("${{ secrets.GITHUB_TOKEN }}")}`);
1077
+ lines.push(" - id: build");
1078
+ lines.push(` if: inputs.ew_mode != 'verify'`);
1079
+ lines.push(` uses: ${actionPin(ACTION_PINS.buildPush)}`);
1080
+ lines.push(" with:");
1081
+ lines.push(` context: ${yamlString(build.context)}`);
1082
+ lines.push(` file: ${yamlString(`${build.context}/${build.dockerfile}`)}`);
1083
+ if (build.target) lines.push(` target: ${yamlString(build.target)}`);
1084
+ lines.push(" load: true");
1085
+ lines.push(" push: false");
1086
+ lines.push(" provenance: false");
1087
+ if (build.services.length > 0) {
1088
+ lines.push(" network: host");
1089
+ lines.push(" allow: network.host");
1090
+ }
1091
+ if (build.args.length > 0) {
1092
+ lines.push(" build-args: |");
1093
+ lines.push(
1094
+ ...scalarBlock(
1095
+ build.args.map(
1096
+ (arg) => arg.fromEnv === void 0 ? `${arg.name}=${arg.value ?? ""}` : `${arg.name}=${restrictedValueExpression(arg.fromEnv, restricted)}`
1097
+ ),
1098
+ 12
1099
+ )
1100
+ );
1101
+ }
1102
+ lines.push(" tags: |");
1103
+ lines.push(
1104
+ ...scalarBlock(
1105
+ [
1106
+ `\${{ env.EW_IMAGE }}:sha-\${{ env.EW_SHA }}`,
1107
+ `\${{ env.EW_IMAGE }}:\${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'branch-${slug}' }}`
1108
+ ],
1109
+ 12
1110
+ )
1111
+ );
1112
+ lines.push(" labels: |");
1113
+ lines.push(
1114
+ ...scalarBlock(
1115
+ [
1116
+ "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}",
1117
+ "org.opencontainers.image.revision=${{ env.EW_SHA }}",
1118
+ `io.ever-works.app-spec-hash=${canonical.appSpecHash}`
1119
+ ],
1120
+ 12
1121
+ )
1122
+ );
1123
+ lines.push(" cache-from: type=registry,ref=${{ env.EW_IMAGE }}:buildcache");
1124
+ lines.push(
1125
+ ` cache-to: \${{ github.event_name == 'push' && format('type=registry,ref={0}:buildcache,mode=max', env.EW_IMAGE) || '' }}`
1126
+ );
1127
+ if (secretNames.length > 0) {
1128
+ lines.push(" - name: Check the image for secret build values");
1129
+ lines.push(" id: secretcheck");
1130
+ lines.push(` if: inputs.ew_mode != 'verify'`);
1131
+ lines.push(" env:");
1132
+ for (const name of secretNames) {
1133
+ lines.push(` ${name}: ${yamlString(`\${{ secrets.${name} }}`)}`);
1134
+ }
1135
+ lines.push(" run: |");
1136
+ lines.push(
1137
+ ...scalarBlock(
1138
+ [
1139
+ "set -euo pipefail",
1140
+ `docker image inspect "$EW_IMAGE:sha-$EW_SHA" > "$RUNNER_TEMP/ew-image.json"`,
1141
+ `docker history --no-trunc --format '{{.CreatedBy}}' "$EW_IMAGE:sha-$EW_SHA" >> "$RUNNER_TEMP/ew-image.json"`,
1142
+ "for name in $EW_SECRET_NAMES; do",
1143
+ ' value="${!name:-}"',
1144
+ ' [ "${#value}" -ge 8 ] || continue',
1145
+ ' if grep -qF -- "$value" "$RUNNER_TEMP/ew-image.json"; then echo "EW_SECRET_IN_IMAGE:${name#EW_}"; exit 79; fi',
1146
+ "done",
1147
+ 'echo "secret-check: passed"'
1148
+ ],
1149
+ 10
1150
+ )
1151
+ );
1152
+ }
1153
+ lines.push(" - name: Push");
1154
+ lines.push(` if: inputs.ew_mode != 'verify'`);
1155
+ lines.push(" run: |");
1156
+ lines.push(
1157
+ ...scalarBlock(
1158
+ [
1159
+ 'docker push --all-tags "$EW_IMAGE"',
1160
+ `docker image inspect --format '{{index .RepoDigests 0}}' "$EW_IMAGE:sha-$EW_SHA" > ew-digest.txt`
1161
+ ],
1162
+ 10
1163
+ )
1164
+ );
1165
+ lines.push(...verifyInRunnerStep(job.verifyRunnerScript));
1166
+ lines.push(...resultStep("build", secretNames.length > 0));
1167
+ lines.push(...uploadArtifactStep());
1168
+ return lines;
1169
+ }
1170
+ function restrictedValueExpression(name, restricted) {
1171
+ const secret = `secrets.${contracts.APP_BUILD_SECRET_PREFIX}${name}`;
1172
+ return restricted ? `\${{ github.event_name == 'pull_request' && '${contracts.APP_BUILD_RESTRICTED_VALUE_LITERAL}' || ${secret} }}` : `\${{ ${secret} }}`;
1173
+ }
1174
+ function serviceLines(service) {
1175
+ const kind = contracts.resolveBuildServiceKind(service.image);
1176
+ const lines = [` ${service.name}:`, ` image: ${yamlString(service.image)}`];
1177
+ const env = kind === null ? Object.fromEntries(service.env.map((entry) => [entry.name, entry.value])) : contracts.resolveBuildServiceEnv(kind, service.env);
1178
+ const names = Object.keys(env).sort();
1179
+ if (names.length > 0) {
1180
+ lines.push(" env:");
1181
+ for (const name of names) lines.push(` ${name}: ${yamlString(env[name])}`);
1182
+ }
1183
+ const port = contracts.evaluateBuildServicePort(service);
1184
+ if (!port.ok) {
1185
+ lines.push(" ports: []");
1186
+ return lines;
1187
+ }
1188
+ lines.push(` ports: [${yamlString(port.published)}]`);
1189
+ if (kind === null) return lines;
1190
+ const health = contracts.BUILD_SERVICE_DEFAULTS[kind].health;
1191
+ const command = health.kind === "command" ? health.command.replace("<user>", env.POSTGRES_USER ?? "").replace("<db>", env.POSTGRES_DB ?? "") : `curl -fsS http://localhost:${contracts.BUILD_SERVICE_DEFAULTS[kind].containerPort}${health.kind === "http" ? health.path : ""}`;
1192
+ lines.push(
1193
+ ` options: ${yamlString(
1194
+ `--health-cmd "${command}" --health-interval 10s --health-timeout 5s --health-retries 5`
1195
+ )}`
1196
+ );
1197
+ return lines;
1198
+ }
1199
+ function verifyJob(job) {
1200
+ const { build, image, runnerLabel, verifyRunnerScript } = job;
1201
+ const buildTimeout = build?.resources.timeoutMinutes ?? 0;
1202
+ const services = build?.services ?? [];
1203
+ const lines = [];
1204
+ lines.push(" verify:");
1205
+ lines.push(` if: github.event_name == 'workflow_dispatch' && inputs.ew_mode == 'verify'`);
1206
+ lines.push(` runs-on: ${yamlString(runnerLabel)}`);
1207
+ lines.push(` timeout-minutes: ${buildTimeout + contracts.APP_BUILD_VERIFY_TIMEOUT_MINUTES}`);
1208
+ lines.push(" permissions: { contents: read, packages: read }");
1209
+ lines.push(" env:");
1210
+ lines.push(` EW_IMAGE: ${yamlString(image)}`);
1211
+ lines.push(` EW_SHA: ${yamlString("${{ inputs.ew_sha }}")}`);
1212
+ lines.push(` EW_REUSE_DIGEST: ${yamlString("${{ inputs.ew_reuse_digest }}")}`);
1213
+ lines.push(" steps:");
1214
+ lines.push(` - uses: ${actionPin(ACTION_PINS.checkout)}`);
1215
+ lines.push(" with:");
1216
+ lines.push(` ref: ${yamlString("${{ inputs.ew_sha }}")}`);
1217
+ lines.push(" fetch-depth: 1");
1218
+ lines.push(" persist-credentials: false");
1219
+ lines.push(" lfs: false");
1220
+ lines.push(` - uses: ${actionPin(ACTION_PINS.setupBuildx)}`);
1221
+ if (services.length > 0) {
1222
+ lines.push(" with:");
1223
+ lines.push(` buildkitd-flags: ${yamlString("--allow-insecure-entitlement network.host")}`);
1224
+ }
1225
+ lines.push(` - uses: ${actionPin(ACTION_PINS.login)}`);
1226
+ lines.push(" with:");
1227
+ lines.push(" registry: ghcr.io");
1228
+ lines.push(` username: ${yamlString("${{ github.actor }}")}`);
1229
+ lines.push(` password: ${yamlString("${{ secrets.GITHUB_TOKEN }}")}`);
1230
+ lines.push(" - name: Take the image to verify");
1231
+ lines.push(" run: |");
1232
+ lines.push(
1233
+ ...scalarBlock(
1234
+ [
1235
+ "set -euo pipefail",
1236
+ 'plan="$RUNNER_TEMP/ew-verify-plan.json"',
1237
+ `plan_b64="$(printf '%s' "\${{ inputs.${contracts.APP_BUILD_VERIFY_PLAN_INPUT} }}" | tr '_-' '/+')"`,
1238
+ 'case $(( ${#plan_b64} % 4 )) in 2) plan_b64="${plan_b64}==" ;; 3) plan_b64="${plan_b64}=" ;; esac',
1239
+ `printf '%s' "$plan_b64" | base64 -d > "$plan"`,
1240
+ `printf 'EW_VERIFY_BUILD=%s\\n' "$(jq -r '.build // empty | @base64' "$plan")" >> "$GITHUB_ENV"`,
1241
+ 'if [ -n "$EW_REUSE_DIGEST" ]; then',
1242
+ ' docker pull "$EW_IMAGE@$EW_REUSE_DIGEST"',
1243
+ " exit 0",
1244
+ "fi",
1245
+ '[ -n "$EW_VERIFY_BUILD" ] || { echo "EW_VERIFY_NO_BUILD"; exit 78; }',
1246
+ `context="$(jq -r '.build.context // "."' "$plan")"`,
1247
+ `dockerfile="$(jq -r '.build.dockerfile // "Dockerfile"' "$plan")"`,
1248
+ `target="$(jq -r '.build.target // empty' "$plan")"`,
1249
+ 'buildx_args=(--load --file "$context/$dockerfile" --cache-from "$EW_IMAGE:buildcache" --tag ew-verify:local)',
1250
+ 'if [ -n "$target" ]; then buildx_args+=(--target "$target"); fi',
1251
+ "while IFS= read -r entry; do",
1252
+ ' [ -n "$entry" ] || continue',
1253
+ ' buildx_args+=(--build-arg "$entry")',
1254
+ `done < <(jq -r '(.build.args // [])[] | select(.value != null) | "\\(.name)=\\(.value)"' "$plan")`,
1255
+ 'docker buildx build "${buildx_args[@]}" "$context"'
1256
+ ],
1257
+ 10
1258
+ )
1259
+ );
1260
+ lines.push(...verifyInRunnerStep(verifyRunnerScript));
1261
+ lines.push(...resultStep("verify", false));
1262
+ lines.push(...uploadArtifactStep());
1263
+ return lines;
1264
+ }
1265
+ function verifyInRunnerStep(verifyRunnerScript) {
1266
+ return [
1267
+ " - name: Verify in the runner",
1268
+ ` if: inputs.${contracts.APP_BUILD_VERIFY_PLAN_INPUT} != ''`,
1269
+ ` timeout-minutes: ${contracts.APP_BUILD_VERIFY_TIMEOUT_MINUTES}`,
1270
+ " run: |",
1271
+ ...scalarBlock(verifyRunnerScript.replace(/\n+$/, "").split("\n"), 10)
1272
+ ];
1273
+ }
1274
+ function resultStep(job, secretCheckPresent) {
1275
+ const script = job === "build" ? [
1276
+ "set -euo pipefail",
1277
+ 'digest=""',
1278
+ `[ ! -f ew-digest.txt ] || digest="$(tr -d '[:space:]' < ew-digest.txt)"`,
1279
+ `secret_check="not_needed"`,
1280
+ ...secretCheckPresent ? [
1281
+ `if [ -n "$EW_SECRET_NAMES" ]; then`,
1282
+ ` secret_check="passed"`,
1283
+ ` [ "\${{ steps.secretcheck.outcome }}" != "failure" ] || secret_check="failed"`,
1284
+ `fi`
1285
+ ] : [],
1286
+ `jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
1287
+ ` --arg imageRepository "$EW_IMAGE" --arg digest "$digest" --arg secretCheck "$secret_check" \\`,
1288
+ ` --arg tag "$EW_IMAGE:sha-$EW_SHA" \\`,
1289
+ ` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,'`,
1290
+ ` digest:$digest,tags:[$tag],secretCheck:$secretCheck,verification:{jobs:[],smoke:[]}}' > ${contracts.APP_BUILD_RESULT_ARTIFACT_FILE}`
1291
+ ] : [
1292
+ "set -euo pipefail",
1293
+ `reused_digest="$EW_REUSE_DIGEST"`,
1294
+ `local_build=true`,
1295
+ `[ -z "$reused_digest" ] || local_build=false`,
1296
+ `jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
1297
+ ` --arg imageRepository "$EW_IMAGE" --arg reusedDigest "$reused_digest" \\`,
1298
+ ` --argjson localBuild "$local_build" \\`,
1299
+ ` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,'`,
1300
+ ` reusedDigest:$reusedDigest,localBuild:$localBuild,verification:{jobs:[],smoke:[]}}' > ${contracts.APP_BUILD_RESULT_ARTIFACT_FILE}`
1301
+ ];
1302
+ return [" - name: Write result", " if: always()", " run: |", ...scalarBlock(script, 10)];
1303
+ }
1304
+ function uploadArtifactStep() {
1305
+ return [
1306
+ ` - uses: ${actionPin(ACTION_PINS.uploadArtifact)}`,
1307
+ " if: always()",
1308
+ " with:",
1309
+ ` name: ${yamlString(contracts.APP_BUILD_RESULT_ARTIFACT_NAME)}`,
1310
+ ` path: ${yamlString(contracts.APP_BUILD_RESULT_ARTIFACT_FILE)}`,
1311
+ ` retention-days: ${contracts.APP_BUILD_RESULT_ARTIFACT_RETENTION_DAYS}`,
1312
+ " if-no-files-found: ignore"
1313
+ ];
1314
+ }
1315
+
1316
+ // src/repo/branch-protection.ts
1317
+ var BRANCH_RULES_PAGE_SIZE = 100;
1318
+ var BRANCH_RULES_MAX_PAGES = 10;
1319
+ var BRANCH_PROTECTION_RULE_TYPES = ["pull_request", "required_status_checks"];
1320
+ function isActiveRule(rule) {
1321
+ return rule.enforcement === void 0 || rule.enforcement === "active";
1322
+ }
1323
+ async function isBranchProtected(port, coordinates) {
1324
+ const classic = await port.readBranchProtection(coordinates);
1325
+ if (!classic.ok && classic.status === 403) return { protected: true, decision: "forbidden" };
1326
+ if (classic.ok) {
1327
+ if (classic.data?.required_pull_request_reviews !== void 0 && classic.data?.required_pull_request_reviews !== null) {
1328
+ return { protected: true, decision: "requiredPullRequestReviews" };
1329
+ }
1330
+ if (classic.data?.required_status_checks !== void 0 && classic.data?.required_status_checks !== null) {
1331
+ return { protected: true, decision: "requiredStatusChecks" };
1332
+ }
1333
+ }
1334
+ for (let page = 1; page <= BRANCH_RULES_MAX_PAGES; page += 1) {
1335
+ const rules = await port.readBranchRules({ ...coordinates, page });
1336
+ if (!rules.ok) return { protected: false, decision: "none" };
1337
+ const page1 = rules.data ?? [];
1338
+ for (const rule of page1) {
1339
+ if (!isActiveRule(rule)) continue;
1340
+ if (rule.type === "pull_request") return { protected: true, decision: "rulesetPullRequest" };
1341
+ if (rule.type === "required_status_checks") return { protected: true, decision: "rulesetStatusChecks" };
1342
+ }
1343
+ if (page1.length < BRANCH_RULES_PAGE_SIZE) break;
1344
+ }
1345
+ return { protected: false, decision: "none" };
1346
+ }
1347
+
1348
+ // src/repo/workflow-writer.ts
1349
+ var WORKFLOW_COMMIT_MESSAGE_ADD = "Add Ever Works build workflow";
1350
+ var WORKFLOW_COMMIT_MESSAGE_UPDATE = "Update Ever Works build workflow";
1351
+ var WORKFLOW_COMMIT_MESSAGE_REMOVE = "Remove Ever Works build workflow";
1352
+ var WORKFLOW_PULL_REQUEST_TITLE = "Add Ever Works build workflow";
1353
+ var WORKFLOW_REMOVAL_PULL_REQUEST_TITLE = "Remove the Ever Works build workflow";
1354
+ var WORKFLOW_PULL_REQUEST_BODY = "Ever Works generated this workflow from the App spec. It builds the image for this App Work on GitHub-hosted runners and publishes it to GHCR. Hand edits to this file are never overwritten: Ever Works proposes its changes as pull requests.";
1355
+ var WORKFLOW_REMOVAL_PULL_REQUEST_BODY = "Ever Works generated this workflow from the App spec. The App spec no longer declares a build or a check, so this pull request removes every job from it. Merge it to remove the workflow, or delete the file outright \u2014 Ever Works never writes to the tracked branch to remove a file.";
1356
+ var WORKFLOW_WRITE_MAX_ATTEMPTS = 3;
1357
+ var WORKFLOW_READ_BACK_ATTEMPTS = 2;
1358
+ function decodeFileContent(file) {
1359
+ return file.encoding === "base64" ? Buffer.from(file.content, "base64").toString("utf-8") : file.content;
1360
+ }
1361
+ async function readFileSha256(writer, path, ref) {
1362
+ const file = await writer.getFileContent(path, ref);
1363
+ if (!file) return null;
1364
+ return contracts.sha256Hex(decodeFileContent(file));
1365
+ }
1366
+ async function readBranchHead(writer, branch) {
1367
+ const existing = await writer.createBranch(branch, branch);
1368
+ return existing.commit;
1369
+ }
1370
+ function repositoryWriteErrorCode(error) {
1371
+ const candidate = error;
1372
+ for (const value of [candidate?.code, candidate?.reason]) {
1373
+ if (value === "nonFastForward" || value === "refRejectedByRule" || value === "pullRequestExists") return value;
1374
+ }
1375
+ return null;
1376
+ }
1377
+ var PULL_REQUEST_ALREADY_EXISTS = /pull request already exists/i;
1378
+ function isPullRequestAlreadyExistsError(error) {
1379
+ if (repositoryWriteErrorCode(error) === "pullRequestExists") return true;
1380
+ const candidate = error;
1381
+ if (candidate?.status !== 422) return false;
1382
+ const messages = [candidate.message, candidate.response?.data?.message];
1383
+ const errors = candidate.response?.data?.errors;
1384
+ if (Array.isArray(errors)) {
1385
+ for (const entry of errors) messages.push(entry?.message);
1386
+ }
1387
+ return messages.some((message) => typeof message === "string" && PULL_REQUEST_ALREADY_EXISTS.test(message));
1388
+ }
1389
+ function commitFile(path, content) {
1390
+ return { path, content, encoding: "utf-8" };
1391
+ }
1392
+ var DELIVERY_COPY = {
1393
+ commitMessage: WORKFLOW_COMMIT_MESSAGE_UPDATE,
1394
+ title: WORKFLOW_PULL_REQUEST_TITLE,
1395
+ body: WORKFLOW_PULL_REQUEST_BODY
1396
+ };
1397
+ var REMOVAL_COPY = {
1398
+ commitMessage: WORKFLOW_COMMIT_MESSAGE_REMOVE,
1399
+ title: WORKFLOW_REMOVAL_PULL_REQUEST_TITLE,
1400
+ body: WORKFLOW_REMOVAL_PULL_REQUEST_BODY
1401
+ };
1402
+ async function writeWorkflow(input, writer, protection) {
1403
+ const path = input.workflowPath ?? contracts.APP_BUILD_WORKFLOW_PATH;
1404
+ const { owner, repo, trackedBranch, createdByAppWork } = input.repository;
1405
+ const contentSha256 = contracts.sha256Hex(input.content);
1406
+ const currentSha256 = await readFileSha256(writer, path);
1407
+ const existsOnTrackedBranch = currentSha256 !== null;
1408
+ if (existsOnTrackedBranch && currentSha256 === contentSha256) {
1409
+ return { state: "unchanged", storedWorkflowSha256: contentSha256 };
1410
+ }
1411
+ const handEdited = existsOnTrackedBranch && currentSha256 !== input.lastWrittenWorkflowSha256 && currentSha256 !== contentSha256;
1412
+ const removal = input.content === "";
1413
+ if (removal) {
1414
+ if (!existsOnTrackedBranch) return { state: "unchanged", storedWorkflowSha256: null };
1415
+ return pullRequestDelivery(input, writer, path, contentSha256, handEdited, REMOVAL_COPY);
1416
+ }
1417
+ const verdict = createdByAppWork && !handEdited && protection ? await isBranchProtected(protection, { owner, repo, branch: trackedBranch }) : { protected: false};
1418
+ const pullRequestPath = !createdByAppWork || handEdited || verdict.protected;
1419
+ if (pullRequestPath) {
1420
+ return pullRequestDelivery(input, writer, path, contentSha256, handEdited);
1421
+ }
1422
+ return directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch);
1423
+ }
1424
+ async function directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch) {
1425
+ const { trackedBranch } = input.repository;
1426
+ const message = existsOnTrackedBranch ? WORKFLOW_COMMIT_MESSAGE_UPDATE : WORKFLOW_COMMIT_MESSAGE_ADD;
1427
+ let attempts = 0;
1428
+ while (attempts < WORKFLOW_WRITE_MAX_ATTEMPTS) {
1429
+ attempts += 1;
1430
+ const baseSha = input.resolveTrackedHead ? await input.resolveTrackedHead() : await readBranchHead(writer, trackedBranch);
1431
+ const commit = {
1432
+ branch: trackedBranch,
1433
+ baseSha,
1434
+ message,
1435
+ files: [commitFile(path, input.content)]
1436
+ };
1437
+ try {
1438
+ const { commitSha } = await writer.commitFiles(commit);
1439
+ const readBack = await readBackMatches(writer, path, commitSha, contentSha256);
1440
+ if (!readBack) return workflowWriteFailed("readBackMismatch");
1441
+ return { state: "committed", commitSha, storedWorkflowSha256: contentSha256 };
1442
+ } catch (error) {
1443
+ const code = repositoryWriteErrorCode(error);
1444
+ if (code === "refRejectedByRule") {
1445
+ return pullRequestDelivery(input, writer, path, contentSha256, false);
1446
+ }
1447
+ if (code === "nonFastForward" && attempts < WORKFLOW_WRITE_MAX_ATTEMPTS) continue;
1448
+ if (code === "nonFastForward") return workflowWriteFailed("nonFastForward");
1449
+ throw error;
1450
+ }
1451
+ }
1452
+ return workflowWriteFailed("nonFastForward");
1453
+ }
1454
+ async function pullRequestDelivery(input, writer, path, contentSha256, handEdited, copy = DELIVERY_COPY) {
1455
+ const { trackedBranch } = input.repository;
1456
+ try {
1457
+ const branch = await writer.createBranch(contracts.APP_BUILD_WORKFLOW_BRANCH, trackedBranch);
1458
+ const branchSha256 = await readFileSha256(writer, path, branch.name);
1459
+ if (branchSha256 !== contentSha256) {
1460
+ const { commitSha } = await writer.commitFiles({
1461
+ branch: branch.name,
1462
+ baseSha: branch.commit,
1463
+ message: copy.commitMessage,
1464
+ files: [commitFile(path, input.content)]
1465
+ });
1466
+ const readBack = await readBackMatches(writer, path, commitSha, contentSha256);
1467
+ if (!readBack) return workflowWriteFailed("readBackMismatch");
1468
+ }
1469
+ try {
1470
+ const pullRequest = await writer.createPullRequest({
1471
+ title: copy.title,
1472
+ head: contracts.APP_BUILD_WORKFLOW_BRANCH,
1473
+ base: trackedBranch,
1474
+ body: copy.body
1475
+ });
1476
+ return {
1477
+ state: handEdited ? "editedByHand" : "pullRequestOpened",
1478
+ pullRequestNumber: pullRequest.number,
1479
+ pullRequestUrl: pullRequest.url,
1480
+ storedWorkflowSha256: null
1481
+ };
1482
+ } catch (error) {
1483
+ if (!isPullRequestAlreadyExistsError(error)) throw error;
1484
+ return {
1485
+ state: handEdited ? "editedByHand" : "pullRequestUpdated",
1486
+ ...input.pullRequestNumber != null ? { pullRequestNumber: input.pullRequestNumber } : {},
1487
+ ...input.pullRequestUrl ? { pullRequestUrl: input.pullRequestUrl } : {},
1488
+ storedWorkflowSha256: null
1489
+ };
1490
+ }
1491
+ } catch (error) {
1492
+ if (repositoryWriteErrorCode(error) === "refRejectedByRule") return workflowWriteFailed("branchRules");
1493
+ throw error;
1494
+ }
1495
+ }
1496
+ async function readBackMatches(writer, path, commitSha, contentSha256) {
1497
+ for (let attempt = 0; attempt < WORKFLOW_READ_BACK_ATTEMPTS; attempt += 1) {
1498
+ if (await readFileSha256(writer, path, commitSha) === contentSha256) return true;
1499
+ }
1500
+ return false;
1501
+ }
1502
+ function workflowWriteFailed(cause) {
1503
+ return {
1504
+ state: "blocked",
1505
+ storedWorkflowSha256: null,
1506
+ blocked: { reason: "workflowWriteFailed", detail: { cause } }
1507
+ };
1508
+ }
1509
+ var GITHUB_SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]{0,254}$/;
1510
+ function isValidGitHubSecretName(name) {
1511
+ return GITHUB_SECRET_NAME_PATTERN.test(name) && !name.startsWith("GITHUB_");
1512
+ }
1513
+ function buildSecretName(valueName) {
1514
+ return `${contracts.APP_BUILD_SECRET_PREFIX}${valueName}`;
1515
+ }
1516
+ var sodiumPromise;
1517
+ function loadSodium() {
1518
+ sodiumPromise ??= (async () => {
1519
+ try {
1520
+ const imported = await import('libsodium-wrappers');
1521
+ if (imported.default) return imported.default;
1522
+ } catch {
1523
+ }
1524
+ const require2 = module$1.createRequire(typeof __filename === "string" ? __filename : (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1525
+ return require2("libsodium-wrappers");
1526
+ })();
1527
+ return sodiumPromise;
1528
+ }
1529
+ async function sealSecretValue(value, publicKey) {
1530
+ const _sodium = await loadSodium();
1531
+ await _sodium.ready;
1532
+ const binaryKey = _sodium.from_base64(publicKey, _sodium.base64_variants.ORIGINAL);
1533
+ const binarySecret = _sodium.from_string(value);
1534
+ const sealed = _sodium.crypto_box_seal(binarySecret, binaryKey);
1535
+ return _sodium.to_base64(sealed, _sodium.base64_variants.ORIGINAL);
1536
+ }
1537
+ function byteLength(value) {
1538
+ return Buffer.byteLength(value, "utf-8");
1539
+ }
1540
+ function isSecretLimitError(error) {
1541
+ const candidate = error;
1542
+ const message = typeof candidate?.message === "string" ? candidate.message : "";
1543
+ if (!/secret/i.test(message)) return false;
1544
+ if (candidate?.status === 422 && /limit/i.test(message)) return true;
1545
+ return /secret(s)? limit/i.test(message);
1546
+ }
1547
+ function isSecretNotFoundError(error) {
1548
+ return error?.status === 404;
1549
+ }
1550
+ function createBuildValueSecretSync(dependencies) {
1551
+ const { port, logger } = dependencies;
1552
+ const seal = dependencies.seal ?? sealSecretValue;
1553
+ async function put(name, value, publicKey) {
1554
+ if (!isValidGitHubSecretName(name)) {
1555
+ throw new Error(
1556
+ `Invalid secret name "${name}": must be 1-255 characters, contain only uppercase letters, digits, and underscores, start with a letter or underscore, and must not begin with "GITHUB_".`
1557
+ );
1558
+ }
1559
+ await port.putRepoSecret({
1560
+ name,
1561
+ encryptedValue: await seal(value, publicKey.key),
1562
+ keyId: publicKey.key_id
1563
+ });
1564
+ logger?.debug?.("build value sealed and written", { name });
1565
+ }
1566
+ return {
1567
+ async syncBuildValues(input) {
1568
+ const buildInputsHash = contracts.computeBuildInputsHash(
1569
+ input.values.map((value) => ({ name: value.name, fingerprint: value.fingerprint }))
1570
+ );
1571
+ if (input.values.length > contracts.APP_BUILD_MAX_VALUES) {
1572
+ return {
1573
+ secretsWritten: [],
1574
+ secretsRemoved: [],
1575
+ buildInputsHash,
1576
+ blocked: {
1577
+ reason: "tooManyBuildValues",
1578
+ detail: { count: input.values.length, max: contracts.APP_BUILD_MAX_VALUES }
1579
+ }
1580
+ };
1581
+ }
1582
+ for (const value of input.values) {
1583
+ const name = buildSecretName(value.name);
1584
+ if (name === contracts.APP_BUILD_VERIFY_PROMPTED_SECRET) {
1585
+ return {
1586
+ secretsWritten: [],
1587
+ secretsRemoved: [],
1588
+ buildInputsHash,
1589
+ blocked: { reason: "buildValueNameReserved", detail: { name } }
1590
+ };
1591
+ }
1592
+ if (byteLength(value.value) > contracts.APP_BUILD_SECRET_MAX_BYTES) {
1593
+ return {
1594
+ secretsWritten: [],
1595
+ secretsRemoved: [],
1596
+ buildInputsHash,
1597
+ blocked: { reason: "buildValueTooLarge", detail: { name } }
1598
+ };
1599
+ }
1600
+ }
1601
+ const written = [];
1602
+ const publicKey = await port.getRepoPublicKey();
1603
+ try {
1604
+ for (const value of input.values) {
1605
+ const name = buildSecretName(value.name);
1606
+ await put(name, value.value, publicKey);
1607
+ written.push(name);
1608
+ }
1609
+ } catch (error) {
1610
+ if (isSecretLimitError(error)) {
1611
+ return {
1612
+ secretsWritten: [],
1613
+ secretsRemoved: [],
1614
+ buildInputsHash,
1615
+ blocked: { reason: "secretLimitReached", detail: { count: input.values.length } }
1616
+ };
1617
+ }
1618
+ throw error;
1619
+ }
1620
+ const referenced = new Set(written);
1621
+ const removed = [];
1622
+ for (const name of new Set(input.previouslyWrittenSecretNames)) {
1623
+ if (referenced.has(name)) continue;
1624
+ try {
1625
+ await port.deleteRepoSecret({ name });
1626
+ logger?.debug?.("build value secret removed", { name });
1627
+ } catch (error) {
1628
+ if (!isSecretNotFoundError(error)) throw error;
1629
+ logger?.debug?.("build value secret already absent", { name });
1630
+ }
1631
+ removed.push(name);
1632
+ }
1633
+ logger?.info?.("build values synced", { written: written.length, removed: removed.length });
1634
+ return { secretsWritten: written, secretsRemoved: removed, buildInputsHash };
1635
+ },
1636
+ async writeVerifyPromptedSecret(values) {
1637
+ const payload = {};
1638
+ for (const value of values) payload[value.name] = value.value;
1639
+ const json = JSON.stringify(payload);
1640
+ const bytes = byteLength(json);
1641
+ if (bytes > contracts.APP_BUILD_SECRET_MAX_BYTES) {
1642
+ return {
1643
+ name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET,
1644
+ bytes,
1645
+ blocked: { reason: "buildValueTooLarge", detail: { name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET } }
1646
+ };
1647
+ }
1648
+ await put(contracts.APP_BUILD_VERIFY_PROMPTED_SECRET, json, await port.getRepoPublicKey());
1649
+ return { name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET, bytes };
1650
+ },
1651
+ async deleteVerifyPromptedSecret() {
1652
+ try {
1653
+ await port.deleteRepoSecret({ name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET });
1654
+ } catch (error) {
1655
+ if (!isSecretNotFoundError(error)) throw error;
1656
+ logger?.debug?.("verification prompted secret already absent", {
1657
+ name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET
1658
+ });
1659
+ return { deleted: false };
1660
+ }
1661
+ logger?.debug?.("verification prompted secret removed", { name: contracts.APP_BUILD_VERIFY_PROMPTED_SECRET });
1662
+ return { deleted: true };
1663
+ }
1664
+ };
1665
+ }
1666
+
1667
+ // src/settings.schema.ts
1668
+ var LARGER_RUNNER_LABEL_PATTERN = "^[A-Za-z0-9._-]{1,64}$";
1669
+ var LARGER_RUNNER_MEMORY_MIN_GIB = 8;
1670
+ var LARGER_RUNNER_MEMORY_MAX_GIB = 256;
1671
+ var LARGER_RUNNER_VCPU_MIN = 2;
1672
+ var LARGER_RUNNER_VCPU_MAX = 64;
1673
+ var GITHUB_ACTIONS_BUILD_SETTING_KEYS = [
1674
+ "largerRunnerLabel",
1675
+ "largerRunnerMemoryGiB",
1676
+ "largerRunnerVcpu",
1677
+ "reclaimDisk",
1678
+ "attestations",
1679
+ "pullToken",
1680
+ "pullTokenExpiresAt",
1681
+ "allowBuildValuesOnPullRequests",
1682
+ "verificationPromptedValuesRequireApproval"
1683
+ ];
1684
+ var GITHUB_ACTIONS_BUILD_PLATFORM_MANAGED_KEYS = ["pullToken", "pullTokenExpiresAt"];
1685
+ var PREPARATION_STATE_KEYS = [
1686
+ "workflowSha256",
1687
+ "workflowPullRequestNumber",
1688
+ "webhookId",
1689
+ "runsEtag",
1690
+ "repositoryBlock"
1691
+ ];
1692
+ var GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS = {
1693
+ largerRunnerLabel: "",
1694
+ largerRunnerMemoryGiB: 0,
1695
+ largerRunnerVcpu: 0,
1696
+ reclaimDisk: true,
1697
+ attestations: false,
1698
+ allowBuildValuesOnPullRequests: false,
1699
+ verificationPromptedValuesRequireApproval: true
1700
+ };
1701
+ var gitHubActionsBuildSettingsSchema = {
1702
+ type: "object",
1703
+ properties: {
1704
+ largerRunnerLabel: {
1705
+ type: "string",
1706
+ title: "Larger runner label",
1707
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.largerRunnerLabel,
1708
+ pattern: LARGER_RUNNER_LABEL_PATTERN,
1709
+ description: "Label of a larger GitHub-hosted runner to use for private repositories, when one is configured. Leave blank to use the standard runner."
1710
+ },
1711
+ largerRunnerMemoryGiB: {
1712
+ type: "integer",
1713
+ title: "Larger runner memory (GiB)",
1714
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.largerRunnerMemoryGiB,
1715
+ minimum: LARGER_RUNNER_MEMORY_MIN_GIB,
1716
+ maximum: LARGER_RUNNER_MEMORY_MAX_GIB,
1717
+ description: 'Memory the larger runner provides. Required when a larger runner label is set \u2014 the memory check cannot run without it. 0 means "no larger runner".'
1718
+ },
1719
+ largerRunnerVcpu: {
1720
+ type: "integer",
1721
+ title: "Larger runner vCPU",
1722
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.largerRunnerVcpu,
1723
+ minimum: LARGER_RUNNER_VCPU_MIN,
1724
+ maximum: LARGER_RUNNER_VCPU_MAX,
1725
+ description: "CPU the larger runner provides. Informational: a declared CPU above it produces a warning, never a blocked Build."
1726
+ },
1727
+ reclaimDisk: {
1728
+ type: "boolean",
1729
+ title: "Reclaim runner disk before building",
1730
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.reclaimDisk,
1731
+ description: "Removes preinstalled toolchains the build does not need, so a large image fits on the runner."
1732
+ },
1733
+ attestations: {
1734
+ type: "boolean",
1735
+ title: "Publish build provenance attestations",
1736
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.attestations,
1737
+ description: "Public repositories only. On a private repository this is ignored: the generated workflow grants the attestation permissions only when the repository is public.",
1738
+ "x-scope": "work"
1739
+ },
1740
+ pullToken: {
1741
+ type: "string",
1742
+ title: "Registry pull token",
1743
+ description: "Written by Ever Works after the token is validated against the built image; never typed here and never returned by an endpoint.",
1744
+ "x-secret": true,
1745
+ "x-scope": "work",
1746
+ "x-widget": "password",
1747
+ "x-platformManaged": true
1748
+ },
1749
+ pullTokenExpiresAt: {
1750
+ type: "string",
1751
+ title: "Registry pull token expires at",
1752
+ description: "Recorded by Ever Works from the token provider when the token was saved. Read-only.",
1753
+ "x-scope": "work",
1754
+ "x-platformManaged": true
1755
+ },
1756
+ allowBuildValuesOnPullRequests: {
1757
+ type: "boolean",
1758
+ title: "Send stored build values to pull requests",
1759
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.allowBuildValuesOnPullRequests,
1760
+ description: "When on, a pull request from this repository receives the stored build values \u2014 which means whatever code the pull request contains can read them. Leave off unless the build genuinely needs a stored value on a pull request.",
1761
+ "x-scope": "work"
1762
+ },
1763
+ verificationPromptedValuesRequireApproval: {
1764
+ type: "boolean",
1765
+ title: "Require approval before a verification uses prompted values",
1766
+ default: GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS.verificationPromptedValuesRequireApproval,
1767
+ description: "When on, a verification receives owner-set prompted values only when the change under verification touches no build-affecting file, or the owner approved it.",
1768
+ "x-scope": "work"
1769
+ }
1770
+ },
1771
+ // plan §4.4: `largerRunnerMemoryGiB` is "required when the label is set (the
1772
+ // memory check needs it)". JSON Schema is the only place this can be stated
1773
+ // before a Build exists, and the platform's validator runs with
1774
+ // `useDefaults: false`, so an absent key stays absent and this `required`
1775
+ // really fires.
1776
+ allOf: [
1777
+ {
1778
+ if: { properties: { largerRunnerLabel: { minLength: 1 } }, required: ["largerRunnerLabel"] },
1779
+ then: { required: ["largerRunnerMemoryGiB"] }
1780
+ }
1781
+ ]
1782
+ };
1783
+
1784
+ // src/github-actions-build.plugin.ts
1785
+ var GitHubActionsBuildPlugin = class {
1786
+ constructor() {
1787
+ this.id = "github-actions-build";
1788
+ this.name = "GitHub Actions builds";
1789
+ this.version = "1.0.0";
1790
+ /**
1791
+ * Plan §4.3: the `build` category — appended to `PLUGIN_CATEGORIES` by APW-05
1792
+ * T3 (`packages/plugin/src/contracts/plugin-manifest.types.ts`), so the
1793
+ * loader's own `isPluginCategory` gate accepts this manifest.
1794
+ */
1795
+ this.category = "build";
1796
+ /** Plan §4.3: exactly one capability. `isBuildPlugin` resolves this plugin on it. */
1797
+ this.capabilities = ["build"];
1798
+ /** Plan §4.3. `AppBuildKind` — a closed union in `@ever-works/contracts`. */
1799
+ this.buildKind = "github-actions";
1800
+ /**
1801
+ * Plan §4.1/R-13: `dockerfile` only.
1802
+ *
1803
+ * `auto` is deliberately absent: the zero-config builder is a provider-internal
1804
+ * choice this plugin does not have, so the strategy gate leaves that Build
1805
+ * unstarted (`strategyNotSupported`) rather than half-built. `image` and `none`
1806
+ * are absent for the same reason — they need no build at all, and the prepare
1807
+ * runner handles them before a build plugin is asked (plan §7.2).
1808
+ */
1809
+ this.supportedStrategies = ["dockerfile"];
1810
+ /** Work-scoped settings, like the other per-Work provider plugins. */
1811
+ this.configurationMode = "user-required";
1812
+ /** Plan §4.4, exactly. */
1813
+ this.settingsSchema = gitHubActionsBuildSettingsSchema;
1814
+ }
1815
+ async onLoad(context) {
1816
+ context.logger.log("GitHub Actions build plugin loaded");
1817
+ }
1818
+ async onUnload() {
1819
+ }
1820
+ /**
1821
+ * The Actions API this plugin talks to, bound per call from the caller's auth.
1822
+ *
1823
+ * `protected` and a method, not a constructor seam: the loader constructs a
1824
+ * plugin with **no arguments** (`PluginLoaderService.loadPluginModule` —
1825
+ * `new PluginClass()`), so anything a spec wants to replace has to be
1826
+ * replaceable on the instance. A spec subclasses and overrides this; production
1827
+ * gets Octokit, which is the official SDK and already this package's dependency.
1828
+ *
1829
+ * A fresh client per call, deliberately: `auth.token` is resolved per call by
1830
+ * the facade (plan §4.1) and may be a different member's installation token
1831
+ * from one call to the next. Caching one would be caching a credential.
1832
+ */
1833
+ actionsPort(auth) {
1834
+ return octokitActionsRunsPort(new octokit.Octokit({ auth: auth.token }));
1835
+ }
1836
+ /** The clock, as a seam, so the correlation window is provable without waiting. */
1837
+ now() {
1838
+ return Date.now();
1839
+ }
1840
+ getManifest() {
1841
+ return {
1842
+ id: this.id,
1843
+ name: this.name,
1844
+ version: this.version,
1845
+ description: "Builds the App Work's container image on GitHub-hosted runners, through the workflow Ever Works writes into the repository's tracked branch",
1846
+ category: this.category,
1847
+ capabilities: [...this.capabilities],
1848
+ author: { name: "Ever Works Team" },
1849
+ license: "AGPL-3.0",
1850
+ builtIn: true,
1851
+ autoEnable: true,
1852
+ visibility: "user-only"
1853
+ };
1854
+ }
1855
+ // IBuildPlugin -----------------------------------------------------------
1856
+ // Every member is declared and none pretends: see the class docstring for the
1857
+ // task each one waits on.
1858
+ /**
1859
+ * APW-05 §4.6 — put the workflow (and the build values) where a Build can use them.
1860
+ *
1861
+ * Four steps, in this order, and the order is the point: nothing is written to a
1862
+ * member's repository until it is known the Build could run at all.
1863
+ *
1864
+ * 1. **Select the runner** (T11). A declared memory the chosen runner cannot
1865
+ * give is `runnerTooSmall`, and it BLOCKS here — before a commit, before a
1866
+ * secret, before anything reaches GitHub. The alternative is writing a
1867
+ * workflow that is guaranteed to be OOM-killed minutes into its first run.
1868
+ * 2. **Generate the file** (T8), with the checks job (T41) when the spec
1869
+ * declares checks and the verify job when the App Work can verify.
1870
+ * 3. **Deliver it** (T9): unchanged, committed, or a pull request for a
1871
+ * repository that is not ours to write into (R-4). A hand-edited file is
1872
+ * `editedByHand` and is never overwritten.
1873
+ * 4. **Sync the build values** (T10), and only then — a secret written for a
1874
+ * workflow that was never delivered is a secret nobody will clean up.
1875
+ *
1876
+ * ## The two failure shapes, kept apart
1877
+ *
1878
+ * A `blocked` result is a Build that will not start and a member who is told
1879
+ * why. A THROW is this plugin failing at its own job. `writeWorkflow`'s own
1880
+ * `'blocked'` state is mapped onto `PrepareRepositoryResult.blocked` rather than
1881
+ * passed through, because that state is the writer's and is not one of the five
1882
+ * the contract's `workflow.state` allows — the writer's docstring says so, and
1883
+ * storing it would put a value in the database that no reader understands.
1884
+ *
1885
+ * ## `contentSha256` is stored only after a matching read-back
1886
+ *
1887
+ * `storedWorkflowSha256` is `null` until the writer has PROVEN the tracked
1888
+ * branch holds the generated bytes (FR-8). Reporting the hash of what we sent,
1889
+ * rather than of what is there, is how a failed write becomes an unnoticed
1890
+ * hand-edit detection failure on the next preparation.
1891
+ */
1892
+ async prepareRepository(input, auth, writer) {
1893
+ const settings = input.settings ?? {};
1894
+ const selection = selectRunner({
1895
+ visibility: input.repository.visibility,
1896
+ resources: resourcesOf(input.build ?? null),
1897
+ settings
1898
+ });
1899
+ if (!selection.ok) {
1900
+ return {
1901
+ workflow: { state: "unchanged", contentSha256: "" },
1902
+ secretsWritten: [],
1903
+ secretsRemoved: [],
1904
+ buildInputsHash: contracts.computeBuildInputsHash(input.values ?? []),
1905
+ blocked: {
1906
+ reason: selection.blocked.reason,
1907
+ detail: { needed: selection.blocked.needed, max: selection.blocked.max }
1908
+ }
1909
+ };
1910
+ }
1911
+ const content = generateWorkflow({
1912
+ trackedBranch: input.repository.trackedBranch,
1913
+ repository: {
1914
+ owner: input.repository.owner,
1915
+ repo: input.repository.repo,
1916
+ visibility: input.repository.visibility
1917
+ },
1918
+ appSpecHash: input.appSpecHash,
1919
+ build: input.build ?? null,
1920
+ values: input.values ?? [],
1921
+ runner: { label: selection.runner.label, class: selection.runner.runnerClass },
1922
+ settings: {
1923
+ reclaimDisk: settings.reclaimDisk,
1924
+ attestations: settings.attestations,
1925
+ allowBuildValuesOnPullRequests: settings.allowBuildValuesOnPullRequests,
1926
+ verificationPromptedValuesRequireApproval: settings.verificationPromptedValuesRequireApproval
1927
+ },
1928
+ // A Build that has no `build` block cannot be verified in the runner; the
1929
+ // verify job would have nothing to run.
1930
+ verifyEnabled: Boolean(input.build),
1931
+ checks: input.checks ?? []
1932
+ });
1933
+ const written = await writeWorkflow(
1934
+ {
1935
+ repository: {
1936
+ owner: input.repository.owner,
1937
+ repo: input.repository.repo,
1938
+ trackedBranch: input.repository.trackedBranch,
1939
+ createdByAppWork: input.repository.createdByAppWork
1940
+ },
1941
+ content,
1942
+ lastWrittenWorkflowSha256: input.lastWrittenWorkflowSha256,
1943
+ // §3.1b's recorded pull request, echoed back when GitHub reports the head's
1944
+ // pull request still open (ACC-05-02). Never what decides that it is open.
1945
+ pullRequestNumber: input.workflowPullRequestNumber ?? null,
1946
+ pullRequestUrl: input.workflowPullRequestUrl ?? null
1947
+ },
1948
+ writer
1949
+ );
1950
+ if (written.state === "blocked") {
1951
+ return {
1952
+ workflow: { state: "unchanged", contentSha256: written.storedWorkflowSha256 ?? "" },
1953
+ secretsWritten: [],
1954
+ secretsRemoved: [],
1955
+ buildInputsHash: contracts.computeBuildInputsHash(input.values ?? []),
1956
+ blocked: {
1957
+ reason: written.blocked?.reason ?? "workflowWriteFailed",
1958
+ detail: { cause: written.blocked?.detail.cause ?? "unknown" }
1959
+ }
1960
+ };
1961
+ }
1962
+ const sync = createBuildValueSecretSync({ port: this.secretPort(auth, input.repository) });
1963
+ const synced = await sync.syncBuildValues({
1964
+ values: input.values ?? [],
1965
+ previouslyWrittenSecretNames: input.previouslyWrittenSecretNames ?? []
1966
+ });
1967
+ return {
1968
+ workflow: {
1969
+ state: written.state,
1970
+ ...written.commitSha ? { commitSha: written.commitSha } : {},
1971
+ ...written.pullRequestUrl ? { pullRequestUrl: written.pullRequestUrl } : {},
1972
+ // Only a matching read-back sets this — see the docstring.
1973
+ contentSha256: written.storedWorkflowSha256 ?? ""
1974
+ },
1975
+ secretsWritten: synced.secretsWritten ?? [],
1976
+ secretsRemoved: synced.secretsRemoved ?? [],
1977
+ buildInputsHash: synced.buildInputsHash ?? contracts.computeBuildInputsHash(input.values ?? []),
1978
+ ...synced.blocked ? { blocked: synced.blocked } : {}
1979
+ };
1980
+ }
1981
+ /**
1982
+ * The repository-secret operations T10's sync needs, over Octokit.
1983
+ *
1984
+ * A seam for the same reason {@link actionsPort} is one: the loader constructs
1985
+ * this class with no arguments, so a spec replaces it on the instance.
1986
+ */
1987
+ secretPort(auth, repository) {
1988
+ const octokit$1 = new octokit.Octokit({ auth: auth.token });
1989
+ const base = { owner: repository.owner, repo: repository.repo };
1990
+ return {
1991
+ getRepoPublicKey: async () => {
1992
+ const { data } = await octokit$1.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", base);
1993
+ return data;
1994
+ },
1995
+ putRepoSecret: async ({ name, encryptedValue, keyId }) => {
1996
+ await octokit$1.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
1997
+ ...base,
1998
+ secret_name: name,
1999
+ encrypted_value: encryptedValue,
2000
+ key_id: keyId
2001
+ });
2002
+ },
2003
+ deleteRepoSecret: async ({ name }) => {
2004
+ await octokit$1.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
2005
+ ...base,
2006
+ secret_name: name
2007
+ });
2008
+ }
2009
+ };
2010
+ }
2011
+ /**
2012
+ * APW-05 T12 — dispatch the generated workflow on the App Work's tracked branch.
2013
+ *
2014
+ * ## `providerRunId` is `null` here, and that is not a failure
2015
+ *
2016
+ * `POST .../dispatches` answers **204 with no body**: GitHub says nothing about
2017
+ * the run it just created. So this method dispatches, records the instant, and
2018
+ * makes ONE correlation attempt; the run is usually not listed that fast, and a
2019
+ * `null` id is the normal answer. `app-build-watch` polls `getBuild`, which
2020
+ * correlates again on every poll. Blocking here until a run appeared would hold
2021
+ * the caller's request open for an arbitrary provider delay.
2022
+ *
2023
+ * ## The branch is the tracked branch, never the caller's ref
2024
+ *
2025
+ * FR-14: a Build may only run for the App Work's tracked branch. `input.ref` is
2026
+ * what to BUILD; `repository.trackedBranch` is where the workflow file lives and
2027
+ * the only `ref` a dispatch is allowed to name. Passing `input.ref` would let a
2028
+ * Build run a workflow file from a branch nobody reviewed.
2029
+ */
2030
+ async startBuild(input, auth) {
2031
+ const port = this.actionsPort(auth);
2032
+ const repository = {
2033
+ owner: input.repository.owner,
2034
+ repo: input.repository.repo
2035
+ };
2036
+ const inputs = {
2037
+ ew_build_id: input.buildId,
2038
+ ew_sha: input.sha,
2039
+ ew_mode: input.mode
2040
+ };
2041
+ if (input.reuseImageDigest) inputs.ew_reuse_digest = input.reuseImageDigest;
2042
+ if (input.verification?.json) inputs.ew_verify_plan = input.verification.json;
2043
+ const dispatchedAtMs = this.now();
2044
+ await port.dispatchWorkflow({
2045
+ repository,
2046
+ workflowFile: WORKFLOW_FILE_NAME,
2047
+ ref: input.repository.trackedBranch,
2048
+ inputs
2049
+ });
2050
+ const correlated = await correlateDispatchedRun(port, {
2051
+ repository,
2052
+ workflowFile: WORKFLOW_FILE_NAME,
2053
+ buildId: input.buildId,
2054
+ dispatchedAtMs
2055
+ });
2056
+ return {
2057
+ providerRunId: correlated.providerRunId,
2058
+ dispatchedAt: new Date(dispatchedAtMs).toISOString()
2059
+ };
2060
+ }
2061
+ /**
2062
+ * APW-05 T12 — observe one run.
2063
+ *
2064
+ * Correlates first when the ref carries no `providerRunId`: a Build dispatched
2065
+ * moments ago has none, and the watch job's whole purpose is to find it. `null`
2066
+ * means "no such run" — never "I could not tell", which the contract says is a
2067
+ * throw, and which is why a failed API call is not caught here.
2068
+ *
2069
+ * `redact` is APW-07's redactor for this App Work, and it is USED: every line
2070
+ * of every excerpt goes through it, and through the secret-shape mask after
2071
+ * it, before it leaves this method (FR-38).
2072
+ */
2073
+ async getBuild(ref, auth, redact) {
2074
+ const port = this.actionsPort(auth);
2075
+ const repository = {
2076
+ owner: ref.repository.owner,
2077
+ repo: ref.repository.repo
2078
+ };
2079
+ const runId = await this.resolveRunId(port, ref, repository);
2080
+ if (runId === null) return null;
2081
+ const run = await port.getWorkflowRun({ repository, runId });
2082
+ if (!run) return null;
2083
+ const jobs = await port.listRunJobs({ repository, runId });
2084
+ const snapshot = observeRun({ run, jobs, mode: "build" });
2085
+ const finished = snapshot.status !== "queued" && snapshot.status !== "running";
2086
+ const artifact = finished ? await readResultArtifact(port, { repository, runId }) : null;
2087
+ const result = artifact?.ok ? artifact.result : null;
2088
+ const failure = snapshot.status === "failed" ? await this.classifyRunFailure({ port, repository, run, jobs, snapshot, result, redact }) : null;
2089
+ const pushLogDigest = result && snapshot.status === "succeeded" && isPushingTrigger(snapshot.trigger) ? await this.readPushLogDigest({ port, repository, jobs, sha: snapshot.commitSha }) : void 0;
2090
+ return {
2091
+ ...snapshot,
2092
+ ...result ? {
2093
+ // `confirmed: false` without exception. This plugin reads the digest
2094
+ // the member's own CI claimed; confirming it against the registry is
2095
+ // `checkImageAccess` (T14), compared by `AppBuildsService.finalize`,
2096
+ // and a plugin that marked its own input confirmed would make plan
2097
+ // §4.8's "never believed" untrue. `pushLogDigest` is evidence the
2098
+ // agent weighs only when the registry cannot be read; it confirms
2099
+ // nothing here either.
2100
+ image: {
2101
+ repository: result.imageRepository ?? "",
2102
+ digest: result.digest,
2103
+ tags: [...result.tags ?? []],
2104
+ confirmed: false,
2105
+ ...pushLogDigest ? { pushLogDigest } : {}
2106
+ },
2107
+ ...result.secretCheck ? { secretCheck: result.secretCheck } : {}
2108
+ } : {},
2109
+ ...failure ? { failure } : {}
2110
+ };
2111
+ }
2112
+ /**
2113
+ * APW-05 T13 — why a failed run failed, as `BuildSnapshot.failure`.
2114
+ *
2115
+ * ## Which job's log
2116
+ *
2117
+ * The `build` job decides the Build's status (`run-observer.ts`, R-9), so when
2118
+ * it failed it is the one worth reading. Only when it did NOT fail — a Build
2119
+ * the run marked failed for some other reason — does any other failing job
2120
+ * stand in. A failed `Ever Works check:` job is deliberately last: it never
2121
+ * flips a Build to `failed`, so if it is the only failure the run's own
2122
+ * conclusion is what actually needs explaining.
2123
+ *
2124
+ * No failing job at all is not an error: `jobCount` then carries the run into
2125
+ * the classifier's `workflowInvalid` row, which is exactly what a run that
2126
+ * produced no jobs is.
2127
+ *
2128
+ * ## Why the artifact's smoke rows are passed in
2129
+ *
2130
+ * `verificationFailed` is a class the LOG cannot show: the smoke rows live in
2131
+ * the result artifact, which `getBuild` has already read by this point. Passing
2132
+ * them costs nothing and is the difference between "your build failed" and
2133
+ * "your app built, and then did not answer".
2134
+ */
2135
+ async classifyRunFailure(input) {
2136
+ const { port, repository, run, jobs, snapshot, result, redact } = input;
2137
+ const failed = (job) => Boolean(job.conclusion) && job.conclusion !== "success";
2138
+ const buildJob2 = jobs.find((job) => job.name === BUILD_JOB_NAME);
2139
+ const failingJob = buildJob2 && failed(buildJob2) ? buildJob2 : jobs.find(failed);
2140
+ const log = failingJob ? await readJobLogTail(port, { repository, jobId: failingJob.id }) : "";
2141
+ const smoke = result?.smoke ?? null;
2142
+ return classifyFailure(
2143
+ {
2144
+ log,
2145
+ jobConclusion: failingJob?.conclusion ?? null,
2146
+ runConclusion: run.conclusion ?? null,
2147
+ jobCount: jobs.length,
2148
+ failingStepName: failingStep(failingJob)?.name ?? null,
2149
+ ...typeof snapshot.billableMinutes === "number" ? { minutes: snapshot.billableMinutes } : {},
2150
+ ...smoke ? { verification: { failed: smoke.filter((row) => !row.passed).length, total: smoke.length } } : {}
2151
+ },
2152
+ redact
2153
+ );
2154
+ }
2155
+ /**
2156
+ * APW-05 T14 remainder — the digest the build job's `Push` step logged for
2157
+ * this Build's `sha-<sha>` tag, or `undefined`.
2158
+ *
2159
+ * The BUILD job's log, because that is the job holding the Push step; read as
2160
+ * the same bounded tail T13 reads (the Push step is near the end of the job,
2161
+ * followed only by the result write and the upload). An unreadable or expired
2162
+ * log is `''` from `readJobLogTail`, so this never fails an observation — it
2163
+ * only costs the fallback. Which lines count is `runs/push-digest.ts`'s rule.
2164
+ */
2165
+ async readPushLogDigest(input) {
2166
+ const buildJob2 = input.jobs.find((job) => job.name === BUILD_JOB_NAME);
2167
+ if (!buildJob2) return void 0;
2168
+ const log = await readJobLogTail(input.port, { repository: input.repository, jobId: buildJob2.id });
2169
+ return extractPushLogDigest(log, input.sha);
2170
+ }
2171
+ /**
2172
+ * The run this ref addresses: the one it names, or the one correlation finds.
2173
+ *
2174
+ * Shared by `getBuild`, `cancelBuild` and `getLogsUrl` so all three agree about
2175
+ * which run a ref means — a cancel that addressed a different run from the
2176
+ * observation would stop the wrong Build.
2177
+ */
2178
+ async resolveRunId(port, ref, repository) {
2179
+ if (ref.providerRunId) {
2180
+ const parsed = Number(ref.providerRunId);
2181
+ return Number.isFinite(parsed) ? parsed : null;
2182
+ }
2183
+ const dispatchedAtMs = ref.dispatchedAt ? Date.parse(ref.dispatchedAt) : this.now();
2184
+ const correlated = await correlateDispatchedRun(port, {
2185
+ repository,
2186
+ workflowFile: WORKFLOW_FILE_NAME,
2187
+ buildId: ref.buildId,
2188
+ dispatchedAtMs: Number.isNaN(dispatchedAtMs) ? this.now() : dispatchedAtMs
2189
+ });
2190
+ return correlated.providerRunId === null ? null : Number(correlated.providerRunId);
2191
+ }
2192
+ /**
2193
+ * APW-05 T12 — stop the run.
2194
+ *
2195
+ * A ref that resolves to no run is a no-op rather than a throw: the caller asked
2196
+ * for the Build to stop, and a Build with no run has stopped. The port swallows
2197
+ * GitHub's 409 ("not cancellable") for the same reason — a finished run is not a
2198
+ * failed cancel.
2199
+ */
2200
+ async cancelBuild(ref, auth) {
2201
+ const port = this.actionsPort(auth);
2202
+ const repository = {
2203
+ owner: ref.repository.owner,
2204
+ repo: ref.repository.repo
2205
+ };
2206
+ const runId = await this.resolveRunId(port, ref, repository);
2207
+ if (runId === null) return;
2208
+ await port.cancelWorkflowRun({ repository, runId });
2209
+ }
2210
+ /**
2211
+ * APW-05 T14 — can this installation read the image, and is its pull token the
2212
+ * right shape?
2213
+ *
2214
+ * Declared now that it does something. It stayed undeclared while it did not,
2215
+ * because the contract makes it optional and a caller materialises the member
2216
+ * before calling it — so a throwing placeholder would have told a reader the
2217
+ * capability was wired when it was not.
2218
+ *
2219
+ * This is the **confirming** half of plan §4.8's "only ever confirmed, never
2220
+ * believed". `getBuild` reports the digest the member's own CI claimed with
2221
+ * `confirmed: false`, always; comparing it against the digest this method reads
2222
+ * from the registry is what makes it true, and that comparison belongs to the
2223
+ * caller — `AppBuildsService.finalize` — which is the only party holding both
2224
+ * the Build row and the installation's `pullToken` setting (`getBuild(ref, auth)`
2225
+ * is given neither).
2226
+ *
2227
+ * The registry is read through GHCR's own token exchange (`registry/ghcr-access.ts`):
2228
+ * a bare manifest `HEAD` is 401 for every image on GHCR, public or not, so a
2229
+ * public image is only ever seen as public through the anonymous registry token.
2230
+ * The pull token reaches `ghcr.io/token` (as Basic) and `api.github.com/user`,
2231
+ * and nothing else.
2232
+ */
2233
+ async checkImageAccess(input) {
2234
+ return checkImageAccess(input, this.registryFetch());
2235
+ }
2236
+ /**
2237
+ * The registry/API HTTP seam.
2238
+ *
2239
+ * `redirect: 'manual'` is set by `ghcr-access.ts` on every request and passed
2240
+ * straight through here: a 3xx followed with the `Authorization` header still
2241
+ * attached is how a pull token reaches a host that is neither `api.github.com`
2242
+ * nor `ghcr.io`, and that module's spec watches every call for exactly that.
2243
+ */
2244
+ registryFetch() {
2245
+ return (url, init) => fetch(url, init);
2246
+ }
2247
+ /** APW-05 T12 — the run's own page, which is where a member reads its logs. */
2248
+ async getLogsUrl(ref, auth) {
2249
+ const port = this.actionsPort(auth);
2250
+ const repository = {
2251
+ owner: ref.repository.owner,
2252
+ repo: ref.repository.repo
2253
+ };
2254
+ const runId = await this.resolveRunId(port, ref, repository);
2255
+ if (runId === null) return null;
2256
+ const run = await port.getWorkflowRun({ repository, runId });
2257
+ return run?.html_url ?? null;
2258
+ }
2259
+ };
2260
+ function resourcesOf(build) {
2261
+ const resources = build?.resources;
2262
+ if (!resources) return void 0;
2263
+ return {
2264
+ ...typeof resources.memoryGiB === "number" ? { memoryGiB: resources.memoryGiB } : {},
2265
+ ...typeof resources.cpu === "number" ? { vcpu: resources.cpu } : {}
2266
+ };
2267
+ }
2268
+ function isPushingTrigger(trigger) {
2269
+ return contracts.APP_BUILD_DEPLOYABLE_TRIGGERS.includes(trigger);
2270
+ }
2271
+ var WORKFLOW_FILE_NAME = contracts.APP_BUILD_WORKFLOW_PATH.split("/").pop();
2272
+ function notImplemented(member, owner) {
2273
+ return new Error(`github-actions-build: ${member} is not implemented yet \u2014 ${owner} owns it.`);
2274
+ }
2275
+
2276
+ exports.ACTION_PINS = ACTION_PINS;
2277
+ exports.ACTION_PIN_SHA_PATTERN = ACTION_PIN_SHA_PATTERN;
2278
+ exports.BRANCH_PROTECTION_RULE_TYPES = BRANCH_PROTECTION_RULE_TYPES;
2279
+ exports.BRANCH_RULES_MAX_PAGES = BRANCH_RULES_MAX_PAGES;
2280
+ exports.BRANCH_RULES_PAGE_SIZE = BRANCH_RULES_PAGE_SIZE;
2281
+ exports.CANONICAL_INPUT_FIELDS = CANONICAL_INPUT_FIELDS;
2282
+ exports.EMBEDDED_VERIFY_RUNNER_SCRIPT = EMBEDDED_VERIFY_RUNNER_SCRIPT;
2283
+ exports.GENERATOR_RESERVED_LITERALS = GENERATOR_RESERVED_LITERALS;
2284
+ exports.GITHUB_ACTIONS_BUILD_PLATFORM_MANAGED_KEYS = GITHUB_ACTIONS_BUILD_PLATFORM_MANAGED_KEYS;
2285
+ exports.GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS = GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS;
2286
+ exports.GITHUB_ACTIONS_BUILD_SETTING_KEYS = GITHUB_ACTIONS_BUILD_SETTING_KEYS;
2287
+ exports.GITHUB_SECRET_NAME_PATTERN = GITHUB_SECRET_NAME_PATTERN;
2288
+ exports.GitHubActionsBuildPlugin = GitHubActionsBuildPlugin;
2289
+ exports.LARGER_RUNNER_LABEL_PATTERN = LARGER_RUNNER_LABEL_PATTERN;
2290
+ exports.LARGER_RUNNER_MEMORY_MAX_GIB = LARGER_RUNNER_MEMORY_MAX_GIB;
2291
+ exports.LARGER_RUNNER_MEMORY_MIN_GIB = LARGER_RUNNER_MEMORY_MIN_GIB;
2292
+ exports.LARGER_RUNNER_VCPU_MAX = LARGER_RUNNER_VCPU_MAX;
2293
+ exports.LARGER_RUNNER_VCPU_MIN = LARGER_RUNNER_VCPU_MIN;
2294
+ exports.PREPARATION_STATE_KEYS = PREPARATION_STATE_KEYS;
2295
+ exports.WORKFLOW_COMMIT_MESSAGE_ADD = WORKFLOW_COMMIT_MESSAGE_ADD;
2296
+ exports.WORKFLOW_COMMIT_MESSAGE_UPDATE = WORKFLOW_COMMIT_MESSAGE_UPDATE;
2297
+ exports.WORKFLOW_PULL_REQUEST_BODY = WORKFLOW_PULL_REQUEST_BODY;
2298
+ exports.WORKFLOW_PULL_REQUEST_TITLE = WORKFLOW_PULL_REQUEST_TITLE;
2299
+ exports.WORKFLOW_READ_BACK_ATTEMPTS = WORKFLOW_READ_BACK_ATTEMPTS;
2300
+ exports.WORKFLOW_WRITE_MAX_ATTEMPTS = WORKFLOW_WRITE_MAX_ATTEMPTS;
2301
+ exports.actionPin = actionPin;
2302
+ exports.actionPinReference = actionPinReference;
2303
+ exports.branchSlug = branchSlug;
2304
+ exports.buildImageRepository = buildImageRepository;
2305
+ exports.buildSecretName = buildSecretName;
2306
+ exports.canonicalActionPins = canonicalActionPins;
2307
+ exports.canonicalCheck = canonicalCheck;
2308
+ exports.canonicalInputsFor = canonicalInputsFor;
2309
+ exports.canonicalWorkflowJson = canonicalWorkflowJson;
2310
+ exports.canonicaliseWorkflowInputs = canonicaliseWorkflowInputs;
2311
+ exports.checkTimeoutMinutes = checkTimeoutMinutes;
2312
+ exports.computeWorkflowInputsHash = computeWorkflowInputsHash;
2313
+ exports.createBuildValueSecretSync = createBuildValueSecretSync;
2314
+ exports.default = GitHubActionsBuildPlugin;
2315
+ exports.fromEnvNames = fromEnvNames;
2316
+ exports.generateWorkflow = generateWorkflow;
2317
+ exports.gitHubActionsBuildSettingsSchema = gitHubActionsBuildSettingsSchema;
2318
+ exports.isBranchProtected = isBranchProtected;
2319
+ exports.isSecretLimitError = isSecretLimitError;
2320
+ exports.isValidGitHubSecretName = isValidGitHubSecretName;
2321
+ exports.loadSodium = loadSodium;
2322
+ exports.normaliseBuildBlock = normaliseBuildBlock;
2323
+ exports.notImplemented = notImplemented;
2324
+ exports.repositoryWriteErrorCode = repositoryWriteErrorCode;
2325
+ exports.sealSecretValue = sealSecretValue;
2326
+ exports.secretNamesFor = secretNamesFor;
2327
+ exports.workflowHeader = workflowHeader;
2328
+ exports.writeWorkflow = writeWorkflow;
2329
+ exports.yamlString = yamlString;