Files
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

15 KiB

GitHub, releases & site

Detail for AGENTS.md.

Every PR must be linked to an issue — Closes #N will NOT do it

Hard rule: link the PR to its issue in the GitHub Development panel (right sidebar → gear → pick the issue). A closing keyword in the body is not a substitute and does not work here.

GitHub creates the linked-issue relationship from a closing keyword only when the PR targets the repo's default branch. This repo's default is master and essentially every PR targets develop, so Closes #N here creates no link at all — not merely "no auto-close".

Measured 2026-08-20: PRs #537/#539/#542/#544/#545/#547 all carried Closes #N; every one reported closingIssuesReferences: []. Only #546, linked by hand in the UI, had a real link. It went unnoticed because timeline cross-references still appear — the issue looks connected while its Development panel stays empty, so the issue never shows which release/pre-release tag contains the work (that tag is the whole reason to link).

  • Check: gh pr view N --json closingIssuesReferences --jq '.closingIssuesReferences | length' — 0 = unlinked.

  • There IS a public API — addCloseIssueReferences ("Adds one or more pull requests as manually linked closing references on an issue"), idempotent. An earlier revision of this file said "Web UI only; don't burn time on a GraphQL mutation", which was wrong and cost every new PR a red check for a step that cannot be done until after the PR exists.

    scripts/pr-link-issue.sh <pr-number> [issue-number] [--repo owner/name]
    

    With no issue number it scans the PR body for a tag. Safe to re-run: already-linked is a no-op.

  • Normal flow: tag the PR body, CI links it. .github/workflows/pr-issue-link.yml runs on opened/edited/reopened/synchronize/labeled/unlabeled/ready_for_review, finds the tag, and creates the link. Accepted tags (case-insensitive):

    Closes #123    Fixes #123    Resolves #123    Refs #123
    <!-- link-issue: #123 -->     (invisible in the rendered body)
    

    The hidden comment form wins over the keyword forms, so a PR can discuss one issue in prose and link a different one deliberately. The keywords still do nothing on GitHub's own side here — they are markers for this job.

  • Forgot the tag at creation? Edit the description. edited re-runs the job and it links then; no need to push a commit. The job checks out the BASE ref, never the PR head, because it runs pull_request_target with a write token.

  • Genuinely issue-less PR → add the no-issue label.

  • Still close the issue by hand when the PR merges (keywords don't fire on develop).

GitHub Copilot PR reviews

  • List unresolved threads: gh api graphql -f query='{repository(owner:"libretro",name:"virtualjaguar-libretro"){pullRequest(number:N){reviewThreads(first:30){nodes{id isResolved comments(first:1){nodes{id author{login} body}}}}}}}'
  • Inline reply: gh api -X POST repos/libretro/virtualjaguar-libretro/pulls/N/comments/<REST_ID>/replies -f body="..." — parent is the REST id from gh api .../comments, NOT the GraphQL PRRC_* id (404s).
  • Resolve thread: gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "PRRT_..."}) { thread { isResolved } } }'.
  • Always reply AND resolve when addressing feedback — an open thread after a fix is noise.
  • Do NOT trigger Copilot reviews. gh pr comment N --body "@copilot review" bills per token since 2026-06-01 and spawns a coding-agent session; kimi-review.yml already reviews every PR to develop/master automatically. Use only when a second opinion is worth the spend, and say why in the PR.
  • Repo-level Copilot instructions: .github/copilot-instructions.md (input on every request — keep short). Depth goes in .github/prompts/*.prompt.md (loads only when invoked).
  • Review bots CodeRabbit/Copilot/Cursor all need a libretro-org App install we cannot do; every PR push spends Kimi quota; @kimi review needs the workflow on master.

Branching (GitFlow)

Branch new work off develop (integration branch); master is release-only (tagged commits, hotfix/release merges). PRs targeting master get auto-warned by .github/workflows/warn-pr-base.yml — retarget to develop unless source is hotfix/* or release/*. Full flow in docs/release-process.md. Branch off libretro/develop, not local develop (local can be stale).

Release process (GitFlow)

Full detail in docs/release-process.md. Quick reference:

Nightlies: a push to develop waits 10 minutes (debounce in release.yml's guard job; a newer push cancels the sleeping run, so a burst of merges builds once), then runs the 16-platform matrix and replaces the rolling nightly prerelease + its pinned tracking issue. Gated on compiling, not on the test suite — don't call nightlies "CI-verified". The nightly tag sits outside the v* filter so it can't trigger a real release.

Cutting a release:

  1. git checkout develop && git checkout -b release/vX.Y.Z
  2. Bump version (all must match): Makefile → CORE_BASE_VERSION := vX.Y.Z; dist/info/virtualjaguar_libretro.info → display_version = "vX.Y.Z"; src/core/version_fallback.h → #define CORE_BASE_VERSION "vX.Y.Z" (used when the generated src/core/version.h is absent). bash scripts/check-info-version.sh checks all three (#734).
  3. Release notes: docs/RELEASE_NOTES_vX.Y.Z.md (template: docs/RELEASE_NOTES_v2.3.0.md) — highlights, bug fixes, perf, testing, known issues, stats (git diff --shortstat vPREV..HEAD), downloads, maintainers.
  4. Verify: make clean && make -j… clean, make test passes, strings *.dylib | grep vX.Y.Z. Also regenerate both boot matrices on the corpus machine and gate the tag on python3 -I test/tools/matrix_diff.py <prev-tag copy> <regenerated> (exit 1 = backward row = blocked; not runnable in CI) — docs/release-process.md §1.
  5. Commit: chore: bump version to vX.Y.Z, add release notes.
  6. Push + PR: git push -u libretro release/vX.Y.Z then gh pr create --base master.
  7. After merge to master: tag vX.Y.Z and push — release.yml builds 16 platforms + publishes.
  8. Back-merge: git checkout develop && git merge master && git push libretro develop.
  9. libretro-super: PR updating dist/info/virtualjaguar_libretro.info there.

Don't: tag before the PR merges to master; put new features on a release branch (bug fixes only); forget the back-merge (step 8) or develop diverges from the tagged version string.

Signing and provenance (#1151/#1152): after the builds, release.yml's stage job writes and minisign-signs SHA256SUMS.txt and attests every file; verify-signatures (macOS runner, scripts/release/verify.sh) then checks each asset against its expected signing state, strict on tags and release/**, warn-only on develop, and the per-artifact table goes into the release body. Windows is expected unsigned until #1150 flips that one line. To prove a change to this pipeline, dispatch release.yml on the branch with Dry run ticked (builds everything, publishes nothing). Key setup: app/packaging/README.md, "Signed checksums" / "Signed AppImages".

Savestate version policy: one bump per release (all in-flight changes share the version); develop breakage OK.

The website is NOT published by this repo's pages.yml

Live site: https://jaguar.provenance-emu.com/, built from site/pages/*.html + scripts/build_site.py on develop.

A green Deploy Pages run here is NOT evidence the site updated. The libretro org's Pages config redirects every project site to www.libretro.com/<repo>/, which 404s — so this repo's pages.yml deploys, reports success, and publishes to a dead path.

Real publisher: Publish site (workflow id 329140069) on the Provenance-Emu/virtualjaguar-libretro fork, branch provenance. Checks out libretro/…@develop (fetch-depth: 0 — generator derives sitemap lastmod from per-file git dates, refuses a shallow clone), builds, deploys to the fork's Pages with the verified domain. Trigger: cron 17 * * * * (hourly) + workflow_dispatch. A change merged to develop goes live up to an hour later. Publish immediately:

gh workflow run 329140069 --repo Provenance-Emu/virtualjaguar-libretro --ref provenance

Verify against the live domain, not workflow status:

curl -sI https://jaguar.provenance-emu.com/ | grep -i last-modified

That timestamp tracks the fork's last publish; if it predates your develop commit the site is stale. Responses carry cache-control: max-age=600 — polling past ~10 min with unchanged last-modified means content is old, not cached. Local preview: python3 scripts/build_site.py → _site/; scripts/check_site.py asserts canonical/og:url/sitemap/robots agree with SITE_BASE (override VJ_SITE_BASE).

Roadmap

Planning lives in GitHub milestones (pinned issue #371), never in md files.

What runs on a push to develop

Only release.yml (the nightly), plus path-filtered publishers (pages.yml, jaguar-toolchain-image.yml, crowdin_source_upload.yml). C/C++ CI, App, Regression, Acid, Installer, JaguarDemos, Microbench, Jaguar toolchain and Cheat import run on PRs (into develop, master, or release/**) and on pushes to master / tags; they no longer re-run on the squash-merge commit because the PR already ran them. Consequence: two PRs that are each green but conflict semantically are caught by the next PR's CI (or by the nightly build, for compile errors), not by a post-merge run. To test a develop tip by hand, dispatch c-cpp.yml / app.yml / acid-test.yml / regression-test.yml / jaguar-demos.yml / microbench.yml / jaguar-toolchain.yml / cheat-import.yml with the branch set to develop. Cache note: caches written on a PR ref are not shared with other PRs; PRs restore from develop and master, so after this change the Tier-1 ROM / vasm / miniretro caches are only refreshed by master pushes or such a dispatch.

Mac Studio self-hosted runner

A repo-level runner labelled self-hosted, studio, vj (macOS arm64, Xcode, Homebrew, non-admin user) takes the heavy macOS jobs: macOS rows of c-cpp.yml (incl. SwiftPM build), app.yml, regression-test.yml, and the unsigned iOS/tvOS rows of the develop nightly in release.yml (not release/**, not tags). It is used only when github.event_name is push, schedule or workflow_dispatch, or a pull_request whose head.repo.full_name == github.repository (same-repo PR). Fork PRs always use macos-latest, and no pull_request_target workflow may ever run there. Not routed (stay hosted): jobs that sign or notarize (release.yml macOS rows, app-package.yml, chdman-tools.yml; app.yml skips its sign step on the Studio via runner.environment), anything on a v* tag or release/** (whose compile-only cores C/C++ CI reuses), installer.yml, librashader.yml, Intel/Rosetta runners. Kill switch: set the repository variable VJ_STUDIO_RUNNER to off and all of it falls back to macos-latest with no code change. Runner studio-virtualjaguar (labels self-hosted,macOS,ARM64,studio,vj) is registered; the repo requires approval for all external contributors' workflows. Studio prerequisites: Xcode, cmake, ninja, imagemagick, swift. If the runner is offline, macOS jobs queue until it returns (or set the kill switch). It coexists with VJ_TEMP_RUNNERS, which only remaps lightweight ubuntu jobs.

Temporary CI runners

VJ_TEMP_RUNNERS is an optional repository variable containing a JSON object with a refs allowlist and mappings from hosted labels to private runner labels. Only listed Git refs use those mappings in opted-in jobs. The C/C++ Linux x86-64 rows and Linux test workflows also honor the ubuntu-latest mapping for trusted events; fork PRs and pull_request_target keep GitHub-hosted runners. A matching runner must actually provide Ubuntu x86-64, including Docker for SDK containers; the macOS/ARM Studio runner cannot serve these jobs directly. For example, temporarily map refs/pull/1096/merge to a uniquely labelled Linux VM. Keep the VM isolated from host folders and remove its registration and mapping after validation. Already-created jobs retain their runner labels; the mapping applies when GitHub creates a new job or reruns the workflow.

Absent refs or labels keep their original GitHub-hosted runners. Leave the variable unset normally. A temporary release runner must use a unique custom label with default labels disabled, preserve the requested OS/architecture, and be removed together with the variable after its checks finish. Do not map Ubuntu 22.04 packaging jobs onto Ubuntu 24.04: that would change the supported glibc floor.

Shared production core builds

For same-repository release/** PRs with a merge tree identical to the release head, C/C++ CI shares ten compile-only production builds from the exact-head Release push run: macOS Intel, WASM, four Android ABIs, iOS, tvOS, Vita and Switch. Other PRs, forks, manual runs and integration-branch pushes keep the full compiler matrix. The configurations live in .github/ci/core-matrix.json; a Ruby workflow-contract check rejects mismatched compiler/SDK/architecture settings before any rows are omitted.

The seven native C/C++ matrix rows, GCC/Clang differences, MSVC checks, coverage, ASan/UBSan, clang-tidy and production -> TEST_EXPORTS=1 -> production mode-switch coverage stay independent. Compile-only fallback builds use the same RELEASE_DEBUG_INFO=1 configuration as Release (adds debug information to the optimized production build). Android's compile-definition and selected-SIMD-object assertions run through scripts/build-ndk-ci.sh in both workflows. Compile-only Mach-O production exports are checked before Release uploads a shared binary.

Release retains an unstripped ci-core-<platform> intermediate before packaging/signing. BUILD.json records the exact commit, platform, filename, production ABI setting and SHA-256. C/C++ CI requires successful production jobs, both raw and distributable artifact groups, and verifies the downloaded metadata/checksums before retaining all ten original C/C++ artifact names. Failed, cancelled, skipped, stale, missing or mismatched evidence cannot make the shared check green. The hosted waiting job does not occupy the temporary runner needed by Release metadata jobs.

These intermediates have 14-day retention and are explicitly excluded by both release publishers' staging filters. Shipped binaries, split debug archives, their names and checksum collection remain unchanged. The C/C++ artifact aliases retain their normal artifact retention. The shared core verifier and shared app-package verifier must both pass before merging a release PR that uses these routes; a configured skip of duplicate build rows is not sufficient.