4646 uses : ./.github/workflows/_security.yml
4747 secrets : inherit
4848
49+ # ── 0. Preflight: refuse malformed dispatch inputs ──────────────
50+ # The tag is inputs.version VERBATIM. A bare "0.10.7" publishes a release the
51+ # installers can never resolve (they fetch releases/download/v<version>/...),
52+ # and with immutable releases the mis-named tag cannot be retagged, deleted,
53+ # or its name reused — the name is burned permanently (2026-08-18 incident).
54+ preflight :
55+ runs-on : ubuntu-latest
56+ timeout-minutes : 5
57+ steps :
58+ - name : Enforce v-prefixed semver version input
59+ env :
60+ VERSION : ${{ inputs.version }}
61+ run : |
62+ if [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
63+ echo "version OK: $VERSION"
64+ else
65+ echo "::error::version must be v-prefixed semver (vX.Y.Z), got '$VERSION'"
66+ exit 1
67+ fi
68+
4969 # ── 1. Lint (cppcheck + clang-format) ───────────────────────────
5070 lint :
71+ needs : [preflight]
5172 uses : ./.github/workflows/_lint.yml
5273
5374 # ── 2. Tests (all platforms, full suite for release) ────────────
@@ -65,11 +86,19 @@ jobs:
6586 shard_suites : true
6687
6788 # ── 3. Build all platforms ──────────────────────────────────────
68- # !cancelled() && !failure(): run when `test` is deliberately skipped, but
69- # never when lint or test actually failed.
89+ # `test` may be skipped ONLY by the skip_tests input. A skipped `test` is
90+ # also what a cancelled/timed-out lint produces (needs-cascade), and the
91+ # bare !cancelled() && !failure() form cannot tell those apart: failure()
92+ # does not cover a needed job that was CANCELLED, so a lint timeout let the
93+ # whole pipeline publish with the test matrix silently skipped
94+ # (v0.10.7 incident, 2026-08-18). Require the explicit results.
7095 build :
71- if : ${{ !cancelled() && !failure() }}
72- needs : [test]
96+ if : >-
97+ ${{ !cancelled()
98+ && needs.lint.result == 'success'
99+ && (needs.test.result == 'success'
100+ || (inputs.skip_tests && needs.test.result == 'skipped')) }}
101+ needs : [lint, test]
73102 permissions :
74103 contents : read
75104 id-token : write
@@ -92,15 +121,15 @@ jobs:
92121 # an optional phase needs this override, or the phase being optional silently
93122 # makes the phases after it optional as well.
94123 smoke :
95- if : ${{ !cancelled() && !failure() }}
124+ if : ${{ !cancelled() && needs.build.result == 'success' }}
96125 needs : [build]
97126 uses : ./.github/workflows/_smoke.yml
98127 with :
99128 broad_platforms : true
100129
101130 # ── 5. Soak tests ──────────────────────────────────────────────
102131 soak :
103- if : ${{ !cancelled() && !failure() }}
132+ if : ${{ !cancelled() && needs.build.result == 'success' }}
104133 needs : [build]
105134 uses : ./.github/workflows/_soak.yml
106135 with :
@@ -173,7 +202,7 @@ jobs:
173202 run : scripts/ci/append-legacy-alias-checksums.sh checksums.txt
174203
175204 - name : Attest checksum provenance
176- uses : actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
205+ uses : actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
177206 with :
178207 subject-path : checksums.txt
179208
0 commit comments