WIP: ci: run checks, builds, releases and the website on CircleCI #417
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!417
Loading…
Reference in a new issue
No description provided.
Delete branch "drcrow/Longshot:ci/circleci"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Pull requests get only the title, description and third-party checks today. The game-free checks and the builders run by hand or at release, so a break is found after it merges. This moves CI and the release pipeline to CircleCI, which runs the checks and builders each pull request needs and nothing more.
What changed
tests/run_all.pygains--list(the checks a run would start, running none),--junit FILE, and an opt-in--exact(names must match exactly; nothing matching fails closed) that CircleCI's test splitter uses.--shard,--write-timesand substring names are unchanged. Tests:tests/test_run_all_junit.py..circleci/config.ymlhas one small setup job per event (pull request, push to main,v*tag, schedule, manual API run). Each one looks at which areas changed (.circleci/mapping.conf: tests, gpu, linux, windows, website, third-party, docs) and continues with.circleci/shared.ymlplus that event's file in.circleci/events/. A docs-only PR runs lint, third-party andgate; agpu/change adds the Vulkan builds and SPIR-V fixtures.magicbeandev/ci, used by all three repos (source in a newMagicBeanDev/orbrepo). This repo keeps only its own jobs..circleci/test-suites.yml), split across 4 containers by timing, with JUnit results.release.ymldoes: the release jobs start only after every check and build passes, so a red main ships nothing.v*tags rerun the core checks, then release. Release jobs run main's copy ofscripts/release.py. Signing (release-sign, signing key only) and publishing (release-publish, Forgejo token only, one release at a time) are separate jobs.release_dry_runruns it all with a throwaway key and stops before publishing.docs/ci.mdlists each job and why)..circleci/forks.yml, started by a Forgejo webhook. A maintainer adds theci: approvedlabel (Forgejo only lets people with write access do that), and it runs that exact commit with no secrets, then postsci/circleci: gateon the PR. The label stays on, so new pushes keep running. After 14 quiet days it's removed.mise.tomlandmise.lock(pinned tools and thedeps,lint,test,ci:smoke,validatetasks), pre-commit hooks (commit-msg included), a devcontainer, and a Chunk sidecar config.docs/ci.mdwith flow diagrams; CONTRIBUTING.md's CI lines.Flags and settings
None. No game switch, menu row or option, and no new environment variable. CircleCI pipeline parameters (
release_dry_run,vulkan_dlss,full) are in docs/ci.md.Measurements
main:
f87557ae(2026-11-14)this PR: ci/circleci @ 2c00a9a6
Hardware: devbox, x86_64 Linux, Docker; the jobs' own CircleCI images (no game, no GPU)
Scene: none: CI only
Method:
run_all.py --strictin one Ubuntu 24.04 image with test-linux's packages, main and this PR interleaved (main, PR, main, PR), 2 runs each, the median. These numbers are from 2c00a9a6.run_all.pyhas been reworked since to keep--shardand substring names; a new interleaved measurement against current main is running, and this section will be updated with it.How to reproduce: the same image and
python3 -I -B tests/run_all.py --stricton each checkout;mise run validatefor the CI configf87557aegaterun_all.py --strict(Linux, 24.04 image): passed / failed / skippedrun_all.py --strictwall time (16 at once)Testing
Tested:
circleci config validateon every event's merged config with the orb inlined, and the orb's conftest policies (28 pass).run_all.py --strictas measured above.mise run lintandmise run teston a fresh Chunk sidecar (Ubuntu 22.04).Not tested: a live CircleCI run (it starts once this is on main and the orb is published), the Arm containers, the Wine VM job, publishing (it needs the signing key and bot token in CircleCI), Cloudflare deploys, and the fork flow end to end (it needs the org webhook).
Needs other testers: None
Screenshots
Nothing on screen
Risk and rollback
A wrong path rule could skip a job a change needed. Main pushes run everything, and
full: trueon a manual run forces it all. The Forgejo workflows stay on, so reverting this commit is enough.Reviewer notes
Start with docs/ci.md, then
.circleci/config.ymland.circleci/events/pr.yml.test_pad_layout.pyfails on main too (after #137, its compile of hid.c doesn't findhost/hold_toggle.h); that is for a separate fix. The shared CI logic lives in themagicbeandev/ciorb (MagicBeanDev/orb), and #353's pull request builds can sit on top of this pipeline. Feedback very welcome, especially on the path areas in.circleci/mapping.conf(what should run for what) and on the fork-PR approval flow.Issues
See #283
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
tests/that reads changed source updated in this PR, not weakened.Looked over what runs when, at
6a8e3659. Read from the files; where I ran something it says so. Nothing here was seen in a live CircleCI run.Stops every run as written
1. The fork condition is always true, and it guards the checkout.
.circleci/shared.yml:80-105(get-source) and.circleci/events/fork.yml:4usegreater: [<< pipeline.parameters.fork_pr >>, 0].greateris not one of CircleCI's logic statements, and the compiler does not reject it: an unknown operator counts as true. I compiled a miniature ofget-sourcethrough CircleCI's config compiler: withfork_prat 0 the job gets the "Fork PR checkout" step and nocheckout; at 7 the same. Socircleci config validatepasses, and in every pull request, main and tag pipeline each job that usesget-source(lint, meta, the tests, the builders, the website jobs, the release builds) fetchespull/0/headand fails. Forms that compiled correctly: at step levelnot: {equal: [0, << pipeline.parameters.fork_pr >>]}; at workflow levelwhen: pipeline.parameters.fork_pr > 0(inside a step'scondition:that expression string also counted as true).2.
metacannot find the description template.shared.yml:380-384copies main's checker to/tmp/main-scripts/check_pr_description.pyand unpacks the template under/tmp/main-scripts/.forgejo/. The checker looks one folder above itself (scripts/check_pr_description.py:41), that is in/tmp/.forgejo/.3.
websitechecks a site it has not built.shared.yml:803-804runsnpm ciandnpm run check;website/scripts/check-site.tsstarts by readingdist. Forgejo'swebsite.ymlbuilds first.4. Lint installs from a lock file with no Linux Arm entries. The
smallexecutor isarm.medium.gen2, the install runs withMISE_LOCKED=1(shared.yml:255-261), andmise.locklistslinux-x64andmacos-arm64only. As I read mise's locked mode it refuses a platform the lock does not have; not run.5.
tests/run_all.pyloses--shard, which the Forgejo checks still pass. On this headpython -I -B tests/run_all.py --shard 1/2 --listends witherror: unrecognized arguments: --shard, and.forgejo/workflows/ci.yml:163runstests/run_all.py --strict --shard ...(so does the newscripts/ci/check.py:159-160). The Forgejo source checks would fail from the merge on, while they are meant to keep running alongside.Differs from the description
6. The nightly does not wait for the checks.
.circleci/events/main.ymlhas two workflows:ci(lines 3-43) andnightly-release(45-62). Signing requires the release builds and notes only, so a redcidoes not stop a signed nightly. Each workflow also has its owngate.7. Main does not run everything. In
ci,linux-appimage,windows-builder,windows-vulkanand the website jobs keep their path filters (main.yml:14-30);docs/ci.md:20says "All areas".8. Fork approval.
.circleci/approvers.txthas no login, sogatekeeper.py:220-224refuses every label. The gatekeeper also takes the label as a one-time approval that must come after the latest push and removes it (:225-242), where the team summary and the launcher and site descriptions say the label stays and new pushes keep running. And it reads the first 50 open pull requests only (:94); there are over 170 open.9.
full. The description lists afullparameter;.circleci/config.yml:6-27does not declare it andevents/manual.ymlhas no route for it.10. Paths. A
.circleci/change is "full" but the website jobs filter onwebsitealone (events/pr.yml:29-41). Forgejo'swebsite.ymlalso triggers ondocs/tested-configurations.mdanddocs/tested/**;mapping.conf:33haswebsite/only.mise.lock,ci/linux/and.forgejo/have no rule.Release chain
11. Main's
release.pyis run from.ci/main-release/(shared.yml:223-226), so its root is<checkout>/.ci(scripts/release.py:64). Loaded from there it finds 0 trusted keys and nocliff.toml; fromscripts/it finds 1 key and the file. Theverifyatshared.yml:1120then has no key to trust.12. A nightly's builder version is the word "nightly".
shared.yml:1028-1036turnsnightly-<n>-<hash>intoVERSION=nightlyfordotnet publish -p:Version. Forgejo'sbuild-windows.yml:103-104takes a numeric version or0.0.0.13. Dry-run signing.
shared.yml:1107-1108uses/tmp/dry-sign.pemas the key file, butkeygenmakes a folder of that name holdingrelease.pemandrelease.pub.pem(scripts/release.py:715-717), andverifygets no public key for it.14.
released-as ... || true(shared.yml:914) turns a failed check of an earlier release into "none found".15. The release Windows build restores a compiler cache.
release-windowsuses thellvm-mingwcommand, which restoresv1-llvm-mingw-*after the hash-checked install (shared.yml:196-207,:1017);docs/ci.md:21says release builds restore no caches.16. Two publishers.
.forgejo/workflows/release.ymlstill publishes on main and onv*tags. Which one is the only writer while both run?Smaller
shared.yml:878-886: the comment job, which holds the comment token, falls back to the pull request's copy of four helper scripts when main lacks them (it does today). Failing instead is safer..circleci/config.yml:79and:95-97putfork_sha, the branch and the tag into shell text; checkingfork_shaas 40 hex digits and passing the others through the environment avoids surprises.HEAD..HEAD(shared.yml:322-323).test-linux,arm.large.gen2); checks that skip off x86-64, such astests/test_fpx_sticky.py:189, then do not run anywhere. Intended?events/release.yml:release-versionandgatehave no tag filter while the jobs between them do. CircleCI's rule for tags asks for one on every job in the chain (https://circleci.com/docs/guides/orchestrate/workflows/#executing-workflows-for-a-git-tag).Checked and fine
The three examples hold: a docs change adds nothing to lint, meta and third-party;
gpu/adds the Linux checks, SPIR-V fixtures and Windows Vulkan build; the builder adds its CLI checks and the Windows builder. Withfork_prset, the checkout compares the fetched commit withfork_shabefore using it. Signing and publishing ask for separate contexts; the manual dry run has no publish job.run_all.py --liststill finds every existing check, and the new JUnit test passes.Questions
What turns Forgejo's events into the
pull_request,pushandapievents the setup expects, and what posts a failedci/circleci: gatewhen a required job fails and the gate job never starts? Which CircleCI settings limit the signing, bot and Cloudflare contexts to main and tags, given that a branch in this repository runs its own configuration?Thanks for this. It's the first ask in #394 (CI that gates merges), so I'm keen to see it land. mooki's review covers the config itself; these are the process points around the cutover. They apply to MagicBeanDev/LongshotLauncher#65 and MagicBeanDev/Site#10 too.
1. Make
ci/circleci: gatea required check on main as part of the cutover. Today main's protection requires no status checks in Longshot or LongshotLauncher, and Site's main isn't protected at all, so a red gate wouldn't stop a merge. Until the gate is required it's advice only. It's also what Forgejo's "merge when checks pass" needs if we use that in place of a merge bot: with no required check, it merges straight away.2. Get main green first. Once the gate is required and the nightly waits for it (mooki's point 6), a red main blocks every merge and every nightly.
test_pad_layout.py(failing since #137) needs fixing before the switch. After that, when main goes red, reverting the PR that broke it should be the first move, so nightlies don't stall.3. The fork label becomes a step before every merge. Most contributors work from forks. With the gate required, a fork PR can't merge until a maintainer adds
ci: approved, and in this PR the label is needed again after every push. Withapprovers.txtempty, that's one maintainer's waking hours again, so fork PRs would wait overnight for a label instead of a merge. Two asks before the cutover:approvers.txtspread across timezones.4. Where the release key lives. The signing key moves from a Forgejo secret to a CircleCI context. Keeping it to the sign job is good, but it does mean an outside service holds the key that signs every build the launcher installs. That may well be the right call; I'd just like the team to make it on purpose, and note it in RELEASING.md, rather than have it arrive with the CI move. This goes with mooki's question about how the contexts are kept to main and tags.
6a8e365958b866a018e3Thanks @mooki and @madebyjordan, both reviews were really useful. Since
6a8e3659the branch has been reworked: the shared setup, fork handling, signing and publishing moved into one orb (magicbeandev/ci), and the branch is rebuilt on current main. So some points are fixed by the rework and some by specific changes. Point by point:mooki
greater:is always true: confirmed withcircleci config process. The fork workflow now useswhen: pipeline.parameters.fork_pr > 0, which is dropped at 0 and kept at 7. The oldget-sourcestep condition is gone: fork checkout is a separate orb command, used only in the fork workflow.metatemplate path: gone. PR title and description checks stay on Forgejo, so CircleCI no longer runscheck_pr_description.py.websitechecks an unbuilt site: it now runsnpm run buildbeforenpm run check.linux-arm64entries.--shard: you were right, and it was worse than that: our change also made name selection exact-only, which brokerun_all.py ub_. Redone as purely additive.--shard,--write-timesand substring names are as on main; new are--list,--junit FILE, and an opt-in--exact(fail closed) that only CircleCI's splitter uses.tests/test_run_all_junit.pychecks that--shard 1/2and2/2cover every check exactly once.release-versionrequires lint, third-party, test-linux, builder-cli, spirv-fixtures and the builds, so a red main signs nothing. Av*tag reruns the core checks before it builds anything.force-fullnow.approvers.txtand the gatekeeper are gone. The label is the approval (Forgejo only lets people with write access add labels), it's sticky, so new pushes keep running, and it's removed after 14 quiet days. The new doorbell is webhook-driven, so there's no 50-PR scan.full: declared inconfig.ymland used by main, tags and API runs..circleci/is full and includes the website jobs. The website jobs also run fordocs/tested-configurations.mdanddocs/tested/**.mise.lockis full;.forgejo/is lint only.release.pyroot: main's copy now goes to.mb-main/release.pyin the checkout, so its root is the checkout: it finds the trusted key andcliff.toml. The orb's sign and publish jobs had the same bug and are fixed the same way.0.0.0, as inbuild-windows.yml.release-signuseskeygen'srelease.pemandrelease.pub.pemand verifies against the public key.released-as ... || true: removed.llvm-mingwtakes acacheparameter, and release builds passfalse.release.ymlandwebsite.ymlstay the only writers. The cutover PR turns CircleCI live and removes those workflows in the same commit.Smaller ones:
fork_sha, branch and tag in shell text: they go throughenvironment:now, and the orb's checkout refuses afork_shathat isn't 40 hex digits.HEAD..HEAD; it compares againstHEAD~1.test-linuxruns on x86 (large), sotest_fpx_stickyand the other x86-only checks still run.events/release.ymlhas one now, and so do the tag setup and noop jobs.Questions:
pushandpull_requestpipelines;apiis a manual run. Fork PRs come in through a second CircleCI project on this repo, started by a Forgejo webhook.gate: each job posts its ownci/circleci: <job>status, so the cause shows red on the commit;gatenever appears. Oncegateis required, that blocks the merge. I'd rather not givegatean API token on PR pipelines just to turn it red.release-signingispipeline.git.branch == "main" or pipeline.git.tag starts-with "v"and is limited to the Longshot and Launcher projects. Those values come from the event, not from the config, so a branch's own config can't change them.Jordan
v*tags, before "merge when checks pass" is turned on.test_pad_layout.pyneeds fixing before the gate is required, and reverting first when main goes red makes sense.The orb's source will be in MagicBeanDev/orb. The owner is creating it, and I'll link the first PR here as soon as it's up.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.