@ship.zone/ci-spec 2.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.
Files changed (116) hide show
  1. package/.smartconfig.json +46 -0
  2. package/changelog.md +190 -0
  3. package/conformance/archive-cases.json +446 -0
  4. package/conformance/ci-actions/compile-invalid/build-arg-secret-collision.yml +31 -0
  5. package/conformance/ci-actions/compile-invalid/candidate-without-needs.yml +36 -0
  6. package/conformance/ci-actions/compile-invalid/needs-wider-triggers.yml +42 -0
  7. package/conformance/ci-actions/compile-invalid/npm-read-registry-not-allowlisted.yml +34 -0
  8. package/conformance/ci-actions/compile-invalid/publish-job-excludes-tag.yml +57 -0
  9. package/conformance/ci-actions/compile-invalid/shared-memory-above-memory.yml +28 -0
  10. package/conformance/ci-actions/compile-invalid/trigger-kind-without-job.yml +30 -0
  11. package/conformance/ci-actions/invalid/alias-affix-invalid.yml +33 -0
  12. package/conformance/ci-actions/invalid/alias.yml +32 -0
  13. package/conformance/ci-actions/invalid/bridge-network.yml +27 -0
  14. package/conformance/ci-actions/invalid/build-darwin-platform.yml +24 -0
  15. package/conformance/ci-actions/invalid/build-with-steps.yml +27 -0
  16. package/conformance/ci-actions/invalid/concurrency-empty-group.yml +28 -0
  17. package/conformance/ci-actions/invalid/concurrency-without-cancel-in-progress.yml +27 -0
  18. package/conformance/ci-actions/invalid/container-job-build-profile.yml +25 -0
  19. package/conformance/ci-actions/invalid/custom-tag.txt +23 -0
  20. package/conformance/ci-actions/invalid/duplicate-key.txt +24 -0
  21. package/conformance/ci-actions/invalid/egress-bare-wildcard.yml +29 -0
  22. package/conformance/ci-actions/invalid/egress-empty-allowlist.yml +28 -0
  23. package/conformance/ci-actions/invalid/egress-ip-literal.yml +29 -0
  24. package/conformance/ci-actions/invalid/egress-single-label-wildcard.yml +29 -0
  25. package/conformance/ci-actions/invalid/job-publish-permission.yml +28 -0
  26. package/conformance/ci-actions/invalid/job-trigger-undeclared-kind.yml +27 -0
  27. package/conformance/ci-actions/invalid/literal-alias-affix.yml +33 -0
  28. package/conformance/ci-actions/invalid/missing-spec.yml +24 -0
  29. package/conformance/ci-actions/invalid/multiple-documents.yml +25 -0
  30. package/conformance/ci-actions/invalid/npm-read-invalid-scope.yml +33 -0
  31. package/conformance/ci-actions/invalid/oci-darwin-platform.yml +25 -0
  32. package/conformance/ci-actions/invalid/permission-without-publication.yml +31 -0
  33. package/conformance/ci-actions/invalid/publish-order-duplicate.yml +50 -0
  34. package/conformance/ci-actions/invalid/publish-order-missing-kind.yml +48 -0
  35. package/conformance/ci-actions/invalid/publish-order-undeclared-kind.yml +45 -0
  36. package/conformance/ci-actions/invalid/publish-without-permission.yml +31 -0
  37. package/conformance/ci-actions/invalid/publish-without-tag-trigger.yml +31 -0
  38. package/conformance/ci-actions/invalid/release-notes-without-assets.yml +46 -0
  39. package/conformance/ci-actions/invalid/reserved-label-prefix.yml +25 -0
  40. package/conformance/ci-actions/invalid/reserved-label.yml +25 -0
  41. package/conformance/ci-actions/invalid/retention-above-maximum.yml +83 -0
  42. package/conformance/ci-actions/invalid/retention-below-minimum.yml +83 -0
  43. package/conformance/ci-actions/invalid/schema-version-field.yml +26 -0
  44. package/conformance/ci-actions/invalid/shared-memory-below-minimum.yml +28 -0
  45. package/conformance/ci-actions/invalid/shell-string.yml +21 -0
  46. package/conformance/ci-actions/invalid/spec-number.yml +25 -0
  47. package/conformance/ci-actions/invalid/spec-prerelease.yml +25 -0
  48. package/conformance/ci-actions/invalid/spec-range.yml +25 -0
  49. package/conformance/ci-actions/invalid/timeout-above-maximum.yml +26 -0
  50. package/conformance/ci-actions/invalid/timeout-below-minimum.yml +26 -0
  51. package/conformance/ci-actions/invalid/unknown-alias-kind.yml +31 -0
  52. package/conformance/ci-actions/invalid/unknown-key.yml +24 -0
  53. package/conformance/ci-actions/invalid/vm-darwin-candidate.yml +38 -0
  54. package/conformance/ci-actions/invalid/vm-darwin-shared-memory.yml +28 -0
  55. package/conformance/ci-actions/invalid/vm-oci-execution.yml +25 -0
  56. package/conformance/ci-actions/invalid/vm-unsupported-platform.yml +25 -0
  57. package/conformance/ci-actions/invalid/vm-without-platform.yml +24 -0
  58. package/conformance/ci-actions/valid/concurrency.yml +83 -0
  59. package/conformance/ci-actions/valid/egress.yml +40 -0
  60. package/conformance/ci-actions/valid/image-build.yml +80 -0
  61. package/conformance/ci-actions/valid/matrix.yml +45 -0
  62. package/conformance/ci-actions/valid/minimal.yml +25 -0
  63. package/conformance/ci-actions/valid/npm-read.yml +60 -0
  64. package/conformance/ci-actions/valid/release-assets.yml +114 -0
  65. package/conformance/ci-actions/valid/release-signing.yml +71 -0
  66. package/conformance/ci-actions/valid/release.yml +109 -0
  67. package/conformance/ci-actions/valid/resources.yml +79 -0
  68. package/conformance/ci-actions/valid/vm.yml +77 -0
  69. package/conformance/compile-cases.json +5885 -0
  70. package/conformance/compile-cases.schema.json +766 -0
  71. package/conformance/digest-cases.json +144 -0
  72. package/conformance/runner-cases.json +1238 -0
  73. package/conformance/runner-jobs/invalid/bridge-network.json +70 -0
  74. package/conformance/runner-jobs/invalid/build-darwin-platform.json +100 -0
  75. package/conformance/runner-jobs/invalid/build-with-steps.json +111 -0
  76. package/conformance/runner-jobs/invalid/build-without-microvm.json +100 -0
  77. package/conformance/runner-jobs/invalid/egress-empty-allowlist.json +71 -0
  78. package/conformance/runner-jobs/invalid/mutable-image.json +70 -0
  79. package/conformance/runner-jobs/invalid/npm-read-reserved-environment.json +82 -0
  80. package/conformance/runner-jobs/invalid/oci-darwin-platform.json +71 -0
  81. package/conformance/runner-jobs/invalid/push-permission-container.json +70 -0
  82. package/conformance/runner-jobs/invalid/shared-memory-below-minimum.json +71 -0
  83. package/conformance/runner-jobs/invalid/timeout-missing.json +69 -0
  84. package/conformance/runner-jobs/invalid/vm-without-microvm.json +82 -0
  85. package/conformance/runner-jobs/valid/candidate-test.json +72 -0
  86. package/conformance/runner-jobs/valid/egress.json +80 -0
  87. package/conformance/runner-jobs/valid/image-build.json +100 -0
  88. package/conformance/runner-jobs/valid/minimal.json +70 -0
  89. package/conformance/runner-jobs/valid/npm-read.json +81 -0
  90. package/conformance/runner-jobs/valid/resources.json +71 -0
  91. package/conformance/runner-jobs/valid/timeout-retention-defaults.json +91 -0
  92. package/conformance/runner-jobs/valid/vm-darwin.json +70 -0
  93. package/conformance/runner-jobs/valid/vm.json +82 -0
  94. package/conformance/runner-messages.json +2035 -0
  95. package/conformance/version-cases.json +125 -0
  96. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  97. package/dist_ts/00_commitinfo_data.js +9 -0
  98. package/dist_ts/constants.d.ts +199 -0
  99. package/dist_ts/constants.js +106 -0
  100. package/dist_ts/index.d.ts +1 -0
  101. package/dist_ts/index.js +2 -0
  102. package/dist_ts/plugins.d.ts +1 -0
  103. package/dist_ts/plugins.js +3 -0
  104. package/examples/ci_actions.yml +64 -0
  105. package/license.md +21 -0
  106. package/package.json +72 -0
  107. package/readme.md +146 -0
  108. package/schemas/ci_actions.schema.json +1664 -0
  109. package/schemas/runner-job.schema.json +1172 -0
  110. package/spec/ci-actions.md +433 -0
  111. package/spec/runner-protocol.md +378 -0
  112. package/spec/runner.openapi.json +2182 -0
  113. package/ts/00_commitinfo_data.ts +8 -0
  114. package/ts/constants.ts +112 -0
  115. package/ts/index.ts +1 -0
  116. package/ts/plugins.ts +1 -0
