Create GitHub releases from inside Neovim, driven by the gh CLI.
Instead of gh's terminal prompts, nvim-ghrelease lists your existing releases,
suggests the next semantic version, and asks each question gh release create asks.
Free-text answers (tag, title, release notes) open in a dedicated buffer per step —
edit, then :wq to move to the next question. When every answer is collected it runs
gh release create and reports the new release URL.
nvim-ghrelease.mp4
- Lists current releases for context before you start.
- Suggests the next version with a semver bump menu (
major/minor/patch/prerelease/custom). - Buffer-per-step editing:
:wqsaves the answer and advances. - Tag collision detection: if the entered tag already has a release, choose to retry with another tag, overwrite the existing release, or cancel — instead of failing after you've already written notes.
- Release-notes sources mirroring gh: write your own, GitHub-generated template, commit-log template, or leave blank.
- Final
Publish / Save as draft / Cancelstep, plus a prerelease question. - Fully async (
vim.system), no blocking of the editor.
- Neovim 0.10+ (needs
vim.system; the plugin refuses to load and warns on older versions). Tested in CI on 0.10, 0.11, stable, and nightly. - The GitHub CLI
gh, installed and authenticated:gh auth login - Run the command from within a GitHub repository
{
"mesirendon/nvim-ghrelease",
-- Load at startup so the default keymap (<leader>gr) is registered.
event = "VeryLazy",
opts = {}, -- optional; see Configuration
}
setup()(viaopts) is what installs the default keymaps, so the plugin must be loaded for them to exist. If you prefer to lazy-load only on the keypress, disable the built-in maps and let lazy own the key:{ "mesirendon/nvim-ghrelease", opts = { keymaps = false }, keys = { { "<leader>gr", "<cmd>GhRelease<cr>", desc = "GitHub Release: create" } }, }
use({
"mesirendon/nvim-ghrelease",
config = function()
require("ghrelease").setup()
end,
})Plug 'mesirendon/nvim-ghrelease'
" then, in your Lua config:
lua require('ghrelease').setup()add({ source = "mesirendon/nvim-ghrelease" })Calling setup() is optional — the :GhRelease command is available as soon as the
plugin is on the runtimepath.
Run inside a GitHub repository:
:GhRelease
or from Lua:
require("ghrelease").create()Then follow the steps:
- Existing releases are shown — press
<CR>,q, or<Esc>to continue. - Version menu — pick a bump or
custom. - Tag buffer (pre-filled) — edit,
:wq. If a release already exists for that tag, you're asked to retry with another tag, overwrite the existing release, or cancel. - Title buffer.
- Notes source — own / generated / commit-log / blank.
- Notes buffer (pre-filled per source), unless blank.
- Prerelease? — yes/no.
- Submit — Publish / Save as draft / Cancel.
In any editing buffer, :wq accepts and continues; :q keeps whatever is shown.
If you chose to overwrite, the flow runs gh release edit instead of
gh release create. Otherwise, when the tag already exists locally but has no
release yet, --verify-tag is added to gh release create so it refuses to
create the release unless that tag is also on the remote — protecting you from
tagging the wrong commit.
setup() installs one default keymap, sitting in the <leader>g (git) family:
| Keys | Mode | Action | Command |
|---|---|---|---|
<leader>gr |
n |
GitHub Release: create | :GhRelease |
The description shows up in which-key automatically under your git group.
Pass a keymaps table to setup() (or opts in lazy.nvim). Each key is an action
name; the value is the normal-mode lhs, or false to skip that action:
require("ghrelease").setup({
keymaps = {
create = "<leader>oR", -- remap "create" to <leader>oR
},
})Disable a single action:
require("ghrelease").setup({
keymaps = { create = false },
})Disable all default keymaps and map it yourself:
require("ghrelease").setup({ keymaps = false })
vim.keymap.set("n", "<leader>R", "<cmd>GhRelease<cr>",
{ desc = "GitHub Release: create", silent = true })Defaults, with overrides passed to setup():
require("ghrelease").setup({
list_limit = 30, -- releases shown in the context view
default_bump = "patch", -- highlighted first in the version menu
tag_prefix = "v", -- prefix for the suggested first tag
first_tag = "v0.1.0", -- seed tag when the repo has no releases
win = "float", -- editing window style: "float" | "split" | "vsplit"
height = 12, -- editing buffer height / float rows
keymaps = { -- default keymaps; false disables all (see Keymaps)
create = "<leader>gr",
},
})The choice menus use vim.ui.select, so they integrate automatically with
dressing.nvim, telescope-ui-select, etc.
Run all specs headlessly (tests/init.lua auto-discovers every tests/*_spec.lua file):
nvim --headless -l tests/init.luaMIT