Files
e7009c440a ci: trim develop-push runs, debounce nightly, route macOS to Studio runner (#1074)
* ci: stop re-running PR-covered workflows on develop pushes; debounce nightly; route macOS to Studio runner

Drop develop from push: in c-cpp, app, regression-test, acid-test, installer,
jaguar-demos, microbench, jaguar-toolchain and cheat-import (PRs already ran
them); ignore develop in pull-request.yml. release.yml's nightly waits 10 min
in its guard job so a merge burst builds once. macOS rows of c-cpp/app/
regression and the unsigned iOS/tvOS nightly rows use the self-hosted Studio
runner on trusted events only, with a VJ_STUDIO_RUNNER=off kill switch.

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

* ci(spm): find blitter_simd objects independent of SwiftPM build layout

Xcode 26 SwiftPM keeps objects under .build/out/Intermediates.noindex/...
instead of <triple>/debug/virtualjaguar.build, which broke the vtable check on
the Studio runner. Search by object name and fail loudly if one is missing.

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

---------

Co-authored-by: dry <dry@x>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
2026-10-22 23:24:46 -04:00

18 KiB
Raw Permalink Blame History

Release process

How to ship a new tagged release of the libretro core.

Branching model (GitFlow)

This repo uses a GitFlow-style layout:

  • master — release-only. Tagged commits, hotfix merges, release-branch merges. Default branch on GitHub for visibility / clone defaults.
  • develop — integration branch. All feature work flows in here first.
  • feature/* — branched from develop, merged back via PR.
  • release/X.Y.Z — branched from develop when cutting a release; bug-fix-only until tagged on master, then back-merged to develop.
  • hotfix/X.Y.Z — branched from master, fixed, tagged on master, back-merged to develop.

PRs targeting master directly trigger a friendly comment from .github/workflows/warn-pr-base.yml asking the contributor to retarget to develop (skipped for release/* and hotfix/* source branches).

TL;DR

  1. Cut release/X.Y.Z from develop. Bump the version in all three places -- CORE_BASE_VERSION in Makefile, display_version in dist/info/virtualjaguar_libretro.info, and CORE_BASE_VERSION in src/core/version_fallback.h (or use the Bump Version & Release workflow); scripts/check-info-version.sh fails CI if they drift. Update docs/RELEASE_NOTES_vX.Y.Z.md. Open a PR from release/X.Y.Z → master.
  2. Merge into master.
  3. git tag vX.Y.Z && git push libretro vX.Y.Z (or via GitHub UI).
  4. Watch Actions — release.yml builds 16 platforms (14 in the build matrix, plus Vita and Switch in their own jobs), generates SHA256SUMS.txt, and publishes the release with docs/RELEASE_NOTES_vX.Y.Z.md as the body.
  5. Back-merge master → develop so the version bump and any release-branch fixes are in develop: git checkout develop && git merge master && git push libretro develop.
  6. After the tag publishes, send a small PR to libretro/libretro-super updating dist/info/virtualjaguar_libretro.info to match dist/info/virtualjaguar_libretro.info from this repo at the tag.

Detail

1. Pre-tag checklist

  • make (default) builds clean, make test is green, bash scripts/c89-lint.sh passes.

  • The audio pair is run by hand against the private ROMs. make test runs test_audio_clipping / test_audio_presence only if the ROMs are present and prints SKIP otherwise — and still exits 0, so on a machine without them a silencing regression passes unnoticed. Iron Soldier 1 presence should sit near RMS 1175 (envelope 200–25000); Skyhammer and Iron Soldier 2 must pass clipping cleanly (the old expected-failure guidance predates the MMULT fix); retain the separate presence check so silence cannot pass as clean audio.

  • The acid gate is python3 test/acid/scripts/check-baseline.py <results.log> test/acid/BASELINE.txt reporting Regressions: 0. make -C test/acid test exits non-zero by design (it returns the FAIL count), so its exit status is not the gate.

  • Boot matrices: regenerate, diff, commit (#749). The committed docs/cart-boot-matrix.md and docs/cd-boot-matrix.md are the release compatibility statement, and the never-backward rule (no row may move backward through the stage taxonomy or gain a crash-watchdog signature) is enforced here, by hand, on the corpus machine. It cannot run in GitHub CI: the private ROM and disc corpus is not available there. (The cart matrix went 492 commits stale across five releases before this step existed.) From a checkout of the release branch with test/roms/private populated:

    DEVELOPER_DIR=/Library/Developer/CommandLineTools make -j"$(getconf _NPROCESSORS_ONLN)" TEST_EXPORTS=1
    DEVELOPER_DIR=/Library/Developer/CommandLineTools cc -O2 -Wall -std=c99 -I. -I./src \
        -I./libretro-common/include -o test/tools/cart_boot_probe \
        test/tools/cart_boot_probe.c test/harness/harness.c -ldl -lm
    bash test/tools/cart_boot_matrix.sh     # -> docs/cart-boot-matrix.md (CART_MATRIX_OUT)
    bash test/tools/cd_boot_matrix.sh       # -> docs/cd-boot-matrix.md   (CD_MATRIX_OUT)
    

    Both sweeps are long (the CD one is chunkable with CD_MATRIX_MAX_RUNS; the cart one with CART_MATRIX_MAX_RUNS) and resume: only rows stamped by the same build are skipped, so a stale row can never pass as fresh. Do not set CART_MATRIX_PROBE_ARGS for the committed matrix: the script refuses it without an explicit CART_MATRIX_OUT, and the committed baseline is the default (Fast-blitter) sweep. A shipped-default sweep (CART_MATRIX_PROBE_ARGS="--option virtualjaguar_usefastblitter=disabled" plus its own CART_MATRIX_OUT=/some/other.md) catches accurate-only hangs (#800), but there is no committed baseline for it yet, so read that output by hand until one exists.

    Then diff each regenerated file against the previous tag's copy:

    prev=vX.Y.Z-1   # the previous release tag
    d="$(mktemp -d)"
    git show "$prev:docs/cart-boot-matrix.md" > "$d/cart.md"
    git show "$prev:docs/cd-boot-matrix.md"   > "$d/cd.md"
    python3 -I test/tools/matrix_diff.py "$d/cart.md" docs/cart-boot-matrix.md
    python3 -I test/tools/matrix_diff.py "$d/cd.md"   docs/cd-boot-matrix.md
    

    Exit 0 = nothing moved backward, 1 = at least one row did, 2 = a file did not parse (or the two files are different kinds). Add --json for a machine-readable report.

    • A backward row blocks the tag until it has a ticket and an explicit deferral (a comment on the release PR naming the row, the ticket and who agreed to ship with it). A new crash-watchdog signature on an unchanged stage counts as backward. Do not "fix" the diff by editing the table or the tool's rankings; fix the emulator or defer the row.
    • Read the still-asymmetric list too (cart: HLE stage != BIOS stage; CD: the hle and bios rows differ). It never blocks, but a row that is new there deserves a ticket.
    • The improved list is release-notes material for "Game compatibility" in docs/RELEASE_NOTES_vX.Y.Z.md. Also account for any added / removed rows.
    • Commit the regenerated docs on the release branch (docs: regenerate cart + CD matrices for vX.Y.Z) so they ship with the tag.
  • CI on the release PR is green except claude-review (non-blocking; AI review service refuses diffs > 20k lines).

  • docs/WHATSNEW vX.Y.Z section is up-to-date.

  • docs/RELEASE_NOTES_vX.Y.Z.md exists. (See below for how to generate one.)

  • src/core/version_fallback.h CORE_BASE_VERSION matches the new tag (scripts/check-info-version.sh checks all three copies).

  • dist/info/virtualjaguar_libretro.info is up-to-date — display_version matches the new tag, any new feature flags are reflected (savestate, cheats, disk_control, hw_render, etc.).

2. Tag

git tag -a v2.2.0 -m "v2.2.0"
git push libretro v2.2.0

The push triggers .github/workflows/release.yml which runs only on v* tags.

3. What release.yml does on tag push

For each of the 14 matrix platforms (Linux x86_64/aarch64/i686, macOS arm64/x86_64, Windows x86_64/i686, Emscripten WASM, Android arm64-v8a/armeabi-v7a/x86_64/x86, iOS arm64, tvOS arm64), in parallel — PS Vita and Switch build in separate jobs, for 16 targets in total:

  1. Build the core with RELEASE_DEBUG_INFO=1 so the optimized binary keeps -g symbols.
  2. Extract split debug info into a <platform>-debug.tar.gz archive (objcopy --only-keep-debug for ELF, dsymutil for Mach-O).
  3. Strip the shipped binary so it's small.
  4. Upload as a CI artifact.

Two extra container-based jobs do PS Vita (vitasdk/vitasdk:latest) and Nintendo Switch (devkitpro/devkita64:latest). A parallel jagcd-tools job (.github/workflows/chdman-tools.yml) builds pinned chdman plus jagcd-to-chd / jagcd-chd-check for linux-x64, macOS, and windows-x64 — the same desktop trio as cue2cdi.

After all platform builds finish, the release job:

  1. Collects every artifact into release/.
  2. Adds dist/info/virtualjaguar_libretro.info to release/.
  3. Generates release/SHA256SUMS.txt (sorted, sha256sum -c compatible).
  4. Reads docs/RELEASE_NOTES_v<TAG>.md if present and uses it as the release body, with the artifact list appended. Falls back to GitHub --generate-notes if no curated file is found.
  5. Creates the GitHub release with all of release/* attached.

3b. Nightly builds (develop pushes)

release.yml also fires on every push to develop, after a 10-minute debounce: the guard job sleeps first and a newer push cancels the sleeping run (concurrency), so a burst of merges produces one nightly, built from the last of them. Tags, release/** and manual dispatches skip the wait. The build matrix is identical — the same 16 jobs (14 matrix entries + Vita + Switch) plus the jagcd-tools zips, the same split-debug packaging — but the publishing step is the nightly job instead of release:

  1. Moves the nightly git tag onto the pushed commit.
  2. Updates the single rolling nightly prerelease in place (gh release edit + gh release upload --clobber, not delete-and-recreate, so /releases/tag/nightly never 404s mid-run and the release keeps its id). After the upload it briefly toggles draft→published so GitHub rewrites published_at — plain edit leaves that stamp on the original create date and the Releases page looks abandoned under newer stable tags.
  3. Rewrites display_version in the bundled .info to v<X.Y.Z>-nightly-<sha>, so a nightly .info can never be mistaken for the file that gets PR'd to libretro-super.
  4. Creates-or-updates a pinned tracking issue with the current build and install instructions. Located by the nightly-tracker label first (server-side, exact, independent of how many issues the repo has) with an exact-title match as fallback — both have to miss before it could duplicate itself.

workflow_dispatch is also wired up, but only to re-publish the nightly — run it with the branch picker set to develop. GitHub defaults that picker to the repo default (master), and both publishing jobs are ref-gated, so a dispatch left on the default would build all 16 targets and publish nothing. The guard job rejects that in seconds instead of burning the matrix. To test-build an arbitrary branch, use c-cpp.yml, which has its own workflow_dispatch and runs on PRs.

The two paths can't collide: the release job is gated on startsWith(github.ref, 'refs/tags/v'), the nightly job on github.ref == 'refs/heads/develop', and the nightly tag deliberately sits outside the v* tag filter so a CI-pushed tag can never re-enter the release path.

What a nightly is gated on: the 16 builds compiling. It is not gated on c-cpp.yml, the acid suite, or the screenshot regression run — those do not run on develop pushes at all (the PR that introduced the commit already ran them). A nightly can build cleanly and still be broken, and the release body says so. Upgrading that claim means switching the job to a workflow_run trigger (see artifacts.yml for the idiom in this repo).

Concurrency: develop pushes here cancel superseded runs, because only the newest commit's binaries matter for a rolling prerelease; the debounce makes that cancel land before any build minutes are spent. Tag pushes always run to completion. If build queue contention ever becomes a problem, the lever is a paths-ignore for docs-only pushes — not a smaller matrix. (Test it carefully: path filters interact badly with tag pushes, and this workflow serves both.)

3c. Standalone app packages (#839)

release.yml's app-packages job calls the reusable .github/workflows/app-package.yml, which builds the desktop app in app/ for macOS, Windows and Linux and uploads each platform's final files as an app-package-<platform> artifact. The release and nightly jobs flatten every artifact into release/, so the packages are checksummed into SHA256SUMS.txt and attached to the same GitHub release as the cores with no extra publishing logic. Nightly assets are replaced in place with --clobber, as before.

Asset Contents
VirtualJaguar-X.Y.Z-macos-universal.zip Virtual Jaguar.app (arm64 + x86_64), signed, notarized and stapled; the only macOS package (no DMG) and the Homebrew cask's download
VirtualJaguar-X.Y.Z-windows-x64.zip portable folder (portable.txt inside)
VirtualJaguar-X.Y.Z-windows-x64-setup.exe Inno Setup installer
VirtualJaguar-X.Y.Z-linux-x86_64.AppImage, ...-linux-aarch64.AppImage AppImages
virtualjaguar_X.Y.Z_amd64.deb, virtualjaguar_X.Y.Z_arm64.deb Debian packages

X.Y.Z is the tag without its v (v3.8.1-rc1 gives 3.8.1-rc1). Nightlies use nightly (VirtualJaguar-nightly-windows-x64.zip) and the release/** lane uses prerelease.

  • Version guard. The app has no version of its own: app/CMakeLists.txt reads the core's CORE_BASE_VERSION from the top-level Makefile, so bumping the core (the usual three places) bumps the app, and every release tag ships app packages with the same number. The job still checks that the tag's version (minus any -suffix) matches and skips with a warning if it does not (a forgotten bump). A commit with no app/ is skipped too.
  • Gating. A tagged release needs the app job to succeed (or be skipped); re-run the failed jobs and the release follows. The nightly waits for it but does not require it, like the jagcd tools, so an Apple notarization outage cannot withhold the core nightly.
  • Signing is optional and secret-driven (macOS Developer ID + notarization, Windows SignPath). Every macOS file we ship is Developer ID signed and notarized: the app, the macOS core dylibs, cue2cdi-macos-universal and the macOS chdman/jagcd tools (composite action .github/actions/macos-sign); same-repo PR app packages are too. Without the secrets (fork PRs) the macOS app is ad-hoc signed, everything else unsigned, and the log warns. A notarization failure fails a tag build and only warns on other refs. The secrets, the hardened-runtime reasoning and the manual steps are in app/packaging/README.md.
  • After the release, a maintainer fills the Homebrew, winget, Scoop and Flatpak templates from the published SHA256SUMS.txt (app/packaging/manifests/render.sh) and submits them. CI never submits to external repositories.

4. Post-tag: update libretro-super

RetroArch ships with a .info file per core, sourced from libretro/libretro-super/dist/info/. Update is manual — there's no automated mirror.

# Fork libretro-super, then:
cd libretro-super
git checkout master && git pull
cp <this-repo>/dist/info/virtualjaguar_libretro.info dist/info/
git checkout -b update-virtualjaguar-vX.Y.Z
git add dist/info/virtualjaguar_libretro.info
git commit -m "virtualjaguar: bump to vX.Y.Z"
git push origin update-virtualjaguar-vX.Y.Z
gh pr create --repo libretro/libretro-super \
  --title "virtualjaguar: bump info to vX.Y.Z" \
  --body  "Mirrors dist/info/virtualjaguar_libretro.info from
libretro/virtualjaguar-libretro at tag vX.Y.Z.  Includes new
savestate/cheats/cheevos flags now that the core supports them."

The libretro-super maintainers (typically @inactive123, @MrHuu, @aliaspider) merge these. The change appears in the next RetroArch update / shipped install.

5. Generating release notes

The docs/RELEASE_NOTES_v<TAG>.md file is written by hand (or with a sub-agent's help) before tagging. Recommended structure is in docs/RELEASE_NOTES_v2.2.0.md — Highlights, What's new (lifted from docs/WHATSNEW), Game compatibility summary, Known issues, "Compared to upstream" stats from git shortlog -sn and git diff --shortstat libretro/master...HEAD, Downloads list, Maintainers.

For future releases (where there's a previous tag to diff against), use git shortlog vPREV..HEAD instead of libretro/master..HEAD.

Hotfix flow

When a critical bug ships in a release and can't wait for the next develop cycle:

git checkout master && git pull
git checkout -b hotfix/X.Y.Z+1
# ... fix, bump CORE_BASE_VERSION patch, update RELEASE_NOTES, commit ...
gh pr create --base master --title "hotfix: <issue>" --body "Fixes #N.  Bumps to vX.Y.Z+1."
# After merge:
git tag vX.Y.Z+1 && git push libretro vX.Y.Z+1
git checkout develop && git pull && git merge master && git push libretro develop

The hotfix/* source branch suppresses the warn-on-master-PR workflow.

6. If a release-job step fails

The workflow runs only on v* tag push, but the release job is re-runnable from the Actions tab without needing a new tag. Common failure modes:

  • A platform build broke (compiler upgrade, NDK version drift): re-run from Actions, or fix and push a new tag vX.Y.Z+1.
  • gh release create 422'd because the tag already has a release: delete the partial release in the GitHub UI and re-run the release job.
  • objcopy not found on a less-tested platform (Vita, Switch): the workflow uses || cp fallbacks so the build succeeds with an unsplit debug archive; the binary itself ships fine.

7. Artifact naming

Each binary follows virtualjaguar_libretro-<platform>.<ext>:

virtualjaguar_libretro-linux-x86_64.so
virtualjaguar_libretro-linux-x86_64-debug.tar.gz
virtualjaguar_libretro-macos-arm64.dylib
virtualjaguar_libretro-macos-arm64-debug.tar.gz   # contains a .dSYM bundle
virtualjaguar_libretro-windows-x86_64.dll
virtualjaguar_libretro-windows-x86_64-debug.tar.gz
virtualjaguar_libretro-android-arm64-v8a.so
virtualjaguar_libretro-emscripten-wasm.bc
virtualjaguar_libretro-ios-arm64.dylib
virtualjaguar_libretro-tvos-arm64.dylib
virtualjaguar_libretro-vita.a
virtualjaguar_libretro-switch.a
SHA256SUMS.txt
virtualjaguar_libretro.info

The .info file in the release is the same one that should land in libretro-super.