Commit 3dd56e2
Changed files (7)
.forgejo
workflows
.forgejo/workflows/auto-update.yml
@@ -0,0 +1,35 @@
+name: "Auto update packages"
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "13 20 * * *"
+# See https://forgejo.org/docs/latest/user/api/authorized-integrations/
+enable-openid-connect: true
+env:
+ AUTHORIZED_INTEGRATION_AUDIENCE: "u:302663:cf6d5efd-a70c-4b33-a5af-d9b6eea3b0bd"
+jobs:
+ auto-update:
+ runs-on: nixos-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ # Do not persist the (read-only) automatic token; all git operations
+ # authenticate with the Authorized Integration JWT instead.
+ persist-credentials: false
+ - name: Fetch JWT for authorized integration
+ id: jwt
+ run: |
+ jwt=$(
+ curl --fail \
+ -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
+ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$AUTHORIZED_INTEGRATION_AUDIENCE" \
+ | jq -r .value
+ )
+ echo "::add-mask::$jwt"
+ echo "jwt=$jwt" >> "$FORGEJO_OUTPUT"
+ - name: Update packages and open PRs
+ env:
+ FORGEJO_TOKEN: ${{ steps.jwt.outputs.jwt }}
+ run: |
+ nix-shell shell.nix --run './scripts/update-ci.sh'
scripts/update-ci.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env bash
+# Update all packages with passthru.updateScript and open one PR per package.
+# Intended to run inside `nix-shell shell.nix` in CI (see
+# .forgejo/workflows/auto-update.yml). Expects FORGEJO_TOKEN to be set.
+set -euo pipefail
+
+FORGEJO_TOKEN="${FORGEJO_TOKEN:?FORGEJO_TOKEN not set}"
+
+# Some packages (e.g. harmonoid, navibeat) are unfree; nix-instantiate
+# refuses them without this.
+export NIXPKGS_ALLOW_UNFREE=1
+
+MAIN_BRANCH=$(git symbolic-ref --short HEAD)
+REPO_REMOTE_URL=$(git remote get-url origin)
+
+# In CI the token is a short-lived JWT minted for a Forgejo "Authorized
+# Integration" (see .forgejo/workflows/auto-update.yml). JWTs authenticate over
+# HTTP via an `Authorization: Bearer` header and cannot be embedded in the
+# remote URL, unlike a classic access token (local runs).
+if [[ "$FORGEJO_TOKEN" =~ ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ ]]; then
+ IS_JWT=1
+else
+ IS_JWT=0
+fi
+
+fj auth add-token "$FORGEJO_TOKEN"
+
+case "$REPO_REMOTE_URL" in
+https://*)
+ if [[ "$IS_JWT" -eq 1 ]]; then
+ git config --local http.extraHeader "Authorization: Bearer ${FORGEJO_TOKEN}"
+ else
+ git remote set-url origin "https://oauth2:${FORGEJO_TOKEN}@${REPO_REMOTE_URL#https://}"
+ fi
+ ;;
+esac
+
+git config user.name "nur-update-bot"
+git config user.email "nur-update-bot@users.noreply.git.net.trin.one"
+
+FAILED=""
+PENDING=""
+
+for PKG in $(./scripts/update-package --list | jq -r '.[]'); do
+ echo ""
+ echo "=== $PKG ==="
+
+ # Reset to a clean main state.
+ git checkout -f "$MAIN_BRANCH"
+ git checkout -- . 2>/dev/null || true
+ git clean -fd -- pkgs/ 2>/dev/null || true
+
+ # Skip if a PR for this package is already open (branch exists on origin).
+ if git ls-remote --exit-code origin "refs/heads/update/$PKG" >/dev/null 2>&1; then
+ echo ">>> $PKG: PR already open, skipping"
+ PENDING="$PENDING $PKG"
+ continue
+ fi
+
+ OLD_VERSION=$(nix-instantiate --eval --strict -A "$PKG.version" . 2>/dev/null | tr -d '"')
+
+ if ! ./scripts/update-package "$PKG"; then
+ echo ">>> $PKG: update script failed" >&2
+ FAILED="$FAILED $PKG"
+ continue
+ fi
+
+ if [ -z "$(git status --porcelain -- pkgs/)" ]; then
+ echo ">>> $PKG: no changes"
+ continue
+ fi
+
+ if ! nix-instantiate --show-trace -A "$PKG" . >/dev/null 2>&1; then
+ echo ">>> $PKG: evaluation failed after update" >&2
+ FAILED="$FAILED $PKG"
+ continue
+ fi
+
+ NEW_VERSION=$(nix-instantiate --eval --strict -A "$PKG.version" . 2>/dev/null | tr -d '"')
+
+ python3 ./scripts/gen-readme.py
+
+ git switch -c "update/$PKG"
+ git add -A
+ git commit -m "$PKG: $OLD_VERSION -> $NEW_VERSION"
+ git push --set-upstream origin "update/$PKG"
+ fj pr create "$PKG: $OLD_VERSION -> $NEW_VERSION" \
+ --body "Automated update by CI.
+
+$PKG: \`$OLD_VERSION\` -> \`$NEW_VERSION\`"
+done
+
+if [ -n "$PENDING" ]; then
+ echo ""
+ echo "Pending (PR already open):$PENDING"
+fi
+if [ -n "$FAILED" ]; then
+ echo ""
+ echo "Finished with failures:$FAILED" >&2
+ exit 1
+fi
+echo ""
+echo "All packages up to date."
scripts/update-package
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Run passthru.updateScript-based package updates (see scripts/update.nix).
+#
+# Usage:
+# ./scripts/update-package <attr-path> [<attr-path>...]
+# ./scripts/update-package --all
+# ./scripts/update-package --list
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+case "${1:-}" in
+--all)
+ exec nix-shell scripts/update.nix --argstr packages ""
+ ;;
+--list)
+ exec nix-instantiate --eval --json --strict \
+ -E 'map (p: p.attrPath) (import ./scripts/update-packages.nix {})'
+ ;;
+"")
+ echo "Usage: $0 <attr-path>... | --all | --list" >&2
+ exit 1
+ ;;
+*)
+ exec nix-shell scripts/update.nix --argstr packages "$*"
+ ;;
+esac
\ No newline at end of file
scripts/update-packages.nix
@@ -0,0 +1,121 @@
+/*
+Selection of packages with `passthru.updateScript`, shared by the
+runner (scripts/update.nix) and scripts/update-package --list.
+
+Usage:
+
+ nix-instantiate --eval --json --strict \
+ -E 'map (p: p.attrPath) (import ./scripts/update-packages.nix {})'
+*/
+{
+ pkgs ? import <nixpkgs> {},
+ # Space-separated list of package attribute paths to update.
+ # Empty string means "update everything".
+ packages ? "",
+}: let
+ inherit (pkgs) lib;
+ nurPkgs = import ../default.nix {inherit pkgs;};
+
+ updateScriptOf = pkg: let
+ script =
+ (
+ if pkg.passthru or null != null
+ then pkg.passthru
+ else {}
+ ).updateScript or null;
+ in
+ if script == null
+ then null
+ else if lib.isDerivation script
+ then [(toString script)]
+ else if lib.isAttrs script
+ then
+ if script ? command
+ then map toString (lib.toList script.command)
+ else null
+ else map toString (lib.toList script);
+
+ reservedNames = [
+ "lib"
+ "overlays"
+ "nixosModules"
+ "homeModules"
+ "darwinModules"
+ "flakeModules"
+ ];
+
+ collect = prefix: attrs: let
+ names = builtins.tryEval (builtins.attrNames attrs);
+ in
+ if !names.success
+ then []
+ else
+ builtins.concatLists (
+ map (
+ name: let
+ result = builtins.tryEval attrs.${name};
+ in
+ if !result.success
+ then []
+ else if lib.isDerivation result.value
+ then let
+ script = updateScriptOf result.value;
+ meta = result.value.meta or null;
+ position =
+ if meta != null
+ then meta.position or null
+ else null;
+ in
+ lib.optional (script != null && !(meta.nurRenamed or false) && !(meta.nurDeprecated or false)) {
+ inherit name;
+ attrPath = lib.concatStringsSep "." (prefix ++ [name]);
+ inherit script;
+ pname = result.value.pname or (lib.getName result.value);
+ pkgName = result.value.name;
+ oldVersion = result.value.version or "";
+ inherit position;
+ }
+ else if lib.isAttrs result.value
+ then collect (prefix ++ [name]) result.value
+ else []
+ )
+ names.value
+ );
+
+ allPackages = collect [] (lib.filterAttrs (name: _: !builtins.elem name reservedNames) nurPkgs);
+
+ deduplicated = let
+ isClean = p: !(p.meta.nurRenamed or false) && !(p.meta.nurDeprecated or false);
+ grouped =
+ lib.foldl (
+ acc: cur: let
+ key = builtins.hashString "sha256" (
+ lib.concatStringsSep "\n" (cur.script ++ [(toString cur.position)])
+ );
+ prev = acc.${key} or null;
+ keep =
+ prev
+ == null
+ || (isClean cur && !isClean prev)
+ || (isClean cur == isClean prev && lib.hasInfix "." cur.attrPath && !lib.hasInfix "." prev.attrPath);
+ in
+ acc
+ // {
+ ${key} =
+ if keep
+ then cur
+ else prev;
+ }
+ ) {}
+ allPackages;
+ in
+ map (k: grouped.${k}) (builtins.attrNames grouped);
+
+ matchesSelection = p: sel: p.attrPath == sel || lib.hasSuffix ".${sel}" p.attrPath;
+
+ selected =
+ if packages != ""
+ then lib.filter (p: lib.any (sel: matchesSelection p sel) (lib.splitString " " packages)) deduplicated
+ else deduplicated;
+in
+ selected
scripts/update.nix
@@ -0,0 +1,136 @@
+/*
+Runner for packages declaring `passthru.updateScript`, modeled after
+nixpkgs' maintainers/scripts/update.nix.
+
+Usage (also see scripts/update-package):
+
+ nix-shell scripts/update.nix --argstr packages "kelivo stinkpot"
+ nix-shell scripts/update.nix --argstr packages ""
+*/
+{
+ pkgs ? import <nixpkgs> {},
+ # Space-separated list of package attribute paths to update.
+ # Empty string means "update everything".
+ packages ? "",
+}: let
+ inherit (pkgs) lib;
+ selected = import ./update-packages.nix {inherit pkgs packages;};
+
+ invalidSelections = builtins.filter (sel: !lib.any (p: p.attrPath == sel || lib.hasSuffix ".${sel}" p.attrPath) selected) (
+ lib.filter (s: s != "") (lib.splitString " " packages)
+ );
+
+ packagesJson = pkgs.writeText "nur-update-packages.json" (
+ builtins.toJSON (
+ map (p: {
+ inherit (p) attrPath oldVersion;
+ inherit (p) pname;
+ name = p.pkgName;
+ updateScript = p.script;
+ })
+ selected
+ )
+ );
+
+ helpText = ''
+ No packages with passthru.updateScript were selected.
+ '';
+in
+ pkgs.stdenvNoCC.mkDerivation {
+ name = "nur-update-script";
+ nativeBuildInputs = [
+ pkgs.git
+ pkgs.cacert
+ pkgs.jq
+ ];
+ buildCommand = ''
+ echo ""
+ echo "Not possible to run update scripts using nix-build."
+ echo ""
+ echo "${lib.replaceStrings ["\n"] [" "] helpText}"
+ exit 1
+ '';
+ shellHook = ''
+ unset shellHook
+
+ ${lib.optionalString (invalidSelections != []) ''
+ echo "WARNING: no updateScript found for: ${lib.concatStringsSep " " invalidSelections}" >&2
+ ''}
+
+ ${lib.optionalString (selected == []) ''
+ echo "${lib.replaceStrings ["\n"] [" "] helpText}" >&2
+ exit 1
+ ''}
+
+ FAILED=""
+ LOGDIR=$(mktemp -d)
+ trap 'rm -rf "$LOGDIR"' EXIT
+ cd "${toString ./.}/.."
+
+ MAX_JOBS="''${UPDATE_PACKAGE_JOBS:-$(nproc)}"
+ declare -A JOB_ATTR=() JOB_LOG=()
+ RUNNING=0
+
+ launch() {
+ local PKG="$1" ATTR_PATH OLD_VERSION LOG
+ ATTR_PATH=$(jq -r .attrPath <<<"$PKG")
+ OLD_VERSION=$(jq -r .oldVersion <<<"$PKG")
+ LOG=$(mktemp "$LOGDIR/log.XXXXXX")
+ echo ">>> $ATTR_PATH: updating ($OLD_VERSION)"
+ (
+ if env \
+ UPDATE_NIX_ATTR_PATH="$ATTR_PATH" \
+ UPDATE_NIX_PNAME="$(jq -r .pname <<<"$PKG")" \
+ UPDATE_NIX_NAME="$(jq -r .name <<<"$PKG")" \
+ UPDATE_NIX_OLD_VERSION="$OLD_VERSION" \
+ bash -c "$(jq -r '.updateScript | map(@sh) | join(" ")' <<<"$PKG")"; then
+ echo ">>> $ATTR_PATH: done"
+ exit 0
+ else
+ echo ">>> $ATTR_PATH: FAILED" >&2
+ exit 1
+ fi
+ ) >"$LOG" 2>&1 &
+ JOB_ATTR[$!]="$ATTR_PATH"
+ JOB_LOG[$!]="$LOG"
+ RUNNING=$((RUNNING + 1))
+ }
+
+ reap_one() {
+ local PID STATUS ATTR_PATH LOG
+ PID=""
+ STATUS=0
+ # Requires bash >= 5.1 for `wait -n -p`
+ wait -n -p PID || STATUS=$?
+ ATTR_PATH="''${JOB_ATTR[$PID]}"
+ LOG="''${JOB_LOG[$PID]}"
+ echo ""
+ cat "$LOG"
+ rm -f "$LOG"
+ if [ "$STATUS" -ne 0 ]; then
+ FAILED="$FAILED $ATTR_PATH"
+ fi
+ unset "JOB_ATTR[$PID]" "JOB_LOG[$PID]"
+ RUNNING=$((RUNNING - 1))
+ }
+
+ while IFS= read -r PKG; do
+ while [ "$RUNNING" -ge "$MAX_JOBS" ]; do
+ reap_one
+ done
+ launch "$PKG"
+ done < <(jq -c '.[]' ${packagesJson})
+
+ while [ "$RUNNING" -gt 0 ]; do
+ reap_one
+ done
+
+ echo ""
+ if [ -n "$FAILED" ]; then
+ echo "Finished with failures:$FAILED" >&2
+ exit 1
+ fi
+ echo "All update scripts finished successfully."
+ exit 0
+ '';
+ }
justfile
@@ -2,3 +2,8 @@
# Generate latest packages list and write to README.md
gen-readme:
python3 ./scripts/gen-readme.py
+
+# Run passthru.updateScript-based package updates
+# Usage: just update-pkg [--all | <attr-path>...]
+update-pkg *args:
+ ./scripts/update-package {{args}}
shell.nix
@@ -1,6 +1,9 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShellNoCC {
packages = with pkgs; [
+ forgejo-cli
+ jq
nurl
+ python3
];
}