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
61 changes: 61 additions & 0 deletions .github/actions/setup-kapi-vim/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: 'Set up kapi-vim'
description: 'Build a kapi-vim package, put its nvim on PATH, and restore every lazy.nvim plugin to the committed lazy-lock.json'
inputs:
target:
description: 'Package to build: lite or bundle (bundle reads $KAPI_VIM_LANGS)'
required: true
save-cache:
description: "Save the plugin cache on a miss. Off for callers that change plugins afterwards, so they can't store them under this lockfile's key."
default: 'true'
runs:
using: 'composite'
steps:
- uses: ./.github/actions/install-nix
# --impure so bundle can read $KAPI_VIM_LANGS; a no-op for lite, which
# never calls builtins.getEnv.
- name: Build and put nvim on PATH
shell: bash
run: |
out=$(nix build --impure ".#${{ inputs.target }}" --print-build-logs --no-link --print-out-paths)
echo "$out/bin" >> "$GITHUB_PATH"
# Confirmed root cause of CI's inflated startup numbers (see PR #44):
# without this cache, plugin files land fresh from a network git clone
# on every run, and each cold file read shows up inside nvim's own
# require()/sourcing timings (--startuptime measures the whole call
# including the read, not just execution). That's what was producing
# 100-600ms readings vs. ~25ms locally for the identical build -- not a
# slow plugin, not CPU contention, not codesign/Gatekeeper overhead (all
# measured and ruled out, see ci.yml's benchmark step). Adding this cache
# alone took every matrix entry down to ~20-27ms, matching local numbers
# almost exactly. Keyed on OS and arch because telescope-fzf-native's
# native build output is platform-specific.
- id: cache
uses: actions/cache/restore@v4
with:
path: ~/.local/share/nvim/lazy
key: lazy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lazy-lock.json') }}
# The wrapped nvim loads its config from the read-only store, so
# lazy.nvim reads and writes the lockfile through a stdpath("state")
# copy (see lua/config/lazy.lua), hence the copy in below.
#
# A bare `Lazy! restore` on a fresh runner (cache miss above) doesn't
# reproduce the lock: the first-run install handles LazyVim before the
# plugins its specs import are known, rewrites the lockfile without
# their entries, and then installs them at their upstream target. So
# install first, put the committed lock back, restore, and check it
# took. On a cache hit every plugin is already on disk, so the first
# `+qa` installs nothing.
- name: Restore plugins
shell: bash
run: |
nvim --headless +qa
cp lazy-lock.json ~/.local/state/nvim/lazy-lock.json
nvim --headless "+Lazy! restore" +qa
cmp lazy-lock.json ~/.local/state/nvim/lazy-lock.json
# Saved here, straight after the checked restore, rather than at job end:
# the cache then always holds exactly the locked plugins.
- if: inputs.save-cache == 'true' && steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.local/share/nvim/lazy
key: ${{ steps.cache.outputs.cache-primary-key }}
134 changes: 134 additions & 0 deletions .github/scripts/lazy-update.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
-- Release-only plugin updates, run by .github/workflows/lazy-update.yml:
--
-- nvim --headless -c "luafile .github/scripts/lazy-update.lua"
--
-- `:Lazy sync` moves every plugin to whatever it resolves to upstream,
-- which for most plugins is the branch head. This instead updates only the
-- plugins whose target is a tagged semver release (defaults.version = "*"
-- in lua/config/lazy.lua) and holds the rest -- plugins with an explicit
-- branch, `version = false`, or no release tags at all -- at their locked
-- commit, since there's no release to tell a fix from a work in progress.
--
-- Forward only: a release that the locked commit already contains is
-- skipped. Plenty of plugins are locked past their last tag (plenary's is
-- years old), and "latest release" would otherwise mean a downgrade.
-- Releases cut from a separate branch (neo-tree's v3.x) aren't ancestors
-- either way, so those are compared by version instead.
--
-- Writes a markdown summary to $LAZY_UPDATE_REPORT (for the PR body) and
-- exits non-zero if the check or update fails.

local Config = require("lazy.core.config")
local Git = require("lazy.manage.git")
local Manage = require("lazy.manage")
local Semver = require("lazy.manage.semver")

local function fail(msg)
io.stderr:write("lazy-update: " .. msg .. "\n")
vim.cmd("cquit 1")
end

local function git(dir, args)
local res = vim.system(vim.list_extend({ "git", "-C", dir }, args), { text = true }):wait()
return res.code == 0 and vim.trim(res.stdout) or nil, res.code
end

-- Does `commit` already contain `target`?
local function contains(dir, commit, target)
local _, code = git(dir, { "merge-base", "--is-ancestor", target, commit })
return code == 0
end

-- Nearest release tag at or behind `commit`. Version-shaped tags only:
-- several plugins keep a floating `stable` tag that describe would pick.
local function base_version(dir, commit)
local tag = git(dir, { "describe", "--tags", "--abbrev=0", "--match=v[0-9]*", "--match=[0-9]*", commit })
return tag and Semver.version(tag)
end

local function level(from, to)
if not from then
return "release"
elseif from.major ~= to.major then
return "major"
elseif from.minor ~= to.minor then
return "minor"
end
return "patch"
end

local ok, err = pcall(function()
Manage.check({ wait = true, show = false })
end)
if not ok then
return fail("check failed: " .. tostring(err))
end

local bumps, held = {}, {}
for name, plugin in pairs(Config.plugins) do
local updates = plugin._.updates
if updates then
local from = Git.info(plugin.dir, true)
local to = updates.to
local base = from and (from.version or base_version(plugin.dir, from.commit))
if
to.version
and from
and not contains(plugin.dir, from.commit, to.commit)
and (not base or base < to.version)
then
bumps[#bumps + 1] = {
name = name,
from = from.tag or from.commit:sub(1, 7),
to = to.tag,
level = level(base, to.version),
}
else
held[#held + 1] = name
end
end
end
table.sort(bumps, function(a, b)
return a.name < b.name
end)
table.sort(held)

if #bumps > 0 then
ok, err = pcall(function()
Manage.update({
wait = true,
show = false,
plugins = vim.tbl_map(function(b)
return b.name
end, bumps),
})
end)
if not ok then
return fail("update failed: " .. tostring(err))
end
end

local lines = {}
if #bumps > 0 then
vim.list_extend(lines, { "| Plugin | From | To | Change |", "|---|---|---|---|" })
for _, b in ipairs(bumps) do
local change = b.level == "major" and "**major**" or b.level
lines[#lines + 1] = ("| %s | %s | %s | %s |"):format(b.name, b.from, b.to, change)
end
else
lines[#lines + 1] = "No new plugin releases."
end
if #held > 0 then
lines[#lines + 1] = ""
lines[#lines + 1] = "Held at their locked commit (no newer release to move to): "
.. table.concat(held, ", ")
.. "."
end

local report = os.getenv("LAZY_UPDATE_REPORT")
if report then
vim.fn.writefile(lines, report)
else
print(table.concat(lines, "\n"))
end
vim.cmd("qall!")
28 changes: 4 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ jobs:
# new user gets, not a hermetic nix build -- but a reproducible one:
# `:Lazy sync` fetches whatever's newest upstream and rewrites the
# lockfile, making this job a moving target; `:Lazy restore` pins to the
# committed lockfile instead (see lua/config/lazy.lua for why that lockfile
# is actually reachable here). Then verifies startup is clean and the
# committed lockfile instead (see .github/actions/setup-kapi-vim for how,
# and the plugin cache that keeps the benchmark below honest). Then verifies startup is clean and the
# expected LSP binaries for that entry's languages resolve on PATH (see
# smoke-test.lua).
#
Expand Down Expand Up @@ -100,29 +100,9 @@ jobs:
KAPI_VIM_LANGS: ${{ matrix.langs }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/install-nix
- id: build
run: echo "out=$(nix build --impure .#${{ matrix.target }} --print-build-logs --no-link --print-out-paths)" >> "$GITHUB_OUTPUT"
- name: Put bin on PATH
run: echo "${{ steps.build.outputs.out }}/bin" >> "$GITHUB_PATH"
# Confirmed root cause of CI's inflated startup numbers below (see
# PR #44): without this cache, plugin files land fresh from a network
# git clone on every run, and each cold file read shows up inside
# nvim's own require()/sourcing timings (--startuptime measures the
# whole call including the read, not just execution). That's what was
# producing 100-600ms readings vs. ~25ms locally for the identical
# build -- not a slow plugin, not CPU contention, not codesign/
# Gatekeeper overhead (all measured and ruled out, see below). Adding
# this cache alone took every matrix entry down to ~20-27ms, matching
# local numbers almost exactly. Keyed on runs-on because
# telescope-fzf-native's native build output is arch-specific.
- name: Cache lazy.nvim plugins
uses: actions/cache@v4
- uses: ./.github/actions/setup-kapi-vim
with:
path: ~/.local/share/nvim/lazy
key: lazy-${{ matrix.runs-on }}-${{ hashFiles('lazy-lock.json') }}
- name: Restore plugins
run: nvim --headless "+Lazy! restore" +qa
target: ${{ matrix.target }}
- name: Run smoke test
run: nvim --headless -S .github/scripts/smoke-test.lua
# Appended here rather than a separate workflow: this job already paid
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/lazy-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: lazy.nvim updates
on:
schedule:
# Mondays, an hour after Dependabot's flake.lock run.
- cron: "0 7 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
# Moves lazy-lock.json to new plugin *releases* only and opens (or
# refreshes) one PR with the result -- see .github/scripts/lazy-update.lua
# for which plugins move and which are held. Dependabot has no lazy.nvim
# ecosystem, hence a workflow.
update:
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
permissions:
contents: write
pull-requests: write
# gh workflow run below. Pushes and PRs made with GITHUB_TOKEN don't
# trigger other workflows; workflow_dispatch is the exception, so CI
# is started explicitly on the PR branch.
actions: write
steps:
- uses: actions/checkout@v7
# Reads CI's plugin cache for the same lockfile, but never writes it:
# the plugins on disk are about to move past that lockfile.
- uses: ./.github/actions/setup-kapi-vim
with:
target: lite
save-cache: 'false'
- name: Update to new releases
env:
LAZY_UPDATE_REPORT: ${{ runner.temp }}/report.md
run: |
nvim --headless -c "luafile .github/scripts/lazy-update.lua"
# Bring lazy.nvim's stdpath("state") lockfile back into the
# checkout (see .github/actions/setup-kapi-vim).
cp ~/.local/state/nvim/lazy-lock.json lazy-lock.json
cat "$LAZY_UPDATE_REPORT" >> "$GITHUB_STEP_SUMMARY"
- id: diff
run: |
if git diff --quiet lazy-lock.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
# Same check CI runs, against the updated plugins already on disk, so
# a release that breaks startup never gets as far as a PR.
- name: Run smoke test
if: steps.diff.outputs.changed == 'true'
run: nvim --headless -S .github/scripts/smoke-test.lua
- id: pr
if: steps.diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
branch: chore/lazy-update
delete-branch: true
add-paths: lazy-lock.json
commit-message: "chore(deps): bump lazy.nvim plugin releases"
title: "chore(deps): bump lazy.nvim plugin releases"
body-path: ${{ runner.temp }}/report.md
- name: Run CI on the PR branch
if: steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run ci.yml --ref chore/lazy-update
5 changes: 5 additions & 0 deletions lua/config/lazy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ lazy.setup({
-- import/override with your plugins
{ import = "plugins" },
},
-- Plugins with semver tags follow releases, not their branch head, so
-- the lockfile only moves when upstream cuts a release. Specs that set
-- `branch` or `version = false`, and plugins with no tags, still track
-- commits; .github/scripts/lazy-update.lua leaves those alone.
defaults = { version = "*" },
checker = {
enabled = true, -- check for plugin updates periodically
notify = false, -- notify on update
Expand Down
Loading