Skip to content

fix(nvim): resolve config path via stdpath so <leader>rc opens init.l… #870

fix(nvim): resolve config path via stdpath so <leader>rc opens init.l…

fix(nvim): resolve config path via stdpath so <leader>rc opens init.l… #870

Workflow file for this run

name: ci
# Core is authored once here, then vendored into all 9 OS repos via git subtree.
# A defect that lands here fans out N-way β€” so gate every push/PR with the same
# audit the maintainer runs locally. CI installs the linters that a bare box may
# lack (shellcheck/shfmt/luacheck/zsh) and then hands off entirely to
# scripts/audit-core.sh, so there is ONE definition of "Core is healthy".
on:
push:
branches: ["**"]
pull_request:
permissions:
contents: read
# One in-flight run per ref. This both (a) cancels a superseded run when you push
# again, and (b) dedupes the push-event + pull_request-event double-trigger on a
# same-repo PR: both share this ref-keyed group, so only the latest survives.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# The linters CI installs are pinned for a REPRODUCIBLE 9-repo gate β€” a new upstream
# release can't silently turn CI red with zero repo changes. Those pins now live in
# ONE place, scripts/tool-versions.env, which each job loads into $GITHUB_ENV via the
# "Load pinned tool versions" step below (no version literals left in this file). The
# audit's version-consistency section keeps the .pre-commit-config.yaml revs in step.
jobs:
# Change detection so a docs- or nvim-only push doesn't pay the full matrix.
# SAFE DEFAULT: if the diff base can't be resolved (first push, force-push, a
# shallow checkout), every flag is 'true' and nothing downstream is skipped β€” a
# detection miss must never hide a needed check from the 9-repo fan-out. Infra
# changes (scripts/, .github/, the manifest, the pre-commit/Makefile config) are
# cross-cutting and likewise force the full run.
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
shell: ${{ steps.classify.outputs.shell }}
nvim: ${{ steps.classify.outputs.nvim }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # full history so the diff base resolves
- id: classify
env:
EVENT: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
BEFORE: ${{ github.event.before }}
run: |
set -uo pipefail
base=""
if [ "$EVENT" = pull_request ]; then
git fetch -q origin "$BASE_REF" 2>/dev/null && base="origin/$BASE_REF" || base=""
elif [ -n "${BEFORE:-}" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
base="$BEFORE"
fi
if [ -z "$base" ] || ! git rev-parse -q --verify "$base^{commit}" >/dev/null 2>&1; then
echo "diff base unknown β€” running everything (safe default)"
files="__ALL__"
else
files="$(git diff --name-only "$base" HEAD)"
echo "changed files vs $base:"; printf '%s\n' "$files" | sed 's/^/ /'
fi
# The path→gate mapping lives in scripts/ci-classify.sh — shellcheck'd,
# unit-tested (scripts/test-core.sh), and FAIL-CLOSED on an unrecognised
# path β€” instead of drift-prone greps inlined in this YAML. It prints
# `shell=…`/`nvim=…`, which we echo and append straight to $GITHUB_OUTPUT.
out="$(printf '%s\n' "$files" | ./scripts/ci-classify.sh)"
printf 'β†’ %s\n' "$out" | tr '\n' ' '; echo
echo "$out" >> "$GITHUB_OUTPUT"
audit:
needs: changes
# Core fans out to macOS (dotfiles-MacBook) AND six Linux distros, so gate
# on BOTH userlands β€” not just Linux. The portable checks (shellcheck, zsh -n,
# manifest/exec-bit) are OS-agnostic, but the BEHAVIORAL tests and bin/clip*
# exercise real runtime: macOS ships bash 3.2 (no `mapfile`/assoc-array niceties
# GNU bash 4+ has) and BSD coreutils, which Ubuntu alone can never surface.
# fail-fast off so a macOS-only break doesn't hide a Linux-only one (and vice
# versa) β€” we want to see every failing platform in one run.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Single source of truth for the pinned linter versions: load them into the
# job env so every step below (and the cache keys) see the same numbers as
# `make setup` does locally. Only KEY=VALUE lines, so comments are skipped.
- name: Load pinned tool versions
run: grep -E '^[A-Z0-9_]+=' scripts/tool-versions.env >> "$GITHUB_ENV"
# ── Tool cache: shellcheck/actionlint/gitleaks are pinned release tarballs that
# were re-downloaded on EVERY run (only luacheck + pre-commit were cached). Install
# them into a cached, PATH-exported dir keyed on tool-versions.env, so the common
# shell-only push restores them instead of paying three network fetches. A pin bump
# rotates the key automatically. Linux-only (macOS uses brew for shellcheck; the
# other two are Linux-only gates). ──
- name: Set up cached tool dir on PATH (Linux)
if: runner.os == 'Linux'
run: |
mkdir -p "$HOME/.local/core-tools/bin"
echo "$HOME/.local/core-tools/bin" >> "$GITHUB_PATH"
- name: Cache pinned CLI tools (shellcheck/actionlint/gitleaks)
id: toolcache
if: runner.os == 'Linux'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local/core-tools
key: core-tools-${{ runner.os }}-${{ hashFiles('scripts/tool-versions.env') }}
# ── Linux: pinned single-binary gate tools (shellcheck/actionlint/gitleaks) via the
# shared composite action β€” ONE definition of each install, also used by freshness.yml
# (B8). Idempotent: it no-ops anything the tool cache already restored, so the
# cache-hit fast path is preserved without a per-tool conditional. ──
- name: Install pinned CLI tools (Linux)
if: runner.os == 'Linux'
uses: ./.github/actions/setup-core-tools
with:
shellcheck: "true"
actionlint: "true"
gitleaks: "true"
- name: Install zsh (Linux)
if: runner.os == 'Linux'
run: |
set -euxo pipefail
# Drop GitHub's flaky third-party apt repos (packages.microsoft.com/azure) first:
# they intermittently serve an unsigned InRelease and fail the whole update (exit 100).
sudo find /etc/apt/sources.list.d -type f \( -iname '*microsoft*' -o -iname '*azure*' \) -delete 2>/dev/null || true
sudo apt-get update
sudo apt-get install -y --no-install-recommends zsh
# ── macOS: shellcheck via Homebrew (no pinned-binary tarball published for
# macOS the way Linux gets one; brew is the supported path). zsh is preinstalled
# on the macOS runner, so there is no zsh step here. ──
- name: Install shellcheck (macOS)
if: runner.os == 'macOS'
run: brew install shellcheck
# Cache the luarocks tree so luacheck (and its lua deps) aren't recompiled
# from source on every run. Key on the pinned version AND the OS (the matrix
# runs both): bump the version and every cache key rotates automatically.
- name: Cache luacheck (luarocks tree)
id: luacache
# luacheck only lints nvim/ β€” skip its (slow) install when nvim/ is untouched.
if: needs.changes.outputs.nvim == 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.luarocks
# `-lua54` busts any tree a prior run cached against brew's now-default Lua 5.5
# (which luacheck 1.2.0 can't load) and pins the cache to the 5.4 install below.
key: luacheck-${{ runner.os }}-${{ env.LUACHECK_VERSION }}-lua54
- name: Install luacheck (Linux, pinned)
if: runner.os == 'Linux' && needs.changes.outputs.nvim == 'true'
run: |
set -euxo pipefail
sudo apt-get install -y --no-install-recommends luarocks
# --local installs under ~/.luarocks (the cached path above).
luarocks --local install luacheck "${LUACHECK_VERSION}"
echo "$HOME/.luarocks/bin" >> "$GITHUB_PATH"
- name: Install luacheck (macOS, pinned)
if: runner.os == 'macOS' && needs.changes.outputs.nvim == 'true'
run: |
set -euxo pipefail
# Pin to Lua 5.4: brew's luarocks now pulls Lua 5.5 as its default interpreter,
# and luacheck 1.2.0 (the last release) fails to even load under 5.5 β€” 5.5 made
# some locals const, tripping "attempt to assign to const variable" in luacheck's
# own source. Install luacheck against an explicit [email protected] so the gate is stable
# until a luacheck release supports 5.5. --lua-dir points luarocks at that keg.
brew install [email protected] luarocks
luarocks --lua-version 5.4 --lua-dir "$(brew --prefix [email protected])" --local install luacheck "${LUACHECK_VERSION}"
echo "$HOME/.luarocks/bin" >> "$GITHUB_PATH"
# markdownlint gates the docs (the deliverable on this showcase repo). Pinned
# to match the .pre-commit-config.yaml hook rev so author-time and CI agree.
# Node is preinstalled on both runners; -g puts the bin on PATH for the audit.
- name: Install markdownlint-cli2 (pinned)
run: npm install -g "markdownlint-cli2@${MARKDOWNLINT_VERSION}"
# actionlint (workflows) + gitleaks (secrets) are installed by the shared composite
# action above (Linux-only; workflows + secrets are OS-agnostic so the macOS leg's
# audit skips both sections). See the "Install pinned CLI tools (Linux)" step.
# ── neovim: Linux gets the maintainer's pinned release tarball via the same shared
# composite action (idempotent; the action no-ops if nvim is already present). macOS
# uses brew below. Drives test-core.sh's headless config-load smoke test. ──
- name: Install neovim (Linux, pinned)
if: runner.os == 'Linux' && needs.changes.outputs.nvim == 'true'
uses: ./.github/actions/setup-core-tools
with:
neovim: "true"
- name: Install neovim (macOS)
if: runner.os == 'macOS' && needs.changes.outputs.nvim == 'true'
run: brew install neovim
# PyYAML for the audit's YAML-parse gate (section 6). The Linux audit runs
# --strict (below), which fails if an IN-SCOPE gate skips for a missing tool β€” so
# the always-on YAML check must have its importable `yaml`. tomllib/json are stdlib
# (Python β‰₯3.11, which the runner ships); only PyYAML needs installing. Linux-only:
# the macOS leg isn't --strict, and Alpine installs py3-yaml in its own job.
- name: Install PyYAML (Linux, for the audit's YAML parse gate)
if: runner.os == 'Linux'
run: sudo apt-get install -y --no-install-recommends python3-yaml
# Tolerant: luacheck/nvim are skipped for non-nvim changes, so report what's
# actually present instead of failing on a deliberately-absent tool.
- name: Tool versions
run: |
for t in shellcheck luacheck zsh bash markdownlint-cli2 actionlint gitleaks nvim; do
if command -v "$t" >/dev/null 2>&1; then
printf '%s: ' "$t"; "$t" --version | head -1
else
echo "$t: (not installed for this change)"
fi
done
# Scope the audit to the areas the classifier flagged, so a docs- or nvim-only
# push skips the gates it can't affect. The cheap structural/config/markdown/
# workflow/version checks always run regardless of scope (see audit-core.sh).
# FAIL-CLOSED: the classifier forces BOTH flags true for infra/unknown changes,
# so this resolves to the full "shell,nvim" run there β€” never a narrowed audit.
- name: Run Core audit (manifest, exec bits, syntax, lint, behavioral)
env:
SHELL_CHANGED: ${{ needs.changes.outputs.shell }}
NVIM_CHANGED: ${{ needs.changes.outputs.nvim }}
run: |
scope=""
[ "$SHELL_CHANGED" = true ] && scope="shell"
[ "$NVIM_CHANGED" = true ] && scope="${scope:+$scope,}nvim"
[ -z "$scope" ] && scope="none"
# --strict on the Linux leg: every IN-SCOPE gate's tool is installed here, so a
# SKIP would mean a tool silently failed to install β€” a false green this repo's
# whole thesis is against. Out-of-scope skips (a narrowed run) don't trip it.
# macOS stays non-strict (actionlint/gitleaks are Linux-only gates there).
strict=""
[ "$RUNNER_OS" = Linux ] && strict="--strict"
echo "audit scope: $scope ${strict:+(strict)}"
./scripts/audit-core.sh --scope "$scope" $strict
# Alpine (musl libc + busybox coreutils) is a named target repo, and functions.zsh
# + the maint/test scripts explicitly claim busybox compatibility β€” yet the matrix
# above only proves glibc Ubuntu and BSD macOS, never musl/busybox. This leg closes
# that asserted-but-unverified gap. We deliberately do NOT use `container: alpine`
# (its musl libc breaks the glibc Node that actions/checkout ships, a known failure);
# instead we check out on the host and run the SAME scripts/audit-core.sh inside an Alpine
# container with the workspace bind-mounted. Only bash/zsh/git/shellcheck/python are
# installed β€” luacheck, markdownlint and nvim skip gracefully here and are already
# covered on the glibc legs. The value is everything ELSE running against busybox
# applets (awk/grep/sed/date/tar/gzip): the behavioral suite, the clip ladder, and
# the manifest/exec-bit/syntax checks, on the one userland Ubuntu can never surface.
audit-alpine:
needs: changes
# Alpine re-validates the SHELL layer against musl/busybox; a docs- or nvim-only
# change can't affect that, so skip the whole container spin unless shell changed.
if: needs.changes.outputs.shell == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Audit on Alpine (musl + busybox)
run: |
docker run --rm -v "$PWD:/core" -w /core alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d sh -euc '
apk add --no-cache bash zsh git shellcheck python3 py3-yaml
# The repo is owned by the host runner uid but the container runs as root;
# without this, git 2.35+ refuses it as "dubious ownership" and the audit
# cannot read ls-files/ls-tree. Scope the exception to the mounted path.
git config --global --add safe.directory /core
# This leg exists to re-validate the SHELL layer on musl/busybox; it only
# runs when shell changed (job-level if:), and nvim tooling is not installed
# here β€” so scope it to shell (the nvim sections skip as out-of-scope).
./scripts/audit-core.sh --scope shell
'
# Arch (glibc, ROLLING release) is a named target repo, and the Alpine leg above
# only proves musl/busybox while the matrix proves Ubuntu LTS + BSD macOS β€” none of
# which surface the BLEEDING-EDGE GNU toolchain Arch actually runs. Arch ships newer
# zsh/bash/coreutils/git than Ubuntu LTS, so a deprecation or behavior change lands
# here MONTHS before an LTS bump would catch it β€” the early-warning a rolling target
# is supposed to give. Mirrors audit-alpine: check out on the host, then run the SAME
# scripts/audit-core.sh inside an Arch container with the workspace bind-mounted (no
# `container: archlinux` β€” same uniform pattern; only the image + package step differ).
audit-arch:
needs: changes
# Arch re-validates the SHELL layer against the rolling GNU toolchain; a docs- or
# nvim-only change can't affect that, so skip the container spin unless shell changed.
if: needs.changes.outputs.shell == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Audit on Arch (rolling glibc toolchain)
run: |
docker run --rm -v "$PWD:/core" -w /core archlinux:latest@sha256:681569955d1d17313ef7134acc8b5cd8adcda2fc24709bed472d95e1cf3d71a1 sh -euc '
# -Syu (not bare -Sy): a partial upgrade is unsupported on Arch and can pull
# mismatched libs. The container is ephemeral, so the full sync+upgrade is the
# correct AND safe idiom here β€” the same rule dotfiles-Arch ships to real hosts.
pacman -Syu --noconfirm --needed bash zsh git shellcheck python python-yaml
# Same "dubious ownership" fix as the Alpine leg: repo is host-uid-owned but
# the container runs as root, so git 2.35+ would refuse ls-files/ls-tree.
git config --global --add safe.directory /core
# Shell scope only: this leg re-validates zsh/bash against the rolling
# toolchain; nvim/lua tooling is not installed here (skips out-of-scope) and
# is already covered on the glibc Ubuntu leg.
./scripts/audit-core.sh --scope shell
'
# Author-time hygiene gate. The .pre-commit-config.yaml hooks only fire for
# contributors who ran `pre-commit install`; without this job a push carrying
# trailing whitespace, a missing final newline, a stray `<<<<<<<` conflict marker,
# or a shebang script that isn't +x sails through green and subtree-syncs into all
# 9 repos. The audit job deliberately does NOT replicate these file-hygiene hooks
# (audit-core.sh owns manifest/lint/behavioral), so run the SAME config here β€”
# author-time == CI, one source of truth, zero drift. SKIP the three hooks the
# audit job already owns so nothing runs twice and this stays the fast lane.
# Linux-only: file hygiene is OS-agnostic, unlike the audit's dual-userland matrix.
precommit:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Single source of truth for the pinned pre-commit version (see the audit job).
- name: Load pinned tool versions
run: grep -E '^[A-Z0-9_]+=' scripts/tool-versions.env >> "$GITHUB_ENV"
# Cache the hook environments pre-commit clones+builds, keyed on the config so a
# hook-rev bump (or a new hook) rotates the key automatically.
- name: Cache pre-commit hooks
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
# pipx is preinstalled on the runner and installs to ~/.local/bin (already on
# PATH), sidestepping PEP-668 "externally-managed-environment" pip errors.
- name: Install pre-commit (pinned)
run: pipx install "pre-commit==${PRECOMMIT_VERSION}"
- name: Run hygiene hooks (audit-owned hooks skipped)
env:
SKIP: audit-core,shellcheck,markdownlint-cli2
run: pre-commit run --all-files --show-diff-on-failure
# Startup-perf REGRESSION GATE. Core invests in startup speed (cached tool-init in
# tools.zsh, deferred plugins in plugins.zsh); a gross regression β€” re-introducing a
# per-shell subprocess, un-deferring heavy work, an uncached eval β€” would fan out to
# all 9 repos silently. This job now ENFORCES a budget instead of only reporting:
# bench-core.sh fails the build when the canonical-chain MEAN exceeds
# CORE_BENCH_BUDGET_MS. The budget is deliberately GENEROUS (the hermetic chain runs
# ~25 ms; 120 ms is ~5x headroom) and the mean is taken over 50 warmed runs, so
# ordinary shared-runner noise can't trip it β€” only a real regression sustains a 5x
# mean. The precise number still prints in the log for trend-watching. No
# continue-on-error: a breach is now a red build, not a buried log line. Linux-only.
bench:
needs: changes
# Startup perf is a property of the zsh load chain; a docs- or nvim-only change
# can't move it, so only benchmark when the shell layer actually changed.
if: needs.changes.outputs.shell == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install zsh + hyperfine
run: |
set -euxo pipefail
# Drop GitHub's flaky third-party apt repos (packages.microsoft.com/azure) first β€”
# an unsigned InRelease there otherwise fails the whole update (exit 100).
sudo find /etc/apt/sources.list.d -type f \( -iname '*microsoft*' -o -iname '*azure*' \) -delete 2>/dev/null || true
sudo apt-get update
sudo apt-get install -y --no-install-recommends zsh hyperfine
- name: Benchmark Core startup (budget gate)
env:
CORE_BENCH_RUNS: "50"
CORE_BENCH_BUDGET_MS: "120"
run: ./scripts/bench-core.sh