diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 416c246..36d9200 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ on: - main jobs: - check: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -14,3 +14,23 @@ jobs: - name: build run: nix-build -A ci + + test-update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v26 + + - name: test update script + run: | + nix-build -A autoPrUpdate + { + result/bin/auto-pr-update . + echo "" + echo '```diff' + git diff + echo '```' + } > $GITHUB_STEP_SUMMARY + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 2e74f74..02fd83a 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -18,6 +18,8 @@ jobs: run: | nix-build repo -A autoPrUpdate result/bin/auto-pr-update repo > body + env: + GH_TOKEN: ${{ github.token }} - name: Create Pull Request uses: peter-evans/create-pull-request@v6 diff --git a/default.nix b/default.nix index 0004bf5..19747ab 100644 --- a/default.nix +++ b/default.nix @@ -11,6 +11,7 @@ let config = {}; overlays = []; }; + inherit (pkgs) lib; runtimeExprPath = ./src/eval.nix; testNixpkgsPath = ./tests/mock-nixpkgs.nix; @@ -32,8 +33,12 @@ let results = { - build = pkgs.callPackage ./package.nix { - inherit nixpkgsLibPath initNix runtimeExprPath testNixpkgsPath; + # We're using this value as the root result. By default, derivations expose all of their + # internal attributes, which is very messy. We prevent this using lib.lazyDerivation + build = lib.lazyDerivation { + derivation = pkgs.callPackage ./package.nix { + inherit nixpkgsLibPath initNix runtimeExprPath testNixpkgsPath; + }; }; shell = pkgs.mkShell { @@ -48,31 +53,61 @@ let }; # Run regularly by CI and turned into a PR - autoPrUpdate = pkgs.writeShellApplication { - name = "auto-pr-update"; - runtimeInputs = with pkgs; [ - npins - cargo - ]; - text = - let - commands = { - "npins changes" = '' - npins update --directory "$REPO_ROOT/npins"''; - "cargo changes" = '' - cargo update --manifest-path "$REPO_ROOT/Cargo.toml"''; + autoPrUpdate = + let + updateScripts = { + npins = pkgs.writeShellApplication { + name = "update-npins"; + runtimeInputs = with pkgs; [ + npins + ]; + text = '' + echo "
npins changes" + # Needed because GitHub's rendering of the first body line breaks down otherwise + echo "" + echo '```' + npins update --directory "$1/npins" 2>&1 + echo '```' + echo "
" + ''; }; - in - '' - REPO_ROOT=$1 - echo "Run automated updates" - '' - + pkgs.lib.concatStrings (pkgs.lib.mapAttrsToList (title: command: '' - echo -e '
${title}\n\n```' - ${command} 2>&1 - echo -e '```\n
' - '') commands); - }; + cargo = pkgs.writeShellApplication { + name = "update-cargo"; + runtimeInputs = with pkgs; [ + cargo + ]; + text = '' + echo "
cargo changes" + # Needed because GitHub's rendering of the first body line breaks down otherwise + echo "" + echo '```' + cargo update --manifest-path "$1/Cargo.toml" 2>&1 + echo '```' + echo "
" + ''; + }; + githubActions = pkgs.writeShellApplication { + name = "update-github-actions"; + runtimeInputs = with pkgs; [ + dependabot-cli + jq + github-cli + ]; + text = builtins.readFile ./scripts/update-github-actions.sh; + }; + }; + in + pkgs.writeShellApplication { + name = "auto-pr-update"; + text = '' + # Prevent impurities + unset PATH + ${lib.concatMapStringsSep "\n" (script: '' + echo >&2 "Running ${script}" + ${lib.getExe script} "$1" + '') (lib.attrValues updateScripts)} + ''; + }; # Tests the tool on the pinned Nixpkgs tree, this is a good sanity check nixpkgsCheck = pkgs.runCommand "test-nixpkgs-check-by-name" { diff --git a/scripts/update-github-actions.sh b/scripts/update-github-actions.sh new file mode 100755 index 0000000..b8ec5e5 --- /dev/null +++ b/scripts/update-github-actions.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# This script calls the dependabot CLI (https://github.com/dependabot/cli) +# to determine updates to GitHub Action dependencies in the local repository. +# It then also applies the updates and outputs the results to standard output. + +set -euo pipefail + +REPO_ROOT=$1 + +echo -e "
GitHub Action updates\n\n" + +# CI sets the GH_TOKEN env var, which `gh auth token` defaults to if set +githubToken=$(gh auth token) + +# Each dependabot update call tries to update all dependencies, +# but it outputs individual results for each dependency, +# with the intention of creating a PR for each. +# +# We want to have all changes together though, +# so we'd need to merge updates of the same files together, +# which could cause merge conflicts, no good. +# +# Instead, we run dependabot repeatedly, +# each time only taking the first dependency update and updating the files with it, +# such that the next iteration takes into account the previous updates. +# We do this until there's no more dependencies to be updated, +# at which point --exit-status will make jq return with a non-zero exit code. +# +# This does mean that dependabot internally needs to perform O(n^2) updates, +# but this isn't a problem in practice, since we run these updates regularly, +# so n is low. +while + # Unused argument would be the remote GitHub repo, which is not used if we pass --local + create_pull_request=$(LOCAL_GITHUB_ACCESS_TOKEN="$githubToken" \ + dependabot update github_actions this-argument-is-unused --local "$REPO_ROOT" \ + | jq --exit-status --compact-output --slurp 'map(select(.type == "create_pull_request")) | .[0].data') +do + title=$(jq --exit-status --raw-output '."pr-title"' <<< "$create_pull_request") + echo "
$title" + + # Needed because GitHub's rendering of the first body line breaks down otherwise + echo "" + + jq --exit-status --raw-output '."pr-body"' <<< "$create_pull_request" + echo '
' + + jq --compact-output '."updated-dependency-files"[]' <<< "$create_pull_request" \ + | while read -r fileUpdate; do + file=$(jq --exit-status --raw-output '.name' <<< "$fileUpdate") + # --join-output makes sure to not output a trailing newline + jq --exit-status --raw-output --join-output '.content' <<< "$fileUpdate" > "$REPO_ROOT/$file" + done +done + +echo -e "
"