Workflows push as a dedicated ci-bot account rather than a human one, so its PAT can be scoped, rotated and revoked on its own. Adding a repo to `ciBotRepos` and redeploying is all it takes to grant access. Collaborator access and branch-protection push-whitelisting exist only on gitea's HTTP API — no CLI, no config-file surface — so this one part stays imperative: a oneshot that PUT/PATCHes the API into the desired state. It runs on deploys where the script changed, which means it won't self-heal a revert done through the web UI unless the unit is restarted too. Two secrets, deliberately distinct: - gitea_provisioning_token is darman's own token (write:repository + write:user). Only an owner-scoped token clears reqOwnerCheck on the collaborator and branch-protection endpoints, and write:user is what lets it write the Actions secret below. ci-bot cannot grant itself access. - gitea_ci_bot_token is ci-bot's push token, generated once by hand (the command is in the comment) and pushed into gitea as a user-level Actions secret CI_BOT_TOKEN. Gitea has no instance-wide secret scope, and every repo here is owned by darman directly rather than an org, so a user-level secret is the closest thing — repo-level lookups fall back to it. Branch protection is applied to the default branch plus `develop`, since version-bump.yml pushes there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
170 lines
7.5 KiB
Nix
170 lines
7.5 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
# Gitea — self-hosted git. stateDir/repositories were migrated from the old
|
|
# ZimaOS docker instance straight into stateDir's default layout, so no
|
|
# import step is needed — just chown it to the gitea user after first deploy
|
|
# (currently darman:users from the CIFS copy):
|
|
# chown -R gitea:gitea /mnt/data/AppData/gitea
|
|
#
|
|
# HTTP is reverse-proxied through Caddy (hosts/jupiter/configuration.nix).
|
|
# SSH uses gitea's own built-in server on :2222 (not the host's :22, and not
|
|
# :222 — the unpriv gitea user can't bind <1024).
|
|
let
|
|
# Repos where the ci-bot account (see below) should be a Write collaborator
|
|
# and whitelisted to push past branch protection. Add a repo here and
|
|
# redeploy — no manual UI clicking needed.
|
|
ciBotRepos = [ "darman/hypr-chrome" ];
|
|
in
|
|
{
|
|
services.gitea = {
|
|
enable = true;
|
|
stateDir = "/mnt/data/AppData/gitea";
|
|
lfs.enable = true;
|
|
|
|
settings = {
|
|
repository = {
|
|
DEFAULT_BRANCH = "master";
|
|
};
|
|
server = {
|
|
DOMAIN = "git.mgaction.town";
|
|
SSH_DOMAIN = "git.mgaction.town";
|
|
# https, not http: neptun's Caddy terminates TLS for this name. Gitea
|
|
# builds its absolute URLs (clone buttons, redirects, webhooks) from
|
|
# ROOT_URL, so an http:// value hands out downgraded links.
|
|
ROOT_URL = "https://git.mgaction.town/";
|
|
HTTP_PORT = 3000;
|
|
START_SSH_SERVER = true;
|
|
SSH_PORT = 2222;
|
|
SSH_LISTEN_PORT = 2222;
|
|
};
|
|
service = {
|
|
DISABLE_REGISTRATION = true;
|
|
};
|
|
actions = {
|
|
ENABLED = true;
|
|
};
|
|
};
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = [ 2222 ];
|
|
|
|
users.users.gitea.extraGroups = [ "users" ];
|
|
|
|
# Runner instance registered against this same gitea. Jobs run in containers
|
|
# (podman, via services/containers.nix — already enabled on jupiter), one
|
|
# image per requested `runs-on` label using the catthehacker act-compatible
|
|
# images (same ones upstream `act`/Forgejo docs recommend).
|
|
#
|
|
# tokenFile points at an env file rendered by sops (TOKEN=<registration
|
|
# token>, see hosts/jupiter/secrets.nix) rather than a plain `token`, so the
|
|
# secret never lands in the Nix store. The registration token itself is NOT
|
|
# generated by this module — it comes from gitea once Actions is enabled:
|
|
# su gitea -s /bin/sh -c \
|
|
# 'GITEA_WORK_DIR=/mnt/data/AppData/gitea gitea actions generate-runner-token'
|
|
# then written into secrets/jupiter.yaml as gitea_runner_token.
|
|
services.gitea-actions-runner.instances.jupiter = {
|
|
enable = true;
|
|
name = "jupiter";
|
|
url = "https://git.mgaction.town/";
|
|
tokenFile = config.sops.templates."gitea-runner.env".path;
|
|
labels = [
|
|
"ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-latest"
|
|
"ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04"
|
|
];
|
|
};
|
|
|
|
# ci-bot: dedicated account CI workflows push as (kept separate from any
|
|
# human account so its own PAT can be scoped/rotated/revoked independently).
|
|
# Collaborator access + branch-protection push-whitelisting have no CLI or
|
|
# config-file surface in gitea — only the HTTP API — so this is the one
|
|
# part of the setup that stays imperative even though it's nix-triggered:
|
|
# a oneshot that PUTs/PATCHes the API into the desired state on every
|
|
# deploy where its script changed (adding a repo to `ciBotRepos` and
|
|
# redeploying is enough to pick it up; it won't self-heal a manual revert
|
|
# done via the web UI unless the unit is also restarted).
|
|
#
|
|
# Auth for those API calls is darman's OWN token (named
|
|
# "jupiter-ci-bot-provisioning" in gitea, scopes write:repository +
|
|
# write:user — see hosts/jupiter/secrets.nix), since darman owns the repos
|
|
# in ciBotRepos and only an owner-scoped token clears the reqOwnerCheck on
|
|
# the collaborator/branch-protection endpoints; write:user is additionally
|
|
# needed to push ci-bot's token below as a secret on darman's own account.
|
|
# It is NOT ci-bot's own push token — ci-bot can't grant itself access.
|
|
#
|
|
# ci-bot's own push token (separate secret, ci_bot_token) is generated
|
|
# once via:
|
|
# su gitea -s /bin/sh -c \
|
|
# 'GITEA_WORK_DIR=/mnt/data/AppData/gitea gitea admin user generate-access-token \
|
|
# --username ci-bot --scopes write:repository'
|
|
# and this service pushes it into gitea itself as a user-level Actions
|
|
# secret (CI_BOT_TOKEN, on darman's account — see the PUT below) so
|
|
# workflows in ciBotRepos can push as ci-bot without a per-repo secret.
|
|
systemd.services.gitea-ci-bot-provision = {
|
|
description = "Provision ci-bot gitea account + repo access";
|
|
after = [ "gitea.service" ];
|
|
requires = [ "gitea.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [ pkgs.curl pkgs.jq config.services.gitea.package ];
|
|
environment = {
|
|
TOKEN_FILE = config.sops.secrets.gitea_provisioning_token.path;
|
|
CI_BOT_TOKEN_FILE = config.sops.secrets.gitea_ci_bot_token.path;
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = config.services.gitea.user;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
api=http://127.0.0.1:${toString config.services.gitea.settings.server.HTTP_PORT}/api/v1
|
|
admin_token="$(cat "$TOKEN_FILE")"
|
|
auth=(-H "Authorization: token $admin_token")
|
|
|
|
for _ in $(seq 1 30); do
|
|
curl -fs "$api/version" >/dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
|
|
if ! curl -fs "''${auth[@]}" "$api/users/ci-bot" >/dev/null 2>&1; then
|
|
GITEA_WORK_DIR=${config.services.gitea.stateDir} gitea admin user create \
|
|
--username ci-bot \
|
|
--email ci-bot@${config.services.gitea.settings.server.DOMAIN} \
|
|
--random-password --must-change-password=false
|
|
fi
|
|
|
|
# No instance-wide secret scope exists in Gitea (it's an open feature
|
|
# request) - a user-level secret on darman's own account is the closest
|
|
# equivalent, since every repo below is owned directly by darman, not
|
|
# an org, and repo-level secrets fall back to user-level when unset.
|
|
ci_bot_token="$(cat "$CI_BOT_TOKEN_FILE")"
|
|
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
|
-X PUT "$api/user/actions/secrets/CI_BOT_TOKEN" \
|
|
-d "$(jq -n --arg data "$ci_bot_token" '{data: $data}')"
|
|
|
|
${lib.concatMapStringsSep "\n" (repo: ''
|
|
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
|
-X PUT "$api/repos/${repo}/collaborators/ci-bot" \
|
|
-d '{"permission":"write"}'
|
|
|
|
default_branch="$(curl -fs "''${auth[@]}" "$api/repos/${repo}" | jq -r .default_branch)"
|
|
|
|
# ci-bot needs push access on every branch a workflow might commit
|
|
# back to (currently just `develop`, where version-bump.yml pushes),
|
|
# in addition to whatever the repo's actual default branch is.
|
|
branches="$(printf '%s\n' "$default_branch" develop | sort -u)"
|
|
for branch in $branches; do
|
|
if curl -fs "''${auth[@]}" "$api/repos/${repo}/branch_protections/$branch" >/dev/null 2>&1; then
|
|
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
|
-X PATCH "$api/repos/${repo}/branch_protections/$branch" \
|
|
-d '{"enable_push":true,"enable_push_whitelist":true,"push_whitelist_usernames":["ci-bot"]}'
|
|
else
|
|
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
|
-X POST "$api/repos/${repo}/branch_protections" \
|
|
-d "{\"branch_name\":\"$branch\",\"enable_push\":true,\"enable_push_whitelist\":true,\"push_whitelist_usernames\":[\"ci-bot\"]}"
|
|
fi
|
|
done
|
|
'') ciBotRepos}
|
|
'';
|
|
};
|
|
}
|