Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/actions/setup-nvim/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Set up Neovim
description: >-
Install Neovim on a Windows runner and point it at this config with empty data, state and cache directories.
Check the repo out with `path: nvim` first, since Neovim reads $XDG_CONFIG_HOME/nvim.

inputs:
version:
description: Neovim release to install
default: 0.11.4 # keep in sync with the Neovim you run locally

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pinned Neovim version now lives here, but .github/agents/pr-reviewer.agent.md (line 28) still tells reviewers to find it in .github/workflows/ci.yml, which no longer has one. Point that line at .github/actions/setup-nvim/action.yml, or reviewers can't tell which Neovim API level to check against.


runs:
using: composite
steps:
- name: Isolate Neovim directories
shell: pwsh
run: |
"XDG_CONFIG_HOME=$env:GITHUB_WORKSPACE" >> $env:GITHUB_ENV
foreach ($kind in 'DATA', 'STATE', 'CACHE') {
"XDG_${kind}_HOME=$(Join-Path $env:RUNNER_TEMP "xdg\$($kind.ToLower())")" >> $env:GITHUB_ENV
}

- name: Install Neovim ${{ inputs.version }}
shell: pwsh
env:
NVIM_VERSION: ${{ inputs.version }}
run: |
$zip = Join-Path $env:RUNNER_TEMP nvim-win64.zip
Invoke-WebRequest "https://github.com/neovim/neovim/releases/download/v$env:NVIM_VERSION/nvim-win64.zip" -OutFile $zip
Expand-Archive $zip -DestinationPath $env:RUNNER_TEMP
$bin = Join-Path $env:RUNNER_TEMP nvim-win64\bin
& "$bin\nvim.exe" --version
$bin >> $env:GITHUB_PATH

- name: Provide make
# telescope-fzf-native and LuaSnip build with `make`; the runner image only ships mingw32-make.
shell: pwsh
run: |
if (Get-Command make -ErrorAction SilentlyContinue) { exit 0 }
$mingw = Get-Command mingw32-make -ErrorAction SilentlyContinue
if ($mingw) {
$bin = Join-Path $env:RUNNER_TEMP make-bin
New-Item -ItemType Directory $bin -Force | Out-Null
Copy-Item $mingw.Source (Join-Path $bin make.exe)
$bin >> $env:GITHUB_PATH
} else {
choco install make --no-progress -y
}
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Personal Neovim config (Lua, lazy.nvim), used on Windows with pwsh. CI checks ar
- Name branches `simonk/dev/<what-youre-doing>` in kebab-case, e.g. `simonk/dev/add-smoke-assertions`. If a tool manages the local branch name, push with `git push -u origin HEAD:simonk/dev/<what-youre-doing>` and open the PR from that branch.
- Commit and push only as the personal `SimonKimDev` GitHub account, never a work account.
- Merge PRs with a merge commit, not squash or rebase. `.git-blame-ignore-revs` lists commit SHAs that must stay on `main`.
- Don't reuse `simonk/dev/plugin-update`; the plugin update workflow owns it.

## Checks

