Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nixbot

Nixbot is a continuous integration (CI) service for the Nix ecosystem, shipped as a single NixOS service. It is a rewrite of buildbot-nix, its spiritual ancestor.

What started as a set of Buildbot plugins now runs standalone. One server handles forge webhooks, nix-eval-jobs evaluation, builds through the local nix daemon (offloaded via remote builders), commit statuses, and its own web frontend.

Chat with us on Matrix: #nixbot:thalheim.io

Features

  • Fast, Parallel evaluation using nix-eval-jobs
  • Gitea/Github integration:
    • Login with GitHub to control builds
    • CI status notification in pull requests and on the default branch
  • Embeddable SVG build-status badges for READMEs
  • All builds share the same nix store for speed
  • Tested with large flakes (50k flake outputs per evaluation)
  • The last attribute of a build is protected from garbage collection
  • Build matrix based on .#checks attributes
  • No arbitrary code runs outside of the Nix sandbox
  • experimental hercules-ci effect to run impure CI steps i.e. deploying NixOS

Getting Started

To set up nixbot, start by exploring the provided examples:

Additionally, you can find real-world examples at the end of this document.

The service runs on one machine; to support multiple architectures and to scale out builds, configure nix remote builders. For a practical NixOS example, see this remote builder configuration.

Migrating from buildbot-nix

Nixbot replaces the buildbot master/worker pair with a single service. See the migration guide for the full upgrade instructions.

Using Nixbot in Your Project

Nixbot automatically triggers builds for your project under these conditions:

  • When a pull request is opened.
  • When a commit is pushed to the default git branch.

It does this by evaluating the .#checks attribute of your project’s flake in parallel. Each attribute found results in a separate build step. You can test these builds locally using nix flake check -L or nix-fast-build.

If you need to build other parts of your flake, such as packages or NixOS machines, you should re-export these into the .#checks output. Here are two examples to guide you:

Authentication backend

Anonymous users get read-only access to public projects; private repositories and their builds are only visible to users with access on the forge. For write actions (restart, cancel) a login is required. Every enabled forge with OAuth credentials configured offers a login, OIDC via services.nixbot.oidc.enable; several providers can be active at once.

We have the following roles:

  • Admins (hard-coded in the NixOS configuration): reload the project list, enable/disable projects, restart/cancel any build.
  • Repo writers (forge write access): restart/cancel that repository’s builds.
  • PR authors: restart/cancel their own pull request’s builds.
GitHub Integration

Nixbot uses GitHub App authentication to integrate with GitHub repositories. This enables automatic webhook setup, check-run reporting (with a working Re-run button), and secure authentication.

See the GitHub documentation for setup instructions.

Gitea Integration

Nixbot integrates with Gitea using access tokens for repository management and OAuth2 for user authentication. This enables automatic webhook setup, commit status updates, and secure authentication.

See the Gitea documentation for setup instructions.

GitLab Integration

Token-based integration with per-repository webhooks and commit status updates.

See the GitLab documentation for setup instructions.

Generic OIDC Authentication

Nixbot supports generic OpenID Connect (OIDC) authentication, allowing you to use any OIDC-compliant identity provider (Keycloak, PocketID, Authentik, etc.) for user login.

See the OIDC documentation for configuration details.

Per Repository Configuration

Currently nixbot will look for a file named nixbot.toml (falling back to the legacy buildbot-nix.toml) in the root of whichever branch it’s currently evaluating, parse it as TOML and apply the configuration specified. The following table illustrates the supported options.

keytypedescriptiondefaultexample
lock filelock_filestrdictates which lock file nixbot will use when evaluating your flakeflake.lockhave multiple lockfiles, one for nixpkgs-stable, one for nixpkgs-unstable or by default pin an input to a private repo, but have a lockfile with that private repo replaced by a public repo for CI
attributeattributestrwhich attribute in the flake to evaluate and buildchecksusing a different attribute, like hydraJobs
flake_dirflake_dirstrwhich directory the flake is located.using a different flake, like ./tests
effects on pull requestseffects_on_pull_requestsboolrun hercules-ci effects on pull requestsfalseset to true to run effects on PRs
effects brancheseffects_brancheslist[str]glob patterns for additional branches that run effects[]["staging", "release/*"]

By default, effects only run on the default branch. The effects_branches and effects_on_pull_requests settings are always read from the default branch’s nixbot.toml (via git show) so that pull request authors cannot grant themselves effects access.

