gitea-mq

module
v0.0.0-...-c98162d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT

README

gitea-mq

Status: stable

A merge queue for Gitea and GitHub. Serializes PR merges so your main branch stays green.

Workflow

There are no bot commands. gitea-mq registers itself as a required status check and hooks into the existing "Merge when checks succeed" / auto-merge button.

When someone clicks that button, gitea-mq notices the pending automerge and enqueues the PR. For the PR at the head of the queue it creates a temporary branch (gitea-mq/<pr>) that merges the PR into the current target branch, and CI runs there. If all required checks pass, gitea-mq marks its own status success and the forge's automerge completes the merge. If a check fails or times out, gitea-mq withdraws the merge intent (cancels automerge, removes the merge label) and posts a comment saying what went wrong.

Alternatively, adding the merge label (default merge-queue, see GITEA_MQ_MERGE_LABEL) to a PR enqueues it without automerge. On GitHub this is the way to merge stacked pull requests, which have no auto-merge: label the topmost PR you want to merge, gitea-mq tests the whole stack against the stack's base branch and, on green, marks all stack heads as passed and performs the atomic stack merge itself. Stacked PRs without the label get a pending gitea-mq status hinting at this workflow. Removing the label removes the PR from the queue.

By default one PR is tested at a time per target branch (see Batching below for testing several at once). PRs targeting different branches get independent queues. If the head-of-queue PR already contains the current target tip, the merge-branch run is skipped and the PR's own green CI is accepted, since the merged tree would be identical (disable with GITEA_MQ_SKIP_QUEUE_IF_UP_TO_DATE=false if your gitea-mq/* CI runs a larger suite than PR CI).

Requirements

  • Gitea >= 1.22 and/or a GitHub App. With Gitea >= 1.24 (or a Forgejo release based on it) CI results arrive via the commit-status webhook and idle repos are only reconciled every GITEA_MQ_IDLE_POLL_INTERVAL; older versions fall back to polling every GITEA_MQ_POLL_INTERVAL.
  • PostgreSQL
  • For Gitea/Forgejo: an API token with repository and issue read/write scopes (the PR timeline and comments are served via the issue API)

Configuration

gitea-mq can manage Gitea repos, GitHub repos, or both from one process. At least one backend must be configured. All configuration is via environment variables.

Variable Required Default Description
GITEA_MQ_GITEA_URL gitea - Gitea instance URL. Setting this enables the Gitea backend.
GITEA_MQ_GITEA_TOKEN gitea - API token with repository and issue read/write scopes
GITEA_MQ_REPOS no - Comma-separated owner/repo list of Gitea repos (optional when GITEA_MQ_TOPIC is set)
GITEA_MQ_TOPIC no - Discover Gitea repos by topic instead of (or in addition to) a static list
GITEA_MQ_WEBHOOK_SECRET gitea - Shared secret for the Gitea webhook HMAC
GITEA_MQ_GITHUB_APP_ID github - GitHub App ID. Setting this enables the GitHub backend.
GITEA_MQ_GITHUB_PRIVATE_KEY / _FILE github - PEM-encoded App private key, or path to a file containing it
GITEA_MQ_GITHUB_WEBHOOK_SECRET github - Webhook secret configured on the GitHub App
GITEA_MQ_GITHUB_REPOS no - Comma-separated owner/repo list of GitHub repos to manage in addition to all repos the App is installed on
GITEA_MQ_GITHUB_POLL_INTERVAL no GITEA_MQ_POLL_INTERVAL Override the reconcile poll interval for GitHub (its rate limit is much higher)
GITEA_MQ_DATABASE_URL yes - PostgreSQL connection string
GITEA_MQ_LISTEN_ADDR no :8080 HTTP listen address
GITEA_MQ_WEBHOOK_PATH no /webhook Legacy alias for the Gitea webhook endpoint (the canonical path is /webhook/gitea)
GITEA_MQ_EXTERNAL_URL yes - URL where Gitea can reach this service (used for webhook auto-setup and commit status target URLs)
GITEA_MQ_POLL_INTERVAL no 30s Reconcile poll interval for repos with active queue work
GITEA_MQ_IDLE_POLL_INTERVAL no 15m Reconcile poll interval for idle repos (no active queue entries). Only used on forges that deliver CI status via webhooks (GitHub, Gitea >= 1.24); there idle repos are driven by webhooks and this periodic poll is just a safety net for deliveries missed while the service was down. Keeping it long bounds forge API usage to the number of repos with live queues rather than all managed repos
GITEA_MQ_CHECK_TIMEOUT no 1h Timeout for required checks
GITEA_MQ_SKIP_QUEUE_IF_UP_TO_DATE no true Skip the merge-branch CI run when a PR is already rebased onto the target branch tip (its own green CI already covers the merged tree)
GITEA_MQ_REQUIRED_CHECKS no - Fallback required CI contexts when branch protection has none (comma-separated)
GITEA_MQ_MERGE_LABEL no merge-queue Label that enqueues a PR (stack-aware on GitHub); set to none to disable
GITEA_MQ_BATCH_MAX no 1 Max PRs tested together as one batch. 1 = batching off (legacy behaviour). 0 = everything currently queued
GITEA_MQ_BISECT_MAX_STEPS no 0 Cap on CI builds spent bisecting one batch. 0 = unlimited
GITEA_MQ_REFRESH_INTERVAL no 10s Dashboard auto-refresh interval
GITEA_MQ_DISCOVERY_INTERVAL no 5m How often to re-scan Gitea topics and GitHub installations
GITEA_MQ_CACHE_DIR no $XDG_CACHE_HOME/gitea-mq Directory for persistent bare git clones used for merge operations; unused repos are removed after 30 days
GITEA_MQ_LOG_LEVEL no info Log level: debug, info, warn, error

