# GitHub, releases & site Detail for [`AGENTS.md`](../../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. ```bash scripts/pr-link-issue.sh [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 (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//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 ` (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//`, 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: ```bash gh workflow run 329140069 --repo Provenance-Emu/virtualjaguar-libretro --ref provenance ``` Verify against the live domain, not workflow status: ```bash 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-` 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.