9 Commits
Author SHA1 Message Date
Joseph MattielloandClaude Opus 5.5 be67eec441 fix(release): skip an earlier attempt's outputs when staging a re-run
Re-running failed jobs in release.yml re-runs the stage job, whose "download
all artifacts" step then also receives the earlier attempt's release-staged,
release-signing-status and signing-table artifacts. stage.sh flattened them
into the release: the old SHA256SUMS.txt got checksummed into the new one,
and README.txt plus eleven .status sidecars became assets. The signing gate
then failed on run 42548727250 attempt 2 (SHA256SUMS mismatch, and
cue2cdi-macos-universal.status read as an unsigned macOS tool).

Exclude those three artifacts. The new test reproduces the failure without
the fix (5 failures) and passes with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 17:55:13 -05:00
Joe Mattiello 6072a8ca5e ci(release): use quick downloads for tagged releases (#1187) 2026-11-06 22:53:41 -05:00
Joe Mattiello 5d0a42c027 ci(release): simplify nightly downloads and signing notes (#1185) 2026-11-06 21:27:34 -05:00
Joe MattielloandClaude Opus 5.5 a314dd3aa4 ci: GL qualification: windowed boot + every shader preset on Linux, Windows x64/arm64 and macOS (#1155) (#1160)
* feat(app): --shader PRESET|off for one run, and a [shader] result line per chain

`--shader PRESET.slangp` runs this one run through that preset and `--shader off`
through none; neither is written back to config.ini (like --theme).  A missing or
malformed preset is a usage error (exit 2) instead of a plain picture with a toast.

shaders.cpp now logs one stderr line when a chain is built or cannot be:
  [shader] <preset> built: GLSL 410 on OpenGL 4.1
  [shader] <preset> refused: ... (the GL_ARB_arrays_of_arrays limit, macOS GL 4.1)
  [shader] <preset> failed: ... (librashader's message)
so a CI run has a record of what the chain did (#1155 GL qualification).

Refs #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci: GL qualification workflow: windowed boot + every shader preset on Linux, Windows, macOS

One reusable workflow, gl-qualification.yml, boots the desktop app windowed with a
real OpenGL context on hosted runners and renders every bundled .slangp through
librashader, keeping the screenshots.  Legs: Linux (ubuntu-24.04, Xvfb + llvmpipe),
Windows x64 (hosted, no vendor GL: the automatic bundled-Mesa relaunch of #1138/#1143
must fire), Windows arm64 (folds in windows-gl-test.yml, which is retired), macOS
arm64 (Apple OpenGL 4.1; the 15 arrays-of-arrays presets are allowlisted as refused).

Per leg: one build, cmake --install into stage/, vj_shader_tests gl run directly
(exit 77 = skipped = failure here), then app/test/gl-qualification/gl_qualify.py
launches --shader off and every preset with --screenshot and asserts exit 0, the
last [gl] renderer line, a non-black PNG, a [shader] built line and a picture that
differs from the plain one.  Artifacts upload pass or fail; the job summary lists
failures with the [gl]/[shader] log tail.  Goldens are designed
(app/test/golden/gl/<os>/<preset>.png, refresh_goldens input) but not gated.

Triggers: pull_request on the GL/shader/packaging paths, workflow_call (release.yml
requires it on tags and release/**), nightly schedule, workflow_dispatch.

Closes #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): judge shaders_gl after the preset loop, fail fast, time the x64 fallback

First run (41565368188) findings: shaders_gl fails on llvmpipe and on Apple's software
renderer (crt-royale renders black in the 128x64 test picture) and the step stopped the
leg before any screenshot existed; the x64 automatic-fallback boot hit `timeout 120`
where arm64 took 3 s; the arm64 shaders_gl could not load the upstream prebuilt
librashader.dll (eager D3DX9_43.dll import, #1153/#1161).

- shaders_gl runs with continue-on-error and a "shaders_gl verdict" step fails the job
  after the preset loop, so the screenshots and logs upload either way.
- gl_qualify.py --fail-fast N (default 8): the loop stops after N consecutive failed
  presets and the summary says how many were not run.
- Windows fallback step: timeout 300 s, elapsed seconds logged (automatic-seconds.txt).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): empty-array expansion safe for macOS bash 3.2 (set -u)

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): Windows leg records the boot, runs timing diagnostics, gates the loop

Second run (41567744247): the x64 automatic-fallback boot did not return within 300 s
although the screenshot was written (frame 20 reached); arm64 does the same in 2 s.

- the loader probe (context create/draw/destroy through the bundled Mesa) runs before
  the app boot, timed;
- the boot step records exit code, seconds and each side condition (automatic-seconds.txt)
  instead of aborting at the first failed check; a verdict step fails the leg later;
- when the boot did not come back, three bounded experiments (the Mesa-side child run
  directly, --emu-thread off, video.vsync=0 seeded) go to timing.txt;
- the preset loop runs only once the plain boot works (500 launches of a boot that does
  not return are hours of nothing); per-launch timeout 150 s.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): known-broken allowlist for presets librashader rejects everywhere

The second Linux run (41567744247) built and rendered 494 of the 500 bundled presets
in ~13 minutes of wall time; the six nedi bilateral-variant presets (all but variant2)
fail reflection with UnknownSemantics("play"): they chain vhs/shaders/vhs.slang, whose
`play` sampler is an LUT the presets never declare.  A preset bug, not a GL one, the
same on every leg, so it gets the same treatment as the macOS refused set: an allowlist
with the reason that may only shrink (a listed preset that builds is an error).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): verdict steps run after an earlier failed step too (always())

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(app): shaders_gl renders the bundled presets at 640x480, not the 128x64 pixel-check picture

On llvmpipe (Linux) and Apple's software renderer (hosted macOS) crt-royale came out
black in shaders_gl: its mask passes scale by 0.0625 of the viewport and the preset says
"Safe for >= 341.333 horizontal triads at viewport size", so at 128x64 they are 8x4 px.
The app at 960x760 renders it (gl-qualification run 41567744247, 76% of pixels differ
from the plain picture).  The pixel checks keep their 128x64 (an exact 4x of the 32x16
source); the bundled loop and VJ_TEST_PRESETS render at a real-world 640x480.

Refs #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): every Windows Mesa launch bounded; llvmpipe single-thread and softpipe probes

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): own librashader artifacts, macOS from source and sized down, release staging excludes qualification artifacts

Review (Codex P1/P2 on #1160) and run 41571727219:
- librashader.yml gains an `artifact_suffix` input; this workflow calls it with `-glq`, so a
  release run (app-package.yml + gl-qualification.yml) uploads no colliding artifact names.
- release.yml's two staging sweeps exclude artifacts/gl-qualification-* (screenshots, logs and
  the arm64 test zip are not release assets).
- macOS leg: the from-source aarch64 runtime the package ships, not upstream's prebuilt 0.12.0
  (crt-black_crush-koko.slangp fails to parse on the prebuilt, parses on the pinned source);
  3 launches at once in a 480x360 window (small-window.ini via gl_qualify.py --seed-config):
  Apple's software renderer managed 205 of 500 presets in 150 min at 960x760 and 2 jobs.
- refused-<os>.txt also covers presets the leg's GL cannot COMPILE (macOS: fsr and smaa+fsr need
  packHalf2x16, GLSL 4.20); a listed preset that builds is an error, the set only shrinks.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(app): gl_context_integration prints unbuffered stage markers (where does the x64 Mesa probe stop?)

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): tracked known failures; the x64 exit hang counted, not masked; loop before shaders_gl

app/test/gl-qualification/known-failures.txt lists one failure per line (<leg> <kind> <reference>);
the workflow reads it and turns exactly that failure into a recorded pass with a note. Nothing else
on the leg is forgiven, and removing the line makes the leg fail again. First entry: windows-x64
exit-hang (#1167), the app never exits on the bundled MSYS2 MINGW64 Mesa 26.2.3 llvmpipe
(SDL_DestroyWindow/SDL_Quit hang after "context destroyed"; LP_NUM_THREADS=0 and softpipe exit
at once, arm64 Mesa exits in 2 s).

gl_qualify.py: --known-exit-hang <ref> polls the launch, kills its process tree 5 s after the
screenshot is complete (IEND present) and counts the preset, noting the known failure in
summary.md and results.json; a launch without a complete screenshot still times out and fails.
The Windows boot step accepts exit 124 when the screenshot and the [gl] lines are there, the
loaders step accepts it on PASS, and shaders_gl passes without its final OK line only when
nothing failed or skipped and its last check (the broken shader) ran.

Every leg runs the preset loop before shaders_gl: on the macOS software renderer a SIGALRM-killed
GL process left the next window creation hanging. shaders_gl's bundled loop and VJ_TEST_PRESETS
render at 384x288 (crt-royale's mask passes need >= 341 px; 640x480 exceeded a 600 s alarm on
Apple's software renderer), the macOS alarm is 1200 s. gl_context_integration prints unbuffered
stage markers so a log shows where a probe stops. docs/agent/testing.md describes the lists.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): capture waits for core frames; unbuffered shaders_gl; macOS 640x480 + capture diagnostics

Run 41727418224 (3eef3305) read per leg:
- Windows x64: the preset loop passed under the tracked exit hang (494 built, 501 launches killed 5 s after
  their screenshot, #1167) but the shaders_gl allowance failed: the test's stdout verdict lines were lost when
  the hung process was killed (a pipe is fully buffered; only the stderr [shader] lines survived).
  vj_shader_tests writes stdout unbuffered from main() on.
- Windows arm64: 493 built, CuNNy-8x32-nvl-2x-rgb timed out at 150 s on the 128-bit llvmpipe. The Windows
  per-launch timeout is 240 s like the other legs.
- macOS: shaders_gl passes at 384x288 (10 min). In the seeded 480x360 window every capture, --shader off
  included, came back a uniform white picture at 1.9 s a launch; not the game (yarc is black at core frame
  1-2 and shows its picture from frame 10 on), not the theme. The leg seeds 640x480 (fits the hosted display,
  keeps crt-royale above 341 px) and a "Capture diagnostics" step launches `off` once per window
  configuration (default, 480x360, +frame 120, +emu frames, no viewports, no vsync, 480x362, 640x480,
  960x760) and prints each picture's statistics to the step summary, so the artifact names the variable.

App: --screenshot-emu-frames M (dev): the capture also waits until the core has shown M frames, with a line
and a capture anyway 600 UI frames later if it never does; gl_qualify.py --emu-frames passes it, every leg
uses 60. VideoOut::emuFrames() exposes the counter.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): even indentation in the x64 shaders_gl allowance (editorconfig-checker)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): macOS captures after 4 UI frames, 240 min budget; 480x360 white = ImGui viewports

Run 41760278887 (6afaae1a): Linux, Windows x64 (loop and shaders_gl under #1167) and Windows arm64
green. macOS: the capture diagnostics named the white picture's variable. With ImGui's extra OS
windows on (ui.viewports, the default) a 480x360 or 480x362 window captures uniform white; with them
off, or at 640x480 and up, it captures the game. The 640x480 leg then built every chain it reached but
ran out of time: 172 built, 57 timeouts at 240 s (49 of them had built the chain; Apple's software
renderer spends ~12 s a frame on the heavy ones), fail-fast after 250 presets, and the job hit its
150 min limit right after the upload.

- The macOS loop captures after 4 UI frames instead of 20; --emu-frames 60 already makes the capture
  the game's picture. The other legs keep 20.
- macOS timeout-minutes 240 (build ~4 min, shaders_gl 10-18 min), shaders_gl alarm 1800 s.
- The diagnostics step keeps four launches (default, 480x360 with and without viewports, 640x480).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): shard the macOS leg 3 ways, track presets over 120 s as macos-arm64 slow

Apple's software OpenGL 4.1 renderer on hosted Macs cannot render ~500 presets in one job
(runs 41760278887 and 41776934638 each ran out of time).  The macOS job becomes a 3-shard
matrix (gl_qualify.py --shard I/N: sorted preset index modulo N, every preset once, each
shard with its own `off` baseline, 2 launches at once), the GL probe, capture diagnostics and
shaders_gl in shard 0 only, and a macos-summary job joins the shards with --merge, failing
when a shard is missing or the shards did not run every preset exactly once.  Only the
joined table goes to the run summary.

known-failures.txt gains a `macos-arm64 slow <measured>@<run> <preset>` kind: 68 presets slow
in every run that measured them.  A listed preset is stopped at --slow-threshold (120 s) and
counted as a known failure, not a checked picture; an unlisted one over the limit fails and
the summary prints its line to paste; a listed one that renders in time is flagged for
removal.  A launch with no [gl] line is retried once and never counts as slow; POSIX kills
send SIGTERM first (--graceful-kill) and report a surviving process group.  Every run writes
times.tsv and a "Slowest launches" table.  macOS captures after SHOT_FRAME UI frames again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): no exit allowance for app launches (#1172); test programs under #1179

#1172 ends the app's bundled-Mesa child without unloading Mesa, so the `windows-x64 exit-hang
#1167` entry and every app-launch allowance go: the automatic-fallback boot step and the preset
loop (--known-exit-hang) fail again if an app launch does not exit on its own.

The two test programs, vj_gl_context_integration and `vj_shader_tests gl`, load the bundled Mesa
in-process and still hang in its DllMain after SDL_Quit; their allowances stay, unchanged in what
they require (the probe's `PASS GL loader` line; shaders_gl with no FAIL/SKIP and its last check
run), now keyed to `windows-x64 test-exit-hang #1179`.

The macOS summary states plainly how many presets are listed `macos-arm64 slow` and therefore
not rendered, with their measured times (finished median and range, timeouts counted apart) and
how many this run stopped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): macOS slow list from the first sharded run; ntsc-blastem refused on GL 4.1

Run 41891214865 (3 shards, all 500 presets, every one run once): Linux, Windows x64 and
Windows arm64 green; macOS 384 built, 57 listed presets stopped at 120 s, 36 failures.

- 31 presets took over 120 s with the chain built (12 timed out at 240 s): listed as
  `macos-arm64 slow` with their measured times.  7 listed presets that rendered in under 100 s
  are dropped; 4 that took 102-114 s stay listed (a listed preset that renders in time is still
  checked).  92 presets are listed.
- ntsc/ntsc-blastem fails to link on Apple's GL 4.1 ("Input of fragment shader
  'LIBRA_VARYING_2' differs in type/qualifiers"); it builds on GL 4.6: refused-macos.txt.
- 4 consecutive shard-0 launches (and their retries) never got a window for ~15 min, then the
  runner recovered, as in run 41760278887: on macOS a launch that times out with no [gl] line
  now leaves a `sample` stack of where the app was stuck.

The summary sentence now says a listed preset stopped at the limit is not rendered or checked,
and counts how many this run stopped and how many rendered in time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): fail a slow macOS preset only if it never renders; reset Apple's GL compile service; no Windows exit allowances

Gate change (option b): an unlisted preset now FAILS only if it does not render within the
240 s timeout.  One that renders but takes over SLOW_LIMIT (120 s) is a ::notice:: with the
line to paste into known-failures.txt and a count in the summary ("Unlisted, rendered but over
120 s").  Hosted-Mac times swing ~2x between runs (sabr-hybrid-deposterize: 131 s, 91 s,
229 s), so the hard 120 s line failed a few new presets every run.  Listed presets keep the
120 s cap; the summary sentence states both rules.

Run 41957577821 (fd76515e): Linux, Windows x64, Windows arm64 green; macOS 18 failures:
- 10 rendered in 121-205 s: notices under the new gate, not listed.
- 2 built and timed out at 240 s: listed (vectorscale-exact-energy-single-aa, xbr-lv2-sharp).
  94 presets are listed; fd76515e's three lines (all rendered within 240 s) are not.
- 6 never got a window, again in the vectorscale/xbr stretch (third run).  The new stack
  samples all wait in CGLChoosePixelFormat -> cvms_connection_create on an XPC reply from
  CVMServer (com.apple.cvmsServ, Apple's GL compile service) that never came.  --reset-cvms
  (macOS leg) kills CVMCompiler/CVMServer with sudo -n before the no-[gl] retry; launchd starts
  a fresh one; the summary counts the resets and how many retries then got a window.

#1181 fixed #1179 (the GL test programs now exit without unloading the bundled Mesa): the
`windows-x64 test-exit-hang #1179` entry, the Known failures step and both test-program
allowances are gone, so any Windows process that does not exit on its own fails the leg.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): list the 2 macOS presets that timed out with a chain on run 42110580090

crt-yah/presets/pure-lcd (204.9 s on 41957577821, over 240 s now) and vectorscale (no window on
41957577821, the CVMServer stall; chain built but no picture in 240 s now) are added as
macos-arm64 slow, 96 listed.  The 15 unlisted presets that rendered in 121-205 s on that run are
notices under the current rule and are not listed.

Also corrects the SMOKE_PRESETS comment (Kimi review): PR runs render all presets; only
workflow_dispatch's preset_set=smoke selects the subset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-05 20:04:02 -05:00
Joseph MattielloandClaude Fable 5.1 8e7fc92296 ci(release): Authenticode for the Windows core DLLs, cue2cdi and the jagcd tools (#1150)
The same .github/actions/windows-sign composite action app-package.yml uses, dropped into
the two remaining Windows jobs, both already on windows-latest (x64):

- release.yml `build`, Windows rows: after strip and before the upload, dist/ is signed
  (virtualjaguar_libretro-windows-{x86_64,i686}.dll, cue2cdi-windows-x86_64.exe; the
  -debug.tar.gz is not a PE file).  The job grants id-token: write; a Windows sidecar
  records the outcome like the macOS rows do.
- chdman-tools.yml: dist-jagcd/ is signed before the zip (chdman.exe, jagcd-chd-check.exe);
  the three AZURE_* secrets join the workflow_call list (release.yml inherits), the job
  grants id-token: write, and a Windows sidecar is uploaded next to the macOS one.
- Gate: release.yml now passes WINDOWS_EXPECT=signed under the same six-value condition as
  WINDOWS_APP_EXPECT, so every Windows file is expected signed once the Azure values exist.
- Docs: README table rows for cores/tools, quota figures (~40 per app-package run, ~45 per
  release run), release-process bullets; the composite's `signed` output description.

After this the only remaining step for #1150 is the maintainer's Azure enrollment.

Refs #1150

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-04 00:00:55 -05:00
Joseph MattielloandClaude Fable 5.1 d7482ba3ec ci(app): Azure Artifact Signing for every Windows binary; retire the SignPath stub (#1150)
Every exe and dll in the Windows app packages (virtualjaguar.exe, librashader.dll,
the software-opengl/ copy and the bundled Mesa DLLs, x64 and arm64) and both Inno
Setup installers are Authenticode-signed with Azure Artifact Signing: SHA-256,
RFC 3161 timestamp from timestamp.acs.microsoft.com, OpenID Connect login
(azure/login), no certificate material or client secret anywhere.

- .github/actions/windows-sign: composite action (azure/login v3.1.0 and
  azure/artifact-signing-action v2.0.0, both pinned by commit) that decides from
  its inputs whether it can sign, prints a ::notice:: and does nothing otherwise,
  signs, then re-reads every file with Get-AuthenticodeSignature and fails when
  one is not Valid and timestamped.
- app-package.yml: the Windows leg becomes build (native runner per arch, install
  tree -> intermediate windows-stage-<arch>) -> windows-package (ONE x64 job for
  both architectures: the signing action does not run on Windows ARM runners,
  its dlib is x64/x86 only) -> windows-smoke (native runner per arch, Authenticode
  re-checked on the DOWNLOADED artifact, portable zip, silent install/uninstall).
  SignPath stub (inputs, secrets, env, steps) removed; secrets are now
  AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID plus the variables
  AZURE_SIGNING_ENDPOINT / AZURE_SIGNING_ACCOUNT / AZURE_SIGNING_PROFILE.
- app.yml and release.yml grant id-token: write to the package job; app.yml
  passes the three secrets so same-repo PR packages are signed like a release.
- Release gate: a second expectation, WINDOWS_APP_EXPECT, for the app zips and
  installers; release.yml passes `signed` when all six Azure values exist. The
  core DLLs, cue2cdi and the jagcd tools keep EXPECT_WINDOWS (unsigned) until
  their jobs sign too. The zip check now covers every PE file inside, summarised
  in one cell. stage.sh skips the windows-stage-* intermediates.
- app/packaging/windows/verify-signatures.ps1: the shared check (files, folders,
  zips; -Expect signed|unsigned|report; table in the job summary).
- package.sh: the local sign hook signed the x64 installer name for both arches.
- Docs: app/packaging/README.md (secrets/variables, layout, Mesa decision, quota,
  the one-time Azure setup incl. roles and federated-credential subjects),
  docs/release-process.md.
- release-packages.cjs: the three new jobs join the completeness list (15).

Refs #1150

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-03 19:48:49 -05:00
Joseph MattielloandClaude Fable 5.1 1773c623dd fix(release): skip per-artifact .info copies; never publish a dry run; bound the gate
- stage.sh ignores the virtualjaguar_libretro.info every build artifact
  carries (the Windows ones differ by CRLF) and writes the shipped .info
  from the checkout, as before.
- The release and nightly jobs refuse to publish when the run is a dry_run
  dispatch, whatever the branch (Codex P1).
- verify-signatures and stage get timeout-minutes: 30, so a hung check cannot
  hold the develop nightly, which waits for the gate to end but ignores its
  verdict (Codex P2).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-03 00:27:35 -05:00
Joseph MattielloandClaude Fable 5.1 4d217a3d45 fix(release): stage.sh accepts byte-identical duplicate assets
Every build job uploads its dist/ tree, and dist/info/virtualjaguar_libretro.info
is in the repository, so each artifact carries the same .info file.  The
duplicate-name guard tripped on that in the first dry run; identical copies
are now skipped and only differing files under one asset name fail.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-02 23:53:30 -05:00
Joseph MattielloandClaude Fable 5.1 313a18d70b ci(release): attest, sign and gate every release asset (#1151, #1152)
One `stage` job in release.yml now flattens the artifacts, writes
SHA256SUMS.txt, signs it with the project minisign key
(SHA256SUMS.txt.minisig; public key docs/release-signing.pub once the
maintainer generates it) and attests build provenance for every file with
actions/attest-build-provenance.  A `verify-signatures` gate on a macOS
runner then checks each asset against its expected signing state
(scripts/release/verify.sh: macOS Developer ID + notarized, Windows
`unsigned` until #1150 flips one line, AppImage and checksum signatures once
their public keys are committed, attestation on everything), strict on tags
and release/**, warn-only on develop so an outage cannot withhold the core
nightly.  Its per-artifact table (signed, notarized, attested) replaces the
signing prose in the release body and the job summary.  The macOS and
AppImage signing steps write signing-status sidecars for the facts the gate
cannot re-derive from a file (Apple's ticket on a bare Mach-O).

AppImages: make-appimage.sh passes --sign --sign-key to appimagetool when
VJ_APPIMAGE_SIGN_KEY is set (app-package.yml imports APPIMAGE_GPG_PRIVATE_KEY
into a throwaway GNUPGHOME; the passphrase goes through
APPIMAGETOOL_SIGN_PASSPHRASE).  appimagetool 1.9 has no verifier, so
app/packaging/linux/verify-appimage.py re-hashes the image with the
signature sections zeroed and runs gpg --verify.

Installers: tools/install.sh and install.ps1 verify SHA256SUMS.txt's
minisign signature (pinned key) or, with gh, its attestation before
trusting it; a bad one aborts, a release that predates signing warns;
--no-verify / -NoVerify skips the step.  Both hermetic suites cover it with
fake minisign/gh.  New release-scripts.yml tests the stage/sign/verify
scripts (Linux also signs a real AppImage).  A `dry_run` dispatch input on
release.yml runs everything up to the gate on any branch and publishes
nothing.

Docs: key generation and secret commands in app/packaging/README.md
(SignPath "dormant" text removed), release-process.md, SECURITY.md, the
site's Download page (public key once committed), the nightly tracker text,
and the stale "signing service" known-issue line in the v3.11.0 notes.

Closes #1151
Closes #1152

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-02 21:36:57 -05:00