Batching (bors-style)

With GITEA_MQ_BATCH_MAX ≠ 1, gitea-mq tests up to N queued PRs at once on a single gitea-mq/batch/<id> branch. On green it fast-forwards the target branch itself to the tested SHA (the merged tree is exactly what CI saw). On red it bisects: split, retest the first half from the current target tip, land passing halves immediately, eject the failing singleton, then continue with the pending halves.

Trade-offs and prerequisites:

  • gitea-mq must be allowed to push to the protected target branch.
    • GitHub: the App is already a bypass actor on the gitea-mq ruleset it creates; no extra setup.
    • Gitea: add the API token's user to the branch protection's push whitelist. If you forget, every PR in the batch is removed with a comment naming the branch and the user to add.
  • Batched PRs land as merge commits regardless of the repo's configured merge style. Repos that mandate squash/rebase should leave BATCH_MAX=1.
  • A semantic conflict between two PRs that bisection puts in different halves can land both (each half passes alone). This matches bors-ng; keep BATCH_MAX modest if it bothers you.
  • If the forge does not detect a PR as merged within ~10s of the fast-forward, gitea-mq closes it with a "Merged as <sha>" comment.

Repo selection

There are three ways to tell gitea-mq which repos to manage.

Static list, listing repos explicitly:

GITEA_MQ_REPOS=org/app,org/lib

Topic discovery: tag repos in Gitea with a topic (e.g. merge-queue) and let gitea-mq find them. Any repo the API token has admin access to and that carries the topic gets picked up. Repos are re-discovered periodically, so adding or removing the topic on a repo is enough.

GITEA_MQ_TOPIC=merge-queue

Both: combine a static list with topic discovery. Explicitly listed repos are always managed regardless of their topics.

GITEA_MQ_REPOS=org/critical-service
GITEA_MQ_TOPIC=merge-queue

GitHub setup

gitea-mq talks to GitHub as a GitHub App. Use the App creation helper to pre-fill the registration form, or register one manually with:

  • Webhook URL: ${GITEA_MQ_EXTERNAL_URL}/webhook/github, secret = GITEA_MQ_GITHUB_WEBHOOK_SECRET
  • Repository permissions: Checks read & write, Commit statuses read, Contents read & write, Pull requests read & write, Administration read & write, Metadata read
  • Subscribed events: pull_request, check_run, status, installation, installation_repositories

