Files
callstack__agent-device/.github/workflows/replays-nightly.yml
Michał Pierzchała 539e848e0c fix(ci): stop ten artifact uploads discarding their hidden paths (#2091)
* fix(ci): stop ten artifact uploads discarding their hidden paths

`actions/upload-artifact` has excluded hidden files and directories by default
since v4.4 (this repository pins v4.6.2), and most diagnostics here are written
under `.tmp`. Ten upload steps across seven files therefore uploaded nothing
from those paths: macos.yml's xcresult bundle, both mutation lanes' reports and
shards, replays-nightly's fuzz output, xctest-nightly's results,
test-app-build-cache's fixture tarball, and 1874-diagnose's per-iteration logs.

Most fail silently, since they pair the omission with `if-no-files-found: warn`
or `ignore`. test-app-build-cache sets `error`, so that one does not.

A structural guard rather than a shared upload wrapper: the wrapper would be a
shallow mirror of the action's options over artifacts with different owners,
while the policy question — a hidden path needs the flag — is one rule that
belongs in one place. Each workflow still declares its own artifact.

test/ci/upload-artifact-hidden-paths.test.ts holds it across every workflow and
composite action, and is red if any single flag is dropped.

* test(ci): scan every YAML shape GitHub accepts, not just top-level *.yml

The guard read `.github/workflows/*.yml` and assumed local actions live one
directory deep as `action.yml`. GitHub also reads `.yaml` for both, and local
actions nest, so a hidden-path upload in any of those shapes passed the gate.

It now walks the `.github` tree recursively for either extension, and a second
test plants the three shapes the old scan missed and asserts all three are
found — executable rather than a one-off manual check. Red against narrowing the
extension, against dropping the recursive walk, and against removing any single
real flag.

Also drops the action-version note under the comment rule in #2087; the version
behaviour belongs in the PR, and the assertion message already says what the
omission costs.
2026-08-27 20:08:45 +02:00

117 lines
4.7 KiB
YAML

name: Replay Nightly
# The device replay suites this workflow used to carry moved to replays-manual.yml (#1781 A1)
# after failing every scheduled run from 2026-07-24 on. What stays here is what a schedule can
# still keep honest: the device-free parser fuzz lane.
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
fuzz-iterations:
description: 'Parser fuzz cases per target'
required: false
default: '38000'
fuzz-seed:
description: 'Parser fuzz PRNG seed (defaults to the run number)'
required: false
permissions:
contents: read
actions: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Parser fuzz lane (#1414, validation targets #1781 B2): hostile input into parseArgs,
# selector parsing, .ad replay scripts, batch --steps JSON, and the Maestro compat parser,
# plus schema-derived CLI/Maestro validation cases that assert a planted outcome (specific
# error code, or acceptance) so a silent acceptance is a finding too. Every rejection must
# be a typed AppError with a non-empty hint, and no case hangs. Device-free, so it rides
# this nightly rather than owning a workflow; the seed varies per run so the lane keeps
# exploring, and anything it catches is appended to the checked-in corpus that the unit
# lane replays (scripts/fuzz/corpus/regressions.json).
#
# 38,000 cases/target holds the job's wall-clock at the pre-B2 five-target/50k budget now that
# seven targets share it: measured on a quiet host, 7x38k = 16.5s against 5x50k = 16.6-16.9s.
# Chosen by measurement rather than rounding — 33k would have been 2s cheaper but cost the
# five untouched targets a third of their depth for no wall-clock reason. The count is
# unchanged by #2053; moving case execution out of process cost that run about 3s (17.8s ->
# 20.9s, best of three on one host), which buys a case that faults its process a named
# `crash` failure instead of a dead caller.
nightly-parser-fuzz:
name: Parser Fuzz Lane
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm
# A lane whose classifier or watchdog regressed would pass forever. This runs the
# broken-on-purpose targets first and fails unless each violation is caught.
- name: Self-check the harness
uses: ./.github/actions/run-gate
with:
gate: fuzz-parsers
args: |
--self-check
--artifact-dir
.tmp/fuzz/self-check
# Runs even when the self-check fails, so the lane always produces a fuzz envelope too
# (a step that never runs writes no envelope, and monitoring cannot tell that apart from
# a lane that went dark). The job still fails because both steps report their status.
- name: Fuzz parsers
if: always()
env:
FUZZ_ITERATIONS: ${{ github.event.inputs.fuzz-iterations || '38000' }}
FUZZ_SEED: ${{ github.event.inputs.fuzz-seed || github.run_number }}
uses: ./.github/actions/run-gate
with:
gate: fuzz-parsers
args: |
--iterations
${{ env.FUZZ_ITERATIONS }}
--seed
${{ env.FUZZ_SEED }}
--artifact-dir
.tmp/fuzz/run
# Uploaded on pass as well as failure: each subdirectory of .tmp/fuzz holds a
# run-envelope.json on #1430's shared lane contract (scripts/lib/lane-envelope.ts) —
# commit/ref/run provenance, tool + config hash, seed, result — which is what freshness
# monitoring reads.
- name: Upload run envelopes and failing cases
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: parser-fuzz-run-${{ github.run_id }}-${{ github.run_attempt }}
include-hidden-files: true
path: .tmp/fuzz
if-no-files-found: warn
# Never fails the job on its own: a missing envelope is reported as such rather than
# masking the real failure with a `cat` error.
- name: Summarize
if: always()
run: |
{
echo '### Parser fuzz lane'
found=0
for envelope in .tmp/fuzz/*/run-envelope.json; do
[ -f "$envelope" ] || continue
found=1
echo "#### $envelope"
echo '```json'
cat "$envelope"
echo '```'
done
[ "$found" = 1 ] || echo 'No run envelope was produced — the lane failed before it could run.'
} >> "$GITHUB_STEP_SUMMARY"