Build self-contained Neovim bundles from any config repo with reusable GitHub Actions workflows.
The point of this project is to turn a Neovim config into a portable artifact that can be moved and reused as-is.
That is especially useful when:
- the target machine sits inside a company intranet and cannot access GitHub
- the target machine has an unusual architecture and you want a prebuilt bundle for it
- you want the exact same config, plugins, parsers, and runtime layout everywhere
Instead of cloning a config repo and letting the target machine bootstrap itself online, this project builds a portable bundle ahead of time and verifies that it can start from its own internal paths.
This provider currently supports consumers that use lazy.nvim as their plugin manager. The shared scripts and default checks assume a lazy.nvim-managed bundle layout.
- A reusable
workflow_callGitHub Actions workflow - Generic scripts to build, smoke-test, package, and verify portable Neovim bundles
| Layer | Repository | Responsibility |
|---|---|---|
| Provider | nvim-portable |
Build orchestration, bundle layout, XDG isolation, artifact packaging, fresh-container verification, shared contract enforcement |
| Consumer | your config repo | The actual Neovim config, lazy.nvim bootstrap, repo-specific checks, and declaring what must exist in the final bundle |
The provider answers how to build and verify a portable bundle. The consumer answers what should be inside this specific bundle.
A consumer repo needs to provide:
- A Neovim config directory containing
init.lua - A bootstrap script that installs
lazy.nvim, repo-specific plugins, and any other runtime assets into the bundle - Optionally, workflow inputs that declare repo-specific bundle paths and Treesitter parsers
- Optionally, smoke and verify scripts with extra assertions
The reusable workflow handles:
- x64/arm64 matrix orchestration
- artifact upload/download
- bundle packaging
- fresh-container verification
- default lazy.nvim baseline checks
The consumer repo handles:
- config contents
- plugin manager bootstrapping
- parser/tool selection
- repo-specific smoke checks
- repo-specific verify checks
name: build-neovim-bundle
on:
push:
pull_request:
workflow_dispatch:
jobs:
bundle:
uses: zzjc1234/nvim-portable/.github/workflows/neovim-bundle.yml@v1
with:
config_path: .config/nvim
bootstrap_script: .github/nvim/bootstrap.sh
neovim_version: v0.11.7
target_arches: amd64,arm64
build_container_image: ubuntu:24.04
test_container_image: ubuntu:24.04
output_dir: .nvim-portable/out
test_install_root: /opt/nvim-bundle-test
required_bundle_paths: |
xdg/data/nvim/lazy/lazy.nvim
xdg/data/nvim/lazy/telescope.nvim
required_treesitter_parsers: |
lua
vim
vimdoc
query
bash
smoke_script: .github/nvim/smoke.sh
verify_script: .github/nvim/verify.sh
bundle_name: my-nvimThe shared workflow accepts multiline string inputs for repo-specific static checks:
required_bundle_paths: |
xdg/data/nvim/lazy/lazy.nvim
xdg/data/nvim/lazy/telescope.nvim
required_treesitter_parsers: |
lua
vim
vimdoc
query
bashBoth inputs are optional. The shared build script treats them as post-build assertions without hardcoding plugin or parser lists in the infrastructure repo.
The reusable workflow now exposes these environment-style knobs through workflow_call inputs:
target_archesneovim_versionbuild_container_imagetest_container_imagebuild_apt_packagestest_apt_packagesoutput_dirtest_install_root
That means the consumer workflow, not the shared scripts, decides the target arches, Neovim version, container images, package lists, and artifact output location.
The reusable build script exports these variables before calling the consumer bootstrap script:
BUNDLE_ROOTBUNDLE_RUNXDG_CONFIG_HOMEXDG_DATA_HOMEXDG_STATE_HOMEXDG_CACHE_HOMENVIM_APPNAME
These variables are for consumer hook authors, not for end users of the final artifact. End users only need to run the bundle's generated run.sh.
The bootstrap script can call:
"$BUNDLE_RUN" --headless "+Lazy! sync" +qaor any equivalent repo-specific setup command.
- If you provide no override scripts, the shared workflow still performs default startup and stdpath isolation checks.
smoke_scriptruns after the bundle is built on the CI runnerverify_scriptruns after the packaged artifact is unpacked inside a fresh offline test container
This split lets each consumer repo keep its own plugin/parser/provider assertions without hardcoding them into shared infrastructure.
The shared verify container intentionally keeps its package list minimal. If a consumer config performs runtime checks that require extra system tools (for example a compiler toolchain), the consumer should opt in through test_apt_packages.
Some assumptions are still fixed on purpose because they are part of the product contract rather than caller-specific customization:
- supported targets are still
amd64andarm64 - consumers are expected to use
lazy.nvim - the bundle archive still contains a top-level
nvim-bundledirectory by default unlessbundle_nameis changed - the bundle still uses the internal XDG layout plus
run.sh - the Neovim download still assumes the official release naming pattern
nvim-linux-<arch>.tar.gz
These stay fixed because the rest of the scripts and verification flow depend on them as the shared portability contract.
- Add first-class support for non-glibc Linux distributions such as Alpine/musl-based systems.
Use semver tags from this repo in consumer workflows, for example @v1. The recommended caller pattern is:
uses: zzjc1234/nvim-portable/.github/workflows/neovim-bundle.yml@v1
with:
infra_repository: zzjc1234/nvim-portable
infra_ref: v1If you want to test unreleased provider changes, point both the reusable workflow ref and infra_ref to main explicitly.