Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/queue-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Queue build

# The merge queue's build of a group, on the host's self-hosted runner. Only
# queue.yml calls it. Once queue.yml calls it at main and the quest-queue runner
# group admits only this workflow at refs/heads/main, no other workflow can
# reach the runner, an approved fork run's included; a PR's edit to this file
# then runs only after it lands.
on:
workflow_call:

permissions:
contents: read
pull-requests: read

jobs:
build:
name: Build
# A caller's own event decides this, so a pull request's workflow that
# calls this file still gets no runner.
if: github.event_name == 'merge_group'
runs-on: [self-hosted, quest-queue]
# Below the queue's 90-minute response window, so a hung build reports
# before the group expires.
timeout-minutes: 60
env:
BASE: ${{ github.event.merge_group.base_sha }}
steps:
# Validate quest completion with the base's CLI, which the PR cannot weaken.
- name: Check out the base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.merge_group.base_sha }}
persist-credentials: false
- name: Build quest from the base
run: nix build --out-link "$RUNNER_TEMP/quest-base" .#quest
- name: Check out the merge group
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Check that the PR leaves its quest accurate
working-directory: ${{ runner.temp }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
QUEUE_REF: ${{ github.event.merge_group.head_ref }}
run: |
# Build concurrency and group size are both one in main's ruleset.
[[ "$QUEUE_REF" =~ ^refs/heads/gh-readonly-queue/main/pr-([0-9]+)- ]] || { echo "::error::unexpected merge group ref" >&2; exit 1; }
head_ref=$(gh api "repos/$REPO/pulls/${BASH_REMATCH[1]}" --jq .head.ref)
./quest-base/bin/quest --root "$GITHUB_WORKSPACE" check --base "$BASE" --branch "$head_ref"
- name: Install dependencies
run: nix develop --command just install
- name: Check
run: nix develop --command just check
- name: Test
run: nix develop --command just test
- name: Check Nix package
run: nix flake check
61 changes: 22 additions & 39 deletions .github/workflows/queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,50 +40,33 @@ concurrency:
cancel-in-progress: true

jobs:
# The build runs in queue-build.yml so its runner group can admit that
# workflow alone; see that file. This calls the merge group's own copy until
# that file is on main, when the call moves to
# moq-dev/quest/.github/workflows/queue-build.yml@main.
build:
name: Queue build
if: github.event_name == 'merge_group'
uses: ./.github/workflows/queue-build.yml

# The required context. A reusable workflow's jobs report as
# `Queue build / Build`, so this job reports the build's result under the
# name main's ruleset requires. It skips with the build on pull requests, and
# runs even when the build fails or is cancelled, since a skipped context
# would pass the group.
queue:
name: Queue
if: github.event_name == 'merge_group'
runs-on: [self-hosted, quest-queue]
# Below the queue's 90-minute response window, so a hung build reports
# before the group expires.
timeout-minutes: 60
env:
BASE: ${{ github.event.merge_group.base_sha }}
needs: build
if: always() && github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Validate quest completion with the base's CLI, which the PR cannot weaken.
- name: Check out the base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.merge_group.base_sha }}
persist-credentials: false
- name: Build quest from the base
run: nix build --out-link "$RUNNER_TEMP/quest-base" .#quest
- name: Check out the merge group
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Check that the PR leaves its quest accurate
working-directory: ${{ runner.temp }}
- name: Require the build to pass
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
QUEUE_REF: ${{ github.event.merge_group.head_ref }}
run: |
# Build concurrency and group size are both one in main's ruleset.
[[ "$QUEUE_REF" =~ ^refs/heads/gh-readonly-queue/main/pr-([0-9]+)- ]] || { echo "::error::unexpected merge group ref" >&2; exit 1; }
head_ref=$(gh api "repos/$REPO/pulls/${BASH_REMATCH[1]}" --jq .head.ref)
./quest-base/bin/quest --root "$GITHUB_WORKSPACE" check --base "$BASE" --branch "$head_ref"
- name: Install dependencies
run: nix develop --command just install
- name: Check
run: nix develop --command just check
- name: Test
run: nix develop --command just test
- name: Check Nix package
run: nix flake check
RESULT: ${{ needs.build.result }}
run: '[[ "$RESULT" == success ]] || { echo "::error::the queue build ended $RESULT" >&2; exit 1; }'

# The merge queue still builds every group with the job above; this only
# The merge queue still builds every group with the jobs above; this only
# satisfies the PR-side requirement, as a pull_request run's skipped job does.
report:
name: Report Queue
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@ The commands accept the same PR argument and authentication as
and 1 for other failures.

This repository lands through GitHub's merge queue. Its required `Queue` check
runs `just check` and `just test` on each merge group on the self-hosted runner;
it skips on pull requests. `quest land watch` runs the fixer on dropped PRs and
passes a merge group only when the group's build, `just check` and `just test`
on the self-hosted runner, passes; it skips on pull requests. `quest land watch` runs the fixer on dropped PRs and
re-enqueues healthy heads at the back of the queue.

## Land a batch with quest land
Expand Down
88 changes: 69 additions & 19 deletions quest/m0/queue-watcher/runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,88 @@

