-
Notifications
You must be signed in to change notification settings - Fork 0
240 lines (226 loc) · 11.1 KB
/
Copy pathfleet-reconcile.yaml
File metadata and controls
240 lines (226 loc) · 11.1 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
# Fleet Reconcile - the structural coverage gate for the cascade example fleet.
#
# This is maintainer fleet infra: hand-written tooling that lives in cascade's
# repo and is CALLED by each example repo's scenario-suite.yaml as its final
# job. It is NOT a cascade product feature and NOT part of cascade's generated
# output.
#
# Why it exists: a scenario suite only verifies the runs it remembers to wait
# on. A single scenario action causes SECONDARY runs (a PR-close hotfix Finalize
# run, a seed-PR preview run, an incidental push-orchestrate) that suites
# routinely forget to gate, so a suite can stay green over a red run it caused.
# This gate makes "no unasserted run in the scenario window" a structural
# invariant: it enumerates EVERY run the repo produced since the scenario began
# and fails if any run is in the window but not in the suite's run ledger, or
# concluded other than the suite registered.
#
# How a suite uses it:
# 1. At scenario start, record an ISO-8601 timestamp (window-start).
# 2. For every run the suite gates, call the register-run composite action
# (stablekernel/cascade/.github/actions/register-run@<ref>) with the run
# id and its expected conclusion. For a MULTI-JOB suite, pass upload: true
# so each job's ledger reaches this job as an artifact; for a single-job
# suite, pass the workspace ledger path via `ledger-path`.
# 3. As the suite's final job (needs: [<all scenario jobs>], if: always()),
# call this workflow with window-start and the ledger artifact name.
#
# The reconcile logic lives in cascade's own Go (internal/fleetreconcile), so it
# is unit-tested with synthetic run lists and cannot silently regress.
name: Fleet Reconcile
on:
workflow_call:
inputs:
window-start:
description: >-
ISO-8601 timestamp (UTC, e.g. 2026-06-23T14:00:00Z) of when the
scenario began. Every run created at or after this in this repo is
reconciled against the ledger.
required: true
type: string
ledger-artifact:
description: >-
Name (or glob) of the run-ledger artifact(s) uploaded by register-run.
Defaults to the per-job pattern register-run uses. Downloaded and
merged before reconcile. Leave empty when the suite passed an
in-workspace ledger via ledger-path instead.
required: false
type: string
default: 'cascade-run-ledger-*'
ledger-path:
description: >-
Path to an in-workspace ledger (single-job suites). Used only when no
artifact is found. Empty by default.
required: false
type: string
default: ''
require-ledger:
description: >-
When true (the default), the gate requires a ledger to be present: a
missing ledger artifact (or, with ledger-path, a missing/empty file)
fails the gate instead of reconciling against an empty ledger. This is
fail-closed - a ledger that merely failed to download must never look
like "no registered runs" and let an expected:failure run that
actually succeeded pass as benign. Set false ONLY for a suite that
intentionally registers nothing (every run in its window is meant to
be benign-unregistered); such a suite has no expected:failure runs to
mis-pass.
required: false
type: boolean
default: true
cascade-ref:
description: >-
The cascade ref to check out for the reconcile core (the rc tag under
test, or a branch/sha). When empty, falls back to github.workflow_sha,
the SHA of this reusable workflow as resolved for the caller, so the
Go core always matches the gate version the caller pinned.
required: false
type: string
default: ''
allow-workflows:
description: >-
Comma-separated workflow names to reconcile. Empty (default) means all
cascade-generated workflows in the window. Set this to scope out an
unrelated sibling workflow that shares the repo.
required: false
type: string
default: ''
run-list-limit:
description: >-
Page size for the strict-backward run enumeration (the gh run list
--limit per page). The Go core pages until the window is exhausted and
fails closed on truncation, so this is a per-page size, not a hard cap
on total runs reconciled.
required: false
type: number
default: 200
permissions:
contents: read
jobs:
reconcile:
name: Reconcile scenario-window runs
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
# Check out cascade itself for the reconcile core. The reusable workflow
# is referenced at a ref by the caller; we re-check-out cascade at that
# same ref (or an override) so the Go core matches the gate version.
- name: Check out cascade (reconcile core)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: stablekernel/cascade
ref: ${{ inputs.cascade-ref || github.workflow_sha }}
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache: true
# Download every per-job ledger artifact, each into its own subdirectory.
# Do NOT use merge-multiple: the per-job files all share the basename
# cascade-run-ledger.jsonl, so flattening them into one directory would
# overwrite each other and only one job's ledger would survive. With
# per-artifact subdirectories the assemble step's recursive find then
# concatenates one file per job. Missing artifacts are tolerated (a suite
# that gated nothing, or one using the in-workspace ledger path instead).
- name: Download run-ledger artifacts
id: ledger
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: ${{ inputs.ledger-artifact }}
path: ${{ runner.temp }}/ledger-artifacts
- name: Assemble the ledger
id: assemble
env:
ART_DIR: ${{ runner.temp }}/ledger-artifacts
LEDGER_PATH_IN: ${{ inputs.ledger-path }}
REQUIRE_LEDGER: ${{ inputs.require-ledger }}
DOWNLOAD_OUTCOME: ${{ steps.ledger.outcome }}
run: |
set -euo pipefail
LEDGER="${RUNNER_TEMP}/cascade-run-ledger.jsonl"
: > "$LEDGER"
# Prefer downloaded artifacts; merge-multiple may have produced one or
# more .jsonl files. Concatenate them all. Count what we found so the
# empty case can be distinguished from a present-but-empty ledger.
found=0
if [ -d "$ART_DIR" ]; then
found=$(find "$ART_DIR" -type f -name '*.jsonl' | wc -l | tr -d ' ')
if [ "$found" -gt 0 ]; then
find "$ART_DIR" -type f -name '*.jsonl' -exec cat {} + >> "$LEDGER"
fi
fi
# Fall back to an in-workspace ledger (single-job suite path).
used_path=false
if [ ! -s "$LEDGER" ] && [ -n "$LEDGER_PATH_IN" ] && [ -f "$LEDGER_PATH_IN" ]; then
cat "$LEDGER_PATH_IN" >> "$LEDGER"
used_path=true
fi
lines=$(grep -c . "$LEDGER" || true)
echo "ledger has ${lines:-0} registered run(s) (artifact files: ${found}, download outcome: ${DOWNLOAD_OUTCOME})"
# Fail-closed: when a ledger is required (default), a download that
# failed or produced no ledger must NOT pass as "no registered runs".
# An expected:failure run whose ledger entry merely failed to download
# would otherwise look benign-unregistered and wrongly pass the gate.
if [ "$REQUIRE_LEDGER" = "true" ]; then
if [ -n "$LEDGER_PATH_IN" ]; then
# In-workspace mode: the named ledger must exist and be non-empty.
if [ "$used_path" != "true" ] || [ ! -s "$LEDGER" ]; then
echo "::error::fleet-reconcile: require-ledger is set but no ledger was found at ledger-path '${LEDGER_PATH_IN}'. Refusing to reconcile against an empty ledger (an expected:failure run that succeeded could pass as benign). Set require-ledger:false only for a suite that registers nothing."
exit 1
fi
else
# Artifact mode: the download must have succeeded and yielded at
# least one ledger file. A missing artifact is treated as an error
# (a suite that registers runs must always upload its ledger).
if [ "$DOWNLOAD_OUTCOME" != "success" ] || [ "$found" -eq 0 ]; then
echo "::error::fleet-reconcile: require-ledger is set but no ledger artifact was downloaded (outcome: ${DOWNLOAD_OUTCOME}, files: ${found}). Refusing to reconcile against an empty ledger (an expected:failure run that succeeded could pass as benign). Ensure the suite uploads its ledger, or set require-ledger:false for a suite that registers nothing."
exit 1
fi
fi
fi
echo "ledger-path=$LEDGER" >> "$GITHUB_OUTPUT"
# Enumerate EVERY run created in this repo since window-start AND reconcile
# in one binary call. The enumeration/pagination lives in the unit-tested
# Go core (internal/fleetreconcile.EnumerateRuns): it pages strictly
# backward by (createdAt, run id) so a boundary-timestamp cluster cannot
# stall the walk, dedupes by run id, and - critically - FAILS CLOSED if the
# page cap is reached on a full page or a same-timestamp cluster cannot be
# paged. It never reconciles a truncated window. The binary shells out to
# `gh run list --created ">=window-start" ...` for each page.
- name: Enumerate and reconcile scenario-window runs
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
WINDOW_START: ${{ inputs.window-start }}
LIMIT: ${{ inputs.run-list-limit }}
LEDGER: ${{ steps.assemble.outputs.ledger-path }}
SELF_RUN_ID: ${{ github.run_id }}
ALLOW: ${{ inputs.allow-workflows }}
run: |
set -euo pipefail
# Exit 0 = every run accounted for; 1 = a coverage gap reds the gate;
# 2 = tool/input error (which includes a truncated/stalled enumeration,
# so a window we could not fully enumerate also reds the gate). The
# report is printed to the job log and the step summary so a red gate
# names the unaccounted run.
set +e
OUT=$(go run ./internal/fleetreconcile/cmd \
--window-start "$WINDOW_START" \
--repo "$REPO" \
--page-size "$LIMIT" \
--ledger "$LEDGER" \
--self-run-id "$SELF_RUN_ID" \
--allow-workflows "$ALLOW" 2>&1)
code=$?
set -e
echo "$OUT"
{
echo "## Fleet Reconcile"
echo ""
echo '```'
echo "$OUT"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$code"