Created
January 22, 2026 08:36
-
-
Save phaer/70ad6674157589cfc54be6d86c9c30cc to your computer and use it in GitHub Desktop.
Periodic flake-updates for buildbot_effects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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