@@ -0,0 +1,1238 @@
1
+ {
2
+ "spec": "2.0.0",
3
+ "status": "draft",
4
+ "cases": [
5
+ {
6
+ "id": "registration-response-replay",
7
+ "category": "registration",
8
+ "given": "A valid bootstrap credential and a registration request that succeeded but whose response was lost",
9
+ "when": "The exact request ID and canonical body are retried within ten minutes",
10
+ "then": "The coordinator returns the byte-equivalent credential response without creating another runner or credential"
11
+ },
12
+ {
13
+ "id": "registration-body-conflict",
14
+ "category": "registration",
15
+ "given": "A consumed bootstrap credential with a stored successful registration response",
16
+ "when": "The request ID or canonical body changes",
17
+ "then": "The coordinator returns a conflict and does not mutate registration state"
18
+ },
19
+ {
20
+ "id": "discovery-reports-spec",
21
+ "category": "versioning",
22
+ "given": "A coordinator built against specification version C",
23
+ "when": "A runner calls discovery",
24
+ "then": "The response spec equals C"
25
+ },
26
+ {
27
+ "id": "runner-spec-major-mismatch",
28
+ "category": "versioning",
29
+ "given": "A coordinator implementing C and a registration or session-start request whose spec R has a different major version than C",
30
+ "when": "The coordinator handles the request",
31
+ "then": "It returns HTTP 409 with error code spec_incompatible without consuming the bootstrap credential, creating a session, or fencing an older session"
32
+ },
33
+ {
34
+ "id": "workflow-spec-incompatible",
35
+ "category": "versioning",
36
+ "given": "A coordinator implementing I and a ci_actions.yml whose spec D has outcome newer or major-mismatch under the version-cases rule",
37
+ "when": "The coordinator compiles the workflow",
38
+ "then": "Compilation fails before any job is enqueued"
39
+ },
40
+ {
41
+ "id": "compiled-job-spec-from-workflow",
42
+ "category": "versioning",
43
+ "given": "A workflow whose spec D is accepted by the coordinator",
44
+ "when": "The coordinator compiles it",
45
+ "then": "Every compiled job carries spec D"
46
+ },
47
+ {
48
+ "id": "job-spec-lease-matching",
49
+ "category": "versioning",
50
+ "given": "A compiled job with spec D and a runner session with spec R whose version-cases outcome for (I = R, D) is not accepted",
51
+ "when": "The runner claims work",
52
+ "then": "The coordinator does not lease the job to that runner; a runner that nevertheless receives such a lease abandons it as runner_error before acceptance"
53
+ },
54
+ {
55
+ "id": "credential-rotation-replay",
56
+ "category": "credentials",
57
+ "given": "A replacement credential response was lost during overlap",
58
+ "when": "The old credential is used again before previousCredentialValidUntil",
59
+ "then": "The coordinator returns the same replacement credential rather than creating another one"
60
+ },
61
+ {
62
+ "id": "credential-rotation-acknowledgement",
63
+ "category": "credentials",
64
+ "given": "Old and replacement credentials overlap",
65
+ "when": "A request authenticates with the replacement credential",
66
+ "then": "The replacement is acknowledged and the old credential is revoked after one heartbeat grace interval"
67
+ },
68
+ {
69
+ "id": "credential-overlap-expiry",
70
+ "category": "credentials",
71
+ "given": "A replacement credential was never acknowledged",
72
+ "when": "The overlap deadline passes",
73
+ "then": "The old credential is rejected and no overlap is extended implicitly"
74
+ },
75
+ {
76
+ "id": "claim-exact-replay",
77
+ "category": "claim",
78
+ "given": "A claim request returned a live lease",
79
+ "when": "The same claimRequestId and canonical body are retried",
80
+ "then": "The same lease and fencing token are returned"
81
+ },
82
+ {
83
+ "id": "claim-transfer-limit-mismatch",
84
+ "category": "claim",
85
+ "given": "A queued job requires a source, artifact, cache, or log limit above the runner capability",
86
+ "when": "The runner claims work",
87
+ "then": "The incompatible job is not leased to that runner"
88
+ },
89
+ {
90
+ "id": "stale-fence-rejection",
91
+ "category": "fencing",
92
+ "given": "A lease has expired or been replaced",
93
+ "when": "The old identity attempts any new or conflicting heartbeat, log, transfer, completion, or abandon mutation",
94
+ "then": "The coordinator returns 409 and does not mutate the current attempt; a retained exact replay only returns its previously recorded response and grants no renewed authority"
95
+ },
96
+ {
97
+ "id": "log-duplicate-ack",
98
+ "category": "logs",
99
+ "given": "A log sequence was appended and its acknowledgement was lost",
100
+ "when": "The byte-identical sequence is retried",
101
+ "then": "The coordinator returns the current nextSequence without appending duplicate data"
102
+ },
103
+ {
104
+ "id": "log-sequence-conflict",
105
+ "category": "logs",
106
+ "given": "A different chunk reuses an accepted sequence or sends a future sequence",
107
+ "when": "The chunk is submitted",
108
+ "then": "The coordinator returns 409 without advancing the acknowledgement"
109
+ },
110
+ {
111
+ "id": "completion-exact-replay",
112
+ "category": "completion",
113
+ "given": "A terminal result was accepted and its response was lost",
114
+ "when": "The exact terminal payload is retried with the retained original identity",
115
+ "then": "The coordinator returns accepted without duplicating terminal effects"
116
+ },
117
+ {
118
+ "id": "completion-conflict",
119
+ "category": "completion",
120
+ "given": "A terminal result is already recorded",
121
+ "when": "A different terminal payload is submitted",
122
+ "then": "The coordinator returns 409 and preserves the original terminal result"
123
+ },
124
+ {
125
+ "id": "cancellation-redelivery",
126
+ "category": "cancellation",
127
+ "given": "A cancel directive exists for a running attempt",
128
+ "when": "Heartbeats continue before terminal completion",
129
+ "then": "Every accepted heartbeat response carries the same cancellation request ID"
130
+ },
131
+ {
132
+ "id": "cancellation-observation",
133
+ "category": "cancellation",
134
+ "given": "The runner received a cancellation directive",
135
+ "when": "The next heartbeat includes observedCancellationRequestId",
136
+ "then": "The coordinator records the idempotent observation and continues redelivery until terminal"
137
+ },
138
+ {
139
+ "id": "cancellation-force-stop",
140
+ "category": "cancellation",
141
+ "given": "Execution does not stop gracefully after cancellation",
142
+ "when": "terminateBy is reached",
143
+ "then": "The runner force-stops execution and completes canceled with the directive ID if the fence remains valid"
144
+ },
145
+ {
146
+ "id": "cancellation-after-fence-loss",
147
+ "category": "cancellation",
148
+ "given": "The runner is terminating a canceled attempt",
149
+ "when": "The lease or fence becomes stale before completion",
150
+ "then": "The runner suppresses terminal completion and further transfer or log mutations"
151
+ },
152
+ {
153
+ "id": "source-digest-mismatch",
154
+ "category": "integrity",
155
+ "given": "The source stream bytes or length differ from the lease descriptor",
156
+ "when": "The runner verifies the downloaded archive",
157
+ "then": "Execution does not start and the attempt ends as system_error while the fence remains valid"
158
+ },
159
+ {
160
+ "id": "artifact-digest-mismatch",
161
+ "category": "integrity",
162
+ "given": "An artifact upload body differs from its declared bytes or SHA-256",
163
+ "when": "The coordinator verifies the staged body",
164
+ "then": "Publication fails atomically and no artifact record becomes visible"
165
+ },
166
+ {
167
+ "id": "cache-trust-isolation",
168
+ "category": "authorization",
169
+ "given": "A cache was produced in a trusted repository context",
170
+ "when": "A pull request run, which is always untrusted (see Trust), requests the same logical cache key",
171
+ "then": "The coordinator returns no trusted cache because the trust namespaces differ"
172
+ },
173
+ {
174
+ "id": "claim-acceptance-required",
175
+ "category": "claim",
176
+ "given": "A runner has received and validated a live lease",
177
+ "when": "Execution would start before a fenced accept request returns 204",
178
+ "then": "The runner does not start execution"
179
+ },
180
+ {
181
+ "id": "claim-tombstone",
182
+ "category": "claim",
183
+ "given": "A claim key previously returned a lease that is now expired or terminal",
184
+ "when": "The exact claim is retried during the session tombstone period",
185
+ "then": "The coordinator returns 409 and never leases a different job for that key"
186
+ },
187
+ {
188
+ "id": "accept-exact-replay",
189
+ "category": "claim",
190
+ "given": "A valid acceptance transitioned a lease to running but its response was lost",
191
+ "when": "The exact accept request is retried while retained",
192
+ "then": "The coordinator returns 204 without another transition or reviving terminal work"
193
+ },
194
+ {
195
+ "id": "recovery-ownership",
196
+ "category": "recovery",
197
+ "given": "A runner starts a new authenticated session",
198
+ "when": "It reports an interrupted attempt not owned by the same runner and exact previous session",
199
+ "then": "The coordinator returns 409 without disclosing state or reconciling the target attempt"
200
+ },
201
+ {
202
+ "id": "recovery-never-resumes",
203
+ "category": "recovery",
204
+ "given": "A valid interrupted-attempt recovery is accepted",
205
+ "when": "The runner tries to use recovery as execution, transfer, log, fence renewal, or completion authority",
206
+ "then": "Every operation is rejected because recovery only reconciles and never restores a fence"
207
+ },
208
+ {
209
+ "id": "compiled-step-order",
210
+ "category": "execution",
211
+ "given": "A workflow job compiles to multiple ordered steps",
212
+ "when": "The runner executes the accepted job",
213
+ "then": "Fresh containers run each argument array in declaration order against the same bounded workspace and stop after the first non-zero exit"
214
+ },
215
+ {
216
+ "id": "immutable-image-required",
217
+ "category": "execution",
218
+ "given": "A compiled job names an OCI image without a sha256 digest",
219
+ "when": "The coordinator validates or runner receives the job",
220
+ "then": "The job is rejected before execution"
221
+ },
222
+ {
223
+ "id": "command-total-bytes",
224
+ "category": "limits",
225
+ "given": "Every command argument is individually valid but their combined UTF-8 bytes exceed 128 KiB",
226
+ "when": "The workflow is compiled or the lease is validated",
227
+ "then": "The job is rejected before execution"
228
+ },
229
+ {
230
+ "id": "workflow-dag-rejection",
231
+ "category": "compilation",
232
+ "given": "A workflow contains a missing dependency, cycle, or self-dependency",
233
+ "when": "The coordinator compiles the expanded DAG",
234
+ "then": "The workflow fails before any job is enqueued"
235
+ },
236
+ {
237
+ "id": "workflow-expansion-limit",
238
+ "category": "compilation",
239
+ "given": "Matrix expansion would produce more than 256 jobs",
240
+ "when": "The coordinator expands the workflow",
241
+ "then": "The entire workflow fails without truncation or enqueueing"
242
+ },
243
+ {
244
+ "id": "duplicate-transfer-names",
245
+ "category": "compilation",
246
+ "given": "A job repeats an artifact name, cache name, or overlaps declared cache paths",
247
+ "when": "The coordinator compiles the job",
248
+ "then": "The workflow fails before enqueueing"
249
+ },
250
+ {
251
+ "id": "environment-secret-collision",
252
+ "category": "compilation",
253
+ "given": "Merged environment, job-wide secret targets, or reserved SHIPZONE_CI names collide or exceed 128 names",
254
+ "when": "The coordinator compiles a step",
255
+ "then": "The workflow fails before secret resolution or enqueueing"
256
+ },
257
+ {
258
+ "id": "capability-fail-closed",
259
+ "category": "claim",
260
+ "given": "A leased job violates the runner capability snapshot despite coordinator matching",
261
+ "when": "The runner validates before acceptance",
262
+ "then": "The runner does not accept or execute the job and abandons it as runner_error while the fence is live"
263
+ },
264
+ {
265
+ "id": "archive-path-escape",
266
+ "category": "integrity",
267
+ "given": "A source, artifact, or cache archive contains traversal, duplicate normalized paths, unsafe links, or unsupported entry types",
268
+ "when": "The receiver validates the staged archive",
269
+ "then": "The archive is rejected before extraction or atomic publication"
270
+ },
271
+ {
272
+ "id": "archive-decompression-limit",
273
+ "category": "limits",
274
+ "given": "A source, artifact, or cache archive exceeds compressed bytes, extracted bytes, or entry ceilings",
275
+ "when": "Either party independently measures the staged archive",
276
+ "then": "Processing stops, no partial archive is published, and execution terminates system_error when required"
277
+ },
278
+ {
279
+ "id": "transfer-metadata-recomputed",
280
+ "category": "integrity",
281
+ "given": "Transfer headers declare digest, compressed bytes, extracted bytes, and entries",
282
+ "when": "The receiver stages the complete tar.gz archive",
283
+ "then": "The receiver recomputes all values and rejects any mismatch rather than trusting sender metadata"
284
+ },
285
+ {
286
+ "id": "workspace-limit",
287
+ "category": "limits",
288
+ "given": "Source, restored caches, or step output exceeds negotiated workspace bytes or entries",
289
+ "when": "The runner enforces its attempt-owned workspace quota",
290
+ "then": "Execution is terminated and completed system_error while the fence remains valid"
291
+ },
292
+ {
293
+ "id": "total-log-limit",
294
+ "category": "limits",
295
+ "given": "An attempt reaches its negotiated total log byte ceiling",
296
+ "when": "Execution produces additional output",
297
+ "then": "The runner terminates execution and completes system_error without silently truncating logs"
298
+ },
299
+ {
300
+ "id": "artifact-upload-exact-replay",
301
+ "category": "transfers",
302
+ "given": "An artifact was atomically published but its TransferRecord response was lost",
303
+ "when": "The exact transfer request ID, identity, metadata, and body digest are retried",
304
+ "then": "The coordinator returns the same TransferRecord without publishing another artifact"
305
+ },
306
+ {
307
+ "id": "cache-upload-exact-replay",
308
+ "category": "transfers",
309
+ "given": "A cache generation was atomically published but its TransferRecord response was lost",
310
+ "when": "The exact transfer request ID, identity, metadata, and body digest are retried",
311
+ "then": "The coordinator returns the same TransferRecord without creating another generation"
312
+ },
313
+ {
314
+ "id": "credential-replay-window-ordering",
315
+ "category": "credentials",
316
+ "given": "A registration or replacement credential response declares a replay or overlap deadline",
317
+ "when": "The response is created",
318
+ "then": "Credential expiry is at least one maximum heartbeat interval after the corresponding deadline"
319
+ },
320
+ {
321
+ "id": "session-start-exact-replay",
322
+ "category": "sessions",
323
+ "given": "A session was atomically created and its response was lost",
324
+ "when": "The exact session request ID and canonical body are retried",
325
+ "then": "The coordinator returns the same session response without fencing that session or creating another"
326
+ },
327
+ {
328
+ "id": "session-start-atomic-fencing",
329
+ "category": "sessions",
330
+ "given": "A runner intentionally starts a session with a new request ID",
331
+ "when": "The new response and replay record are durably committed",
332
+ "then": "All older sessions become stale atomically and cannot mutate their attempts"
333
+ },
334
+ {
335
+ "id": "compiled-job-permission-coherence",
336
+ "category": "compilation",
337
+ "given": "A compiled job declares transfers inconsistent with its permissions or required feature flags",
338
+ "when": "The coordinator enqueues or runner validates the job",
339
+ "then": "The job is rejected before execution"
340
+ },
341
+ {
342
+ "id": "compiled-job-limit-coherence",
343
+ "category": "compilation",
344
+ "given": "Source metadata, declaration counts, or artifact/cache maxima exceed the compiled job requirements",
345
+ "when": "The coordinator enqueues or runner validates the job",
346
+ "then": "The job is rejected before execution"
347
+ },
348
+ {
349
+ "id": "source-header-ceilings",
350
+ "category": "limits",
351
+ "given": "A source response declares compressed or extracted measurements",
352
+ "when": "The response is validated",
353
+ "then": "The source-specific 8 GiB compressed, 64 GiB extracted, and 1,000,000 entry ceilings apply"
354
+ },
355
+ {
356
+ "id": "safe-archive-collection",
357
+ "category": "integrity",
358
+ "given": "An artifact or cache path contains symlinks, mount points, or filesystem boundaries",
359
+ "when": "The runner creates its tar.gz archive",
360
+ "then": "Traversal never follows links or leaves the declared attempt-workspace root"
361
+ },
362
+ {
363
+ "id": "matrix-json-binding",
364
+ "category": "compilation",
365
+ "given": "A matrix job expands to a selected dimension/value object",
366
+ "when": "The coordinator compiles each expanded job",
367
+ "then": "Every step receives the RFC 8785 object as SHIPZONE_CI_MATRIX_JSON without interpolation"
368
+ },
369
+ {
370
+ "id": "manual-input-json-binding",
371
+ "category": "compilation",
372
+ "given": "A manual dispatch supplies values matching the declared types and choices",
373
+ "when": "The coordinator compiles the run",
374
+ "then": "Every step receives the RFC 8785 supplied-value object as SHIPZONE_CI_INPUTS_JSON without interpolation"
375
+ },
376
+ {
377
+ "id": "anchored-glob-grammar",
378
+ "category": "triggers",
379
+ "given": "Branch and tag filters contain literals, star, globstar, question mark, or escapes",
380
+ "when": "The coordinator matches a complete UTF-8 short ref name",
381
+ "then": "Matching follows the normative anchored case-sensitive grammar and rejects invalid patterns"
382
+ },
383
+ {
384
+ "id": "matrix-number-domain",
385
+ "category": "compilation",
386
+ "given": "A matrix contains a YAML numeric scalar",
387
+ "when": "The coordinator resolves the scalar before schema validation and RFC 8785 binding",
388
+ "then": "It rejects non-finite or unsafe-integer values and converts accepted values to IEEE 754 binary64 using round-to-nearest ties-to-even"
389
+ },
390
+ {
391
+ "id": "json-and-log-limits",
392
+ "category": "limits",
393
+ "given": "A client sends a JSON body above 2 MiB or a log chunk above the negotiated maximum",
394
+ "when": "The request crosses the limit",
395
+ "then": "The server stops reading, returns 413, and performs no state mutation"
396
+ },
397
+ {
398
+ "id": "profile-shape-exclusive",
399
+ "category": "execution",
400
+ "given": "A compiled job mixes profile shapes, such as an oci-image job with steps or an oci job with a build object",
401
+ "when": "The coordinator enqueues or the runner validates the job",
402
+ "then": "The job is rejected before execution because each execution profile admits exactly one shape"
403
+ },
404
+ {
405
+ "id": "lease-grant-binding-coherence",
406
+ "category": "claim",
407
+ "given": "A lease has a missing or extra image binding, a second grant for one registry, repository, and access, a push grant on an oci job, or an oci-image job without exactly one push grant",
408
+ "when": "The runner validates the lease before acceptance",
409
+ "then": "The runner does not accept the job and abandons it as runner_error while the fence is live"
410
+ },
411
+ {
412
+ "id": "builder-runner-dedicated",
413
+ "category": "claim",
414
+ "given": "A runner advertises oci-image together with another execution profile or without microvm isolation",
415
+ "when": "The coordinator validates the capability snapshot",
416
+ "then": "The capability snapshot is rejected and no build job is leased to that runner"
417
+ },
418
+ {
419
+ "id": "native-platform-only",
420
+ "category": "isolation",
421
+ "given": "A job requires a platform that the runner can execute only through binfmt_misc or user-mode emulation",
422
+ "when": "The runner advertises capabilities or validates the lease",
423
+ "then": "The platform is not advertised, the coordinator does not lease the job, and a leased mismatch is abandoned as runner_error"
424
+ },
425
+ {
426
+ "id": "microvm-attempt-teardown",
427
+ "category": "isolation",
428
+ "given": "A microvm attempt reaches a terminal state, is abandoned, or loses its fence",
429
+ "when": "The runner releases the attempt",
430
+ "then": "The guest, its root filesystem, image store, builder state, and workspace are destroyed and no later attempt observes any of them"
431
+ },
432
+ {
433
+ "id": "grant-not-in-environment",
434
+ "category": "grants",
435
+ "given": "A lease carries push or pull grants",
436
+ "when": "The runner executes steps or Dockerfile instructions and emits logs, errors, and image layers",
437
+ "then": "No grant secret appears in any step environment, build argument, build secret, log chunk, error body, image layer, or image configuration"
438
+ },
439
+ {
440
+ "id": "grant-fence-revocation",
441
+ "category": "grants",
442
+ "given": "An attempt holds a push grant or a pull grant",
443
+ "when": "Its fence is lost, it is abandoned, or it becomes terminal before a later registry request that uses the grant, whether a read or a write",
444
+ "then": "The registry rejects the request immediately because it checks live attempt and fence state on every grant use, without waiting for grant expiry"
445
+ },
446
+ {
447
+ "id": "push-grant-scope",
448
+ "category": "grants",
449
+ "given": "A build attempt holds a push grant for one candidate repository",
450
+ "when": "It writes a tag, deletes content, or writes to any other repository",
451
+ "then": "The registry rejects the request; only blob uploads, permitted blob mounts, and manifests referenced by digest are accepted"
452
+ },
453
+ {
454
+ "id": "pull-grant-read-only",
455
+ "category": "grants",
456
+ "given": "An attempt holds a pull grant for one repository",
457
+ "when": "It uploads or mounts a blob, writes a manifest or a tag, deletes content, or reads any other repository",
458
+ "then": "The registry rejects the request; only reads of blobs and manifests in the granted repository are accepted"
459
+ },
460
+ {
461
+ "id": "grant-origin-binding",
462
+ "category": "grants",
463
+ "given": "A grant names one registry host",
464
+ "when": "A registry response redirects, or a request would reach any other origin",
465
+ "then": "The runner never sends the grant credential to another origin and fails the operation instead of following the redirect"
466
+ },
467
+ {
468
+ "id": "image-record-exact-replay",
469
+ "category": "images",
470
+ "given": "An image record was accepted and its response was lost",
471
+ "when": "The exact recordRequestId, identity, and canonical body are retried",
472
+ "then": "The coordinator returns the same ImageRecord without creating another record; a changed body returns 409"
473
+ },
474
+ {
475
+ "id": "image-record-verification",
476
+ "category": "images",
477
+ "given": "A runner records an image whose manifest digest, size, media type, platform, or configuration labels differ from the pushed manifest or the compiled job, or whose manifest was not written under the attempt push grant",
478
+ "when": "The coordinator verifies the record against the registry",
479
+ "then": "The coordinator returns 409, records nothing, and the attempt cannot succeed"
480
+ },
481
+ {
482
+ "id": "build-success-requires-image-record",
483
+ "category": "images",
484
+ "given": "An oci-image attempt has no accepted image record",
485
+ "when": "The runner submits a succeeded completion",
486
+ "then": "The coordinator returns 409; the runner completes system_error while its fence remains valid"
487
+ },
488
+ {
489
+ "id": "build-output-single-manifest",
490
+ "category": "builds",
491
+ "given": "An oci-image build completes",
492
+ "when": "The runner pushes its output",
493
+ "then": "Exactly one single-platform OCI image manifest is pushed by digest without tags, provenance, SBOM, or attestation manifests"
494
+ },
495
+ {
496
+ "id": "build-unpinned-base-rejected",
497
+ "category": "builds",
498
+ "given": "A Dockerfile resolves an image source, including a syntax frontend, that is neither a declared base image nor pinned by SHA-256 digest",
499
+ "when": "The runner builds the job",
500
+ "then": "The build fails before the image source is fetched and the attempt completes failed"
501
+ },
502
+ {
503
+ "id": "build-secret-not-in-history",
504
+ "category": "builds",
505
+ "given": "A build secret is mounted into a Dockerfile instruction that reads it without writing it to the image filesystem",
506
+ "when": "The output image and build cache are inspected",
507
+ "then": "The secret value appears in no layer, history entry, configuration, label, or exported cache record"
508
+ },
509
+ {
510
+ "id": "build-input-secret-collision",
511
+ "category": "compilation",
512
+ "given": "A build argument name equals a job secret target or build secret id, or a build secret references an undeclared secret target, or a declared secret target is not referenced by any build secret",
513
+ "when": "The coordinator compiles the workflow",
514
+ "then": "The workflow fails before secret resolution or enqueueing",
515
+ "fixture": "conformance/ci-actions/compile-invalid/build-arg-secret-collision.yml"
516
+ },
517
+ {
518
+ "id": "candidate-reference-requires-needs",
519
+ "category": "compilation",
520
+ "given": "A job references a candidate without listing the producing build job in needs, or references a job that is not a build job, or a base-image candidate lacks a platform of the consuming build",
521
+ "when": "The coordinator compiles the workflow",
522
+ "then": "The workflow fails before any job is enqueued",
523
+ "fixture": "conformance/ci-actions/compile-invalid/candidate-without-needs.yml"
524
+ },
525
+ {
526
+ "id": "candidate-binding-mismatch",
527
+ "category": "bindings",
528
+ "given": "A lease binds a candidate to a digest",
529
+ "when": "The pulled manifest or index digest differs, or the index lacks the job platform",
530
+ "then": "The runner does not execute the job and completes system_error while the fence remains valid"
531
+ },
532
+ {
533
+ "id": "candidate-trust-isolation",
534
+ "category": "bindings",
535
+ "given": "Candidates exist from runs of different trust classes",
536
+ "when": "The coordinator issues bindings and pull grants",
537
+ "then": "A job is bound only to candidates assembled in its own run, and a grant never reaches a repository of another trust class"
538
+ },
539
+ {
540
+ "id": "candidate-pull-attempt-private",
541
+ "category": "bindings",
542
+ "given": "An image was pulled with a pull grant",
543
+ "when": "Another attempt on the same runner requests the same digest",
544
+ "then": "The content is served only from the pulling attempt private store, which is removed at attempt end"
545
+ },
546
+ {
547
+ "id": "index-assembly-deterministic",
548
+ "category": "candidates",
549
+ "given": "Every platform build of a candidate has an accepted image record",
550
+ "when": "The coordinator assembles the candidate index",
551
+ "then": "The index lists the manifests in declared platform order with no annotations and is serialized with RFC 8785, so equal inputs produce the candidate-image-index digest fixture bytes"
552
+ },
553
+ {
554
+ "id": "untrusted-candidate-retention",
555
+ "category": "candidates",
556
+ "given": "An untrusted run assembles a candidate",
557
+ "when": "The coordinator stores and later evaluates it for publication",
558
+ "then": "The candidate lives only in the untrusted candidate namespace, is deleted within 7 days, and is never promotable"
559
+ },
560
+ {
561
+ "id": "network-default-none",
562
+ "category": "network",
563
+ "given": "A workflow job omits network",
564
+ "when": "The coordinator compiles the job",
565
+ "then": "The compiled job carries network mode none; no implicit allowlist exists and egress requires at least one entry"
566
+ },
567
+ {
568
+ "id": "egress-unlisted-host",
569
+ "category": "network",
570
+ "given": "A job runs with an egress allowlist",
571
+ "when": "Code in the sandbox resolves or connects to a name outside the allowlist, uses a non-allowlisted port, or sends UDP other than DNS to the runner resolver",
572
+ "then": "The resolver refuses the name and the connection is blocked"
573
+ },
574
+ {
575
+ "id": "egress-ip-literal",
576
+ "category": "network",
577
+ "given": "A job runs with an egress allowlist",
578
+ "when": "Code connects to an IP address that the runner resolver did not return to this attempt for an allowed name within its TTL",
579
+ "then": "The connection is blocked; IP literals are also rejected in allowlist entries at compilation"
580
+ },
581
+ {
582
+ "id": "egress-private-rebinding",
583
+ "category": "network",
584
+ "given": "An allowlisted name resolves to a loopback, private, link-local, metadata, shared, unique-local, multicast, unspecified, or deployment-internal address",
585
+ "when": "The runner resolver answers the query",
586
+ "then": "The address is withheld and connections to it are blocked"
587
+ },
588
+ {
589
+ "id": "egress-name-mismatch",
590
+ "category": "network",
591
+ "given": "A connection to an address pinned for an allowed name uses port 443 or 80",
592
+ "when": "Its TLS SNI or HTTP Host names a different host",
593
+ "then": "The runner blocks the connection"
594
+ },
595
+ {
596
+ "id": "publish-requires-all-jobs-succeeded",
597
+ "category": "publication",
598
+ "given": "A tag run has at least one failed, canceled, timed-out, or system_error job",
599
+ "when": "The run finishes",
600
+ "then": "Qualification never starts and nothing is published"
601
+ },
602
+ {
603
+ "id": "publish-denied-untrusted",
604
+ "category": "publication",
605
+ "given": "A pull request, branch push, manual dispatch, or other untrusted run declares a publish block",
606
+ "when": "The coordinator resolves permissions",
607
+ "then": "Publish permissions resolve to none and no qualification or publication occurs"
608
+ },
609
+ {
610
+ "id": "publish-requires-protected-tag",
611
+ "category": "publication",
612
+ "given": "A tag push runs a workflow with a publish block",
613
+ "when": "The tag matches no protected-tag rule, the rule does not authorize the actor, or the tagged commit is not reachable from a protected branch",
614
+ "then": "Publish permissions resolve to none and nothing is published"
615
+ },
616
+ {
617
+ "id": "publish-resume-authorized-maintainer",
618
+ "category": "publication",
619
+ "given": "A tag run stopped before completing its publications",
620
+ "when": "A maintainer authorized by the protected-tag rule dispatches a resume",
621
+ "then": "The coordinator reuses the compiled plan, candidates, artifacts, and qualification record, continues from the first incomplete publication, and rejects the resume from any other actor"
622
+ },
623
+ {
624
+ "id": "publish-credentials-coordinator-held",
625
+ "category": "publication",
626
+ "given": "A publication targets an image registry, npm registry, or release asset store",
627
+ "when": "The coordinator publishes",
628
+ "then": "It uses destination credentials held by the coordinator and no job, lease, grant, log, or artifact ever receives them"
629
+ },
630
+ {
631
+ "id": "promotion-predecessor-precondition",
632
+ "category": "publication",
633
+ "given": "Qualification recorded the predecessor of each destination alias",
634
+ "when": "The alias target changes before promotion writes it",
635
+ "then": "The coordinator does not overwrite the alias and marks the publication conflict for human resolution"
636
+ },
637
+ {
638
+ "id": "promotion-exact-replay",
639
+ "category": "publication",
640
+ "given": "A destination alias already points at the candidate index digest",
641
+ "when": "Promotion runs again for the same run",
642
+ "then": "The coordinator treats the alias as published without another write"
643
+ },
644
+ {
645
+ "id": "npm-version-matches-tag",
646
+ "category": "publication",
647
+ "given": "An npm publication references an artifact tarball",
648
+ "when": "Qualification reads the tarball package.json",
649
+ "then": "Publication proceeds only when its version equals the tag version, and an existing identical version is success while different content is a conflict"
650
+ },
651
+ {
652
+ "id": "version-alias-requires-semver-tag",
653
+ "category": "publication",
654
+ "given": "A publish block uses a version alias or an npm publication",
655
+ "when": "A tag run is triggered by a tag that, with one optional leading v removed, is not a SemVer 2.0.0 version",
656
+ "then": "Compilation fails and nothing is built or published"
657
+ },
658
+ {
659
+ "id": "sandbox-loopback-private",
660
+ "category": "network",
661
+ "given": "Two concurrent attempts, one with network mode none and one with egress, each start a server on the same fixed loopback port and connect to it",
662
+ "when": "The runner executes both attempts",
663
+ "then": "Each attempt reaches only its own server over loopback, neither bind fails, and no loopback traffic is subject to the network mode or reaches another attempt or the host"
664
+ },
665
+ {
666
+ "id": "matrix-cache-key-shared",
667
+ "category": "compilation",
668
+ "given": "A matrix job declares a cache with a literal key",
669
+ "when": "The coordinator expands and compiles the job",
670
+ "then": "Every expanded job carries the unchanged key, so all expansions address the same cache in the run's cache namespace"
671
+ },
672
+ {
673
+ "id": "workspace-resources-compile-to-limits",
674
+ "category": "compilation",
675
+ "given": "A workflow job declares resources with workspaceBytes, workspaceEntries, memoryBytes, cpus, pids, and sharedMemoryBytes",
676
+ "when": "The coordinator compiles the job",
677
+ "then": "workspaceBytes and workspaceEntries become requirements.limits.maximumWorkspaceBytes and maximumWorkspaceEntries, and the other values appear unchanged in the compiled resources"
678
+ },
679
+ {
680
+ "id": "shared-memory-within-memory",
681
+ "category": "compilation",
682
+ "given": "A workflow job declares resources.sharedMemoryBytes greater than its resources.memoryBytes",
683
+ "when": "The coordinator compiles the workflow",
684
+ "then": "Compilation fails before enqueueing",
685
+ "fixture": "conformance/ci-actions/compile-invalid/shared-memory-above-memory.yml"
686
+ },
687
+ {
688
+ "id": "shared-memory-size",
689
+ "category": "execution",
690
+ "given": "A compiled oci job declares resources.sharedMemoryBytes",
691
+ "when": "A step inspects /dev/shm",
692
+ "then": "/dev/shm in every step container has exactly the declared size, and writes beyond it fail inside the step"
693
+ },
694
+ {
695
+ "id": "resource-capability-matching",
696
+ "category": "claim",
697
+ "given": "A runner advertises resources maxima and a queued job declares a resources value above the matching maximum",
698
+ "when": "The runner claims work",
699
+ "then": "The coordinator does not lease that job to the runner; a job whose declared resources all fit the maxima remains leasable"
700
+ },
701
+ {
702
+ "id": "version-alias-affixes",
703
+ "category": "publication",
704
+ "given": "A tag run for tag v1.2.3 publishes a destination with aliases { kind: version, prefix: v } and { kind: version, prefix: base_, suffix: -alpine }",
705
+ "when": "The coordinator compiles and promotes the candidate",
706
+ "then": "The aliases are v1.2.3 and base_1.2.3-alpine; an expansion that is not a valid OCI tag of at most 128 characters, or two aliases expanding to the same tag, fails compilation"
707
+ },
708
+ {
709
+ "id": "publish-order-declared",
710
+ "category": "publication",
711
+ "given": "A qualified run declares publish.order [releaseAssets, npm]",
712
+ "when": "The coordinator publishes",
713
+ "then": "Every release-asset publication completes before the first npm publication starts, and a release-asset failure leaves npm unpublished; without publish.order the order is images, npm, releaseAssets"
714
+ },
715
+ {
716
+ "id": "release-asset-files-flattened",
717
+ "category": "publication",
718
+ "given": "A release-asset artifact contains a nested path, a link, or a file whose name another release-asset file of the run also uses",
719
+ "when": "The coordinator qualifies the run",
720
+ "then": "Qualification fails and nothing is published; an artifact of regular root files publishes one asset per file named by its file name"
721
+ },
722
+ {
723
+ "id": "release-asset-readback-verified",
724
+ "category": "publication",
725
+ "given": "The forge serves a release asset whose bytes differ from the uploaded file",
726
+ "when": "The coordinator verifies the publication by downloading every asset from the release download location",
727
+ "then": "The publication fails on the size or SHA-256 mismatch and the remaining publications do not run"
728
+ },
729
+ {
730
+ "id": "release-asset-exact-replay",
731
+ "category": "publication",
732
+ "given": "The tag release already holds an asset with the same name and SHA-256, and another asset with the same name but different bytes",
733
+ "when": "Release-asset publication runs",
734
+ "then": "The identical asset counts as published without another upload, and the differing asset is a conflict requiring a human decision"
735
+ },
736
+ {
737
+ "id": "release-prerelease-from-tag",
738
+ "category": "publication",
739
+ "given": "Release-asset publications run for tags v2.0.0-rc.1 and v2.0.0",
740
+ "when": "The coordinator creates the releases",
741
+ "then": "The v2.0.0-rc.1 release is marked as a prerelease and the v2.0.0 release is not"
742
+ },
743
+ {
744
+ "id": "release-draft-conflict",
745
+ "category": "publication",
746
+ "given": "Tag v1.2.3 has an existing draft release named v1.2.3 with the expected body and prerelease mark",
747
+ "when": "Release-asset publication runs for the tag",
748
+ "then": "The publication is a conflict that needs a human; the coordinator neither uploads assets to the draft nor reports success, and every release it creates itself is published, not a draft"
749
+ },
750
+ {
751
+ "id": "release-notes-artifact",
752
+ "category": "publication",
753
+ "given": "publish.releaseNotes references an artifact with exactly one UTF-8 file of at most 1 MiB",
754
+ "when": "The coordinator creates the tag release",
755
+ "then": "The file content is the release body; an artifact with more than one entry, a non-regular entry, or more than 1 MiB fails qualification, and an existing release with a different body is a conflict"
756
+ },
757
+ {
758
+ "id": "vm-requires-microvm",
759
+ "category": "compilation",
760
+ "given": "A workflow job declares runner.profile vm, or a compiled vm job declares container isolation or no requirements.platform",
761
+ "when": "The coordinator compiles or enqueues the job, or the runner validates it",
762
+ "then": "A valid vm job compiles to requirements.isolation microvm and requirements.platform equal to runner.platform; a vm job with container isolation or without a platform is rejected before execution"
763
+ },
764
+ {
765
+ "id": "vm-platform-matching",
766
+ "category": "claim",
767
+ "given": "Runners advertise vm with microvm for linux/amd64, vm with microvm for darwin/arm64, and oci without vm, and a vm job requires linux/amd64 or darwin/arm64",
768
+ "when": "The coordinator matches the job to runners",
769
+ "then": "The job is leased only to a runner whose capability snapshot contains vm, microvm, and exactly the job platform; a darwin/arm64 job never reaches a linux runner, a linux job never reaches a runner that advertises only darwin/arm64, and a capability snapshot with vm but without microvm, or with vm and oci-image, is rejected"
770
+ },
771
+ {
772
+ "id": "vm-guest-root-confined",
773
+ "category": "isolation",
774
+ "given": "A vm job step runs as root in the guest, attaches a loop device, creates network namespaces, starts a container engine, and enumerates devices",
775
+ "when": "The step inspects its environment and attempts to reach host resources",
776
+ "then": "Root privileges work inside the guest, but no host device, filesystem path, or socket is visible, no image grant or runner credential is present in the guest, network enforcement is unchanged, and no other attempt is observable"
777
+ },
778
+ {
779
+ "id": "vm-step-process-teardown",
780
+ "category": "execution",
781
+ "given": "A vm job step starts a daemonized descendant process, such as a container engine, and exits zero",
782
+ "when": "The runner starts the next step",
783
+ "then": "Every process the previous step started, including detached and daemonized descendants, has been terminated before the next step starts, while files the previous step wrote remain in the guest"
784
+ },
785
+ {
786
+ "id": "vm-nested-traffic-enforced",
787
+ "category": "network",
788
+ "given": "A vm job with an egress allowlist runs a container engine in the guest and a nested container connects to an unlisted host, an allowlisted host, and a private address, and a second nested container on the guest bridge",
789
+ "when": "The connections leave the guest through the guest address translation, or stay on the guest bridge",
790
+ "then": "Only the connection to the allowlisted host on an allowlisted port leaves the sandbox; the unlisted host and the private address are refused as for any sandbox connection, and traffic between the two nested containers on the guest bridge is permitted and never leaves the guest"
791
+ },
792
+ {
793
+ "id": "container-platform-linux-only",
794
+ "category": "compilation",
795
+ "given": "An oci job pins runner.platform darwin/arm64, a build job lists darwin/arm64 in build.platforms, or a compiled oci or oci-image job carries requirements.platform darwin/arm64",
796
+ "when": "The workflow or the compiled job is validated",
797
+ "then": "The schema rejects it before compilation or execution, because oci and oci-image execute only linux platforms; darwin/arm64 is accepted only for vm jobs"
798
+ },
799
+ {
800
+ "id": "job-trigger-filter-skips",
801
+ "category": "compilation",
802
+ "given": "A workflow declares push and tag triggers and a job with triggers [tag] that references a secret",
803
+ "when": "A branch push run and a tag run of the same commit are compiled",
804
+ "then": "The push run contains no compiled job, plan digest, lease, or resolved secret for the job or any of its expansions, and its result is unaffected by it; the tag run compiles the job exactly as it would compile it without triggers"
805
+ },
806
+ {
807
+ "id": "needs-on-skipped-job",
808
+ "category": "compilation",
809
+ "given": "A job admits a trigger kind that a job it needs does not admit",
810
+ "when": "The coordinator compiles any run of the workflow, whatever its kind",
811
+ "then": "Compilation fails before enqueueing, so no executed job ever waits for a skipped job",
812
+ "fixture": "conformance/ci-actions/compile-invalid/needs-wider-triggers.yml"
813
+ },
814
+ {
815
+ "id": "publish-reference-admits-tag",
816
+ "category": "compilation",
817
+ "given": "A publish entry references an artifact of a job, or a candidate of a build job, whose triggers do not include tag",
818
+ "when": "The coordinator compiles any run of the workflow",
819
+ "then": "Compilation fails before enqueueing",
820
+ "fixture": "conformance/ci-actions/compile-invalid/publish-job-excludes-tag.yml"
821
+ },
822
+ {
823
+ "id": "trigger-kind-admitted",
824
+ "category": "compilation",
825
+ "given": "A workflow declares a trigger kind in on that no job admits",
826
+ "when": "The coordinator compiles any run of the workflow",
827
+ "then": "Compilation fails before enqueueing, so no run consists only of skipped jobs",
828
+ "fixture": "conformance/ci-actions/compile-invalid/trigger-kind-without-job.yml"
829
+ },
830
+ {
831
+ "id": "secret-denied-untrusted",
832
+ "category": "secrets",
833
+ "given": "A job that is not skipped references a repository secret, in one workflow a secret the repository does not mark protected and in another a protected secret",
834
+ "when": "An untrusted run of the workflow, such as any pull request run, is compiled",
835
+ "then": "Compilation fails with code secret_untrusted before any secret value is resolved or any job is enqueued, whether or not the secret is marked protected"
836
+ },
837
+ {
838
+ "id": "protected-secret-denied-unprotected-run",
839
+ "category": "secrets",
840
+ "given": "A repository marks a secret protected and a job that is not skipped references it",
841
+ "when": "A trusted branch push or manual run of the workflow is compiled",
842
+ "then": "Compilation fails with code protected_secret_denied before any secret value is resolved or any job is enqueued"
843
+ },
844
+ {
845
+ "id": "protected-secret-denied-unprotected-tag",
846
+ "category": "secrets",
847
+ "given": "A repository marks a secret protected and a tag-only job references it",
848
+ "when": "A trusted tag run is compiled whose tag matches no protected-tag rule, whose rule does not authorize the pushing actor, or whose tagged commit is not reachable from a protected branch",
849
+ "then": "Compilation fails with code protected_secret_denied before any secret value is resolved or any job is enqueued"
850
+ },
851
+ {
852
+ "id": "protected-secret-protected-tag-delivered",
853
+ "category": "secrets",
854
+ "given": "A repository marks a secret protected and a tag-only job references it",
855
+ "when": "A protected tag run of the workflow is compiled and the job is leased",
856
+ "then": "Only the lease of that job carries the secret value under its declared target; the compiled plan digest carries only the target, and no other job of the run receives the value"
857
+ },
858
+ {
859
+ "id": "protected-secret-lease-recheck",
860
+ "category": "secrets",
861
+ "given": "A protected tag run compiled a job that references a protected secret",
862
+ "when": "Before the job is leased, the protected-tag rule stops authorizing the tag or the tag moves to another commit",
863
+ "then": "The job is not leased, the secret value is never resolved into a lease, and the job completes system_error"
864
+ },
865
+ {
866
+ "id": "protected-run-cache-isolation",
867
+ "category": "authorization",
868
+ "given": "Caches with one key exist, one published by a job of a trusted branch push run and one by a job of a protected tag run of the same repository",
869
+ "when": "A protected tag run and a trusted run that is not a protected tag run each restore that key and publish it again",
870
+ "then": "The protected tag run restores only the cache a protected tag run published and the other run only the cache the trusted run published; each publication is visible only in its own namespace, and an untrusted run restores neither"
871
+ },
872
+ {
873
+ "id": "secret-log-redaction",
874
+ "category": "secrets",
875
+ "given": "A job receives a secret value of at least 8 bytes, including a multi-line value, and its steps print the value and each of its lines, also across a log chunk boundary",
876
+ "when": "The runner sends the log chunks",
877
+ "then": "Every occurrence is replaced with *** and no chunk, and no concatenation of consecutive chunks, contains the value or any of its lines of at least 8 bytes"
878
+ },
879
+ {
880
+ "id": "secret-artifact-rejected",
881
+ "category": "secrets",
882
+ "given": "A job writes a secret value or an npm grant token into the content, path, or link target of an entry of a declared artifact or read-write cache",
883
+ "when": "The runner prepares the archive for publication",
884
+ "then": "The archive is not published; a required artifact or a cache completes the attempt system_error and an optional artifact is omitted and reported"
885
+ },
886
+ {
887
+ "id": "npm-read-coherence",
888
+ "category": "compilation",
889
+ "given": "A job with npmRead lists a registry twice, lists a scope under two registries, names a registry that its egress allowlist does not permit, or defines NPM_CONFIG_USERCONFIG or a build secret with id npmrc",
890
+ "when": "The coordinator compiles the workflow",
891
+ "then": "Compilation fails before enqueueing",
892
+ "fixture": "conformance/ci-actions/compile-invalid/npm-read-registry-not-allowlisted.yml"
893
+ },
894
+ {
895
+ "id": "npm-read-untrusted-rejected",
896
+ "category": "compilation",
897
+ "given": "A job that is not skipped declares npmRead",
898
+ "when": "An untrusted run of the workflow is compiled, including a pull request from a repository writer",
899
+ "then": "Compilation fails with code npm_read_untrusted before any grant is issued or any job is enqueued"
900
+ },
901
+ {
902
+ "id": "npm-grant-delivery",
903
+ "category": "grants",
904
+ "given": "A trusted run leases an oci job, a vm job, and an oci-image job, each with npmRead",
905
+ "when": "The runner executes them",
906
+ "then": "The oci and vm steps find NPM_CONFIG_USERCONFIG naming a read-only file outside the workspace that maps every declared scope to its registry and holds one token per registry; the oci-image build receives the same content only as the BuildKit secret npmrc"
907
+ },
908
+ {
909
+ "id": "npm-grant-not-in-environment",
910
+ "category": "grants",
911
+ "given": "A lease carries npm grants and the job's steps and Dockerfile instructions use the npm configuration without copying it",
912
+ "when": "The runner prepares the steps or the build, sends logs and errors, publishes artifacts and caches, and records the built image",
913
+ "then": "The runner has placed the token only in the npm configuration file or the npmrc BuildKit secret: no other environment value, command, build argument, build secret, compiled job, log chunk, error body, published artifact or cache, or image layer, label, or configuration field contains it"
914
+ },
915
+ {
916
+ "id": "npm-grant-fence-revocation",
917
+ "category": "grants",
918
+ "given": "An attempt holds an npm grant",
919
+ "when": "Its fence is lost, it is abandoned, or it becomes terminal before a later registry request that uses the token, or the token is used to write or to read another scope",
920
+ "then": "The registry rejects the request immediately because it checks live attempt state and the granted scopes on every use, without waiting for grant expiry"
921
+ },
922
+ {
923
+ "id": "concurrency-waits-in-order",
924
+ "category": "concurrency",
925
+ "given": "A workflow declares concurrency with cancelInProgress false, run A with its concurrency key is active, and runs B and then C with the same key are created",
926
+ "when": "The coordinator schedules the runs",
927
+ "then": "No job of B or C is enqueued while A is active; B starts only after A is terminal and C only after B is terminal, and no run is canceled"
928
+ },
929
+ {
930
+ "id": "concurrency-cancel-in-progress",
931
+ "category": "concurrency",
932
+ "given": "Run A with a concurrency key is active and has not begun qualification; it has a running attempt, a queued job, and a job waiting on needs",
933
+ "when": "Run B with the same key and cancelInProgress true is created",
934
+ "then": "A's waiting and queued jobs become canceled without a lease, the running attempt receives a cancellation directive with reason superseded, no job of A is leased again, A publishes nothing, and no job of B is enqueued before A is terminal"
935
+ },
936
+ {
937
+ "id": "concurrency-cancel-pending",
938
+ "category": "concurrency",
939
+ "given": "Run A with a concurrency key is active and run B with the same key is pending",
940
+ "when": "Run C with the same key and cancelInProgress true is created",
941
+ "then": "B becomes terminal as canceled without any of its jobs being enqueued, A is canceled unless it has begun qualification, and C starts once A is terminal"
942
+ },
943
+ {
944
+ "id": "concurrency-publication-not-canceled",
945
+ "category": "concurrency",
946
+ "given": "Run A with a concurrency key has begun qualification or publication",
947
+ "when": "Run B with the same key and cancelInProgress true is created",
948
+ "then": "A is not canceled and completes its publications; B stays pending until A is terminal"
949
+ },
950
+ {
951
+ "id": "concurrency-key-scope",
952
+ "category": "concurrency",
953
+ "given": "Workflows that declare the same concurrency group and cancelInProgress true, with runs of equal trust class on refs/heads/main, on refs/heads/feature, on two pull requests, a second run of one of those pull requests at a new head commit, and a manual run executing refs/heads/main",
954
+ "when": "The runs are created while earlier ones are active",
955
+ "then": "Runs on different refs and runs of different pull requests never wait for or cancel each other; the second run of a pull request supersedes its first run; the manual run on refs/heads/main supersedes the branch push run of main"
956
+ },
957
+ {
958
+ "id": "concurrency-trust-isolation",
959
+ "category": "concurrency",
960
+ "given": "An active trusted run and an active protected tag run, each with a concurrency key",
961
+ "when": "An untrusted run with the same group and ref as the trusted run, and a trusted tag run that is not a protected tag run with the same group and tag as the protected tag run, are created with cancelInProgress true",
962
+ "then": "Neither new run waits for or cancels the active run it shares a group and ref with, because trust class and the protected-tag-run condition are part of the key"
963
+ },
964
+ {
965
+ "id": "concurrency-requires-key",
966
+ "category": "concurrency",
967
+ "given": "An active run with a concurrency key on a ref, a later commit on that ref whose workflow declares no concurrency, and another commit whose workflow fails compilation",
968
+ "when": "Runs for both commits are created",
969
+ "then": "The run without concurrency executes without waiting and cancels nothing, and the run that failed compilation cancels nothing"
970
+ },
971
+ {
972
+ "id": "concurrency-not-compiled",
973
+ "category": "compilation",
974
+ "given": "Two workflows that differ only in their concurrency declaration",
975
+ "when": "The coordinator compiles both for the same run context",
976
+ "then": "The compiled jobs and their compiled plan digests are identical"
977
+ },
978
+ {
979
+ "id": "timeout-default-compiled",
980
+ "category": "compilation",
981
+ "given": "A matrix job and a build job with two platforms, neither declaring timeoutMs",
982
+ "when": "The coordinator compiles the workflow",
983
+ "then": "Every matrix expansion and every per-platform build job carries timeoutMs 3600000, and the compiled plan digest covers it"
984
+ },
985
+ {
986
+ "id": "timeout-policy-maximum",
987
+ "category": "limits",
988
+ "given": "A deployment job-timeout maximum below a job's declared timeoutMs, or below 3600000 for a job that declares none",
989
+ "when": "The coordinator compiles the workflow",
990
+ "then": "Compilation fails before any job is enqueued; the coordinator never compiles a shortened timeout"
991
+ },
992
+ {
993
+ "id": "timeout-deadline-enforced",
994
+ "category": "execution",
995
+ "given": "An accepted oci or vm attempt whose steps are still running when timeoutMs has elapsed since the runner received HTTP 204 for acceptance; the job declares artifacts with when always, failure, and success and a read-write cache",
996
+ "when": "The deadline is reached",
997
+ "then": "The runner begins termination immediately, force-stops execution within 30 seconds, archives only the always and failure artifacts, publishes no cache, and completes timed_out while its fence remains valid"
998
+ },
999
+ {
1000
+ "id": "timeout-deadline-scope",
1001
+ "category": "execution",
1002
+ "given": "An accepted attempt whose source extraction or cache restoration, or for oci-image whose build, push, or image record, has not finished when timeoutMs elapses, and another attempt whose last step ends before the deadline but whose artifact upload and cache publication end after it",
1003
+ "when": "The runner handles both attempts",
1004
+ "then": "The first completes timed_out; the second completes by its step outcome, because artifact collection and upload, cache publication, and completion are outside the deadline"
1005
+ },
1006
+ {
1007
+ "id": "timeout-cancellation-precedence",
1008
+ "category": "cancellation",
1009
+ "given": "An attempt that receives a cancellation directive before its deadline, and another that reaches its deadline before a cancellation directive arrives",
1010
+ "when": "Both finish termination",
1011
+ "then": "The first completes canceled with the directive ID and the second completes timed_out"
1012
+ },
1013
+ {
1014
+ "id": "timeout-grant-expiry",
1015
+ "category": "grants",
1016
+ "given": "A lease of a job with timeoutMs T that carries image grants or npm grants",
1017
+ "when": "The coordinator issues the lease",
1018
+ "then": "Every grant expiresAt is no later than the attempt timeout plus 30 seconds; because the lease precedes acceptance, the coordinator bounds it by the time it issues the lease plus T plus 30 seconds"
1019
+ },
1020
+ {
1021
+ "id": "retention-compiles-unchanged",
1022
+ "category": "compilation",
1023
+ "given": "A job with one artifact and one cache that declare retentionDays and one artifact and one cache that do not",
1024
+ "when": "The coordinator compiles the workflow",
1025
+ "then": "Each compiled declaration carries retentionDays exactly when the workflow declares it, with the declared value"
1026
+ },
1027
+ {
1028
+ "id": "retention-policy-maximum",
1029
+ "category": "limits",
1030
+ "given": "A deployment retention maximum below a declared retentionDays, or below 30 for an artifact or 7 for a cache that declares none",
1031
+ "when": "The coordinator compiles the workflow",
1032
+ "then": "Compilation fails before any job is enqueued; the coordinator never shortens retention"
1033
+ },
1034
+ {
1035
+ "id": "artifact-retention-expiry",
1036
+ "category": "retention",
1037
+ "given": "Artifacts published with retentionDays N and without retentionDays",
1038
+ "when": "N days, respectively 30 days, have passed since the createdAt of their TransferRecord",
1039
+ "then": "Each artifact is no longer served for download or usable for qualification or publication, and its content is deleted within 7 days after its expiry"
1040
+ },
1041
+ {
1042
+ "id": "cache-retention-expiry",
1043
+ "category": "retention",
1044
+ "given": "The current cache of a key was published with retentionDays N, or without retentionDays, and has been restored since",
1045
+ "when": "N days, respectively 7 days, have passed since the createdAt of its TransferRecord and a job declaring the key starts",
1046
+ "then": "The restores did not extend the expiry; the attempt restores nothing into the cache paths and proceeds as when no cache of the key was ever published, and an older superseded publication of the key is never restored"
1047
+ },
1048
+ {
1049
+ "id": "retention-upload-replay",
1050
+ "category": "retention",
1051
+ "given": "An artifact or cache upload whose content has expired",
1052
+ "when": "The runner sends an exact replay of the upload",
1053
+ "then": "The coordinator returns the recorded TransferRecord without making the content available again or extending its expiry"
1054
+ },
1055
+ {
1056
+ "id": "publication-expired-artifact",
1057
+ "category": "publication",
1058
+ "given": "A protected tag run whose publish block references an artifact that has expired, either before qualification or before a publication resume",
1059
+ "when": "The coordinator qualifies the run or resumes its publications",
1060
+ "then": "Qualification fails, or the publication that needs the artifact fails and stops the remaining publications; release assets and npm packages published earlier from the artifact are unaffected"
1061
+ },
1062
+ {
1063
+ "id": "secret-value-minimum-bytes",
1064
+ "category": "secrets",
1065
+ "given": "A repository secret value of 7 UTF-8 bytes, such as abcdefg, and a value of four two-byte characters, such as äöüß, which is 8 UTF-8 bytes",
1066
+ "when": "Each is stored as a new secret and as the replacement of an existing secret",
1067
+ "then": "The 7-byte value is refused with code secret_too_short, nothing is stored, and a replaced secret keeps its previous value; the 8-byte value is stored, because length counts UTF-8 bytes and not characters"
1068
+ },
1069
+ {
1070
+ "id": "secret-value-short-line",
1071
+ "category": "secrets",
1072
+ "given": "Values of at least 8 bytes: one with a line shorter than 8 bytes, one with an empty line between two lines of at least 8 bytes, and one whose lines are separated by a lone CR and one of them is shorter than 8 bytes",
1073
+ "when": "Each is stored as a secret, and a job that receives it prints the value and then each of its lines on its own",
1074
+ "then": "Every value is stored; the runner replaces the whole value and every line of at least 8 bytes with ***, and leaves every line shorter than 8 bytes, including the line after the lone CR, unmasked by design"
1075
+ },
1076
+ {
1077
+ "id": "secret-value-short-not-delivered",
1078
+ "category": "secrets",
1079
+ "given": "A coordinator holds a secret value shorter than 8 bytes, stored before it implemented the rule, and a job that is not skipped references it",
1080
+ "when": "A trusted run of the workflow is compiled, and, if the secret is protected, the run is a protected tag run",
1081
+ "then": "Compilation fails with code secret_too_short before any secret value is resolved or any job is enqueued; an untrusted run fails with secret_untrusted and a trusted run that is not a protected tag run with protected_secret_denied instead"
1082
+ },
1083
+ {
1084
+ "id": "npm-grant-token-single-line",
1085
+ "category": "grants",
1086
+ "given": "A job that declares npmRead",
1087
+ "when": "The coordinator issues the lease",
1088
+ "then": "Every npmGrants secret has at least 32 characters and contains neither CR nor LF, so the runner's npm configuration holds it on one line and its redaction set holds it as one member of at least 32 bytes"
1089
+ },
1090
+ {
1091
+ "id": "trust-pull-request-untrusted",
1092
+ "category": "trust",
1093
+ "given": "A workflow with a pullRequest trigger and a job that is not skipped referencing a secret the repository does not mark protected, and pull requests opened by a repository writer from a branch of the same repository, by the repository owner, and by an actor without write access from another repository",
1094
+ "when": "The coordinator creates a run for each pull request, and for each update of its head",
1095
+ "then": "Every run is untrusted: compilation fails with code secret_untrusted, and a run of the same workflow without that job restores and publishes caches only in the untrusted namespace"
1096
+ },
1097
+ {
1098
+ "id": "trust-writer-runs-trusted",
1099
+ "category": "trust",
1100
+ "given": "A workflow whose job that is not skipped references a secret the repository does not mark protected",
1101
+ "when": "A repository writer pushes a branch, pushes a tag, or dispatches a manual run, and the coordinator creates and compiles the run",
1102
+ "then": "The run is trusted: compilation succeeds and the lease of that job carries the secret value under its declared target"
1103
+ },
1104
+ {
1105
+ "id": "trust-non-writer-untrusted",
1106
+ "category": "trust",
1107
+ "given": "An actor that the repository's access control does not permit to push to the repository, such as an actor with read access only",
1108
+ "when": "The coordinator creates a manual or push run whose actor is that actor",
1109
+ "then": "The run is untrusted: a job that is not skipped and references a secret fails compilation with code secret_untrusted, and the run is never a protected tag run"
1110
+ },
1111
+ {
1112
+ "id": "trust-fixed-at-creation",
1113
+ "category": "trust",
1114
+ "given": "A trusted branch push run whose actor was a repository writer when the coordinator created the run",
1115
+ "when": "The actor loses write access before the run's jobs are leased",
1116
+ "then": "The run stays trusted: its cache namespace, concurrency key, and secret delivery are those of a trusted run, and no deployment policy or workflow content changes its trust class"
1117
+ },
1118
+ {
1119
+ "id": "lease-feature-matching",
1120
+ "category": "claim",
1121
+ "given": "A runner whose capability snapshot contains leaseFeatures listing image-pull-grants only, and queued jobs whose leases would carry a pull grant, a candidate binding, or npmGrants",
1122
+ "when": "The runner claims work",
1123
+ "then": "The coordinator leases only the job whose lease carries pull grants and no binding or npmGrants; a runner whose snapshot has no leaseFeatures is matched as before, without a lease-feature condition"
1124
+ },
1125
+ {
1126
+ "id": "timeout-capability-matching",
1127
+ "category": "claim",
1128
+ "given": "A runner whose capability snapshot contains maximumTimeoutMs 7200000, and queued jobs with timeoutMs 7200000 and 7200001",
1129
+ "when": "The runner claims work",
1130
+ "then": "The coordinator leases the first job and never the second to that runner; a runner whose snapshot has no maximumTimeoutMs may be leased either"
1131
+ },
1132
+ {
1133
+ "id": "lease-feature-fail-closed",
1134
+ "category": "claim",
1135
+ "given": "A runner that advertised leaseFeatures without npm-read-grants and maximumTimeoutMs 3600000",
1136
+ "when": "It nevertheless receives a lease that carries npmGrants, or a job whose timeoutMs is 3600001",
1137
+ "then": "The runner does not accept or execute the job and abandons it as runner_error before acceptance while the fence is live"
1138
+ },
1139
+ {
1140
+ "id": "grant-applies-exact",
1141
+ "category": "grants",
1142
+ "given": "A lease with a pull grant for registry registry.example and repository ci/deno, and images registry.example/ci/deno@sha256:…, registry.example/ci/deno-tools@sha256:…, and the hostless deno@sha256:…",
1143
+ "when": "The runner pulls each image",
1144
+ "then": "It presents the grant only when pulling registry.example/ci/deno@sha256:…, and pulls the other two without credentials; no default registry or repository prefix is applied to the hostless reference"
1145
+ },
1146
+ {
1147
+ "id": "grant-coherence-mapping",
1148
+ "category": "claim",
1149
+ "given": "A job whose private image is registry.example:5000/team/app@sha256:…",
1150
+ "when": "The coordinator issues the lease",
1151
+ "then": "The lease carries a pull grant whose registry is registry.example:5000 and whose repository is team/app, the text before the first / and between it and the @; a lease without such a grant is not coherent and is not issued"
1152
+ },
1153
+ {
1154
+ "id": "trust-runner-registration-matching",
1155
+ "category": "trust",
1156
+ "given": "Runners registered for trusted work, for untrusted work, and for both, and queued jobs of a trusted branch push run, a protected tag run, and a pull request run, which is always untrusted",
1157
+ "when": "Each runner claims work",
1158
+ "then": "The runner registered for trusted work is leased only jobs of the trusted and protected tag runs, the runner registered for untrusted work only jobs of the pull request run, and the runner registered for both any of them; no lease or capability snapshot carries the trust class"
1159
+ },
1160
+ {
1161
+ "id": "compile-failure-single-code",
1162
+ "category": "compilation",
1163
+ "given": "A workflow and run that violate rules of several layers, or several rules of the workflow, policy, or run layer",
1164
+ "when": "The coordinator compiles the run",
1165
+ "then": "Compilation fails with exactly one code: a code of the first violated layer in the Compilation Failures table and, within the workflow, policy, and run layers, the first violated code in table order"
1166
+ },
1167
+ {
1168
+ "id": "yaml-subset-before-schema",
1169
+ "category": "compilation",
1170
+ "given": "A workflow that is schema-valid except for an anchor, alias, merge key, explicit tag, directive, duplicate key, or non-string key",
1171
+ "when": "The coordinator parses the workflow",
1172
+ "then": "Compilation fails with the YAML-layer code of the violation before JSON Schema validation runs"
1173
+ },
1174
+ {
1175
+ "id": "spec-checked-before-schema",
1176
+ "category": "versioning",
1177
+ "given": "A workflow whose canonical spec is newer than or of another major version than the coordinator's, and which uses a property the coordinator's schema does not define",
1178
+ "when": "The coordinator compiles the workflow",
1179
+ "then": "Compilation fails with spec_incompatible, not with workflow_schema_invalid"
1180
+ },
1181
+ {
1182
+ "id": "trigger-event-not-matched",
1183
+ "category": "triggers",
1184
+ "given": "A valid workflow whose on block does not declare the event's kind, or whose filter does not match the event's ref or target branch",
1185
+ "when": "The event occurs",
1186
+ "then": "No run is created; a commit whose workflow fails the source, YAML, version, schema, or workflow layer instead gets a run that fails with that code for every event"
1187
+ },
1188
+ {
1189
+ "id": "secret-unknown-rejected",
1190
+ "category": "secrets",
1191
+ "given": "A trusted run in which a job that is not skipped references a secret the repository does not hold",
1192
+ "when": "The coordinator compiles the run",
1193
+ "then": "Compilation fails with code secret_unknown before any secret value is resolved or any job is enqueued"
1194
+ },
1195
+ {
1196
+ "id": "secret-catalogue-hidden-from-untrusted",
1197
+ "category": "secrets",
1198
+ "given": "An untrusted run in which jobs that are not skipped reference secrets that do not exist, are protected, or hold values shorter than 8 bytes",
1199
+ "when": "The coordinator compiles the run",
1200
+ "then": "Compilation fails with secret_untrusted, whatever secrets the repository holds, so the failure reveals nothing about them"
1201
+ },
1202
+ {
1203
+ "id": "permission-declaration-required",
1204
+ "category": "compilation",
1205
+ "given": "A job declares an artifact without resolved artifacts write, a cache without resolved caches read or read-write, or a read-write cache without resolved caches read-write",
1206
+ "when": "The coordinator compiles the workflow",
1207
+ "then": "Compilation fails with permission_insufficient and no declaration is dropped"
1208
+ },
1209
+ {
1210
+ "id": "environment-innermost-wins",
1211
+ "category": "compilation",
1212
+ "given": "A name defined in the workflow, job, and step environment of a container job",
1213
+ "when": "The coordinator compiles the job",
1214
+ "then": "Each compiled step carries the value of the innermost level that defines the name, and the name counts once toward the 128-name limit"
1215
+ },
1216
+ {
1217
+ "id": "compiled-job-derivation",
1218
+ "category": "compilation",
1219
+ "given": "A workflow, run context, and deployment policy of a compile vector",
1220
+ "when": "Two conforming coordinators compile the run",
1221
+ "then": "Both produce the node identities, compiled jobs, and compiled plan digests the vector fixes, including requirements.limits derived from the declarations, the isolation of oci jobs, and the order of features and labels"
1222
+ },
1223
+ {
1224
+ "id": "expansion-node-identity",
1225
+ "category": "compilation",
1226
+ "given": "A plain job, a matrix job, and a build job with two platforms",
1227
+ "when": "The coordinator expands the run",
1228
+ "then": "The nodes are named job, job[n] in expansion order, and job[platform] followed by the assembly node job, in plan order, and needs edges point at every expansion of a needed job or at the assembly node of a needed build job"
1229
+ },
1230
+ {
1231
+ "id": "candidate-platform-required",
1232
+ "category": "compilation",
1233
+ "given": "A container or VM job with a candidate image and a runner.platform that the producing build job does not list, or a base-image candidate that lacks a platform of the consuming build",
1234
+ "when": "The coordinator compiles the workflow",
1235
+ "then": "Compilation fails with candidate_platform_missing before any job is enqueued"
1236
+ }
1237
+ ]
1238
+ }