⚠️ Security warning: PR effects receive the same effects_per_repo_secrets as default-branch effects. A malicious PR can modify the effect code to exfiltrate these secrets. Only enable effects_on_pull_requests for repositories where you trust all contributors, or where no secrets are configured.

Status badges

Nixbot serves an SVG build-status badge for the latest build on a branch:

https://<nixbot-host>/repos/<forge>/<owner>/<repo>/badge.svg

It reports the default branch unless ?branch=<name> is given. Embed it in a README with:

[![nixbot](https://<nixbot-host>/repos/github/<owner>/<repo>/badge.svg)](https://<nixbot-host>/repos/github/<owner>/<repo>)

Private repositories return 404 to anonymous requests, so status is never exposed.

Binary caches

To access the build results on other machines there are two options at the moment

Local binary cache (harmonia)

You can set up a binary cache on the CI machine to make its nix store accessible from other machines. Check out the README of the project, for an example configuration

Cachix

Nixbot also supports pushing packages to cachix via the services.nixbot.cachix options.

Attic

Nixbot does not have native support for pushing packages to attic yet. However it’s possible to integrate run a systemd service as described in this example configuration. The systemd service watches for changes in the local nix store and uploads the contents to the attic cache.

(experimental) Hercules CI effects

See docs/EFFECTS.md for CLI usage, flake reference support, and secrets configuration.

Incompatibilities with the lix overlay

The lix overlay overrides nix-eval-jobs with a version that doesn’t work with nixbot because of missing features and therefore cannot be used together with the nixbot module.

Possible workaround: Don’t use the overlay and only set the nix.package = pkgs.lix; NixOS option.

Alternatives

  • Garnix - Fully hosted, zero-config CI for flakes.
  • Hercules CI - Hosted CI with self-hosted agents. Nixbot’s effects system is inspired by theirs.
  • Hydra - The original Nix CI, powers nixos.org. Written in Perl, not recommended for new deployments.

Real-World Deployments

See Nixbot in action in these deployments:

Need commercial support or customization?

For commercial support, please contact Mic92 at joerg@thalheim.io.

GitHub Integration

Buildbot-nix uses GitHub App authentication to integrate with GitHub repositories. This enables automatic webhook setup, check-run reporting, and secure authentication.

Step 1: Create a GitHub App

  1. Navigate to:

    • For personal accounts: https://github.com/settings/apps/new
    • For organizations: https://github.com/organizations/<org>/settings/apps/new
  2. Configure the app with these settings:

    • GitHub App Name: nixbot-<org> (or any unique name)
    • Homepage URL: https://nixbot.<your-domain>
    • Webhook: Enable (Active) and set:
      • Webhook URL: https://nixbot.<your-domain>/webhooks/github
      • Webhook secret: the same value as webhookSecretFile below
    • Callback URL (optional, for OAuth): https://nixbot.<your-domain>/auth/github/callback
  3. Set the required permissions:

    • Repository Permissions:
      • Contents: Read-only (to clone repositories), (read and write is required for effects to push to branches)
      • Checks: Read and write (to report build status as check runs)
      • Metadata: Read-only (basic repository info)
      • Pull requests: Read-only (required to subscribe to the pull_request event), (read and write is required for effects to create PRs)
    • Organization Permissions (if app is for an organization):
      • Members: Read-only (to verify organization membership for access control)
    • Subscribe to events: Push, Pull request, Check run, Check suite

    Note: when adding permissions to an existing app, every installation (your user account and each organization) must accept the new permissions under Settings → GitHub Apps → Configure before events are delivered.

  4. After creating the app:

    • Note the App ID
    • Generate and download a private key (.pem file)

Step 2: Configure nixbot

Add the GitHub configuration to your NixOS module:

services.nixbot = {
  enable = true;
  domain = "nixbot.example.com";  # Your nixbot domain
  github = {
    enable = true;
    appId = <your-app-id>;  # The numeric App ID
    appSecretKeyFile = "/path/to/private-key.pem";  # Path to the downloaded private key

    # OAuth credentials enable the GitHub login button
    oauthId = "<oauth-client-id>";
    oauthSecretFile = "/path/to/oauth-secret";

    # Optional: request the write-capable "repo" OAuth scope at login so
    # private repositories are visible to their members, and so the login
    # token carries the push access that drives the per-repo restart
    # button (see "OAuth scope and the restart button" below). GitHub has
    # no read-only repo scope: "repo" grants write access and nixbot
    # stores the token server-side for the session. Off by default; leave
    # it off unless the instance builds private repositories.
    # oauthPrivateRepoScope = true;

    # A random secret used to verify incoming webhooks from GitHub
    webhookSecretFile = "/path/to/webhook-secret";

    # Optional: only allow these owners/repositories to be built
    userAllowlist = [ "my-org" ];
    repoAllowlist = [ "other-org/repo" ];

    # One-shot import: repositories with this topic are enabled on first
    # startup with an empty database; afterwards manage projects in the web UI
    topic = "build-with-buildbot";
  };
};

Step 3: Install the GitHub App

  1. Go to your app’s settings page
  2. Click “Install App” and choose which repositories to grant access
  3. The app needs access to all repositories you want to build with nixbot

Step 4: Repository Configuration

For each repository you want to build:

  1. Enable the project:

    • Toggle the project on in the web UI (as admin)
  2. Webhook delivery:

    • GitHub delivers push, pull_request, check_run and check_suite events through the App-level webhook configured in Step 1; no per-repository webhooks are created.
    • The endpoint is https://nixbot.<your-domain>/webhooks/github (the legacy /change_hook/github path also works).

How It Works

  • Authentication: Uses GitHub App JWT tokens for API access and installation tokens for repository-specific operations
  • Project Discovery: Automatically discovers repositories the app has access to (restricted by userAllowlist/repoAllowlist if set); discovered projects are built once enabled in the web UI
  • Webhook Delivery: Push, pull_request, check_run and check_suite events arrive via the GitHub App webhook; the payload signature is verified with the webhook secret
  • Status Updates: Reports build status as Check Runs (with markdown log excerpts and a working Re-run button) on commits and pull requests
  • Access Control:
    • Admins: Configured users can reload projects and manage builds
    • Repo writers: Users with write access to the repo can restart/cancel its builds
    • PR authors: Can restart/cancel the builds of their own pull request

OAuth scope and the restart button

The “repo writers” role is decided from the permissions.push flag GitHub returns for each repository from GET /user/repos, matched against the login user’s OAuth token. Adding a user as a collaborator is therefore not enough on its own: they must log in via the GitHub button so nixbot holds a token, and that token must actually surface their push access.

oauthPrivateRepoScope controls the scope of that login token (read:user by default, read:user repo when enabled). GitHub classic OAuth has no read-only repo scope, so repo unavoidably grants write access and nixbot stores the token server-side for the session. Enable it only if you need private-repo visibility.

Instance admins and PR authors can always restart their builds regardless of this setting.

Troubleshooting

  • Projects not appearing: Check that:

    • The GitHub App is installed for the repository
    • The repository is not excluded by userAllowlist/repoAllowlist
    • Reload projects manually through the web UI
  • Project appears but nothing builds: Enable the project in the web UI

  • No builds on push: Verify the App webhook is Active, its URL points to https://nixbot.<your-domain>/webhooks/github, and its secret matches webhookSecretFile. Check recent deliveries under the app’s “Advanced” tab.

  • Authentication issues: Ensure the private key file is readable by the nixbot service

Gitea Integration

Buildbot-nix integrates with Gitea using access tokens for repository management and OAuth2 for user authentication. This enables automatic webhook setup, commit status updates, and secure authentication.

Step 1: Create a Gitea Access Token

  1. Create a dedicated Gitea user (recommended for organizations):

    • This user will manage webhooks and report build statuses
    • Add this user as a collaborator with Administrator permission to every repository you want to build: Gitea only allows repo admins to manage webhooks. Without admin the repository is still discovered, but the webhook (push, pull_request, pull_request_sync events) must be created manually. Private repositories are invisible to the token until the user is added.
  2. Generate an access token:

    • Log in as the dedicated user
    • Go to Settings → Applications → Generate New Token
    • Required permissions:
      • write:repository - To create webhooks and update commit statuses
      • read:user - To list the repositories the user has access to
    • Save the token securely

Step 2: Set up OAuth2 Authentication (for user login)

  1. Create an OAuth2 Application:

    • Navigate to one of these locations:
      • Site Administration → Applications (for admins, applies globally)
      • Organization Settings → Applications (for organization-wide access)
      • User Settings → Applications (for personal use)
  2. Configure the OAuth2 app:

    • Application Name: nixbot
    • Redirect URI: https://nixbot.<your-domain>/auth/gitea/callback
  3. Note the credentials:

    • Client ID
    • Client Secret

Step 3: Configure nixbot

Add the Gitea configuration to your NixOS module:

services.nixbot = {
  gitea = {
    enable = true;
    instanceUrl = "https://gitea.example.com";

    # Access token for API operations
    tokenFile = "/path/to/gitea-token";

    # OAuth2 for user authentication
    oauthId = "<oauth-client-id>";
    oauthSecretFile = "/path/to/oauth-secret";

    # Optional: SSH authentication for private repositories
    sshPrivateKeyFile = "/path/to/ssh-key";
    sshKnownHostsFile = "/path/to/known-hosts";

    # Optional: only allow these owners/repositories to be built
    userAllowlist = [ "my-org" ];
    repoAllowlist = [ "other-org/repo" ];

    # One-shot import: repositories with this topic are enabled on first
    # startup with an empty database; afterwards manage projects in the web UI
    topic = "build-with-buildbot";
  };
};

If webhooks must reach nixbot under a different URL than the web UI, set services.nixbot.webhookBaseUrl.

Step 4: Repository Configuration

For each repository you want to build:

  1. Grant repository access:

    • Add the nixbot user as a collaborator with admin access
    • Admin access is required for webhook creation
  2. Enable the project:

    • Toggle the project on in the web UI (as admin)
  3. Automatic webhook creation:

    • Webhooks are created for enabled projects on every discovery cycle (startup, periodic refresh, manual reload) at https://nixbot.<your-domain>/webhooks/gitea
    • Each repository gets an auto-generated secret stored in the database; existing hooks are re-synced in place, leftover buildbot-era hooks pointing at this instance are removed
    • Webhook events: push, pull_request and pull_request_sync
  4. Manual webhook creation (only when the nixbot user is not a repo admin; watch for the “no admin permission to manage webhooks” warning):

    1. Enable the project
    2. As a nixbot admin, open the repository page in the nixbot web UI, expand webhook setup and press regenerate - the secret is shown exactly once (rotating later invalidates the old secret; auto-managed hooks pick the new one up on the next discovery cycle)
    3. In the Gitea repository: Settings → Webhooks → Add Webhook → Gitea
    4. Target URL and Secret from step 2, POST Content Type application/json
    5. Trigger On: Custom Events → check Push, Pull Request, and Pull Request Synchronized
    6. Activate and save

How It Works

  • Authentication: Uses Gitea access tokens for API operations
  • Project Discovery: Automatically discovers repositories where the nixbot user has admin access (restricted by userAllowlist/repoAllowlist if set); discovered projects are built once enabled in the web UI
  • Webhook Management: Automatically creates and manages webhooks (push and pull request events) for enabled projects
  • Status Updates: Reports build status back to Gitea commits and pull requests
  • Access Control:
    • Admins: Configured users can reload projects and manage builds
    • Repo writers: Users with write access to the repo can restart/cancel its builds (when OAuth is configured)
    • PR authors: Can restart/cancel the builds of their own pull request
  • Repository Access: Can use either HTTPS (with token) or SSH authentication for cloning private repositories

Troubleshooting

  • Projects not appearing: Check that:

    • The nixbot user has admin access to the repository
    • The repository is not excluded by userAllowlist/repoAllowlist
    • The access token has the correct permissions
    • Reload projects manually through the web UI
  • Project appears but nothing builds: Enable the project in the web UI

  • Webhooks not created: Verify the project is enabled and the nixbot user has admin permissions on the repository

  • Authentication issues:

    • Ensure the access token is valid and has required permissions
    • For OAuth issues, verify the redirect URI matches exactly
  • Private repositories: If using SSH, ensure the SSH key is properly configured and the known_hosts file contains the Gitea server

GitLab Integration

Buildbot-nix integrates with GitLab using an access token. GitLab has no GitHub-App equivalent, so the setup follows the Gitea model: token-based API access, per-repository webhooks, commit status updates.

Step 1: Create a GitLab Access Token

Use one of:

  • Personal access token (simplest; CI acts as your user): User Settings → Access tokens → Add new token, scope api
  • Group or project access token (separate bot identity): Group/Project Settings → Access tokens, role Maintainer, scope api
  • Service account (self-managed admin or paid tier)

Maintainer permission on a project is required for automatic webhook registration; without it the project is still discovered and built, but the webhook must be created manually (see below).

Step 2: Configure nixbot

services.nixbot = {
  gitlab = {
    enable = true;
    # instanceUrl defaults to https://gitlab.com
    tokenFile = "/path/to/gitlab-token";

    # Optional: restrict which repositories are built
    userAllowlist = [ "mygroup" ];
    # repoAllowlist = [ "mygroup/myrepo" ];

    # Optional: SSH authentication for fetching
    # sshPrivateKeyFile = "/path/to/ssh-key";
    # sshKnownHostsFile = "/path/to/known-hosts";
  };
};

Step 3: Enable Projects

  1. Open the nixbot web UI as an admin
  2. Enable the repository on the dashboard
  3. With Maintainer permission the webhook (push + merge request events) is registered automatically on the next discovery cycle

Manual webhook creation

Only needed when the token lacks Maintainer on the project (watch for the “no maintainer permission to manage webhooks” warning):

  1. Enable the project
  2. On the repository page in the nixbot web UI, expand webhook setup and press regenerate - the secret is shown exactly once
  3. In GitLab: Settings → Webhooks → Add new webhook
  4. URL from step 2, Secret token from step 2
  5. Trigger: Push events and Merge request events
  6. Save

Notes

  • Commit statuses are posted per attribute plus nixbot/nix-eval; use them in merge request approval rules / merged results pipelines.
  • GitLab does not sign webhook payloads; the secret is compared against the X-Gitlab-Token header. Use HTTPS for the webhook URL.
  • Access control (with OAuth login): admins manage everything; Developer access or higher to a project allows restart/cancel of its builds; MR authors can restart/cancel their own MR’s builds.

(experimental) Hercules CI effects

See flake.nix for an example and the Hercules CI effects documentation for the upstream reference.

CLI usage

The nixbot-effects CLI can list and run effects locally, or against remote repositories using Nix flake references.

Local repository

$ cd my-repo
$ nixbot-effects list
["deploy", "notify"]

$ nixbot-effects run deploy

Remote repository (flake reference)

No local checkout needed:

$ nixbot-effects run github:org/repo/branch#deploy
$ nixbot-effects list github:org/repo/branch
$ nixbot-effects list-schedules github:org/repo/branch
$ nixbot-effects run-scheduled github:org/repo#flake-update update

Subcommands

CommandDescription
listList available effects
runRun a single effect
list-schedulesList scheduled effects
run-scheduledRun a specific effect from a schedule

Flags

All subcommands accept:

FlagDescription
--revGit revision to use
--branchGit branch to use
--repoGit repo name
--pathPath to the repository (default: current directory)
--debugEnable debug mode (may leak secrets such as GITHUB_TOKEN)

run and run-scheduled also accept:

FlagDescription
--secretsPath to a JSON file with secrets

Running effects locally with secrets

Pass --secrets to provide secrets when running effects locally. The file is a JSON object where each key is a secret name and its value has a "data" field containing key-value pairs:

{
  "my-secret": {
    "data": {
      "token": "ghp_xxxxxxxxxxxx",
      "username": "deploy-bot"
    }
  }
}
$ nixbot-effects run --secrets secrets.json deploy

Inside the effect, secrets are available at /run/secrets.json (via HERCULES_CI_SECRETS_JSON). This follows the hercules-ci secrets format.

Buildbot secrets configuration

When running effects through nixbot (not locally), secrets are configured at different scopes:

  1. Repository-specific: "github:owner/repo" — applies to a single repository
  2. Organization-wide: "github:org/*" — applies to all repositories in an organization
services.nixbot.effects.perRepoSecretFiles = {
  # All repos in nix-community org get this token
  "github:nix-community/*" = config.agenix.secrets.nix-community-effects.path;

  # This specific repo gets its own token (overrides org-level)
  "github:nix-community/nixbot" = config.agenix.secrets.nixbot-effects.path;

  # All repos in a Gitea org
  "gitea:my-org/*" = config.agenix.secrets.my-org-effects.path;
};

The secrets files must be valid JSON files containing the secrets that will be made available to your effects at runtime.

OIDC Authentication

nixbot supports generic OpenID Connect (OIDC) authentication, allowing you to use any OIDC-compliant identity provider (Keycloak, PocketID, Authentik, etc.) for user login.

NixOS Configuration

{
  services.nixbot = {
    oidc.enable = true;

    # Provider-qualified identities: "oidc:<issuer-host>:<sub>" with the
    # default mapping (see "User Identification" below).
    admins = [
      "oidc:keycloak.example.com:f3b0a6f0-1c2d-4e5f-9a8b-7c6d5e4f3a2b"
      "oidc:keycloak.example.com:0f9e8d7c-6b5a-4f3e-2d1c-0b9a8f7e6d5c"
    ];

    oidc = {
      # Display name shown on login button
      name = "My Identity Provider";

      # OIDC discovery endpoint URL
      discoveryUrl = "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration";

      clientId = "nixbot";
      clientSecretFile = "/run/secrets/nixbot-oidc-secret";

      # Scopes to request (defaults shown)
      scope = [ "openid" "email" "profile" ];

      # Optional: include groups scope for group-based authorization
      # scope = [ "openid" "email" "profile" "groups" ];

      # Claim mapping (defaults shown)
      mapping = {
        username = "sub";  # Identity claim used for admin matching
        groups = null;  # Set to "groups" if using group sync
      };
    };
  };
}

Set your OIDC provider’s callback URL to: https://nixbot.example.com/auth/oidc/callback

Manual Configuration

For non-NixOS setups or local development, the service is configured via a JSON file passed to nixbot --config:

{
  "admins": [
    "oidc:keycloak.example.com:f3b0a6f0-1c2d-4e5f-9a8b-7c6d5e4f3a2b",
    "oidc:keycloak.example.com:0f9e8d7c-6b5a-4f3e-2d1c-0b9a8f7e6d5c"
  ],
  "oidc": {
    "name": "My Identity Provider",
    "discovery_url": "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
    "client_id": "nixbot",
    "client_secret_file": "/path/to/client_secret",
    "scope": ["openid", "email", "profile"],
    "mapping": {
      "username": "sub",
      "groups": null
    }
  }
}

Provider Examples

Keycloak

oidc = {
  name = "Keycloak";
  discoveryUrl = "https://keycloak.example.com/realms/{realm-name}/.well-known/openid-configuration";
  clientId = "nixbot";
  clientSecretFile = "/run/secrets/keycloak-secret";
};

Authelia

See examples/oidc-authelia.nix for a complete configuration including the Authelia side: the client registration (Authelia stores only a digest of the client secret) and the provider’s HMAC secret and issuer key.

PocketID

oidc = {
  name = "PocketID";
  discoveryUrl = "https://id.example.com/.well-known/openid-configuration";
  clientId = "nixbot";
  clientSecretFile = "/run/secrets/pocketid-secret";
};

Testing with Mock Provider

For local development, you can use oidc-provider-mock:

# Create a client secret file
echo "abc" > /tmp/client_secret

# Start the mock OIDC provider
nix run nixpkgs#pipx -- run oidc-provider-mock \
  --user-claims '{"sub": "alice", "email": "alice@example.com", "name": "Alice", "preferred_username": "alice123"}'

Then configure nixbot to use it (in the JSON config file):

"oidc": {
  "name": "Mock",
  "discovery_url": "http://localhost:9400/.well-known/openid-configuration",
  "client_id": "123",
  "client_secret_file": "/tmp/client_secret",
  "scope": ["openid", "email", "profile"],
  "mapping": {
    "username": "sub",
    "groups": null
  }
}

User Identification

Users are identified by the claim configured in mapping.username, which defaults to sub. This means:

  • The admins list entries must be provider-qualified: oidc:<issuer-host>:<identity> where <issuer-host> is the issuer URL without the https:// prefix and <identity> is the value of the configured claim
  • sub is the default because it is stable and unique per provider. A human-readable claim such as preferred_username can be configured instead, but only do so if your provider guarantees the claim is unique and not user-editable: a user who can change their own preferred_username could otherwise take over another user’s admin or viewer entry

Private Repositories

OIDC users have no forge token, so they only see public repositories by default. privateRepoViewers grants visibility (read-only; build control stays with admins):

services.nixbot.privateRepoViewers = {
  # Repository keys: "forge:owner/repo", "forge:owner/*" or "*";
  # the most specific key wins.
  "*" = [
    # Any authenticated user of this provider.
    "oidc:auth.example.com:*"
  ];
  "gitlab:acme/secret" = [
    # Exact identity or OIDC groups claim.
    "oidc:auth.example.com:alice"
    "oidc:auth.example.com:group:auditors"
  ];
};

Group rules need the groups claim in the session: add the groups scope and set mapping.groups = "groups".

The owner segment of a repository key is any namespace: a user, an organization, or a nested GitLab group ("gitlab:org/subgroup/*").

Members of a GitHub/Gitea organization do not need viewer rules for their own organization’s repositories: they log in with a forge token, and everything that token can access is visible to them. Viewer rules add visibility on top, for logins without forge access (OIDC) or for repositories outside a user’s own forge permissions. The OIDC equivalent of an organization is a group rule ("oidc:<issuer>:group:<name>").

Two caveats around group rules:

  • Group membership is captured in the session at login, so revoking a group in the identity provider takes effect on the next login or session expiry, not immediately.
  • Personal API tokens snapshot the creator’s groups: group rules apply to API requests with the groups the user had when the token was created, regardless of later membership changes.

Groups Support

To sync groups from your OIDC provider:

  1. Add the groups scope: scope = [ "openid" "email" "profile" "groups" ];
  2. Set the groups claim mapping: mapping.groups = "groups";

The claim name varies by provider - check your provider’s documentation.

Local Development

Running the test suite

nix develop
cd nixbot
python -m pytest nixbot/tests -q

The tests cover the full pipeline (webhook parsing, evaluation, scheduling, building, status reporting, web frontend) against an ephemeral PostgreSQL instance and real nix/git where available.

Running the service locally

nixbot is a single process configured by a JSON file:

# Start an ephemeral PostgreSQL
initdb -D /tmp/bb-pg
pg_ctl -D /tmp/bb-pg -o "-k /tmp/bb-pg -c listen_addresses=" start
createdb -h /tmp/bb-pg nixbot

cat > /tmp/nixbot.json <<EOF
{
  "db_url": "postgresql://$(whoami)@/nixbot?host=/tmp/bb-pg",
  "build_systems": ["x86_64-linux"],
  "url": "http://localhost:8010/",
  "state_dir": "/tmp/nixbot-state",
  "pull_based": {
    "repositories": {
      "my-project": {
        "name": "my-project",
        "default_branch": "main",
        "url": "https://github.com/example/my-project"
      }
    }
  }
}
EOF

python -m nixbot.main --config /tmp/nixbot.json --log-format text

Access the web UI at http://localhost:8010. Pull-based repositories need no forge credentials, which makes them convenient for local hacking; GitHub/Gitea configuration works the same way as in the NixOS module, with secret paths pointing at plain local files.

VM integration test

The end-to-end NixOS test (fake GitHub + real Gitea) lives in checks/nixbot.nix:

nix build .#checks.x86_64-linux.nixbot -L

For interactive debugging:

nix build .#checks.x86_64-linux.nixbot.driverInteractive
./result/bin/nixos-test-driver

Add breakpoint() in the test script to pause execution.

Code quality

nix develop -c flake-fmt   # treefmt: ruff format, ruff check, mypy, nixfmt

SQL queries (sqlc)

SQL statements live in nixbot/nixbot/queries/*.sql and are compiled to typed asyncpg query functions in nixbot/nixbot/db_gen/ (generated code, do not edit) by sqlc with the sqlc-gen-better-python plugin; the schema is read from nixbot/nixbot/migrations/. After changing a query or migration, regenerate and commit the output:

nix develop -c sqlc generate

The dev shell’s sqlc is wrapped (nix/sqlc.nix) to use the same nix-pinned plugin as CI and works offline; when bumping the plugin, update the URL/checksum in both sqlc.yaml and nix/sqlc.nix.

CI verifies freshness via nix build .#checks.x86_64-linux.sqlc-generated. Dynamically assembled SQL (e.g. the build-list filters in web/queries.py) and the migration runner stay on raw asyncpg.

Migrating from buildbot-nix

Nixbot is a rewrite of buildbot-nix, its spiritual ancestor, and no longer runs on top of Buildbot. The old master/worker pair is now a single asyncio service that does everything itself: forge webhooks, evaluation with nix-eval-jobs, builds through the local nix daemon, commit statuses, hercules-ci-style effects, and its own web UI with a JSON API, live logs over SSE, and Prometheus metrics.

This guide covers moving an existing buildbot-nix deployment to nixbot.

What you need to do when upgrading

NixOS module. Import nixosModules.nixbot; options live under services.nixbot. Your existing services.buildbot-nix.master.* config keeps working: options rename to services.nixbot.* automatically and print deprecation warnings. nixosModules.buildbot-master and nixosModules.buildbot-worker are aliases for the new module. Options that have no equivalent anymore (workers, oauth2-proxy mode, dbUrl, the Gitea webhook secret) fail the build with a hint on what to do instead.

Workers are gone. Delete workersFile, worker passwords and localWorkers. Builds go through the nix daemon and scale with ordinary nix remote builders.

Database. PostgreSQL only, with a fresh schema and plain SQL migrations. Build history does not carry over. By default the module provisions a local PostgreSQL over the unix socket; for a remote database set database.url or database.urlFile.

Authentication. httpbasicauth and the oauth2-proxy accessMode.fullyPrivate setup are gone. The built-in GitHub/Gitea/OIDC login covers the same need: private repositories are hidden from anyone not authorized to see them. GitHub token mode is also gone; use a GitHub App.

authBackend is removed. Enable forges explicitly (github.enable, gitea.enable, oidc.enable); every enabled forge with OAuth credentials configured offers a login, and several can be active at once.

admins entries must be provider-qualified: github:Mic92, not Mic92. Unqualified entries never match and only log a warning.

OAuth callback URLs change: update your GitHub App / Gitea application to https://<domain>/auth/<provider>/callback (buildbot used /auth/login), e.g. https://buildbot.example.com/auth/github/callback.

Commit statuses / check runs. GitHub now receives Check Runs instead of commit statuses; Gitea/GitLab keep commit statuses. Names default to the nixbot/ prefix (nixbot/nix-eval, nixbot/nix-build ...). Set services.nixbot.statusContextPrefix = "buildbot" to keep the buildbot-era names so existing branch protection rules keep working (GitHub matches check-run names and commit-status contexts in the same required-checks namespace); otherwise update the required status checks on every repository. Grant the GitHub App the Checks: read & write permission and subscribe it to the Check run / Check suite events.

Webhooks. The old GitHub endpoint (/change_hook/github) still works as an alias. /change_hook/gitea is gone: legacy Gitea hooks carry old buildbot secrets that cannot match the per-repository secrets this service generates, and the service re-registers Gitea hooks against /webhooks/gitea automatically. However, per-repository GitHub webhooks are no longer created: events arrive through the App-level webhook. Enable the webhook on your GitHub App (Active, URL https://<domain>/webhooks/github, secret matching webhookSecretFile, events push, pull_request, check_run, check_suite); see docs/GITHUB.md. Subscribing to the pull_request event requires the “Pull requests: Read-only” repository permission. Adding a permission must be accepted on every installation of the app (your user account and each organization) under Settings → GitHub Apps → Configure. The service logs a warning at startup if the app is misconfigured.

Gitea webhooks now register at https://<domain>/webhooks/gitea with an auto-generated per-repository secret stored in the database — gitea.webhookSecretFile is gone. Existing hooks are re-synced in place, leftover buildbot-era hooks pointing at this instance are removed. Hooks subscribe to push, pull_request and pull_request_sync.

Project enablement. Which repositories get built is now a per-project toggle in the web UI (admins only). topic is reduced to a one-shot import: on first startup with an empty database, repositories carrying the topic are enabled; afterwards it is ignored. userAllowlist/repoAllowlist remain a hard boundary at discovery time.

Per-repository config. The file is now called nixbot.toml; the legacy buildbot-nix.toml name keeps working with unchanged contents.

Post-build steps. interpolate placeholders still work. Properties: attr, out_path, drv_path, system, project, branch, revision, pr_number, default_branch. Two changes:

  • PR builds no longer run under a refs/pull/N/merge branch — use %(prop:pr_number)s instead of parsing the branch name.
  • %(secret:NAME)s reads systemd credentials of the nixbot unit, so move LoadCredential entries from systemd.services.buildbot-master to systemd.services.nixbot.

Buildbot customizations. Anything that reached into Buildbot itself — services.buildbot-master.extraConfig, the manhole, pythonPackages — has no equivalent.

API. The JSON API moves to /api/*, with an OpenAPI schema at /openapi.json.

What you get compared to buildbot-nix

  • Builds are keyed by the post-merge tree hash: identical trees across branches and PRs reuse results instead of rebuilding.
  • Crash recovery: after a restart, unfinished builds resume from their stored eval results without re-evaluating.
  • Evaluation runs in a bwrap sandbox with a kernel-enforced memory cap (delegated cgroup v2 subtree).
  • Per-user API tokens for scripted access.
  • GitLab support (services.nixbot.gitlab): token-based, with per-repository webhooks and commit statuses. See docs/GITLAB.md.
  • Effects show up on the build page with status, live log and failure excerpt.
  • Effects parity with hercules-ci-agent: secretsMap selection with per-secret conditions, GitToken references, the current-task state API (getStateFile/putStateFile), per-effect mounts (effects.mountables), virtual uid/gid, a disk-backed /build, and a private untrusted nix daemon per effect (effects.extraNixOptions). Effects without secretsMap keep receiving the whole secrets file.