Expand Down
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2
updates:
# lazy.nvim plugins aren't a Dependabot ecosystem; .github/workflows/plugin-update.yml updates lazy-lock.json.
- package-ecosystem: github-actions
directories:
- /
- /.github/actions/*
exclude-paths:
- "*.lock.yml" # generated by `gh aw compile`; refresh those with `gh aw upgrade` instead
schedule:
interval: monthly
groups:
actions:
patterns: ["*"]
commit-message:
prefix: ci
81 changes: 81 additions & 0 deletions .github/scripts/plugin-update.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
-- Update every plugin, rewrite lazy-lock.json and write a Markdown summary for the update PR.
-- Run by .github/workflows/plugin-update.yml, after the setup-nvim action isolated Neovim's directories:
-- nvim --headless "+luafile .github/scripts/plugin-update.lua"
-- Env: OLD_LOCKFILE is a copy of lazy-lock.json from before the update; the summary goes to SUMMARY_FILE.

local function read_json(path)
return vim.json.decode(table.concat(vim.fn.readfile(path), "\n"))
end

local function short(sha)
return "`" .. sha:sub(1, 7) .. "`"
end

local function update()
local Config = require("lazy.core.config")
local Plugin = require("lazy.core.plugin")

require("lazy").update({ wait = true, show = false })

local old, new = read_json(vim.env.OLD_LOCKFILE), read_json(Config.options.lockfile)
local names = vim.tbl_keys(vim.tbl_extend("force", {}, old, new))
table.sort(names)

local rows = {}
for _, name in ipairs(names) do
local from, to = old[name] and old[name].commit, new[name] and new[name].commit
if from ~= to then
local plugin = Config.plugins[name]
local repo = plugin and plugin.url and (plugin.url:gsub("%.git$", ""))
local change
if not from then
change = "added at " .. short(to)
elseif not to then
change = "removed"
elseif repo and repo:match("^https://github%.com/") then
change = ("[%s...%s](%s/compare/%s...%s)"):format(short(from), short(to), repo, from, to)
else
change = short(from) .. "..." .. short(to)
end
table.insert(rows, ("| %s | %s |"):format(name, change))
end
end

local failed = {}
for name, plugin in pairs(Config.plugins) do
if Plugin.has_errors(plugin) then
table.insert(failed, name)
end
end
table.sort(failed)

local run = ("%s/%s/actions/runs/%s"):format(
vim.env.GITHUB_SERVER_URL or "https://github.com",
vim.env.GITHUB_REPOSITORY or "",
vim.env.GITHUB_RUN_ID or ""
)
local lines = {
("Weekly `:Lazy update` from the [plugin update workflow](%s)."):format(run),
"CI installs these exact commits into empty directories and runs the smoke test; merge if it's green.",
"",
"| Plugin | Change |",
"| --- | --- |",
}
vim.list_extend(lines, rows)
if #failed > 0 then
vim.list_extend(lines, {
"",
"> [!WARNING]",
"> These plugins reported errors while updating (see the workflow log): " .. table.concat(failed, ", "),
})
end
vim.fn.writefile(lines, vim.env.SUMMARY_FILE)

io.stdout:write(("\n%d plugin(s) changed, %d with errors\n"):format(#rows, #failed))
end

local ok, err = xpcall(update, debug.traceback)
if not ok then
io.stdout:write("\nplugin update failed:\n" .. err .. "\n")
end
vim.cmd(ok and "qa!" or "cquit 1")
32 changes: 1 addition & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ jobs:
name: Startup smoke test (Windows)
runs-on: windows-latest
timeout-minutes: 20
env:
NVIM_VERSION: 0.11.4 # keep in sync with the Neovim you run locally
defaults:
run:
shell: pwsh
Expand All @@ -39,35 +37,7 @@ jobs:
with:
path: nvim # Neovim reads $XDG_CONFIG_HOME/nvim

- name: Isolate Neovim directories
run: |
"XDG_CONFIG_HOME=$env:GITHUB_WORKSPACE" >> $env:GITHUB_ENV
foreach ($kind in 'DATA', 'STATE', 'CACHE') {
"XDG_${kind}_HOME=$(Join-Path $env:RUNNER_TEMP "xdg\$($kind.ToLower())")" >> $env:GITHUB_ENV
}

- name: Install Neovim ${{ env.NVIM_VERSION }}
run: |
$zip = Join-Path $env:RUNNER_TEMP nvim-win64.zip
Invoke-WebRequest "https://github.com/neovim/neovim/releases/download/v$env:NVIM_VERSION/nvim-win64.zip" -OutFile $zip
Expand-Archive $zip -DestinationPath $env:RUNNER_TEMP
$bin = Join-Path $env:RUNNER_TEMP nvim-win64\bin
& "$bin\nvim.exe" --version
$bin >> $env:GITHUB_PATH

- name: Provide make
# telescope-fzf-native and LuaSnip build with `make`; the runner image only ships mingw32-make.
run: |
if (Get-Command make -ErrorAction SilentlyContinue) { exit 0 }
$mingw = Get-Command mingw32-make -ErrorAction SilentlyContinue
if ($mingw) {
$bin = Join-Path $env:RUNNER_TEMP make-bin
New-Item -ItemType Directory $bin -Force | Out-Null
Copy-Item $mingw.Source (Join-Path $bin make.exe)
$bin >> $env:GITHUB_PATH
} else {
choco install make --no-progress -y
}
- uses: ./nvim/.github/actions/setup-nvim

- name: Smoke test
run: |
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/plugin-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Plugin update

# Weekly `:Lazy update`. Commits the new lazy-lock.json to simonk/dev/plugin-update, opens or refreshes a PR,
# and dispatches CI on it, which runs the smoke test on the new commits. On a PR that changes the updater it only
# does a dry run and shows the would-be PR body in the job summary.

on:
schedule:
- cron: "0 15 * * 1" # Mondays 15:00 UTC
workflow_dispatch:
pull_request:
paths:
- .github/workflows/plugin-update.yml
- .github/scripts/plugin-update.lua
- .github/actions/setup-nvim/**

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
update:
name: Update plugins
runs-on: windows-latest
timeout-minutes: 30
permissions:
contents: write # push the update branch
pull-requests: write # open or refresh the PR
actions: write # dispatch CI; pushes made with GITHUB_TOKEN don't trigger workflows
env:
BRANCH: simonk/dev/plugin-update
BOT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GH_TOKEN: ${{ github.token }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply chain: the Update plugins step runs nvim, which loads your whole config, pulls the newest unreviewed commit of every plugin with :Lazy update, and runs their build steps (make for telescope-fzf-native and LuaSnip). This job has contents: write, pull-requests: write and actions: write. The token is exposed in two ways:

  1. GH_TOKEN here is in the job-level env, so nvim and every build it spawns inherit it.
  2. actions/checkout keeps the token in the git config by default (persist-credentials: true).

A compromised or broken plugin could push to main (if branch protection allows it), edit workflows or start other workflows. The pull_request dry run has the same exposure for same-repo branches.

Suggested fix: split into two jobs.

  • update: permissions: contents: read, persist-credentials: false, no GH_TOKEN. It runs nvim, then uploads lazy-lock.json and pr-body.md with actions/upload-artifact.
  • publish: needs: update, if: github.event_name != 'pull_request', with the write permissions. It downloads the artifact and does the commit, push, gh pr and gh workflow run steps. Neovim never runs here.

Docs: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions

defaults:
run:
shell: pwsh
working-directory: nvim
steps:
- uses: actions/checkout@v7
with:
path: nvim # Neovim reads $XDG_CONFIG_HOME/nvim

- uses: ./nvim/.github/actions/setup-nvim

- name: Update plugins
id: update
env:
OLD_LOCKFILE: ${{ runner.temp }}/old-lock.json
SUMMARY_FILE: ${{ runner.temp }}/pr-body.md
run: |
Copy-Item lazy-lock.json $env:OLD_LOCKFILE
nvim --headless "+luafile .github/scripts/plugin-update.lua"
if ($LASTEXITCODE) { exit $LASTEXITCODE }
if (-not (git status --porcelain -- lazy-lock.json)) {
'Every plugin is already at its latest commit.' >> $env:GITHUB_STEP_SUMMARY
exit 0
}
Get-Content $env:SUMMARY_FILE >> $env:GITHUB_STEP_SUMMARY
"changed=true" >> $env:GITHUB_OUTPUT

- name: Open or refresh the PR
if: steps.update.outputs.changed == 'true' && github.event_name != 'pull_request'
run: |
$PSNativeCommandUseErrorActionPreference = $true
$pr = gh pr list --head $env:BRANCH --state open --json number --jq '.[].number'
if ($pr) {
$authors = gh pr view $pr --json commits --jq '.commits[].authors[].email'
if ($authors | Where-Object { $_ -ne $env:BOT_EMAIL }) {
"::warning::#$pr has commits from someone else, so it wasn't overwritten. Merge or close it, then rerun this workflow."
exit 0
}
}

git config user.name 'github-actions[bot]'
git config user.email $env:BOT_EMAIL
git switch --quiet -C $env:BRANCH
git commit --quiet -m 'Update plugins' -- lazy-lock.json
git push --quiet --force origin "HEAD:refs/heads/$env:BRANCH"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The author check only runs when an open PR exists. If the branch is left behind after a PR is closed without merging, --force overwrites whatever is on it, including your own fix commits. A commit that lands between the check and the push is also overwritten.

Consider recording the remote SHA (git ls-remote origin refs/heads/$env:BRANCH), checking its authors even without a PR, and pushing with --force-with-lease=refs/heads/${env:BRANCH}:<sha>. Docs: (gitscm.com/redacted) Minor, since the instructions already say not to reuse the branch.


$body = Join-Path $env:RUNNER_TEMP pr-body.md
if ($pr) {
gh pr edit $pr --body-file $body
} else {
gh pr create --base main --head $env:BRANCH --title 'Update plugins' --body-file $body
}
gh workflow run ci.yml --ref $env:BRANCH
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ This is my Neovim setup largely based and expanded on Josean Martinez's [blog po

Other than small retouch the setup is virtually identitcal to Josean's setup, checkout his video for more detailed explanation on the setup.

## Updates

- **Plugins**: `.github/workflows/plugin-update.yml` runs `:Lazy update` every Monday (or on demand from the Actions tab), commits the new `lazy-lock.json` to `simonk/dev/plugin-update`, and opens or refreshes an *Update plugins* PR that links each plugin's changes. CI runs on that PR, so the smoke test covers the new commits; merge it if it's green. The workflow force-pushes the branch each week, but leaves it alone once it has a commit from anyone else, so you can push a fix to it. It needs **Allow GitHub Actions to create and approve pull requests** turned on in Settings → Actions → General.
- **GitHub Actions**: Dependabot (`.github/dependabot.yml`) opens one grouped PR a month for the actions used in workflows and `.github/actions/`. It skips generated `*.lock.yml` workflows, because `gh aw compile` would overwrite its edits; refresh those with `gh aw upgrade`. Dependabot can't update lazy.nvim plugins, hence the workflow above.

## CI checks

Every pull request (and push to `main`) runs `.github/workflows/ci.yml`:
Expand Down
Loading