nix-community.nixvim/.github/workflows/update.yml
Matt Sturgeon 2b2b1e6d8f ci: rename update-scriptsci
This changes how we think about this directory; it does not need to be
exclusively for scripts related to updates, but should be a place for
any scripts intended to be run by CI workflows.

This mindset should make it easier to develop and test the business
logic of workflows, without always needing to test "in production" on
the nixvim repo or a fork.
2025-06-15 21:23:58 +00:00

347 lines
12 KiB
YAML

name: Update
on:
# Runs everyday at noon
schedule:
- cron: "0 12 * * *"
# Allow manual triggering
workflow_dispatch:
inputs:
update_lock:
type: boolean
default: true
description: Update flake.lock files
generate:
type: boolean
default: true
description: Update generated files
re_apply:
type: boolean
default: true
description: Re-apply additional commits from the PR
# Allow one concurrent update per branch
concurrency:
group: "update-${{ github.ref_name }}"
cancel-in-progress: true
# Allow pushing and creating PRs
permissions:
contents: write
pull-requests: write
jobs:
update:
name: Update the flake inputs and generate options
runs-on: ubuntu-latest
timeout-minutes: 40
if: github.event_name != 'schedule' || github.repository == 'nix-community/nixvim'
env:
repo: ${{ github.repository }}
base_branch: ${{ github.ref_name }}
pr_branch: update/${{ github.ref_name }}
workflow_run_id: ${{ github.run_id }}
workflow_run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps:
- name: Create GitHub App token
uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Get GitHub App user info
id: user-info
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
slug: ${{ steps.app-token.outputs.app-slug }}
run: |
name="$slug[bot]"
name_regex="$slug"'\[bot\]'
id=$(gh api "/users/$name" --jq .id)
{
echo "id=$id"
echo "name=$name"
echo "email=$id+$name@users.noreply.github.com"
echo 'author-regex=^'"$name_regex"' <'"$id+$name_regex"'@users\.noreply\.github\.com>$'
} >> "$GITHUB_OUTPUT"
- name: Configure git
env:
name: ${{ steps.user-info.outputs.name }}
email: ${{ steps.user-info.outputs.email }}
run: |
git config --global user.name "$name"
git config --global user.email "$email"
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ steps.app-token.outputs.token }}
- name: Create update branch
run: |
git branch -D "$pr_branch" || echo "Nothing to delete"
git switch -c "$pr_branch"
- name: Get info on the current PR
id: open_pr_info
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Query for info about the already open update PR
info=$(
gh api graphql -F owner='{owner}' -F repo='{repo}' -F branch="$pr_branch" -f query='
query($owner:String!, $repo:String!, $branch:String!) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 1, states: OPEN, headRefName: $branch) {
nodes {
number
url
}
}
}
}
' | jq --raw-output '
.data.repository.pullRequests.nodes[]
| to_entries[]
| "\(.key)=\(.value)"
'
)
if [[ -n "$info" ]]; then
echo "PR info:"
echo "$info"
echo "$info" >> $GITHUB_OUTPUT
else
echo "No PR is currently open"
fi
- name: Fetch current PR's branch
if: steps.open_pr_info.outputs.number
run: |
git fetch origin "$pr_branch"
git branch --set-upstream-to "origin/$pr_branch"
- name: Update flake.lock files
id: update_flake_lock
if: inputs.update_lock || github.event_name == 'schedule'
run: |
nix-build ./ci -A update
./result/bin/update --commit --github-output
- name: Update generated files
id: generate
if: inputs.generate || github.event_name == 'schedule'
run: |
old=$(git show --no-patch --format=%h)
nix-build ./ci -A generate
./result/bin/generate --commit
new=$(git show --no-patch --format=%h)
if [ "$old" != "$new" ]; then
body=$(git show --no-patch --format=%b)
echo "body<<EOF" >> "$GITHUB_OUTPUT"
if [ -n "$body" ]; then
# Multi-file changes are listed in the body
echo "$body" >> "$GITHUB_OUTPUT"
else
# Single-file changes are only in the summary,
# e.g. "generated: Updated none-ls.nix"
git show --no-patch --format=%s | \
sed -e 's/^generated:/-/' >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
fi
- name: Apply commits from the open PR
id: re_apply
if: (inputs.re_apply || github.event_name == 'schedule') && steps.open_pr_info.outputs.number
env:
author_regex: ${{ steps.user-info.outputs.author-regex }}
run: |
# The base is the most recent commit on the remote branch authored by nixvim-ci
# This should be a flake.lock bump or a generated-files update
# We will cherry-pick all commits on the remote _after_ the $base commit
remote="origin/$pr_branch"
base=$(git rev-list --author="$author_regex" --max-count=1 "$remote")
commits=( $(git rev-list --reverse "$base..$remote") )
if [[ -n "$commits" ]]; then
echo "Applying ${#commits[@]} commits..."
echo "count=${#commits[@]}" >> $GITHUB_OUTPUT
git cherry-pick \
--strategy-option=theirs \
--empty=drop \
"${commits[@]}"
else
echo "Nothing to re-apply"
fi
- name: Check if there are differences to push
id: diff
env:
pr_num: ${{ steps.open_pr_info.outputs.number }}
run: |
if [[ -n "$pr_num" ]]; then
remote="origin/$pr_branch"
else
remote="origin/$base_branch"
fi
diff=( $(git diff --cached --name-only "$remote") )
if [[ -n "$diff" ]]; then
echo "${#diff[@]} files different to $remote"
for file in "${diff[@]}"; do
echo "- $file"
done
echo "count=${#diff[@]}" >> $GITHUB_OUTPUT
else
echo "No files are different to $remote"
fi
- name: Create or Update Pull Request
id: updated_pr
if: steps.diff.outputs.count
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
pr_num: ${{ steps.open_pr_info.outputs.number }}
title: |
[${{ github.ref_name }}] Update flake.lock & generated files
root_lock: ${{ steps.update_flake_lock.outputs.root_lock_body }}
dev_lock: ${{ steps.update_flake_lock.outputs.dev_lock_body }}
generated: ${{ steps.generate.outputs.body }}
run: |
echo "Pushing to remote branch $pr_branch"
git push --force --set-upstream origin "$pr_branch"
echo "Writing PR body file"
(
if [[ -z "$root_lock$dev_lock$generated" ]]; then
echo '## No changes'
echo
fi
if [[ -n "$root_lock" ]]; then
echo '## Root lockfile'
echo '```'
echo "$root_lock"
echo '```'
echo
fi
if [[ -n "$dev_lock" ]]; then
echo '## Dev lockfile'
echo '```'
echo "$dev_lock"
echo '```'
echo
fi
if [[ -n "$generated" ]]; then
echo '## Generated files'
echo "$generated"
echo
fi
run_cmd='gh workflow run update.yml'
if [[ "$base_branch" != "main" ]]; then
run_cmd+=" --ref $base_branch"
fi
echo '---'
echo
echo -n 'This PR was most recently updated by workflow run '
echo "[$workflow_run_id]($workflow_run_url)."
echo
echo -n 'You can re-run the update by going to the '
echo -n '[workflow'"'"'s page](https://github.com/nix-community/nixvim/actions/workflows/update.yml) '
echo 'or by using the `gh` command:'
echo '```sh'
echo "$run_cmd"
echo '```'
echo
echo -n 'If needed, you can also specify workflow inputs on the command line, '
echo 'using the `-F --field`, `-f --raw-field`, or `--json` flags.'
echo 'See `gh workflow run --help`.'
echo
) > body.md
if [[ -n "$pr_num" ]]; then
echo "Editing existing PR #$pr_num"
operation=updated
gh pr edit "$pr_num" --body-file body.md
else
echo "Creating new PR"
operation=created
gh pr create \
--base "$base_branch" \
--title "$title" \
--body-file body.md
fi
pr_info=$(
# Get info from `gh pr view`
gh pr view --json 'headRefName,number,url' --jq '
to_entries[]
| .key |=
# Rename headRefName -> branch
if . == "headRefName" then "branch"
else . end
| "\(.key)=\(.value)"
'
# Get additional info locally
echo "head=$(git rev-parse HEAD)"
echo "operation=$operation"
)
echo "PR Info:"
echo "$pr_info"
echo "$pr_info" >> $GITHUB_OUTPUT
- name: Print summary
if: steps.updated_pr.outputs.number
env:
pr_num: ${{ steps.updated_pr.outputs.number }}
pr_url: ${{ steps.updated_pr.outputs.url }}
pr_branch: ${{ steps.updated_pr.outputs.branch }}
head: ${{ steps.updated_pr.outputs.head }}
operation: ${{ steps.updated_pr.outputs.operation }}
re_apply_count: ${{ steps.re_apply.outputs.count }}
run: |
short=${head:0:6}
# stdout
echo "${short} pushed to ${pr_branch}"
echo "#${pr_num} was ${operation}: ${pr_url}"
( # markdown summary
echo "## ${{ github.ref_name }}"
echo
echo "\`${short}\` pushed to \`${pr_branch}\`"
echo
echo "[#${pr_num}](${pr_url}) was ${operation}."
echo
if [[ -n "$re_apply_count" ]]; then
echo "Re-applied $re_apply_count commits from the existing PR."
fi
echo
) >> $GITHUB_STEP_SUMMARY
- name: Print cancellation summary
if: (!steps.updated_pr.outputs.number)
env:
pr_num: ${{ steps.open_pr_info.outputs.number }}
pr_url: ${{ steps.open_pr_info.outputs.url }}
changes: ${{ steps.diff.outputs.count || '0' }}
re_apply_count: ${{ steps.re_apply.outputs.count }}
run: |
(
echo "## Not updated"
echo
echo -n "$changes files with differences compared to "
if [[ -n "$pr_num" ]]; then
echo "[#$pr_num]($pr_url)."
else
echo "\`$base_branch\`"
fi
echo
if [[ -n "$re_apply_count" ]]; then
echo "Re-applied $re_apply_count commits from the existing PR."
fi
echo
) >> $GITHUB_STEP_SUMMARY