Generate a private key, then set GITEA_MQ_GITHUB_APP_ID and GITEA_MQ_GITHUB_PRIVATE_KEY_FILE. On startup gitea-mq patches the App's webhook URL and secret to match GITEA_MQ_EXTERNAL_URL / GITEA_MQ_GITHUB_WEBHOOK_SECRET, so you can leave those fields blank when registering the App. Install the App on the orgs/repos you want managed; gitea-mq picks up every installation automatically. GITEA_MQ_GITHUB_REPOS is optional and additive: listed repos stay managed even if the installation is later removed.

Auto-setup

On startup, gitea-mq configures each managed repository:

  • Gitea: adds gitea-mq as a required status check to all existing branch protection rules and creates a status webhook pointed at the service.
  • GitHub: enables allow_auto_merge and creates a gitea-mq repository ruleset that requires the gitea-mq check on the default branch (the App and repo admins are bypass actors). Add further target branches to the ruleset's include list if you queue PRs against more than the default branch.

If the GitHub App lacks the Administration permission, auto-setup is skipped with a warning and the queue still runs against whatever the operator pre-configured.

GITEA_MQ_EXTERNAL_URL is the externally reachable URL of gitea-mq itself (e.g. https://mq.example.com), not the Gitea URL. It is used for webhook auto-setup and as the target URL in commit statuses, which links to the dashboard.

Hiding merge branches from git clients

gitea-mq creates temporary branches under gitea-mq/* for CI testing. By default these are visible to git clients and will be picked up by git fetch. You can configure Gitea's git to hide them from the ref advertisement.

On NixOS, when services.gitea is enabled on the same host, the module configures uploadpack.hideRefs for you. To turn that off:

services.gitea-mq.hideRefFromClients = false;

Elsewhere, run this as the Gitea system user (the user that owns the Gitea data directory):

git config --global uploadpack.hideRefs refs/heads/gitea-mq/

This hides the branches from git fetch and git ls-remote but does not affect the Gitea web UI, API, or webhook-driven CI. CI systems that need to check out merge branches can still fetch them with an explicit refspec, e.g. git fetch origin gitea-mq/123.

CI configuration

Your CI must run on gitea-mq/* branches. For example, in a Woodpecker/Drone pipeline:

when:
  branch:
    - main
    - gitea-mq/*

gitea-mq needs to know which CI checks must pass on the merge branch before it allows a merge. It resolves this in order:

  1. Branch protection. If the target branch has protection rules with required status checks, those are used (excluding gitea-mq itself, to avoid a circular dependency).
  2. GITEA_MQ_REQUIRED_CHECKS. If branch protection has no required status checks, or there is no branch protection at all, this comma-separated list is used as a fallback (e.g. ci/woodpecker,lint).
  3. Any single success. If neither is configured, any single passing commit status on the merge branch is enough.

Dashboard

A small web dashboard shows queue status across all managed repos, lets you drill into individual repos to see queued PRs, and inspect check results per PR. It auto-refreshes without JavaScript. There is also a /healthz endpoint for monitoring.

Repo and PR pages live under /repo/{forge}/{owner}/{name} (e.g. /repo/github/org/app/pr/42). Paths without the forge segment resolve as Gitea for compatibility with links posted by older versions.

NixOS module

{
  inputs.gitea-mq.url = "github:Mic92/gitea-mq";

  # In your NixOS configuration:
  imports = [ inputs.gitea-mq.nixosModules.default ];

  services.gitea-mq = {
    enable = true;
    giteaUrl = "https://gitea.example.com";
    giteaTokenFile = "/run/secrets/gitea-mq-token";  # file containing the API token
    repos = [ "org/app" "org/lib" ];
    databaseUrl = "postgres:///gitea-mq?host=/run/postgresql";
    webhookSecretFile = "/run/secrets/gitea-mq-webhook-secret";
    externalUrl = "https://mq.example.com";
  };
}

Real-world deployments:

NixOS module options
Option Type Default Description
enable bool false Enable the service
package package pkgs.gitea-mq Package to use
giteaUrl string or null null Gitea instance URL; enables the Gitea backend
giteaTokenFile path - File containing the Gitea API token
webhookSecretFile path - File containing the Gitea webhook secret
github.appId int or null null GitHub App ID; enables the GitHub backend
github.privateKeyFile path - File containing the GitHub App private key (PEM)
github.webhookSecretFile path - File containing the GitHub App webhook secret
github.repos list of strings [] GitHub repos in addition to all installations
github.pollInterval string or null null Override poll interval for GitHub
repos list of strings [] Repos to manage (owner/name); optional when topic is set
topic string or null null Discover repos by Gitea topic
databaseUrl string postgres:///gitea-mq?host=/run/postgresql PostgreSQL connection string
listenAddr string :8080 HTTP listen address
webhookPath string /webhook Webhook endpoint path
externalUrl string - URL where Gitea can reach this service (for webhook auto-setup and commit status links)
pollInterval string 30s Poll interval
checkTimeout string 1h Check timeout
skipQueueIfUpToDate bool true Skip merge-branch CI for PRs already rebased onto the target tip
requiredChecks list of strings [] Fallback required CI contexts when branch protection has none
refreshInterval string 10s Dashboard refresh interval
discoveryInterval string 5m How often to re-discover repos by topic
logLevel enum info Log level

Development

# Enter dev shell
nix develop

# Run tests (requires PostgreSQL, provided by the dev shell)
go test ./...

# Build
nix build

# Run NixOS integration test
nix build .#checks.x86_64-linux.nixos-test

# Format
nix fmt

License

MIT

Directories

Path Synopsis
cmd
gitea-mq command
internal
batch
Package batch implements bors-style batch testing: build a single merge branch from N queued PRs, fast-forward the target on green, and bisect on red.
Package batch implements bors-style batch testing: build a single merge branch from N queued PRs, fast-forward the target on green, and bisect on red.
discovery
Package discovery reconciles the desired set of managed repos against the registry.
Package discovery reconciles the desired set of managed repos against the registry.
forge
Package forge defines a forge-agnostic interface for repo, PR, status, branch, comment and setup operations.
Package forge defines a forge-agnostic interface for repo, PR, status, branch, comment and setup operations.
gitea
Package gitea provides a client interface and HTTP implementation for the Gitea REST API.
Package gitea provides a client interface and HTTP implementation for the Gitea REST API.
github
Package github implements forge.Forge for GitHub via a GitHub App.
Package github implements forge.Forge for GitHub via a GitHub App.
github/ghfake
It is not a faithful simulator: only the endpoints gitea-mq calls are implemented, with just enough state to drive the adapter and integration tests deterministically without network access.
It is not a faithful simulator: only the endpoints gitea-mq calls are implemented, with just enough state to drive the adapter and integration tests deterministically without network access.
logutil
Package logutil holds small logging helpers.
Package logutil holds small logging helpers.
merge
Package merge handles merge branch creation, cleanup, and startup reconciliation for the merge queue.
Package merge handles merge branch creation, cleanup, and startup reconciliation for the merge queue.
monitor
Package monitor evaluates commit status checks on merge branches and decides when the head-of-queue PR passes, fails, or times out.
Package monitor evaluates commit status checks on merge branches and decides when the head-of-queue PR passes, fails, or times out.
poller
Package poller reconciles the queue with the forge: it discovers PRs with auto-merge enabled, enqueues them once their own CI is green, and removes queued PRs that were merged/closed/retargeted/pushed/cancelled.
Package poller reconciles the queue with the forge: it discovers PRs with auto-merge enabled, enqueues them once their own CI is green, and removes queued PRs that were merged/closed/retargeted/pushed/cancelled.
queue
Package queue implements merge queue operations on top of the PostgreSQL store.
Package queue implements merge queue operations on top of the PostgreSQL store.
registry
Package registry coordinates the lifecycle of managed repos: adding, removing, and looking up repos at runtime.
Package registry coordinates the lifecycle of managed repos: adding, removing, and looking up repos at runtime.
testutil
Package testutil provides shared test infrastructure.
Package testutil provides shared test infrastructure.
web
Package web provides the server-rendered HTML dashboard for gitea-mq.
Package web provides the server-rendered HTML dashboard for gitea-mq.
webhook
Package webhook implements the HTTP handler that receives Gitea webhook events (commit_status) and routes them to the check monitor.
Package webhook implements the HTTP handler that receives Gitea webhook events (commit_status) and routes them to the check monitor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL