-
Notifications
You must be signed in to change notification settings - Fork 0
401 lines (394 loc) · 18 KB
/
Copy pathshadow-sweep.yml
File metadata and controls
401 lines (394 loc) · 18 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
# The scheduled/manual half of #260's CI strategy: compare mode over the five
# pinned OSS repositories of #243, the large/multi-project solution controls,
# the examples tree, and the Windows path forms. The fast PR gate (the
# committed corpus and the C# samples) lives in ci.yml and is unchanged.
#
# THE RULES THIS WORKFLOW EXISTS TO ENFORCE, none of which a green tick alone
# would show:
#
# * a target is checked out AT ITS PIN and `git rev-parse HEAD` is compared
# with it before anything runs — a target that moved is a failed job, not a
# newer measurement;
# * the extractor runs EXACTLY ONCE per document (`own-check.sh
# --emit-facts` persists the file stage 1 already wrote), and the driver
# reads that file once, as bytes;
# * `OWN_SHADOW_ENGINE` is always set explicitly and the result names the
# adapter by sha256, so a stale build cannot stand in;
# * zero documents compared is a FAILURE, and so is a declared target the run
# never reached — the aggregation re-checks both against the committed
# sweep definition rather than trusting the legs' own exit codes.
#
# Reproduction artifacts upload on failure only (#260's CI strategy). The run
# summary and the per-document results upload always: they are the record.
name: shadow sweep (#260)
on:
workflow_dispatch:
schedule:
# Weekly, Mondays 04:17 UTC. Off the hour on purpose — the top of the hour
# is the busiest slot on shared runners.
- cron: "17 4 * * 1"
permissions:
contents: read
env:
DOCUMENT_OUT: shadow-sweep
jobs:
# One job per DOCUMENT rather than per repository: the directory walk and the
# `.sln` fan-out are different extractor paths over the same checkout, they
# fail differently, and a matrix leg that covered both could not say which
# half a failure belonged to.
document:
name: ${{ matrix.id }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- id: ShareX.repo
target: ShareX
repository: ShareX/ShareX
commit: 0df9ca4d83eed9d2489048c539d7d1fc2860fdec
mode: directory-walk
input: ""
- id: ShareX.sln
target: ShareX
repository: ShareX/ShareX
commit: 0df9ca4d83eed9d2489048c539d7d1fc2860fdec
mode: solution
input: ShareX.sln
- id: MahApps.Metro.repo
target: MahApps.Metro
repository: MahApps/MahApps.Metro
commit: 72099e310bac2d12ac98fd7560b69679252519f5
mode: directory-walk
input: ""
- id: MahApps.Metro.sln
target: MahApps.Metro
repository: MahApps/MahApps.Metro
commit: 72099e310bac2d12ac98fd7560b69679252519f5
mode: solution
input: src/MahApps.Metro.sln
- id: MaterialDesignInXamlToolkit.repo
target: MaterialDesignInXamlToolkit
repository: MaterialDesignInXAML/MaterialDesignInXamlToolkit
commit: ef3a5ea434e39182b1848f5e11aaea6b3890581f
mode: directory-walk
input: ""
- id: AvalonEdit.repo
target: AvalonEdit
repository: icsharpcode/AvalonEdit
commit: ed0bd149059469ac9bd39b13cf8a341b12a6c1da
mode: directory-walk
input: ""
- id: AvalonEdit.sln
target: AvalonEdit
repository: icsharpcode/AvalonEdit
commit: ed0bd149059469ac9bd39b13cf8a341b12a6c1da
mode: solution
input: ICSharpCode.AvalonEdit.sln
- id: ClosedXML.repo
target: ClosedXML
repository: ClosedXML/ClosedXML
commit: 4e89dcedd83cad553e84d2d97f77fc3d7deb630f
mode: directory-walk
input: ""
- id: ClosedXML.sln
target: ClosedXML
repository: ClosedXML/ClosedXML
commit: 4e89dcedd83cad553e84d2d97f77fc3d7deb630f
mode: solution
input: ClosedXML.sln
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# The target, AT ITS PIN. `persist-credentials: false` because nothing in
# this job pushes anywhere and a third-party checkout has no business
# carrying a token.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: ${{ matrix.repository }}
ref: ${{ matrix.commit }}
path: targets/${{ matrix.target }}
persist-credentials: false
- name: Verify the pin
# Drift is a hard failure of this leg, never a warning: a target that
# moved has not been measured, it has been replaced.
run: |
head=$(git -C "targets/${{ matrix.target }}" rev-parse HEAD)
echo "${{ matrix.target }} HEAD=$head pin=${{ matrix.commit }}"
test "$head" = "${{ matrix.commit }}" || {
echo "PIN DRIFT: ${{ matrix.target }} is at $head, the sweep pins ${{ matrix.commit }}"
exit 1
}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10
with:
toolchain: stable
# The WindowsDesktop reference pack, materialized exactly the way the
# corpus-benchmark job does it. NOT continue-on-error here: the pinned
# targets are WPF/WinForms, so a run without the refs measures a
# different document than the sweep says it measured.
- name: Materialize framework reference assemblies
run: |
tmp=$(mktemp -d)
printf '%s\n' \
'<Project Sdk="Microsoft.NET.Sdk">' \
' <PropertyGroup>' \
' <TargetFramework>net8.0-windows</TargetFramework>' \
' <UseWPF>true</UseWPF>' \
' <UseWindowsForms>true</UseWindowsForms>' \
' <EnableWindowsTargeting>true</EnableWindowsTargeting>' \
' </PropertyGroup>' \
'</Project>' > "$tmp/ref.csproj"
dotnet restore "$tmp/ref.csproj" >/dev/null
d=$(find "$HOME/.nuget/packages/microsoft.windowsdesktop.app.ref" -type d -name 'net8.0' | sort | tail -1)
test -n "$d" || { echo "the WindowsDesktop reference pack did not resolve"; exit 1; }
echo "OWN_EXTRA_REF_DIRS=$d" >> "$GITHUB_ENV"
echo "framework refs: $d ($(find "$d" -name '*.dll' | wc -l) dlls)"
- name: Build the dev-only engine adapter
working-directory: rust
run: cargo build --release -p own-shadow --bin own-shadow-engine
# #262 Stage 3: the extraction below invokes `own-check.sh` BARE, and the
# bare surface now runs Rust, so this leg needs a candidate or it exits 2
# before writing a single fact.
#
# The candidate is supplied rather than the command changed. The
# extraction command is part of this sweep's frozen definition and is
# compared against it (tests/shadow_sweep.py), so adding `--engine python`
# here would have meant re-writing the definition and the recorded runs
# that match it. Supplying a candidate also keeps the extraction running
# the actual PUBLIC default, which for a gate about the public product is
# the better of the two.
- name: Build the production own-cli (the bare surface's default engine)
working-directory: rust
run: cargo build --release -p own-cli
- name: Select it
run: echo "OWEN_RUST_CORE=$PWD/rust/target/release/own-cli" >> "$GITHUB_ENV"
# ONE extraction. `--emit-facts` persists the file stage 1 already wrote;
# stage 2 is the reference's verdict path and is neither a second
# extraction nor the comparison.
- name: Extract the OwnIR facts, exactly once
run: |
input="targets/${{ matrix.target }}"
if [ -n "${{ matrix.input }}" ]; then
input="targets/${{ matrix.target }}/${{ matrix.input }}"
fi
echo "extracting: $input"
scripts/own-check.sh --format sarif --severity warning \
--emit-facts "$RUNNER_TEMP/${{ matrix.id }}.facts.json" -- "$input" \
> "$RUNNER_TEMP/findings.sarif.json"
ls -l "$RUNNER_TEMP/${{ matrix.id }}.facts.json"
- name: Describe the document
run: |
python - <<'PY'
import hashlib, json, os
# The manifest lives BESIDE the facts file and names it by a bare
# file name, so the assembled record carries no runner path; the
# name is the document's id, so `source` says which document it is
# rather than repeating one constant for every leg.
name = "${{ matrix.id }}.facts.json"
facts = os.path.join(os.environ["RUNNER_TEMP"], name)
with open(facts, "rb") as f:
digest = hashlib.sha256(f.read()).hexdigest()
target = "${{ matrix.target }}"
rel = "${{ matrix.input }}"
where = f"targets/{target}" + (f"/{rel}" if rel else "")
manifest = {
"schema": 1,
"targets": [target],
"documents": [{
"id": "${{ matrix.id }}",
"source": name,
"target": target,
"target_commit": "${{ matrix.commit }}",
"extraction_mode": "${{ matrix.mode }}",
"extraction_command": (
"OWN_EXTRA_REF_DIRS=<WindowsDesktop ref pack net8.0> "
"scripts/own-check.sh --format sarif --severity warning "
f"--emit-facts <FACTS> -- {where}"),
"facts_sha256": digest,
"timeout_seconds": 600.0,
}],
}
with open(os.path.join(os.environ["RUNNER_TEMP"], "manifest.json"), "w") as f:
json.dump(manifest, f, indent=2)
print(json.dumps(manifest, indent=2))
PY
- name: Compare mode over the document
env:
OWN_SHADOW_ENGINE: ${{ github.workspace }}/rust/target/release/own-shadow-engine
run: |
python scripts/shadow_compare.py --engine compare \
--manifest "$RUNNER_TEMP/manifest.json" \
--out "$RUNNER_TEMP/$DOCUMENT_OUT"
# The record uploads whether or not the run agreed; a leg that only
# uploaded on failure would leave the aggregation unable to tell a green
# target from a skipped one.
- name: Upload the run record
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: shadow-sweep-${{ matrix.id }}
path: ${{ runner.temp }}/${{ env.DOCUMENT_OUT }}
retention-days: 30
if-no-files-found: warn
# The examples tree: cheap, named in #260's test matrix, and the one document
# of this sweep that needs no third-party checkout.
examples:
name: examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "8.0.x"
- uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10
with:
toolchain: stable
- name: Build the dev-only engine adapter
working-directory: rust
run: cargo build --release -p own-shadow --bin own-shadow-engine
# #262 Stage 3, same reason as the matrix leg above: the bare surface's
# default engine is Rust, so this leg needs a candidate to extract at all.
- name: Build the production own-cli (the bare surface's default engine)
working-directory: rust
run: cargo build --release -p own-cli
- name: Select it
run: echo "OWEN_RUST_CORE=$PWD/rust/target/release/own-cli" >> "$GITHUB_ENV"
- name: Extract the OwnIR facts, exactly once
run: |
scripts/own-check.sh --format sarif --severity warning \
--emit-facts "$RUNNER_TEMP/examples.facts.json" -- examples \
> "$RUNNER_TEMP/findings.sarif.json"
- name: Describe the document
run: |
python - <<'PY'
import hashlib, json, os
# A bare file name, for the reason the document job states.
name = "examples.facts.json"
facts = os.path.join(os.environ["RUNNER_TEMP"], name)
with open(facts, "rb") as f:
digest = hashlib.sha256(f.read()).hexdigest()
manifest = {
"schema": 1,
"targets": ["examples"],
"documents": [{
"id": "examples",
"source": name,
"target": "examples",
"target_commit": os.environ["GITHUB_SHA"],
"extraction_mode": "directory-walk",
"extraction_command": (
"scripts/own-check.sh --format sarif --severity warning "
"--emit-facts <FACTS> -- examples"),
"facts_sha256": digest,
"timeout_seconds": 600.0,
}],
}
with open(os.path.join(os.environ["RUNNER_TEMP"], "manifest.json"), "w") as f:
json.dump(manifest, f, indent=2)
PY
- name: Compare mode over the examples document
env:
OWN_SHADOW_ENGINE: ${{ github.workspace }}/rust/target/release/own-shadow-engine
run: |
python scripts/shadow_compare.py --engine compare \
--manifest "$RUNNER_TEMP/manifest.json" \
--out "$RUNNER_TEMP/$DOCUMENT_OUT"
- name: Upload the run record
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: shadow-sweep-examples
path: ${{ runner.temp }}/${{ env.DOCUMENT_OUT }}
retention-days: 30
if-no-files-found: warn
# Windows path forms, MEASURED and nothing promised beyond what runs: the
# committed-corpus gate, on a Windows runner, with an adapter built there.
# The driver's own controls run too and cannot skip — that group is the one
# that had never executed on Windows at all.
windows-path-forms:
name: windows path forms (committed corpus)
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10
with:
toolchain: stable
- name: Build the dev-only engine adapter
working-directory: rust
run: cargo build --release -p own-shadow --bin own-shadow-engine
- name: Compare mode over the committed corpus
env:
OWN_SHADOW_ENGINE: ${{ github.workspace }}/rust/target/release/own-shadow-engine.exe
run: python scripts/shadow_compare.py --engine compare --corpus --quiet --out "$RUNNER_TEMP/shadow"
- name: The compare driver's controls (adapter required)
env:
OWN_SHADOW_ENGINE: ${{ github.workspace }}/rust/target/release/own-shadow-engine.exe
OWN_SHADOW_COMPARE_REQUIRED: "1"
run: python tests/test_shadow_compare.py
- name: Upload the divergence reports
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: shadow-sweep-windows-reports
path: ${{ runner.temp }}/shadow
retention-days: 14
if-no-files-found: ignore
# The aggregation, and the reason it exists: every leg above can be green and
# the SWEEP still be a lie, because a leg that never ran uploads nothing and a
# matrix that lost an entry says nothing at all. This job assembles one run
# record from every leg and checks it against the committed sweep DEFINITION —
# the denominator lives there, not in whatever happened to be measured.
aggregate:
name: aggregate the sweep
runs-on: ubuntu-latest
needs: [document, examples, windows-path-forms]
if: always()
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: shadow-sweep-*
path: ${{ runner.temp }}/legs
- name: Every leg must have run
run: |
echo "document: ${{ needs.document.result }}"
echo "examples: ${{ needs.examples.result }}"
echo "windows path forms: ${{ needs['windows-path-forms'].result }}"
fail=0
for r in "${{ needs.document.result }}" "${{ needs.examples.result }}" \
"${{ needs['windows-path-forms'].result }}"; do
[ "$r" = "success" ] || fail=1
done
test "$fail" -eq 0 || { echo "a leg of the sweep did not succeed"; exit 1; }
- name: Assemble one run record
run: |
python tests/shadow_sweep.py --collect "$RUNNER_TEMP/legs" \
--write "$RUNNER_TEMP/p022-shadow-sweep.result.json" \
--workflow-run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
- name: Check it against the committed sweep definition
run: python tests/shadow_sweep.py --result "$RUNNER_TEMP/p022-shadow-sweep.result.json"
- name: Upload the run summary
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: shadow-sweep-run-summary
path: ${{ runner.temp }}/p022-shadow-sweep.result.json
retention-days: 90
if-no-files-found: warn