The self-hosted `quest-queue`, `rind-queue` and `rust2-queue` runners that
[kixelated/nixos#19](https://github.com/kixelated/nixos/pull/19) adds run only
their own repository's merge-queue workflow, and nothing else on the host
their own repository's merge-queue builds, and nothing else on the host
reaches them:

- kixelated/nixos's quest-land module creates no check launcher, users,
networks, kvm access or storage tagging for a repository in watch mode.
- Each runner group admits only its repository's
`.github/workflows/queue.yml`.
- Each runner group admits only its repository's queue build.
- moq-dev/quest, moq-dev/rind and moq-dev/rust2 require approval before
running workflows from any outside contributor's fork PR, not only a
first-time contributor's (moq-dev/quest was `first_time_contributors` on
2026-11-02).
first-time contributor's.

## Plan

Decided 2026-11-02 (user, /quest-plan of the follow-ups from Claude's review
of kixelated/nixos#19 in that day's `/quest-complete` session): ✅ all three
items, m0, ranked right after the [cutover](/quest/m0/queue-watcher/follow.md).

- Watch-mode cleanup: the review found the base branch
([kixelated/nixos#16](https://github.com/kixelated/nixos/pull/16)) already
creates check sockets and users for moq in watch mode, which only that
repository's daemon user can reach. Gate them on `quest land serve` mode.
- Runner groups: #19 puts each runner in its own group, limited to its one
repository. Limit each group to its repository's `queue.yml` too, through
the org's runner group settings (`restricted_to_workflows`).
- Fork approval: a returning contributor's fork PR runs its own copy of the
workflows, which can name a self-hosted label. Set each repository's
`fork-pr-contributor-approval` policy to all outside contributors. Do this
no later than the cutover registers the runners, if it's still open then.
- Verify by checking that a fork PR's workflow waits for approval, and by
listing each group's allowed workflows.
Decided 2026-11-03 (user, /quest-complete grill round), since GitHub matches a
runner group's selected workflow by its exact ref, with no wildcard, and a
merge group runs `queue.yml` from its own `gh-readonly-queue/main/pr-<n>-<sha>`
branch:

- How should each self-hosted runner group (quest-queue, rind-queue,
rust2-queue) be restricted to its queue build?
- ✅ (1) Reusable workflow (Recommended): move each repository's
self-hosted job into a reusable workflow that `queue.yml` calls at `@main`
and runs only for `merge_group`, then restrict the group to that workflow
at `refs/heads/main`.
- (2) Leave the groups unrestricted and rely on one-repository groups plus
fork approval.

Done in [moq-dev/quest#439](https://github.com/moq-dev/quest/pull/439):

- Fork approval: on 2026-11-02 each repository's
`fork-pr-contributor-approval` policy went from `first_time_contributors`
to `all_external_contributors`.
- Watch-mode cleanup:
[kixelated/nixos#20](https://github.com/kixelated/nixos/pull/20), stacked on
#19, generates check launchers, users, networks, staging, KVM grants and
storage tagging only for repositories on `quest land serve`, and adds the
`quest-land-watch` flake check.
- Reusable build, first step: each repository's self-hosted job moved into
`.github/workflows/queue-build.yml`, which `queue.yml` calls by its local
path. The build job and its caller both run only for `merge_group`, and a
`Queue` job on a hosted runner reports the build's result under the context
main's ruleset requires. This is moq-dev/quest#439 itself,
[moq-dev/rind#289](https://github.com/moq-dev/rind/pull/289) and
[moq-dev/rust2#536](https://github.com/moq-dev/rust2/pull/536).

What remains:

- Once a repository's first step is on main, switch its `queue.yml` call to
`moq-dev/<repo>/.github/workflows/queue-build.yml@main`. It can't be one
step: a merge group calling `@main` before the file is there finds no
workflow and fails. Say in that repository's contributing guide that a PR's
edit to `queue-build.yml` runs only after it lands.
- Then restrict each group (see Waiting).
- Deploy kixelated/nixos#20 with the rest of the stack.
- Verify that a fork PR's workflow waits for approval.

## Waiting

- Maintainer: once a repository's `queue.yml` calls `queue-build.yml@main` on
main, restrict its runner group to that workflow. Not before: the group
would then admit no merge-group job and stall the queue. For moq-dev/quest
(group 8; rind-queue is 9 and rust2-queue is 10, each with its own name and repository):
`gh api -X PATCH orgs/moq-dev/actions/runner-groups/8 -f name=quest-queue -F restricted_to_workflows=true -f 'selected_workflows[]=moq-dev/quest/.github/workflows/queue-build.yml@refs/heads/main'`.
Check that `gh api orgs/moq-dev/actions/runner-groups/8` shows
`restricted_to_workflows: true` with that one workflow, and that the next
merge group's `Queue build / Build` job still runs on the runner. GitHub
documents workflow restriction for Enterprise Cloud while moq-dev is on the
Team plan, though the API accepts the fields; if GitHub rejects or drops
them, revert with `-F restricted_to_workflows=false` and fall back to option
(2) above.
- Maintainer: deploy [kixelated/nixos#20](https://github.com/kixelated/nixos/pull/20)
through `deploy/quest-land`, since only the maintainer activates the live
host. Check that `systemctl list-units 'quest-land*check*'` shows only
`quest-land-moq-pro-check.socket`, and that `getfacl /dev/kvm` names no
check UID.
- Maintainer: the next time an outside contributor opens a fork PR on
moq-dev/quest, moq-dev/rind or moq-dev/rust2, check that its workflow runs
wait for approval.

## Related

- [Watcher on the host](/quest/m0/queue-watcher/host.md) - the host module these runners and daemons share
- [Gate host](/quest/m0/host-land/host.md) - the host module these runners and daemons share