From 95b3ba94483d989aa735eee0d7b103849597734d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 7 Aug 2026 19:14:54 +0200 Subject: [PATCH] ci(dry-run): add skip_smoke for a direct build -> VirusTotal pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding the VirusTotal step left one trip unavailable: build straight to the scan. Smoke is in the VirusTotal job's needs, so the only way to reach the scan was to run smoke first — and a smoke failure would withhold precisely the answer such a run was dispatched to get. The skips now cover the three questions worth asking separately: skip_builds no artifacts at all "do lint and tests pass?" skip_smoke build + scan, no smoke "does this diff scan red?" skip_virustotal build + smoke + soak, no scan "do the archives pass CI?" skip_smoke is for the case where the scan verdict is the entire point: the last release scanned clean, so the question is only whether the accumulated diff turns it red. Everything between build and the scan is cost without information for that question. VirusTotal keeps smoke and soak in its needs, and that is deliberate: they order the job last, they do not gate it. A SKIPPED smoke leaves the scan reachable (result 'skipped', which is not 'failure'); a FAILED smoke still withholds it, because a verdict on artifacts already known to be broken is not worth two hours of polling. The release-gate-chain contract covers exactly this distinction — an optional phase must not silently disable the phases after it — and passes. Signed-off-by: Martin Vogel --- .github/workflows/dry-run.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index 49f363d51f..bb0c51c71b 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -13,11 +13,18 @@ # back clean one day and flagged the next, which is precisely why finding out # during a dry run is worth the wait. # -# Two independent skips, because they answer different questions: -# skip_builds no artifacts at all — "do lint and tests pass?" -# skip_virustotal build + smoke + soak, no scan — "do the archives pass CI?" -# The second exists because the scan costs API quota and polls for up to two -# hours, which is pure noise while iterating on a build or smoke failure. +# The skips are independent because they answer different questions: +# skip_builds no artifacts at all "do lint and tests pass?" +# skip_smoke build + scan, no smoke "does this diff scan red?" +# skip_virustotal build + smoke + soak, no scan "do the archives pass CI?" +# +# skip_virustotal exists because the scan costs API quota and polls for up to +# two hours, which is pure noise while iterating on a build or smoke failure. +# +# skip_smoke is the opposite trip: go straight from build to VirusTotal when the +# question is only whether a diff turns the scan red. Smoke is in the VirusTotal +# job's needs, so without this a smoke failure would withhold exactly the answer +# such a run was dispatched to get. name: Dry Run on: @@ -35,6 +42,10 @@ on: description: 'Skip build + smoke (also skips VirusTotal — nothing to scan)' type: boolean default: false + skip_smoke: + description: 'Skip smoke (build straight to VirusTotal)' + type: boolean + default: false skip_virustotal: description: 'Skip VirusTotal scan (still builds + smokes the archives)' type: boolean @@ -86,7 +97,7 @@ jobs: # (including the macos-15-intel darwin-amd64 binary) is blocking now, so a # failed/missing platform binary correctly stops smoke — see _build.yml. smoke: - if: ${{ inputs.skip_builds != true && !cancelled() && needs.build.result != 'failure' && needs.build.result != 'skipped' }} + if: ${{ inputs.skip_builds != true && inputs.skip_smoke != true && !cancelled() && needs.build.result != 'failure' && needs.build.result != 'skipped' }} needs: [build] uses: ./.github/workflows/_smoke.yml with: @@ -112,6 +123,11 @@ jobs: # take up to two hours) and the one most likely to be waiting on someone # else's queue, so it must not delay the feedback that is actually about our # code. + # + # It needs smoke and soak only so it runs LAST, not because it depends on + # them: a skipped smoke or soak leaves the scan reachable (result 'skipped', + # which is not 'failure'), while a FAILED one withholds it — if the artifacts + # are broken, a verdict on them is not worth two hours of polling. virustotal: if: ${{ inputs.skip_builds != true && inputs.skip_virustotal != true && !cancelled() && needs.build.result == 'success' && needs.smoke.result != 'failure' && needs.soak.result != 'failure' }} needs: [build, smoke, soak]