Skip to content

Instantly share code, notes, and snippets.

@phaer
Created January 22, 2026 08:36
Show Gist options
  • Select an option

  • Save phaer/70ad6674157589cfc54be6d86c9c30cc to your computer and use it in GitHub Desktop.

Select an option

Save phaer/70ad6674157589cfc54be6d86c9c30cc to your computer and use it in GitHub Desktop.
Periodic flake-updates for buildbot_effects
# Flake Update Effect for Forgejo
# Written with buildbot_effects in mind, as hercule-ci-effects flake-update is nice but leans heavily into using flake parts and it's module system.
# We keep it minimal here>
#
# 1. Runs `nix flake update`
# 2. Commits changes to a branch
# 3. Opens a Pull Request on Forgejo
#
# # Example
# ```nix
# herculesCI = args: {
# onSchedule.flake-update = {
# when = { hour = 4; minute = 0; dayOfWeek = [ "Mon" ]; };
# outputs.effects.update = withSystem "x86_64-linux" ({ pkgs, hci-effects, ... }:
# import ./flake-update.nix { inherit hci-effects pkgs args; } {}
# );
# };
# };
# ```
{
hci-effects,
pkgs,
args,
}:
{
baseBranch ? "main",
updateBranch ? "flake-update",
prTitle ? "flake.lock: Update",
prBody ? "Automated flake.lock update",
secretName ? "forgejo",
gitUser ? "Flake Update Bot",
gitEmail ? "bot@example.com",
inputs ? null, # null = update all inputs, or list of input names
}:
let
# Parse remoteHttpUrl: "https://forgejo.example.com/owner/repo.git"
# Also handles URLs with username: "https://user@forgejo.example.com/owner/repo.git"
url = args.primaryRepo.remoteHttpUrl;
cleanUrl = builtins.replaceStrings [ ".git" ] [ "" ] url;
# Split by "/" - builtins.split returns a list with matches and separators
# For "https://forgejo.example.com/owner/repo" this gives:
# ["https:" "" "forgejo.example.com" "owner" "repo"]
parts = builtins.filter (x: x != "" && builtins.isString x) (builtins.split "/" cleanUrl);
hostPart = builtins.elemAt parts 1;
# Strip username@ prefix if present (e.g., "forgejo@git.example.com" -> "git.example.com")
hostSplit = builtins.filter builtins.isString (builtins.split "@" hostPart);
host = builtins.elemAt hostSplit (builtins.length hostSplit - 1);
forgejoUrl = "https://${host}";
owner = builtins.elemAt parts 2;
repo = builtins.elemAt parts 3;
inputArgs =
if inputs == null then
""
else
builtins.concatStringsSep " " (map (i: "--update-input ${i}") inputs);
effectScript = ''
set -euo pipefail
# Read Forgejo token from secrets
if [[ ! -f /run/secrets.json ]]; then
echo "Error: /run/secrets.json not found"
exit 1
fi
FORGEJO_TOKEN=$(jq -r '.${secretName}.data.token // empty' /run/secrets.json)
if [[ -z "$FORGEJO_TOKEN" ]]; then
echo "Error: Forgejo token not found in secrets.json under '${secretName}.data.token'"
exit 1
fi
WORK_DIR=$(mktemp -d)
cd "$WORK_DIR"
trap 'rm -rf "$WORK_DIR"' EXIT
echo "Cloning repository..."
REPO_URL="${forgejoUrl}/${owner}/${repo}.git"
AUTH_URL=$(echo "$REPO_URL" | sed "s|https://|https://oauth2:$FORGEJO_TOKEN@|")
git clone --depth 1 --branch "${baseBranch}" "$AUTH_URL" repo
cd repo
git config user.name "${gitUser}"
git config user.email "${gitEmail}"
ORIGINAL_HASH=""
if [[ -f flake.lock ]]; then
ORIGINAL_HASH=$(sha256sum flake.lock | cut -d' ' -f1)
fi
echo "Running nix flake update..."
nix --extra-experimental-features "nix-command flakes" flake update ${inputArgs}
NEW_HASH=""
if [[ -f flake.lock ]]; then
NEW_HASH=$(sha256sum flake.lock | cut -d' ' -f1)
fi
if [[ "$ORIGINAL_HASH" == "$NEW_HASH" ]]; then
echo "No changes to flake.lock, nothing to do"
exit 0
fi
echo "flake.lock has changed, creating PR..."
git checkout -B "${updateBranch}"
git add flake.lock
git commit -m "${prTitle}"
git push --force-with-lease origin "${updateBranch}"
API_URL="${forgejoUrl}/api/v1"
EXISTING_PR=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_URL/repos/${owner}/${repo}/pulls?state=open&head=${owner}:${updateBranch}" \
| jq 'length')
if [[ "$EXISTING_PR" -gt 0 ]]; then
echo "PR already exists for branch '${updateBranch}', updated with new commits"
exit 0
fi
echo "Creating Pull Request..."
PR_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg title "${prTitle}" \
--arg body "${prBody}" \
--arg head "${updateBranch}" \
--arg base "${baseBranch}" \
'{title: $title, body: $body, head: $head, base: $base}')" \
"$API_URL/repos/${owner}/${repo}/pulls")
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number // empty')
if [[ -n "$PR_NUMBER" ]]; then
echo "Created PR #$PR_NUMBER: ${forgejoUrl}/${owner}/${repo}/pulls/$PR_NUMBER"
else
echo "Failed to create PR. Response:"
echo "$PR_RESPONSE" | jq .
exit 1
fi
'';
in
hci-effects.mkEffect {
inherit effectScript;
inputs = [
pkgs.git
pkgs.jq
pkgs.curl
pkgs.nix
pkgs.coreutils
];
secretsMap.${secretName} = secretName;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment