@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 +2329 -0
- package/dist/index.d.cts +1474 -0
- package/dist/index.d.ts +1474 -0
- package/dist/index.js +2271 -0
- package/package.json +87 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
import { BuildValue, JsonSchema, IBuildPlugin, PluginCategory, BuildStrategy, PluginContext, BuildAuth, PluginManifest, PrepareRepositoryInput, RepositoryWriter, PrepareRepositoryResult, StartBuildInput, BuildRef, BuildSnapshot, AppBuildBlock, RepositoryWriteErrorCode } from '@ever-works/plugin';
|
|
2
|
+
import { AppBuildBlockedReason, AppBuildKind, AppBuildRunnerClass, APP_BUILD_GENERATOR_VERSION } from '@ever-works/contracts';
|
|
3
|
+
import sodiumType from 'libsodium-wrappers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* APW-05 T12 — the narrow Actions API this plugin needs, and nothing else.
|
|
7
|
+
*
|
|
8
|
+
* ## Why a port, when `octokit` is already a dependency
|
|
9
|
+
*
|
|
10
|
+
* The same reason `RepositorySecretPort` is one (`repo/secret-sync.ts:58`): a
|
|
11
|
+
* fake is a few lines, so every rule in `run-correlator.ts` and
|
|
12
|
+
* `run-observer.ts` — the adoption window, the minutes rounding, "only the
|
|
13
|
+
* `build` job decides status" — is provable against recorded GitHub shapes
|
|
14
|
+
* without a socket, a token or a recorded-cassette harness. The port is the
|
|
15
|
+
* seam; {@link octokitActionsRunsPort} is the one implementation, and it is a
|
|
16
|
+
* thin call-through with no logic of its own to test.
|
|
17
|
+
*
|
|
18
|
+
* Octokit rather than hand-rolled `fetch`: it is the official SDK, it is already
|
|
19
|
+
* in this package's dependencies, and it handles the redirect-following that
|
|
20
|
+
* `GET actions/jobs/{id}/logs` needs (plan §4.8's logs tail).
|
|
21
|
+
*
|
|
22
|
+
* ## Shapes are the subset this plugin reads
|
|
23
|
+
*
|
|
24
|
+
* Each interface below names only the fields the correlator, the observer or the
|
|
25
|
+
* artifact reader actually use. That is deliberate: a wider type would invite a
|
|
26
|
+
* reader to assume a field is handled when nothing reads it, and GitHub's run
|
|
27
|
+
* payload is large enough that "the fields we use" is genuinely useful
|
|
28
|
+
* documentation.
|
|
29
|
+
*/
|
|
30
|
+
/** One workflow run, as this plugin reads it (`GET /repos/{o}/{r}/actions/runs/{id}`). */
|
|
31
|
+
interface ActionsRun {
|
|
32
|
+
readonly id: number;
|
|
33
|
+
/** `queued` · `in_progress` · `completed` — GitHub's own vocabulary, mapped by the observer. */
|
|
34
|
+
readonly status: string | null;
|
|
35
|
+
readonly conclusion: string | null;
|
|
36
|
+
readonly run_attempt?: number | null;
|
|
37
|
+
/** What the generated workflow's `run-name:` produced — the correlation key (plan §4.8). */
|
|
38
|
+
readonly display_title?: string | null;
|
|
39
|
+
readonly name?: string | null;
|
|
40
|
+
readonly event?: string | null;
|
|
41
|
+
readonly head_branch?: string | null;
|
|
42
|
+
readonly head_sha?: string | null;
|
|
43
|
+
readonly html_url?: string | null;
|
|
44
|
+
readonly created_at?: string | null;
|
|
45
|
+
readonly run_started_at?: string | null;
|
|
46
|
+
readonly updated_at?: string | null;
|
|
47
|
+
readonly pull_requests?: ReadonlyArray<{
|
|
48
|
+
readonly number?: number | null;
|
|
49
|
+
}> | null;
|
|
50
|
+
}
|
|
51
|
+
/** One job of a run (`GET /repos/{o}/{r}/actions/runs/{id}/jobs`). */
|
|
52
|
+
interface ActionsJob {
|
|
53
|
+
readonly id: number;
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly status?: string | null;
|
|
56
|
+
readonly conclusion?: string | null;
|
|
57
|
+
readonly started_at?: string | null;
|
|
58
|
+
readonly completed_at?: string | null;
|
|
59
|
+
readonly html_url?: string | null;
|
|
60
|
+
readonly labels?: readonly string[] | null;
|
|
61
|
+
readonly steps?: ReadonlyArray<{
|
|
62
|
+
readonly name?: string | null;
|
|
63
|
+
readonly number?: number | null;
|
|
64
|
+
readonly conclusion?: string | null;
|
|
65
|
+
}> | null;
|
|
66
|
+
}
|
|
67
|
+
/** One artifact of a run (`GET /repos/{o}/{r}/actions/runs/{id}/artifacts`). */
|
|
68
|
+
interface ActionsArtifact {
|
|
69
|
+
readonly id: number;
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly size_in_bytes?: number | null;
|
|
72
|
+
readonly expired?: boolean | null;
|
|
73
|
+
}
|
|
74
|
+
/** The repository coordinates every call below takes. */
|
|
75
|
+
interface ActionsRepositoryRef {
|
|
76
|
+
readonly owner: string;
|
|
77
|
+
readonly repo: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The Actions operations APW-05 T12 needs.
|
|
81
|
+
*
|
|
82
|
+
* Every method may reject; the plugin turns a rejection into a throw, never into
|
|
83
|
+
* a `null` "no such run" — those are different answers and the contract
|
|
84
|
+
* (`IBuildPlugin.getBuild`) says so in its own docstring.
|
|
85
|
+
*/
|
|
86
|
+
interface ActionsRunsPort {
|
|
87
|
+
/**
|
|
88
|
+
* `POST /repos/{o}/{r}/actions/workflows/{file}/dispatches` — 204, no body.
|
|
89
|
+
*
|
|
90
|
+
* GitHub answers a dispatch with no run id: the run is found afterwards, by
|
|
91
|
+
* `display_title`, which is what {@link ActionsRunsPort.listWorkflowRuns} and
|
|
92
|
+
* the correlator are for.
|
|
93
|
+
*/
|
|
94
|
+
dispatchWorkflow(input: {
|
|
95
|
+
readonly repository: ActionsRepositoryRef;
|
|
96
|
+
readonly workflowFile: string;
|
|
97
|
+
readonly ref: string;
|
|
98
|
+
readonly inputs: Record<string, string>;
|
|
99
|
+
}): Promise<void>;
|
|
100
|
+
/** `GET /repos/{o}/{r}/actions/workflows/{file}/runs`, newest first. */
|
|
101
|
+
listWorkflowRuns(input: {
|
|
102
|
+
readonly repository: ActionsRepositoryRef;
|
|
103
|
+
readonly workflowFile: string;
|
|
104
|
+
readonly perPage: number;
|
|
105
|
+
readonly headSha?: string;
|
|
106
|
+
readonly event?: string;
|
|
107
|
+
}): Promise<readonly ActionsRun[]>;
|
|
108
|
+
/** `GET /repos/{o}/{r}/actions/runs/{id}`. `null` when GitHub answers 404. */
|
|
109
|
+
getWorkflowRun(input: {
|
|
110
|
+
readonly repository: ActionsRepositoryRef;
|
|
111
|
+
readonly runId: number;
|
|
112
|
+
}): Promise<ActionsRun | null>;
|
|
113
|
+
/** `GET /repos/{o}/{r}/actions/runs/{id}/jobs` (all attempts of the latest run attempt). */
|
|
114
|
+
listRunJobs(input: {
|
|
115
|
+
readonly repository: ActionsRepositoryRef;
|
|
116
|
+
readonly runId: number;
|
|
117
|
+
}): Promise<readonly ActionsJob[]>;
|
|
118
|
+
/** `POST /repos/{o}/{r}/actions/runs/{id}/cancel`. A run that is already finished is not an error. */
|
|
119
|
+
cancelWorkflowRun(input: {
|
|
120
|
+
readonly repository: ActionsRepositoryRef;
|
|
121
|
+
readonly runId: number;
|
|
122
|
+
}): Promise<void>;
|
|
123
|
+
/** `GET /repos/{o}/{r}/actions/runs/{id}/artifacts`. */
|
|
124
|
+
listRunArtifacts(input: {
|
|
125
|
+
readonly repository: ActionsRepositoryRef;
|
|
126
|
+
readonly runId: number;
|
|
127
|
+
}): Promise<readonly ActionsArtifact[]>;
|
|
128
|
+
/**
|
|
129
|
+
* `GET /repos/{o}/{r}/actions/artifacts/{id}/zip`, following the redirect.
|
|
130
|
+
*
|
|
131
|
+
* Answers the raw zip bytes. The caller enforces the size cap BEFORE
|
|
132
|
+
* unzipping (plan §4.8: "≤ 64 KB, refuse larger") — this port does not,
|
|
133
|
+
* because a port that silently truncated would make the cap untestable.
|
|
134
|
+
*/
|
|
135
|
+
downloadArtifactZip(input: {
|
|
136
|
+
readonly repository: ActionsRepositoryRef;
|
|
137
|
+
readonly artifactId: number;
|
|
138
|
+
}): Promise<Uint8Array>;
|
|
139
|
+
/**
|
|
140
|
+
* `GET /repos/{o}/{r}/actions/jobs/{id}/logs`, following the redirect, asking
|
|
141
|
+
* for at most the last `maxBytes` with a `Range` header (plan §4.8).
|
|
142
|
+
*
|
|
143
|
+
* **Optional, and the only optional member of this port.** A caller whose
|
|
144
|
+
* token cannot read logs — or an adapter written before this landed — still
|
|
145
|
+
* gets a classified failure (`unknown`) rather than a thrown observation,
|
|
146
|
+
* which is why {@link readJobLogTail} probes for it instead of requiring it.
|
|
147
|
+
*
|
|
148
|
+
* `partial` reports whether the server honoured the range (`206`) rather than
|
|
149
|
+
* answering the whole body (`200`). The reader uses it to decide whether to
|
|
150
|
+
* drop the first line: a byte range almost never starts on a line boundary.
|
|
151
|
+
*
|
|
152
|
+
* `null` is "there is no log" — an expired one, or a job that never ran.
|
|
153
|
+
*/
|
|
154
|
+
downloadJobLogTail?(input: {
|
|
155
|
+
readonly repository: ActionsRepositoryRef;
|
|
156
|
+
readonly jobId: number;
|
|
157
|
+
readonly maxBytes: number;
|
|
158
|
+
}): Promise<{
|
|
159
|
+
readonly bytes: Uint8Array;
|
|
160
|
+
readonly partial: boolean;
|
|
161
|
+
} | null>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What {@link checkImageAccess} answers — the plugin's `ImageAccessResult`, plus nothing. */
|
|
165
|
+
interface GhcrAccessResult {
|
|
166
|
+
readonly visibility: 'public' | 'private' | 'unknown';
|
|
167
|
+
readonly readable: boolean;
|
|
168
|
+
readonly tokenScopesOk?: boolean;
|
|
169
|
+
readonly tokenExpiresAt?: string | null;
|
|
170
|
+
readonly digest?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The one HTTP seam, so every rule here is provable without a socket.
|
|
174
|
+
*
|
|
175
|
+
* `json()` is read only from `ghcr.io/token`'s 200 answer — the one body this
|
|
176
|
+
* module needs; the real `fetch` `Response` satisfies the shape as it is.
|
|
177
|
+
*/
|
|
178
|
+
type GhcrFetch = (url: string, init: {
|
|
179
|
+
readonly method: string;
|
|
180
|
+
readonly headers: Record<string, string>;
|
|
181
|
+
readonly redirect: 'manual';
|
|
182
|
+
}) => Promise<{
|
|
183
|
+
readonly status: number;
|
|
184
|
+
readonly headers: {
|
|
185
|
+
get(name: string): string | null;
|
|
186
|
+
};
|
|
187
|
+
json(): Promise<unknown>;
|
|
188
|
+
}>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* APW-05 T10 — build values in, sealed repository secrets out (plan §4.7, §4.10).
|
|
192
|
+
*
|
|
193
|
+
* ## What this file is, and what it is not
|
|
194
|
+
*
|
|
195
|
+
* It is the **only** place a build value becomes a repository secret, and the only
|
|
196
|
+
* place one is deleted. It is not a store: the value lives in the caller's memory,
|
|
197
|
+
* inside one sealed box, and in the `PUT` body — never in a return value, never in
|
|
198
|
+
* a log line, never in a thrown message. The three refusals below name things and
|
|
199
|
+
* counts, never values, because a refusal travels into `blockedReason` detail,
|
|
200
|
+
* Activity rows and telemetry (plan §9.1: "counters and identifiers only").
|
|
201
|
+
*
|
|
202
|
+
* ## The seals, and the sequence that must not drift
|
|
203
|
+
*
|
|
204
|
+
* `setActionSecret` in the github plugin
|
|
205
|
+
* (`packages/plugins/github/src/github-actions.service.ts:49-76`) is the pattern:
|
|
206
|
+
* validate the name, `await _sodium.ready`, `from_base64` the repository public
|
|
207
|
+
* key, `from_string` the value, `crypto_box_seal`, `to_base64`, `PUT` with
|
|
208
|
+
* `key_id`. This file repeats it exactly — the public key is fetched **once** for
|
|
209
|
+
* the whole batch, and every value is sealed with that one key before any `PUT`.
|
|
210
|
+
*
|
|
211
|
+
* The name rule is the same one the github plugin applies
|
|
212
|
+
* (`/^[A-Z_][A-Z0-9_]{0,254}$/`, never `GITHUB_`): GitHub itself rejects anything
|
|
213
|
+
* else, and a name that reaches GitHub is a name that already left the process.
|
|
214
|
+
* An invalid `EW_<NAME>` cannot come from APW-07's resolver (`^[A-Z_][A-Z0-9_]{0,127}$`
|
|
215
|
+
* env names), so it throws rather than blocking a Build — a programming error, not
|
|
216
|
+
* a user's mistake.
|
|
217
|
+
*
|
|
218
|
+
* ## The four refusals (plan §4.7:914–917, §4.10:1030–1033)
|
|
219
|
+
*
|
|
220
|
+
* | Condition | `blocked.reason` | detail |
|
|
221
|
+
* | ---------------------------------------------------------- | ----------------------- | ----------------- |
|
|
222
|
+
* | more than {@link APP_BUILD_MAX_VALUES} values | `tooManyBuildValues` | `{ count, max }` |
|
|
223
|
+
* | a value over {@link APP_BUILD_SECRET_MAX_BYTES} bytes | `buildValueTooLarge` | `{ name }` only |
|
|
224
|
+
* | an env name mapping onto `EW_VERIFY__PROMPTED` | `buildValueNameReserved`| `{ name }` |
|
|
225
|
+
* | GitHub answers the repository secret limit | `secretLimitReached` | `{}` |
|
|
226
|
+
*
|
|
227
|
+
* ## Removal is set arithmetic, and it is deliberately narrow
|
|
228
|
+
*
|
|
229
|
+
* The delete set is exactly `previouslyWrittenSecretNames` **minus** the names
|
|
230
|
+
* this call wrote. A name that was never written by the platform is never deleted,
|
|
231
|
+
* whatever it looks like — including one that starts with `EW_`, which is how an
|
|
232
|
+
* owner's own repository secret survives a preparation (FR-18).
|
|
233
|
+
*
|
|
234
|
+
* A DELETE that GitHub answers with 404 counts as removed: the name is already
|
|
235
|
+
* gone (an owner deleted it by hand, or an earlier preparation deleted it and
|
|
236
|
+
* failed before its row was written), and FR-18 is unaffected because the delete
|
|
237
|
+
* set is still only names the platform wrote. Throwing instead would leave the
|
|
238
|
+
* name on the row and fail every later preparation on the same 404, forever. A
|
|
239
|
+
* 404 from the public-key fetch or a `PUT` is NOT tolerated — there it means the
|
|
240
|
+
* repository itself is gone or not visible.
|
|
241
|
+
*/
|
|
242
|
+
/** The three repository-secret operations this sync needs, and nothing else (a fake is four lines). */
|
|
243
|
+
interface RepositorySecretPort {
|
|
244
|
+
/** The repository's Actions public key — fetched once per operation (plan §4.7:916). */
|
|
245
|
+
getRepoPublicKey(): Promise<{
|
|
246
|
+
readonly key_id: string;
|
|
247
|
+
readonly key: string;
|
|
248
|
+
}>;
|
|
249
|
+
putRepoSecret(input: {
|
|
250
|
+
readonly name: string;
|
|
251
|
+
readonly encryptedValue: string;
|
|
252
|
+
readonly keyId: string;
|
|
253
|
+
}): Promise<void>;
|
|
254
|
+
deleteRepoSecret(input: {
|
|
255
|
+
readonly name: string;
|
|
256
|
+
}): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
/** Names and counts only. A logger that receives a value is a leak, and the spec proves it does not. */
|
|
259
|
+
interface SecretSyncLogger {
|
|
260
|
+
debug?(message: string, meta?: Record<string, unknown>): void;
|
|
261
|
+
info?(message: string, meta?: Record<string, unknown>): void;
|
|
262
|
+
warn?(message: string, meta?: Record<string, unknown>): void;
|
|
263
|
+
error?(message: string, meta?: Record<string, unknown>): void;
|
|
264
|
+
}
|
|
265
|
+
/** What one build-value sync did. */
|
|
266
|
+
interface BuildValueSyncResult {
|
|
267
|
+
/** The `EW_<NAME>` secrets written, in the order they were written. */
|
|
268
|
+
readonly secretsWritten: readonly string[];
|
|
269
|
+
/** The previously written names deleted, because nothing references them any more. */
|
|
270
|
+
readonly secretsRemoved: readonly string[];
|
|
271
|
+
/** `sha256(sorted (name, fingerprint))` over the values handed in (plan §4.7:918–919). */
|
|
272
|
+
readonly buildInputsHash: string;
|
|
273
|
+
readonly blocked?: {
|
|
274
|
+
readonly reason: AppBuildBlockedReason;
|
|
275
|
+
readonly detail: Record<string, string | number | string[]>;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** What the sync is called with. */
|
|
279
|
+
interface BuildValueSyncInput {
|
|
280
|
+
readonly values: readonly BuildValue[];
|
|
281
|
+
/** The `EW_` names the platform wrote earlier and may now delete (plan §4.7:916–917). */
|
|
282
|
+
readonly previouslyWrittenSecretNames: readonly string[];
|
|
283
|
+
}
|
|
284
|
+
/** The result of writing §4.10's per-verification prompted-value secret. */
|
|
285
|
+
interface VerifyPromptedSecretResult {
|
|
286
|
+
readonly name: string;
|
|
287
|
+
/** The JSON payload's size in bytes — the same 48,000-byte ceiling every other value has. */
|
|
288
|
+
readonly bytes: number;
|
|
289
|
+
readonly blocked?: {
|
|
290
|
+
readonly reason: AppBuildBlockedReason;
|
|
291
|
+
readonly detail: Record<string, string | number | string[]>;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** The factory's dependencies. */
|
|
295
|
+
interface SecretSyncDependencies {
|
|
296
|
+
readonly port: RepositorySecretPort;
|
|
297
|
+
readonly logger?: SecretSyncLogger;
|
|
298
|
+
/** Test seam for the seal. Defaults to {@link sealSecretValue} — the real libsodium sealed box. */
|
|
299
|
+
readonly seal?: (value: string, publicKey: string) => Promise<string>;
|
|
300
|
+
}
|
|
301
|
+
/** The operations §4.7 and §4.10 need, bound to one repository port. */
|
|
302
|
+
interface BuildValueSecretSync {
|
|
303
|
+
syncBuildValues(input: BuildValueSyncInput): Promise<BuildValueSyncResult>;
|
|
304
|
+
/** §4.10: the prompted values the owner set, as **one** JSON secret, just before dispatch. */
|
|
305
|
+
writeVerifyPromptedSecret(values: readonly BuildValue[]): Promise<VerifyPromptedSecretResult>;
|
|
306
|
+
/** §4.10: delete that secret by name, on the Build's terminal transition and in the sweep. */
|
|
307
|
+
deleteVerifyPromptedSecret(): Promise<{
|
|
308
|
+
readonly deleted: boolean;
|
|
309
|
+
}>;
|
|
310
|
+
}
|
|
311
|
+
/** GitHub's own secret-name rule, as the github plugin applies it (plan §4.7:914). */
|
|
312
|
+
declare const GITHUB_SECRET_NAME_PATTERN: RegExp;
|
|
313
|
+
/** True when GitHub would accept this secret name. */
|
|
314
|
+
declare function isValidGitHubSecretName(name: string): boolean;
|
|
315
|
+
/** The `EW_` name a build value is stored under. */
|
|
316
|
+
declare function buildSecretName(valueName: string): string;
|
|
317
|
+
declare function loadSodium(): Promise<typeof sodiumType>;
|
|
318
|
+
/**
|
|
319
|
+
* Seal one value with a repository public key — the sequence of
|
|
320
|
+
* `github-actions.service.ts:64-73`, unchanged.
|
|
321
|
+
*/
|
|
322
|
+
declare function sealSecretValue(value: string, publicKey: string): Promise<string>;
|
|
323
|
+
/** GitHub's repository-secret limit answer, recognised without pinning a message GitHub may reword. */
|
|
324
|
+
declare function isSecretLimitError(error: unknown): boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Build the §4.7/§4.10 operations over one repository port.
|
|
327
|
+
*
|
|
328
|
+
* Every operation fetches the public key once, at its own start: a key can rotate
|
|
329
|
+
* between a preparation and a verification, and a cached key would seal a value
|
|
330
|
+
* GitHub then refuses to decrypt.
|
|
331
|
+
*/
|
|
332
|
+
declare function createBuildValueSecretSync(dependencies: SecretSyncDependencies): BuildValueSecretSync;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* APW-05 T7 — the `github-actions-build` plugin settings (plan §4.4, setting by
|
|
336
|
+
* setting), and the one place the two platform-managed keys are declared.
|
|
337
|
+
*
|
|
338
|
+
* Every key, default, bound and marker below is transcribed from plan §4.4's
|
|
339
|
+
* table. Nothing is invented: no key appears that the table does not state, so a
|
|
340
|
+
* tightening of this schema cannot silently refuse a configuration the plan
|
|
341
|
+
* allows.
|
|
342
|
+
*
|
|
343
|
+
* ## What is load-bearing here, and why (pinned by
|
|
344
|
+
* `src/__tests__/plugin.manifest.spec.ts` rather than asserted in prose)
|
|
345
|
+
*
|
|
346
|
+
* - **`pullToken` is `x-secret`** (Constitution VII, plan §4.12). The marker is
|
|
347
|
+
* the only thing that makes the platform treat the value as a secret:
|
|
348
|
+
* `packages/plugin/src/api/api-response.types.ts` turns it into
|
|
349
|
+
* `secret: true` on the descriptor every API response is built from, and
|
|
350
|
+
* `plugin-operations.service.ts` masks exactly those fields. Drop the marker
|
|
351
|
+
* and the pull token becomes an ordinary setting, readable back out of the
|
|
352
|
+
* settings API.
|
|
353
|
+
* - **`pullToken` and `pullTokenExpiresAt` are `x-platformManaged`** (plan
|
|
354
|
+
* §4.4, `APW05-G07`): only `AppBuildPullTokenService.save` (APW-05 T29)
|
|
355
|
+
* writes them, through `PluginSettingsService.writePlatformManagedWorkSettings`,
|
|
356
|
+
* and the generic settings PATCH route must refuse both at every scope.
|
|
357
|
+
* - **no preparation key is declared** (`APW05-G03`). `workflowSha256`,
|
|
358
|
+
* `workflowPullRequestNumber`, `webhookId`, `runsEtag` and `repositoryBlock`
|
|
359
|
+
* are *preparation* state: their one home is the per-App-Work
|
|
360
|
+
* `work_build_preparations` row (plan §3.1b, landed in APW-05 T4/T5/T6), so
|
|
361
|
+
* a user cannot PATCH what only the platform may write. Declaring one here
|
|
362
|
+
* would put it back in reach of the settings API.
|
|
363
|
+
*
|
|
364
|
+
* ## `x-platformManaged` and the shared schema type
|
|
365
|
+
*
|
|
366
|
+
* `PluginSchemaExtensions` (`packages/plugin/src/settings/json-schema.types.ts`)
|
|
367
|
+
* does not declare `x-platformManaged`, and `PluginSettingsService.validateSettingsScope`
|
|
368
|
+
* (`packages/agent/src/plugins/services/plugin-settings.service.ts:812`) does not
|
|
369
|
+
* read it yet. Both files belong to other epics' slices, so the marker is
|
|
370
|
+
* declared **locally** by {@link PlatformManagedJsonSchema} — the narrowest
|
|
371
|
+
* annotation that lets plan §4.4's two markers exist in the schema today without
|
|
372
|
+
* a cast that would hide them. When the shared type grows the flag, this
|
|
373
|
+
* interface collapses to `JsonSchema` and nothing else moves. Until the service
|
|
374
|
+
* reads it, the marker is declared and asserted here while the refusal it
|
|
375
|
+
* enables is T29's (`APW05-G07`).
|
|
376
|
+
*/
|
|
377
|
+
/**
|
|
378
|
+
* `JsonSchema` plus plan §4.4's platform-managed marker.
|
|
379
|
+
*
|
|
380
|
+
* A local widening, deliberately: the shared extension interface is owned by
|
|
381
|
+
* another slice (see the file docstring).
|
|
382
|
+
*/
|
|
383
|
+
interface PlatformManagedJsonSchema extends JsonSchema {
|
|
384
|
+
/** Written by the platform only; the settings PATCH route refuses it at every scope (plan §4.4, `APW05-G07`). */
|
|
385
|
+
readonly 'x-platformManaged'?: boolean;
|
|
386
|
+
}
|
|
387
|
+
/** This plugin's settings schema — a {@link JsonSchema} whose properties may carry {@link PlatformManagedJsonSchema}. */
|
|
388
|
+
interface GitHubActionsBuildSettingsSchema extends JsonSchema {
|
|
389
|
+
readonly properties?: Record<string, JsonSchema | PlatformManagedJsonSchema>;
|
|
390
|
+
}
|
|
391
|
+
/** `largerRunnerLabel` (plan §4.4): private repositories only, when set. */
|
|
392
|
+
declare const LARGER_RUNNER_LABEL_PATTERN = "^[A-Za-z0-9._-]{1,64}$";
|
|
393
|
+
/** Memory the platform reserves before a declared `memoryGiB` is allowed to fit (plan §4.4: `memory − 2`). */
|
|
394
|
+
declare const LARGER_RUNNER_MEMORY_MIN_GIB = 8;
|
|
395
|
+
/** Upper bound of `largerRunnerMemoryGiB` (plan §4.4). */
|
|
396
|
+
declare const LARGER_RUNNER_MEMORY_MAX_GIB = 256;
|
|
397
|
+
/** Lower bound of `largerRunnerVcpu` (plan §4.4). */
|
|
398
|
+
declare const LARGER_RUNNER_VCPU_MIN = 2;
|
|
399
|
+
/** Upper bound of `largerRunnerVcpu` (plan §4.4). */
|
|
400
|
+
declare const LARGER_RUNNER_VCPU_MAX = 64;
|
|
401
|
+
/**
|
|
402
|
+
* Every settings key this plugin declares, in plan §4.4's table order.
|
|
403
|
+
*
|
|
404
|
+
* Exported so the spec can assert the set, the order and the markers against one
|
|
405
|
+
* list instead of three hand-copied ones.
|
|
406
|
+
*/
|
|
407
|
+
declare const GITHUB_ACTIONS_BUILD_SETTING_KEYS: readonly ["largerRunnerLabel", "largerRunnerMemoryGiB", "largerRunnerVcpu", "reclaimDisk", "attestations", "pullToken", "pullTokenExpiresAt", "allowBuildValuesOnPullRequests", "verificationPromptedValuesRequireApproval"];
|
|
408
|
+
/** A settings key of this plugin (plan §4.4). */
|
|
409
|
+
type GitHubActionsBuildSettingKey = (typeof GITHUB_ACTIONS_BUILD_SETTING_KEYS)[number];
|
|
410
|
+
/** The keys the platform writes and a user may never set (plan §4.4, `APW05-G07`). */
|
|
411
|
+
declare const GITHUB_ACTIONS_BUILD_PLATFORM_MANAGED_KEYS: readonly ["pullToken", "pullTokenExpiresAt"];
|
|
412
|
+
/**
|
|
413
|
+
* The preparation keys of plan §3.1b that this schema must NOT declare
|
|
414
|
+
* (`APW05-G03`). Named here so the negative assertion has one authority, and so
|
|
415
|
+
* the collision with the `work_build_preparations` columns is visible from the
|
|
416
|
+
* plugin side too.
|
|
417
|
+
*/
|
|
418
|
+
declare const PREPARATION_STATE_KEYS: readonly ["workflowSha256", "workflowPullRequestNumber", "webhookId", "runsEtag", "repositoryBlock"];
|
|
419
|
+
/** The defaults plan §4.4 gives the four booleans and the two runner numbers. */
|
|
420
|
+
declare const GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS: {
|
|
421
|
+
readonly largerRunnerLabel: "";
|
|
422
|
+
readonly largerRunnerMemoryGiB: 0;
|
|
423
|
+
readonly largerRunnerVcpu: 0;
|
|
424
|
+
readonly reclaimDisk: true;
|
|
425
|
+
readonly attestations: false;
|
|
426
|
+
readonly allowBuildValuesOnPullRequests: false;
|
|
427
|
+
readonly verificationPromptedValuesRequireApproval: true;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* The settings this plugin reads once resolved (plan §4.4).
|
|
431
|
+
*
|
|
432
|
+
* `pullToken`/`pullTokenExpiresAt` are optional: they exist only after
|
|
433
|
+
* `AppBuildPullTokenService` has written them (plan §4.12).
|
|
434
|
+
*/
|
|
435
|
+
interface GitHubActionsBuildSettings {
|
|
436
|
+
largerRunnerLabel?: string;
|
|
437
|
+
largerRunnerMemoryGiB?: number;
|
|
438
|
+
largerRunnerVcpu?: number;
|
|
439
|
+
reclaimDisk?: boolean;
|
|
440
|
+
attestations?: boolean;
|
|
441
|
+
pullToken?: string;
|
|
442
|
+
pullTokenExpiresAt?: string;
|
|
443
|
+
allowBuildValuesOnPullRequests?: boolean;
|
|
444
|
+
verificationPromptedValuesRequireApproval?: boolean;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Plan §4.4's table as a JSON Schema.
|
|
448
|
+
*
|
|
449
|
+
* Scope, resolved from the table's own statements rather than from its summary
|
|
450
|
+
* sentence, which pulls in two directions: the notes mark **Work scope only**
|
|
451
|
+
* for `allowBuildValuesOnPullRequests` and `verificationPromptedValuesRequireApproval`
|
|
452
|
+
* explicitly, and plan §4.4:799 says "Work scope only for `pullToken`,
|
|
453
|
+
* `largerRunner*`, `attestations`; user and admin scope may set defaults for
|
|
454
|
+
* `reclaimDisk` and `largerRunner*`". Both cannot hold for `largerRunner*`, so
|
|
455
|
+
* the schema follows the second half (a user/admin **default** that a Work
|
|
456
|
+
* overrides) and keeps `pullToken`, `pullTokenExpiresAt`, `attestations` and the
|
|
457
|
+
* two XC-01 booleans at `x-scope: 'work'`, which is what the platform's scope
|
|
458
|
+
* model can express. Recorded for T11, which owns the runner selector and reads
|
|
459
|
+
* these three keys.
|
|
460
|
+
*/
|
|
461
|
+
declare const gitHubActionsBuildSettingsSchema: GitHubActionsBuildSettingsSchema;
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* APW-05 T7 — the `github-actions-build` plugin: the identity the platform's
|
|
465
|
+
* discovery path and the `build` capability need, the settings of plan §4.4, and
|
|
466
|
+
* the members T8–T10 have not reached yet.
|
|
467
|
+
*
|
|
468
|
+
* What exists here:
|
|
469
|
+
*
|
|
470
|
+
* - the manifest the `everworks.plugin` block in `package.json` also declares
|
|
471
|
+
* (they must agree — `plugin-manifest-validator.service.ts` extracts the
|
|
472
|
+
* block from disk and `plugin-class-validator.service.ts` compares the loaded
|
|
473
|
+
* class against it);
|
|
474
|
+
* - `settingsSchema` — plan §4.4, with `pullToken` `x-secret` and both
|
|
475
|
+
* platform-managed markers (`APW05-G07`);
|
|
476
|
+
* - the two lifecycle hooks the loader calls. Both are **prototype methods on
|
|
477
|
+
* purpose**: `PluginClassValidatorService.isPluginClass` accepts a class only
|
|
478
|
+
* when `onLoad` and `onUnload` sit on its prototype, so an arrow-function
|
|
479
|
+
* property would make this plugin undiscoverable.
|
|
480
|
+
*
|
|
481
|
+
* ## Every `IBuildPlugin` member is implemented (Constitution VI, Rule: a
|
|
482
|
+
* ## capability a plugin claims, it implements — no member pretends)
|
|
483
|
+
*
|
|
484
|
+
* **All five, as of 2026-09-22.** T12 did four on 2026-09-21; T11's runner
|
|
485
|
+
* selector landed the next day, T41's checks job turned out to be already
|
|
486
|
+
* written, and §4.6 could then be composed — so `prepareRepository` is the last
|
|
487
|
+
* one and the `notImplemented` list is empty.
|
|
488
|
+
*
|
|
489
|
+
* | Member | Detail |
|
|
490
|
+
* | --------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
491
|
+
* | `prepareRepository` | §4.6: select the runner (T11), generate the file (T8) with the checks job (T41), deliver it (T9), then sync the build values (T10) — in that order, so nothing reaches a member's repository until it is known the Build could run |
|
|
492
|
+
* | `startBuild` | dispatches the generated file on the TRACKED branch with `ew_build_id` / `ew_sha` / `ew_mode` and the two optional inputs, then correlates once |
|
|
493
|
+
* | `getBuild` | run + jobs → `BuildSnapshot` via `runs/run-observer.ts`, plus the result artifact for a finished run and, for a FAILED one, the log tail (T13) classified into `failure`. T43 fills `verification` |
|
|
494
|
+
* | `cancelBuild` | resolves the same run `getBuild` would, then the cancel endpoint |
|
|
495
|
+
* | `getLogsUrl` | the run's own page |
|
|
496
|
+
*
|
|
497
|
+
* What is deliberately still NOT done, so nobody reads the table above as more
|
|
498
|
+
* than it says:
|
|
499
|
+
*
|
|
500
|
+
* - the **404/422 retry after a bootstrap commit** (`APW05-G02`) — a
|
|
501
|
+
* just-written workflow file is briefly undispatchable;
|
|
502
|
+
* - a digest read from the artifact is reported `confirmed: false` **always**.
|
|
503
|
+
* Confirming it is `checkImageAccess` (T14, below) and the comparison belongs
|
|
504
|
+
* to the caller, `AppBuildsService.finalize` (plan §4.8). For a succeeded
|
|
505
|
+
* push or manual run the snapshot also carries `image.pushLogDigest`, the
|
|
506
|
+
* digest the build job's Push step logged (`runs/push-digest.ts`), which the
|
|
507
|
+
* caller weighs only when the registry cannot be read;
|
|
508
|
+
* - a failure whose log has EXPIRED classifies as `unknown`. GitHub deletes
|
|
509
|
+
* job logs on the repository's retention schedule, and a Build observed
|
|
510
|
+
* after that is genuinely unexplainable — reporting `unknown` is the honest
|
|
511
|
+
* answer, and re-running the Build is the only way to get a better one.
|
|
512
|
+
*
|
|
513
|
+
* `checkImageAccess?` (T14) IS declared now, because it does something: it reads
|
|
514
|
+
* the manifest through GHCR's token exchange (`ghcr.io/token`, then a `HEAD` with
|
|
515
|
+
* the registry-issued bearer) — anonymously, then with the pull token presented
|
|
516
|
+
* to `/token` as Basic credentials — and checks that token's scopes through
|
|
517
|
+
* `GET /user`. It is the confirming half of §4.8's "only ever confirmed, never
|
|
518
|
+
* believed" — `getBuild` still reports an artifact digest as `confirmed: false`
|
|
519
|
+
* always, and `AppBuildsService.finalize` does the comparison, because it is the
|
|
520
|
+
* only party holding both the Build row and the `pullToken` setting.
|
|
521
|
+
*
|
|
522
|
+
* `listRecentRuns?` (T12's run discovery, §7.4a) is still deliberately **not
|
|
523
|
+
* declared**: it is optional on the contract and a caller materialises the member
|
|
524
|
+
* before calling it, so declaring a throwing placeholder would buy nothing and
|
|
525
|
+
* would tell a future reader the capability is wired when it is not.
|
|
526
|
+
*/
|
|
527
|
+
declare class GitHubActionsBuildPlugin implements IBuildPlugin {
|
|
528
|
+
readonly id = "github-actions-build";
|
|
529
|
+
readonly name = "GitHub Actions builds";
|
|
530
|
+
readonly version = "1.0.0";
|
|
531
|
+
/**
|
|
532
|
+
* Plan §4.3: the `build` category — appended to `PLUGIN_CATEGORIES` by APW-05
|
|
533
|
+
* T3 (`packages/plugin/src/contracts/plugin-manifest.types.ts`), so the
|
|
534
|
+
* loader's own `isPluginCategory` gate accepts this manifest.
|
|
535
|
+
*/
|
|
536
|
+
readonly category: PluginCategory;
|
|
537
|
+
/** Plan §4.3: exactly one capability. `isBuildPlugin` resolves this plugin on it. */
|
|
538
|
+
readonly capabilities: readonly string[];
|
|
539
|
+
/** Plan §4.3. `AppBuildKind` — a closed union in `@ever-works/contracts`. */
|
|
540
|
+
readonly buildKind: AppBuildKind;
|
|
541
|
+
/**
|
|
542
|
+
* Plan §4.1/R-13: `dockerfile` only.
|
|
543
|
+
*
|
|
544
|
+
* `auto` is deliberately absent: the zero-config builder is a provider-internal
|
|
545
|
+
* choice this plugin does not have, so the strategy gate leaves that Build
|
|
546
|
+
* unstarted (`strategyNotSupported`) rather than half-built. `image` and `none`
|
|
547
|
+
* are absent for the same reason — they need no build at all, and the prepare
|
|
548
|
+
* runner handles them before a build plugin is asked (plan §7.2).
|
|
549
|
+
*/
|
|
550
|
+
readonly supportedStrategies: readonly BuildStrategy[];
|
|
551
|
+
/** Work-scoped settings, like the other per-Work provider plugins. */
|
|
552
|
+
readonly configurationMode = "user-required";
|
|
553
|
+
/** Plan §4.4, exactly. */
|
|
554
|
+
readonly settingsSchema: JsonSchema;
|
|
555
|
+
onLoad(context: PluginContext): Promise<void>;
|
|
556
|
+
onUnload(): Promise<void>;
|
|
557
|
+
/**
|
|
558
|
+
* The Actions API this plugin talks to, bound per call from the caller's auth.
|
|
559
|
+
*
|
|
560
|
+
* `protected` and a method, not a constructor seam: the loader constructs a
|
|
561
|
+
* plugin with **no arguments** (`PluginLoaderService.loadPluginModule` —
|
|
562
|
+
* `new PluginClass()`), so anything a spec wants to replace has to be
|
|
563
|
+
* replaceable on the instance. A spec subclasses and overrides this; production
|
|
564
|
+
* gets Octokit, which is the official SDK and already this package's dependency.
|
|
565
|
+
*
|
|
566
|
+
* A fresh client per call, deliberately: `auth.token` is resolved per call by
|
|
567
|
+
* the facade (plan §4.1) and may be a different member's installation token
|
|
568
|
+
* from one call to the next. Caching one would be caching a credential.
|
|
569
|
+
*/
|
|
570
|
+
protected actionsPort(auth: BuildAuth): ActionsRunsPort;
|
|
571
|
+
/** The clock, as a seam, so the correlation window is provable without waiting. */
|
|
572
|
+
protected now(): number;
|
|
573
|
+
getManifest(): PluginManifest;
|
|
574
|
+
/**
|
|
575
|
+
* APW-05 §4.6 — put the workflow (and the build values) where a Build can use them.
|
|
576
|
+
*
|
|
577
|
+
* Four steps, in this order, and the order is the point: nothing is written to a
|
|
578
|
+
* member's repository until it is known the Build could run at all.
|
|
579
|
+
*
|
|
580
|
+
* 1. **Select the runner** (T11). A declared memory the chosen runner cannot
|
|
581
|
+
* give is `runnerTooSmall`, and it BLOCKS here — before a commit, before a
|
|
582
|
+
* secret, before anything reaches GitHub. The alternative is writing a
|
|
583
|
+
* workflow that is guaranteed to be OOM-killed minutes into its first run.
|
|
584
|
+
* 2. **Generate the file** (T8), with the checks job (T41) when the spec
|
|
585
|
+
* declares checks and the verify job when the App Work can verify.
|
|
586
|
+
* 3. **Deliver it** (T9): unchanged, committed, or a pull request for a
|
|
587
|
+
* repository that is not ours to write into (R-4). A hand-edited file is
|
|
588
|
+
* `editedByHand` and is never overwritten.
|
|
589
|
+
* 4. **Sync the build values** (T10), and only then — a secret written for a
|
|
590
|
+
* workflow that was never delivered is a secret nobody will clean up.
|
|
591
|
+
*
|
|
592
|
+
* ## The two failure shapes, kept apart
|
|
593
|
+
*
|
|
594
|
+
* A `blocked` result is a Build that will not start and a member who is told
|
|
595
|
+
* why. A THROW is this plugin failing at its own job. `writeWorkflow`'s own
|
|
596
|
+
* `'blocked'` state is mapped onto `PrepareRepositoryResult.blocked` rather than
|
|
597
|
+
* passed through, because that state is the writer's and is not one of the five
|
|
598
|
+
* the contract's `workflow.state` allows — the writer's docstring says so, and
|
|
599
|
+
* storing it would put a value in the database that no reader understands.
|
|
600
|
+
*
|
|
601
|
+
* ## `contentSha256` is stored only after a matching read-back
|
|
602
|
+
*
|
|
603
|
+
* `storedWorkflowSha256` is `null` until the writer has PROVEN the tracked
|
|
604
|
+
* branch holds the generated bytes (FR-8). Reporting the hash of what we sent,
|
|
605
|
+
* rather than of what is there, is how a failed write becomes an unnoticed
|
|
606
|
+
* hand-edit detection failure on the next preparation.
|
|
607
|
+
*/
|
|
608
|
+
prepareRepository(input: PrepareRepositoryInput, auth: BuildAuth, writer: RepositoryWriter): Promise<PrepareRepositoryResult>;
|
|
609
|
+
/**
|
|
610
|
+
* The repository-secret operations T10's sync needs, over Octokit.
|
|
611
|
+
*
|
|
612
|
+
* A seam for the same reason {@link actionsPort} is one: the loader constructs
|
|
613
|
+
* this class with no arguments, so a spec replaces it on the instance.
|
|
614
|
+
*/
|
|
615
|
+
protected secretPort(auth: BuildAuth, repository: {
|
|
616
|
+
readonly owner: string;
|
|
617
|
+
readonly repo: string;
|
|
618
|
+
}): RepositorySecretPort;
|
|
619
|
+
/**
|
|
620
|
+
* APW-05 T12 — dispatch the generated workflow on the App Work's tracked branch.
|
|
621
|
+
*
|
|
622
|
+
* ## `providerRunId` is `null` here, and that is not a failure
|
|
623
|
+
*
|
|
624
|
+
* `POST .../dispatches` answers **204 with no body**: GitHub says nothing about
|
|
625
|
+
* the run it just created. So this method dispatches, records the instant, and
|
|
626
|
+
* makes ONE correlation attempt; the run is usually not listed that fast, and a
|
|
627
|
+
* `null` id is the normal answer. `app-build-watch` polls `getBuild`, which
|
|
628
|
+
* correlates again on every poll. Blocking here until a run appeared would hold
|
|
629
|
+
* the caller's request open for an arbitrary provider delay.
|
|
630
|
+
*
|
|
631
|
+
* ## The branch is the tracked branch, never the caller's ref
|
|
632
|
+
*
|
|
633
|
+
* FR-14: a Build may only run for the App Work's tracked branch. `input.ref` is
|
|
634
|
+
* what to BUILD; `repository.trackedBranch` is where the workflow file lives and
|
|
635
|
+
* the only `ref` a dispatch is allowed to name. Passing `input.ref` would let a
|
|
636
|
+
* Build run a workflow file from a branch nobody reviewed.
|
|
637
|
+
*/
|
|
638
|
+
startBuild(input: StartBuildInput, auth: BuildAuth): Promise<{
|
|
639
|
+
providerRunId: string | null;
|
|
640
|
+
dispatchedAt: string;
|
|
641
|
+
}>;
|
|
642
|
+
/**
|
|
643
|
+
* APW-05 T12 — observe one run.
|
|
644
|
+
*
|
|
645
|
+
* Correlates first when the ref carries no `providerRunId`: a Build dispatched
|
|
646
|
+
* moments ago has none, and the watch job's whole purpose is to find it. `null`
|
|
647
|
+
* means "no such run" — never "I could not tell", which the contract says is a
|
|
648
|
+
* throw, and which is why a failed API call is not caught here.
|
|
649
|
+
*
|
|
650
|
+
* `redact` is APW-07's redactor for this App Work, and it is USED: every line
|
|
651
|
+
* of every excerpt goes through it, and through the secret-shape mask after
|
|
652
|
+
* it, before it leaves this method (FR-38).
|
|
653
|
+
*/
|
|
654
|
+
getBuild(ref: BuildRef, auth: BuildAuth, redact: (text: string) => string): Promise<BuildSnapshot | null>;
|
|
655
|
+
/**
|
|
656
|
+
* APW-05 T13 — why a failed run failed, as `BuildSnapshot.failure`.
|
|
657
|
+
*
|
|
658
|
+
* ## Which job's log
|
|
659
|
+
*
|
|
660
|
+
* The `build` job decides the Build's status (`run-observer.ts`, R-9), so when
|
|
661
|
+
* it failed it is the one worth reading. Only when it did NOT fail — a Build
|
|
662
|
+
* the run marked failed for some other reason — does any other failing job
|
|
663
|
+
* stand in. A failed `Ever Works check:` job is deliberately last: it never
|
|
664
|
+
* flips a Build to `failed`, so if it is the only failure the run's own
|
|
665
|
+
* conclusion is what actually needs explaining.
|
|
666
|
+
*
|
|
667
|
+
* No failing job at all is not an error: `jobCount` then carries the run into
|
|
668
|
+
* the classifier's `workflowInvalid` row, which is exactly what a run that
|
|
669
|
+
* produced no jobs is.
|
|
670
|
+
*
|
|
671
|
+
* ## Why the artifact's smoke rows are passed in
|
|
672
|
+
*
|
|
673
|
+
* `verificationFailed` is a class the LOG cannot show: the smoke rows live in
|
|
674
|
+
* the result artifact, which `getBuild` has already read by this point. Passing
|
|
675
|
+
* them costs nothing and is the difference between "your build failed" and
|
|
676
|
+
* "your app built, and then did not answer".
|
|
677
|
+
*/
|
|
678
|
+
private classifyRunFailure;
|
|
679
|
+
/**
|
|
680
|
+
* APW-05 T14 remainder — the digest the build job's `Push` step logged for
|
|
681
|
+
* this Build's `sha-<sha>` tag, or `undefined`.
|
|
682
|
+
*
|
|
683
|
+
* The BUILD job's log, because that is the job holding the Push step; read as
|
|
684
|
+
* the same bounded tail T13 reads (the Push step is near the end of the job,
|
|
685
|
+
* followed only by the result write and the upload). An unreadable or expired
|
|
686
|
+
* log is `''` from `readJobLogTail`, so this never fails an observation — it
|
|
687
|
+
* only costs the fallback. Which lines count is `runs/push-digest.ts`'s rule.
|
|
688
|
+
*/
|
|
689
|
+
private readPushLogDigest;
|
|
690
|
+
/**
|
|
691
|
+
* The run this ref addresses: the one it names, or the one correlation finds.
|
|
692
|
+
*
|
|
693
|
+
* Shared by `getBuild`, `cancelBuild` and `getLogsUrl` so all three agree about
|
|
694
|
+
* which run a ref means — a cancel that addressed a different run from the
|
|
695
|
+
* observation would stop the wrong Build.
|
|
696
|
+
*/
|
|
697
|
+
private resolveRunId;
|
|
698
|
+
/**
|
|
699
|
+
* APW-05 T12 — stop the run.
|
|
700
|
+
*
|
|
701
|
+
* A ref that resolves to no run is a no-op rather than a throw: the caller asked
|
|
702
|
+
* for the Build to stop, and a Build with no run has stopped. The port swallows
|
|
703
|
+
* GitHub's 409 ("not cancellable") for the same reason — a finished run is not a
|
|
704
|
+
* failed cancel.
|
|
705
|
+
*/
|
|
706
|
+
cancelBuild(ref: BuildRef, auth: BuildAuth): Promise<void>;
|
|
707
|
+
/**
|
|
708
|
+
* APW-05 T14 — can this installation read the image, and is its pull token the
|
|
709
|
+
* right shape?
|
|
710
|
+
*
|
|
711
|
+
* Declared now that it does something. It stayed undeclared while it did not,
|
|
712
|
+
* because the contract makes it optional and a caller materialises the member
|
|
713
|
+
* before calling it — so a throwing placeholder would have told a reader the
|
|
714
|
+
* capability was wired when it was not.
|
|
715
|
+
*
|
|
716
|
+
* This is the **confirming** half of plan §4.8's "only ever confirmed, never
|
|
717
|
+
* believed". `getBuild` reports the digest the member's own CI claimed with
|
|
718
|
+
* `confirmed: false`, always; comparing it against the digest this method reads
|
|
719
|
+
* from the registry is what makes it true, and that comparison belongs to the
|
|
720
|
+
* caller — `AppBuildsService.finalize` — which is the only party holding both
|
|
721
|
+
* the Build row and the installation's `pullToken` setting (`getBuild(ref, auth)`
|
|
722
|
+
* is given neither).
|
|
723
|
+
*
|
|
724
|
+
* The registry is read through GHCR's own token exchange (`registry/ghcr-access.ts`):
|
|
725
|
+
* a bare manifest `HEAD` is 401 for every image on GHCR, public or not, so a
|
|
726
|
+
* public image is only ever seen as public through the anonymous registry token.
|
|
727
|
+
* The pull token reaches `ghcr.io/token` (as Basic) and `api.github.com/user`,
|
|
728
|
+
* and nothing else.
|
|
729
|
+
*/
|
|
730
|
+
checkImageAccess(input: {
|
|
731
|
+
readonly imageRepository: string;
|
|
732
|
+
readonly tag: string;
|
|
733
|
+
readonly pullToken?: string;
|
|
734
|
+
}): Promise<GhcrAccessResult>;
|
|
735
|
+
/**
|
|
736
|
+
* The registry/API HTTP seam.
|
|
737
|
+
*
|
|
738
|
+
* `redirect: 'manual'` is set by `ghcr-access.ts` on every request and passed
|
|
739
|
+
* straight through here: a 3xx followed with the `Authorization` header still
|
|
740
|
+
* attached is how a pull token reaches a host that is neither `api.github.com`
|
|
741
|
+
* nor `ghcr.io`, and that module's spec watches every call for exactly that.
|
|
742
|
+
*/
|
|
743
|
+
protected registryFetch(): GhcrFetch;
|
|
744
|
+
/** APW-05 T12 — the run's own page, which is where a member reads its logs. */
|
|
745
|
+
getLogsUrl(ref: BuildRef, auth: BuildAuth): Promise<string | null>;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* The error every unimplemented member throws.
|
|
749
|
+
*
|
|
750
|
+
* One shape and one message format, so a caller can tell "this plugin does not
|
|
751
|
+
* do that yet" from a provider failure — and so the message names the task that
|
|
752
|
+
* will make it true rather than a bare "not implemented".
|
|
753
|
+
*/
|
|
754
|
+
declare function notImplemented(member: string, owner: string): Error;
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* APW-05 T8 — the third-party actions the generated workflow is allowed to use,
|
|
758
|
+
* pinned to a commit.
|
|
759
|
+
*
|
|
760
|
+
* Plan §4.5: "`ACTION_PINS` values are 40-character commit hashes with the
|
|
761
|
+
* release tag in a trailing comment; a unit test fails when any pin is not 40 hex
|
|
762
|
+
* characters. Bumping pins bumps `inputsHash`, so every App Work gets a pull
|
|
763
|
+
* request or commit with the new file on its next preparation."
|
|
764
|
+
*
|
|
765
|
+
* ## How these five were resolved (and how to bump them)
|
|
766
|
+
*
|
|
767
|
+
* Each `sha` is the **commit** a release tag pointed at when this file was
|
|
768
|
+
* written, read with `git ls-remote --tags https://github.com/<owner>/<repo>`,
|
|
769
|
+
* peeling annotated tags (`^{}`) so a tag object's hash can never be mistaken for
|
|
770
|
+
* a commit. `resolvedOn` records the day. A bump is: re-run that command, take
|
|
771
|
+
* the newest stable tag's commit, update `sha` + `tag` + `resolvedOn` — nothing
|
|
772
|
+
* else changes, and `inputsHash` moves on its own.
|
|
773
|
+
*
|
|
774
|
+
* The five names are exactly the plan §4.3 generator's set. A sixth action is
|
|
775
|
+
* not "just another entry": §4.5's pin set is what the golden files pin, so a new
|
|
776
|
+
* action is a new decision about what runs inside a customer's repository.
|
|
777
|
+
*/
|
|
778
|
+
interface ActionPin {
|
|
779
|
+
/** `owner/repo`, as it appears before the `@` in a `uses:` line. */
|
|
780
|
+
readonly action: string;
|
|
781
|
+
/** The release tag this commit was the tip of, for the trailing comment. */
|
|
782
|
+
readonly tag: string;
|
|
783
|
+
/** The 40-character commit hash that must appear after the `@`. */
|
|
784
|
+
readonly sha: string;
|
|
785
|
+
/** The day `sha` was resolved from the tag. */
|
|
786
|
+
readonly resolvedOn: string;
|
|
787
|
+
}
|
|
788
|
+
declare const ACTION_PINS: {
|
|
789
|
+
/** `actions/checkout@v7.0.1`. */
|
|
790
|
+
readonly checkout: {
|
|
791
|
+
readonly action: "actions/checkout";
|
|
792
|
+
readonly tag: "v7.0.1";
|
|
793
|
+
readonly sha: "3d3c42e5aac5ba805825da76410c181273ba90b1";
|
|
794
|
+
readonly resolvedOn: "2026-09-18";
|
|
795
|
+
};
|
|
796
|
+
/** `docker/setup-buildx-action@v4.4.1`. */
|
|
797
|
+
readonly setupBuildx: {
|
|
798
|
+
readonly action: "docker/setup-buildx-action";
|
|
799
|
+
readonly tag: "v4.4.1";
|
|
800
|
+
readonly sha: "f87e5991a6d7451dcb8d9637bfbc97413f497069";
|
|
801
|
+
readonly resolvedOn: "2026-09-18";
|
|
802
|
+
};
|
|
803
|
+
/** `docker/login-action@v4.6.0`. */
|
|
804
|
+
readonly login: {
|
|
805
|
+
readonly action: "docker/login-action";
|
|
806
|
+
readonly tag: "v4.6.0";
|
|
807
|
+
readonly sha: "dbcb813823bdd20940b903addbd779551569679f";
|
|
808
|
+
readonly resolvedOn: "2026-09-18";
|
|
809
|
+
};
|
|
810
|
+
/** `docker/build-push-action@v7.4.0`. */
|
|
811
|
+
readonly buildPush: {
|
|
812
|
+
readonly action: "docker/build-push-action";
|
|
813
|
+
readonly tag: "v7.4.0";
|
|
814
|
+
readonly sha: "c3c9e263c25d99ce0380d002d59b67737d91b0dc";
|
|
815
|
+
readonly resolvedOn: "2026-09-18";
|
|
816
|
+
};
|
|
817
|
+
/** `actions/upload-artifact@v7.0.1`. */
|
|
818
|
+
readonly uploadArtifact: {
|
|
819
|
+
readonly action: "actions/upload-artifact";
|
|
820
|
+
readonly tag: "v7.0.1";
|
|
821
|
+
readonly sha: "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
|
|
822
|
+
readonly resolvedOn: "2026-09-18";
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
/** The keys of {@link ACTION_PINS}, in declaration order. */
|
|
826
|
+
type ActionPinKey = keyof typeof ACTION_PINS;
|
|
827
|
+
/** A commit hash, and nothing else — plan §4.5's "40 hex characters" (ACC-05-05). */
|
|
828
|
+
declare const ACTION_PIN_SHA_PATTERN: RegExp;
|
|
829
|
+
/** `owner/repo@<40 hex> # <tag>` — the exact `uses:` value, comment included. */
|
|
830
|
+
declare function actionPin(pin: ActionPin): string;
|
|
831
|
+
/** `owner/repo@<40 hex>` — the pin without its trailing comment, for a canonical input. */
|
|
832
|
+
declare function actionPinReference(pin: ActionPin): string;
|
|
833
|
+
/**
|
|
834
|
+
* The pin set in canonical form: key → `owner/repo@<40 hex>`, keys sorted.
|
|
835
|
+
*
|
|
836
|
+
* This is what `inputs-hash.ts` hashes, so a bump moves `inputsHash` (and
|
|
837
|
+
* `resolvedOn` deliberately does **not**: it records when the hash was read, not
|
|
838
|
+
* what will run).
|
|
839
|
+
*/
|
|
840
|
+
declare function canonicalActionPins(): Record<string, string>;
|
|
841
|
+
/**
|
|
842
|
+
* Names the generated workflow must never reference in clear text.
|
|
843
|
+
*
|
|
844
|
+
* Exported for the generator's own guard rather than for a caller: the
|
|
845
|
+
* restricted pull-request literal and the check-name prefix are the two strings
|
|
846
|
+
* the golden and generator specs assert against each other.
|
|
847
|
+
*/
|
|
848
|
+
declare const GENERATOR_RESERVED_LITERALS: readonly ["ew-restricted", "Ever Works check: "];
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* APW-05 T8 — the canonical inputs of a generated workflow and their fingerprint.
|
|
852
|
+
*
|
|
853
|
+
* Plan §4.5: "**Canonical inputs**: `{ generator: 1, trackedBranch, build
|
|
854
|
+
* (normalised: defaults applied, keys sorted), values: names + secret +
|
|
855
|
+
* fromBuildService (never values), runner: { label, class }, settings: {
|
|
856
|
+
* reclaimDisk, attestations }, pins: ACTION_PINS, verifyEnabled, checks: [{ name,
|
|
857
|
+
* required, timeoutMinutes, commandSha256 }] }`. `inputsHash = sha256(canonical
|
|
858
|
+
* JSON)`."
|
|
859
|
+
*
|
|
860
|
+
* ## Why this canonical set is wider than the plan's line, deliberately
|
|
861
|
+
*
|
|
862
|
+
* The line above was written before `XC-01` (2026-09-17) added
|
|
863
|
+
* `allowBuildValuesOnPullRequests`, and it never mentioned the repository, the
|
|
864
|
+
* App spec hash, the bootstrap flag or the embedded verify-runner script — all of
|
|
865
|
+
* which change the generated **bytes**. A fingerprint that stays still while the
|
|
866
|
+
* bytes move is worse than no fingerprint: plan §4.5 relies on it to decide
|
|
867
|
+
* whether an App Work needs a new commit or pull request ("Bumping pins bumps
|
|
868
|
+
* `inputsHash`, so every App Work gets a pull request or commit with the new file
|
|
869
|
+
* on its next preparation"), and §2.4's header line publishes it as the file's
|
|
870
|
+
* identity. So every field below is either the plan's own or one that changes the
|
|
871
|
+
* output, and {@link CANONICAL_INPUT_FIELDS} lists them in one place.
|
|
872
|
+
*
|
|
873
|
+
* `values` keeps the plan's rule exactly: **names, `secret` and
|
|
874
|
+
* `fromBuildService` only — never a value, never a fingerprint.** Two App Works
|
|
875
|
+
* whose `DATABASE_URL` is the same env entry name with different stored values
|
|
876
|
+
* have the same workflow bytes, and that is the point: rotating a secret must not
|
|
877
|
+
* rewrite a workflow file.
|
|
878
|
+
*
|
|
879
|
+
* The JSON form is fixed here rather than left to `JSON.stringify`'s insertion
|
|
880
|
+
* order: keys are written in {@link CANONICAL_INPUT_FIELDS} order, arrays that
|
|
881
|
+
* the caller may hand in unordered (values, build args, services, checks) are
|
|
882
|
+
* sorted by their own key, and the result contains no whitespace — so the same
|
|
883
|
+
* inputs hash the same on every platform (ACC-05-03).
|
|
884
|
+
*/
|
|
885
|
+
/** One build service, canonicalised (plan §2.4 `services`, §4.5 `BUILD_SERVICE_DEFAULTS`). */
|
|
886
|
+
interface CanonicalBuildService {
|
|
887
|
+
readonly name: string;
|
|
888
|
+
readonly image: string;
|
|
889
|
+
/** The declared published port, when the App spec gives one. */
|
|
890
|
+
readonly port?: number;
|
|
891
|
+
/** The declared `env`, sorted by name — the image defaults are applied at emission, not here. */
|
|
892
|
+
readonly env: ReadonlyArray<{
|
|
893
|
+
readonly name: string;
|
|
894
|
+
readonly value: string;
|
|
895
|
+
}>;
|
|
896
|
+
}
|
|
897
|
+
/** One build argument, canonicalised (plan §2.4 `build-args`). */
|
|
898
|
+
interface CanonicalBuildArg {
|
|
899
|
+
readonly name: string;
|
|
900
|
+
/** A literal value, as written in the App spec. */
|
|
901
|
+
readonly value?: string;
|
|
902
|
+
/** The build-value name this argument reads from the repository secrets. */
|
|
903
|
+
readonly fromEnv?: string;
|
|
904
|
+
}
|
|
905
|
+
/** The App spec's `build` block, normalised (plan §4.5: "defaults applied, keys sorted"). */
|
|
906
|
+
interface CanonicalBuildBlock {
|
|
907
|
+
readonly strategy: string;
|
|
908
|
+
/** `build.context`, defaulted to `.` — plan §2.4 `context: <build.context>`. */
|
|
909
|
+
readonly context: string;
|
|
910
|
+
/** `build.dockerfile`, defaulted to `Dockerfile`, relative to the repository root. */
|
|
911
|
+
readonly dockerfile: string;
|
|
912
|
+
readonly target?: string;
|
|
913
|
+
readonly args: readonly CanonicalBuildArg[];
|
|
914
|
+
readonly services: readonly CanonicalBuildService[];
|
|
915
|
+
readonly resources: {
|
|
916
|
+
readonly cpu: number;
|
|
917
|
+
readonly memoryGiB?: number;
|
|
918
|
+
readonly timeoutMinutes: number;
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/** One synced build value's contribution — plan §4.5: "names + secret + fromBuildService (never values)". */
|
|
922
|
+
interface CanonicalBuildValue {
|
|
923
|
+
readonly name: string;
|
|
924
|
+
readonly secret: boolean;
|
|
925
|
+
readonly fromBuildService: boolean;
|
|
926
|
+
}
|
|
927
|
+
/** One App spec check (R-9, plan §4.5) — the command travels as its digest. */
|
|
928
|
+
interface CanonicalCheck {
|
|
929
|
+
readonly name: string;
|
|
930
|
+
readonly required: boolean;
|
|
931
|
+
/** `ceil(timeoutSeconds / 60)` — the plan's unit. */
|
|
932
|
+
readonly timeoutMinutes: number;
|
|
933
|
+
readonly commandSha256: string;
|
|
934
|
+
}
|
|
935
|
+
/** The runner the workflow will ask for (plan §4.5 `runner: { label, class }`). */
|
|
936
|
+
interface CanonicalRunner {
|
|
937
|
+
readonly label: string;
|
|
938
|
+
readonly class: AppBuildRunnerClass;
|
|
939
|
+
}
|
|
940
|
+
/** The two settings that change the emitted bytes (plan §4.4, §4.5) plus XC-01's opt-out. */
|
|
941
|
+
interface CanonicalSettings {
|
|
942
|
+
readonly reclaimDisk: boolean;
|
|
943
|
+
/** The **effective** value: `settings.attestations === true && repository.visibility === 'public'` (plan §4.5, `APW05-G07`). */
|
|
944
|
+
readonly attestations: boolean;
|
|
945
|
+
readonly allowBuildValuesOnPullRequests: boolean;
|
|
946
|
+
readonly verificationPromptedValuesRequireApproval: boolean;
|
|
947
|
+
}
|
|
948
|
+
/** Everything the fingerprint covers, in the order the canonical JSON writes it. */
|
|
949
|
+
interface CanonicalWorkflowInputs {
|
|
950
|
+
readonly generator: typeof APP_BUILD_GENERATOR_VERSION;
|
|
951
|
+
readonly bootstrap: boolean;
|
|
952
|
+
readonly trackedBranch: string;
|
|
953
|
+
readonly repository: {
|
|
954
|
+
readonly owner: string;
|
|
955
|
+
readonly repo: string;
|
|
956
|
+
readonly visibility: 'public' | 'private';
|
|
957
|
+
};
|
|
958
|
+
readonly appSpecHash: string;
|
|
959
|
+
/** `null` for the bootstrap file of plan §4.6 step 0, which carries no build block. */
|
|
960
|
+
readonly build: CanonicalBuildBlock | null;
|
|
961
|
+
readonly values: readonly CanonicalBuildValue[];
|
|
962
|
+
readonly runner: CanonicalRunner;
|
|
963
|
+
readonly settings: CanonicalSettings;
|
|
964
|
+
readonly pins: Record<string, string>;
|
|
965
|
+
readonly verifyEnabled: boolean;
|
|
966
|
+
readonly checks: readonly CanonicalCheck[];
|
|
967
|
+
/** sha256 of the embedded verify-runner script — a T15 change must move the file. */
|
|
968
|
+
readonly verifyRunnerScriptSha256: string;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The canonical key order, as data.
|
|
972
|
+
*
|
|
973
|
+
* `canonicalJson` writes exactly these keys in this order; the spec asserts that
|
|
974
|
+
* this list and the emitted header agree, so adding a field cannot half-land.
|
|
975
|
+
*/
|
|
976
|
+
declare const CANONICAL_INPUT_FIELDS: readonly ["generator", "bootstrap", "trackedBranch", "repository", "appSpecHash", "build", "values", "runner", "settings", "pins", "verifyEnabled", "checks", "verifyRunnerScriptSha256"];
|
|
977
|
+
/** `ceil(timeoutSeconds / 60)` — the plan's minutes unit for a check (R-9, plan §4.5). */
|
|
978
|
+
declare function checkTimeoutMinutes(timeoutSeconds: number): number;
|
|
979
|
+
/** A check as the canonical inputs carry it — the command only as its digest. */
|
|
980
|
+
declare function canonicalCheck(check: {
|
|
981
|
+
readonly name: string;
|
|
982
|
+
readonly command: string;
|
|
983
|
+
readonly required: boolean;
|
|
984
|
+
readonly timeoutSeconds: number;
|
|
985
|
+
}): CanonicalCheck;
|
|
986
|
+
/**
|
|
987
|
+
* Sort the parts a caller may hand in unordered, so the canonical JSON — and the
|
|
988
|
+
* hash — is order-independent (ACC-05-03).
|
|
989
|
+
*
|
|
990
|
+
* `values` by name (then `secret`, then `fromBuildService`, so two entries that
|
|
991
|
+
* somehow share a name still order deterministically); build args by name;
|
|
992
|
+
* services by name; checks by name.
|
|
993
|
+
*/
|
|
994
|
+
declare function canonicaliseWorkflowInputs(inputs: CanonicalWorkflowInputs): CanonicalWorkflowInputs;
|
|
995
|
+
/** The canonical JSON of {@link CANONICAL_INPUT_FIELDS}, no whitespace, fixed key order. */
|
|
996
|
+
declare function canonicalWorkflowJson(inputs: CanonicalWorkflowInputs): string;
|
|
997
|
+
/** `sha256:<64 hex>` over {@link canonicalWorkflowJson} — the header line's fingerprint (plan §2.4, §4.5). */
|
|
998
|
+
declare function computeWorkflowInputsHash(inputs: CanonicalWorkflowInputs): string;
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* APW-05 T8 — canonical inputs in, the workflow file out (plan §2.4, §4.5).
|
|
1002
|
+
*
|
|
1003
|
+
* The file this produces is the platform's, not the user's: it is written to the
|
|
1004
|
+
* tracked branch, hand edits are never overwritten (plan §4.6 step 5, T9), and
|
|
1005
|
+
* every value it needs travels as a repository secret (`EW_<NAME>`, plan §4.7,
|
|
1006
|
+
* T10) rather than as workflow text. Five properties are load-bearing and are
|
|
1007
|
+
* pinned by `src/__tests__/generator.spec.ts` against the ten goldens under
|
|
1008
|
+
* `src/__tests__/golden/`:
|
|
1009
|
+
*
|
|
1010
|
+
* 1. **Byte-stable.** Plan §4.5: a line-array builder plus {@link yamlString},
|
|
1011
|
+
* LF endings, one trailing newline, and every unordered input sorted before
|
|
1012
|
+
* it is hashed or emitted — the same inputs yield identical bytes on every
|
|
1013
|
+
* platform (ACC-05-03).
|
|
1014
|
+
* 2. **No stored value ever appears.** A `fromEnv` argument is emitted as a
|
|
1015
|
+
* `secrets.EW_<NAME>` reference; the resolved value only ever exists inside
|
|
1016
|
+
* the job's environment and the sealed-box PUT (ACC-05-05). A literal
|
|
1017
|
+
* `value` argument *is* emitted verbatim — it is written in the App spec, not
|
|
1018
|
+
* resolved from a stored value, which is exactly the distinction plan §2.4's
|
|
1019
|
+
* two `build-args` lines draw.
|
|
1020
|
+
* 3. **A pull request from another repository runs nothing.** The `build` job's
|
|
1021
|
+
* `if:` requires the head repository to be the repository, and the only job
|
|
1022
|
+
* that references `secrets.` is that one (ACC-05-06).
|
|
1023
|
+
* 4. **`load: true`, `push: false`, then a separate `docker push`.** A secret
|
|
1024
|
+
* found in the image metadata is caught *before* a byte leaves the runner
|
|
1025
|
+
* (FR-21), which is why the secret check sits between the build and the push.
|
|
1026
|
+
* 5. **The `verify` job can neither publish an image nor write the cache**
|
|
1027
|
+
* (FR-54): `packages: read`, no `docker push`, no `--push`, no `cache-to`,
|
|
1028
|
+
* and its own `verify-<buildId>` concurrency group so it can never replace —
|
|
1029
|
+
* or be replaced by — the push Build of the head commit (`APW05-G02`).
|
|
1030
|
+
*
|
|
1031
|
+
* ## The `checks` job
|
|
1032
|
+
*
|
|
1033
|
+
* T41 landed it: the job is `src/workflow/checks-job.ts`'s (R-9, plan §2.4:276–308,
|
|
1034
|
+
* §4.14) and this file only decides **whether** it is emitted — after the `build`
|
|
1035
|
+
* job, which is where §2.4's job order puts it (build → verify → checks), and only
|
|
1036
|
+
* when the App spec declares at least one check and the file is not the bootstrap
|
|
1037
|
+
* file of §4.6 step 0 (which has no App spec to read checks from). `checks` also
|
|
1038
|
+
* travels into the canonical inputs, so the fingerprint covers a check-command
|
|
1039
|
+
* change as well as the bytes (plan §4.5: "A check command change therefore
|
|
1040
|
+
* changes the file and its fingerprint").
|
|
1041
|
+
*
|
|
1042
|
+
* ## The checks-only file (T42, R-9, FR-70)
|
|
1043
|
+
*
|
|
1044
|
+
* `image` and `none` never produce a Build (plan §7.2 step 2: "`strategy`
|
|
1045
|
+
* `image`/`none` → no Build"), so a file generated for one of them must not carry
|
|
1046
|
+
* a `build` job — and §4.14 fixes what it carries instead, exactly: "the header,
|
|
1047
|
+
* `on: pull_request` only (no `push`, no `workflow_dispatch`), `permissions: {}`,
|
|
1048
|
+
* the same concurrency block and the `checks` job — nothing else". That is
|
|
1049
|
+
* {@link checksOnlyWorkflow}, reached through {@link checksOnlyFile}. With no
|
|
1050
|
+
* check left there is no workflow content at all, and this function answers the
|
|
1051
|
+
* empty string: §4.6 step 8's "with no checks, nothing is written", which
|
|
1052
|
+
* `src/repo/workflow-writer.ts` turns into a removal proposed by pull request
|
|
1053
|
+
* (FR-70, ACC-05-30).
|
|
1054
|
+
*
|
|
1055
|
+
* Two details of that file are deliberate and are why they are listed here:
|
|
1056
|
+
*
|
|
1057
|
+
* - **No `run-name`.** §4.14's "nothing else" and the normative draft
|
|
1058
|
+
* `docs/specs/features/app-works/APW-05-builds/golden-draft/checks.ever-works-build.yml`
|
|
1059
|
+
* (which carries `run-name` in every other draft and none here) agree: nothing
|
|
1060
|
+
* correlates a checks-only run to a Build, because §4.14's observation records
|
|
1061
|
+
* none for this strategy.
|
|
1062
|
+
* - **Its concurrency group is the draft's, not the full file's** — the one
|
|
1063
|
+
* place where §4.14's prose ("the same concurrency block") and §4.14's own
|
|
1064
|
+
* draft disagree. The draft wins, on a measurement: the full file's group names
|
|
1065
|
+
* `inputs.ew_mode` and `inputs.ew_build_id`, and the `inputs` context exists
|
|
1066
|
+
* only for `workflow_dispatch`/`workflow_call`, so a checks-only file that
|
|
1067
|
+
* carried it would be a file GitHub may refuse. `actionlint` v1.7.12 agrees —
|
|
1068
|
+
* on the shared block it exits 1 with
|
|
1069
|
+
* `checks-only.yml:11:15: property "ew_mode" is not defined in object type {}`
|
|
1070
|
+
* (and `:11:66` for `ew_build_id`); on the draft's group it exits 0. Both
|
|
1071
|
+
* blocks come from ONE helper, {@link concurrencyLines}, so they cannot drift;
|
|
1072
|
+
* `generator.spec.ts` pins each shape from both sides. Routed as a finding
|
|
1073
|
+
* against §4.14's sentence, which is the artefact that should be corrected.
|
|
1074
|
+
*
|
|
1075
|
+
* ## What this generator deliberately does not emit
|
|
1076
|
+
*
|
|
1077
|
+
* - **An attestation step.** Plan §4.5 says a private repository never gets "the
|
|
1078
|
+
* attestation permissions or steps", but §4.3's pin set carries no attestation
|
|
1079
|
+
* action and §2.4's step list no attestation step, so only the two permissions
|
|
1080
|
+
* are emitted. Routed, not invented.
|
|
1081
|
+
*
|
|
1082
|
+
* ## Three readings this file had to fix
|
|
1083
|
+
*
|
|
1084
|
+
* - **The branch tag.** §2.4 writes `format('branch-{0}', '<branch slug>')`; the
|
|
1085
|
+
* slug is known at generation time, so it is emitted as the literal
|
|
1086
|
+
* `'branch-<slug>'` inside the same expression. Same value, one less
|
|
1087
|
+
* indirection, and the golden contains the tag verbatim.
|
|
1088
|
+
* - **The restricted literal** (XC-01). §4.7b:948–952 says "a fixed non-secret
|
|
1089
|
+
* marker, one per value name so a build script can still distinguish 'absent'
|
|
1090
|
+
* from 'present'". The marker is used **verbatim**
|
|
1091
|
+
* (`APP_BUILD_RESTRICTED_VALUE_LITERAL`) rather than suffixed with the name:
|
|
1092
|
+
* the constant is the platform's single definition of the marker, and a
|
|
1093
|
+
* name-carrying variant could be mistaken for a value.
|
|
1094
|
+
* - **The build job's "Verify in the runner" step.** §2.4 emits it guarded by
|
|
1095
|
+
* `inputs.ew_verify_plan != ''`, and `ew_verify_plan` is only ever non-empty
|
|
1096
|
+
* for a `verify`-mode dispatch — which the `build` job excludes. It is emitted
|
|
1097
|
+
* as §2.4 writes it and reported as unreachable rather than quietly dropped.
|
|
1098
|
+
*/
|
|
1099
|
+
/** The App Work's repository as the workflow needs it — the emit-time half of plan §4.5's canonical inputs. */
|
|
1100
|
+
interface WorkflowRepositoryInput {
|
|
1101
|
+
readonly owner: string;
|
|
1102
|
+
readonly repo: string;
|
|
1103
|
+
readonly visibility: 'public' | 'private';
|
|
1104
|
+
}
|
|
1105
|
+
/** The runner the workflow asks for — T11 selects it, this file only prints it (plan §4.5). */
|
|
1106
|
+
interface WorkflowRunnerInput {
|
|
1107
|
+
readonly label: string;
|
|
1108
|
+
readonly class: AppBuildRunnerClass;
|
|
1109
|
+
}
|
|
1110
|
+
/** The plan §4.4 settings that change the emitted bytes. */
|
|
1111
|
+
interface WorkflowSettingsInput {
|
|
1112
|
+
readonly reclaimDisk?: boolean;
|
|
1113
|
+
/** Honoured only for a public repository — the effective value is resolved here (plan §4.5, `APW05-G07`). */
|
|
1114
|
+
readonly attestations?: boolean;
|
|
1115
|
+
readonly allowBuildValuesOnPullRequests?: boolean;
|
|
1116
|
+
readonly verificationPromptedValuesRequireApproval?: boolean;
|
|
1117
|
+
}
|
|
1118
|
+
/** One App spec check, as `PrepareRepositoryInput.checks` carries it (R-9, plan §4.1). */
|
|
1119
|
+
interface WorkflowCheckInput {
|
|
1120
|
+
readonly name: string;
|
|
1121
|
+
readonly command: string;
|
|
1122
|
+
readonly required: boolean;
|
|
1123
|
+
readonly timeoutSeconds: number;
|
|
1124
|
+
}
|
|
1125
|
+
/** Everything the generator needs (plan §4.5's canonical inputs, plus the emit-time facts). */
|
|
1126
|
+
interface WorkflowGeneratorInput {
|
|
1127
|
+
readonly trackedBranch: string;
|
|
1128
|
+
readonly repository: WorkflowRepositoryInput;
|
|
1129
|
+
/** The hash of the App spec this file was generated from — the `io.ever-works.app-spec-hash` label (plan §2.4). */
|
|
1130
|
+
readonly appSpecHash: string;
|
|
1131
|
+
/** The App spec's `build` block; `null` (or absent) for the bootstrap file of plan §4.6 step 0. */
|
|
1132
|
+
readonly build?: AppBuildBlock | null;
|
|
1133
|
+
readonly values: readonly BuildValue[];
|
|
1134
|
+
readonly runner: WorkflowRunnerInput;
|
|
1135
|
+
readonly settings?: WorkflowSettingsInput;
|
|
1136
|
+
/** Plan §4.5: true exactly when the file carries the `verify` job that can run for this App Work. */
|
|
1137
|
+
readonly verifyEnabled: boolean;
|
|
1138
|
+
readonly checks?: readonly WorkflowCheckInput[];
|
|
1139
|
+
/** Plan §4.6 step 0: a dispatch-only file with no `build` job and no `checks` job. */
|
|
1140
|
+
readonly bootstrap?: boolean;
|
|
1141
|
+
/** T15's embedded verification script; defaults to {@link EMBEDDED_VERIFY_RUNNER_SCRIPT}. */
|
|
1142
|
+
readonly verifyRunnerScript?: string;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* A YAML double-quoted scalar, always.
|
|
1146
|
+
*
|
|
1147
|
+
* Plan §4.5: "a `yamlString()` helper that always double-quotes and escapes, so
|
|
1148
|
+
* the same inputs yield identical bytes on every platform". Everything the
|
|
1149
|
+
* generator interpolates goes through here — including expressions, whose `{`,
|
|
1150
|
+
* `}`, `:` and quotes would otherwise depend on the reader's YAML dialect.
|
|
1151
|
+
*/
|
|
1152
|
+
declare function yamlString(value: string): string;
|
|
1153
|
+
/**
|
|
1154
|
+
* Plan §4.5's branch slug: lower case, `[^a-z0-9._-]` → `-`, runs collapsed,
|
|
1155
|
+
* trimmed to {@link APP_BUILD_BRANCH_SLUG_MAX_CHARS}.
|
|
1156
|
+
*/
|
|
1157
|
+
declare function branchSlug(branch: string): string;
|
|
1158
|
+
/** The image the build publishes (plan §2.4 `EW_IMAGE`, `APP_BUILD_IMAGE_NAME`). */
|
|
1159
|
+
declare function buildImageRepository(repository: WorkflowRepositoryInput): string;
|
|
1160
|
+
/** The `EW_` secret names a build job writes, space separated — `secret` and not build-service derived (§2.4, §4.11). */
|
|
1161
|
+
declare function secretNamesFor(values: readonly BuildValue[]): string[];
|
|
1162
|
+
/** The `fromEnv` names the build arguments read, in the order the build block declares them. */
|
|
1163
|
+
declare function fromEnvNames(build: AppBuildBlock): string[];
|
|
1164
|
+
/** Normalise the App spec's build block: defaults applied, keys sorted (plan §4.5). */
|
|
1165
|
+
declare function normaliseBuildBlock(build: AppBuildBlock): CanonicalBuildBlock;
|
|
1166
|
+
/** The canonical inputs of one generation — what the header line's fingerprint covers. */
|
|
1167
|
+
declare function canonicalInputsFor(input: WorkflowGeneratorInput, verifyRunnerScript: string): CanonicalWorkflowInputs;
|
|
1168
|
+
/** The header every generated file carries — plan §2.4:157–159, FR-6. */
|
|
1169
|
+
declare function workflowHeader(inputsHash: string): string[];
|
|
1170
|
+
/**
|
|
1171
|
+
* Generate the workflow file (plan §2.4).
|
|
1172
|
+
*
|
|
1173
|
+
* Byte-stable by construction: every branch below is a pure function of
|
|
1174
|
+
* {@link WorkflowGeneratorInput}, and the two unordered collections (build args,
|
|
1175
|
+
* services) are emitted in their declared order while the fingerprint sorts them
|
|
1176
|
+
* again for the hash.
|
|
1177
|
+
*
|
|
1178
|
+
* Three shapes come out of it, and the first is T42's:
|
|
1179
|
+
*
|
|
1180
|
+
* 1. **the checks-only file** ({@link checksOnlyFile}) — §4.14, or the empty
|
|
1181
|
+
* string when no check is left (§4.6 step 8);
|
|
1182
|
+
* 2. **the bootstrap file** (`bootstrap: true`) — dispatch-only, `verify` alone
|
|
1183
|
+
* (§4.6 step 0);
|
|
1184
|
+
* 3. **the full file** — `build`, `verify` and, when the App spec declares any,
|
|
1185
|
+
* `checks`.
|
|
1186
|
+
*/
|
|
1187
|
+
declare function generateWorkflow(input: WorkflowGeneratorInput): string;
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* APW-05 T8 — the embedded verification script, as the generated workflow
|
|
1191
|
+
* carries it (`run: |` under the "Verify in the runner" step of §2.4).
|
|
1192
|
+
*
|
|
1193
|
+
* ## This is a deliberate interlock shell, not the verification
|
|
1194
|
+
*
|
|
1195
|
+
* Plan §4.3 gives T15 `src/workflow/verify-runner.sh.ts` — "the embedded
|
|
1196
|
+
* verification script as a template literal": the plan schema check, the 12 GiB
|
|
1197
|
+
* refusal, the throwaway dependency containers, the recipe materialisation into a
|
|
1198
|
+
* `0600` env file, the readiness waits, the smoke requests, the shred. **None of
|
|
1199
|
+
* that exists yet.** T8 needs *a* script because §2.4's step list carries the
|
|
1200
|
+
* step and T12/T13's observation and classification paths key off its name
|
|
1201
|
+
* ("Verify in the runner" → `verificationFailed`, plan §4.9 row 10).
|
|
1202
|
+
*
|
|
1203
|
+
* So the constant below does the only honest thing an unfinished script can do:
|
|
1204
|
+
* it **fails loudly**, before touching anything, naming the task that owes the
|
|
1205
|
+
* real one. A verification Build dispatched today ends `failed` with
|
|
1206
|
+
* `EW_VERIFY_NOT_IMPLEMENTED` on its stderr — it does not report success, does not
|
|
1207
|
+
* silently skip the checks the owner asked for, and cannot be mistaken for a
|
|
1208
|
+
* passing verification.
|
|
1209
|
+
*
|
|
1210
|
+
* **T15 replaces this constant's body** (`EMBEDDED_VERIFY_RUNNER_SCRIPT`) and
|
|
1211
|
+
* leaves the name, the export and its use in `generator.ts` alone. Because the
|
|
1212
|
+
* generator hashes the script into the canonical inputs
|
|
1213
|
+
* (`verifyRunnerScriptSha256`), that replacement moves `inputsHash` on its own and
|
|
1214
|
+
* every App Work gets a commit or pull request with the new file on its next
|
|
1215
|
+
* preparation (plan §4.5) — no second place to remember.
|
|
1216
|
+
*/
|
|
1217
|
+
declare const EMBEDDED_VERIFY_RUNNER_SCRIPT: string;
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* APW-05 T9 — is the tracked branch protected? (plan §4.6 step 1, `APW05-G16`)
|
|
1221
|
+
*
|
|
1222
|
+
* Two reads, because one is not enough: the classic endpoint
|
|
1223
|
+
* `GET /repos/{o}/{r}/branches/{branch}/protection` answers **404** for a branch
|
|
1224
|
+
* that is protected only by a **ruleset**, so a plugin that trusted it alone would
|
|
1225
|
+
* commit straight into a protected branch. The rules read
|
|
1226
|
+
* (`GET /repos/{o}/{r}/rules/branches/{branch}`, paginated, read access only)
|
|
1227
|
+
* closes that hole.
|
|
1228
|
+
*
|
|
1229
|
+
* The verdict carries **which read decided it**, because "protected" is not a
|
|
1230
|
+
* boolean anyone can act on: a 403 is treated as protected *without* knowing why
|
|
1231
|
+
* (the token cannot see the rules), and the delivery rules of plan §4.6 step 2
|
|
1232
|
+
* then rely on `refRejectedByRule` as the fallback. That distinction is what the
|
|
1233
|
+
* T9 spec asserts, and it is the difference between a branch we know is protected
|
|
1234
|
+
* and one we merely cannot prove is open.
|
|
1235
|
+
*
|
|
1236
|
+
* Nothing here writes, and nothing here throws on a provider refusal: a 403 or a
|
|
1237
|
+
* 404 from either endpoint is an *answer* (the plan's own words: "A 403 or 404
|
|
1238
|
+
* from the rules endpoint means no rules are known locally; the refusal fallback
|
|
1239
|
+
* of step 2 covers that case").
|
|
1240
|
+
*/
|
|
1241
|
+
/** One endpoint's answer — a status and, when there is one, a body. A refusal is data, not an exception. */
|
|
1242
|
+
interface BranchProtectionResponse<T> {
|
|
1243
|
+
readonly ok: boolean;
|
|
1244
|
+
readonly status: number;
|
|
1245
|
+
readonly data?: T;
|
|
1246
|
+
}
|
|
1247
|
+
/** The fields of the classic protection payload this decision reads (plan §4.6 step 1). */
|
|
1248
|
+
interface BranchProtectionPayload {
|
|
1249
|
+
readonly required_pull_request_reviews?: unknown;
|
|
1250
|
+
readonly required_status_checks?: unknown;
|
|
1251
|
+
}
|
|
1252
|
+
/** One rule from `GET /rules/branches/{branch}` — the fields this decision reads. */
|
|
1253
|
+
interface BranchRulePayload {
|
|
1254
|
+
readonly type?: string;
|
|
1255
|
+
/** Present on some payloads; when it is, only `active` counts (plan §4.6 step 1). */
|
|
1256
|
+
readonly enforcement?: string;
|
|
1257
|
+
}
|
|
1258
|
+
/** The two reads the branch-rules decision needs, and nothing else. */
|
|
1259
|
+
interface BranchProtectionPort {
|
|
1260
|
+
readBranchProtection(input: {
|
|
1261
|
+
readonly owner: string;
|
|
1262
|
+
readonly repo: string;
|
|
1263
|
+
readonly branch: string;
|
|
1264
|
+
}): Promise<BranchProtectionResponse<BranchProtectionPayload>>;
|
|
1265
|
+
readBranchRules(input: {
|
|
1266
|
+
readonly owner: string;
|
|
1267
|
+
readonly repo: string;
|
|
1268
|
+
readonly branch: string;
|
|
1269
|
+
readonly page: number;
|
|
1270
|
+
}): Promise<BranchProtectionResponse<readonly BranchRulePayload[]>>;
|
|
1271
|
+
}
|
|
1272
|
+
/** Which read decided the verdict — the reason, not just the boolean. */
|
|
1273
|
+
type BranchProtectionDecision = 'requiredPullRequestReviews' | 'requiredStatusChecks' | 'forbidden' | 'rulesetPullRequest' | 'rulesetStatusChecks' | 'none';
|
|
1274
|
+
/** The decision and the read that produced it. */
|
|
1275
|
+
interface BranchProtectionVerdict {
|
|
1276
|
+
readonly protected: boolean;
|
|
1277
|
+
readonly decision: BranchProtectionDecision;
|
|
1278
|
+
}
|
|
1279
|
+
/** The branch coordinates both reads address. */
|
|
1280
|
+
interface BranchCoordinates {
|
|
1281
|
+
readonly owner: string;
|
|
1282
|
+
readonly repo: string;
|
|
1283
|
+
readonly branch: string;
|
|
1284
|
+
}
|
|
1285
|
+
/** Page size for the rules read — GitHub's maximum, so the pagination loop is as short as it can be. */
|
|
1286
|
+
declare const BRANCH_RULES_PAGE_SIZE = 100;
|
|
1287
|
+
/**
|
|
1288
|
+
* How many rules pages are read before the loop stops.
|
|
1289
|
+
*
|
|
1290
|
+
* A bound, not a guess: an active `pull_request` or `required_status_checks` rule
|
|
1291
|
+
* is almost always on the first page, and a repository with more than a thousand
|
|
1292
|
+
* branch rules is not a case worth hanging a preparation job on. Reaching the cap
|
|
1293
|
+
* without a match answers `unprotected`, which the `refRejectedByRule` fallback
|
|
1294
|
+
* of §4.6 step 2 covers.
|
|
1295
|
+
*/
|
|
1296
|
+
declare const BRANCH_RULES_MAX_PAGES = 10;
|
|
1297
|
+
/** The two rule types plan §4.6 step 1 names. */
|
|
1298
|
+
declare const BRANCH_PROTECTION_RULE_TYPES: readonly ["pull_request", "required_status_checks"];
|
|
1299
|
+
/**
|
|
1300
|
+
* Decide whether the tracked branch is protected (plan §4.6 step 1).
|
|
1301
|
+
*
|
|
1302
|
+
* Order, and why:
|
|
1303
|
+
*
|
|
1304
|
+
* 1. the classic endpoint — `403` is **protected** (we cannot see the rules, so
|
|
1305
|
+
* we do not get to assume the branch is open); a `200` protects when either
|
|
1306
|
+
* `required_pull_request_reviews` or `required_status_checks` is present;
|
|
1307
|
+
* 2. the rules endpoint — consulted on a `404` **and** on a `200` that carried
|
|
1308
|
+
* neither field, because a ruleset can protect a branch the classic endpoint
|
|
1309
|
+
* reports as unprotected.
|
|
1310
|
+
*/
|
|
1311
|
+
declare function isBranchProtected(port: BranchProtectionPort, coordinates: BranchCoordinates): Promise<BranchProtectionVerdict>;
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* APW-05 T9 — delivering the generated workflow (plan §4.6, `APW05-G16`, R-4).
|
|
1315
|
+
*
|
|
1316
|
+
* ## The one rule that shapes everything here
|
|
1317
|
+
*
|
|
1318
|
+
* **The plugin never clones.** Every write goes through the `RepositoryWriter` the
|
|
1319
|
+
* facade binds (APW-03's `commitFiles?` via `GitFacadeService`), whose four members
|
|
1320
|
+
* are all this file calls: `getFileContent`, `commitFiles`, `createBranch`,
|
|
1321
|
+
* `createPullRequest`. Writing one file must not clone a 1 GB repository
|
|
1322
|
+
* (plan §1.2), and the T9 spec asserts that no clone-shaped call exists — the fake
|
|
1323
|
+
* writer has no clone member to call.
|
|
1324
|
+
*
|
|
1325
|
+
* ## The paths, and which one a branch gets
|
|
1326
|
+
*
|
|
1327
|
+
* | Branch state | Path |
|
|
1328
|
+
* | ------------------------------------------------------------- | ---------------------------------------- |
|
|
1329
|
+
* | File on the tracked branch already equals the generated bytes | `unchanged`, **zero write calls** |
|
|
1330
|
+
* | File exists, its sha256 is neither the stored one nor the new one | **hand edit** → pull request, never an overwrite on the tracked branch (FR-9) |
|
|
1331
|
+
* | `createdByAppWork === false` (a Link relation) | pull request, always (R-4) |
|
|
1332
|
+
* | Protected (reviews, status checks, ruleset, or a 403) | pull request |
|
|
1333
|
+
* | Otherwise | direct commit onto the tracked branch |
|
|
1334
|
+
* | **No workflow content** (T42: no build and no check remains) | pull request — the removal of §4.6 step 8, never a direct commit (FR-70) |
|
|
1335
|
+
*
|
|
1336
|
+
* The pull request path reuses **one** pull request: `ever-works/build-workflow` is
|
|
1337
|
+
* created from the tracked head when absent, the commit lands on it, and the pull
|
|
1338
|
+
* request is then ALWAYS asked for. Reuse is proven by GitHub, never assumed from
|
|
1339
|
+
* a stored number: when an open pull request already exists for that head, the
|
|
1340
|
+
* create is refused with `pullRequestExists` (the contract's code, or GitHub's own
|
|
1341
|
+
* 422 "A pull request already exists" when nothing translated it), and that
|
|
1342
|
+
* refusal is what `pullRequestUpdated` means (ACC-05-02). The stored
|
|
1343
|
+
* `pullRequestNumber`/`pullRequestUrl` are only echoed back so the reused pull
|
|
1344
|
+
* request keeps its link; a stored pull request the owner CLOSED is therefore
|
|
1345
|
+
* replaced by a new one rather than "updated" forever. A second and a third
|
|
1346
|
+
* preparation leave exactly one open pull request, and a preparation whose bytes
|
|
1347
|
+
* are already on that branch commits nothing at all.
|
|
1348
|
+
*
|
|
1349
|
+
* ## The refusals
|
|
1350
|
+
*
|
|
1351
|
+
* - `nonFastForward` — a race: retry the direct write, at most
|
|
1352
|
+
* {@link WORKFLOW_WRITE_MAX_ATTEMPTS} attempts in total (plan §4.6 step 2's
|
|
1353
|
+
* "up to 3 times"), then `blocked workflowWriteFailed`.
|
|
1354
|
+
* - `refRejectedByRule` — a protection or ruleset the pre-check could not see:
|
|
1355
|
+
* switch to the pull request path **once** and never retry the direct write.
|
|
1356
|
+
* If the pull request path is refused the same way, stop with
|
|
1357
|
+
* `blocked workflowWriteFailed`, detail `{ cause: 'branchRules' }` — never a
|
|
1358
|
+
* loop. Its copy is "The build workflow could not be written." plus "A rule on
|
|
1359
|
+
* this repository refuses the change." (plan §4.6 steps 2–3).
|
|
1360
|
+
* - a read-back that does not match: retried **once**; still mismatched →
|
|
1361
|
+
* `blocked workflowWriteFailed`, and `storedWorkflowSha256` stays untouched —
|
|
1362
|
+
* only a matching read-back may store a hash (FR-8).
|
|
1363
|
+
*
|
|
1364
|
+
* ## Where the branch head comes from
|
|
1365
|
+
*
|
|
1366
|
+
* `commitFiles` needs the commit the caller believes the branch is at, and the
|
|
1367
|
+
* `RepositoryWriter` contract has no `getBranch` member. Two ways in, both within
|
|
1368
|
+
* the four:
|
|
1369
|
+
*
|
|
1370
|
+
* 1. `input.resolveTrackedHead` — the facade, which already holds
|
|
1371
|
+
* `GitFacadeService`, passes the real head (T16's binding);
|
|
1372
|
+
* 2. otherwise {@link readBranchHead} calls `createBranch(branch, branch)`, which
|
|
1373
|
+
* the contract documents as returning an existing branch rather than
|
|
1374
|
+
* recreating it — a read that happens to be spelled as a create.
|
|
1375
|
+
*
|
|
1376
|
+
* The head is re-read on **every** attempt, which is what makes the
|
|
1377
|
+
* `nonFastForward` retry real rather than a repeat of the same stale commit.
|
|
1378
|
+
*/
|
|
1379
|
+
/** The commit message of a file that did not exist (plan §4.6 step 2). */
|
|
1380
|
+
declare const WORKFLOW_COMMIT_MESSAGE_ADD = "Add Ever Works build workflow";
|
|
1381
|
+
/** The commit message of a file the platform is changing (plan §4.6 step 2). */
|
|
1382
|
+
declare const WORKFLOW_COMMIT_MESSAGE_UPDATE = "Update Ever Works build workflow";
|
|
1383
|
+
/** The pull request title (plan §4.6 step 3). */
|
|
1384
|
+
declare const WORKFLOW_PULL_REQUEST_TITLE = "Add Ever Works build workflow";
|
|
1385
|
+
/** The pull request body — what a reviewer is looking at, and what not to do with it. */
|
|
1386
|
+
declare const 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.";
|
|
1387
|
+
/**
|
|
1388
|
+
* How many direct-write attempts are made in total (plan §4.6 step 2: "A
|
|
1389
|
+
* `nonFastForward` error retries from step 1 up to 3 times").
|
|
1390
|
+
*
|
|
1391
|
+
* Read as at most three attempts, which is the stricter of the two readings of
|
|
1392
|
+
* "up to 3 times" and therefore cannot exceed the bound the plan states.
|
|
1393
|
+
*/
|
|
1394
|
+
declare const WORKFLOW_WRITE_MAX_ATTEMPTS = 3;
|
|
1395
|
+
/** How many read-backs one successful write gets: the first, plus the plan's single retry (FR-8). */
|
|
1396
|
+
declare const WORKFLOW_READ_BACK_ATTEMPTS = 2;
|
|
1397
|
+
/** The tracked branch and the App Work's relation to it. */
|
|
1398
|
+
interface WorkflowRepositoryCoordinates {
|
|
1399
|
+
readonly owner: string;
|
|
1400
|
+
readonly repo: string;
|
|
1401
|
+
readonly trackedBranch: string;
|
|
1402
|
+
/** `false` for a Link relation — that repository is not ours to write into (R-4). */
|
|
1403
|
+
readonly createdByAppWork: boolean;
|
|
1404
|
+
}
|
|
1405
|
+
/** What the caller asks to deliver, and what it remembers from the last delivery. */
|
|
1406
|
+
interface WorkflowWriteInput {
|
|
1407
|
+
readonly repository: WorkflowRepositoryCoordinates;
|
|
1408
|
+
/** The generated bytes. */
|
|
1409
|
+
readonly content: string;
|
|
1410
|
+
/** The workflow sha256 the platform last wrote, or `null` — the other half of hand-edit detection (FR-9). */
|
|
1411
|
+
readonly lastWrittenWorkflowSha256: string | null;
|
|
1412
|
+
/**
|
|
1413
|
+
* The pull request the platform recorded for this App Work (§3.1b), when there is one.
|
|
1414
|
+
* Echoed back when GitHub reports an open pull request for the head; never used to
|
|
1415
|
+
* decide that one is open.
|
|
1416
|
+
*/
|
|
1417
|
+
readonly pullRequestNumber?: number | null;
|
|
1418
|
+
/** That pull request's URL, echoed back on the reuse path (the writer never re-reads it). */
|
|
1419
|
+
readonly pullRequestUrl?: string | null;
|
|
1420
|
+
/** Overrides {@link APP_BUILD_WORKFLOW_PATH}; exists so a spec can name its own path. */
|
|
1421
|
+
readonly workflowPath?: string;
|
|
1422
|
+
/** The facade's branch-head read (T16). Without it, {@link readBranchHead} is used. */
|
|
1423
|
+
readonly resolveTrackedHead?: () => Promise<string>;
|
|
1424
|
+
}
|
|
1425
|
+
/** What the delivery decided — the transport state §3.1b maps onto `workflowState`. */
|
|
1426
|
+
interface WorkflowWriteResult {
|
|
1427
|
+
/**
|
|
1428
|
+
* The delivery outcome.
|
|
1429
|
+
*
|
|
1430
|
+
* `'blocked'` is this writer's own terminal state and is **not** a member of
|
|
1431
|
+
* `PrepareRepositoryResult.workflow.state` (which has five): the caller maps it
|
|
1432
|
+
* onto `PrepareRepositoryResult.blocked` with reason `workflowWriteFailed`, and
|
|
1433
|
+
* must not pass it through as a stored workflow state.
|
|
1434
|
+
*/
|
|
1435
|
+
readonly state: 'unchanged' | 'committed' | 'pullRequestOpened' | 'pullRequestUpdated' | 'editedByHand' | 'blocked';
|
|
1436
|
+
readonly commitSha?: string;
|
|
1437
|
+
readonly pullRequestNumber?: number;
|
|
1438
|
+
readonly pullRequestUrl?: string;
|
|
1439
|
+
/**
|
|
1440
|
+
* The sha256 the platform may store as `lastWrittenWorkflowSha256`, or `null`
|
|
1441
|
+
* when nothing on the tracked branch has been proven to equal the generated
|
|
1442
|
+
* bytes. Only a matching read-back sets it (FR-8, plan §4.6 step 4).
|
|
1443
|
+
*/
|
|
1444
|
+
readonly storedWorkflowSha256: string | null;
|
|
1445
|
+
readonly blocked?: {
|
|
1446
|
+
readonly reason: 'workflowWriteFailed';
|
|
1447
|
+
readonly detail: {
|
|
1448
|
+
readonly cause: string;
|
|
1449
|
+
};
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
/** The refusal code of a thrown write error, if it is one of the codes the contract names. */
|
|
1453
|
+
declare function repositoryWriteErrorCode(error: unknown): RepositoryWriteErrorCode | null;
|
|
1454
|
+
/**
|
|
1455
|
+
* Deliver the generated workflow (plan §4.6).
|
|
1456
|
+
*
|
|
1457
|
+
* `protection` is the branch-rules port; leave it out when the caller has no
|
|
1458
|
+
* protection read (a fork the platform owns, a spec), in which case only
|
|
1459
|
+
* `createdByAppWork` and hand-edit detection decide the path.
|
|
1460
|
+
*
|
|
1461
|
+
* **Empty content is T42's removal request** (plan §4.6 step 8, FR-70): the
|
|
1462
|
+
* generator answers `''` when neither a build nor a check remains, and
|
|
1463
|
+
*
|
|
1464
|
+
* - with no file on the tracked branch there is nothing to remove — no write, no
|
|
1465
|
+
* pull request, `unchanged` (and no stored hash: nothing was proven);
|
|
1466
|
+
* - with one, the removal is **proposed by pull request** and never committed to
|
|
1467
|
+
* the tracked branch: §4.6 step 8's "never deleted directly". The branch
|
|
1468
|
+
* protections of steps 1–2 are not consulted, because there is no direct path
|
|
1469
|
+
* to protect; `createdByAppWork` and the hand-edit verdict still decide the
|
|
1470
|
+
* state the caller stores (§3.1b).
|
|
1471
|
+
*/
|
|
1472
|
+
declare function writeWorkflow(input: WorkflowWriteInput, writer: RepositoryWriter, protection?: BranchProtectionPort): Promise<WorkflowWriteResult>;
|
|
1473
|
+
|
|
1474
|
+
export { ACTION_PINS, ACTION_PIN_SHA_PATTERN, type ActionPin, type ActionPinKey, BRANCH_PROTECTION_RULE_TYPES, BRANCH_RULES_MAX_PAGES, BRANCH_RULES_PAGE_SIZE, type BranchCoordinates, type BranchProtectionDecision, type BranchProtectionPayload, type BranchProtectionPort, type BranchProtectionResponse, type BranchProtectionVerdict, type BranchRulePayload, type BuildValueSecretSync, type BuildValueSyncInput, type BuildValueSyncResult, CANONICAL_INPUT_FIELDS, type CanonicalBuildArg, type CanonicalBuildBlock, type CanonicalBuildService, type CanonicalBuildValue, type CanonicalCheck, type CanonicalRunner, type CanonicalSettings, type CanonicalWorkflowInputs, EMBEDDED_VERIFY_RUNNER_SCRIPT, GENERATOR_RESERVED_LITERALS, GITHUB_ACTIONS_BUILD_PLATFORM_MANAGED_KEYS, GITHUB_ACTIONS_BUILD_SETTING_DEFAULTS, GITHUB_ACTIONS_BUILD_SETTING_KEYS, GITHUB_SECRET_NAME_PATTERN, GitHubActionsBuildPlugin, type GitHubActionsBuildSettingKey, type GitHubActionsBuildSettings, type GitHubActionsBuildSettingsSchema, LARGER_RUNNER_LABEL_PATTERN, LARGER_RUNNER_MEMORY_MAX_GIB, LARGER_RUNNER_MEMORY_MIN_GIB, LARGER_RUNNER_VCPU_MAX, LARGER_RUNNER_VCPU_MIN, PREPARATION_STATE_KEYS, type PlatformManagedJsonSchema, type RepositorySecretPort, type SecretSyncDependencies, type SecretSyncLogger, type VerifyPromptedSecretResult, WORKFLOW_COMMIT_MESSAGE_ADD, WORKFLOW_COMMIT_MESSAGE_UPDATE, WORKFLOW_PULL_REQUEST_BODY, WORKFLOW_PULL_REQUEST_TITLE, WORKFLOW_READ_BACK_ATTEMPTS, WORKFLOW_WRITE_MAX_ATTEMPTS, type WorkflowCheckInput, type WorkflowGeneratorInput, type WorkflowRepositoryCoordinates, type WorkflowRepositoryInput, type WorkflowRunnerInput, type WorkflowSettingsInput, type WorkflowWriteInput, type WorkflowWriteResult, actionPin, actionPinReference, branchSlug, buildImageRepository, buildSecretName, canonicalActionPins, canonicalCheck, canonicalInputsFor, canonicalWorkflowJson, canonicaliseWorkflowInputs, checkTimeoutMinutes, computeWorkflowInputsHash, createBuildValueSecretSync, GitHubActionsBuildPlugin as default, fromEnvNames, generateWorkflow, gitHubActionsBuildSettingsSchema, isBranchProtected, isSecretLimitError, isValidGitHubSecretName, loadSodium, normaliseBuildBlock, notImplemented, repositoryWriteErrorCode, sealSecretValue, secretNamesFor, workflowHeader, writeWorkflow, yamlString };
|