From 8c5d2d59eb62e6f552761fc4152f7b7b0f431049 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Wed, 26 Aug 2026 13:30:51 -0400 Subject: [PATCH] fix(release): the workflow has been invalid since #512 -- `secrets` is not allowed in an `if:` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Release` has not been dispatchable since 2026-08-23. Every push since has recorded a failed run, and the last twenty are all the same failure. ── WHAT WAS WRONG ───────────────────────────────────────────────────────────── The four signing steps #512 added were gated as: if: runner.os == 'macOS' && secrets.MACOS_CERT_P12_BASE64 != '' && ... `secrets` is not a context GitHub allows in a step-level `if:`. It rejects the file at PARSE time -- "Unrecognized named-value: 'secrets'", lines 500, 598, 608 and 644 -- so the whole workflow was invalid, not just those steps. ── WHY NOBODY NOTICED FOR THREE DAYS ────────────────────────────────────────── An unparseable workflow cannot report its own name. GitHub recorded each failure under the FILE PATH (`.github/workflows/release.yml`) rather than under `name: Release`, with zero jobs, on a `push` event -- for a workflow that is `workflow_dispatch`-only and should never run on push at all. Three signals that each look like noise: a run nobody triggered, for a workflow nobody recognises, that failed before doing anything. It was then masked entirely: an unrelated org billing lock started failing every workflow on 2026-08-26 with a three-second "account is locked" error, and this became one red X among many. ── THE FIX, AND THE ONE IT IS NOT ───────────────────────────────────────────── The usual remedy is a job-level `env:` block, because `env` IS allowed in `if:`. Rejected here: it would put every signing certificate and notarisation key into the environment of EVERY step in the job -- `uv sync`, the PyInstaller build, the lot -- which is the exposure #512's own "protected signing environment" exists to prevent. So the secrets stay scoped to the single step that reads them, and what leaves that step is a boolean. `steps.signing.outputs.{macos,windows}` is `yes`/`no`, nothing derived from a certificate reaches an output, and the four conditions read the boolean instead. `shell: bash` on that step because it runs on both macOS and Windows runners and the Windows default is pwsh. ── WHAT THIS MEANS FOR D5 ───────────────────────────────────────────────────── The signing and notarisation work in #512 has never executed. The workflow could not be dispatched, so no release has been cut through it since it landed. This restores the ability to dispatch; whether signing then WORKS is unverified and needs a real run with the secrets present. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2 --- .github/workflows/release.yml | 62 +++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69b0b27..7ac7a7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -490,6 +490,50 @@ jobs: # Checksums AFTER (the sums must cover the signed bytes -- a checksum of an unsigned # binary that is then signed proves nothing about what the user downloads). + # ── WHETHER THIS LEG CAN SIGN, DECIDED IN A STEP AND NOT IN AN `if:` ──────────────────── + # + # **`secrets` is not a context GitHub allows in a step-level `if:`.** The four signing + # steps below were written as `if: runner.os == 'macOS' && secrets.X != ''`, which GitHub + # rejects at PARSE time with "Unrecognized named-value: 'secrets'" -- so the whole file was + # invalid from the moment #512 landed it, `Release` never appeared as a dispatchable + # workflow, and every push recorded a failed run named after the file path rather than + # after `name: Release`. It read as CI noise for three days. + # + # The usual fix is a job-level `env:` block, because `env` IS allowed in `if:`. That is + # rejected here: it would put every signing certificate and notarisation key into the + # environment of EVERY step in this job, including `uv sync` and the PyInstaller build, + # which is precisely the exposure #512's "protected signing environment" exists to avoid. + # + # So the secrets stay scoped to the one step that reads them, and what leaves that step is + # a boolean. `shell: bash` because this runs on both macOS and Windows runners and the + # default on Windows is pwsh; `-n` on the quoted expansions is the whole test. + - name: Can this leg sign? + id: signing + shell: bash + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} + WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }} + WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} + run: | + macos=no + if [ -n "$MACOS_CERT_P12_BASE64" ] && [ -n "$MACOS_CERT_PASSWORD" ] \ + && [ -n "$APP_STORE_CONNECT_KEY_ID" ] && [ -n "$APP_STORE_CONNECT_ISSUER_ID" ] \ + && [ -n "$APP_STORE_CONNECT_KEY_CONTENT" ]; then + macos=yes + fi + windows=no + if [ -n "$WINDOWS_CERT_PFX_BASE64" ] && [ -n "$WINDOWS_CERT_PASSWORD" ]; then + windows=yes + fi + # Only ever yes/no reaches an output. An output is not a secret and is visible in the + # run's API payload, so nothing derived from the certificates themselves goes here. + echo "macos=$macos" >> "$GITHUB_OUTPUT" + echo "windows=$windows" >> "$GITHUB_OUTPUT" + # macOS: Developer ID Application certificate (p12) + App Store Connect API key (the # notarytool credential). Sign with the hardened runtime notarisation requires, # notarise the app and the dmg (--wait, so a Rejected submission fails the step), @@ -497,10 +541,7 @@ jobs: # macos_app.sh produced carries a READ ME that explains Gatekeeper refusals, and a # signed build must not ship a note about a warning it no longer triggers. - name: Sign, notarise and staple (macOS) - if: >- - runner.os == 'macOS' && secrets.MACOS_CERT_P12_BASE64 != '' && - secrets.MACOS_CERT_PASSWORD != '' && secrets.APP_STORE_CONNECT_KEY_ID != '' && - secrets.APP_STORE_CONNECT_ISSUER_ID != '' && secrets.APP_STORE_CONNECT_KEY_CONTENT != '' + if: runner.os == 'macOS' && steps.signing.outputs.macos == 'yes' env: MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} @@ -595,19 +636,14 @@ jobs: # every missing secret and the product that unlocks them -- the #402 discipline, so a # future reader of a green run learns signing was SKIPPED, not forgotten. - name: "Notice: macOS signing skipped" - if: >- - runner.os == 'macOS' && (secrets.MACOS_CERT_P12_BASE64 == '' || - secrets.MACOS_CERT_PASSWORD == '' || secrets.APP_STORE_CONNECT_KEY_ID == '' || - secrets.APP_STORE_CONNECT_ISSUER_ID == '' || secrets.APP_STORE_CONNECT_KEY_CONTENT == '') + if: runner.os == 'macOS' && steps.signing.outputs.macos != 'yes' run: | echo "::notice::macOS build shipped UNSIGNED -- signing, notarisation and stapling are implemented in this workflow but skipped: the Apple credentials are not configured. Notarisation requires the \$99/yr Apple Developer Program (a Developer ID Application certificate, plus an App Store Connect API key for notarytool). To activate: Settings > Environments > 'signing' > add repository secrets MACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER_ID, APP_STORE_CONNECT_KEY_CONTENT -- the full checklist is in docs/desktop-install.md. Until then the artifacts carry build attestations and SHA256SUMS instead." # Windows: sign the setup.exe ONLY (the zip cannot carry a signature). RFC 3161 # timestamping is not optional: an untimestamped signature dies with the certificate. - name: Sign the installer (Windows) - if: >- - runner.os == 'Windows' && secrets.WINDOWS_CERT_PFX_BASE64 != '' && - secrets.WINDOWS_CERT_PASSWORD != '' + if: runner.os == 'Windows' && steps.signing.outputs.windows == 'yes' env: WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }} WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} @@ -641,9 +677,7 @@ jobs: "signed $($setup.Name)" - name: "Notice: Windows signing skipped" - if: >- - runner.os == 'Windows' && (secrets.WINDOWS_CERT_PFX_BASE64 == '' || - secrets.WINDOWS_CERT_PASSWORD == '') + if: runner.os == 'Windows' && steps.signing.outputs.windows != 'yes' run: | echo "::notice::setup.exe shipped UNSIGNED -- signing is implemented in this workflow but skipped: the Windows certificate is not configured. An OV code-signing certificate costs ~\$70-500/yr from a CA (SSL.com, Certum, Sectigo) or Azure Trusted Signing is \$9.99/mo (~\$120/yr); since 2024 an EV certificate no longer buys an instant SmartScreen pass, so EV is not worth paying extra for. To activate: Settings > Environments > 'signing' > add repository secrets WINDOWS_CERT_PFX_BASE64 and WINDOWS_CERT_PASSWORD -- the full checklist is in docs/desktop-install.md. Until then the artifacts carry build attestations and SHA256SUMS instead."