gitea: provision a ci-bot account with repo + branch-protection access
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>
This commit is contained in:
+101
-1
@@ -1,4 +1,4 @@
|
||||
{ config, ... }:
|
||||
{ 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
|
||||
@@ -9,6 +9,12 @@
|
||||
# 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;
|
||||
@@ -66,4 +72,98 @@
|
||||
"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}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user