Only GitHub-hosted runners, public from the first push¶
Context and Problem Statement¶
Contributions are to flow one after another as pull requests into a public repository, checked by CI, SonarQube Cloud and CodeRabbit. For public repositories: "Use of the standard GitHub-hosted runners is free and unlimited on public repositories" (https://docs.github.com/en/actions/reference/runners/github-hosted-runners, retrieved 2026-10-11) [verified]. A runner that the maintainers operate themselves would, in a public repository, run code from other people's pull requests on their machines; GitHub advises against this for public repositories (https://docs.github.com/en/actions/reference/security/secure-use, retrieved 2026-10-10) [verified]. On which runners do the workflows run, and from when on is the repository public?
Decision Drivers¶
- No pull request runs code on a machine of the maintainers.
- One set of workflow files without a switch, valid from the first push on.
- CI on GitHub's standard runners, which are free for public repositories.
- Checkable by a tool, not only by review.
Considered Options¶
- Only GitHub-hosted runners, repository public from the first push
- Self-hosted runners as long as the repository is private, then GitHub-hosted runners
(switch in
runs-on) - Always self-hosted runners
Decision Outcome¶
Chosen option: "Only GitHub-hosted runners, repository public from the first push", because no
runner of the maintainers exists that other people's code can reach, and the workflows need no
switch. There are no self-hosted runners. Every job names a fixed label of a
standard runner: ubuntu-24.04, for the portability test additionally ubuntu-24.04-arm, for
the app builds macos-15 and windows-2025. The repository is switched to public directly after
the first push (public-switch.md).
Supplementary rules:
- Public-only jobs. CodeQL, Scorecard and Pages run only while the repository is
public (job condition
github.event.repository.visibility == 'public'): code scanning and Scorecard require a public repository, and Pages would turn even a private repository into a public website. Scheduled runs carry no repository object in the payload [derived from the event reference, which names no payload forschedule, https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows, retrieved 2026-10-10]; for themvars.BOTHY_VISIBILITY == 'public'applies in addition, set when switching to public. If the variable is missing, scheduled jobs do not run. - Release workflows.
release.ymlstarts only on a tag push,portability.ymlonly as a call fromrelease.ymlafterpublish(workflow_call) or by hand with a product tag (workflow_dispatch, D-W-59). Onlyrelease.ymlcalls a release workflow, from one job and without a matrix (D-W-73), and no workflow starts another one byworkflow_dispatch, because none hasactions: write(D-W-111; the dispatch endpoint requires the permission "Actions" withwrite, https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps, retrieved 2026-10-11 [verified]): the portability test checks the release that the same run published, and does so once. - Fixed labels. No label
*-latest(it changes the image without a change in the repository), no larger runners (they are not standard runners), no list, no group and no expression except${{ matrix.<key> }}over a static matrix of fixed labels. - Release jobs. Signing secrets exist only in the environments
release,release-appleandrelease-windows. A job with one of these environments uses outputs of other jobs only in theif:of the job or of a step (D-W-72), resolves the tag again itself and compares, and fetches artifacts only by ID (D-W-52). GitHub compares environment names without case ("Environment names are not case sensitive", https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments, retrieved 2026-10-11 [verified]); the check does so too and requires a literal environment name without an expression (D-W-110). Every standard runner except the single-core runners is a new VM ("each GitHub-hosted runner is a new virtual machine (VM) hosted by GitHub", same source as above, retrieved 2026-10-11) [verified]; a job therefore takes over no state of another job except through caches, artifacts and outputs. - Scan after the merge. The only exceptions to "no
pushon branches" aredenylist-main.yml: apushto exactlymain, which checks the pushed commits against the private deny list (D-W-34), andbuild-verify.yml: apushto exactlybuild/**, which runs every gate ofmake gateswithout secrets for a build team and is no required check (push-discipline.md).
Consequences¶
- Good, because no pull request runs code on a machine of the maintainers.
- Good, because the workflows are simple: one fixed label per job, no switch, no setup and no removal of runners.
- Good, because CI, CodeQL, Scorecard, SonarQube Cloud and CodeRabbit run from the first pull request on.
- Bad, because everything in the repository is public from the first push; the state must be fully checked beforehand (public-switch.md, part A).
- Bad, because no CI runs before the first push; until then
make checkchecks locally (ADR 0038). - Bad, because a new image version from GitHub needs a change to the workflows; in return no run changes without a change in the repository.
Confirmation¶
tools/ci/check_runners.py (gate runners-check, tests in tools/ci/tests/) rejects every job
whose runner is not a fixed GitHub-hosted label (rule runner-not-github-hosted),
as well as scheduled workflows without a public condition (schedule-without-public-guard),
release workflows with triggers other than their own (release-trigger), calls of a
release workflow outside a matrix-free job of release.yml (reusable-release-caller,
D-W-73), actions: write and write-all, also on a job (rules actions-write,
broad-permissions, D-W-111), push on branches except main in denylist-main.yml
(push-on-branch), environment names that are not literal (environment-not-literal, D-W-110),
in jobs with environment release* in any spelling outputs of other jobs outside the if:
(allow list D-W-61, D-W-72, rule release-untrusted-output) and artifacts not fetched by ID
(release-artifact-by-id).
Pros and Cons of the Options¶
Only GitHub-hosted runners, public from the first push¶
- Good, because it is simple and secure.
- Good, because the standard runners are free in public repositories (see above).
- Bad, because no CI runs before the publication.
Self-hosted runners while private, then GitHub-hosted runners¶
- Good, because CI would already run before the publication.
- Bad, because a switch stands in every job, runners have to be set up, separated and removed before switching to public, and a forgotten step brings other people's code onto a machine of the maintainers.
Always self-hosted runners¶
- Bad, because it is insecure in the public repository (see above).
More Information¶
Procedure of the publication: public-switch.md. Local gates: ADR 0038.