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]>
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.ymlruns onopened/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.
editedre-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 runspull_request_targetwith a write token. -
Genuinely issue-less PR → add the
no-issuelabel. -
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 RESTidfromgh api .../comments, NOT the GraphQLPRRC_*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.ymlalready reviews every PR todevelop/masterautomatically. 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 reviewneeds 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:
git checkout develop && git checkout -b release/vX.Y.Z- 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 generatedsrc/core/version.his absent).bash scripts/check-info-version.shchecks all three (#734). - 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. - Verify:
make clean && make -j…clean,make testpasses,strings *.dylib | grep vX.Y.Z. Also regenerate both boot matrices on the corpus machine and gate the tag onpython3 -I test/tools/matrix_diff.py <prev-tag copy> <regenerated>(exit 1 = backward row = blocked; not runnable in CI) —docs/release-process.md§1. - Commit:
chore: bump version to vX.Y.Z, add release notes. - Push + PR:
git push -u libretro release/vX.Y.Zthengh pr create --base master. - After merge to master: tag
vX.Y.Zand push —release.ymlbuilds 16 platforms + publishes. - Back-merge:
git checkout develop && git merge master && git push libretro develop. - libretro-super: PR updating
dist/info/virtualjaguar_libretro.infothere.
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.