-
Notifications
You must be signed in to change notification settings - Fork 0
547 lines (490 loc) · 23.3 KB
/
Copy pathe2e-cli.yml
File metadata and controls
547 lines (490 loc) · 23.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
name: e2e-cli
# Toolchain-heavy CLI runtime E2E gate. ci.yml deliberately defers this (see its
# header: "the toolchain-heavy CLI runtime e2e ... which spawns aspire + docker +
# postgres ... deferred Phase-2 repo process automation umbrella"). This workflow
# is that gate, split into two tiers over `deno task e2e:cli`:
#
# - scaffold-static — deno-only suites (scaffold.service, scaffold.contracts,
# scaffold.plugins): init, generated-surface discovery, registry generation,
# plugin doctor, and generated typecheck. No aspire / docker / postgres, so it
# runs green on a stock runner. This is the immediate scaffold-output
# regression gate and is intended to become a required check once observed
# green.
#
# - scaffold-runtime — the full merge-readiness suite
# (`deno task e2e:cli run scaffold.runtime --cleanup`): scaffold + official
# plugins (workers/sagas/triggers/streams) + DB init/generate/seed + generated
# typecheck + Aspire AppHost boot + HTTP/OTEL behavior + cleanup. Needs the
# standalone `aspire` CLI, .NET, and Docker. ADDITIVE UNTIL OBSERVED GREEN
# (mirrors the `quality` lane rollout in ci.yml): the Aspire-in-CI bring-up is
# unverified and likely needs a debugging pass before this job is promoted to a
# required check via branch protection.
#
# - scaffold-runtime-sqlite — the cheaper full-runtime tier
# (`deno task e2e:cli run scaffold.runtime.sqlite --cleanup`): the same
# generated-project runtime path with sqlite and no Postgres/Redis resources,
# avoiding Postgres and Redis containers. The postgres tier above remains
# the merge-readiness bar.
#
# Cost policy: these additive runtime/native lanes do not run on ordinary PRs.
# `e2e-cli-gate` opts a PR into the scaffold lanes; `desktop-native-gate` opts
# it into native packaging; `ci:full` opts into both. Applying an opt-in label
# triggers the workflow immediately. Version tags and manual dispatch run the
# complete release-grade suite.
#
# Selection policy (see `.github/scripts/ci-classify-changes.ts`): an opted-in
# PR first runs `classify`, which emits the #1152 capability vector
# (`run_static`, `run_runtime_sqlite`, `run_runtime`, `needs_*`) from the diff
# and labels. Ordinary PRs schedule no runner in this workflow. On an opted-in
# PR, lanes outside the selected capability short-circuit to SUCCESS:
#
# - `run_static`: any scaffold-impacting change (`packages/`/`plugins/`/
# `apps/`, tier-defining workflows, toolchain `deno.json*`/`deno.lock`,
# or any unrecognised path).
# - `run_runtime_sqlite`: the cheap runtime tier — `run_static` unless
# `ci:skip-e2e` is present.
# - `run_runtime`: the docker tier — same signal v1 (deliberately wide;
# tighten only against observed green history, per #1152).
# - `needs_desktop`: the `.deb`/updater surface — `packages/cli/`, tier
# workflows, toolchain config (#1151/#1152).
# - Only `e2e-cli.yml` and `ci.yml` escalate the scaffold tiers; other
# workflow edits and tasks-only root `deno.json` diffs do not (#1122).
# - `ci:skip-scaffold` -> skip scaffold-static and its derived sqlite runtime
# tier; `ci:skip-e2e` -> skip both runtime tiers; `ci:full` -> force
# everything regardless.
#
# These lanes are deliberately not required branch-protection checks. Required
# checks live in ci.yml and always materialize, then use the classifier to report
# a native policy skip without doing unrelated work. Release gates (publish,
# e2e-cli-prod*) have no skip.
#
# Within an applicable release/manual/opt-in run, the policy is fail-closed: a
# skip requires `classify` to SUCCEED with an explicit `run_*=false`; a failed
# classifier forces the selected family to run.
# Rename-aware diff: `--name-status -M` feeds BOTH sides of renames/copies so a
# source->docs rename can never classify docs-only.
# Draft PR pushes schedule no classifier, scaffold, runtime, desktop, or
# visibility jobs. Capability-vector and label policy resumes unchanged on
# `ready_for_review` and later non-draft events.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled]
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-cli-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
classify:
name: classify changes
# Applicability gate: release/manual events, or explicitly opted-in PRs.
if: >-
(github.event_name != 'pull_request' || github.event.pull_request.merged != true) &&
(github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
(github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'e2e-cli-gate') ||
contains(github.event.pull_request.labels.*.name, 'desktop-native-gate') ||
contains(github.event.pull_request.labels.*.name, 'ci:full'))
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
diff_unavailable: ${{ steps.diff.outputs.diff_unavailable }}
run_static: ${{ steps.decide.outputs.run_static }}
run_runtime_sqlite: ${{ steps.decide.outputs.run_runtime_sqlite }}
run_runtime: ${{ steps.decide.outputs.run_runtime }}
docs_only: ${{ steps.decide.outputs.docs_only }}
needs_desktop: ${{ steps.decide.outputs.needs_desktop }}
reason: ${{ steps.decide.outputs.reason }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: Self-check the classifier
run: >-
deno test --allow-read --allow-write --allow-env
.github/scripts/ci-classify-changes.test.ts
- name: Compute changed files (merge-base diff, rename-aware)
id: diff
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# `--name-status -M` (NOT `--name-only`) so renames/copies report BOTH
# sides: a `packages/cli/a.ts -> docs/a.md` rename must surface the
# impacting source path, not just the docs destination (rename hole
# found in adversarial review). The classifier parses this format and
# feeds old+new for R*/C* and the deleted path for D.
run: |
diff_file="$(mktemp)"
error_file="$(mktemp)"
trap 'rm -f "$diff_file" "$error_file"' EXIT
if ! git diff --name-status -M "$BASE_SHA...$HEAD_SHA" > "$diff_file" 2> "$error_file"; then
echo "Unable to classify changes because the PR base or head SHA is no longer resolvable." >&2
cat "$error_file" >&2
echo "diff_unavailable=true" >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "diff_unavailable=false"
echo "changed<<__EOF__"
cat "$diff_file"
echo "__EOF__"
} >> "$GITHUB_OUTPUT"
- name: Extract root deno.json (base + head, tasks-only discrimination)
id: rootcfg
if: github.event_name == 'pull_request' && steps.diff.outputs.diff_unavailable != 'true'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# Missing/unreadable content classifies as `toolchain` (fail toward
# running) — hence `|| true` on both sides.
run: |
{
echo "base<<__CFG_EOF__"
git show "$BASE_SHA:deno.json" 2>/dev/null || true
echo "__CFG_EOF__"
echo "head<<__CFG_EOF__"
git show "$HEAD_SHA:deno.json" 2>/dev/null || true
echo "__CFG_EOF__"
} >> "$GITHUB_OUTPUT"
- name: Decide run/skip
id: decide
if: github.event_name != 'pull_request' || steps.diff.outputs.diff_unavailable != 'true'
env:
EVENT_NAME: ${{ github.event_name }}
CHANGED_NAME_STATUS: ${{ steps.diff.outputs.changed }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
ROOT_DENO_JSON_BASE: ${{ steps.rootcfg.outputs.base }}
ROOT_DENO_JSON_HEAD: ${{ steps.rootcfg.outputs.head }}
run: deno run --allow-read --allow-write --allow-env .github/scripts/ci-classify-changes.ts
scaffold-static:
name: scaffold-static (deno-only)
needs: classify
# Applicable only after release/manual/PR opt-in. Within that run, docs-only
# or `ci:skip-scaffold` short-circuits to SUCCESS; classifier failure is
# fail-closed and runs the lane.
if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RUN: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full') || (contains(github.event.pull_request.labels.*.name, 'e2e-cli-gate') && (needs.classify.result != 'success' || needs.classify.outputs.run_static == 'true')) }}
SKIP_REASON: ${{ needs.classify.outputs.reason }}
steps:
- name: Skipped by policy
if: env.RUN != 'true'
# REASON contains raw changed-file names — never interpolate it into
# shell source; pass via env and print with printf.
run: |
printf '::notice::scaffold-static skipped by policy. %s\n' "$SKIP_REASON"
- name: Checkout
if: env.RUN == 'true'
uses: actions/checkout@v5
- name: Setup Deno
if: env.RUN == 'true'
uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: Install workspace dependencies
if: env.RUN == 'true'
run: deno install
- name: scaffold.service (init + generated discovery + typecheck)
if: env.RUN == 'true'
run: deno task e2e:cli run scaffold.service --format pretty --report .llm/tmp/e2e-report-scaffold-service.json
- name: scaffold.contracts (init + contract discovery + typecheck)
if: env.RUN == 'true'
run: deno task e2e:cli run scaffold.contracts --format pretty --report .llm/tmp/e2e-report-scaffold-contracts.json
- name: generated README from a clean clone
if: env.RUN == 'true'
run: deno run --allow-all packages/cli/e2e/src/application/gates/scaffold/verify-clean-clone-readme.ts
- name: scaffold.plugins (official plugins + registry gen + plugin doctor)
if: env.RUN == 'true'
run: deno task e2e:cli run scaffold.plugins --format pretty --report .llm/tmp/e2e-report-scaffold-plugins.json
- name: Print failed static E2E gate evidence
if: failure() && env.RUN == 'true'
shell: bash
run: |
for report in .llm/tmp/e2e-report-scaffold-{service,contracts,plugins}.json; do
if [[ -f "$report" ]]; then
deno run --allow-read .llm/tools/e2e/print-failed-report-steps.ts "$report"
fi
done
- name: Upload static E2E reports
if: always() && env.RUN == 'true'
uses: actions/upload-artifact@v5
with:
name: e2e-cli-scaffold-static-reports
path: .llm/tmp/e2e-report-scaffold-*.json
if-no-files-found: error
scaffold-runtime:
name: scaffold-runtime (aspire + docker + postgres)
needs: classify
concurrency:
group: e2e-scaffold-runtime-global
cancel-in-progress: false
# Applicable only after release/manual/PR opt-in. `ci:skip-e2e` can
# short-circuit the lane; classifier failure is fail-closed and runs it.
# This remains additive until it has enough observed-green history.
if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
RUN: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full') || (contains(github.event.pull_request.labels.*.name, 'e2e-cli-gate') && (needs.classify.result != 'success' || needs.classify.outputs.run_runtime == 'true')) }}
SKIP_REASON: ${{ needs.classify.outputs.reason }}
steps:
- name: Skipped by policy
if: env.RUN != 'true'
# REASON contains raw changed-file names — never interpolate it into
# shell source; pass via env and print with printf.
run: |
printf '::notice::scaffold-runtime skipped by policy. %s\n' "$SKIP_REASON"
- name: Checkout
if: env.RUN == 'true'
uses: actions/checkout@v5
- name: Setup Deno
if: env.RUN == 'true'
uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: Setup .NET
if: env.RUN == 'true'
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Restore pinned Aspire NuGet package cache
if: env.RUN == 'true'
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-aspire-${{ runner.os }}-13.4.6-v1
- name: Install Aspire CLI
if: env.RUN == 'true'
env:
ASPIRE_CLI_VERSION: "13.4.6"
run: |
dotnet tool install Aspire.Cli --tool-path "$HOME/.aspire/bin" --version "$ASPIRE_CLI_VERSION"
echo "$HOME/.aspire/bin" >> "$GITHUB_PATH"
- name: Aspire CLI preflight
if: env.RUN == 'true'
run: |
aspire_version="$(aspire --version)"
echo "$aspire_version"
case "$aspire_version" in
13.4.*) ;;
*) echo "Expected Aspire CLI 13.4.x, got $aspire_version" >&2; exit 1 ;;
esac
- name: Install workspace dependencies
if: env.RUN == 'true'
run: deno install
- name: Full scaffold runtime E2E (one pass, with cleanup)
if: env.RUN == 'true'
run: deno task e2e:cli run scaffold.runtime --cleanup --format pretty --report .llm/tmp/e2e-report-scaffold-runtime.json
- name: Print failed E2E gate evidence
if: failure() && env.RUN == 'true'
run: deno run --allow-read .llm/tools/e2e/print-failed-report-steps.ts .llm/tmp/e2e-report-scaffold-runtime.json
- name: Upload E2E report artifact
if: always() && env.RUN == 'true'
uses: actions/upload-artifact@v5
with:
name: e2e-cli-scaffold-runtime-report
path: |
.llm/tmp/**/report*.json
.llm/tmp/**/report*.ndjson
**/e2e-report*.json
if-no-files-found: ignore
scaffold-runtime-sqlite:
name: scaffold-runtime-sqlite (aspire + sqlite + garnet)
needs: classify
concurrency:
group: e2e-scaffold-runtime-sqlite-global
cancel-in-progress: false
# Applicable only after release/manual/PR opt-in. The sqlite runtime follows
# the scaffold-impact signal; ci:skip-e2e remains authoritative over both
# runtime tiers. Classifier failure is fail-closed and runs it.
if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 40
env:
RUN: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full') || (contains(github.event.pull_request.labels.*.name, 'e2e-cli-gate') && (needs.classify.result != 'success' || needs.classify.outputs.run_runtime_sqlite == 'true')) }}
SKIP_REASON: ${{ needs.classify.outputs.reason }}
steps:
- name: Skipped by policy
if: env.RUN != 'true'
# REASON contains raw changed-file names — never interpolate it into
# shell source; pass via env and print with printf.
run: |
printf '::notice::scaffold-runtime-sqlite skipped by policy. %s\n' "$SKIP_REASON"
- name: Checkout
if: env.RUN == 'true'
uses: actions/checkout@v5
- name: Setup Deno
if: env.RUN == 'true'
uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: Setup .NET
if: env.RUN == 'true'
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Restore pinned Aspire NuGet package cache
if: env.RUN == 'true'
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-aspire-${{ runner.os }}-13.4.6-v1
- name: Install Aspire CLI
if: env.RUN == 'true'
env:
ASPIRE_CLI_VERSION: "13.4.6"
run: |
dotnet tool install Aspire.Cli --tool-path "$HOME/.aspire/bin" --version "$ASPIRE_CLI_VERSION"
echo "$HOME/.aspire/bin" >> "$GITHUB_PATH"
- name: Aspire CLI preflight
if: env.RUN == 'true'
run: |
aspire_version="$(aspire --version)"
echo "$aspire_version"
case "$aspire_version" in
13.4.*) ;;
*) echo "Expected Aspire CLI 13.4.x, got $aspire_version" >&2; exit 1 ;;
esac
- name: Install workspace dependencies
if: env.RUN == 'true'
run: deno install
- name: SQLite scaffold runtime E2E (one pass, with cleanup)
if: env.RUN == 'true'
run: deno task e2e:cli run scaffold.runtime.sqlite --cleanup --format pretty --report .llm/tmp/e2e-report-scaffold-runtime-sqlite.json
- name: Print failed E2E gate evidence
if: failure() && env.RUN == 'true'
run: deno run --allow-read .llm/tools/e2e/print-failed-report-steps.ts .llm/tmp/e2e-report-scaffold-runtime-sqlite.json
- name: Upload E2E report artifact
if: always() && env.RUN == 'true'
uses: actions/upload-artifact@v5
with:
name: e2e-cli-scaffold-runtime-sqlite-report
path: |
.llm/tmp/**/report*.json
.llm/tmp/**/report*.ndjson
**/e2e-report*.json
if-no-files-found: ignore
desktop-native-linux:
name: desktop-native-linux (deb + signed updater)
needs: classify
# Release/manual only unless a PR explicitly requests `desktop-native-gate`
# or `ci:full`. Unlike scaffold lanes, changed paths alone never select this
# roughly three-minute native package/install/update/rollback gate.
if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RUN: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full') || contains(github.event.pull_request.labels.*.name, 'desktop-native-gate') }}
SKIP_REASON: desktop-native runs only for release/manual events or PRs labeled desktop-native-gate/ci:full.
steps:
- name: Skipped by policy
if: env.RUN != 'true'
# REASON contains raw changed-file names — never interpolate it into
# shell source; pass via env and print with printf.
run: |
printf '::notice::desktop-native-linux skipped by policy. %s\n' "$SKIP_REASON"
- name: Checkout
if: env.RUN == 'true'
uses: actions/checkout@v5
- name: Setup Deno
if: env.RUN == 'true'
uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: Install native packaging and desktop prerequisites
if: env.RUN == 'true'
run: >-
sudo apt-get update && sudo apt-get install --yes
bsdiff libgtk-3-0 libwebkit2gtk-4.1-0 xvfb
- name: Install workspace dependencies
if: env.RUN == 'true'
run: deno install
- name: Native desktop package, install, update, and rollback (one pass; #859 exception bounded below)
id: native_suite
if: env.RUN == 'true'
# Active non-blocking policy: #859 tracks the remaining Deno Desktop
# callback/apply gap. The next step accepts only that exact evidence;
# fixture, packaging, or unrelated native failures still fail the job.
continue-on-error: true
run: >-
xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24"
deno task e2e:cli run deploy.desktop-native --cleanup --format pretty
- name: Enforce active desktop exception boundary (#859)
if: always() && env.RUN == 'true'
env:
EVIDENCE: .llm/tmp/desktop-native-e2e/evidence.json
NATIVE_SUITE_OUTCOME: ${{ steps.native_suite.outcome }}
run: >-
deno run --allow-env --allow-read --allow-write
.github/scripts/desktop-native-exception.ts
--outcome "$NATIVE_SUITE_OUTCOME" --evidence "$EVIDENCE"
- name: Upload native desktop evidence
if: always() && env.RUN == 'true'
uses: actions/upload-artifact@v5
with:
name: e2e-cli-desktop-native-linux-report
path: .llm/tmp/desktop-native-e2e/evidence.json
if-no-files-found: warn
lane-visibility:
name: scaffold CI lane visibility
needs: [classify, scaffold-static, scaffold-runtime, scaffold-runtime-sqlite, desktop-native-linux]
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.draft == false && needs.classify.result != 'skipped' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Summarize scaffold lane outcomes
env:
CLASSIFY_RESULT: ${{ needs.classify.result }}
RUN_STATIC: ${{ needs.classify.outputs.run_static }}
RUN_RUNTIME_SQLITE: ${{ needs.classify.outputs.run_runtime_sqlite }}
RUN_RUNTIME: ${{ needs.classify.outputs.run_runtime }}
RUN_DESKTOP: ${{ needs.classify.outputs.needs_desktop }}
STATIC_RESULT: ${{ needs.scaffold-static.result }}
RUNTIME_SQLITE_RESULT: ${{ needs.scaffold-runtime-sqlite.result }}
RUNTIME_RESULT: ${{ needs.scaffold-runtime.result }}
DESKTOP_NATIVE_LINUX_RESULT: ${{ needs.desktop-native-linux.result }}
run: |
describe_scaffold_lane() {
selected="$1"
result="$2"
if [ "$CLASSIFY_RESULT" = "success" ] && [ "$selected" != "true" ]; then
printf 'skipped by policy'
elif [ "$result" = "skipped" ]; then
printf 'not scheduled'
else
printf 'ran (%s)' "$result"
fi
}
if [ "$CLASSIFY_RESULT" = "skipped" ]; then
classify_outcome="not scheduled (unsupported base and no opt-in label)"
else
classify_outcome="ran ($CLASSIFY_RESULT)"
fi
static_outcome="$(describe_scaffold_lane "$RUN_STATIC" "$STATIC_RESULT")"
runtime_sqlite_outcome="$(describe_scaffold_lane "$RUN_RUNTIME_SQLITE" "$RUNTIME_SQLITE_RESULT")"
runtime_outcome="$(describe_scaffold_lane "$RUN_RUNTIME" "$RUNTIME_RESULT")"
desktop_outcome="$(describe_scaffold_lane "$RUN_DESKTOP" "$DESKTOP_NATIVE_LINUX_RESULT")"
{
echo "## Scaffold CI lane visibility"
echo
echo "A successful scaffold job can be a policy skip; this table uses classifier outputs to distinguish it from real execution."
echo
echo "| Lane | Outcome |"
echo "| --- | --- |"
printf '| `classify` | %s |\n' "$classify_outcome"
printf '| `scaffold-static` | %s |\n' "$static_outcome"
printf '| `scaffold-runtime-sqlite` | %s |\n' "$runtime_sqlite_outcome"
printf '| `scaffold-runtime` | %s |\n' "$runtime_outcome"
printf '| `desktop-native-linux` | %s |\n' "$desktop_outcome"
} >> "$GITHUB_STEP_SUMMARY"