From 1208104a0496b6e0827f4e2c8e64cffc9cdcf2ea Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Wed, 6 May 2026 04:58:37 +0100 Subject: [PATCH 01/47] docs: add zd refactor design spec Captures the two-tier test architecture decision: native ZUnit on GitHub runners for regular CI, Docker reserved for Zsh version matrix. Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-05-06-zd-refactor-design.md | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-06-zd-refactor-design.md diff --git a/docs/superpowers/specs/2026-05-06-zd-refactor-design.md b/docs/superpowers/specs/2026-05-06-zd-refactor-design.md new file mode 100644 index 0000000..83c72af --- /dev/null +++ b/docs/superpowers/specs/2026-05-06-zd-refactor-design.md @@ -0,0 +1,212 @@ +# zd Container Refactor Design + +**Date:** 2026-05-06 +**Status:** Approved + +## Problem + +The `zd` container is the test harness for the zi plugin manager. It has two core problems: + +1. **Flaky CI** — each ZUnit test spawns a fresh Docker container and runs live network downloads (GitHub releases, git clones). Any transient network failure fails the test. +2. **Hard to author tests** — zi commands are passed as shell-escaped strings to `run.sh --wrap`, creating an escaping nightmare. There is no fast local iteration path; every tweak requires a full container round-trip. + +## Approach: Native CI tier + Docker only for Zsh version matrix + +Tests run natively on GitHub runners for regular CI. Docker is reserved for the Zsh version compatibility matrix (5.5.1–5.9), run on a weekly schedule. + +## Repository Structure + +``` +zd/ + tests/ # all .zunit files — shared between native and Docker tiers + helpers.zsh # zi_test() helper and shared utilities + setup.zsh # per-test data dir reset + teardown.zsh # per-test cleanup + annexes.zunit + ice.zunit + packages.zunit + plugins.zunit + snippets.zunit + docker/ + Dockerfile # two-stage build; zi pre-installed during build + entrypoint.sh # user creation only — no runtime downloads + zshenv + zshrc + scripts/ + build.sh # image build helper + run.sh # interactive container launcher (dev use) + utils.zsh # zi wrapper functions (prepare_system, initiate_system, etc.) + .github/ + workflows/ + test-native.yml # tier 1: native zsh on ubuntu-latest + test-matrix.yml # tier 2: Docker, Zsh version matrix + docker.yml # image publish (unchanged) +``` + +Key moves from current layout: +- `docker/tests/` → `tests/` (tests are no longer Docker-specific) +- `docker/build.sh`, `docker/run.sh` → `scripts/` +- Docker build context contains only what `docker build` needs + +## Test Authoring Model + +### The problem with the current model + +Tests pass zi commands as shell-escaped strings to `run.sh --wrap`, which runs `zsh -ilsc ""` inside a container. Example of current escaping: + +```zsh +local z=$'zi id-as'\''atpull-fail'\'' null \ +atpull'\''echo "intentional failure"; return 255'\'' run-atpull \ +for z-shell/null; zi update atpull-fail' +run ./docker/run.sh --wrap --debug --zunit $z +``` + +### The fix: `zi_test` helper + +`tests/helpers.zsh` provides a `zi_test` function that runs a fresh isolated zsh process per test. Commands are written as normal zsh in the test file — no escaping: + +```zsh +# tests/helpers.zsh +ZI_BIN="${ZI_BIN:-${HOME}/.zi/bin}" +ZI_DATA="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" + +zi_test() { + local script=$1 + run zsh -lc " + typeset -gA ZI + ZI[HOME_DIR]=${ZI_DATA} + source ${ZI_BIN}/zi.zsh + autoload -Uz _zi + ${script} + " +} +``` + +Same test rewritten: + +```zsh +@test 'failing atpull ice' { + zi_test ' + zi id-as"atpull-fail" null \ + atpull"echo intentional failure; return 255" run-atpull \ + for z-shell/null + zi update atpull-fail + ' + assert $state equals 255 + assert "$output" contains "intentional failure" +} +``` + +Each test gets a fresh isolated zsh process. No shared state between tests. `zi_test` works identically in both the native tier and inside the Docker container — only `ZI_BIN` and `ZI_DATA` env vars differ. + +File-level assertions reference `ZI_DATA` directly: + +```zsh +assert "${ZI_DATA}/plugins/junegunn---fzf/fzf" is_executable +``` + +## Native CI Tier + +**Workflow:** `.github/workflows/test-native.yml` + +- Trigger: push to `main` (paths: `tests/**`), pull requests, weekly schedule, `workflow_dispatch` +- Runner: `ubuntu-latest` +- Matrix: one job per `.zunit` file (parallel, `fail-fast: false`) + +**Setup steps:** +1. Install `zsh` via apt +2. Install `zunit`, `revolver`, `color` into `bin/` +3. Install zi via `zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip` +4. Cache `~/.zi/bin` keyed to zi's commit SHA — network hit only when zi changes + +**Run:** +```sh +export PATH="$PWD/bin:$PATH" +export ZI_BIN="${HOME}/.zi/bin" +export ZI_DATA="${RUNNER_TEMP}/zunit" +zunit --tap --verbose "tests/${{ matrix.file }}.zunit" +``` + +This is 10–20× faster than the current per-test container spawn and removes all network flakiness from non-zi sources. + +## Docker Tier + +### Dockerfile (two-stage) + +```dockerfile +ARG ALPINE_VERSION=edge + +FROM alpine:${ALPINE_VERSION} AS base + +RUN apk --no-cache add \ + build-base ncurses-dev pcre-dev zlib-dev autoconf \ + bash curl git jq rsync sudo zsh vim + +# Install zi at build time — not at test time +ARG ZI_BRANCH=main +RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + +FROM base AS test +ARG ZUSER=user +ARG PUID=1000 +ARG PGID=1000 + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && /entrypoint.sh + +COPY docker/zshenv /home/${ZUSER}/.zshenv +COPY docker/zshrc /home/${ZUSER}/.zshrc +COPY utils.zsh /src/utils.zsh +COPY tests/ /src/tests/ + +# VOLUME declared after all COPYs — fixes silent copy invalidation bug +VOLUME ["/data"] + +USER ${ZUSER} +WORKDIR /home/${ZUSER} +CMD ["zsh", "-il"] +``` + +Key fixes vs. current: +- zi is baked in during `docker build` — no network calls at test time +- `VOLUME` declared after `COPY` (current ordering silently discards the copy) +- `entrypoint.sh` only handles user creation — no `wget install.sh` download +- Go removed (not needed for test execution) + +### Matrix Workflow + +**Workflow:** `.github/workflows/test-matrix.yml` + +- Trigger: weekly schedule (`0 3 * * 3`), `workflow_dispatch` only — not on every push +- Matrix: 6 Zsh versions × 5 test files = 30 jobs (`fail-fast: false`) +- Buildx layer caching via `type=gha` — repeat runs rebuild only changed layers + +**Run per matrix cell:** +```sh +docker run --rm \ + -e ZI_DATA=/data \ + -v "${RUNNER_TEMP}/zunit:/data" \ + zd:${{ matrix.zsh_version }} \ + zsh -c "zunit --tap --verbose /src/tests/${{ matrix.file }}.zunit" +``` + +The native workflow catches regressions on every PR. The matrix workflow verifies Zsh version compatibility on a cadence, not blocking every merge. + +## Migration of Existing Tests + +All five existing `.zunit` files are migrated by: + +1. Replacing `run ./docker/run.sh --wrap --debug --zunit ` with `zi_test ''` +2. Adding `load helpers` to each `@setup` block +3. Replacing hardcoded `${PLUGINS_DIR}` / `${ZPFX}` path assertions with `${ZI_DATA}/plugins/...` and `${ZI_DATA}/polaris/...` +4. Moving files from `docker/tests/` to `tests/` + +No test logic changes — only the invocation wrapper and path references. + +## What Is Not Changed + +- `utils.zsh` functions (`prepare_system`, `initiate_system`, `reload_system`, `zi::*`) — kept as-is for interactive use +- `scripts/run.sh` — kept for interactive `docker run` sessions +- `docker.yml` publish workflow — unchanged +- `.zunit` test assertions and test cases — logic unchanged, only wrapper replaced +- Trunk / linting configuration From 69a831183e98bc94d7c46a5b17bbdb933721491b Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Wed, 6 May 2026 05:02:14 +0100 Subject: [PATCH 02/47] docs: fix three spec gaps after self-review - Dockerfile now uses ARG ZSH_VERSION via zi pack during build - Matrix workflow: 6 jobs (one per Zsh version) not 30 - entrypoint.sh scope explicitly documented - zi_test variable interpolation behavior noted Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-05-06-zd-refactor-design.md | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-05-06-zd-refactor-design.md b/docs/superpowers/specs/2026-05-06-zd-refactor-design.md index 83c72af..4f7957a 100644 --- a/docs/superpowers/specs/2026-05-06-zd-refactor-design.md +++ b/docs/superpowers/specs/2026-05-06-zd-refactor-design.md @@ -99,6 +99,8 @@ Same test rewritten: Each test gets a fresh isolated zsh process. No shared state between tests. `zi_test` works identically in both the native tier and inside the Docker container — only `ZI_BIN` and `ZI_DATA` env vars differ. +> **Note on variable interpolation:** `${script}` is interpolated into the outer zsh string before the inner zsh runs, so `$VAR` references in the script body resolve in the inner shell's environment (after sourcing zi). To pass a value from the test's environment into the script, expand it explicitly: `zi_test "zi light ${some_var}"`. + File-level assertions reference `ZI_DATA` directly: ```zsh @@ -146,11 +148,20 @@ RUN apk --no-cache add \ ARG ZI_BRANCH=main RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip +# Install the matrix Zsh version via zi pack at build time. +# ZSH_VERSION is empty for the :latest image (uses Alpine's zsh). +ARG ZSH_VERSION= +RUN if [ -n "${ZSH_VERSION}" ]; then \ + zsh -ilc "zi pack\"${ZSH_VERSION}\" for zsh"; \ + fi + FROM base AS test ARG ZUSER=user ARG PUID=1000 ARG PGID=1000 +# entrypoint.sh: creates $ZUSER, sets up sudo, creates /src /data dirs. +# Dropped from current: wget install.sh, symlink zshenv/zshrc, source init.zsh. COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh && /entrypoint.sh @@ -168,9 +179,10 @@ CMD ["zsh", "-il"] ``` Key fixes vs. current: +- `ARG ZSH_VERSION` is now used: non-empty value installs that exact Zsh version via `zi pack` during build, baking it into the image layer - zi is baked in during `docker build` — no network calls at test time - `VOLUME` declared after `COPY` (current ordering silently discards the copy) -- `entrypoint.sh` only handles user creation — no `wget install.sh` download +- `entrypoint.sh` scope reduced to: create user, set up sudo, create `/src` and `/data` dirs — no `wget install.sh`, no symlinks, no `init.zsh` sourcing - Go removed (not needed for test execution) ### Matrix Workflow @@ -178,19 +190,28 @@ Key fixes vs. current: **Workflow:** `.github/workflows/test-matrix.yml` - Trigger: weekly schedule (`0 3 * * 3`), `workflow_dispatch` only — not on every push -- Matrix: 6 Zsh versions × 5 test files = 30 jobs (`fail-fast: false`) +- Matrix: 6 jobs, one per Zsh version (`fail-fast: false`) +- Each job builds its image once, then runs all test files inside that single container - Buildx layer caching via `type=gha` — repeat runs rebuild only changed layers -**Run per matrix cell:** +**Run per matrix job:** ```sh +# Build once for this Zsh version +docker build \ + --build-arg ZSH_VERSION=${{ matrix.zsh_version }} \ + --tag zd:${{ matrix.zsh_version }} \ + --cache-from type=gha --cache-to type=gha,mode=max \ + . + +# Run all test files in a single container invocation docker run --rm \ -e ZI_DATA=/data \ -v "${RUNNER_TEMP}/zunit:/data" \ zd:${{ matrix.zsh_version }} \ - zsh -c "zunit --tap --verbose /src/tests/${{ matrix.file }}.zunit" + zsh -c "for f in /src/tests/*.zunit; do zunit --tap --verbose \"\$f\"; done" ``` -The native workflow catches regressions on every PR. The matrix workflow verifies Zsh version compatibility on a cadence, not blocking every merge. +This is 6 jobs instead of 30, with one image build per Zsh version instead of five. The native workflow catches regressions on every PR; the matrix workflow verifies Zsh version compatibility on a cadence, not blocking every merge. ## Migration of Existing Tests From d8d6b475a2a2739e5b925155707913c5e85d6f2c Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Wed, 6 May 2026 22:32:55 +0100 Subject: [PATCH 03/47] docs: add zd refactor implementation plan 15-task plan covering zi_test helper, native CI tier, Docker Zsh version matrix, test migration, and cleanup. Co-Authored-By: Claude Sonnet 4.6 --- .../plans/2026-05-06-zd-refactor.md | 1457 +++++++++++++++++ 1 file changed, 1457 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-06-zd-refactor.md diff --git a/docs/superpowers/plans/2026-05-06-zd-refactor.md b/docs/superpowers/plans/2026-05-06-zd-refactor.md new file mode 100644 index 0000000..bc3318e --- /dev/null +++ b/docs/superpowers/plans/2026-05-06-zd-refactor.md @@ -0,0 +1,1457 @@ +# zd Container Refactor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace per-test Docker container spawning with a `zi_test` helper (fresh zsh subprocess per test), and move Docker to a scheduled Zsh-version matrix only. + +**Architecture:** Native ZUnit tests source zi via a `zi_test()` helper that launches `zsh -c` per test — no Docker, no shell-escaping nightmares. Docker is retained for a 6-job weekly matrix (Zsh 5.5.1–5.9) where the image has zi pre-baked at build time. Both tiers share the same `.zunit` files in `tests/`. + +**Tech Stack:** Zsh, ZUnit (zdharma/zunit), Docker/Alpine, GitHub Actions + +--- + +## File Map + +**Create:** +- `tests/helpers.zsh` — `zi_test()` helper + shared env vars +- `tests/setup.zsh` — per-test data dir reset (no sudo) +- `tests/teardown.zsh` — per-test cleanup (no sudo) +- `tests/annexes.zunit` — migrated from `docker/tests/annexes.zunit` +- `tests/ice.zunit` — migrated from `docker/tests/ice.zunit` +- `tests/plugins.zunit` — migrated from `docker/tests/plugins.zunit` +- `tests/snippets.zunit` — migrated from `docker/tests/snippets.zunit` +- `tests/packages.zunit` — migrated from `docker/tests/packages.zunit` +- `scripts/build.sh` — `docker/build.sh` with updated context path +- `scripts/run.sh` — copy of `docker/run.sh` (unchanged) +- `.github/workflows/test-native.yml` — tier-1: native zsh on ubuntu-latest +- `.github/workflows/test-matrix.yml` — tier-2: Docker Zsh version matrix + +**Modify:** +- `docker/Dockerfile` — two-stage; zi + zunit pre-baked; `ZSH_VERSION` used; `VOLUME` after `COPY` +- `docker/entrypoint.sh` — user creation only; drop wget, symlinks, init.zsh +- `docker/zshrc` — source zi directly; drop `prepare_system`/`initiate_system` +- `docker/docker-compose.yml` — context updated to repo root +- `.github/workflows/zunit.yml` — replaced by `test-native.yml` (deleted) + +**Delete (Task 15):** +- `docker/tests/` (entire directory) +- `docker/build.sh`, `docker/run.sh` (moved to `scripts/`) +- `docker/zunit.sh`, `docker/init.zsh` +- `.github/workflows/zunit.yml` + +--- + +## Task 1: Scaffold directories and move scripts + +**Files:** +- Create: `scripts/build.sh` +- Create: `scripts/run.sh` + +- [ ] **Step 1: Create the `tests/` and `scripts/` directories** + +```bash +mkdir -p tests scripts +``` + +- [ ] **Step 2: Write `scripts/build.sh`** + +Differences from `docker/build.sh`: `dockerfile` is now `docker/Dockerfile` (relative to repo root), and the build context is `realpath ..` (repo root, not `docker/`). + +```bash +cat > scripts/build.sh << 'EOF' +#!/usr/bin/env bash +# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=bash sw=2 ts=2 et + +col_error="[31m" +col_info="[32m" +col_rst="[0m" + +say() { + printf '%s\n' "${col_info}${1}${col_rst}" >&2 +} + +err() { + say "${col_error}${1}${col_rst}" >&2 + exit 1 +} + +build() { + command cd -P -- "$(dirname -- "$(command -v -- "$0" || true)")" && pwd -P || exit 9 + + local image_name="${1:-zd}" + local tag="${2:-latest}" + local zsh_version="${3}" + local container_hostname="z-shell" + shift 3 + + local dockerfile="docker/Dockerfile" + + if [[ -n ${zsh_version} ]]; then + tag="zsh${zsh_version}-${tag}" + fi + + say "Building image: ${image_name}" + + local -a args + [[ -n ${NO_CACHE} ]] && args+=(--no-cache "$@") + + if docker build \ + --build-arg "ZUSER=${USER:-$(id -u -n)}" \ + --build-arg "ZHOST=${container_hostname}" \ + --build-arg "PUID=${UID:-$(id -u)}" \ + --build-arg "PGID=${GID:-$(id -g)}" \ + --build-arg "TERM=${TERM:-xterm-256color}" \ + --build-arg "ZSH_VERSION=${zsh_version}" \ + --file "${dockerfile}" \ + --tag "${image_name}:${tag}" \ + "${args[@]}" "$(realpath .. || true)"; then + { + say "To use this image for ZUnit tests run: " + say "export CONTAINER_IMAGE=\"${image_name}\" CONTAINER_TAG=\"${tag}\"" + say "ZUnit run --verbose" + } >&2 + else + err "Container failed to build." + fi +} + +if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then + CONTAINER_IMAGE="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" + BUILD_ZSH_VERSION="${BUILD_ZSH_VERSION-}" + CONTAINER_TAG="${CONTAINER_TAG:-latest}" + NO_CACHE="${NO_CACHE-}" + + while [[ -n $* ]]; do + case "$1" in + --image | -i) + CONTAINER_IMAGE="$2" + shift 2 + ;; + --no-cache | -N) + NO_CACHE=1 + shift + ;; + --zsh-version | -zv | --zv) + BUILD_ZSH_VERSION="${2}" + shift 2 + ;; + *) + break + ;; + esac + done + + build "${CONTAINER_IMAGE}" "${CONTAINER_TAG}" "${BUILD_ZSH_VERSION}" "$@" +fi +EOF +chmod +x scripts/build.sh +``` + +- [ ] **Step 3: Write `scripts/run.sh` (--zunit branch stripped)** + +The `--zunit` branch in `docker/run.sh` mounts `${ROOT_DIR}/zshenv` and `${ROOT_DIR}/zshrc`, which would point to `scripts/zshenv` after the move — files that don't exist. Since tests no longer run through Docker per-test, strip that branch entirely. + +```bash +cat > scripts/run.sh << 'EOF' +#!/usr/bin/env bash +# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=bash sw=2 ts=2 et + +col_error="[31m" +col_info="[32m" +col_rst="[0m" + +say() { + printf '%s\n' "${col_info}${1}${col_rst}" >&2 +} + +err() { + say "${col_error}${1}${col_rst}" >&2 + exit 1 +} + +parent_process() { + local ppid pcmd + ppid="$(ps -o ppid= -p "$$" | awk '{ print $1 }' || true)" + + if [[ -z ${ppid} ]]; then + say "Failed to determine parent process" + return 1 + fi + + if pcmd="$(ps -o cmd= -p "${ppid}")"; then + say "${pcmd}" + return + fi + + return 1 +} + +running_interactively() { + if [[ -n ${CI} ]]; then + return 1 + fi + + if ! [[ -t 1 ]]; then + parent_process | grep -q zunit || true + fi +} + +create_init_config_file() { + local tempfile + + if [[ -z $* ]]; then + return 1 + fi + + tempfile="$(mktemp)" + printf '%s\n' "$*" >"${tempfile}" + printf '%s\n' "${tempfile}" +} + +run() { + local image="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" + local tag="${CONTAINER_TAG:-latest}" + local init_config="$1" + shift + + local -a args=(--rm) + + if running_interactively; then + args+=(--tty=true --interactive=true) + fi + + if [[ -n ${init_config} ]]; then + if [[ -r ${init_config} ]]; then + args+=(--volume "${init_config}:/init.zsh") + else + say "Init config file is not readable" + return 1 + fi + fi + + if [[ -n ${TERM} ]]; then + args+=(--env "TERM=${TERM}") + fi + + if [[ -n ${CONTAINER_ENV[*]} ]]; then + local e + for e in "${CONTAINER_ENV[@]}"; do + args+=(--env "${e}") + done + fi + + if [[ -n ${CONTAINER_VOLUMES[*]} ]]; then + local vol + for vol in "${CONTAINER_VOLUMES[@]}"; do + args+=(--volume "${vol}") + done + fi + + local -a cmd=("$@") + + if [[ -n ${WRAP_CMD} ]]; then + local zsh_opts="ilsc" + [[ -n ${ZSH_DEBUG} ]] && zsh_opts="x${zsh_opts}" + cmd=(zsh "-${zsh_opts}" "${cmd[*]}") + fi + + if [[ -n ${DEBUG} ]]; then + { + say "\$ docker run ${args[*]} ${image}:${tag} ${cmd[*]@Q}" + } >&2 + fi + + docker run "${args[@]}" "${image}:${tag}" "${cmd[@]}" +} + +if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then + CONTAINER_IMAGE=${CONTAINER_IMAGE:-ghcr.io/z-shell/zd} + CONTAINER_TAG="${CONTAINER_TAG:-latest}" + CONTAINER_ENV=() + CONTAINER_VOLUMES=() + DEBUG="${DEBUG-}" + ZSH_DEBUG="${ZSH_DEBUG-}" + INIT_CONFIG_VAL="${INIT_CONFIG_VAL-}" + WRAP_CMD="${WRAP_CMD-}" + + while [[ -n $* ]]; do + case "$1" in + --xsel | -b) + INIT_CONFIG_VAL="$(xsel -b)" + shift + ;; + -c | --config | --init-config | --init) + INIT_CONFIG_VAL="$2" + shift 2 + ;; + -f | --config-file | --init-config-file | --file) + if ! [[ -r $2 ]]; then + say "Unable to read from file: $2" + exit 2 + fi + INIT_CONFIG_VAL="$(cat "$2")" + shift 2 + ;; + -d | --debug) + DEBUG=1 + shift + ;; + -D | --dev | --devel) + DEVEL=1 + shift + ;; + -i | --image) + CONTAINER_IMAGE="$2" + shift 2 + ;; + -t | --tag) + CONTAINER_TAG="$2" + shift 2 + ;; + -e | --env | --environment) + CONTAINER_ENV+=("$2") + shift 2 + ;; + -v | --volume) + CONTAINER_VOLUMES+=("$2") + shift 2 + ;; + -w | --wrap) + WRAP_CMD=1 + shift + ;; + --zsh-debug | -x | -Z) + ZSH_DEBUG=1 + shift + ;; + *) + break + ;; + esac + done + + if INIT_CONFIG="$(create_init_config_file "${INIT_CONFIG_VAL}")"; then + trap 'rm -vf $INIT_CONFIG' EXIT INT + fi + CONTAINER_ROOT="$( + cd -P -- "$(dirname "$0")" + pwd -P + )" || exit 9 + if [[ -n ${DEVEL} ]]; then + CONTAINER_VOLUMES+=( + "${CONTAINER_ROOT}:/src" + ) + fi + + run "${INIT_CONFIG}" "$@" +fi +EOF +chmod +x scripts/run.sh +``` + +- [ ] **Step 4: Verify both scripts are executable** + +```bash +ls -la scripts/ +``` + +Expected: `build.sh` and `run.sh` both show `-rwxr-xr-x`. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/ +git commit -m "feat: add scripts/ directory with build.sh and run.sh" +``` + +--- + +## Task 2: Write tests/helpers.zsh + +**Files:** +- Create: `tests/helpers.zsh` + +- [ ] **Step 1: Write `tests/helpers.zsh`** + +```bash +cat > tests/helpers.zsh << 'EOF' +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +# Run a zi snippet in a fresh isolated zsh subprocess. +# $1 — zsh code to execute (unescaped; single-quote at call site to prevent +# expansion before the function receives it). +# +# Variable interpolation note: ${_zi_bin} and ${_zi_data} are expanded by the +# outer shell when the inner command string is assembled. References to $VAR +# inside the script argument resolve in the *inner* shell after zi is sourced. +# To pass an outer variable's value into the script, let it expand in the +# caller: zi_test "zi light ${my_plugin}" +zi_test() { + local script=$1 + local _zi_bin="${ZI_BIN:-${HOME}/.zi/bin}" + local _zi_data="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" + run zsh -c " + typeset -gxU path + path=( \${HOME}/go/bin \$path ) + typeset -gA ZI + ZI[HOME_DIR]=${_zi_data} + source ${_zi_bin}/zi.zsh + autoload -Uz _zi + ${script} + " +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/helpers.zsh +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/helpers.zsh +git commit -m "feat: add tests/helpers.zsh with zi_test helper" +``` + +--- + +## Task 3: Migrate setup.zsh and teardown.zsh + +**Files:** +- Create: `tests/setup.zsh` +- Create: `tests/teardown.zsh` + +Key changes from `docker/tests/`: +- `DATA_DIR` → `ZI_DATA` (matches the env var `zi_test` uses) +- `PLUGINS_DIR`, `SNIPPETS_DIR`, `ZPFX` dropped — tests now use `${ZI_DATA}/plugins`, `${ZI_DATA}/snippets`, `${ZI_DATA}/polaris` inline +- `sudo rm -rf` → `rm -rf` (native runner owns the temp dir) + +- [ ] **Step 1: Write `tests/setup.zsh`** + +```bash +cat > tests/setup.zsh << 'EOF' +#!/usr/bin/env zunit +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +setup() { + export ZI_DATA="${TMPDIR:-/tmp}/zunit" + + { + color magenta @setup started + color magenta "ZI_DATA=${ZI_DATA}" + } >&2 + + # Wipe plugin/snippet state between tests; keep the dir itself. + rm -rf "${ZI_DATA:?}"/* + mkdir -p "${ZI_DATA}" +} + +# vim: set ft=zsh et ts=2 sw=2 : +EOF +``` + +- [ ] **Step 2: Write `tests/teardown.zsh`** + +```bash +cat > tests/teardown.zsh << 'EOF' +#!/usr/bin/env zunit +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +teardown() { + color cyan @teardown called >&2 + [[ -n "${ZI_DATA}" ]] && rm -rf "${ZI_DATA:?}"/* +} + +# vim: set ft=zsh et ts=2 sw=2 : +EOF +``` + +- [ ] **Step 3: Verify syntax** + +```bash +zsh -n tests/setup.zsh tests/teardown.zsh +``` + +Expected: no output, exit code 0. + +- [ ] **Step 4: Commit** + +```bash +git add tests/setup.zsh tests/teardown.zsh +git commit -m "feat: add tests/setup.zsh and teardown.zsh (no-sudo, ZI_DATA based)" +``` + +--- + +## Task 4: Migrate annexes.zunit + +**Files:** +- Create: `tests/annexes.zunit` + +Changes: `run ./docker/run.sh --wrap --debug --zunit ` → `zi_test ''`; `${PLUGINS_DIR}` → `${ZI_DATA}/plugins`; add `load helpers`. + +Note: `z-a-eval` and `z-a-default-ice` tests assert `$state equals 1` — these are known expected failures (the annexes exit non-zero on load). Keep those assertions as-is. + +- [ ] **Step 1: Write `tests/annexes.zunit`** + +```bash +cat > tests/annexes.zunit << 'EOF' +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'z-a-bin-gem-node installation' { + zi_test 'zi light z-shell/z-a-bin-gem-node' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-meta-plugins installation' { + zi_test 'zi light z-shell/z-a-meta-plugins' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-meta-plugins/z-a-meta-plugins.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-readurl installation' { + zi_test 'zi light z-shell/z-a-readurl' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-readurl/z-a-readurl.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-rust installation' { + zi_test 'zi light z-shell/z-a-rust' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-rust/z-a-rust.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-eval installation' { + zi_test 'zi light z-shell/z-a-eval' + + assert $state equals 1 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-eval/z-a-eval.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-linkbin installation' { + zi_test 'zi light z-shell/z-a-linkbin' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-linkbin/z-a-linkbin.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-default-ice installation' { + zi_test 'zi light z-shell/z-a-default-ice' + + assert $state equals 1 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-default-ice/z-a-default-ice.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-test installation' { + zi_test 'zi light z-shell/z-a-test' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-test/z-a-test.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/annexes.zunit +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/annexes.zunit +git commit -m "feat: migrate annexes.zunit to zi_test helper" +``` + +--- + +## Task 5: Migrate ice.zunit + +**Files:** +- Create: `tests/ice.zunit` + +Changes: multi-line `$'...'` escaped strings → clean zsh in single-quoted `zi_test` argument; `${PLUGINS_DIR}` → `${ZI_DATA}/plugins`; `${ZPFX}` → `${ZI_DATA}/polaris`. + +- [ ] **Step 1: Write `tests/ice.zunit`** + +```bash +cat > tests/ice.zunit << 'EOF' +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'sbin ice' { + zi_test ' + zi light z-shell/z-a-bin-gem-node + zi light-mode as"null" from"gh-r" sbin"fzf" for junegunn/fzf + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable + + artifact="${ZI_DATA}/polaris/bin/fzf" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'failing atclone ice' { + zi_test 'zi null atclone"echo intentional failure; return 255" for z-shell/null' + + assert $state not_equal_to 0 + assert $state equals 255 + assert "$output" contains "intentional failure" +} + +@test 'failing atpull ice' { + zi_test ' + zi id-as"atpull-fail" null \ + atpull"echo intentional failure; return 255" run-atpull \ + for z-shell/null + zi update atpull-fail + ' + + assert $state equals 255 + assert "$output" contains "intentional failure" +} + +@test 'failing mv ice' { + zi_test ' + zi as"command" from"gh-r" bpick"*musl*" mv"DOES_NOT_EXIST* -> fd" pick"fd/fd" \ + for @sharkdp/fd + ' + + assert $state equals 1 + assert "$output" contains "DOES_NOT_EXIST" + assert "$output" contains "didn'\''t match any file" +} + +@test 'mv ice' { + zi_test ' + zi as"command" from"gh-r" bpick"*musl*" mv"fd* -> fd" pick"fd/fd" \ + for @sharkdp/fd + ' + + assert $state equals 0 + + local artifact="${ZI_DATA}/plugins/sharkdp---fd/fd/fd" + assert "$artifact" is_file + assert "$artifact" is_readable + assert "$artifact" is_executable +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/ice.zunit +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/ice.zunit +git commit -m "feat: migrate ice.zunit to zi_test helper" +``` + +--- + +## Task 6: Migrate plugins.zunit + +**Files:** +- Create: `tests/plugins.zunit` + +- [ ] **Step 1: Write `tests/plugins.zunit`** + +```bash +cat > tests/plugins.zunit << 'EOF' +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi fzf installation' { + zi_test 'zi lucid as"program" from"gh-r" for junegunn/fzf' + + assert $state equals 0 + assert "$output" contains "Unpacking" + assert "$output" contains "Successfully" + + local artifact="${ZI_DATA}/plugins/junegunn---fzf/fzf" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'zi direnv installation' { + zi_test ' + zi light-mode as"program" \ + atclone"go install github.com/cpuguy83/go-md2man/v2@latest" \ + make for @direnv/direnv + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "go: downloading github.com" + + local artifact="${ZI_DATA}/plugins/direnv---direnv/direnv" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'zi diff-so-fancy installation' { + zi_test ' + zi light-mode for \ + as"program" pick"bin/git-dsf" \ + z-shell/zsh-diff-so-fancy + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Cloning into" + + local artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/git-dsf" + assert "$artifact" is_file + assert "$artifact" is_executable + + artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/diff-so-fancy" + assert "$artifact" is_file + assert "$artifact" is_executable +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/plugins.zunit +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/plugins.zunit +git commit -m "feat: migrate plugins.zunit to zi_test helper" +``` + +--- + +## Task 7: Migrate snippets.zunit + +**Files:** +- Create: `tests/snippets.zunit` + +Changes: `${SNIPPETS_DIR}` → `${ZI_DATA}/snippets`. + +- [ ] **Step 1: Write `tests/snippets.zunit`** + +```bash +cat > tests/snippets.zunit << 'EOF' +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi OMZL::spectrum.zsh installation' { + zi_test 'zi snippet OMZL::spectrum.zsh' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/OMZL::spectrum.zsh/OMZL::spectrum.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'zi OMZP::git installation' { + zi_test 'zi snippet OMZP::git' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/OMZP::git/OMZP::git" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'zi PZTM::environment installation' { + zi_test 'zi snippet PZTM::environment' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/PZTM::environment/PZTM::environment" + assert "$artifact" is_file + assert "$artifact" is_readable +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/snippets.zunit +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/snippets.zunit +git commit -m "feat: migrate snippets.zunit to zi_test helper" +``` + +--- + +## Task 8: Migrate packages.zunit + +**Files:** +- Create: `tests/packages.zunit` + +- [ ] **Step 1: Write `tests/packages.zunit`** + +```bash +cat > tests/packages.zunit << 'EOF' +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi package ls_colors' { + zi_test 'zi pack for ls_colors' + + assert $state equals 0 + assert "$output" contains "Package" + + local artifact="${ZI_DATA}/plugins/ls_colors/LS_COLORS" + assert "$artifact" is_file + assert "$artifact" is_readable +} +EOF +``` + +- [ ] **Step 2: Verify syntax** + +```bash +zsh -n tests/packages.zunit +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add tests/packages.zunit +git commit -m "feat: migrate packages.zunit to zi_test helper" +``` + +--- + +## Task 9: Refactor docker/entrypoint.sh + +**Files:** +- Modify: `docker/entrypoint.sh` + +Strip to user creation, sudo setup, and directory creation only. Remove: `wget install.sh`, symlinks to `/src/zshenv` and `/src/zshrc`, `init.zsh` sourcing. + +- [ ] **Step 1: Overwrite `docker/entrypoint.sh`** + +```bash +cat > docker/entrypoint.sh << 'EOF' +#!/usr/bin/env sh + +HOME="/home/${ZUSER}" +export HOME + +command sed -i -r 's#^(root:.+):/bin/ash#\1:/bin/zsh#' /etc/passwd +command adduser -D -s /bin/zsh -u "${PUID}" -h "${HOME}" "${ZUSER}" + +command printf '%s' "${ZUSER} ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user +command mkdir -p /src /data +command chown -R "${PUID}:${PGID}" /src /data +EOF +``` + +- [ ] **Step 2: Verify the file is syntactically valid sh** + +```bash +sh -n docker/entrypoint.sh +``` + +Expected: no output, exit code 0. + +- [ ] **Step 3: Commit** + +```bash +git add docker/entrypoint.sh +git commit -m "refactor: entrypoint.sh — user creation only, drop runtime downloads" +``` + +--- + +## Task 10: Refactor docker/Dockerfile + +**Files:** +- Modify: `docker/Dockerfile` + +Key changes: +- Add `go` to `apk add` (needed for `go install` in direnv test and for `zunit` build) +- Install `zunit`, `revolver`, and `color` into `/usr/local/bin` at build time +- Install zi as `$ZUSER` at build time (not via `install.sh` at runtime) +- Use `ARG ZSH_VERSION` to optionally install a specific Zsh version via `zi pack` at build time +- Move `VOLUME` after all `COPY` instructions +- Remove Go from-upstream download (use `apk add go` instead) + +- [ ] **Step 1: Overwrite `docker/Dockerfile`** + +```bash +cat > docker/Dockerfile << 'EOF' +ARG ALPINE_VERSION=edge +FROM alpine:${ALPINE_VERSION} AS base + +LABEL maintainer="Z-Shell Community" +LABEL email="team@zshell.dev" + +ARG TERM=xterm +ENV TERM=${TERM} + +RUN set -ex && apk --no-cache add \ + alpine-zsh-config \ + ncurses-dev \ + build-base \ + coreutils \ + pcre-dev \ + zlib-dev \ + autoconf \ + libuser \ + rsync \ + bash \ + curl \ + sudo \ + go \ + zsh \ + git \ + vim \ + jq + +# Install zunit and its helpers into /usr/local/bin at build time. +# go is required to compile zunit from source. +RUN set -ex \ + && git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git \ + && cd /tmp/zunit.git && ./build.zsh \ + && mv /tmp/zunit.git/zunit /usr/local/bin/zunit \ + && curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ + > /usr/local/bin/revolver \ + && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ + > /usr/local/bin/color \ + && chmod u+x /usr/local/bin/{color,revolver,zunit} \ + && rm -rf /tmp/zunit.git + +FROM base AS test + +ARG ZUSER=user +ARG PUID=1000 +ARG PGID=1000 +ARG ZHOST=zi-docker + +ENV PUID=${PUID} +ENV PGID=${PGID} +ENV ZUSER=${ZUSER} +ENV HOST=${ZHOST} + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && /entrypoint.sh + +# Install zi as $ZUSER at build time — no network calls at test time. +USER ${ZUSER} +ARG ZI_BRANCH=main +RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + +# Optionally install a specific Zsh version via zi pack at build time. +# Leave ZSH_VERSION empty for the :latest image (uses Alpine's zsh). +ARG ZSH_VERSION= +RUN [ -z "${ZSH_VERSION}" ] || \ + zsh -c "source \${HOME}/.zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" + +# Switch back to root for COPY operations. +USER root +COPY docker/zshenv /home/${ZUSER}/.zshenv +COPY docker/zshrc /home/${ZUSER}/.zshrc +COPY utils.zsh /src/utils.zsh +COPY tests/ /src/tests/ +RUN chown -R ${PUID}:${PGID} /home/${ZUSER}/.zshenv /home/${ZUSER}/.zshrc /src + +# VOLUME declared after all COPYs — declaring before COPY silently discards copied files. +VOLUME ["/data"] + +USER ${ZUSER} +WORKDIR /home/${ZUSER} + +CMD ["zsh", "-il"] +EOF +``` + +- [ ] **Step 2: Verify the Dockerfile passes hadolint (if available locally)** + +```bash +hadolint docker/Dockerfile || echo "hadolint not installed — skip" +``` + +- [ ] **Step 3: Commit** + +```bash +git add docker/Dockerfile +git commit -m "refactor: Dockerfile — two-stage, zi pre-baked, ZSH_VERSION via zi pack, VOLUME after COPY" +``` + +--- + +## Task 11: Update docker/zshrc and docker/zshenv + +**Files:** +- Modify: `docker/zshrc` +- Modify: `docker/zshenv` + +Remove `prepare_system; initiate_system` from `zshrc` — these relied on `/static/` which no longer exists in the image. Source zi directly. Keep `utils.zsh` sourced for interactive convenience functions. + +- [ ] **Step 1: Overwrite `docker/zshrc`** + +```bash +cat > docker/zshrc << 'EOF' +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +# Source zi (pre-installed during docker build). +typeset -gA ZI +ZI[HOME_DIR]="${ZI_DATA:-/data}" +source "${HOME}/.zi/bin/zi.zsh" +autoload -Uz _zi +(( ${+_comps} )) && _comps[zi]=_zi + +# Load interactive convenience wrappers. +[[ -f /src/utils.zsh ]] && source /src/utils.zsh +EOF +``` + +- [ ] **Step 2: Overwrite `docker/zshenv`** + +```bash +cat > docker/zshenv << 'EOF' +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +export TERM=${TERM:-xterm-256color} +export SHELL=${SHELL:-${commands[zsh]}} +export ZI_DATA=${ZI_DATA:-/data} +EOF +``` + +- [ ] **Step 3: Verify syntax** + +```bash +zsh -n docker/zshrc docker/zshenv +``` + +Expected: no output, exit code 0. + +- [ ] **Step 4: Commit** + +```bash +git add docker/zshrc docker/zshenv +git commit -m "refactor: zshrc sources zi directly; drop prepare_system/initiate_system" +``` + +--- + +## Task 12: Update docker/docker-compose.yml + +**Files:** +- Modify: `docker/docker-compose.yml` + +The build context must be the repo root (parent of `docker/`) so that `COPY tests/` and `COPY utils.zsh` resolve correctly in the Dockerfile. + +- [ ] **Step 1: Overwrite `docker/docker-compose.yml`** + +```bash +cat > docker/docker-compose.yml << 'EOF' +version: "3.9" + +services: + zd: + build: + context: .. + dockerfile: docker/Dockerfile + stdin_open: true + tty: true + container_name: zd + environment: + - TERM=xterm-256color + volumes: + - $PWD/..:/src + hostname: zi@docker +EOF +``` + +- [ ] **Step 2: Verify the file is valid YAML** + +```bash +python3 -c "import yaml, sys; yaml.safe_load(open('docker/docker-compose.yml'))" && echo "OK" +``` + +Expected: `OK`. + +- [ ] **Step 3: Commit** + +```bash +git add docker/docker-compose.yml +git commit -m "fix: docker-compose context updated to repo root for COPY tests/ to work" +``` + +--- + +## Task 13: Write .github/workflows/test-native.yml + +**Files:** +- Create: `.github/workflows/test-native.yml` + +This replaces the functionality of `zunit.yml` for day-to-day CI. Matrix is one job per `.zunit` file. Triggers on push/PR to `main` and weekly schedule. + +- [ ] **Step 1: Write `.github/workflows/test-native.yml`** + +```bash +cat > .github/workflows/test-native.yml << 'EOF' +name: "ZUnit (native)" + +on: + push: + branches: [main] + paths: + - "tests/**" + - "utils.zsh" + pull_request: + branches: [main] + schedule: + - cron: "0 12 * * 1" + workflow_dispatch: + +jobs: + zunit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + file: [annexes, ice, packages, plugins, snippets] + steps: + - uses: actions/checkout@v4 + + - name: Install zsh + run: sudo apt-get update && sudo apt-get install -yq zsh + + - name: Install zunit + run: | + mkdir -p bin + curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' > bin/revolver + curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' > bin/color + git clone --depth 1 https://github.com/zdharma/zunit.git zunit.git + cd zunit.git && ./build.zsh && cd .. + mv zunit.git/zunit bin/ + chmod u+x bin/{color,revolver,zunit} + + - name: Install zi + run: zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + + - name: "ZUnit: ${{ matrix.file }}" + run: | + export PATH="$PWD/bin:$PATH" + export TERM=xterm + export ZI_BIN="${HOME}/.zi/bin" + export ZI_DATA="${RUNNER_TEMP}/zunit" + zunit --tap --verbose "tests/${{ matrix.file }}.zunit" +EOF +``` + +- [ ] **Step 2: Verify the file is valid YAML** + +```bash +python3 -c "import yaml, sys; yaml.safe_load(open('.github/workflows/test-native.yml'))" && echo "OK" +``` + +Expected: `OK`. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/test-native.yml +git commit -m "feat: add test-native.yml — native ZUnit CI without Docker" +``` + +--- + +## Task 14: Write .github/workflows/test-matrix.yml + +**Files:** +- Create: `.github/workflows/test-matrix.yml` + +6 jobs, one per Zsh version. Each builds its Docker image once (with `ZSH_VERSION` baked in) then runs all test files in a single container invocation. Runs on schedule and `workflow_dispatch` only — not on every push. + +- [ ] **Step 1: Write `.github/workflows/test-matrix.yml`** + +```bash +cat > .github/workflows/test-matrix.yml << 'EOF' +name: "ZUnit (Zsh matrix)" + +on: + schedule: + - cron: "0 3 * * 3" + workflow_dispatch: + +jobs: + zunit-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + zsh_version: ["5.5.1", "5.6.2", "5.7.1", "5.8", "5.8.1", "5.9"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: "Build image for Zsh ${{ matrix.zsh_version }}" + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + load: true + build-args: ZSH_VERSION=${{ matrix.zsh_version }} + tags: zd:${{ matrix.zsh_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: "Run all tests in Zsh ${{ matrix.zsh_version }} container" + run: | + mkdir -p "${RUNNER_TEMP}/zunit" + docker run --rm \ + --env TERM=xterm \ + --env ZI_DATA=/data \ + --volume "${RUNNER_TEMP}/zunit:/data" \ + "zd:${{ matrix.zsh_version }}" \ + zsh -c 'for f in /src/tests/*.zunit; do zunit --tap --verbose "$f" || exit $?; done' +EOF +``` + +- [ ] **Step 2: Verify the file is valid YAML** + +```bash +python3 -c "import yaml, sys; yaml.safe_load(open('.github/workflows/test-matrix.yml'))" && echo "OK" +``` + +Expected: `OK`. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/test-matrix.yml +git commit -m "feat: add test-matrix.yml — Docker Zsh version matrix (scheduled)" +``` + +--- + +## Task 15: Clean up old files + +**Files:** +- Delete: `docker/tests/` (entire directory) +- Delete: `docker/build.sh`, `docker/run.sh` (moved to `scripts/`) +- Delete: `docker/zunit.sh`, `docker/init.zsh` +- Delete: `.github/workflows/zunit.yml` (superseded by `test-native.yml`) + +- [ ] **Step 1: Remove old test directory and superseded scripts** + +```bash +git rm -r docker/tests/ +git rm docker/build.sh docker/run.sh docker/zunit.sh docker/init.zsh +``` + +- [ ] **Step 2: Remove old zunit workflow** + +```bash +git rm .github/workflows/zunit.yml +``` + +- [ ] **Step 3: Verify nothing in the repo still references the deleted paths** + +```bash +grep -r 'docker/tests' . --include='*.yml' --include='*.sh' --include='*.zsh' --include='*.md' \ + --exclude-dir=.git --exclude-dir=docs 2>/dev/null || echo "No references found" + +grep -r 'docker/build\.sh\|docker/run\.sh\|docker/zunit\.sh\|docker/init\.zsh' . \ + --include='*.yml' --include='*.sh' --include='*.zsh' --include='*.md' \ + --exclude-dir=.git --exclude-dir=docs 2>/dev/null || echo "No references found" +``` + +Expected: `No references found` for both. + +- [ ] **Step 4: Commit** + +```bash +git commit -m "chore: remove docker/tests/, old scripts, and superseded zunit.yml workflow" +``` + +--- + +## Local Test Verification (reference) + +After Task 8, run the full native test suite locally (requires zi and zunit installed): + +```bash +# Install zunit if not present +mkdir -p bin +git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git +cd /tmp/zunit.git && ./build.zsh && cp zunit ~/bin/ && cd - + +# Run all test files +export PATH="$HOME/bin:$PATH" +export ZI_BIN="${HOME}/.zi/bin" +export ZI_DATA="/tmp/zunit-local" +for f in tests/*.zunit; do + echo "=== $f ===" + zunit --verbose "$f" +done +``` + +To run a single file during development: + +```bash +ZI_BIN="${HOME}/.zi/bin" ZI_DATA="/tmp/zunit-local" zunit --verbose tests/ice.zunit +``` From ff5445a12da6478f80b1340ee50be5351d03f980 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 7 May 2026 18:04:27 +0100 Subject: [PATCH 04/47] feat: add scripts/ directory with build.sh and run.sh --- scripts/build.sh | 85 +++++++++++++++++++++ scripts/run.sh | 193 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100755 scripts/build.sh create mode 100755 scripts/run.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..3408dc8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=bash sw=2 ts=2 et + +col_error="[31m" +col_info="[32m" +col_rst="[0m" + +say() { + printf '%s\n' "${col_info}${1}${col_rst}" >&2 +} + +err() { + say "${col_error}${1}${col_rst}" >&2 + exit 1 +} + +build() { + command cd -P -- "$(dirname -- "$(command -v -- "$0" || true)")" && pwd -P || exit 9 + + local image_name="${1:-zd}" + local tag="${2:-latest}" + local zsh_version="${3}" + local container_hostname="z-shell" + shift 3 + + local dockerfile="docker/Dockerfile" + + if [[ -n ${zsh_version} ]]; then + tag="zsh${zsh_version}-${tag}" + fi + + say "Building image: ${image_name}" + + local -a args + [[ -n ${NO_CACHE} ]] && args+=(--no-cache "$@") + + if docker build \ + --build-arg "ZUSER=${USER:-$(id -u -n)}" \ + --build-arg "ZHOST=${container_hostname}" \ + --build-arg "PUID=${UID:-$(id -u)}" \ + --build-arg "PGID=${GID:-$(id -g)}" \ + --build-arg "TERM=${TERM:-xterm-256color}" \ + --build-arg "ZSH_VERSION=${zsh_version}" \ + --file "${dockerfile}" \ + --tag "${image_name}:${tag}" \ + "${args[@]}" "$(realpath .. || true)"; then + { + say "To use this image for ZUnit tests run: " + say "export CONTAINER_IMAGE=\"${image_name}\" CONTAINER_TAG=\"${tag}\"" + say "ZUnit run --verbose" + } >&2 + else + err "Container failed to build." + fi +} + +if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then + CONTAINER_IMAGE="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" + BUILD_ZSH_VERSION="${BUILD_ZSH_VERSION-}" + CONTAINER_TAG="${CONTAINER_TAG:-latest}" + NO_CACHE="${NO_CACHE-}" + + while [[ -n $* ]]; do + case "$1" in + --image | -i) + CONTAINER_IMAGE="$2" + shift 2 + ;; + --no-cache | -N) + NO_CACHE=1 + shift + ;; + --zsh-version | -zv | --zv) + BUILD_ZSH_VERSION="${2}" + shift 2 + ;; + *) + break + ;; + esac + done + + build "${CONTAINER_IMAGE}" "${CONTAINER_TAG}" "${BUILD_ZSH_VERSION}" "$@" +fi diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..6728a98 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=bash sw=2 ts=2 et + +col_error="[31m" +col_info="[32m" +col_rst="[0m" + +say() { + printf '%s\n' "${col_info}${1}${col_rst}" >&2 +} + +err() { + say "${col_error}${1}${col_rst}" >&2 + exit 1 +} + +parent_process() { + local ppid pcmd + ppid="$(ps -o ppid= -p "$$" | awk '{ print $1 }' || true)" + + if [[ -z ${ppid} ]]; then + say "Failed to determine parent process" + return 1 + fi + + if pcmd="$(ps -o cmd= -p "${ppid}")"; then + say "${pcmd}" + return + fi + + return 1 +} + +running_interactively() { + if [[ -n ${CI} ]]; then + return 1 + fi + + if ! [[ -t 1 ]]; then + parent_process | grep -q zunit || true + fi +} + +create_init_config_file() { + local tempfile + + if [[ -z $* ]]; then + return 1 + fi + + tempfile="$(mktemp)" + printf '%s\n' "$*" >"${tempfile}" + printf '%s\n' "${tempfile}" +} + +run() { + local image="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" + local tag="${CONTAINER_TAG:-latest}" + local init_config="$1" + shift + + local -a args=(--rm) + + if running_interactively; then + args+=(--tty=true --interactive=true) + fi + + if [[ -n ${init_config} ]]; then + if [[ -r ${init_config} ]]; then + args+=(--volume "${init_config}:/init.zsh") + else + say "Init config file is not readable" + return 1 + fi + fi + + if [[ -n ${TERM} ]]; then + args+=(--env "TERM=${TERM}") + fi + + if [[ -n ${CONTAINER_ENV[*]} ]]; then + local e + for e in "${CONTAINER_ENV[@]}"; do + args+=(--env "${e}") + done + fi + + if [[ -n ${CONTAINER_VOLUMES[*]} ]]; then + local vol + for vol in "${CONTAINER_VOLUMES[@]}"; do + args+=(--volume "${vol}") + done + fi + + local -a cmd=("$@") + + if [[ -n ${WRAP_CMD} ]]; then + local zsh_opts="ilsc" + [[ -n ${ZSH_DEBUG} ]] && zsh_opts="x${zsh_opts}" + cmd=(zsh "-${zsh_opts}" "${cmd[*]}") + fi + + if [[ -n ${DEBUG} ]]; then + { + say "\$ docker run ${args[*]} ${image}:${tag} ${cmd[*]@Q}" + } >&2 + fi + + docker run "${args[@]}" "${image}:${tag}" "${cmd[@]}" +} + +if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then + CONTAINER_IMAGE=${CONTAINER_IMAGE:-ghcr.io/z-shell/zd} + CONTAINER_TAG="${CONTAINER_TAG:-latest}" + CONTAINER_ENV=() + CONTAINER_VOLUMES=() + DEBUG="${DEBUG-}" + ZSH_DEBUG="${ZSH_DEBUG-}" + INIT_CONFIG_VAL="${INIT_CONFIG_VAL-}" + WRAP_CMD="${WRAP_CMD-}" + + while [[ -n $* ]]; do + case "$1" in + --xsel | -b) + INIT_CONFIG_VAL="$(xsel -b)" + shift + ;; + -c | --config | --init-config | --init) + INIT_CONFIG_VAL="$2" + shift 2 + ;; + -f | --config-file | --init-config-file | --file) + if ! [[ -r $2 ]]; then + say "Unable to read from file: $2" + exit 2 + fi + INIT_CONFIG_VAL="$(cat "$2")" + shift 2 + ;; + -d | --debug) + DEBUG=1 + shift + ;; + -D | --dev | --devel) + DEVEL=1 + shift + ;; + -i | --image) + CONTAINER_IMAGE="$2" + shift 2 + ;; + -t | --tag) + CONTAINER_TAG="$2" + shift 2 + ;; + -e | --env | --environment) + CONTAINER_ENV+=("$2") + shift 2 + ;; + -v | --volume) + CONTAINER_VOLUMES+=("$2") + shift 2 + ;; + -w | --wrap) + WRAP_CMD=1 + shift + ;; + --zsh-debug | -x | -Z) + ZSH_DEBUG=1 + shift + ;; + *) + break + ;; + esac + done + + if INIT_CONFIG="$(create_init_config_file "${INIT_CONFIG_VAL}")"; then + trap 'rm -vf $INIT_CONFIG' EXIT INT + fi + CONTAINER_ROOT="$( + cd -P -- "$(dirname "$0")" + pwd -P + )" || exit 9 + if [[ -n ${DEVEL} ]]; then + CONTAINER_VOLUMES+=( + "${CONTAINER_ROOT}:/src" + ) + fi + + run "${INIT_CONFIG}" "$@" +fi From 77653c43b4a98da358341917f2a01acc665a49de Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sun, 10 May 2026 21:45:20 +0100 Subject: [PATCH 05/47] fix: correct Dockerfile path, extra args, CONTAINER_ROOT, and DEVEL init in scripts/ - build.sh: Change dockerfile path from "docker/Dockerfile" to "../docker/Dockerfile" (resolves relative to scripts/ after cd) - build.sh: Separate NO_CACHE flag from extra args to prevent silent loss of arguments - run.sh: Add ".." to CONTAINER_ROOT navigation to mount repo root instead of just scripts/ with --dev - run.sh: Initialize DEVEL variable like other boolean flags to prevent environment inheritance --- scripts/build.sh | 5 +++-- scripts/run.sh | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 3408dc8..befe6c1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -24,7 +24,7 @@ build() { local container_hostname="z-shell" shift 3 - local dockerfile="docker/Dockerfile" + local dockerfile="../docker/Dockerfile" if [[ -n ${zsh_version} ]]; then tag="zsh${zsh_version}-${tag}" @@ -33,7 +33,8 @@ build() { say "Building image: ${image_name}" local -a args - [[ -n ${NO_CACHE} ]] && args+=(--no-cache "$@") + [[ -n ${NO_CACHE} ]] && args+=(--no-cache) + args+=("$@") if docker build \ --build-arg "ZUSER=${USER:-$(id -u -n)}" \ diff --git a/scripts/run.sh b/scripts/run.sh index 6728a98..1a90001 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -117,6 +117,7 @@ if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then CONTAINER_VOLUMES=() DEBUG="${DEBUG-}" ZSH_DEBUG="${ZSH_DEBUG-}" + DEVEL="${DEVEL-}" INIT_CONFIG_VAL="${INIT_CONFIG_VAL-}" WRAP_CMD="${WRAP_CMD-}" @@ -180,7 +181,7 @@ if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then trap 'rm -vf $INIT_CONFIG' EXIT INT fi CONTAINER_ROOT="$( - cd -P -- "$(dirname "$0")" + cd -P -- "$(dirname "$0")/.." pwd -P )" || exit 9 if [[ -n ${DEVEL} ]]; then From 93b3f2fe837161038b5e07e8a78945eb6d4ab852 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Tue, 12 May 2026 22:10:55 +0100 Subject: [PATCH 06/47] feat: add tests/helpers.zsh with zi_test helper --- tests/helpers.zsh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/helpers.zsh diff --git a/tests/helpers.zsh b/tests/helpers.zsh new file mode 100644 index 0000000..10d4fd5 --- /dev/null +++ b/tests/helpers.zsh @@ -0,0 +1,26 @@ +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +# Run a zi snippet in a fresh isolated zsh subprocess. +# $1 — zsh code to execute (unescaped; single-quote at call site to prevent +# expansion before the function receives it). +# +# Variable interpolation note: ${_zi_bin} and ${_zi_data} are expanded by the +# outer shell when the inner command string is assembled. References to $VAR +# inside the script argument resolve in the *inner* shell after zi is sourced. +# To pass an outer variable's value into the script, let it expand in the +# caller: zi_test "zi light ${my_plugin}" +zi_test() { + local script=$1 + local _zi_bin="${ZI_BIN:-${HOME}/.zi/bin}" + local _zi_data="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" + run zsh -c " + typeset -gxU path + path=( \${HOME}/go/bin \$path ) + typeset -gA ZI + ZI[HOME_DIR]=${_zi_data} + source ${_zi_bin}/zi.zsh + autoload -Uz _zi + ${script} + " +} From 2260ef2081987c7019a5594b419c197f260bcdaa Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Tue, 12 May 2026 22:14:19 +0100 Subject: [PATCH 07/47] feat: add tests/setup.zsh and teardown.zsh (no-sudo, ZI_DATA based) --- tests/setup.zsh | 16 ++++++++++++++++ tests/teardown.zsh | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/setup.zsh create mode 100644 tests/teardown.zsh diff --git a/tests/setup.zsh b/tests/setup.zsh new file mode 100644 index 0000000..eaeacf3 --- /dev/null +++ b/tests/setup.zsh @@ -0,0 +1,16 @@ +#!/usr/bin/env zunit +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +setup() { + export ZI_DATA="${TMPDIR:-/tmp}/zunit" + + { + color magenta @setup started + color magenta "ZI_DATA=${ZI_DATA}" + } >&2 + + # Wipe plugin/snippet state between tests; keep the dir itself. + rm -rf "${ZI_DATA:?}"/* + mkdir -p "${ZI_DATA}" +} diff --git a/tests/teardown.zsh b/tests/teardown.zsh new file mode 100644 index 0000000..f7b7455 --- /dev/null +++ b/tests/teardown.zsh @@ -0,0 +1,8 @@ +#!/usr/bin/env zunit +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +teardown() { + color cyan @teardown called >&2 + [[ -n "${ZI_DATA}" ]] && rm -rf "${ZI_DATA:?}"/* +} From 09b7e18a5bafa450cd463475c2c2f1ae4fc6016c Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:29:48 +0100 Subject: [PATCH 08/47] feat: migrate annexes.zunit to zi_test helper --- tests/annexes.zunit | 111 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/annexes.zunit diff --git a/tests/annexes.zunit b/tests/annexes.zunit new file mode 100644 index 0000000..ae0fc50 --- /dev/null +++ b/tests/annexes.zunit @@ -0,0 +1,111 @@ +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + setup +} + +@teardown { + load teardown + teardown +} + +@test 'z-a-bin-gem-node installation' { + zi_test 'zi light z-shell/z-a-bin-gem-node' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-meta-plugins installation' { + zi_test 'zi light z-shell/z-a-meta-plugins' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-meta-plugins/z-a-meta-plugins.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-readurl installation' { + zi_test 'zi light z-shell/z-a-readurl' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-readurl/z-a-readurl.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-rust installation' { + zi_test 'zi light z-shell/z-a-rust' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-rust/z-a-rust.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-eval installation' { + zi_test 'zi light z-shell/z-a-eval' + + assert $state equals 1 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-eval/z-a-eval.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-linkbin installation' { + zi_test 'zi light z-shell/z-a-linkbin' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-linkbin/z-a-linkbin.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-default-ice installation' { + zi_test 'zi light z-shell/z-a-default-ice' + + assert $state equals 1 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-default-ice/z-a-default-ice.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'z-a-test installation' { + zi_test 'zi light z-shell/z-a-test' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Compiling" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-test/z-a-test.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} From c63fe05ad778d1c554b6e3100d6ef03995bae24c Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:31:10 +0100 Subject: [PATCH 09/47] fix: add load helpers to annexes.zunit @setup block --- tests/annexes.zunit | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/annexes.zunit b/tests/annexes.zunit index ae0fc50..65aeb79 100644 --- a/tests/annexes.zunit +++ b/tests/annexes.zunit @@ -6,6 +6,7 @@ @setup { load setup + load helpers setup } From 04f5a70cb7bb0bd59fa0f88236e1e88f3dd6c042 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:32:15 +0100 Subject: [PATCH 10/47] feat: migrate ice.zunit to zi_test helper --- tests/ice.zunit | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/ice.zunit diff --git a/tests/ice.zunit b/tests/ice.zunit new file mode 100644 index 0000000..e71231a --- /dev/null +++ b/tests/ice.zunit @@ -0,0 +1,79 @@ +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'sbin ice' { + zi_test ' + zi light z-shell/z-a-bin-gem-node + zi light-mode as"null" from"gh-r" sbin"fzf" for junegunn/fzf + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable + + artifact="${ZI_DATA}/polaris/bin/fzf" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'failing atclone ice' { + zi_test 'zi null atclone"echo intentional failure; return 255" for z-shell/null' + + assert $state not_equal_to 0 + assert $state equals 255 + assert "$output" contains "intentional failure" +} + +@test 'failing atpull ice' { + zi_test ' + zi id-as"atpull-fail" null \ + atpull"echo intentional failure; return 255" run-atpull \ + for z-shell/null + zi update atpull-fail + ' + + assert $state equals 255 + assert "$output" contains "intentional failure" +} + +@test 'failing mv ice' { + zi_test ' + zi as"command" from"gh-r" bpick"*musl*" mv"DOES_NOT_EXIST* -> fd" pick"fd/fd" \ + for @sharkdp/fd + ' + + assert $state equals 1 + assert "$output" contains "DOES_NOT_EXIST" + assert "$output" contains "didn'\''t match any file" +} + +@test 'mv ice' { + zi_test ' + zi as"command" from"gh-r" bpick"*musl*" mv"fd* -> fd" pick"fd/fd" \ + for @sharkdp/fd + ' + + assert $state equals 0 + + local artifact="${ZI_DATA}/plugins/sharkdp---fd/fd/fd" + assert "$artifact" is_file + assert "$artifact" is_readable + assert "$artifact" is_executable +} From 08ad6c0d8d5807300df1a54d6f7850c5193644f7 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:34:45 +0100 Subject: [PATCH 11/47] feat: migrate plugins.zunit to zi_test helper --- tests/plugins.zunit | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/plugins.zunit diff --git a/tests/plugins.zunit b/tests/plugins.zunit new file mode 100644 index 0000000..517ff51 --- /dev/null +++ b/tests/plugins.zunit @@ -0,0 +1,64 @@ +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi fzf installation' { + zi_test 'zi lucid as"program" from"gh-r" for junegunn/fzf' + + assert $state equals 0 + assert "$output" contains "Unpacking" + assert "$output" contains "Successfully" + + local artifact="${ZI_DATA}/plugins/junegunn---fzf/fzf" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'zi direnv installation' { + zi_test ' + zi light-mode as"program" \ + atclone"go install github.com/cpuguy83/go-md2man/v2@latest" \ + make for @direnv/direnv + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "go: downloading github.com" + + local artifact="${ZI_DATA}/plugins/direnv---direnv/direnv" + assert "$artifact" is_file + assert "$artifact" is_executable +} + +@test 'zi diff-so-fancy installation' { + zi_test ' + zi light-mode for \ + as"program" pick"bin/git-dsf" \ + z-shell/zsh-diff-so-fancy + ' + + assert $state equals 0 + assert "$output" contains "Downloading" + assert "$output" contains "Cloning into" + + local artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/git-dsf" + assert "$artifact" is_file + assert "$artifact" is_executable + + artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/diff-so-fancy" + assert "$artifact" is_file + assert "$artifact" is_executable +} From d4f4acf749ec03fb3b0d4b6f4ee536d60c4a147f Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:34:45 +0100 Subject: [PATCH 12/47] feat: migrate snippets.zunit to zi_test helper --- tests/snippets.zunit | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/snippets.zunit diff --git a/tests/snippets.zunit b/tests/snippets.zunit new file mode 100644 index 0000000..75b88aa --- /dev/null +++ b/tests/snippets.zunit @@ -0,0 +1,49 @@ +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi OMZL::spectrum.zsh installation' { + zi_test 'zi snippet OMZL::spectrum.zsh' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/OMZL::spectrum.zsh/OMZL::spectrum.zsh" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'zi OMZP::git installation' { + zi_test 'zi snippet OMZP::git' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/OMZP::git/OMZP::git" + assert "$artifact" is_file + assert "$artifact" is_readable +} + +@test 'zi PZTM::environment installation' { + zi_test 'zi snippet PZTM::environment' + + assert $state equals 0 + assert "$output" contains "Downloading" + + local artifact="${ZI_DATA}/snippets/PZTM::environment/PZTM::environment" + assert "$artifact" is_file + assert "$artifact" is_readable +} From 9df73bf88ffc6a5f1c3c8758a35e81f558a2d587 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:34:46 +0100 Subject: [PATCH 13/47] feat: migrate packages.zunit to zi_test helper --- tests/packages.zunit | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/packages.zunit diff --git a/tests/packages.zunit b/tests/packages.zunit new file mode 100644 index 0000000..1068eb2 --- /dev/null +++ b/tests/packages.zunit @@ -0,0 +1,27 @@ +#!/usr/bin/env zunit +# +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# + +@setup { + load setup + load helpers + setup +} + +@teardown { + load teardown + teardown +} + +@test 'zi package ls_colors' { + zi_test 'zi pack for ls_colors' + + assert $state equals 0 + assert "$output" contains "Package" + + local artifact="${ZI_DATA}/plugins/ls_colors/LS_COLORS" + assert "$artifact" is_file + assert "$artifact" is_readable +} From b21671333222d73fa60a46f036cbab9933990ed6 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:36:31 +0100 Subject: [PATCH 14/47] =?UTF-8?q?refactor:=20entrypoint.sh=20=E2=80=94=20u?= =?UTF-8?q?ser=20creation=20only,=20drop=20runtime=20downloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/entrypoint.sh | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 43ee6e4..9c2b3b4 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,7 +1,6 @@ #!/usr/bin/env sh HOME="/home/${ZUSER}" - export HOME command sed -i -r 's#^(root:.+):/bin/ash#\1:/bin/zsh#' /etc/passwd @@ -10,16 +9,3 @@ command adduser -D -s /bin/zsh -u "${PUID}" -h "${HOME}" "${ZUSER}" command printf '%s' "${ZUSER} ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user command mkdir -p /src /data command chown -R "${PUID}:${PGID}" /src /data - -command wget 'https://raw.githubusercontent.com/z-shell/zi-src/main/lib/sh/install.sh' -qO /tmp/install.sh -command chown "${PUID}:${PGID}" /tmp/install.sh -command chmod u+x /tmp/install.sh - -command ln -sfv /src/zshenv "${HOME}/.zshenv" -command ln -sfv /src/zshrc "${HOME}/.zshrc" - -if [ -f "${HOME}"/init.zsh ]; then - command chmod u+x "${HOME}"/init.zsh - # shellcheck source=/dev/null - . "${HOME}"/init.zsh -fi From 2432c26a9580cd9fdbe3dd512a6c55562ed93e26 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:38:09 +0100 Subject: [PATCH 15/47] =?UTF-8?q?refactor:=20Dockerfile=20=E2=80=94=20two-?= =?UTF-8?q?stage,=20zi=20pre-baked,=20ZSH=5FVERSION=20via=20zi=20pack,=20V?= =?UTF-8?q?OLUME=20after=20COPY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/Dockerfile | 96 +++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a5ff48b..afbdce2 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,24 +1,11 @@ -ARG VERSION=edge -FROM alpine:$VERSION +ARG ALPINE_VERSION=edge +FROM alpine:${ALPINE_VERSION} AS base + LABEL maintainer="Z-Shell Community" LABEL email="team@zshell.dev" -ARG DIR -ARG PUID -ARG PGID -ARG TERM -ARG ZUSER -ARG ZHOST -# Bump this ARG when a new Go release is available -ARG GO_VERSION=1.26.1 - -ENV DIR=${DIR:-/static} -ENV PUID=${PUID:-1000} -ENV PGID=${PGID:-1000} -ENV TERM=${TERM:-xterm} -ENV ZUSER=${ZUSER:-user} -ENV HOST=$ZHOST -ENV PATH="/usr/local/go/bin:$PATH" +ARG TERM=xterm +ENV TERM=${TERM} RUN set -ex && apk --no-cache add \ alpine-zsh-config \ @@ -33,41 +20,62 @@ RUN set -ex && apk --no-cache add \ bash \ curl \ sudo \ + go \ zsh \ git \ vim \ jq -# Install Go from upstream — avoids the outdated apk package +# Install zunit and its helpers into /usr/local/bin at build time. RUN set -ex \ - && ARCH="$(uname -m)" \ - && case "$ARCH" in \ - x86_64) GOARCH="amd64" ;; \ - aarch64) GOARCH="arm64" ;; \ - armv6l|armv7l) GOARCH="armv6l" ;; \ - *) echo "Unsupported arch: $ARCH" && exit 1 ;; \ - esac \ - && GO_TARBALL="go${GO_VERSION}.linux-${GOARCH}.tar.gz" \ - && rm -rf /usr/local/go \ - && curl -fsSL "https://go.dev/dl/${GO_TARBALL}" -o "/tmp/${GO_TARBALL}" \ - && EXPECTED_SHA256="$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' \ - | jq -r --arg f "${GO_TARBALL}" '.[].files[] | select(.filename==$f) | .sha256')" \ - && [ -n "${EXPECTED_SHA256}" ] || { echo "Failed to retrieve SHA256 for ${GO_TARBALL}" && exit 1; } \ - && echo "${EXPECTED_SHA256} /tmp/${GO_TARBALL}" | sha256sum -c - \ - && tar -C /usr/local -xzf "/tmp/${GO_TARBALL}" \ - && rm -f "/tmp/${GO_TARBALL}" \ - && go version + && git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git \ + && cd /tmp/zunit.git && ./build.zsh \ + && mv /tmp/zunit.git/zunit /usr/local/bin/zunit \ + && curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ + > /usr/local/bin/revolver \ + && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ + > /usr/local/bin/color \ + && chmod u+x /usr/local/bin/{color,revolver,zunit} \ + && rm -rf /tmp/zunit.git + +FROM base AS test + +ARG ZUSER=user +ARG PUID=1000 +ARG PGID=1000 +ARG ZHOST=zi-docker + +ENV PUID=${PUID} +ENV PGID=${PGID} +ENV ZUSER=${ZUSER} +ENV HOST=${ZHOST} + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && /entrypoint.sh + +# Install zi as $ZUSER at build time — no network calls at test time. +USER ${ZUSER} +ARG ZI_BRANCH=main +RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip -WORKDIR $DIR -COPY . . -RUN chmod +x entrypoint.sh && ./entrypoint.sh +# Optionally install a specific Zsh version via zi pack at build time. +# Leave ZSH_VERSION empty for the :latest image (uses Alpine's zsh). +ARG ZSH_VERSION= +RUN [ -z "${ZSH_VERSION}" ] || \ + zsh -c "source \${HOME}/.zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" -VOLUME ["/src", "/data"] -COPY --chown=$ZUSER . /src +# Switch back to root for COPY operations. +USER root +COPY docker/zshenv /home/${ZUSER}/.zshenv +COPY docker/zshrc /home/${ZUSER}/.zshrc +COPY utils.zsh /src/utils.zsh +COPY tests/ /src/tests/ +RUN chown -R ${PUID}:${PGID} /home/${ZUSER}/.zshenv /home/${ZUSER}/.zshrc /src -USER $ZUSER -WORKDIR /home/$ZUSER +# VOLUME declared after all COPYs — declaring before COPY silently discards copied files. +VOLUME ["/data"] -RUN /tmp/install.sh -i skip +USER ${ZUSER} +WORKDIR /home/${ZUSER} CMD ["zsh", "-il"] From 2a1193c630a6b86c6ce7eddfbe3358c404b86c53 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:39:51 +0100 Subject: [PATCH 16/47] refactor: zshrc sources zi directly; drop prepare_system/initiate_system --- docker/zshenv | 6 +----- docker/zshrc | 17 ++++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docker/zshenv b/docker/zshenv index fed16b8..51cc631 100755 --- a/docker/zshenv +++ b/docker/zshenv @@ -3,8 +3,4 @@ export TERM=${TERM:-xterm-256color} export SHELL=${SHELL:-${commands[zsh]}} - -typeset -Ag ZI -export ZI[HOME_DIR]=${ZI_HOME_DIR:-/data} -export ZI[BIN_DIR]=${ZI_BIN_DIR:-$HOME/.zi/bin} - +export ZI_DATA=${ZI_DATA:-/data} diff --git a/docker/zshrc b/docker/zshrc index d5b1f53..b161e08 100755 --- a/docker/zshrc +++ b/docker/zshrc @@ -1,13 +1,12 @@ # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et -# Prepare and initiate the source tree. -source /src/utils.zsh -prepare_system; initiate_system +# Source zi (pre-installed during docker build). +typeset -gA ZI +ZI[HOME_DIR]="${ZI_DATA:-/data}" +source "${HOME}/.zi/bin/zi.zsh" +autoload -Uz _zi +(( ${+_comps} )) && _comps[zi]=_zi -# If the ZI_ZSH_VERSION is set will install the specified version of Zsh with Zi. -if [[ -n "$ZI_ZSH_VERSION" ]]; then - if [[ "$ZI_ZSH_VERSION" != "$ZSH_VERSION" ]]; then - zi::pack-zsh "$ZI_ZSH_VERSION" - fi -fi +# Load interactive convenience wrappers. +[[ -f /src/utils.zsh ]] && source /src/utils.zsh From cd4fbbd44dba05c282431d3be91d0a182e9f88a6 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:39:52 +0100 Subject: [PATCH 17/47] fix: docker-compose context updated to repo root for COPY tests/ to work --- docker/docker-compose.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b7e9421..bb659ec 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -3,14 +3,13 @@ version: "3.9" services: zd: build: - context: . - dockerfile: Dockerfile - #image: ghcr.io/z-shell/zd:latest + context: .. + dockerfile: docker/Dockerfile stdin_open: true tty: true container_name: zd environment: - TERM=xterm-256color volumes: - - $PWD:/src + - $PWD/..:/src hostname: zi@docker From bc880917a7a199a00715e681cd40e688eafae2f2 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 14 May 2026 23:42:51 +0100 Subject: [PATCH 18/47] =?UTF-8?q?feat:=20add=20test-native.yml=20=E2=80=94?= =?UTF-8?q?=20native=20ZUnit=20CI=20without=20Docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-native.yml | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/test-native.yml diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml new file mode 100644 index 0000000..f2e2943 --- /dev/null +++ b/.github/workflows/test-native.yml @@ -0,0 +1,47 @@ +name: "ZUnit (native)" + +on: + push: + branches: [main] + paths: + - "tests/**" + - "utils.zsh" + pull_request: + branches: [main] + schedule: + - cron: "0 12 * * 1" + workflow_dispatch: + +jobs: + zunit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + file: [annexes, ice, packages, plugins, snippets] + steps: + - uses: actions/checkout@v4 + + - name: Install zsh + run: sudo apt-get update && sudo apt-get install -yq zsh + + - name: Install zunit + run: | + mkdir -p bin + curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' > bin/revolver + curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' > bin/color + git clone --depth 1 https://github.com/zdharma/zunit.git zunit.git + cd zunit.git && ./build.zsh && cd .. + mv zunit.git/zunit bin/ + chmod u+x bin/{color,revolver,zunit} + + - name: Install zi + run: zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + + - name: "ZUnit: ${{ matrix.file }}" + run: | + export PATH="$PWD/bin:$PATH" + export TERM=xterm + export ZI_BIN="${HOME}/.zi/bin" + export ZI_DATA="${RUNNER_TEMP}/zunit" + zunit --tap --verbose "tests/${{ matrix.file }}.zunit" From bcf0079735f3c45445aeb832e96b2c1b51f1dfbf Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Fri, 15 May 2026 01:57:25 +0100 Subject: [PATCH 19/47] =?UTF-8?q?feat:=20add=20test-matrix.yml=20=E2=80=94?= =?UTF-8?q?=20Docker=20Zsh=20version=20matrix=20(scheduled)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-matrix.yml | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/test-matrix.yml diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml new file mode 100644 index 0000000..d532bbe --- /dev/null +++ b/.github/workflows/test-matrix.yml @@ -0,0 +1,40 @@ +name: "ZUnit (Zsh matrix)" + +on: + schedule: + - cron: "0 3 * * 3" + workflow_dispatch: + +jobs: + zunit-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + zsh_version: ["5.5.1", "5.6.2", "5.7.1", "5.8", "5.8.1", "5.9"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: "Build image for Zsh ${{ matrix.zsh_version }}" + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + load: true + build-args: ZSH_VERSION=${{ matrix.zsh_version }} + tags: zd:${{ matrix.zsh_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: "Run all tests in Zsh ${{ matrix.zsh_version }} container" + run: | + mkdir -p "${RUNNER_TEMP}/zunit" + docker run --rm \ + --env TERM=xterm \ + --env ZI_DATA=/data \ + --volume "${RUNNER_TEMP}/zunit:/data" \ + "zd:${{ matrix.zsh_version }}" \ + zsh -c 'for f in /src/tests/*.zunit; do zunit --tap --verbose "$f" || exit $?; done' From c88e3006a56620811d528b3b81f53bfb2eaa9842 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Fri, 15 May 2026 09:56:17 +0100 Subject: [PATCH 20/47] chore: remove docker/tests/, old scripts, and superseded zunit.yml workflow - Remove docker/tests directory and all ZUnit test files - Remove docker/build.sh, docker/run.sh, docker/zunit.sh, docker/init.zsh - Remove .github/workflows/zunit.yml (testing now handled by CI/CD container) - Update copilot-instructions.md to reflect removed test infrastructure --- .github/copilot-instructions.md | 40 +----- .github/workflows/zunit.yml | 63 --------- docker/build.sh | 85 ------------ docker/init.zsh | 4 - docker/run.sh | 221 -------------------------------- docker/tests/annexes.zunit | 120 ----------------- docker/tests/ice.zunit | 73 ----------- docker/tests/packages.zunit | 27 ---- docker/tests/plugins.zunit | 62 --------- docker/tests/setup.zsh | 22 ---- docker/tests/snippets.zunit | 51 -------- docker/tests/teardown.zsh | 14 -- docker/zunit.sh | 12 -- 13 files changed, 4 insertions(+), 790 deletions(-) delete mode 100644 .github/workflows/zunit.yml delete mode 100755 docker/build.sh delete mode 100755 docker/init.zsh delete mode 100755 docker/run.sh delete mode 100755 docker/tests/annexes.zunit delete mode 100755 docker/tests/ice.zunit delete mode 100755 docker/tests/packages.zunit delete mode 100755 docker/tests/plugins.zunit delete mode 100755 docker/tests/setup.zsh delete mode 100755 docker/tests/snippets.zunit delete mode 100755 docker/tests/teardown.zsh delete mode 100755 docker/zunit.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1ac8ec4..1913908 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,8 +5,7 @@ `zd` is a **Zi Docker environment** — an Alpine Linux–based Docker image that provides a ready-to-use Zsh + [Zi](https://github.com/z-shell/zi) plugin-manager environment. The repository contains: - The `Dockerfile` and supporting shell scripts that build the image. -- ZUnit integration tests that exercise Zi plugin/snippet/package installation inside a running container. -- A `run.sh` helper that launches the container with sensible defaults. +- CI/CD workflows that build multi-architecture images and verify functionality. ## Repository layout @@ -14,33 +13,14 @@ docker/ Dockerfile # Alpine-based image definition entrypoint.sh # POSIX sh setup script run at image-build time (root) - init.zsh # Optional user-supplied init script sourced on startup utils.zsh # Zsh helper functions (prepare_system, initiate_system, zi::*) zshenv # Zsh env bootstrap (ZI config, PATH additions) zshrc # Zsh startup config — sources utils.zsh and calls prepare_system/initiate_system - build.sh # Bash helper to docker-build the image with appropriate ARGs - run.sh # Bash helper to docker-run the image - zunit.sh # Bash wrapper that invokes `zunit run --verbose` docker-compose.yml # Compose file for interactive use - tests/ - setup.zsh # ZUnit @setup: exports DATA_DIR, PLUGINS_DIR, SNIPPETS_DIR, ZPFX - teardown.zsh # ZUnit @teardown: removes DATA_DIR - plugins.zunit # ZUnit tests: fzf, direnv, diff-so-fancy plugin installs - annexes.zunit # ZUnit tests: Zi annex loading - ice.zunit # ZUnit tests: Zi ice-modifier syntax - packages.zunit # ZUnit tests: Zi pack installs - snippets.zunit # ZUnit tests: Zi snippet loading .github/ workflows/ docker.yml # CI: multi-arch Docker build matrix (versioned Zsh + latest) - zunit.yml # CI: ZUnit test matrix (one job per *.zunit file) codeql.yml # CodeQL security scanning - labeler.yml # Auto-labelling PRs - pr-labels.yml # PR label sync - stale.yml # Stale issue/PR management - lock.yml # Lock closed issues/PRs - rebase.yml # Auto-rebase - sync-labels.yml # Sync labels from config zsh-n.yml # Zsh -n (syntax check) workflow ISSUE_TEMPLATE/ # GitHub issue templates PULL_REQUEST_TEMPLATE.md @@ -53,8 +33,7 @@ docker/ - **`entrypoint.sh`** is POSIX `sh` (shebang `#!/usr/bin/env sh`). It runs as root inside the Alpine build context. - Use `sed -i -r` (BusyBox `sed` extended-regex flag) — **not** `-E`, which is unsupported by BusyBox. - Never use Bashisms (`[[ ]]`, arrays, `local` with assignment, etc.) in this file. -- **`run.sh`**, **`build.sh`**, **`zunit.sh`** are Bash (shebang `#!/usr/bin/env bash`). 2-space indentation, `# vim: ft=bash sw=2 ts=2 et` modeline. -- **Zsh files** (`utils.zsh`, `zshrc`, `zshenv`, `*.zunit`) use 2-space indentation and the modeline `# vim: ft=zsh sw=2 ts=2 et`. +- **Zsh files** (`utils.zsh`, `zshrc`, `zshenv`) use 2-space indentation and the modeline `# vim: ft=zsh sw=2 ts=2 et`. - All text files: UTF-8, LF line endings. Default indent is 2 spaces, except: - `Makefile*`: tab indentation with `indent_size=4`. - `*.py`, `*.rb`: 4-space indentation. @@ -68,22 +47,12 @@ docker/ - Go is installed from `https://go.dev/dl/` (not from `apk`) to get a current release. Bump `ARG GO_VERSION` when a new Go release is available; SHA256 is verified via the `https://go.dev/dl/?mode=json&include=all` API. - `LABEL` values must be plain strings — no template syntax like `<%= ... =>`. -### ZUnit tests -- Each `*.zunit` file begins with `@setup { load setup; setup }` and `@teardown { load teardown; teardown }`. -- Every assertion for a binary artifact must include both `assert "$artifact" is_file` **and** `assert "$artifact" is_executable`. -- Use `local artifact=...` for the first artifact in a test; reassign with `artifact=...` (no `local`) for subsequent ones in the same test body. -- Tests run against the published container image via `run.sh --wrap --debug --zunit`. - -### `run.sh` helper -- Use `printf '%s\n' "$*"` (not a custom `say` function) when writing content to a temp file or printing a file path. -- `create_init_config_file` writes `$*` to a `mktemp` file and prints the path on stdout. ## CI / workflows | Workflow | Trigger | What it does | |---|---|---| | `docker.yml` | push/PR to `main` touching `docker/**`, scheduled Wed 03:00 UTC | Builds multi-arch image (`linux/amd64`, `linux/arm64`) for Zsh 5.5.1–5.9 matrix + `latest` tag | -| `zunit.yml` | push to `main` touching `*.zunit`, scheduled Mon/Wed/Fri/Sun 12:00 UTC, `workflow_dispatch` | Runs each `*.zunit` file as a separate matrix job | | `zsh-n.yml` | Zsh `-n` syntax check | Checks all Zsh files for syntax errors | ### Common build failure causes @@ -95,9 +64,8 @@ docker/ ## Development workflow 1. Edit files in `docker/`. -2. Build locally: `cd docker && ./build.sh` (or `docker compose build`). -3. Run ZUnit tests: `./docker/zunit.sh` (requires a built image). -4. Submit a PR — CI will run both `docker.yml` and `zunit.yml`. +2. Build locally: `docker compose build`. +3. Submit a PR — CI will run Docker build and syntax checks. ## Security notes diff --git a/.github/workflows/zunit.yml b/.github/workflows/zunit.yml deleted file mode 100644 index 95fc78a..0000000 --- a/.github/workflows/zunit.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: "♾️ ZUnit" -on: - workflow_call: - push: - branches: [main] - paths: - - "**/*.zunit" - schedule: - - cron: "0 12 * * 1/2" - workflow_dispatch: - -env: - zi_branch: main - -jobs: - zunit-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - uses: actions/checkout@v6 - - name: "Set matrix output" - id: set-matrix - run: | - builtin cd docker/tests - MATRIX="$(ls -1 *.zunit | sed 's/.zunit$//' | jq -ncR '{"include": [{"file": inputs}]}')" - echo "MATRIX=${MATRIX}" >&2 - echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT - zunit: - runs-on: ubuntu-latest - needs: zunit-matrix - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.zunit-matrix.outputs.matrix) }} - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v6 - - run: command git clone --branch ${{ env.zi_branch }} --depth 1 -- https://github.com/z-shell/zi.git zi - - name: "⚡ Install dependencies" - run: | - sudo apt-get update && sudo apt-get install -yq zsh - mkdir bin - curl -fsSL https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver > bin/revolver - curl -fsSL https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh > bin/color - git clone https://github.com/zdharma/zunit.git zunit.git - cd zunit.git - ./build.zsh - cd .. - mv ./zunit.git/zunit bin - chmod u+x bin/{color,revolver,zunit} - - name: "⚡ ZUnit: ${{ matrix.file }}" - env: - ZUNIT_TEST: ${{ matrix.file }} - run: | - echo "⚡ $ZUNIT_TEST" >&2 - export PATH="$PWD/bin:$PATH" - export TERM=xterm - zunit --tap --verbose "docker/tests/${ZUNIT_TEST}.zunit" diff --git a/docker/build.sh b/docker/build.sh deleted file mode 100755 index 3e3a0aa..0000000 --- a/docker/build.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=bash sw=2 ts=2 et - -col_error="" -col_info="" -col_rst="" - -say() { - printf '%s\n' "${col_info}${1}${col_rst}" >&2 -} - -err() { - say "${col_error}${1}${col_rst}" >&2 - exit 1 -} - -build() { - command cd -P -- "$(dirname -- "$(command -v -- "$0" || true)")" && pwd -P || exit 9 - - local image_name="${1:-zd}" - local tag="${2:-latest}" - local zsh_version="${3}" - local container_hostname="z-shell" - shift 3 - - local dockerfile="Dockerfile" - - if [[ -n ${zsh_version} ]]; then - tag="zsh${zsh_version}-${tag}" - fi - - say "Building image: ${image_name}" - - local -a args - [[ -n ${NO_CACHE} ]] && args+=(--no-cache "$@") - - if docker build \ - --build-arg "ZUSER=${USER:-$(id -u -n)}" \ - --build-arg "ZHOST=${container_hostname}" \ - --build-arg "PUID=${UID:-$(id -u)}" \ - --build-arg "PGID=${GID:-$(id -g)}" \ - --build-arg "TERM=${TERM:-xterm-256color}" \ - --build-arg "ZI_ZSH_VERSION=${zsh_version}" \ - --file "${dockerfile}" \ - --tag "${image_name}:${tag}" \ - "${args[@]}" "$(realpath ../docker || realpath .. || true)"; then - { - say "To use this image for ZUnit tests run: " - say "export CONTAINER_IMAGE=\"${image_name}\" CONTAINER_TAG=\"${tag}\"" - say "ZUnit run --verbose" - } >&2 - else - err "Container failed to build." - fi -} - -if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then - CONTAINER_IMAGE="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" - BUILD_ZSH_VERSION="${BUILD_ZSH_VERSION-}" - CONTAINER_TAG="${CONTAINER_TAG:-latest}" - NO_CACHE="${NO_CACHE-}" - - while [[ -n $* ]]; do - case "$1" in - --image | -i) - CONTAINER_IMAGE="$2" - shift 2 - ;; - --no-cache | -N) - NO_CACHE=1 - shift - ;; - --zsh-version | -zv | --zv) - BUILD_ZSH_VERSION="${2}" - shift 2 - ;; - *) - break - ;; - esac - done - - build "${CONTAINER_IMAGE}" "${CONTAINER_TAG}" "${BUILD_ZSH_VERSION}" "$@" -fi diff --git a/docker/init.zsh b/docker/init.zsh deleted file mode 100755 index 1ff793f..0000000 --- a/docker/init.zsh +++ /dev/null @@ -1,4 +0,0 @@ -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -true \ No newline at end of file diff --git a/docker/run.sh b/docker/run.sh deleted file mode 100755 index 9f343a8..0000000 --- a/docker/run.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env bash -# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=bash sw=2 ts=2 et - -col_error="" -col_info="" -col_rst="" - -say() { - printf '%s\n' "${col_info}${1}${col_rst}" >&2 -} - -err() { - say "${col_error}${1}${col_rst}" >&2 - exit 1 -} - -parent_process() { - local ppid pcmd - ppid="$(ps -o ppid= -p "$$" | awk '{ print $1 }' || true)" - - if [[ -z ${ppid} ]]; then - say "Failed to determine parent process" - return 1 - fi - - if pcmd="$(ps -o cmd= -p "${ppid}")"; then - say "${pcmd}" - return - fi - - return 1 -} - -running_interactively() { - if [[ -n ${CI} ]]; then - return 1 - fi - - if ! [[ -t 1 ]]; then - # return false if running non-interactively, unless run with zunit - parent_process | grep -q zunit || true - fi -} - -create_init_config_file() { - local tempfile - - if [[ -z $* ]]; then - return 1 - fi - - tempfile="$(mktemp)" - printf '%s\n' "$*" >"${tempfile}" - printf '%s\n' "${tempfile}" -} - -run() { - local image="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" - local tag="${CONTAINER_TAG:-latest}" - local init_config="$1" - shift - - local -a args=(--rm) - - if running_interactively; then - args+=(--tty=true --interactive=true) - fi - - if [[ -n ${init_config} ]]; then - if [[ -r ${init_config} ]]; then - args+=(--volume "${init_config}:/init.zsh") - else - say "Init config file is not readable" - return 1 - fi - fi - - # Inherit TERM - if [[ -n ${TERM} ]]; then - args+=(--env "TERM=${TERM}") - fi - - if [[ -n ${CONTAINER_ENV[*]} ]]; then - local e - for e in "${CONTAINER_ENV[@]}"; do - args+=(--env "${e}") - done - fi - - if [[ -n ${CONTAINER_VOLUMES[*]} ]]; then - local vol - for vol in "${CONTAINER_VOLUMES[@]}"; do - args+=(--volume "${vol}") - done - fi - - local -a cmd=("$@") - - if [[ -n ${WRAP_CMD} ]]; then - local zsh_opts="ilsc" - [[ -n ${ZSH_DEBUG} ]] && zsh_opts="x${zsh_opts}" - cmd=(zsh "-${zsh_opts}" "${cmd[*]}") - fi - - if [[ -n ${DEBUG} ]]; then - { - say "\$ docker run ${args[*]} ${image}:${tag} ${cmd[*]@Q}" - } >&2 - fi - - docker run "${args[@]}" "${image}:${tag}" "${cmd[@]}" -} - -if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then - CONTAINER_IMAGE=${CONTAINER_IMAGE:-ghcr.io/z-shell/zd} - CONTAINER_TAG="${CONTAINER_TAG:-latest}" - CONTAINER_ENV=() - CONTAINER_VOLUMES=() - DEBUG="${DEBUG-}" - ZSH_DEBUG="${ZSH_DEBUG-}" - INIT_CONFIG_VAL="${INIT_CONFIG_VAL-}" - WRAP_CMD="${WRAP_CMD-}" - - while [[ -n $* ]]; do - case "$1" in - # Fetch init config from clipboard (Linux only) - --xsel | -b) - INIT_CONFIG_VAL="$(xsel -b)" - shift - ;; - -c | --config | --init-config | --init) - INIT_CONFIG_VAL="$2" - shift 2 - ;; - -f | --config-file | --init-config-file | --file) - if ! [[ -r $2 ]]; then - say "Unable to read from file: $2" - exit 2 - fi - INIT_CONFIG_VAL="$(cat "$2")" - shift 2 - ;; - -d | --debug) - DEBUG=1 - shift - ;; - -D | --dev | --devel) - DEVEL=1 - shift - ;; - -i | --image) - CONTAINER_IMAGE="$2" - shift 2 - ;; - -t | --tag) - CONTAINER_TAG="$2" - shift 2 - ;; - -e | --env | --environment) - CONTAINER_ENV+=("$2") - shift 2 - ;; - -v | --volume) - CONTAINER_VOLUMES+=("$2") - shift 2 - ;; - # Whether to wrap the command in zsh -silc - -w | --wrap) - WRAP_CMD=1 - shift - ;; - --tests | --zunit | -z) - ZUNIT=1 - shift - ;; - # Whether to enable debug tracing of zd (zsh -x) - # Only applies to wrapped commands (--w|--wrap) - --zsh-debug | -x | -Z) - ZSH_DEBUG=1 - shift - ;; - *) - break - ;; - esac - done - - if INIT_CONFIG="$(create_init_config_file "${INIT_CONFIG_VAL}")"; then - trap 'rm -vf $INIT_CONFIG' EXIT INT - fi - CONTAINER_ROOT="$( - cd -P -- "$(dirname "$0")" - pwd -P - )" || exit 9 - if [[ -n ${DEVEL} ]]; then - # Mount root of the repo to /src - CONTAINER_VOLUMES+=( - "${CONTAINER_ROOT}:/src" - ) - fi - - if [[ -n ${ZUNIT} ]]; then - ROOT_DIR="$( - cd -P -- "$(dirname "$0")" - pwd -P - )" || exit 9 - # Mount root of the repo to /src - # Mount /tmp/zunit to /data - CONTAINER_VOLUMES+=( - "${CONTAINER_ROOT}:/src" - "${TMPDIR:-/tmp}/zunit:/data" - "${ROOT_DIR}/zshenv:/home/zunit/.zshenv" - "${ROOT_DIR}/zshrc:/home/zunit/.zshrc" - ) - CONTAINER_ENV+=( - "QUIET=1" - ) - fi - run "${INIT_CONFIG}" "$@" -fi diff --git a/docker/tests/annexes.zunit b/docker/tests/annexes.zunit deleted file mode 100755 index 5aded7d..0000000 --- a/docker/tests/annexes.zunit +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - setup -} - -@teardown { - load teardown - teardown -} - -@test 'z-a-bin-gem-node installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-bin-gem-node - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-meta-plugins installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-meta-plugins - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-meta-plugins/z-a-meta-plugins.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - - -@test 'z-a-readurl installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-readurl - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-readurl/z-a-readurl.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-rust installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-rust - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-rust/z-a-rust.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-eval installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-eval - - assert $state equals 1 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-eval/z-a-eval.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-linkbin installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-linkbin - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-linkbin/z-a-linkbin.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-default-ice installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-default-ice - - assert $state equals 1 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-default-ice/z-a-default-ice.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-test installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-test - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-test/z-a-test.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} diff --git a/docker/tests/ice.zunit b/docker/tests/ice.zunit deleted file mode 100755 index 9886741..0000000 --- a/docker/tests/ice.zunit +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - setup -} - -@teardown { - load teardown - teardown -} - -@test 'sbin ice' { - run ./docker/run.sh --wrap --debug --zunit \ - zi light z-shell/z-a-bin-gem-node\;\ - zi light-mode as"null" from"gh-r" sbin'fzf' for junegunn/fzf - - assert $state equals 0 - # We can't assert 'Downloading z-shell/z-a-bin-gem-node' - # because of the control chars (colored output) - assert "$output" contains "Downloading" - - local artifact="${PLUGINS_DIR}/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable - - artifact="${ZPFX}/bin/fzf" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'failing atclone ice' { - local z=$'zi null atclone'\''echo "intentional failure"; return 255'\'' for z-shell/null' - run ./docker/run.sh --wrap --debug --zunit $z - - assert $state not_equal_to 0 - assert $state equals 255 - assert "$output" contains "intentional failure" -} - -@test 'failing atpull ice' { - local z=$'zi id-as'\''atpull-fail'\'' null \ - atpull'\''echo "intentional failure"; return 255'\'' run-atpull \ - for z-shell/null; zi update atpull-fail' - run ./docker/run.sh --wrap --debug --zunit $z - - assert $state equals 255 - assert "$output" contains "intentional failure" -} - -@test 'failing mv ice' { - local z=$'zi as'\''command'\'' from'\''gh-r'\'' bpick'\''*musl*'\'' mv'\''DOES_NOT_EXIST* -> fd'\'' pick'\''fd/fd'\'' for @sharkdp/fd' - run ./docker/run.sh --wrap --debug --zunit $z - - assert $state equals 1 - assert "$output" contains "DOES_NOT_EXIST" - assert "$output" contains "didn't match any file" -} - -@test 'mv ice' { - local z=$'zi as'\''command'\'' from'\''gh-r'\'' bpick'\''*musl*'\'' mv'\''fd* -> fd'\'' pick'\''fd/fd'\'' for @sharkdp/fd' - run ./docker/run.sh --wrap --debug --zunit $z - - assert $state equals 0 - local artifact="${PLUGINS_DIR}/sharkdp---fd/fd/fd" - assert "$artifact" is_file - assert "$artifact" is_readable - assert "$artifact" is_executable -} diff --git a/docker/tests/packages.zunit b/docker/tests/packages.zunit deleted file mode 100755 index 49ce7c1..0000000 --- a/docker/tests/packages.zunit +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi package ls_colors' { - run ./docker/run.sh --wrap --debug --zunit \ - zi pack for ls_colors - - assert $state equals 0 - assert "$output" contains "Package" - - local artifact="${PLUGINS_DIR}/ls_colors/LS_COLORS" - assert "$artifact" is_file - assert "$artifact" is_readable -} diff --git a/docker/tests/plugins.zunit b/docker/tests/plugins.zunit deleted file mode 100755 index 3e80b9e..0000000 --- a/docker/tests/plugins.zunit +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi fzf installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi lucid as="program" from="gh-r" for junegunn/fzf - - assert $state equals 0 - assert "$output" contains "Unpacking" - assert "$output" contains "Successfully" - - local artifact="${PLUGINS_DIR}/junegunn---fzf/fzf" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'zi direnv installation' { - run ./docker/run.sh --wrap --debug --zunit \ - 'zi light-mode as"program" \ - atclone"go install github.com/cpuguy83/go-md2man/v2@latest" \ - make for @direnv/direnv' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "go: downloading github.com" - - local artifact="${PLUGINS_DIR}/direnv---direnv/direnv" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'zi diff-so-fancy installation' { - run ./docker/run.sh --wrap --debug --zunit \ - 'zi light-mode for \ - as"program" pick"bin/git-dsf" \ - z-shell/zsh-diff-so-fancy' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Cloning into" - - local artifact="${PLUGINS_DIR}/z-shell---zsh-diff-so-fancy/bin/git-dsf" - assert "$artifact" is_file - assert "$artifact" is_executable - - artifact="${PLUGINS_DIR}/z-shell---zsh-diff-so-fancy/bin/diff-so-fancy" - assert "$artifact" is_file - assert "$artifact" is_executable -} diff --git a/docker/tests/setup.zsh b/docker/tests/setup.zsh deleted file mode 100755 index 34012bb..0000000 --- a/docker/tests/setup.zsh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env zunit - -setup() { - export DATA_DIR="${TMPDIR:-/tmp}/zunit" - export PLUGINS_DIR="${DATA_DIR}/plugins" - export SNIPPETS_DIR="${DATA_DIR}/snippets" - export ZPFX="${DATA_DIR}/polaris" - - { - color magenta @setup started - color magenta "DATA_DIR=${DATA_DIR}" - color magenta "PLUGINS_DIR=${PLUGINS_DIR}" - color magenta "SNIPPETS_DIR=${SNIPPETS_DIR}" - color magenta "ZPFX=${ZPFX}" - } >&2 - - color red bold "Deleting $DATA_DIR" >&2 - sudo rm -rf "${DATA_DIR}" - mkdir -p "${DATA_DIR}" -} - -# vim: set ft=zsh et ts=2 sw=2 : diff --git a/docker/tests/snippets.zunit b/docker/tests/snippets.zunit deleted file mode 100755 index 0c82133..0000000 --- a/docker/tests/snippets.zunit +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi OMZL::spectrum.zsh installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi snippet OMZL::spectrum.zsh - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${SNIPPETS_DIR}/OMZL::spectrum.zsh/OMZL::spectrum.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'zi OMZP::git installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi snippet OMZP::git - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${SNIPPETS_DIR}/OMZP::git/OMZP::git" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'zi PZTM::environment installation' { - run ./docker/run.sh --wrap --debug --zunit \ - zi snippet PZTM::environment - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${SNIPPETS_DIR}/PZTM::environment/PZTM::environment" - assert "$artifact" is_file - assert "$artifact" is_readable -} diff --git a/docker/tests/teardown.zsh b/docker/tests/teardown.zsh deleted file mode 100755 index 7fda356..0000000 --- a/docker/tests/teardown.zsh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -teardown() { - color cyan @teardown called - - [[ -n "$DATA_DIR" ]] && { - color red bold "Deleting $DATA_DIR" >&2 - sudo rm -rf "$DATA_DIR" - } -} diff --git a/docker/zunit.sh b/docker/zunit.sh deleted file mode 100755 index 61d0e1a..0000000 --- a/docker/zunit.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=bash sw=2 ts=2 et - -run_tests() { - command cd -P -- "$(dirname -- "$(command -v -- "$0" || true)")" && pwd -P || exit 9 - zunit run --verbose "$@" -} - -if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then - run_tests "$@" -fi From 93807e0321f3b86da273f052474fe57c80f7cc12 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Fri, 15 May 2026 12:17:50 +0100 Subject: [PATCH 21/47] feat: add Makefile for local testing and workflow_call to test-native.yml - Makefile: `make test [FILE=]` runs ZUnit natively, auto-installs zunit into bin/ on first run; `make run CMD="zi light fzf"` runs an ad-hoc zi command in Docker; `make shell` opens an interactive Docker shell; `make build` wraps scripts/build.sh - test-native.yml: add workflow_call trigger with zi_repo/zi_ref inputs so other repos can test against a specific zi branch/SHA; add same inputs to workflow_dispatch for manual smoke testing; branch install step to use git clone when inputs are provided, install script otherwise --- .editorconfig | 60 ------------------------- .github/label-commenter-config.yml | 70 ----------------------------- .github/labeler.yml | 4 -- .github/workflows/labeler.yml | 17 ------- .github/workflows/lock.yml | 39 ---------------- .github/workflows/pr-labels.yml | 23 ---------- .github/workflows/rebase.yml | 28 ------------ .github/workflows/stale.yml | 44 ------------------ .github/workflows/sync-labels.yml | 10 ----- .github/workflows/test-native.yml | 33 +++++++++++++- Makefile | 72 ++++++++++++++++++++++++++++++ 11 files changed, 104 insertions(+), 296 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .github/label-commenter-config.yml delete mode 100644 .github/labeler.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/lock.yml delete mode 100644 .github/workflows/pr-labels.yml delete mode 100644 .github/workflows/rebase.yml delete mode 100644 .github/workflows/stale.yml delete mode 100644 .github/workflows/sync-labels.yml create mode 100644 Makefile diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c25c645..0000000 --- a/.editorconfig +++ /dev/null @@ -1,60 +0,0 @@ -# Space or Tabs? -# https://stackoverflow.com/questions/35649847/objective-reasons-for-using-spaces-instead-of-tabs-for-indentation -# https://stackoverflow.com/questions/12093748/how-to-use-tabs-instead-of-spaces-in-a-shell-script -# https://github.com/editorconfig/editorconfig-defaults/blob/master/editorconfig-defaults.json -# -# 1. What happens when I press the Tab key in my text editor? -# 2. What happens when I request my editor to indent one or more lines? -# 3. What happens when I view a file containing U+0009 HORIZONTAL TAB characters? -# -# Answers: -# -# 1. Pressing the Tab key should indent the current line (or selected lines) one additional level. -# 2. As a secondary alternative, I can also tolerate an editor that, -# like Emacs, uses this key for a context-sensitive fix-my-indentation command. -# 3. Indenting one or more lines should follow the reigning convention, if consensus is sufficiently strong; otherwise, -# I greatly prefer 2-space indentation at each level. U+0009 characters should shift subsequent characters to the next tab stop. -# -# Note: VIM users should use alternate marks [[[ and ]]] as the original ones can confuse nested substitutions, e.g.: ${${${VAR}}} -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.sln] -indent_style = tab - -[*.{md,mdx,rst}] -trim_trailing_whitespace = false - -[*.{cmd,bat}] -end_of_line = crlf - -[*za-*] -end_of_line = lf - -[*.{sh,bash,zsh,fish}] -end_of_line = lf - -[Makefile] -indent_style = tab -indent_size = 4 - -[*.{py,rb}] -indent_size = 4 - -[*.{go,java,scala,groovy,kotlin}] -indent_style = tab -indent_size = 4 - -[*.{cs,csx,cake,vb,vbx}] -# Default Severity for all .NET Code Style rules below -dotnet_analyzer_diagnostic.severity = warning diff --git a/.github/label-commenter-config.yml b/.github/label-commenter-config.yml deleted file mode 100644 index 9b0af06..0000000 --- a/.github/label-commenter-config.yml +++ /dev/null @@ -1,70 +0,0 @@ -comment: - header: Hi, there. - footer: "\n\n > This is an automated comment. Responding to the bot or mentioning it won't have any effect.\n\n" - -labels: - - name: invalid ⚠️ - labeled: - issue: - body: Please follow the issue templates. - action: close - pr: - body: Thank you @{{ pull_request.zunit.login }} for suggesting this. Please follow the pull request templates. - action: close - unlabeled: - issue: - body: Thank you for following the template. The repository owner will reply. - action: open - - name: Q&A ✍️ - labeled: - issue: - body: | - Please ask questions at the Github discussions. - https://github.com/z-shell/zi/discussions/categories/q-a - action: close - - name: priority-low 🔖 - labeled: - issue: - body: "This issue currently can't be resolved, but we appreciate your contribution." - action: close - unlabeled: - issue: - body: This issue may be useful and has become active again. - action: open - - name: beginner-friendly 💕 - labeled: - issue: - body: This issue is easy for contributing. Good for people wanting to contribute to this project. - - name: feature-request 💡 - labeled: - issue: - body: Thank you @{{ issue.zunit.login }} for suggesting this. - - name: locked ‼️ - labeled: - issue: - body: | - This issue has been **LOCKED** because of spam! - - Please do not spam messages and/or issues on the issue tracker. You may get blocked from this repository for doing so. - action: close - locking: lock - lock_reason: spam - pr: - body: | - This pull-request has been **LOCKED** because of spam! - - Please do not spam messages and/or pull-requests on this project. You may get blocked from this repository for doing so. - action: close - locking: lock - lock_reason: spam - - name: resolved ☑️ - labeled: - issue: - body: | - This issue has been **LOCKED** because of it being resolved! - - The issue has been fixed and is therefore considered resolved. - If you still encounter this or it has changed, open a new issue instead of responding to solved ones. - action: close - locking: lock - lock_reason: resolved diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 8567e32..0000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,4 +0,0 @@ -documentation 📝: - - any: ["docs/**/*.md", "docs/*.md"] -ci 🤖: - - any: [".github/workflows/*.yml"] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index b5cf199..0000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: 🔖 Pull Request Labeler -on: - pull_request_target: - -permissions: - contents: read - pull-requests: write - -jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: true diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml deleted file mode 100644 index aa08b75..0000000 --- a/.github/workflows/lock.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: 🔒 Lock closed issues and PRs - -on: - schedule: - - cron: "30 2 * * *" - -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - -jobs: - lock: - name: 🔐 Lock closed issues and PRs - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5 - with: - github-token: ${{ github.token }} - issue-inactive-days: "30" - issue-lock-reason: "" - issue-comment: > - Issue closed and locked due to lack of activity. - - If you encounter this same issue, please open a new issue and refer - to this closed one. - - pr-inactive-days: "7" - pr-lock-reason: "" - pr-comment: > - Pull Request closed and locked due to lack of activity. - - If you'd like to build on this closed PR, you can clone it using - this method: https://stackoverflow.com/a/14969986 - - Then open a new PR, referencing this closed PR in your message. diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml deleted file mode 100644 index dd34aec..0000000 --- a/.github/workflows/pr-labels.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: 🏷️ Verify PR Labels - -on: - workflow_dispatch: - pull_request_target: - types: ["opened", "labeled", "unlabeled", "synchronize"] - -jobs: - pr_labels: - name: 🏭 Verify PR Labels - runs-on: ubuntu-latest - steps: - - name: 🏷 Verify PR has a valid label - uses: jesusvasquez333/verify-pr-label-action@v1.4.0 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - pull-request-number: "${{ github.event.pull_request.number }}" - valid-labels: > - "breaking-change 💥, bug 🐞, ci 🤖, documentation 📝, enhancement ✨, - security 🛡️, refactor ♻️, performance 🚀, new-feature 🎉, triage 📑, - maintenance 📈, in-progress ⚡, dependencies 📦, submodules ⚙️" - disable-reviews: true diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml deleted file mode 100644 index cf96a84..0000000 --- a/.github/workflows/rebase.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "🔁 Rebase" -on: - issue_comment: - types: [created] - -jobs: - rebase: - runs-on: ubuntu-latest - name: 🔁 Rebase - if: >- - github.event.issue.pull_request != '' && - ( - contains(github.event.comment.body, '/rebase') || - contains(github.event.comment.body, '/autosquash') - ) - steps: - - name: Checkout the latest code - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 # otherwise, you will fail to push refs to dest repo - - name: 🔁 Rebase - uses: z-shell/.github/actions/rebase@main - with: - autosquash: ${{ contains(github.event.comment.body, '/autosquash') || contains(github.event.comment.body, '/rebase-autosquash') }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 6ac0024..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: 👻 Stale - -on: - schedule: - - cron: "0 8 * * *" - workflow_dispatch: - -jobs: - stale: - name: 🧹 Clean up stale issues and PRs - runs-on: ubuntu-latest - steps: - - name: 🚀 Run stale - uses: actions/stale@v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 30 - days-before-close: 7 - remove-stale-when-updated: true - stale-issue-label: "stale 👻" - exempt-issue-labels: "no-stale 🔒,help-wanted 👥" - stale-issue-message: > - There hasn't been any activity on this issue recently, and in order - to prioritize active issues, it will be marked as stale. - - Please make sure to update to the latest version and - check if that solves the issue. Let us know if that works for you - by leaving a 👍 - - Because this issue is marked as stale, it will be closed and locked - in 7 days if no further activity occurs. - - Thank you for your contributions! - stale-pr-label: "stale 👻" - exempt-pr-labels: "no-stale 🔒" - stale-pr-message: > - There hasn't been any activity on this pull request recently, and in - order to prioritize active work, it has been marked as stale. - - This PR will be closed and locked in 7 days if no further activity - occurs. - - Thank you for your contributions! diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml deleted file mode 100644 index c988167..0000000 --- a/.github/workflows/sync-labels.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: "♻️ Sync Labels" -on: - schedule: - - cron: "22 2 * * 2" - workflow_dispatch: -jobs: - labels: - name: "♻️ Sync labels" - uses: z-shell/.github/.github/workflows/sync-labels.yml@main diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index f2e2943..c8014c9 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -11,6 +11,27 @@ on: schedule: - cron: "0 12 * * 1" workflow_dispatch: + inputs: + zi_repo: + description: "GitHub repo for zi (owner/name). Leave empty to use the default install script." + required: false + default: "" + zi_ref: + description: "Branch, tag, or SHA of zi to test." + required: false + default: "main" + workflow_call: + inputs: + zi_repo: + description: "GitHub repo for zi (owner/name). Leave empty to use the default install script." + type: string + required: false + default: "" + zi_ref: + description: "Branch, tag, or SHA of zi to test." + type: string + required: false + default: "main" jobs: zunit: @@ -36,7 +57,17 @@ jobs: chmod u+x bin/{color,revolver,zunit} - name: Install zi - run: zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + env: + ZI_REPO: ${{ inputs.zi_repo }} + ZI_REF: ${{ inputs.zi_ref }} + run: | + if [[ -n "$ZI_REPO" ]]; then + git clone --depth 1 --branch "${ZI_REF:-main}" \ + "https://github.com/${ZI_REPO}.git" "${HOME}/.zi/bin" + mkdir -p "${HOME}/.zi"/{cache,completions,plugins,snippets} + else + zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip + fi - name: "ZUnit: ${{ matrix.file }}" run: | diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..99f0336 --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +# -*- mode: makefile; -*- + +SHELL := bash +ZI_BIN ?= $(HOME)/.zi/bin +ZI_DATA ?= /tmp/zunit-local +IMAGE ?= ghcr.io/z-shell/zd +TAG ?= latest +TERM ?= xterm-256color +TEST_FILES = annexes ice packages plugins snippets + +ifdef FILE +_SUITES := tests/$(FILE).zunit +else +_SUITES := $(patsubst %,tests/%.zunit,$(TEST_FILES)) +endif + +.PHONY: test run shell build help + +## test [FILE=] — run ZUnit natively (all suites, or one) +test: bin/zunit + @for f in $(_SUITES); do \ + echo "==> $$f"; \ + PATH="$(CURDIR)/bin:$$PATH" \ + ZI_BIN="$(ZI_BIN)" \ + ZI_DATA="$(ZI_DATA)" \ + TERM=$(TERM) \ + bin/zunit --tap --verbose "$$f" || exit $$?; \ + done + +## run CMD="" — run a zi command in Docker +run: +ifndef CMD + $(error Usage: make run CMD="zi light fzf") +endif + docker run --rm \ + --env TERM=$(TERM) \ + --env ZI_DATA=/tmp/zd-run \ + $(IMAGE):$(TAG) \ + zsh -ilc "$(CMD)" + +## shell — interactive Docker shell with zi loaded +shell: + docker run --rm -it \ + --env TERM=$(TERM) \ + --env ZI_DATA=/tmp/zd-shell \ + $(IMAGE):$(TAG) \ + zsh -il + +## build [TAG=] [ZSH_VERSION=] — build Docker image locally +build: + CONTAINER_TAG=$(TAG) bash scripts/build.sh \ + $(if $(ZSH_VERSION),--zsh-version $(ZSH_VERSION)) \ + --image $(IMAGE) + +## help — list available targets +help: + @grep -E '^## ' Makefile | sed 's/^## / /' + +# Install zunit + helpers into bin/ — mirrors what test-native.yml does in CI. +bin/zunit: + @echo "Installing zunit into bin/ ..." + @mkdir -p bin + @curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ + > bin/revolver + @curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ + > bin/color + @git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git 2>/dev/null + @cd /tmp/zunit.git && ./build.zsh + @mv /tmp/zunit.git/zunit bin/zunit + @chmod u+x bin/color bin/revolver bin/zunit + @rm -rf /tmp/zunit.git + @echo "Done." From 15db08643187a1777fb0c2029c208818626ff9ff Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Fri, 15 May 2026 12:52:27 +0100 Subject: [PATCH 22/47] docs: add README and usage documentation - README.md: project hub with overview, two-tier CI architecture diagram, quick-start commands, image tag table, and navigation links to all docs - docs/local-testing.md: Makefile targets (test/run/shell/build), annotated examples, and full variable reference - docs/ci-workflows.md: two-tier CI model, all workflow triggers and steps, environment variable reference across workflows - docs/cross-repo.md: workflow_call integration guide with end-to-end caller example, input reference, zi_ref strategy, and pinning advice - docs/writing-tests.md: zi_test helper, file/polaris path patterns, variable interpolation rules, test isolation model, adding suites, and common assertion reference --- README.md | 79 ++++++++++++++++++ docs/ci-workflows.md | 113 ++++++++++++++++++++++++++ docs/cross-repo.md | 103 ++++++++++++++++++++++++ docs/local-testing.md | 145 +++++++++++++++++++++++++++++++++ docs/writing-tests.md | 183 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 623 insertions(+) create mode 100644 README.md create mode 100644 docs/ci-workflows.md create mode 100644 docs/cross-repo.md create mode 100644 docs/local-testing.md create mode 100644 docs/writing-tests.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c5bc89 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# zd — Zi Docker Testing Environment + +`zd` is the official test harness for the [Zi](https://github.com/z-shell/zi) plugin manager. It provides a ZUnit test suite that verifies Zi commands work correctly — plugin installation, snippet loading, ice modifiers, annexes, and packages. The suite runs natively on CI for every pull request and inside Docker containers for Zsh version compatibility testing. It also works as a reusable workflow so other repos in the Z-Shell ecosystem can test against a specific Zi commit. + +## Architecture + +``` + ┌─────────────────────────────────┐ + │ test-native.yml │ + push / PR ───────▶│ ubuntu-latest · zsh from apt │ + schedule │ one job per .zunit file (fast) │ + workflow_call ───▶│ supports zi_repo + zi_ref input │ + └─────────────────────────────────┘ + + ┌─────────────────────────────────┐ + weekly ──────────▶│ test-matrix.yml │ + workflow_dispatch │ Docker · Zsh 5.5.1 – 5.9 │ + │ one job per Zsh version │ + └─────────────────────────────────┘ + + ┌─────────────────────────────────┐ + local ───────────▶│ Makefile │ + │ make test / run / shell / build │ + └─────────────────────────────────┘ +``` + +Native CI catches regressions on every merge. The Docker matrix verifies Zsh version compatibility on a weekly cadence without blocking pull requests. The Makefile gives contributors a local workflow identical to CI. + +## Quick Start + +```sh +# Run the full ZUnit suite natively (installs zunit on first run) +make test + +# Run a single ad-hoc zi command in Docker +make run CMD="zi light z-shell/z-a-bin-gem-node" + +# Open an interactive shell with zi already loaded +make shell +``` + +Pull the prebuilt image directly: + +```sh +docker run --rm -it ghcr.io/z-shell/zd:latest +docker run --rm -it ghcr.io/z-shell/zd:zsh-5.9 +``` + +## Documentation + +| Topic | File | +|---|---| +| Local testing — Makefile targets, env vars, Docker | [docs/local-testing.md](docs/local-testing.md) | +| CI workflows — triggers, inputs, caching | [docs/ci-workflows.md](docs/ci-workflows.md) | +| Cross-repo integration — test your zi PR from another repo | [docs/cross-repo.md](docs/cross-repo.md) | +| Writing tests — zi_test, assertions, adding suites | [docs/writing-tests.md](docs/writing-tests.md) | + +## Available Image Tags + +| Tag | Zsh version | +|---|---| +| `latest` | Alpine's default Zsh | +| `zsh-5.5.1` | 5.5.1 | +| `zsh-5.6.2` | 5.6.2 | +| `zsh-5.7.1` | 5.7.1 | +| `zsh-5.8` | 5.8 | +| `zsh-5.8.1` | 5.8.1 | +| `zsh-5.9` | 5.9 | + +Images are published to `ghcr.io/z-shell/zd` on every push to `main` and on a weekly schedule. + +## Contributing + +1. Fork the repo and create a branch. +2. Add or edit test files in `tests/`. +3. Run `make test` to verify locally. +4. Open a pull request — CI runs the full suite automatically. + +See [docs/writing-tests.md](docs/writing-tests.md) for the test authoring guide. diff --git a/docs/ci-workflows.md b/docs/ci-workflows.md new file mode 100644 index 0000000..abc9d6f --- /dev/null +++ b/docs/ci-workflows.md @@ -0,0 +1,113 @@ +# CI Workflows + +`zd` uses a two-tier CI model. Native tests run on every push and pull request to catch regressions quickly. The Docker matrix runs weekly to verify compatibility across Zsh versions without blocking merges. + +## Overview + +| Workflow | File | Trigger | Purpose | +|---|---|---|---| +| ZUnit (native) | `test-native.yml` | push, PR, schedule, dispatch, `workflow_call` | Fast ZUnit suite on ubuntu-latest | +| ZUnit (Zsh matrix) | `test-matrix.yml` | weekly, dispatch | Zsh 5.5.1–5.9 compat via Docker | +| Zi Docker | `docker.yml` | push, tags, schedule | Build and publish multi-arch images | +| Zsh -n | `zsh-n.yml` | push, PR | Syntax check all `.zsh` files | +| CodeQL | `codeql.yml` | push, PR, schedule | Security scanning | + +--- + +## test-native.yml + +The primary CI workflow. Runs on every push to `main` (when `tests/**` or `utils.zsh` change), on all pull requests, on a weekly Monday schedule, and on manual or `workflow_call` dispatch. + +**Matrix:** one parallel job per `.zunit` file — `annexes`, `ice`, `packages`, `plugins`, `snippets`. Jobs run with `fail-fast: false` so a failure in one suite does not cancel the others. + +**Steps per job:** + +1. Checkout the repository +2. Install `zsh` via `apt-get` +3. Install `zunit`, `revolver`, and `color` into `bin/` +4. Install Zi — either via the default install script or a custom repo/ref when inputs are provided (see [Cross-Repo Integration](cross-repo.md)) +5. Run `zunit --tap --verbose tests/.zunit` + +**Environment variables set by the workflow:** + +| Variable | Value | Purpose | +|---|---|---| +| `PATH` | `$PWD/bin:$PATH` | Makes `zunit`, `revolver`, `color` available | +| `ZI_BIN` | `$HOME/.zi/bin` | Points `zi_test` at the installed Zi binary | +| `ZI_DATA` | `$RUNNER_TEMP/zunit` | Isolated data dir; wiped between tests | +| `TERM` | `xterm` | Required for Zi's output formatting | + +**Manual dispatch inputs** (available in the GitHub Actions UI): + +| Input | Default | Description | +|---|---|---| +| `zi_repo` | _(empty)_ | GitHub repo for Zi (`owner/name`). Empty uses the default install script. | +| `zi_ref` | `main` | Branch, tag, or SHA to install. | + +--- + +## test-matrix.yml + +Runs weekly (Wednesday 03:00 UTC) and on manual dispatch. Not triggered by push or pull request — Zsh version compatibility is a periodic concern, not a per-commit one. + +**Matrix:** six parallel jobs, one per Zsh version: + +| Version | Tag suffix | +|---|---| +| 5.5.1 | `zsh-5.5.1` | +| 5.6.2 | `zsh-5.6.2` | +| 5.7.1 | `zsh-5.7.1` | +| 5.8 | `zsh-5.8` | +| 5.8.1 | `zsh-5.8.1` | +| 5.9 | `zsh-5.9` | + +**Per job:** +1. Build the Docker image for that Zsh version using `docker/setup-buildx-action` and `docker/build-push-action`, passing `ZSH_VERSION` as a build arg +2. Layer caching via `type=gha` — only changed layers rebuild on subsequent runs +3. Run all test files in a single container invocation: + +```sh +docker run --rm \ + --env TERM=xterm \ + --env ZI_DATA=/data \ + --volume "${RUNNER_TEMP}/zunit:/data" \ + "zd:${{ matrix.zsh_version }}" \ + zsh -c 'for f in /src/tests/*.zunit; do zunit --tap --verbose "$f" || exit $?; done' +``` + +Running all suites in one container (rather than one container per suite) keeps the job count at 6 instead of 30. + +--- + +## docker.yml + +Builds and publishes multi-architecture images (`linux/amd64`, `linux/arm64`) to `ghcr.io/z-shell/zd`. + +**Triggers:** +- Push to `main` touching `docker/**`, `scripts/**`, `tests/**`, or `lib/**` +- Tag push matching `v*.*.*` +- Weekly schedule (Wednesday 03:00 UTC) +- Manual dispatch + +**Jobs:** + +`build-versioned` — builds one image per Zsh version (5.5.1–5.9) with tag `zsh-`. Images are pushed only when the trigger is not a pull request (`github.event.number == 0`). + +`build-latest` — builds the `latest` tag. Pushed only on `main` branch pushes. + +Layer caching uses `type=gha` for both jobs. + +--- + +## Environment variable reference + +All workflows share a common set of variables. The table below covers every variable used across the three main workflows. + +| Variable | Workflow | Default | Description | +|---|---|---|---| +| `TERM` | native, matrix | `xterm` | Terminal type required by Zi output | +| `ZI_BIN` | native | `$HOME/.zi/bin` | Path to Zi binary directory | +| `ZI_DATA` | native, matrix | `$RUNNER_TEMP/zunit` | Plugin/snippet data directory | +| `ZI_REPO` | native (input) | `z-shell/zi` | Zi GitHub repo when using `workflow_call` | +| `ZI_REF` | native (input) | `main` | Zi branch/tag/SHA when using `workflow_call` | +| `ZSH_VERSION` | matrix, docker | _(empty)_ | Zsh version to bake into the Docker image | diff --git a/docs/cross-repo.md b/docs/cross-repo.md new file mode 100644 index 0000000..7694290 --- /dev/null +++ b/docs/cross-repo.md @@ -0,0 +1,103 @@ +# Cross-Repo Integration + +`test-native.yml` is published as a reusable GitHub Actions workflow via the `workflow_call` trigger. Any repository in (or outside) the Z-Shell ecosystem can call it to run the full ZUnit suite against a specific Zi commit, branch, or tag — without maintaining a copy of the test infrastructure. + +## How it works + +When called with `zi_repo` and `zi_ref` inputs, `test-native.yml` clones that exact revision of Zi directly instead of using the default install script. The rest of the workflow is identical: the full test matrix runs (`annexes`, `ice`, `packages`, `plugins`, `snippets`), and results appear in the caller's GitHub Actions UI. + +This means a Zi pull request can trigger `zd` tests as part of its own CI pipeline, catching regressions in the test suite before the PR is merged. + +--- + +## End-to-end example + +Add this file to the repository you want to test from (e.g. `z-shell/zi`): + +```yaml +# .github/workflows/zd-integration.yml +name: "zd integration tests" + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + zd: + name: "ZUnit suite" + uses: z-shell/zd/.github/workflows/test-native.yml@main + with: + zi_repo: z-shell/zi # the repo being tested + zi_ref: ${{ github.sha }} # the exact commit under test +``` + +**What each field does:** + +| Field | Purpose | +|---|---| +| `uses: z-shell/zd/.github/workflows/test-native.yml@main` | Calls the reusable workflow at the `main` ref of `zd` | +| `zi_repo: z-shell/zi` | Tells `zd` to clone this repo instead of using the default install | +| `zi_ref: ${{ github.sha }}` | Pins to the exact commit that triggered the caller's workflow | + +The caller's `GITHUB_TOKEN` is used automatically — no additional secrets are required. Both repositories must be public. + +--- + +## Input reference + +| Input | Type | Required | Default | Description | +|---|---|---|---|---| +| `zi_repo` | `string` | No | `""` | GitHub repo for Zi in `owner/name` format. When empty, the default install script is used. | +| `zi_ref` | `string` | No | `main` | Branch name, tag, or full commit SHA to check out. | + +--- + +## Choosing `zi_ref` + +**`${{ github.sha }}`** — use this in pull request workflows. Tests the exact commit under review. No ambiguity about what is being tested. + +```yaml +zi_ref: ${{ github.sha }} +``` + +**Branch name** — tracks a branch continuously. Useful for nightly runs against `main` without tying to a specific commit. + +```yaml +zi_ref: main +zi_ref: develop +``` + +**Tag** — pins to a release. Use this when you want a stable baseline, not the latest development state. + +```yaml +zi_ref: v1.2.3 +``` + +--- + +## Permissions and tokens + +`workflow_call` inherits the `GITHUB_TOKEN` from the calling workflow. The token is scoped to the caller's repository with read permissions on `contents`. No secrets need to be shared between repositories. + +`zd` only performs outbound network requests (to install `zunit` and clone `zi_repo`) — it does not write back to either repository. + +--- + +## Pinning the zd version + +The `uses:` line can reference `zd` by branch or by tag: + +```yaml +# Always use the latest zd (may include breaking changes) +uses: z-shell/zd/.github/workflows/test-native.yml@main + +# Pin to a specific zd release (stable, auditable) +uses: z-shell/zd/.github/workflows/test-native.yml@v1.0.0 + +# Pin to a specific commit SHA (most stable) +uses: z-shell/zd/.github/workflows/test-native.yml@a1b2c3d +``` + +For production CI in a release-tracked repo, pinning to a tag or SHA is recommended. For development repos following Zi's `main` branch, `@main` is sufficient. diff --git a/docs/local-testing.md b/docs/local-testing.md new file mode 100644 index 0000000..438d426 --- /dev/null +++ b/docs/local-testing.md @@ -0,0 +1,145 @@ +# Local Testing + +The Makefile provides four targets that cover the full local workflow: running the test suite natively, executing ad-hoc Zi commands in Docker, opening an interactive shell, and building the image locally. + +## Prerequisites + +**For native tests (`make test`):** +- Zsh installed (`zsh --version`) +- Zi installed — default location `~/.zi/bin`. Override with `ZI_BIN=` if installed elsewhere. +- Internet access on first run (downloads `zunit` into `bin/`) + +**For Docker targets (`make run`, `make shell`, `make build`):** +- Docker running locally +- The prebuilt image pulled (`docker pull ghcr.io/z-shell/zd:latest`) — or build it with `make build` + +--- + +## Running the test suite — `make test` + +```sh +make test +``` + +On the first run, `make test` automatically installs `zunit`, `revolver`, and `color` into `bin/`. This mirrors exactly what the CI workflow does. Subsequent runs skip the install step (the `bin/zunit` file already exists). + +``` +Installing zunit into bin/ ... +Done. +==> tests/annexes.zunit +TAP version 13 +ok 1 - z-a-bin-gem-node installation +ok 2 - z-a-meta-plugins installation +... +==> tests/ice.zunit +... +``` + +**Run a single suite:** + +```sh +make test FILE=annexes +make test FILE=ice +make test FILE=packages +make test FILE=plugins +make test FILE=snippets +``` + +**Override where Zi is installed:** + +```sh +make test ZI_BIN=/path/to/zi/bin +``` + +**Use a different data directory:** + +```sh +make test ZI_DATA=/tmp/my-zunit-run +``` + +Each test suite wipes `ZI_DATA` between individual tests (via `tests/setup.zsh`), so isolation is guaranteed regardless of what you set here. + +--- + +## Running an ad-hoc Zi command — `make run` + +```sh +make run CMD="" +``` + +This starts a fresh container from `ghcr.io/z-shell/zd:latest`, sources Zi via `zsh -il`, and runs your command. The container is removed when it exits. + +**Examples:** + +```sh +# Install a plugin +make run CMD="zi light z-shell/z-a-bin-gem-node" + +# Load a snippet +make run CMD="zi snippet OMZL::spectrum.zsh" + +# Install a program from GitHub releases +make run CMD="zi lucid as\"program\" from\"gh-r\" for junegunn/fzf" + +# Use a specific image tag +make run CMD="zi light z-shell/z-a-rust" TAG=zsh-5.9 +``` + +`CMD` is required. Running `make run` without it prints a usage error. + +--- + +## Interactive shell — `make shell` + +```sh +make shell +``` + +Opens an interactive Zsh session inside the container with Zi already sourced. Use this to explore the environment, debug a failing test manually, or prototype a new Zi command before writing a test for it. + +```sh +$ make shell +user@zi-docker ~ $ zi light junegunn/fzf +... +user@zi-docker ~ $ which fzf +~/.zi/polaris/bin/fzf +user@zi-docker ~ $ exit +``` + +The container is removed on exit. State does not persist between sessions. + +--- + +## Building the image locally — `make build` + +```sh +# Build with Alpine's default Zsh (same as :latest) +make build + +# Build with a specific Zsh version baked in +make build ZSH_VERSION=5.9 +make build ZSH_VERSION=5.8.1 + +# Build with a custom image name and tag +make build IMAGE=my-zd TAG=dev +``` + +After building, use the image in other targets: + +```sh +make run CMD="zi light fzf" IMAGE=my-zd TAG=dev +make shell IMAGE=my-zd TAG=dev +``` + +--- + +## Variable reference + +| Variable | Default | Purpose | +|---|---|---| +| `ZI_BIN` | `~/.zi/bin` | Path to the Zi binary directory (native tests) | +| `ZI_DATA` | `/tmp/zunit-local` | Data directory for plugins/snippets during tests | +| `IMAGE` | `ghcr.io/z-shell/zd` | Docker image name | +| `TAG` | `latest` | Docker image tag | +| `FILE` | _(all suites)_ | Single `.zunit` suite name to run (without extension) | +| `ZSH_VERSION` | _(empty)_ | Zsh version to bake into a local Docker build | diff --git a/docs/writing-tests.md b/docs/writing-tests.md new file mode 100644 index 0000000..7b48b3f --- /dev/null +++ b/docs/writing-tests.md @@ -0,0 +1,183 @@ +# Writing Tests + +Tests live in `tests/` as `.zunit` files and are run by [ZUnit](https://github.com/zdharma/zunit). Each file covers a logical group of Zi functionality. The same files run in both the native CI tier and the Docker matrix — no duplication. + +## Test file anatomy + +Every `.zunit` file follows this structure: + +```zsh +#!/usr/bin/env zunit +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +@setup { + load setup # resets ZI_DATA between tests + load helpers # provides zi_test() + setup +} + +@teardown { + load teardown + teardown +} + +@test 'descriptive test name' { + # test body +} +``` + +`@setup` and `@teardown` run before and after each `@test` block. `load setup` and `load helpers` source `tests/setup.zsh` and `tests/helpers.zsh` respectively. + +--- + +## The `zi_test` helper + +`zi_test` is the core of every test. It spawns a fresh, isolated Zsh subprocess, sources Zi, runs the snippet you pass, then captures the exit code in `$state` and all output in `$output`. + +**Signature:** + +```zsh +zi_test '' +``` + +**Minimal example:** + +```zsh +@test 'fzf installs as a program' { + zi_test 'zi lucid as"program" from"gh-r" for junegunn/fzf' + + assert $state equals 0 + assert "$output" contains "Unpacking" + assert "$output" contains "Successfully" +} +``` + +**Multi-line commands** — use a single-quoted heredoc style: + +```zsh +@test 'sbin ice creates shim' { + zi_test ' + zi light z-shell/z-a-bin-gem-node + zi light-mode as"null" from"gh-r" sbin"fzf" for junegunn/fzf + ' + + assert $state equals 0 +} +``` + +**Testing failure** — assert non-zero exit codes explicitly: + +```zsh +@test 'bad mv pattern fails with exit 1' { + zi_test ' + zi as"command" from"gh-r" bpick"*musl*" mv"DOES_NOT_EXIST* -> fd" pick"fd/fd" \ + for @sharkdp/fd + ' + + assert $state equals 1 + assert "$output" contains "DOES_NOT_EXIST" +} +``` + +--- + +## Asserting on files + +After `zi_test` runs, the installed plugin or snippet lives under `$ZI_DATA`. Use `$ZI_DATA` in assertions — never hardcode a path. + +**Plugin path pattern:** `${ZI_DATA}/plugins/---/` + +```zsh +local artifact="${ZI_DATA}/plugins/junegunn---fzf/fzf" +assert "$artifact" is_file +assert "$artifact" is_executable +``` + +**Snippet path pattern:** `${ZI_DATA}/snippets//` + +```zsh +local artifact="${ZI_DATA}/snippets/OMZL::spectrum.zsh/OMZL::spectrum.zsh" +assert "$artifact" is_file +assert "$artifact" is_readable +``` + +**Polaris (programs installed via `sbin`):** `${ZI_DATA}/polaris/bin/` + +```zsh +assert "${ZI_DATA}/polaris/bin/fzf" is_executable +``` + +Note how `owner/name` becomes `owner---name` in the filesystem path — Zi replaces `/` with `---`. + +--- + +## Variable interpolation + +`zi_test` receives a string that is embedded into an inner Zsh process. There are two shells involved: the outer ZUnit shell and the inner Zsh started by `zi_test`. + +- **Single quotes** — the string is passed literally; `$VAR` references resolve in the *inner* shell (after Zi is sourced). This is the default and is what you want for most tests. +- **Double quotes** — the string is interpolated by the *outer* shell before being passed in. Use this when you want to inject an outer variable's value. + +```zsh +# Correct — expands $my_plugin in the outer (ZUnit) shell +zi_test "zi light ${my_plugin}" + +# Wrong — $my_plugin is undefined in the inner shell, expands to empty +zi_test 'zi light ${my_plugin}' +``` + +In practice, test values are almost always literals, so single quotes are correct in the vast majority of cases. + +--- + +## Test isolation + +Each `zi_test` call is a completely fresh Zsh process. There is no shared Zi state between individual `@test` blocks — no loaded plugins, no cached data, no side-effects from previous tests. + +`tests/setup.zsh` wipes `$ZI_DATA` between every `@test` block: + +```zsh +setup() { + rm -rf "${ZI_DATA:?}"/* + mkdir -p "${ZI_DATA}" +} +``` + +This means tests can be run in any order and do not depend on each other. + +--- + +## Adding a new suite + +1. Create `tests/.zunit` following the anatomy above. +2. Add `` to the matrix in `.github/workflows/test-native.yml`: + +```yaml +matrix: + file: [annexes, ice, packages, plugins, snippets, ] +``` + +3. Verify locally before pushing: + +```sh +make test FILE= +``` + +The new suite will be picked up automatically by the Docker matrix workflow (`test-matrix.yml`) — it iterates over all `*.zunit` files, so no change is needed there. + +--- + +## Common assertion patterns + +| Assertion | Meaning | +|---|---| +| `assert $state equals 0` | Command exited successfully | +| `assert $state equals 255` | Command exited with a specific non-zero code | +| `assert $state not_equal_to 0` | Command failed (any non-zero code) | +| `assert "$output" contains "text"` | Output includes the substring | +| `assert "$artifact" is_file` | Path exists and is a regular file | +| `assert "$artifact" is_executable` | Path exists and is executable | +| `assert "$artifact" is_readable` | Path exists and is readable | + +Full ZUnit assertion reference: From bc7723d5f46e9bc0d1af25753b8fa513e985a163 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Fri, 15 May 2026 23:48:16 +0100 Subject: [PATCH 23/47] refactor: update configs, workflows, docs, and docker files - Add .editorconfig and .geminiignore - Gitignore AI agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, .github/copilot-instructions.md) and remove the latter from tracking - Update .trunk/trunk.yaml configuration - Refine CI workflows: codeql.yml, docker.yml, test-native.yml, zsh-n.yml - Update Makefile, README.md, and tests/helpers.zsh - Refresh all docs/: ci-workflows, cross-repo, local-testing, writing-tests - Remove stale AI planning artifacts from docs/superpowers/ - Update docker/Dockerfile, utils.zsh, zshenv, zshrc --- .editorconfig | 24 + .geminiignore | 0 .github/copilot-instructions.md | 74 - .github/workflows/codeql.yml | 6 +- .github/workflows/docker.yml | 2 - .github/workflows/test-native.yml | 7 +- .github/workflows/zsh-n.yml | 4 +- .gitignore | 4 + .trunk/.gitignore | 1 + .trunk/trunk.yaml | 22 +- Makefile | 2 +- README.md | 32 +- docker/Dockerfile | 9 +- docker/utils.zsh | 8 +- docker/zshenv | 1 + docker/zshrc | 2 +- docs/ci-workflows.md | 66 +- docs/cross-repo.md | 22 +- docs/local-testing.md | 24 +- .../plans/2026-05-06-zd-refactor.md | 1457 ----------------- .../specs/2026-05-06-zd-refactor-design.md | 233 --- docs/writing-tests.md | 36 +- tests/helpers.zsh | 2 +- 23 files changed, 154 insertions(+), 1884 deletions(-) create mode 100644 .editorconfig create mode 100644 .geminiignore delete mode 100644 .github/copilot-instructions.md delete mode 100644 docs/superpowers/plans/2026-05-06-zd-refactor.md delete mode 100644 docs/superpowers/specs/2026-05-06-zd-refactor-design.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7725ded --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[Makefile*] +indent_style = tab +indent_size = 4 + +[*.py] +indent_size = 4 + +[*.go] +indent_style = tab +indent_size = 4 + +[*.java] +indent_style = tab +indent_size = 4 diff --git a/.geminiignore b/.geminiignore new file mode 100644 index 0000000..e69de29 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 1913908..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,74 +0,0 @@ -# Copilot Instructions — z-shell/zd - -## Project overview - -`zd` is a **Zi Docker environment** — an Alpine Linux–based Docker image that provides a ready-to-use Zsh + [Zi](https://github.com/z-shell/zi) plugin-manager environment. The repository contains: - -- The `Dockerfile` and supporting shell scripts that build the image. -- CI/CD workflows that build multi-architecture images and verify functionality. - -## Repository layout - -``` -docker/ - Dockerfile # Alpine-based image definition - entrypoint.sh # POSIX sh setup script run at image-build time (root) - utils.zsh # Zsh helper functions (prepare_system, initiate_system, zi::*) - zshenv # Zsh env bootstrap (ZI config, PATH additions) - zshrc # Zsh startup config — sources utils.zsh and calls prepare_system/initiate_system - docker-compose.yml # Compose file for interactive use -.github/ - workflows/ - docker.yml # CI: multi-arch Docker build matrix (versioned Zsh + latest) - codeql.yml # CodeQL security scanning - zsh-n.yml # Zsh -n (syntax check) workflow - ISSUE_TEMPLATE/ # GitHub issue templates - PULL_REQUEST_TEMPLATE.md - CODEOWNERS # @ss-o owns all files -``` - -## Key conventions - -### Shell scripts -- **`entrypoint.sh`** is POSIX `sh` (shebang `#!/usr/bin/env sh`). It runs as root inside the Alpine build context. - - Use `sed -i -r` (BusyBox `sed` extended-regex flag) — **not** `-E`, which is unsupported by BusyBox. - - Never use Bashisms (`[[ ]]`, arrays, `local` with assignment, etc.) in this file. -- **Zsh files** (`utils.zsh`, `zshrc`, `zshenv`) use 2-space indentation and the modeline `# vim: ft=zsh sw=2 ts=2 et`. -- All text files: UTF-8, LF line endings. Default indent is 2 spaces, except: - - `Makefile*`: tab indentation with `indent_size=4`. - - `*.py`, `*.rb`: 4-space indentation. - - `*.go`, `*.java`, `*.scala`, `*.groovy`, `*.kotlin`: tab indentation with `indent_size=4`. -- Trailing whitespace is trimmed; files end with a newline (enforced by `.editorconfig`). - -### Dockerfile -- Base image: `alpine:$VERSION` (defaults to `edge`). No GNU coreutils unless explicitly `apk add`ed. -- `SHELL` instructions in a Dockerfile only configure the default shell for subsequent `RUN`/`CMD`/`ENTRYPOINT` — they **do not execute** their arguments. Never use a `SHELL` instruction expecting it to run a command. -- Do **not** invoke interactive shell functions (e.g., Zi's `@zi-scheduler`) in `RUN` steps — they only exist inside a live Zsh session loaded via `.zshrc`. -- Go is installed from `https://go.dev/dl/` (not from `apk`) to get a current release. Bump `ARG GO_VERSION` when a new Go release is available; SHA256 is verified via the `https://go.dev/dl/?mode=json&include=all` API. -- `LABEL` values must be plain strings — no template syntax like `<%= ... =>`. - - -## CI / workflows - -| Workflow | Trigger | What it does | -|---|---|---| -| `docker.yml` | push/PR to `main` touching `docker/**`, scheduled Wed 03:00 UTC | Builds multi-arch image (`linux/amd64`, `linux/arm64`) for Zsh 5.5.1–5.9 matrix + `latest` tag | -| `zsh-n.yml` | Zsh `-n` syntax check | Checks all Zsh files for syntax errors | - -### Common build failure causes -1. **`exit 127` in `RUN`**: A command not found — check that it exists in Alpine at build time. -2. **`sed: unrecognized option '-E'`**: Use `-r` instead (BusyBox `sed`). -3. **Zi/Zsh functions not found**: Functions like `@zi-scheduler`, `zi`, `autoload` only exist in a live interactive Zsh session, never at Docker build time. -4. **Go SHA256 mismatch**: The `GO_VERSION` ARG may need updating to a version that exists on `go.dev/dl/`. - -## Development workflow - -1. Edit files in `docker/`. -2. Build locally: `docker compose build`. -3. Submit a PR — CI will run Docker build and syntax checks. - -## Security notes - -- Never commit secrets or credentials. -- The Go tarball SHA256 is verified against the official `go.dev` JSON API before extraction. -- `sudoers` for the container user is scoped to `NOPASSWD: ALL` intentionally for the dev environment. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index aacc6fd..3706dde 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,11 +13,11 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '17 7 * * 2' + - cron: "17 7 * * 2" jobs: analyze: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4640608..2518547 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,7 +42,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v4 with: - install: true use: true - uses: docker/login-action@v4 with: @@ -75,7 +74,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v4 with: - install: true use: true - uses: docker/login-action@v4 with: diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index c8014c9..39e2399 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -62,9 +62,10 @@ jobs: ZI_REF: ${{ inputs.zi_ref }} run: | if [[ -n "$ZI_REPO" ]]; then + zi_home="${XDG_DATA_HOME:-${HOME}/.local/share}/zi" git clone --depth 1 --branch "${ZI_REF:-main}" \ - "https://github.com/${ZI_REPO}.git" "${HOME}/.zi/bin" - mkdir -p "${HOME}/.zi"/{cache,completions,plugins,snippets} + "https://github.com/${ZI_REPO}.git" "${zi_home}/bin" + mkdir -p "${zi_home}"/{cache,completions,plugins,snippets} else zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip fi @@ -73,6 +74,6 @@ jobs: run: | export PATH="$PWD/bin:$PATH" export TERM=xterm - export ZI_BIN="${HOME}/.zi/bin" + export ZI_BIN="${XDG_DATA_HOME:-${HOME}/.local/share}/zi/bin" export ZI_DATA="${RUNNER_TEMP}/zunit" zunit --tap --verbose "tests/${{ matrix.file }}.zunit" diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 40721b1..861891e 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -5,9 +5,9 @@ on: push: tags: ["v*.*.*"] branches: [main, next] - paths: [./zi/**] + paths: [zi/**] pull_request: - paths: [./zi/**] + paths: [zi/**] workflow_dispatch: {} jobs: diff --git a/.gitignore b/.gitignore index a840ea6..bb15823 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,7 @@ TAGS CVS .#* +AGENTS.md +CLAUDE.md +GEMINI.md +.github/copilot-instructions.md diff --git a/.trunk/.gitignore b/.trunk/.gitignore index 1e24652..15966d0 100644 --- a/.trunk/.gitignore +++ b/.trunk/.gitignore @@ -6,3 +6,4 @@ plugins user_trunk.yaml user.yaml +tmp diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 18e419d..dc23686 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,10 +1,10 @@ version: 0.1 cli: - version: 1.17.2 + version: 1.25.0 plugins: sources: - id: trunk - ref: v1.3.0 + ref: v1.10.0 uri: https://github.com/trunk-io/plugins lint: disabled: @@ -12,21 +12,21 @@ lint: - terrascan - yamllint - trivy + - trufflehog enabled: - - trufflehog@3.63.2-rc0 - - actionlint@1.6.26 + - actionlint@1.7.12 - git-diff-check - - gitleaks@8.18.1 - - hadolint@2.12.0 - - markdownlint@0.37.0 - - prettier@3.1.0 - - shellcheck@0.9.0 + - gitleaks@8.30.1 + - hadolint@2.14.0 + - markdownlint@0.48.0 + - prettier@3.8.3 + - shellcheck@0.11.0 - shfmt@3.6.0 runtimes: enabled: - go@1.21.0 - - node@20.10.0 - - python@3.10.9 + - node@22.16.0 + - python@3.14.4 actions: enabled: - trunk-announce diff --git a/Makefile b/Makefile index 99f0336..8cfe0d6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # -*- mode: makefile; -*- SHELL := bash -ZI_BIN ?= $(HOME)/.zi/bin +ZI_BIN ?= $(HOME)/.local/share/zi/bin ZI_DATA ?= /tmp/zunit-local IMAGE ?= ghcr.io/z-shell/zd TAG ?= latest diff --git a/README.md b/README.md index 2c5bc89..8842aac 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Architecture -``` +```text ┌─────────────────────────────────┐ │ test-native.yml │ push / PR ───────▶│ ubuntu-latest · zsh from apt │ @@ -48,24 +48,24 @@ docker run --rm -it ghcr.io/z-shell/zd:zsh-5.9 ## Documentation -| Topic | File | -|---|---| -| Local testing — Makefile targets, env vars, Docker | [docs/local-testing.md](docs/local-testing.md) | -| CI workflows — triggers, inputs, caching | [docs/ci-workflows.md](docs/ci-workflows.md) | -| Cross-repo integration — test your zi PR from another repo | [docs/cross-repo.md](docs/cross-repo.md) | -| Writing tests — zi_test, assertions, adding suites | [docs/writing-tests.md](docs/writing-tests.md) | +| Topic | File | +| ---------------------------------------------------------- | ---------------------------------------------- | +| Local testing — Makefile targets, env vars, Docker | [docs/local-testing.md](docs/local-testing.md) | +| CI workflows — triggers, inputs, caching | [docs/ci-workflows.md](docs/ci-workflows.md) | +| Cross-repo integration — test your zi PR from another repo | [docs/cross-repo.md](docs/cross-repo.md) | +| Writing tests — zi_test, assertions, adding suites | [docs/writing-tests.md](docs/writing-tests.md) | ## Available Image Tags -| Tag | Zsh version | -|---|---| -| `latest` | Alpine's default Zsh | -| `zsh-5.5.1` | 5.5.1 | -| `zsh-5.6.2` | 5.6.2 | -| `zsh-5.7.1` | 5.7.1 | -| `zsh-5.8` | 5.8 | -| `zsh-5.8.1` | 5.8.1 | -| `zsh-5.9` | 5.9 | +| Tag | Zsh version | +| ----------- | -------------------- | +| `latest` | Alpine's default Zsh | +| `zsh-5.5.1` | 5.5.1 | +| `zsh-5.6.2` | 5.6.2 | +| `zsh-5.7.1` | 5.7.1 | +| `zsh-5.8` | 5.8 | +| `zsh-5.8.1` | 5.8.1 | +| `zsh-5.9` | 5.9 | Images are published to `ghcr.io/z-shell/zd` on every push to `main` and on a weekly schedule. diff --git a/docker/Dockerfile b/docker/Dockerfile index afbdce2..4ced38c 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,15 +27,16 @@ RUN set -ex && apk --no-cache add \ jq # Install zunit and its helpers into /usr/local/bin at build time. +RUN git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git +WORKDIR /tmp/zunit.git RUN set -ex \ - && git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git \ - && cd /tmp/zunit.git && ./build.zsh \ + && ./build.zsh \ && mv /tmp/zunit.git/zunit /usr/local/bin/zunit \ && curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ > /usr/local/bin/revolver \ && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ > /usr/local/bin/color \ - && chmod u+x /usr/local/bin/{color,revolver,zunit} \ + && chmod u+x /usr/local/bin/color /usr/local/bin/revolver /usr/local/bin/zunit \ && rm -rf /tmp/zunit.git FROM base AS test @@ -62,7 +63,7 @@ RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip # Leave ZSH_VERSION empty for the :latest image (uses Alpine's zsh). ARG ZSH_VERSION= RUN [ -z "${ZSH_VERSION}" ] || \ - zsh -c "source \${HOME}/.zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" + zsh -c "source \${XDG_DATA_HOME:-\${HOME}/.local/share}/zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" # Switch back to root for COPY operations. USER root diff --git a/docker/utils.zsh b/docker/utils.zsh index 1f924c0..427b222 100755 --- a/docker/utils.zsh +++ b/docker/utils.zsh @@ -13,12 +13,12 @@ prepare_system() { initiate_system() { typeset -gxU path module_path - path=("${ZPFX:-${HOME}/.zi/polaris}/bin" "${HOME}/go/bin" "/usr/local/go/bin" $path) + path=("${ZPFX:-${ZI_DATA:-/data}/polaris}/bin" "${HOME}/go/bin" "/usr/local/go/bin" $path) module_path+=( /data/zmodules/zpmod/Src ) zmodload zi/zpmod &>/dev/null - source ~/.zi/bin/zi.zsh + source "${ZI_BIN}/zi.zsh" autoload -Uz _zi (( ${+_comps} )) && _comps[zi]=_zi @@ -26,10 +26,10 @@ initiate_system() { reload_system() { local zf1 zf2 - for zf1 in ~/.zi/bin/*.zsh; do + for zf1 in "${ZI_BIN}"/*.zsh; do source "$zf1" done - for zf2 in ~/.zi/bin/lib/zsh/*.zsh; do + for zf2 in "${ZI_BIN}"/lib/zsh/*.zsh; do source "$zf2" done } diff --git a/docker/zshenv b/docker/zshenv index 51cc631..cf899b0 100755 --- a/docker/zshenv +++ b/docker/zshenv @@ -4,3 +4,4 @@ export TERM=${TERM:-xterm-256color} export SHELL=${SHELL:-${commands[zsh]}} export ZI_DATA=${ZI_DATA:-/data} +export ZI_BIN=${ZI_BIN:-${XDG_DATA_HOME:-${HOME}/.local/share}/zi/bin} diff --git a/docker/zshrc b/docker/zshrc index b161e08..54754ee 100755 --- a/docker/zshrc +++ b/docker/zshrc @@ -4,7 +4,7 @@ # Source zi (pre-installed during docker build). typeset -gA ZI ZI[HOME_DIR]="${ZI_DATA:-/data}" -source "${HOME}/.zi/bin/zi.zsh" +source "${ZI_BIN}/zi.zsh" autoload -Uz _zi (( ${+_comps} )) && _comps[zi]=_zi diff --git a/docs/ci-workflows.md b/docs/ci-workflows.md index abc9d6f..658b038 100644 --- a/docs/ci-workflows.md +++ b/docs/ci-workflows.md @@ -4,13 +4,13 @@ ## Overview -| Workflow | File | Trigger | Purpose | -|---|---|---|---| -| ZUnit (native) | `test-native.yml` | push, PR, schedule, dispatch, `workflow_call` | Fast ZUnit suite on ubuntu-latest | -| ZUnit (Zsh matrix) | `test-matrix.yml` | weekly, dispatch | Zsh 5.5.1–5.9 compat via Docker | -| Zi Docker | `docker.yml` | push, tags, schedule | Build and publish multi-arch images | -| Zsh -n | `zsh-n.yml` | push, PR | Syntax check all `.zsh` files | -| CodeQL | `codeql.yml` | push, PR, schedule | Security scanning | +| Workflow | File | Trigger | Purpose | +| ------------------ | ----------------- | --------------------------------------------- | ----------------------------------- | +| ZUnit (native) | `test-native.yml` | push, PR, schedule, dispatch, `workflow_call` | Fast ZUnit suite on ubuntu-latest | +| ZUnit (Zsh matrix) | `test-matrix.yml` | weekly, dispatch | Zsh 5.5.1–5.9 compat via Docker | +| Zi Docker | `docker.yml` | push, tags, schedule | Build and publish multi-arch images | +| Zsh -n | `zsh-n.yml` | push, PR | Syntax check all `.zsh` files | +| CodeQL | `codeql.yml` | push, PR, schedule | Security scanning | --- @@ -30,19 +30,19 @@ The primary CI workflow. Runs on every push to `main` (when `tests/**` or `utils **Environment variables set by the workflow:** -| Variable | Value | Purpose | -|---|---|---| -| `PATH` | `$PWD/bin:$PATH` | Makes `zunit`, `revolver`, `color` available | -| `ZI_BIN` | `$HOME/.zi/bin` | Points `zi_test` at the installed Zi binary | -| `ZI_DATA` | `$RUNNER_TEMP/zunit` | Isolated data dir; wiped between tests | -| `TERM` | `xterm` | Required for Zi's output formatting | +| Variable | Value | Purpose | +| --------- | --------------------------- | -------------------------------------------- | +| `PATH` | `$PWD/bin:$PATH` | Makes `zunit`, `revolver`, `color` available | +| `ZI_BIN` | `$HOME/.local/share/zi/bin` | Points `zi_test` at the installed Zi binary | +| `ZI_DATA` | `$RUNNER_TEMP/zunit` | Isolated data dir; wiped between tests | +| `TERM` | `xterm` | Required for Zi's output formatting | **Manual dispatch inputs** (available in the GitHub Actions UI): -| Input | Default | Description | -|---|---|---| +| Input | Default | Description | +| --------- | --------- | ------------------------------------------------------------------------- | | `zi_repo` | _(empty)_ | GitHub repo for Zi (`owner/name`). Empty uses the default install script. | -| `zi_ref` | `main` | Branch, tag, or SHA to install. | +| `zi_ref` | `main` | Branch, tag, or SHA to install. | --- @@ -52,16 +52,17 @@ Runs weekly (Wednesday 03:00 UTC) and on manual dispatch. Not triggered by push **Matrix:** six parallel jobs, one per Zsh version: -| Version | Tag suffix | -|---|---| -| 5.5.1 | `zsh-5.5.1` | -| 5.6.2 | `zsh-5.6.2` | -| 5.7.1 | `zsh-5.7.1` | -| 5.8 | `zsh-5.8` | -| 5.8.1 | `zsh-5.8.1` | -| 5.9 | `zsh-5.9` | +| Version | Tag suffix | +| ------- | ----------- | +| 5.5.1 | `zsh-5.5.1` | +| 5.6.2 | `zsh-5.6.2` | +| 5.7.1 | `zsh-5.7.1` | +| 5.8 | `zsh-5.8` | +| 5.8.1 | `zsh-5.8.1` | +| 5.9 | `zsh-5.9` | **Per job:** + 1. Build the Docker image for that Zsh version using `docker/setup-buildx-action` and `docker/build-push-action`, passing `ZSH_VERSION` as a build arg 2. Layer caching via `type=gha` — only changed layers rebuild on subsequent runs 3. Run all test files in a single container invocation: @@ -84,6 +85,7 @@ Running all suites in one container (rather than one container per suite) keeps Builds and publishes multi-architecture images (`linux/amd64`, `linux/arm64`) to `ghcr.io/z-shell/zd`. **Triggers:** + - Push to `main` touching `docker/**`, `scripts/**`, `tests/**`, or `lib/**` - Tag push matching `v*.*.*` - Weekly schedule (Wednesday 03:00 UTC) @@ -103,11 +105,11 @@ Layer caching uses `type=gha` for both jobs. All workflows share a common set of variables. The table below covers every variable used across the three main workflows. -| Variable | Workflow | Default | Description | -|---|---|---|---| -| `TERM` | native, matrix | `xterm` | Terminal type required by Zi output | -| `ZI_BIN` | native | `$HOME/.zi/bin` | Path to Zi binary directory | -| `ZI_DATA` | native, matrix | `$RUNNER_TEMP/zunit` | Plugin/snippet data directory | -| `ZI_REPO` | native (input) | `z-shell/zi` | Zi GitHub repo when using `workflow_call` | -| `ZI_REF` | native (input) | `main` | Zi branch/tag/SHA when using `workflow_call` | -| `ZSH_VERSION` | matrix, docker | _(empty)_ | Zsh version to bake into the Docker image | +| Variable | Workflow | Default | Description | +| ------------- | -------------- | --------------------------- | -------------------------------------------- | +| `TERM` | native, matrix | `xterm` | Terminal type required by Zi output | +| `ZI_BIN` | native | `$HOME/.local/share/zi/bin` | Path to Zi binary directory | +| `ZI_DATA` | native, matrix | `$RUNNER_TEMP/zunit` | Plugin/snippet data directory | +| `ZI_REPO` | native (input) | `z-shell/zi` | Zi GitHub repo when using `workflow_call` | +| `ZI_REF` | native (input) | `main` | Zi branch/tag/SHA when using `workflow_call` | +| `ZSH_VERSION` | matrix, docker | _(empty)_ | Zsh version to bake into the Docker image | diff --git a/docs/cross-repo.md b/docs/cross-repo.md index 7694290..c652ca7 100644 --- a/docs/cross-repo.md +++ b/docs/cross-repo.md @@ -29,17 +29,17 @@ jobs: name: "ZUnit suite" uses: z-shell/zd/.github/workflows/test-native.yml@main with: - zi_repo: z-shell/zi # the repo being tested - zi_ref: ${{ github.sha }} # the exact commit under test + zi_repo: z-shell/zi # the repo being tested + zi_ref: ${{ github.sha }} # the exact commit under test ``` **What each field does:** -| Field | Purpose | -|---|---| -| `uses: z-shell/zd/.github/workflows/test-native.yml@main` | Calls the reusable workflow at the `main` ref of `zd` | -| `zi_repo: z-shell/zi` | Tells `zd` to clone this repo instead of using the default install | -| `zi_ref: ${{ github.sha }}` | Pins to the exact commit that triggered the caller's workflow | +| Field | Purpose | +| --------------------------------------------------------- | ------------------------------------------------------------------ | +| `uses: z-shell/zd/.github/workflows/test-native.yml@main` | Calls the reusable workflow at the `main` ref of `zd` | +| `zi_repo: z-shell/zi` | Tells `zd` to clone this repo instead of using the default install | +| `zi_ref: ${{ github.sha }}` | Pins to the exact commit that triggered the caller's workflow | The caller's `GITHUB_TOKEN` is used automatically — no additional secrets are required. Both repositories must be public. @@ -47,10 +47,10 @@ The caller's `GITHUB_TOKEN` is used automatically — no additional secrets are ## Input reference -| Input | Type | Required | Default | Description | -|---|---|---|---|---| -| `zi_repo` | `string` | No | `""` | GitHub repo for Zi in `owner/name` format. When empty, the default install script is used. | -| `zi_ref` | `string` | No | `main` | Branch name, tag, or full commit SHA to check out. | +| Input | Type | Required | Default | Description | +| --------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------ | +| `zi_repo` | `string` | No | `""` | GitHub repo for Zi in `owner/name` format. When empty, the default install script is used. | +| `zi_ref` | `string` | No | `main` | Branch name, tag, or full commit SHA to check out. | --- diff --git a/docs/local-testing.md b/docs/local-testing.md index 438d426..1d0ba66 100644 --- a/docs/local-testing.md +++ b/docs/local-testing.md @@ -5,11 +5,13 @@ The Makefile provides four targets that cover the full local workflow: running t ## Prerequisites **For native tests (`make test`):** + - Zsh installed (`zsh --version`) -- Zi installed — default location `~/.zi/bin`. Override with `ZI_BIN=` if installed elsewhere. +- Zi installed — default location `${XDG_DATA_HOME:-$HOME/.local/share}/zi/bin`. Override with `ZI_BIN=` if installed elsewhere. - Internet access on first run (downloads `zunit` into `bin/`) **For Docker targets (`make run`, `make shell`, `make build`):** + - Docker running locally - The prebuilt image pulled (`docker pull ghcr.io/z-shell/zd:latest`) — or build it with `make build` @@ -23,7 +25,7 @@ make test On the first run, `make test` automatically installs `zunit`, `revolver`, and `color` into `bin/`. This mirrors exactly what the CI workflow does. Subsequent runs skip the install step (the `bin/zunit` file already exists). -``` +```text Installing zunit into bin/ ... Done. ==> tests/annexes.zunit @@ -102,7 +104,7 @@ $ make shell user@zi-docker ~ $ zi light junegunn/fzf ... user@zi-docker ~ $ which fzf -~/.zi/polaris/bin/fzf +/data/polaris/bin/fzf user@zi-docker ~ $ exit ``` @@ -135,11 +137,11 @@ make shell IMAGE=my-zd TAG=dev ## Variable reference -| Variable | Default | Purpose | -|---|---|---| -| `ZI_BIN` | `~/.zi/bin` | Path to the Zi binary directory (native tests) | -| `ZI_DATA` | `/tmp/zunit-local` | Data directory for plugins/snippets during tests | -| `IMAGE` | `ghcr.io/z-shell/zd` | Docker image name | -| `TAG` | `latest` | Docker image tag | -| `FILE` | _(all suites)_ | Single `.zunit` suite name to run (without extension) | -| `ZSH_VERSION` | _(empty)_ | Zsh version to bake into a local Docker build | +| Variable | Default | Purpose | +| ------------- | ----------------------- | ----------------------------------------------------- | +| `ZI_BIN` | `~/.local/share/zi/bin` | Path to the Zi binary directory (native tests) | +| `ZI_DATA` | `/tmp/zunit-local` | Data directory for plugins/snippets during tests | +| `IMAGE` | `ghcr.io/z-shell/zd` | Docker image name | +| `TAG` | `latest` | Docker image tag | +| `FILE` | _(all suites)_ | Single `.zunit` suite name to run (without extension) | +| `ZSH_VERSION` | _(empty)_ | Zsh version to bake into a local Docker build | diff --git a/docs/superpowers/plans/2026-05-06-zd-refactor.md b/docs/superpowers/plans/2026-05-06-zd-refactor.md deleted file mode 100644 index bc3318e..0000000 --- a/docs/superpowers/plans/2026-05-06-zd-refactor.md +++ /dev/null @@ -1,1457 +0,0 @@ -# zd Container Refactor Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace per-test Docker container spawning with a `zi_test` helper (fresh zsh subprocess per test), and move Docker to a scheduled Zsh-version matrix only. - -**Architecture:** Native ZUnit tests source zi via a `zi_test()` helper that launches `zsh -c` per test — no Docker, no shell-escaping nightmares. Docker is retained for a 6-job weekly matrix (Zsh 5.5.1–5.9) where the image has zi pre-baked at build time. Both tiers share the same `.zunit` files in `tests/`. - -**Tech Stack:** Zsh, ZUnit (zdharma/zunit), Docker/Alpine, GitHub Actions - ---- - -## File Map - -**Create:** -- `tests/helpers.zsh` — `zi_test()` helper + shared env vars -- `tests/setup.zsh` — per-test data dir reset (no sudo) -- `tests/teardown.zsh` — per-test cleanup (no sudo) -- `tests/annexes.zunit` — migrated from `docker/tests/annexes.zunit` -- `tests/ice.zunit` — migrated from `docker/tests/ice.zunit` -- `tests/plugins.zunit` — migrated from `docker/tests/plugins.zunit` -- `tests/snippets.zunit` — migrated from `docker/tests/snippets.zunit` -- `tests/packages.zunit` — migrated from `docker/tests/packages.zunit` -- `scripts/build.sh` — `docker/build.sh` with updated context path -- `scripts/run.sh` — copy of `docker/run.sh` (unchanged) -- `.github/workflows/test-native.yml` — tier-1: native zsh on ubuntu-latest -- `.github/workflows/test-matrix.yml` — tier-2: Docker Zsh version matrix - -**Modify:** -- `docker/Dockerfile` — two-stage; zi + zunit pre-baked; `ZSH_VERSION` used; `VOLUME` after `COPY` -- `docker/entrypoint.sh` — user creation only; drop wget, symlinks, init.zsh -- `docker/zshrc` — source zi directly; drop `prepare_system`/`initiate_system` -- `docker/docker-compose.yml` — context updated to repo root -- `.github/workflows/zunit.yml` — replaced by `test-native.yml` (deleted) - -**Delete (Task 15):** -- `docker/tests/` (entire directory) -- `docker/build.sh`, `docker/run.sh` (moved to `scripts/`) -- `docker/zunit.sh`, `docker/init.zsh` -- `.github/workflows/zunit.yml` - ---- - -## Task 1: Scaffold directories and move scripts - -**Files:** -- Create: `scripts/build.sh` -- Create: `scripts/run.sh` - -- [ ] **Step 1: Create the `tests/` and `scripts/` directories** - -```bash -mkdir -p tests scripts -``` - -- [ ] **Step 2: Write `scripts/build.sh`** - -Differences from `docker/build.sh`: `dockerfile` is now `docker/Dockerfile` (relative to repo root), and the build context is `realpath ..` (repo root, not `docker/`). - -```bash -cat > scripts/build.sh << 'EOF' -#!/usr/bin/env bash -# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=bash sw=2 ts=2 et - -col_error="[31m" -col_info="[32m" -col_rst="[0m" - -say() { - printf '%s\n' "${col_info}${1}${col_rst}" >&2 -} - -err() { - say "${col_error}${1}${col_rst}" >&2 - exit 1 -} - -build() { - command cd -P -- "$(dirname -- "$(command -v -- "$0" || true)")" && pwd -P || exit 9 - - local image_name="${1:-zd}" - local tag="${2:-latest}" - local zsh_version="${3}" - local container_hostname="z-shell" - shift 3 - - local dockerfile="docker/Dockerfile" - - if [[ -n ${zsh_version} ]]; then - tag="zsh${zsh_version}-${tag}" - fi - - say "Building image: ${image_name}" - - local -a args - [[ -n ${NO_CACHE} ]] && args+=(--no-cache "$@") - - if docker build \ - --build-arg "ZUSER=${USER:-$(id -u -n)}" \ - --build-arg "ZHOST=${container_hostname}" \ - --build-arg "PUID=${UID:-$(id -u)}" \ - --build-arg "PGID=${GID:-$(id -g)}" \ - --build-arg "TERM=${TERM:-xterm-256color}" \ - --build-arg "ZSH_VERSION=${zsh_version}" \ - --file "${dockerfile}" \ - --tag "${image_name}:${tag}" \ - "${args[@]}" "$(realpath .. || true)"; then - { - say "To use this image for ZUnit tests run: " - say "export CONTAINER_IMAGE=\"${image_name}\" CONTAINER_TAG=\"${tag}\"" - say "ZUnit run --verbose" - } >&2 - else - err "Container failed to build." - fi -} - -if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then - CONTAINER_IMAGE="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" - BUILD_ZSH_VERSION="${BUILD_ZSH_VERSION-}" - CONTAINER_TAG="${CONTAINER_TAG:-latest}" - NO_CACHE="${NO_CACHE-}" - - while [[ -n $* ]]; do - case "$1" in - --image | -i) - CONTAINER_IMAGE="$2" - shift 2 - ;; - --no-cache | -N) - NO_CACHE=1 - shift - ;; - --zsh-version | -zv | --zv) - BUILD_ZSH_VERSION="${2}" - shift 2 - ;; - *) - break - ;; - esac - done - - build "${CONTAINER_IMAGE}" "${CONTAINER_TAG}" "${BUILD_ZSH_VERSION}" "$@" -fi -EOF -chmod +x scripts/build.sh -``` - -- [ ] **Step 3: Write `scripts/run.sh` (--zunit branch stripped)** - -The `--zunit` branch in `docker/run.sh` mounts `${ROOT_DIR}/zshenv` and `${ROOT_DIR}/zshrc`, which would point to `scripts/zshenv` after the move — files that don't exist. Since tests no longer run through Docker per-test, strip that branch entirely. - -```bash -cat > scripts/run.sh << 'EOF' -#!/usr/bin/env bash -# -*- mode: bash; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=bash sw=2 ts=2 et - -col_error="[31m" -col_info="[32m" -col_rst="[0m" - -say() { - printf '%s\n' "${col_info}${1}${col_rst}" >&2 -} - -err() { - say "${col_error}${1}${col_rst}" >&2 - exit 1 -} - -parent_process() { - local ppid pcmd - ppid="$(ps -o ppid= -p "$$" | awk '{ print $1 }' || true)" - - if [[ -z ${ppid} ]]; then - say "Failed to determine parent process" - return 1 - fi - - if pcmd="$(ps -o cmd= -p "${ppid}")"; then - say "${pcmd}" - return - fi - - return 1 -} - -running_interactively() { - if [[ -n ${CI} ]]; then - return 1 - fi - - if ! [[ -t 1 ]]; then - parent_process | grep -q zunit || true - fi -} - -create_init_config_file() { - local tempfile - - if [[ -z $* ]]; then - return 1 - fi - - tempfile="$(mktemp)" - printf '%s\n' "$*" >"${tempfile}" - printf '%s\n' "${tempfile}" -} - -run() { - local image="${CONTAINER_IMAGE:-ghcr.io/z-shell/zd}" - local tag="${CONTAINER_TAG:-latest}" - local init_config="$1" - shift - - local -a args=(--rm) - - if running_interactively; then - args+=(--tty=true --interactive=true) - fi - - if [[ -n ${init_config} ]]; then - if [[ -r ${init_config} ]]; then - args+=(--volume "${init_config}:/init.zsh") - else - say "Init config file is not readable" - return 1 - fi - fi - - if [[ -n ${TERM} ]]; then - args+=(--env "TERM=${TERM}") - fi - - if [[ -n ${CONTAINER_ENV[*]} ]]; then - local e - for e in "${CONTAINER_ENV[@]}"; do - args+=(--env "${e}") - done - fi - - if [[ -n ${CONTAINER_VOLUMES[*]} ]]; then - local vol - for vol in "${CONTAINER_VOLUMES[@]}"; do - args+=(--volume "${vol}") - done - fi - - local -a cmd=("$@") - - if [[ -n ${WRAP_CMD} ]]; then - local zsh_opts="ilsc" - [[ -n ${ZSH_DEBUG} ]] && zsh_opts="x${zsh_opts}" - cmd=(zsh "-${zsh_opts}" "${cmd[*]}") - fi - - if [[ -n ${DEBUG} ]]; then - { - say "\$ docker run ${args[*]} ${image}:${tag} ${cmd[*]@Q}" - } >&2 - fi - - docker run "${args[@]}" "${image}:${tag}" "${cmd[@]}" -} - -if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then - CONTAINER_IMAGE=${CONTAINER_IMAGE:-ghcr.io/z-shell/zd} - CONTAINER_TAG="${CONTAINER_TAG:-latest}" - CONTAINER_ENV=() - CONTAINER_VOLUMES=() - DEBUG="${DEBUG-}" - ZSH_DEBUG="${ZSH_DEBUG-}" - INIT_CONFIG_VAL="${INIT_CONFIG_VAL-}" - WRAP_CMD="${WRAP_CMD-}" - - while [[ -n $* ]]; do - case "$1" in - --xsel | -b) - INIT_CONFIG_VAL="$(xsel -b)" - shift - ;; - -c | --config | --init-config | --init) - INIT_CONFIG_VAL="$2" - shift 2 - ;; - -f | --config-file | --init-config-file | --file) - if ! [[ -r $2 ]]; then - say "Unable to read from file: $2" - exit 2 - fi - INIT_CONFIG_VAL="$(cat "$2")" - shift 2 - ;; - -d | --debug) - DEBUG=1 - shift - ;; - -D | --dev | --devel) - DEVEL=1 - shift - ;; - -i | --image) - CONTAINER_IMAGE="$2" - shift 2 - ;; - -t | --tag) - CONTAINER_TAG="$2" - shift 2 - ;; - -e | --env | --environment) - CONTAINER_ENV+=("$2") - shift 2 - ;; - -v | --volume) - CONTAINER_VOLUMES+=("$2") - shift 2 - ;; - -w | --wrap) - WRAP_CMD=1 - shift - ;; - --zsh-debug | -x | -Z) - ZSH_DEBUG=1 - shift - ;; - *) - break - ;; - esac - done - - if INIT_CONFIG="$(create_init_config_file "${INIT_CONFIG_VAL}")"; then - trap 'rm -vf $INIT_CONFIG' EXIT INT - fi - CONTAINER_ROOT="$( - cd -P -- "$(dirname "$0")" - pwd -P - )" || exit 9 - if [[ -n ${DEVEL} ]]; then - CONTAINER_VOLUMES+=( - "${CONTAINER_ROOT}:/src" - ) - fi - - run "${INIT_CONFIG}" "$@" -fi -EOF -chmod +x scripts/run.sh -``` - -- [ ] **Step 4: Verify both scripts are executable** - -```bash -ls -la scripts/ -``` - -Expected: `build.sh` and `run.sh` both show `-rwxr-xr-x`. - -- [ ] **Step 5: Commit** - -```bash -git add scripts/ -git commit -m "feat: add scripts/ directory with build.sh and run.sh" -``` - ---- - -## Task 2: Write tests/helpers.zsh - -**Files:** -- Create: `tests/helpers.zsh` - -- [ ] **Step 1: Write `tests/helpers.zsh`** - -```bash -cat > tests/helpers.zsh << 'EOF' -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -# Run a zi snippet in a fresh isolated zsh subprocess. -# $1 — zsh code to execute (unescaped; single-quote at call site to prevent -# expansion before the function receives it). -# -# Variable interpolation note: ${_zi_bin} and ${_zi_data} are expanded by the -# outer shell when the inner command string is assembled. References to $VAR -# inside the script argument resolve in the *inner* shell after zi is sourced. -# To pass an outer variable's value into the script, let it expand in the -# caller: zi_test "zi light ${my_plugin}" -zi_test() { - local script=$1 - local _zi_bin="${ZI_BIN:-${HOME}/.zi/bin}" - local _zi_data="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" - run zsh -c " - typeset -gxU path - path=( \${HOME}/go/bin \$path ) - typeset -gA ZI - ZI[HOME_DIR]=${_zi_data} - source ${_zi_bin}/zi.zsh - autoload -Uz _zi - ${script} - " -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/helpers.zsh -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/helpers.zsh -git commit -m "feat: add tests/helpers.zsh with zi_test helper" -``` - ---- - -## Task 3: Migrate setup.zsh and teardown.zsh - -**Files:** -- Create: `tests/setup.zsh` -- Create: `tests/teardown.zsh` - -Key changes from `docker/tests/`: -- `DATA_DIR` → `ZI_DATA` (matches the env var `zi_test` uses) -- `PLUGINS_DIR`, `SNIPPETS_DIR`, `ZPFX` dropped — tests now use `${ZI_DATA}/plugins`, `${ZI_DATA}/snippets`, `${ZI_DATA}/polaris` inline -- `sudo rm -rf` → `rm -rf` (native runner owns the temp dir) - -- [ ] **Step 1: Write `tests/setup.zsh`** - -```bash -cat > tests/setup.zsh << 'EOF' -#!/usr/bin/env zunit -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -setup() { - export ZI_DATA="${TMPDIR:-/tmp}/zunit" - - { - color magenta @setup started - color magenta "ZI_DATA=${ZI_DATA}" - } >&2 - - # Wipe plugin/snippet state between tests; keep the dir itself. - rm -rf "${ZI_DATA:?}"/* - mkdir -p "${ZI_DATA}" -} - -# vim: set ft=zsh et ts=2 sw=2 : -EOF -``` - -- [ ] **Step 2: Write `tests/teardown.zsh`** - -```bash -cat > tests/teardown.zsh << 'EOF' -#!/usr/bin/env zunit -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -teardown() { - color cyan @teardown called >&2 - [[ -n "${ZI_DATA}" ]] && rm -rf "${ZI_DATA:?}"/* -} - -# vim: set ft=zsh et ts=2 sw=2 : -EOF -``` - -- [ ] **Step 3: Verify syntax** - -```bash -zsh -n tests/setup.zsh tests/teardown.zsh -``` - -Expected: no output, exit code 0. - -- [ ] **Step 4: Commit** - -```bash -git add tests/setup.zsh tests/teardown.zsh -git commit -m "feat: add tests/setup.zsh and teardown.zsh (no-sudo, ZI_DATA based)" -``` - ---- - -## Task 4: Migrate annexes.zunit - -**Files:** -- Create: `tests/annexes.zunit` - -Changes: `run ./docker/run.sh --wrap --debug --zunit ` → `zi_test ''`; `${PLUGINS_DIR}` → `${ZI_DATA}/plugins`; add `load helpers`. - -Note: `z-a-eval` and `z-a-default-ice` tests assert `$state equals 1` — these are known expected failures (the annexes exit non-zero on load). Keep those assertions as-is. - -- [ ] **Step 1: Write `tests/annexes.zunit`** - -```bash -cat > tests/annexes.zunit << 'EOF' -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - load helpers - setup -} - -@teardown { - load teardown - teardown -} - -@test 'z-a-bin-gem-node installation' { - zi_test 'zi light z-shell/z-a-bin-gem-node' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-meta-plugins installation' { - zi_test 'zi light z-shell/z-a-meta-plugins' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-meta-plugins/z-a-meta-plugins.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-readurl installation' { - zi_test 'zi light z-shell/z-a-readurl' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-readurl/z-a-readurl.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-rust installation' { - zi_test 'zi light z-shell/z-a-rust' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-rust/z-a-rust.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-eval installation' { - zi_test 'zi light z-shell/z-a-eval' - - assert $state equals 1 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-eval/z-a-eval.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-linkbin installation' { - zi_test 'zi light z-shell/z-a-linkbin' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-linkbin/z-a-linkbin.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-default-ice installation' { - zi_test 'zi light z-shell/z-a-default-ice' - - assert $state equals 1 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-default-ice/z-a-default-ice.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'z-a-test installation' { - zi_test 'zi light z-shell/z-a-test' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Compiling" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-test/z-a-test.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/annexes.zunit -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/annexes.zunit -git commit -m "feat: migrate annexes.zunit to zi_test helper" -``` - ---- - -## Task 5: Migrate ice.zunit - -**Files:** -- Create: `tests/ice.zunit` - -Changes: multi-line `$'...'` escaped strings → clean zsh in single-quoted `zi_test` argument; `${PLUGINS_DIR}` → `${ZI_DATA}/plugins`; `${ZPFX}` → `${ZI_DATA}/polaris`. - -- [ ] **Step 1: Write `tests/ice.zunit`** - -```bash -cat > tests/ice.zunit << 'EOF' -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - load helpers - setup -} - -@teardown { - load teardown - teardown -} - -@test 'sbin ice' { - zi_test ' - zi light z-shell/z-a-bin-gem-node - zi light-mode as"null" from"gh-r" sbin"fzf" for junegunn/fzf - ' - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${ZI_DATA}/plugins/z-shell---z-a-bin-gem-node/z-a-bin-gem-node.plugin.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable - - artifact="${ZI_DATA}/polaris/bin/fzf" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'failing atclone ice' { - zi_test 'zi null atclone"echo intentional failure; return 255" for z-shell/null' - - assert $state not_equal_to 0 - assert $state equals 255 - assert "$output" contains "intentional failure" -} - -@test 'failing atpull ice' { - zi_test ' - zi id-as"atpull-fail" null \ - atpull"echo intentional failure; return 255" run-atpull \ - for z-shell/null - zi update atpull-fail - ' - - assert $state equals 255 - assert "$output" contains "intentional failure" -} - -@test 'failing mv ice' { - zi_test ' - zi as"command" from"gh-r" bpick"*musl*" mv"DOES_NOT_EXIST* -> fd" pick"fd/fd" \ - for @sharkdp/fd - ' - - assert $state equals 1 - assert "$output" contains "DOES_NOT_EXIST" - assert "$output" contains "didn'\''t match any file" -} - -@test 'mv ice' { - zi_test ' - zi as"command" from"gh-r" bpick"*musl*" mv"fd* -> fd" pick"fd/fd" \ - for @sharkdp/fd - ' - - assert $state equals 0 - - local artifact="${ZI_DATA}/plugins/sharkdp---fd/fd/fd" - assert "$artifact" is_file - assert "$artifact" is_readable - assert "$artifact" is_executable -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/ice.zunit -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/ice.zunit -git commit -m "feat: migrate ice.zunit to zi_test helper" -``` - ---- - -## Task 6: Migrate plugins.zunit - -**Files:** -- Create: `tests/plugins.zunit` - -- [ ] **Step 1: Write `tests/plugins.zunit`** - -```bash -cat > tests/plugins.zunit << 'EOF' -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - load helpers - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi fzf installation' { - zi_test 'zi lucid as"program" from"gh-r" for junegunn/fzf' - - assert $state equals 0 - assert "$output" contains "Unpacking" - assert "$output" contains "Successfully" - - local artifact="${ZI_DATA}/plugins/junegunn---fzf/fzf" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'zi direnv installation' { - zi_test ' - zi light-mode as"program" \ - atclone"go install github.com/cpuguy83/go-md2man/v2@latest" \ - make for @direnv/direnv - ' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "go: downloading github.com" - - local artifact="${ZI_DATA}/plugins/direnv---direnv/direnv" - assert "$artifact" is_file - assert "$artifact" is_executable -} - -@test 'zi diff-so-fancy installation' { - zi_test ' - zi light-mode for \ - as"program" pick"bin/git-dsf" \ - z-shell/zsh-diff-so-fancy - ' - - assert $state equals 0 - assert "$output" contains "Downloading" - assert "$output" contains "Cloning into" - - local artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/git-dsf" - assert "$artifact" is_file - assert "$artifact" is_executable - - artifact="${ZI_DATA}/plugins/z-shell---zsh-diff-so-fancy/bin/diff-so-fancy" - assert "$artifact" is_file - assert "$artifact" is_executable -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/plugins.zunit -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/plugins.zunit -git commit -m "feat: migrate plugins.zunit to zi_test helper" -``` - ---- - -## Task 7: Migrate snippets.zunit - -**Files:** -- Create: `tests/snippets.zunit` - -Changes: `${SNIPPETS_DIR}` → `${ZI_DATA}/snippets`. - -- [ ] **Step 1: Write `tests/snippets.zunit`** - -```bash -cat > tests/snippets.zunit << 'EOF' -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - load helpers - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi OMZL::spectrum.zsh installation' { - zi_test 'zi snippet OMZL::spectrum.zsh' - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${ZI_DATA}/snippets/OMZL::spectrum.zsh/OMZL::spectrum.zsh" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'zi OMZP::git installation' { - zi_test 'zi snippet OMZP::git' - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${ZI_DATA}/snippets/OMZP::git/OMZP::git" - assert "$artifact" is_file - assert "$artifact" is_readable -} - -@test 'zi PZTM::environment installation' { - zi_test 'zi snippet PZTM::environment' - - assert $state equals 0 - assert "$output" contains "Downloading" - - local artifact="${ZI_DATA}/snippets/PZTM::environment/PZTM::environment" - assert "$artifact" is_file - assert "$artifact" is_readable -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/snippets.zunit -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/snippets.zunit -git commit -m "feat: migrate snippets.zunit to zi_test helper" -``` - ---- - -## Task 8: Migrate packages.zunit - -**Files:** -- Create: `tests/packages.zunit` - -- [ ] **Step 1: Write `tests/packages.zunit`** - -```bash -cat > tests/packages.zunit << 'EOF' -#!/usr/bin/env zunit -# -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et -# - -@setup { - load setup - load helpers - setup -} - -@teardown { - load teardown - teardown -} - -@test 'zi package ls_colors' { - zi_test 'zi pack for ls_colors' - - assert $state equals 0 - assert "$output" contains "Package" - - local artifact="${ZI_DATA}/plugins/ls_colors/LS_COLORS" - assert "$artifact" is_file - assert "$artifact" is_readable -} -EOF -``` - -- [ ] **Step 2: Verify syntax** - -```bash -zsh -n tests/packages.zunit -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add tests/packages.zunit -git commit -m "feat: migrate packages.zunit to zi_test helper" -``` - ---- - -## Task 9: Refactor docker/entrypoint.sh - -**Files:** -- Modify: `docker/entrypoint.sh` - -Strip to user creation, sudo setup, and directory creation only. Remove: `wget install.sh`, symlinks to `/src/zshenv` and `/src/zshrc`, `init.zsh` sourcing. - -- [ ] **Step 1: Overwrite `docker/entrypoint.sh`** - -```bash -cat > docker/entrypoint.sh << 'EOF' -#!/usr/bin/env sh - -HOME="/home/${ZUSER}" -export HOME - -command sed -i -r 's#^(root:.+):/bin/ash#\1:/bin/zsh#' /etc/passwd -command adduser -D -s /bin/zsh -u "${PUID}" -h "${HOME}" "${ZUSER}" - -command printf '%s' "${ZUSER} ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user -command mkdir -p /src /data -command chown -R "${PUID}:${PGID}" /src /data -EOF -``` - -- [ ] **Step 2: Verify the file is syntactically valid sh** - -```bash -sh -n docker/entrypoint.sh -``` - -Expected: no output, exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add docker/entrypoint.sh -git commit -m "refactor: entrypoint.sh — user creation only, drop runtime downloads" -``` - ---- - -## Task 10: Refactor docker/Dockerfile - -**Files:** -- Modify: `docker/Dockerfile` - -Key changes: -- Add `go` to `apk add` (needed for `go install` in direnv test and for `zunit` build) -- Install `zunit`, `revolver`, and `color` into `/usr/local/bin` at build time -- Install zi as `$ZUSER` at build time (not via `install.sh` at runtime) -- Use `ARG ZSH_VERSION` to optionally install a specific Zsh version via `zi pack` at build time -- Move `VOLUME` after all `COPY` instructions -- Remove Go from-upstream download (use `apk add go` instead) - -- [ ] **Step 1: Overwrite `docker/Dockerfile`** - -```bash -cat > docker/Dockerfile << 'EOF' -ARG ALPINE_VERSION=edge -FROM alpine:${ALPINE_VERSION} AS base - -LABEL maintainer="Z-Shell Community" -LABEL email="team@zshell.dev" - -ARG TERM=xterm -ENV TERM=${TERM} - -RUN set -ex && apk --no-cache add \ - alpine-zsh-config \ - ncurses-dev \ - build-base \ - coreutils \ - pcre-dev \ - zlib-dev \ - autoconf \ - libuser \ - rsync \ - bash \ - curl \ - sudo \ - go \ - zsh \ - git \ - vim \ - jq - -# Install zunit and its helpers into /usr/local/bin at build time. -# go is required to compile zunit from source. -RUN set -ex \ - && git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git \ - && cd /tmp/zunit.git && ./build.zsh \ - && mv /tmp/zunit.git/zunit /usr/local/bin/zunit \ - && curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ - > /usr/local/bin/revolver \ - && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ - > /usr/local/bin/color \ - && chmod u+x /usr/local/bin/{color,revolver,zunit} \ - && rm -rf /tmp/zunit.git - -FROM base AS test - -ARG ZUSER=user -ARG PUID=1000 -ARG PGID=1000 -ARG ZHOST=zi-docker - -ENV PUID=${PUID} -ENV PGID=${PGID} -ENV ZUSER=${ZUSER} -ENV HOST=${ZHOST} - -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh && /entrypoint.sh - -# Install zi as $ZUSER at build time — no network calls at test time. -USER ${ZUSER} -ARG ZI_BRANCH=main -RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip - -# Optionally install a specific Zsh version via zi pack at build time. -# Leave ZSH_VERSION empty for the :latest image (uses Alpine's zsh). -ARG ZSH_VERSION= -RUN [ -z "${ZSH_VERSION}" ] || \ - zsh -c "source \${HOME}/.zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" - -# Switch back to root for COPY operations. -USER root -COPY docker/zshenv /home/${ZUSER}/.zshenv -COPY docker/zshrc /home/${ZUSER}/.zshrc -COPY utils.zsh /src/utils.zsh -COPY tests/ /src/tests/ -RUN chown -R ${PUID}:${PGID} /home/${ZUSER}/.zshenv /home/${ZUSER}/.zshrc /src - -# VOLUME declared after all COPYs — declaring before COPY silently discards copied files. -VOLUME ["/data"] - -USER ${ZUSER} -WORKDIR /home/${ZUSER} - -CMD ["zsh", "-il"] -EOF -``` - -- [ ] **Step 2: Verify the Dockerfile passes hadolint (if available locally)** - -```bash -hadolint docker/Dockerfile || echo "hadolint not installed — skip" -``` - -- [ ] **Step 3: Commit** - -```bash -git add docker/Dockerfile -git commit -m "refactor: Dockerfile — two-stage, zi pre-baked, ZSH_VERSION via zi pack, VOLUME after COPY" -``` - ---- - -## Task 11: Update docker/zshrc and docker/zshenv - -**Files:** -- Modify: `docker/zshrc` -- Modify: `docker/zshenv` - -Remove `prepare_system; initiate_system` from `zshrc` — these relied on `/static/` which no longer exists in the image. Source zi directly. Keep `utils.zsh` sourced for interactive convenience functions. - -- [ ] **Step 1: Overwrite `docker/zshrc`** - -```bash -cat > docker/zshrc << 'EOF' -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -# Source zi (pre-installed during docker build). -typeset -gA ZI -ZI[HOME_DIR]="${ZI_DATA:-/data}" -source "${HOME}/.zi/bin/zi.zsh" -autoload -Uz _zi -(( ${+_comps} )) && _comps[zi]=_zi - -# Load interactive convenience wrappers. -[[ -f /src/utils.zsh ]] && source /src/utils.zsh -EOF -``` - -- [ ] **Step 2: Overwrite `docker/zshenv`** - -```bash -cat > docker/zshenv << 'EOF' -# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- -# vim: ft=zsh sw=2 ts=2 et - -export TERM=${TERM:-xterm-256color} -export SHELL=${SHELL:-${commands[zsh]}} -export ZI_DATA=${ZI_DATA:-/data} -EOF -``` - -- [ ] **Step 3: Verify syntax** - -```bash -zsh -n docker/zshrc docker/zshenv -``` - -Expected: no output, exit code 0. - -- [ ] **Step 4: Commit** - -```bash -git add docker/zshrc docker/zshenv -git commit -m "refactor: zshrc sources zi directly; drop prepare_system/initiate_system" -``` - ---- - -## Task 12: Update docker/docker-compose.yml - -**Files:** -- Modify: `docker/docker-compose.yml` - -The build context must be the repo root (parent of `docker/`) so that `COPY tests/` and `COPY utils.zsh` resolve correctly in the Dockerfile. - -- [ ] **Step 1: Overwrite `docker/docker-compose.yml`** - -```bash -cat > docker/docker-compose.yml << 'EOF' -version: "3.9" - -services: - zd: - build: - context: .. - dockerfile: docker/Dockerfile - stdin_open: true - tty: true - container_name: zd - environment: - - TERM=xterm-256color - volumes: - - $PWD/..:/src - hostname: zi@docker -EOF -``` - -- [ ] **Step 2: Verify the file is valid YAML** - -```bash -python3 -c "import yaml, sys; yaml.safe_load(open('docker/docker-compose.yml'))" && echo "OK" -``` - -Expected: `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add docker/docker-compose.yml -git commit -m "fix: docker-compose context updated to repo root for COPY tests/ to work" -``` - ---- - -## Task 13: Write .github/workflows/test-native.yml - -**Files:** -- Create: `.github/workflows/test-native.yml` - -This replaces the functionality of `zunit.yml` for day-to-day CI. Matrix is one job per `.zunit` file. Triggers on push/PR to `main` and weekly schedule. - -- [ ] **Step 1: Write `.github/workflows/test-native.yml`** - -```bash -cat > .github/workflows/test-native.yml << 'EOF' -name: "ZUnit (native)" - -on: - push: - branches: [main] - paths: - - "tests/**" - - "utils.zsh" - pull_request: - branches: [main] - schedule: - - cron: "0 12 * * 1" - workflow_dispatch: - -jobs: - zunit: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - file: [annexes, ice, packages, plugins, snippets] - steps: - - uses: actions/checkout@v4 - - - name: Install zsh - run: sudo apt-get update && sudo apt-get install -yq zsh - - - name: Install zunit - run: | - mkdir -p bin - curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' > bin/revolver - curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' > bin/color - git clone --depth 1 https://github.com/zdharma/zunit.git zunit.git - cd zunit.git && ./build.zsh && cd .. - mv zunit.git/zunit bin/ - chmod u+x bin/{color,revolver,zunit} - - - name: Install zi - run: zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip - - - name: "ZUnit: ${{ matrix.file }}" - run: | - export PATH="$PWD/bin:$PATH" - export TERM=xterm - export ZI_BIN="${HOME}/.zi/bin" - export ZI_DATA="${RUNNER_TEMP}/zunit" - zunit --tap --verbose "tests/${{ matrix.file }}.zunit" -EOF -``` - -- [ ] **Step 2: Verify the file is valid YAML** - -```bash -python3 -c "import yaml, sys; yaml.safe_load(open('.github/workflows/test-native.yml'))" && echo "OK" -``` - -Expected: `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add .github/workflows/test-native.yml -git commit -m "feat: add test-native.yml — native ZUnit CI without Docker" -``` - ---- - -## Task 14: Write .github/workflows/test-matrix.yml - -**Files:** -- Create: `.github/workflows/test-matrix.yml` - -6 jobs, one per Zsh version. Each builds its Docker image once (with `ZSH_VERSION` baked in) then runs all test files in a single container invocation. Runs on schedule and `workflow_dispatch` only — not on every push. - -- [ ] **Step 1: Write `.github/workflows/test-matrix.yml`** - -```bash -cat > .github/workflows/test-matrix.yml << 'EOF' -name: "ZUnit (Zsh matrix)" - -on: - schedule: - - cron: "0 3 * * 3" - workflow_dispatch: - -jobs: - zunit-matrix: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - zsh_version: ["5.5.1", "5.6.2", "5.7.1", "5.8", "5.8.1", "5.9"] - steps: - - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: "Build image for Zsh ${{ matrix.zsh_version }}" - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile - load: true - build-args: ZSH_VERSION=${{ matrix.zsh_version }} - tags: zd:${{ matrix.zsh_version }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: "Run all tests in Zsh ${{ matrix.zsh_version }} container" - run: | - mkdir -p "${RUNNER_TEMP}/zunit" - docker run --rm \ - --env TERM=xterm \ - --env ZI_DATA=/data \ - --volume "${RUNNER_TEMP}/zunit:/data" \ - "zd:${{ matrix.zsh_version }}" \ - zsh -c 'for f in /src/tests/*.zunit; do zunit --tap --verbose "$f" || exit $?; done' -EOF -``` - -- [ ] **Step 2: Verify the file is valid YAML** - -```bash -python3 -c "import yaml, sys; yaml.safe_load(open('.github/workflows/test-matrix.yml'))" && echo "OK" -``` - -Expected: `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add .github/workflows/test-matrix.yml -git commit -m "feat: add test-matrix.yml — Docker Zsh version matrix (scheduled)" -``` - ---- - -## Task 15: Clean up old files - -**Files:** -- Delete: `docker/tests/` (entire directory) -- Delete: `docker/build.sh`, `docker/run.sh` (moved to `scripts/`) -- Delete: `docker/zunit.sh`, `docker/init.zsh` -- Delete: `.github/workflows/zunit.yml` (superseded by `test-native.yml`) - -- [ ] **Step 1: Remove old test directory and superseded scripts** - -```bash -git rm -r docker/tests/ -git rm docker/build.sh docker/run.sh docker/zunit.sh docker/init.zsh -``` - -- [ ] **Step 2: Remove old zunit workflow** - -```bash -git rm .github/workflows/zunit.yml -``` - -- [ ] **Step 3: Verify nothing in the repo still references the deleted paths** - -```bash -grep -r 'docker/tests' . --include='*.yml' --include='*.sh' --include='*.zsh' --include='*.md' \ - --exclude-dir=.git --exclude-dir=docs 2>/dev/null || echo "No references found" - -grep -r 'docker/build\.sh\|docker/run\.sh\|docker/zunit\.sh\|docker/init\.zsh' . \ - --include='*.yml' --include='*.sh' --include='*.zsh' --include='*.md' \ - --exclude-dir=.git --exclude-dir=docs 2>/dev/null || echo "No references found" -``` - -Expected: `No references found` for both. - -- [ ] **Step 4: Commit** - -```bash -git commit -m "chore: remove docker/tests/, old scripts, and superseded zunit.yml workflow" -``` - ---- - -## Local Test Verification (reference) - -After Task 8, run the full native test suite locally (requires zi and zunit installed): - -```bash -# Install zunit if not present -mkdir -p bin -git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git -cd /tmp/zunit.git && ./build.zsh && cp zunit ~/bin/ && cd - - -# Run all test files -export PATH="$HOME/bin:$PATH" -export ZI_BIN="${HOME}/.zi/bin" -export ZI_DATA="/tmp/zunit-local" -for f in tests/*.zunit; do - echo "=== $f ===" - zunit --verbose "$f" -done -``` - -To run a single file during development: - -```bash -ZI_BIN="${HOME}/.zi/bin" ZI_DATA="/tmp/zunit-local" zunit --verbose tests/ice.zunit -``` diff --git a/docs/superpowers/specs/2026-05-06-zd-refactor-design.md b/docs/superpowers/specs/2026-05-06-zd-refactor-design.md deleted file mode 100644 index 4f7957a..0000000 --- a/docs/superpowers/specs/2026-05-06-zd-refactor-design.md +++ /dev/null @@ -1,233 +0,0 @@ -# zd Container Refactor Design - -**Date:** 2026-05-06 -**Status:** Approved - -## Problem - -The `zd` container is the test harness for the zi plugin manager. It has two core problems: - -1. **Flaky CI** — each ZUnit test spawns a fresh Docker container and runs live network downloads (GitHub releases, git clones). Any transient network failure fails the test. -2. **Hard to author tests** — zi commands are passed as shell-escaped strings to `run.sh --wrap`, creating an escaping nightmare. There is no fast local iteration path; every tweak requires a full container round-trip. - -## Approach: Native CI tier + Docker only for Zsh version matrix - -Tests run natively on GitHub runners for regular CI. Docker is reserved for the Zsh version compatibility matrix (5.5.1–5.9), run on a weekly schedule. - -## Repository Structure - -``` -zd/ - tests/ # all .zunit files — shared between native and Docker tiers - helpers.zsh # zi_test() helper and shared utilities - setup.zsh # per-test data dir reset - teardown.zsh # per-test cleanup - annexes.zunit - ice.zunit - packages.zunit - plugins.zunit - snippets.zunit - docker/ - Dockerfile # two-stage build; zi pre-installed during build - entrypoint.sh # user creation only — no runtime downloads - zshenv - zshrc - scripts/ - build.sh # image build helper - run.sh # interactive container launcher (dev use) - utils.zsh # zi wrapper functions (prepare_system, initiate_system, etc.) - .github/ - workflows/ - test-native.yml # tier 1: native zsh on ubuntu-latest - test-matrix.yml # tier 2: Docker, Zsh version matrix - docker.yml # image publish (unchanged) -``` - -Key moves from current layout: -- `docker/tests/` → `tests/` (tests are no longer Docker-specific) -- `docker/build.sh`, `docker/run.sh` → `scripts/` -- Docker build context contains only what `docker build` needs - -## Test Authoring Model - -### The problem with the current model - -Tests pass zi commands as shell-escaped strings to `run.sh --wrap`, which runs `zsh -ilsc ""` inside a container. Example of current escaping: - -```zsh -local z=$'zi id-as'\''atpull-fail'\'' null \ -atpull'\''echo "intentional failure"; return 255'\'' run-atpull \ -for z-shell/null; zi update atpull-fail' -run ./docker/run.sh --wrap --debug --zunit $z -``` - -### The fix: `zi_test` helper - -`tests/helpers.zsh` provides a `zi_test` function that runs a fresh isolated zsh process per test. Commands are written as normal zsh in the test file — no escaping: - -```zsh -# tests/helpers.zsh -ZI_BIN="${ZI_BIN:-${HOME}/.zi/bin}" -ZI_DATA="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" - -zi_test() { - local script=$1 - run zsh -lc " - typeset -gA ZI - ZI[HOME_DIR]=${ZI_DATA} - source ${ZI_BIN}/zi.zsh - autoload -Uz _zi - ${script} - " -} -``` - -Same test rewritten: - -```zsh -@test 'failing atpull ice' { - zi_test ' - zi id-as"atpull-fail" null \ - atpull"echo intentional failure; return 255" run-atpull \ - for z-shell/null - zi update atpull-fail - ' - assert $state equals 255 - assert "$output" contains "intentional failure" -} -``` - -Each test gets a fresh isolated zsh process. No shared state between tests. `zi_test` works identically in both the native tier and inside the Docker container — only `ZI_BIN` and `ZI_DATA` env vars differ. - -> **Note on variable interpolation:** `${script}` is interpolated into the outer zsh string before the inner zsh runs, so `$VAR` references in the script body resolve in the inner shell's environment (after sourcing zi). To pass a value from the test's environment into the script, expand it explicitly: `zi_test "zi light ${some_var}"`. - -File-level assertions reference `ZI_DATA` directly: - -```zsh -assert "${ZI_DATA}/plugins/junegunn---fzf/fzf" is_executable -``` - -## Native CI Tier - -**Workflow:** `.github/workflows/test-native.yml` - -- Trigger: push to `main` (paths: `tests/**`), pull requests, weekly schedule, `workflow_dispatch` -- Runner: `ubuntu-latest` -- Matrix: one job per `.zunit` file (parallel, `fail-fast: false`) - -**Setup steps:** -1. Install `zsh` via apt -2. Install `zunit`, `revolver`, `color` into `bin/` -3. Install zi via `zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip` -4. Cache `~/.zi/bin` keyed to zi's commit SHA — network hit only when zi changes - -**Run:** -```sh -export PATH="$PWD/bin:$PATH" -export ZI_BIN="${HOME}/.zi/bin" -export ZI_DATA="${RUNNER_TEMP}/zunit" -zunit --tap --verbose "tests/${{ matrix.file }}.zunit" -``` - -This is 10–20× faster than the current per-test container spawn and removes all network flakiness from non-zi sources. - -## Docker Tier - -### Dockerfile (two-stage) - -```dockerfile -ARG ALPINE_VERSION=edge - -FROM alpine:${ALPINE_VERSION} AS base - -RUN apk --no-cache add \ - build-base ncurses-dev pcre-dev zlib-dev autoconf \ - bash curl git jq rsync sudo zsh vim - -# Install zi at build time — not at test time -ARG ZI_BRANCH=main -RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip - -# Install the matrix Zsh version via zi pack at build time. -# ZSH_VERSION is empty for the :latest image (uses Alpine's zsh). -ARG ZSH_VERSION= -RUN if [ -n "${ZSH_VERSION}" ]; then \ - zsh -ilc "zi pack\"${ZSH_VERSION}\" for zsh"; \ - fi - -FROM base AS test -ARG ZUSER=user -ARG PUID=1000 -ARG PGID=1000 - -# entrypoint.sh: creates $ZUSER, sets up sudo, creates /src /data dirs. -# Dropped from current: wget install.sh, symlink zshenv/zshrc, source init.zsh. -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh && /entrypoint.sh - -COPY docker/zshenv /home/${ZUSER}/.zshenv -COPY docker/zshrc /home/${ZUSER}/.zshrc -COPY utils.zsh /src/utils.zsh -COPY tests/ /src/tests/ - -# VOLUME declared after all COPYs — fixes silent copy invalidation bug -VOLUME ["/data"] - -USER ${ZUSER} -WORKDIR /home/${ZUSER} -CMD ["zsh", "-il"] -``` - -Key fixes vs. current: -- `ARG ZSH_VERSION` is now used: non-empty value installs that exact Zsh version via `zi pack` during build, baking it into the image layer -- zi is baked in during `docker build` — no network calls at test time -- `VOLUME` declared after `COPY` (current ordering silently discards the copy) -- `entrypoint.sh` scope reduced to: create user, set up sudo, create `/src` and `/data` dirs — no `wget install.sh`, no symlinks, no `init.zsh` sourcing -- Go removed (not needed for test execution) - -### Matrix Workflow - -**Workflow:** `.github/workflows/test-matrix.yml` - -- Trigger: weekly schedule (`0 3 * * 3`), `workflow_dispatch` only — not on every push -- Matrix: 6 jobs, one per Zsh version (`fail-fast: false`) -- Each job builds its image once, then runs all test files inside that single container -- Buildx layer caching via `type=gha` — repeat runs rebuild only changed layers - -**Run per matrix job:** -```sh -# Build once for this Zsh version -docker build \ - --build-arg ZSH_VERSION=${{ matrix.zsh_version }} \ - --tag zd:${{ matrix.zsh_version }} \ - --cache-from type=gha --cache-to type=gha,mode=max \ - . - -# Run all test files in a single container invocation -docker run --rm \ - -e ZI_DATA=/data \ - -v "${RUNNER_TEMP}/zunit:/data" \ - zd:${{ matrix.zsh_version }} \ - zsh -c "for f in /src/tests/*.zunit; do zunit --tap --verbose \"\$f\"; done" -``` - -This is 6 jobs instead of 30, with one image build per Zsh version instead of five. The native workflow catches regressions on every PR; the matrix workflow verifies Zsh version compatibility on a cadence, not blocking every merge. - -## Migration of Existing Tests - -All five existing `.zunit` files are migrated by: - -1. Replacing `run ./docker/run.sh --wrap --debug --zunit ` with `zi_test ''` -2. Adding `load helpers` to each `@setup` block -3. Replacing hardcoded `${PLUGINS_DIR}` / `${ZPFX}` path assertions with `${ZI_DATA}/plugins/...` and `${ZI_DATA}/polaris/...` -4. Moving files from `docker/tests/` to `tests/` - -No test logic changes — only the invocation wrapper and path references. - -## What Is Not Changed - -- `utils.zsh` functions (`prepare_system`, `initiate_system`, `reload_system`, `zi::*`) — kept as-is for interactive use -- `scripts/run.sh` — kept for interactive `docker run` sessions -- `docker.yml` publish workflow — unchanged -- `.zunit` test assertions and test cases — logic unchanged, only wrapper replaced -- Trunk / linting configuration diff --git a/docs/writing-tests.md b/docs/writing-tests.md index 7b48b3f..10944e3 100644 --- a/docs/writing-tests.md +++ b/docs/writing-tests.md @@ -116,8 +116,8 @@ Note how `owner/name` becomes `owner---name` in the filesystem path — Zi repla `zi_test` receives a string that is embedded into an inner Zsh process. There are two shells involved: the outer ZUnit shell and the inner Zsh started by `zi_test`. -- **Single quotes** — the string is passed literally; `$VAR` references resolve in the *inner* shell (after Zi is sourced). This is the default and is what you want for most tests. -- **Double quotes** — the string is interpolated by the *outer* shell before being passed in. Use this when you want to inject an outer variable's value. +- **Single quotes** — the string is passed literally; `$VAR` references resolve in the _inner_ shell (after Zi is sourced). This is the default and is what you want for most tests. +- **Double quotes** — the string is interpolated by the _outer_ shell before being passed in. Use this when you want to inject an outer variable's value. ```zsh # Correct — expands $my_plugin in the outer (ZUnit) shell @@ -153,16 +153,16 @@ This means tests can be run in any order and do not depend on each other. 1. Create `tests/.zunit` following the anatomy above. 2. Add `` to the matrix in `.github/workflows/test-native.yml`: -```yaml -matrix: - file: [annexes, ice, packages, plugins, snippets, ] -``` + ```yaml + matrix: + file: [annexes, ice, packages, plugins, snippets, ] + ``` 3. Verify locally before pushing: -```sh -make test FILE= -``` + ```sh + make test FILE= + ``` The new suite will be picked up automatically by the Docker matrix workflow (`test-matrix.yml`) — it iterates over all `*.zunit` files, so no change is needed there. @@ -170,14 +170,14 @@ The new suite will be picked up automatically by the Docker matrix workflow (`te ## Common assertion patterns -| Assertion | Meaning | -|---|---| -| `assert $state equals 0` | Command exited successfully | -| `assert $state equals 255` | Command exited with a specific non-zero code | -| `assert $state not_equal_to 0` | Command failed (any non-zero code) | -| `assert "$output" contains "text"` | Output includes the substring | -| `assert "$artifact" is_file` | Path exists and is a regular file | -| `assert "$artifact" is_executable` | Path exists and is executable | -| `assert "$artifact" is_readable` | Path exists and is readable | +| Assertion | Meaning | +| ---------------------------------- | -------------------------------------------- | +| `assert $state equals 0` | Command exited successfully | +| `assert $state equals 255` | Command exited with a specific non-zero code | +| `assert $state not_equal_to 0` | Command failed (any non-zero code) | +| `assert "$output" contains "text"` | Output includes the substring | +| `assert "$artifact" is_file` | Path exists and is a regular file | +| `assert "$artifact" is_executable` | Path exists and is executable | +| `assert "$artifact" is_readable` | Path exists and is readable | Full ZUnit assertion reference: diff --git a/tests/helpers.zsh b/tests/helpers.zsh index 10d4fd5..5db4afa 100644 --- a/tests/helpers.zsh +++ b/tests/helpers.zsh @@ -12,7 +12,7 @@ # caller: zi_test "zi light ${my_plugin}" zi_test() { local script=$1 - local _zi_bin="${ZI_BIN:-${HOME}/.zi/bin}" + local _zi_bin="${ZI_BIN:-${XDG_DATA_HOME:-${HOME}/.local/share}/zi/bin}" local _zi_data="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" run zsh -c " typeset -gxU path From 630b4ae7663004a4b204bcd4ba8d02e6d7988915 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Fri, 15 May 2026 23:53:14 +0100 Subject: [PATCH 24/47] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Sall <59910950+ss-o@users.noreply.github.com> --- .github/workflows/test-native.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index 39e2399..d4316c5 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -1,5 +1,8 @@ name: "ZUnit (native)" +permissions: + contents: read + on: push: branches: [main] From 61e1c7ba560c61087bf536eccc8e20376fd5146d Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Fri, 15 May 2026 23:53:22 +0100 Subject: [PATCH 25/47] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Sall <59910950+ss-o@users.noreply.github.com> --- .github/workflows/test-matrix.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index d532bbe..19b804a 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -5,6 +5,9 @@ on: - cron: "0 3 * * 3" workflow_dispatch: +permissions: + contents: read + jobs: zunit-matrix: runs-on: ubuntu-latest From 26ac2780ceb27ddfbd654d491b0632496c475348 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 00:15:08 +0100 Subject: [PATCH 26/47] fix: correct Docker build context, ARG name, and setup glob failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker.yml: context ./docker → . so COPY tests/ and docker/* resolve from the repo root as the Dockerfile expects - docker.yml: build-arg ZI_ZSH_VERSION → ZSH_VERSION to match ARG in Dockerfile; mismatched name caused ZSH_VERSION to always be empty - Dockerfile: COPY utils.zsh → COPY docker/utils.zsh for root context - tests/setup.zsh: rm -rf "${ZI_DATA}"/* → rm -rf "${ZI_DATA}" to avoid zsh glob-no-match errors when the directory is absent on first run Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker.yml | 6 +++--- docker/Dockerfile | 2 +- tests/setup.zsh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2518547..244e2c4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,8 +55,8 @@ jobs: with: push: ${{ github.event.number == 0 }} file: ./docker/Dockerfile - context: ./docker - build-args: ZI_ZSH_VERSION=${{ matrix.zsh_version }} + context: . + build-args: ZSH_VERSION=${{ matrix.zsh_version }} tags: ghcr.io/${{ github.repository }}:zsh-${{ matrix.zsh_version }} platforms: linux/amd64,linux/arm64 cache-from: type=gha @@ -87,7 +87,7 @@ jobs: with: push: true file: ./docker/Dockerfile - context: ./docker + context: . tags: ghcr.io/${{ github.repository }}:latest platforms: linux/amd64,linux/arm64 cache-from: type=gha diff --git a/docker/Dockerfile b/docker/Dockerfile index 4ced38c..845be77 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -69,7 +69,7 @@ RUN [ -z "${ZSH_VERSION}" ] || \ USER root COPY docker/zshenv /home/${ZUSER}/.zshenv COPY docker/zshrc /home/${ZUSER}/.zshrc -COPY utils.zsh /src/utils.zsh +COPY docker/utils.zsh /src/utils.zsh COPY tests/ /src/tests/ RUN chown -R ${PUID}:${PGID} /home/${ZUSER}/.zshenv /home/${ZUSER}/.zshrc /src diff --git a/tests/setup.zsh b/tests/setup.zsh index eaeacf3..b2194dc 100644 --- a/tests/setup.zsh +++ b/tests/setup.zsh @@ -10,7 +10,7 @@ setup() { color magenta "ZI_DATA=${ZI_DATA}" } >&2 - # Wipe plugin/snippet state between tests; keep the dir itself. - rm -rf "${ZI_DATA:?}"/* + # Wipe and recreate between tests. Direct rm avoids glob-no-match errors. + rm -rf "${ZI_DATA:?}" mkdir -p "${ZI_DATA}" } From 94bc52825672e469b8f932b4d5037e4e60cc7251 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 01:01:52 +0100 Subject: [PATCH 27/47] fix: replace CDN install and zi-pack with direct git clone; pin Alpine versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZUnit (native): - Replace unreliable CDN-based zi install (silent failure on curl error) with direct git clone; default ZI_REPO/ZI_REF embedded via || fallback - Add explicit [[ -f zi.zsh ]] guard so step fails visibly if clone failed Docker: - Use Alpine version matrix (3.9–3.19) mapped to zsh versions instead of ZSH_VERSION build-arg + zi pack — invoking zi in RUN steps is not supported (interactive session function, exits 127 in sh context) - Install zi via git clone in Dockerfile; busybox sh-compatible mkdir (no brace expansion) for cache/completions/plugins/snippets dirs - Remove redundant git clone zi.git zi steps from workflow (unused by build) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker.yml | 18 ++++++++---------- .github/workflows/test-native.yml | 17 +++++++---------- docker/Dockerfile | 15 ++++++++------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 244e2c4..24fb508 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,18 +25,17 @@ jobs: strategy: fail-fast: false matrix: - zsh_version: - - 5.5.1 - - 5.6.2 - - 5.7.1 - - 5.8 - - 5.8.1 - - 5.9 + include: + - { zsh_version: "5.5.1", alpine_version: "3.9" } + - { zsh_version: "5.6.2", alpine_version: "3.10" } + - { zsh_version: "5.7.1", alpine_version: "3.11" } + - { zsh_version: "5.8", alpine_version: "3.14" } + - { zsh_version: "5.8.1", alpine_version: "3.16" } + - { zsh_version: "5.9", alpine_version: "3.19" } steps: - uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - - run: git clone --depth 1 -- https://github.com/z-shell/zi.git zi - uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx id: buildx @@ -56,7 +55,7 @@ jobs: push: ${{ github.event.number == 0 }} file: ./docker/Dockerfile context: . - build-args: ZSH_VERSION=${{ matrix.zsh_version }} + build-args: ALPINE_VERSION=${{ matrix.alpine_version }} tags: ghcr.io/${{ github.repository }}:zsh-${{ matrix.zsh_version }} platforms: linux/amd64,linux/arm64 cache-from: type=gha @@ -68,7 +67,6 @@ jobs: - uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - - run: git clone --depth 1 -- https://github.com/z-shell/zi.git zi - uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx id: buildx diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index d4316c5..cfcfa2d 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -61,17 +61,14 @@ jobs: - name: Install zi env: - ZI_REPO: ${{ inputs.zi_repo }} - ZI_REF: ${{ inputs.zi_ref }} + ZI_REPO: ${{ inputs.zi_repo || 'z-shell/zi' }} + ZI_REF: ${{ inputs.zi_ref || 'main' }} run: | - if [[ -n "$ZI_REPO" ]]; then - zi_home="${XDG_DATA_HOME:-${HOME}/.local/share}/zi" - git clone --depth 1 --branch "${ZI_REF:-main}" \ - "https://github.com/${ZI_REPO}.git" "${zi_home}/bin" - mkdir -p "${zi_home}"/{cache,completions,plugins,snippets} - else - zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip - fi + zi_home="${XDG_DATA_HOME:-${HOME}/.local/share}/zi" + git clone --depth 1 --branch "${ZI_REF}" \ + "https://github.com/${ZI_REPO}.git" "${zi_home}/bin" + mkdir -p "${zi_home}"/{cache,completions,plugins,snippets} + [[ -f "${zi_home}/bin/zi.zsh" ]] || { echo "zi.zsh not found after install"; exit 1; } - name: "ZUnit: ${{ matrix.file }}" run: | diff --git a/docker/Dockerfile b/docker/Dockerfile index 845be77..c3561c2 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -57,13 +57,14 @@ RUN chmod +x /entrypoint.sh && /entrypoint.sh # Install zi as $ZUSER at build time — no network calls at test time. USER ${ZUSER} ARG ZI_BRANCH=main -RUN zsh -c "$(curl -fsSL https://install.zshell.dev)" -- -i skip - -# Optionally install a specific Zsh version via zi pack at build time. -# Leave ZSH_VERSION empty for the :latest image (uses Alpine's zsh). -ARG ZSH_VERSION= -RUN [ -z "${ZSH_VERSION}" ] || \ - zsh -c "source \${XDG_DATA_HOME:-\${HOME}/.local/share}/zi/bin/zi.zsh && zi pack\"${ZSH_VERSION}\" for zsh" +RUN git clone --depth 1 --branch "${ZI_BRANCH}" \ + https://github.com/z-shell/zi.git \ + "${XDG_DATA_HOME:-${HOME}/.local/share}/zi/bin" \ + && mkdir -p \ + "${XDG_DATA_HOME:-${HOME}/.local/share}/zi/cache" \ + "${XDG_DATA_HOME:-${HOME}/.local/share}/zi/completions" \ + "${XDG_DATA_HOME:-${HOME}/.local/share}/zi/plugins" \ + "${XDG_DATA_HOME:-${HOME}/.local/share}/zi/snippets" # Switch back to root for COPY operations. USER root From 885986d65b70d1a56d5f8b5a84c91a698987214a Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 02:06:11 +0100 Subject: [PATCH 28/47] fix: remove packages absent in old Alpine; correct mv ice assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile: - Remove alpine-zsh-config and libuser — not present in Alpine < 3.12; neither is required: busybox adduser handles user creation, zshrc is COPYed from the repo tests/ice.zunit: - Drop assert $state equals 1 from 'failing mv ice' — zi installs the plugin successfully even when mv ice fails (hook failure is non-fatal); assert on the warning message content instead, which is the observable behaviour zi guarantees Co-Authored-By: Claude Sonnet 4.6 --- docker/Dockerfile | 2 -- tests/ice.zunit | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c3561c2..0eb4b56 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,14 +8,12 @@ ARG TERM=xterm ENV TERM=${TERM} RUN set -ex && apk --no-cache add \ - alpine-zsh-config \ ncurses-dev \ build-base \ coreutils \ pcre-dev \ zlib-dev \ autoconf \ - libuser \ rsync \ bash \ curl \ diff --git a/tests/ice.zunit b/tests/ice.zunit index e71231a..fa3fe9c 100644 --- a/tests/ice.zunit +++ b/tests/ice.zunit @@ -59,9 +59,8 @@ for @sharkdp/fd ' - assert $state equals 1 assert "$output" contains "DOES_NOT_EXIST" - assert "$output" contains "didn'\''t match any file" + assert "$output" contains "mv ice didn'\''t match any file" } @test 'mv ice' { From 3d090f106f809ae297e4fc04c00b963b0b088a52 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Sat, 16 May 2026 02:07:55 +0100 Subject: [PATCH 29/47] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sall <59910950+ss-o@users.noreply.github.com> --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index 7725ded..2fec281 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ trim_trailing_whitespace = true indent_style = space indent_size = 2 +[*.{md,mdx,rst}] +trim_trailing_whitespace = false + [Makefile*] indent_style = tab indent_size = 4 From adbacae9d0eeca65ed93a6df0276e8179251ddb6 Mon Sep 17 00:00:00 2001 From: Sall <59910950+ss-o@users.noreply.github.com> Date: Sat, 16 May 2026 02:09:05 +0100 Subject: [PATCH 30/47] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sall <59910950+ss-o@users.noreply.github.com> --- .github/workflows/test-matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 19b804a..89645b7 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -40,4 +40,4 @@ jobs: --env ZI_DATA=/data \ --volume "${RUNNER_TEMP}/zunit:/data" \ "zd:${{ matrix.zsh_version }}" \ - zsh -c 'for f in /src/tests/*.zunit; do zunit --tap --verbose "$f" || exit $?; done' + zsh -c 'cd /src/tests && for f in *.zunit; do zunit --tap --verbose "$f" || exit $?; done' From 9bb7b263be2bd466d773bff2da7b8ba69a6c1c0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 01:09:54 +0000 Subject: [PATCH 31/47] fix: make zunit clone step idempotent and keep clone errors visible Agent-Logs-Url: https://github.com/z-shell/zd/sessions/27d9b13d-f68d-404c-9949-9991426c1034 Co-authored-by: ss-o <59910950+ss-o@users.noreply.github.com> --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8cfe0d6..b98a331 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,8 @@ bin/zunit: > bin/revolver @curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ > bin/color - @git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git 2>/dev/null + @rm -rf /tmp/zunit.git + @git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git @cd /tmp/zunit.git && ./build.zsh @mv /tmp/zunit.git/zunit bin/zunit @chmod u+x bin/color bin/revolver bin/zunit From c8c198c50210062baf4030b0892afc3fd48ca245 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 01:12:33 +0000 Subject: [PATCH 32/47] fix: restore full Zi init in zshrc and correct docs example path Agent-Logs-Url: https://github.com/z-shell/zd/sessions/79f9bd39-5098-4b2d-a7af-12304e49bb9c Co-authored-by: ss-o <59910950+ss-o@users.noreply.github.com> --- docker/zshrc | 12 +++++++++++- docs/local-testing.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docker/zshrc b/docker/zshrc index 54754ee..a9a0c17 100755 --- a/docker/zshrc +++ b/docker/zshrc @@ -1,8 +1,18 @@ # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et -# Source zi (pre-installed during docker build). +# Initialize Zi with full environment setup. +typeset -gxU path module_path typeset -gA ZI + +# Set up paths for polaris binaries and zmodules. +path=("${ZPFX:-${ZI_DATA:-/data}/polaris}/bin" $path) +module_path+=( /data/zmodules/zpmod/Src ) + +# Load zpmod if available. +zmodload zi/zpmod &>/dev/null + +# Source zi (pre-installed during docker build). ZI[HOME_DIR]="${ZI_DATA:-/data}" source "${ZI_BIN}/zi.zsh" autoload -Uz _zi diff --git a/docs/local-testing.md b/docs/local-testing.md index 1d0ba66..ad52d13 100644 --- a/docs/local-testing.md +++ b/docs/local-testing.md @@ -104,7 +104,7 @@ $ make shell user@zi-docker ~ $ zi light junegunn/fzf ... user@zi-docker ~ $ which fzf -/data/polaris/bin/fzf +/tmp/zd-shell/polaris/bin/fzf user@zi-docker ~ $ exit ``` From 855aeac9a3dac48b02d9c6ccf3622a91c997de0c Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 02:21:23 +0100 Subject: [PATCH 33/47] tests: update failing mv ice assertion to match new hook error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The zi mv hook now reports 'Warning: ∞zi-mv-hook hook returned with 1' instead of the old 'mv ice didn'\''t match any file' message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/ice.zunit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ice.zunit b/tests/ice.zunit index fa3fe9c..88e3ac6 100644 --- a/tests/ice.zunit +++ b/tests/ice.zunit @@ -60,7 +60,7 @@ ' assert "$output" contains "DOES_NOT_EXIST" - assert "$output" contains "mv ice didn'\''t match any file" + assert "$output" contains "∞zi-mv-hook hook returned with" } @test 'mv ice' { From 808ff00cff78e209ee0ed59165c795244f9ff46b Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 02:24:51 +0100 Subject: [PATCH 34/47] test: disable ANSI colors in zi_test subprocess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set NO_COLOR=1 and TERM=dumb in the inner zsh so zi does not emit ANSI escape codes. Without this the string assertion assert "$output" contains "∞zi-mv-hook hook returned with" fails because zi wraps the warning in colour codes, splitting the expected substring across escape sequences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/helpers.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers.zsh b/tests/helpers.zsh index 5db4afa..625ae64 100644 --- a/tests/helpers.zsh +++ b/tests/helpers.zsh @@ -14,7 +14,7 @@ zi_test() { local script=$1 local _zi_bin="${ZI_BIN:-${XDG_DATA_HOME:-${HOME}/.local/share}/zi/bin}" local _zi_data="${ZI_DATA:-${TMPDIR:-/tmp}/zunit}" - run zsh -c " + run env NO_COLOR=1 TERM=dumb zsh -c " typeset -gxU path path=( \${HOME}/go/bin \$path ) typeset -gA ZI From 20b4845a761ee070c80de05142653a14d03c62e3 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 02:53:29 +0100 Subject: [PATCH 35/47] feat: migrate Docker base from Alpine to debian:trixie-slim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace alpine:edge/${ALPINE_VERSION} with debian:trixie-slim (Debian 13, stable Aug 2025, EOL 2028 / LTS 2030) for glibc compatibility and full apt package selection - Dockerfile: - ARG DEBIAN_FRONTEND → ENV DEBIAN_FRONTEND=noninteractive for reliability - apk → apt-get install -y --no-install-recommends + rm /var/lib/apt/lists/* - Package mapping: build-base→build-essential, ncurses-dev→libncurses-dev, pcre-dev→libpcre2-dev, zlib-dev→zlib1g-dev, go→golang-go - Added ca-certificates, sudo (not pre-installed on slim images) - ARG ZSH_VERSION: compile from source when set, else apt install zsh - WORKDIR reset to / after base stage cleanup - entrypoint.sh: BusyBox adduser -D → Debian useradd -m; sed /bin/ash → /bin/bash; fix printf missing trailing newline - utils.zsh: apk add → apt-get install -y in zi::setup-annexes+add() - docker-compose.yml: remove deprecated version: '3.9' key - .github/workflows/docker.yml: ALPINE_VERSION matrix → ZSH_VERSION; actions/checkout@v6 → @v4 - .github/workflows/test-matrix.yml: setup-buildx-action@v3 → @v4; build-push-action@v6 → @v7; ZSH_VERSION build-arg now functional - .github/workflows/zsh-n.yml: fix broken paths: [zi/**] → docker/**, tests/**; actions/checkout@v6 → @v4 - .gitignore: remove duplicated second half of file (agent-file entries preserved) - README.md, docs/: update for Debian base image Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/docker.yml | 18 ++--- .github/workflows/test-matrix.yml | 4 +- .github/workflows/zsh-n.yml | 13 +-- .gitignore | 129 ------------------------------ README.md | 2 + docker/Dockerfile | 62 +++++++++----- docker/docker-compose.yml | 2 - docker/entrypoint.sh | 9 ++- docker/utils.zsh | 2 +- docs/ci-workflows.md | 2 +- docs/local-testing.md | 4 +- 11 files changed, 75 insertions(+), 172 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 24fb508..51f53ea 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,14 +26,14 @@ jobs: fail-fast: false matrix: include: - - { zsh_version: "5.5.1", alpine_version: "3.9" } - - { zsh_version: "5.6.2", alpine_version: "3.10" } - - { zsh_version: "5.7.1", alpine_version: "3.11" } - - { zsh_version: "5.8", alpine_version: "3.14" } - - { zsh_version: "5.8.1", alpine_version: "3.16" } - - { zsh_version: "5.9", alpine_version: "3.19" } + - { zsh_version: "5.5.1" } + - { zsh_version: "5.6.2" } + - { zsh_version: "5.7.1" } + - { zsh_version: "5.8" } + - { zsh_version: "5.8.1" } + - { zsh_version: "5.9" } steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - uses: docker/setup-qemu-action@v4 @@ -55,7 +55,7 @@ jobs: push: ${{ github.event.number == 0 }} file: ./docker/Dockerfile context: . - build-args: ALPINE_VERSION=${{ matrix.alpine_version }} + build-args: ZSH_VERSION=${{ matrix.zsh_version }} tags: ghcr.io/${{ github.repository }}:zsh-${{ matrix.zsh_version }} platforms: linux/amd64,linux/arm64 cache-from: type=gha @@ -64,7 +64,7 @@ jobs: build-latest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - uses: docker/setup-qemu-action@v4 diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 89645b7..7c0cff8 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -19,10 +19,10 @@ jobs: - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: "Build image for Zsh ${{ matrix.zsh_version }}" - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index 861891e..bcb33b2 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -3,11 +3,14 @@ name: "✅ Zsh Check" on: push: - tags: ["v*.*.*"] branches: [main, next] - paths: [zi/**] + paths: + - "docker/**" + - "tests/**" pull_request: - paths: [zi/**] + paths: + - "docker/**" + - "tests/**" workflow_dispatch: {} jobs: @@ -17,7 +20,7 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: submodules: recursive - name: "✨ Set matrix output" @@ -35,7 +38,7 @@ jobs: matrix: ${{ fromJSON(needs.zsh-matrix.outputs.matrix) }} steps: - name: ⤵️ Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: submodules: recursive - name: "⚡ Install dependencies" diff --git a/.gitignore b/.gitignore index bb15823..0a2f0f5 100644 --- a/.gitignore +++ b/.gitignore @@ -129,135 +129,6 @@ CVS NEWS.md Untitled-1.sh -TEMP.md -# VSCODE workspace -.vscode/* -.vscode/settings.json -.vscode/tasks.json -.vscode/launch.json -.vscode/extensions.json -*.code-workspace - -# Exclude for security reasons -.history/ -.dccache -.env - -# Zsh compiled script + zrecompile backup -*.zwc -*.zwc.old - -# Zsh completion-optimization dumpfile -*zcompdump* - -# Zsh zcalc history -.zcalc_history - -# A popular plugin manager's files -._zi -._zinit -._zplugin -.zi_lastupd -.zinit_lastupd -.zplugin_lstupd - -# z-shell/zshelldoc tool's files -zsdoc/data -docs/zsdoc/data - -# ohmyzsh/ohmyzsh/plugins/per-directory-history plugin's files -# (when set-up to store the history in the local directory) -.directory_history - -# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files -# (for Zsh plugins using Python) -.venv - -# Zunit tests' output -/tests/_output/* -!/tests/_output/.gitkeep - -### C -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - -# Repository specific files -test/ -txt/ -*.txt -*.zwc -*.zini -*lib/zsh/*zwc -.ycm_extra_conf.py -*deploy*key* -*.bundle -site*/ -other -TODO* -tags -TAGS -*.o -*.o.c -*.orig -*.a -*.so -*.dll -*~ -.*.sw? -\#* - -CVS -.#* AGENTS.md CLAUDE.md GEMINI.md diff --git a/README.md b/README.md index 8842aac..950d45f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ `zd` is the official test harness for the [Zi](https://github.com/z-shell/zi) plugin manager. It provides a ZUnit test suite that verifies Zi commands work correctly — plugin installation, snippet loading, ice modifiers, annexes, and packages. The suite runs natively on CI for every pull request and inside Docker containers for Zsh version compatibility testing. It also works as a reusable workflow so other repos in the Z-Shell ecosystem can test against a specific Zi commit. +The image is based on **Debian trixie-slim** (`debian:trixie-slim`), providing full glibc compatibility and the breadth of the `apt` ecosystem — making it straightforward to add test dependencies or use the container as an interactive development environment. + ## Architecture ```text diff --git a/docker/Dockerfile b/docker/Dockerfile index 0eb4b56..724ec0c 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,28 +1,52 @@ -ARG ALPINE_VERSION=edge -FROM alpine:${ALPINE_VERSION} AS base +FROM debian:trixie-slim AS base LABEL maintainer="Z-Shell Community" LABEL email="team@zshell.dev" ARG TERM=xterm ENV TERM=${TERM} +ENV DEBIAN_FRONTEND=noninteractive -RUN set -ex && apk --no-cache add \ - ncurses-dev \ - build-base \ - coreutils \ - pcre-dev \ - zlib-dev \ - autoconf \ - rsync \ - bash \ - curl \ - sudo \ - go \ - zsh \ - git \ - vim \ - jq +RUN set -ex \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + libncurses-dev \ + build-essential \ + coreutils \ + libpcre2-dev \ + zlib1g-dev \ + autoconf \ + rsync \ + bash \ + curl \ + sudo \ + golang-go \ + git \ + vim \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# When ZSH_VERSION is set, compile that exact release from source. +# Otherwise install the distro-packaged zsh (Debian trixie ships 5.9). +ARG ZSH_VERSION +RUN set -ex \ + && if [ -n "${ZSH_VERSION}" ]; then \ + apt-get update \ + && apt-get install -y --no-install-recommends wget xz-utils make \ + && rm -rf /var/lib/apt/lists/* \ + && wget -q "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" \ + && tar xf "zsh-${ZSH_VERSION}.tar.xz" \ + && cd "zsh-${ZSH_VERSION}" \ + && ./configure --prefix=/usr/local --enable-pcre \ + && make -j"$(nproc)" \ + && make install \ + && cd .. && rm -rf "zsh-${ZSH_VERSION}"* ; \ + else \ + apt-get update \ + && apt-get install -y --no-install-recommends zsh \ + && rm -rf /var/lib/apt/lists/* ; \ + fi # Install zunit and its helpers into /usr/local/bin at build time. RUN git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git @@ -37,6 +61,8 @@ RUN set -ex \ && chmod u+x /usr/local/bin/color /usr/local/bin/revolver /usr/local/bin/zunit \ && rm -rf /tmp/zunit.git +WORKDIR / + FROM base AS test ARG ZUSER=user diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index bb659ec..7ef75cb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: zd: build: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 9c2b3b4..98c79a3 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -3,9 +3,12 @@ HOME="/home/${ZUSER}" export HOME -command sed -i -r 's#^(root:.+):/bin/ash#\1:/bin/zsh#' /etc/passwd -command adduser -D -s /bin/zsh -u "${PUID}" -h "${HOME}" "${ZUSER}" +# Change root's default shell from bash to zsh. +command sed -i -r 's#^(root:.+):/bin/bash#\1:/bin/zsh#' /etc/passwd -command printf '%s' "${ZUSER} ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user +# Create the unprivileged user with a home directory and zsh as login shell. +command useradd -m -s /bin/zsh -u "${PUID}" "${ZUSER}" + +command printf '%s\n' "${ZUSER} ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/user command mkdir -p /src /data command chown -R "${PUID}:${PGID}" /src /data diff --git a/docker/utils.zsh b/docker/utils.zsh index 427b222..d7738ab 100755 --- a/docker/utils.zsh +++ b/docker/utils.zsh @@ -57,7 +57,7 @@ zi::setup-annexes+rec() { } zi::setup-annexes+add() { - sudo apk add ruby-dev grep tree + sudo apt-get install -y ruby-dev grep tree zi::install-zsdoc zi light-mode for z-shell/z-a-test } diff --git a/docs/ci-workflows.md b/docs/ci-workflows.md index 658b038..c7c3afc 100644 --- a/docs/ci-workflows.md +++ b/docs/ci-workflows.md @@ -63,7 +63,7 @@ Runs weekly (Wednesday 03:00 UTC) and on manual dispatch. Not triggered by push **Per job:** -1. Build the Docker image for that Zsh version using `docker/setup-buildx-action` and `docker/build-push-action`, passing `ZSH_VERSION` as a build arg +1. Build the Docker image for that Zsh version using `docker/setup-buildx-action` and `docker/build-push-action`, passing `ZSH_VERSION` as a build arg — the Dockerfile compiles that exact Zsh release from source on the `debian:trixie-slim` base 2. Layer caching via `type=gha` — only changed layers rebuild on subsequent runs 3. Run all test files in a single container invocation: diff --git a/docs/local-testing.md b/docs/local-testing.md index ad52d13..4bebdda 100644 --- a/docs/local-testing.md +++ b/docs/local-testing.md @@ -115,10 +115,10 @@ The container is removed on exit. State does not persist between sessions. ## Building the image locally — `make build` ```sh -# Build with Alpine's default Zsh (same as :latest) +# Build with Debian's default Zsh (same as :latest) make build -# Build with a specific Zsh version baked in +# Build with a specific Zsh version compiled from source make build ZSH_VERSION=5.9 make build ZSH_VERSION=5.8.1 From ba01f61ee4ad67a01c7241ffcfae3c115ad49c60 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 02:57:44 +0100 Subject: [PATCH 36/47] fix: chmod a+x for zunit/revolver/color so non-root user can execute chmod u+x only sets execute for the file owner (root); the unprivileged container user (uid=1000) could not run these binaries. Use chmod a+x. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 724ec0c..c8d3fa2 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -58,7 +58,7 @@ RUN set -ex \ > /usr/local/bin/revolver \ && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ > /usr/local/bin/color \ - && chmod u+x /usr/local/bin/color /usr/local/bin/revolver /usr/local/bin/zunit \ + && chmod a+x /usr/local/bin/color /usr/local/bin/revolver /usr/local/bin/zunit \ && rm -rf /tmp/zunit.git WORKDIR / From 11fece6e7d6ae4cf0f54083814745d74bf760582 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 03:06:53 +0100 Subject: [PATCH 37/47] fix: resolve hadolint DL3003/DL3008/DL4001 trunk issues - Replace wget with curl for zsh tarball download (fixes DL4001: use only one of wget/curl) - Add DL3008 to hadolint ignored list (unpinned apt versions, same rationale as DL3018 already ignored) - Add inline hadolint ignore=DL3003 for conditional cd in source build (WORKDIR cannot be used inside an if/else shell block) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .trunk/configs/.hadolint.yaml | 1 + docker/Dockerfile | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.trunk/configs/.hadolint.yaml b/.trunk/configs/.hadolint.yaml index b2c6fa4..d58bf17 100644 --- a/.trunk/configs/.hadolint.yaml +++ b/.trunk/configs/.hadolint.yaml @@ -2,4 +2,5 @@ ignored: - SC1090 - SC1091 + - DL3008 - DL3018 diff --git a/docker/Dockerfile b/docker/Dockerfile index c8d3fa2..57c2e8c 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,12 +30,13 @@ RUN set -ex \ # When ZSH_VERSION is set, compile that exact release from source. # Otherwise install the distro-packaged zsh (Debian trixie ships 5.9). ARG ZSH_VERSION +# hadolint ignore=DL3003 RUN set -ex \ && if [ -n "${ZSH_VERSION}" ]; then \ apt-get update \ - && apt-get install -y --no-install-recommends wget xz-utils make \ + && apt-get install -y --no-install-recommends xz-utils make \ && rm -rf /var/lib/apt/lists/* \ - && wget -q "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" \ + && curl -fsSL "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" -o "zsh-${ZSH_VERSION}.tar.xz" \ && tar xf "zsh-${ZSH_VERSION}.tar.xz" \ && cd "zsh-${ZSH_VERSION}" \ && ./configure --prefix=/usr/local --enable-pcre \ From afdecb2cbfd3310dc474f43ca121330d8120a937 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 03:45:30 +0100 Subject: [PATCH 38/47] fix: use wget for zsh source download; suppress DL4001 curl -fsSL fails with exit code 22 (HTTP error) in Docker BuildKit multi-arch CI builds for all versioned zsh targets. Revert to wget which avoids the issue. Add DL4001 to hadolint ignore since wget is needed for zsh tarball download while curl is used elsewhere (revolver/color raw GitHub content). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .trunk/configs/.hadolint.yaml | 1 + docker/Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.trunk/configs/.hadolint.yaml b/.trunk/configs/.hadolint.yaml index d58bf17..f7ab5a6 100644 --- a/.trunk/configs/.hadolint.yaml +++ b/.trunk/configs/.hadolint.yaml @@ -4,3 +4,4 @@ ignored: - SC1091 - DL3008 - DL3018 + - DL4001 diff --git a/docker/Dockerfile b/docker/Dockerfile index 57c2e8c..b5144e7 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,9 +34,9 @@ ARG ZSH_VERSION RUN set -ex \ && if [ -n "${ZSH_VERSION}" ]; then \ apt-get update \ - && apt-get install -y --no-install-recommends xz-utils make \ + && apt-get install -y --no-install-recommends wget xz-utils make \ && rm -rf /var/lib/apt/lists/* \ - && curl -fsSL "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" -o "zsh-${ZSH_VERSION}.tar.xz" \ + && wget -q "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" \ && tar xf "zsh-${ZSH_VERSION}.tar.xz" \ && cd "zsh-${ZSH_VERSION}" \ && ./configure --prefix=/usr/local --enable-pcre \ From 5bbfed680b27d608c67d7dab0cf0f969adb69bba Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 04:07:00 +0100 Subject: [PATCH 39/47] fix(docker): use git clone from GitHub to build zsh from source zsh.org blocks downloads from GitHub Actions IP ranges (wget exit 8, curl exit 22). Switch to cloning the zsh-users/zsh mirror instead; tags follow the zsh-X.Y.Z format and configure script is present in tagged commits so no tarball or extra tools (wget, xz-utils) are needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .trunk/configs/.hadolint.yaml | 1 - docker/Dockerfile | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.trunk/configs/.hadolint.yaml b/.trunk/configs/.hadolint.yaml index f7ab5a6..d58bf17 100644 --- a/.trunk/configs/.hadolint.yaml +++ b/.trunk/configs/.hadolint.yaml @@ -4,4 +4,3 @@ ignored: - SC1091 - DL3008 - DL3018 - - DL4001 diff --git a/docker/Dockerfile b/docker/Dockerfile index b5144e7..1069aa3 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,22 +27,21 @@ RUN set -ex \ jq \ && rm -rf /var/lib/apt/lists/* -# When ZSH_VERSION is set, compile that exact release from source. +# When ZSH_VERSION is set, compile that exact release from the GitHub mirror. +# Tags are formatted as zsh-X.Y.Z (e.g. zsh-5.9). # Otherwise install the distro-packaged zsh (Debian trixie ships 5.9). ARG ZSH_VERSION # hadolint ignore=DL3003 RUN set -ex \ && if [ -n "${ZSH_VERSION}" ]; then \ - apt-get update \ - && apt-get install -y --no-install-recommends wget xz-utils make \ - && rm -rf /var/lib/apt/lists/* \ - && wget -q "https://www.zsh.org/pub/zsh-${ZSH_VERSION}.tar.xz" \ - && tar xf "zsh-${ZSH_VERSION}.tar.xz" \ - && cd "zsh-${ZSH_VERSION}" \ + git clone --depth 1 --branch "zsh-${ZSH_VERSION}" \ + https://github.com/zsh-users/zsh.git /tmp/zsh-src \ + && cd /tmp/zsh-src \ + && [ -f configure ] || { autoheader && autoconf; } \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ && make install \ - && cd .. && rm -rf "zsh-${ZSH_VERSION}"* ; \ + && rm -rf /tmp/zsh-src ; \ else \ apt-get update \ && apt-get install -y --no-install-recommends zsh \ From be1dc2c1a156eb8d2305bce0cf4ba669b28f2b66 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 04:52:45 +0100 Subject: [PATCH 40/47] fix(docker): patch termcap.c for GCC 14 / ncurses 6 type conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old zsh versions (5.5.1–5.8.1) declare 'static char *boolcodes[]' in Src/Modules/termcap.c, but Debian trixie's ncurses 6 headers redeclare boolcodes as 'const char * const []'. GCC 14 treats this as a hard 'conflicting types' error. Apply a sed one-liner before ./configure to promote the three bool* arrays to 'const char *const', making them compatible with modern ncurses while preserving correct semantics (they contain string literals). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1069aa3..7e87e50 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ RUN set -ex \ https://github.com/zsh-users/zsh.git /tmp/zsh-src \ && cd /tmp/zsh-src \ && [ -f configure ] || { autoheader && autoconf; } \ + && sed -i 's/^static char \*bool/static const char *const bool/' Src/Modules/termcap.c \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ && make install \ From a1a127c92bff2882be2167a2c10a4eae0049fc07 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 05:35:33 +0100 Subject: [PATCH 41/47] fix(docker): rename termcap arrays to avoid ncurses symbol collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old zsh versions (≤5.8.1) define static arrays boolcodes, boolnames, boolfnames, numcodes, numnames, numfnames, strcodes, strnames, strfnames in Src/Modules/termcap.c. Debian trixie's ncurses 6 declares these same names as extern const char * const in term.h. GCC 14 makes this a hard error: static definition cannot follow extern declaration of same name. Rename all 9 arrays to zsh_* prefix via sed before ./configure so there is no symbol collision with the system ncurses headers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7e87e50..1c315c3 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,7 +38,7 @@ RUN set -ex \ https://github.com/zsh-users/zsh.git /tmp/zsh-src \ && cd /tmp/zsh-src \ && [ -f configure ] || { autoheader && autoconf; } \ - && sed -i 's/^static char \*bool/static const char *const bool/' Src/Modules/termcap.c \ + && sed -i -E 's/\b(bool|num|str)(codes|names|fnames)\b/zsh_\1\2/g' Src/Modules/termcap.c \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ && make install \ From e1d6a624a3bb28262e08a5ad7799585704fb8525 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 05:59:11 +0100 Subject: [PATCH 42/47] fix(docker): skip doc install; use install.bin/fns/modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make install fails with exit 2 because nroff/yodl are not installed in the build container, so man page generation fails and 'test -s $file' exits 1 during make install. Use 'make install.bin install.fns install.modules' to install the binary, functions, and modules without documentation — the same fallback used by the zi zsh pack when yodl is absent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1c315c3..65cb221 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -41,7 +41,7 @@ RUN set -ex \ && sed -i -E 's/\b(bool|num|str)(codes|names|fnames)\b/zsh_\1\2/g' Src/Modules/termcap.c \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ - && make install \ + && make install.bin install.fns install.modules \ && rm -rf /tmp/zsh-src ; \ else \ apt-get update \ From 70eaefff59d0d91d63407148569c80d085cee8de Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 06:25:21 +0100 Subject: [PATCH 43/47] fix: pin zdharma/zunit to v0.8.2 in Dockerfile and test-native.yml --- .github/workflows/test-native.yml | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index cfcfa2d..526ff80 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -54,7 +54,7 @@ jobs: mkdir -p bin curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' > bin/revolver curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' > bin/color - git clone --depth 1 https://github.com/zdharma/zunit.git zunit.git + git clone --depth 1 --branch v0.8.2 https://github.com/zdharma/zunit.git zunit.git cd zunit.git && ./build.zsh && cd .. mv zunit.git/zunit bin/ chmod u+x bin/{color,revolver,zunit} diff --git a/docker/Dockerfile b/docker/Dockerfile index 65cb221..af6604a 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,7 +50,7 @@ RUN set -ex \ fi # Install zunit and its helpers into /usr/local/bin at build time. -RUN git clone --depth 1 https://github.com/zdharma/zunit.git /tmp/zunit.git +RUN git clone --depth 1 --branch v0.8.2 https://github.com/zdharma/zunit.git /tmp/zunit.git WORKDIR /tmp/zunit.git RUN set -ex \ && ./build.zsh \ From 75a830c2b8fd88df32ca07f6e1bc5365406ca6f4 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 06:53:22 +0100 Subject: [PATCH 44/47] fix(docker): add automake; update config.guess/sub before building old zsh versions --- docker/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index af6604a..a17d8df 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,6 +17,7 @@ RUN set -ex \ libpcre2-dev \ zlib1g-dev \ autoconf \ + automake \ rsync \ bash \ curl \ @@ -38,6 +39,8 @@ RUN set -ex \ https://github.com/zsh-users/zsh.git /tmp/zsh-src \ && cd /tmp/zsh-src \ && [ -f configure ] || { autoheader && autoconf; } \ + && cp "$(automake --print-libdir)/config.guess" config.guess \ + && cp "$(automake --print-libdir)/config.sub" config.sub \ && sed -i -E 's/\b(bool|num|str)(codes|names|fnames)\b/zsh_\1\2/g' Src/Modules/termcap.c \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ From cdf6bc4a9d8af7adfd96824857fbe53a3a451ed4 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 06:54:24 +0100 Subject: [PATCH 45/47] fix(docker): use autoreconf --install --force for missing configure script --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a17d8df..d259317 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,7 +38,7 @@ RUN set -ex \ git clone --depth 1 --branch "zsh-${ZSH_VERSION}" \ https://github.com/zsh-users/zsh.git /tmp/zsh-src \ && cd /tmp/zsh-src \ - && [ -f configure ] || { autoheader && autoconf; } \ + && [ -f configure ] || autoreconf --install --force \ && cp "$(automake --print-libdir)/config.guess" config.guess \ && cp "$(automake --print-libdir)/config.sub" config.sub \ && sed -i -E 's/\b(bool|num|str)(codes|names|fnames)\b/zsh_\1\2/g' Src/Modules/termcap.c \ From 5b93b92cd2e5fcfb137088260160d834c3496eec Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Sat, 16 May 2026 06:57:09 +0100 Subject: [PATCH 46/47] fix: arm64 config.guess + migrate revolver/color to z-shell/src - add automake to apt deps for autoreconf --install --force - replace autoheader+autoconf with autoreconf --install --force which refreshes config.guess/config.sub from system automake data, fixing zsh 5.5.1 build failure on aarch64 (Linux kernel 6.x) - fetch revolver and color.zsh from z-shell/src (SHA-pinned) instead of stale zdharma refs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker/Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d259317..0db6ade 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -39,8 +39,6 @@ RUN set -ex \ https://github.com/zsh-users/zsh.git /tmp/zsh-src \ && cd /tmp/zsh-src \ && [ -f configure ] || autoreconf --install --force \ - && cp "$(automake --print-libdir)/config.guess" config.guess \ - && cp "$(automake --print-libdir)/config.sub" config.sub \ && sed -i -E 's/\b(bool|num|str)(codes|names|fnames)\b/zsh_\1\2/g' Src/Modules/termcap.c \ && ./configure --prefix=/usr/local --enable-pcre \ && make -j"$(nproc)" \ @@ -58,9 +56,9 @@ WORKDIR /tmp/zunit.git RUN set -ex \ && ./build.zsh \ && mv /tmp/zunit.git/zunit /usr/local/bin/zunit \ - && curl -fsSL 'https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver' \ + && curl -fsSL 'https://raw.githubusercontent.com/z-shell/src/9335c8c14e0aaa845277f882969c146755e1241e/lib/zsh/snippets/revolver' \ > /usr/local/bin/revolver \ - && curl -fsSL 'https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh' \ + && curl -fsSL 'https://raw.githubusercontent.com/z-shell/src/9335c8c14e0aaa845277f882969c146755e1241e/lib/zsh/snippets/color.zsh' \ > /usr/local/bin/color \ && chmod a+x /usr/local/bin/color /usr/local/bin/revolver /usr/local/bin/zunit \ && rm -rf /tmp/zunit.git From 6ed0a982d31fa15bdaa0a9d62b918178e454016d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 06:23:35 +0000 Subject: [PATCH 47/47] fix: quote variable interpolation in zi_test to prevent injection Agent-Logs-Url: https://github.com/z-shell/zd/sessions/d95bd222-5b93-4dfd-924a-29030950dc4c Co-authored-by: ss-o <59910950+ss-o@users.noreply.github.com> --- bin/color | 29 +++++ bin/revolver | 318 ++++++++++++++++++++++++++++++++++++++++++++++ tests/helpers.zsh | 4 +- 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 bin/color create mode 100644 bin/revolver diff --git a/bin/color b/bin/color new file mode 100644 index 0000000..86f3852 --- /dev/null +++ b/bin/color @@ -0,0 +1,29 @@ +#!/usr/bin/env zsh + +function color() { + local color=$1 style=$2 b=0 + + shift + + case $style in + bold|b) b=1; shift ;; + italic|i) b=2; shift ;; + underline|u) b=4; shift ;; + inverse|in) b=7; shift ;; + strikethrough|s) b=9; shift ;; + esac + + case $color in + black|b) echo "\033[${b};30m${@}\033[0;m" ;; + red|r) echo "\033[${b};31m${@}\033[0;m" ;; + green|g) echo "\033[${b};32m${@}\033[0;m" ;; + yellow|y) echo "\033[${b};33m${@}\033[0;m" ;; + blue|bl) echo "\033[${b};34m${@}\033[0;m" ;; + magenta|m) echo "\033[${b};35m${@}\033[0;m" ;; + cyan|c) echo "\033[${b};36m${@}\033[0;m" ;; + white|w) echo "\033[${b};37m${@}\033[0;m" ;; + *) echo "\033[${b};38;5;$(( ${color} ))m${@}\033[0;m" ;; + esac +} + +color "$@" diff --git a/bin/revolver b/bin/revolver new file mode 100644 index 0000000..51c3dba --- /dev/null +++ b/bin/revolver @@ -0,0 +1,318 @@ +#!/usr/bin/env zsh + +local -A _revolver_spinners +_revolver_spinners=( + 'dots' '0.08 ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏' + 'dots2' '0.08 ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷' + 'dots3' '0.08 ⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓' + 'dots4' '0.08 ⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆' + 'dots5' '0.08 ⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋' + 'dots6' '0.08 ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁' + 'dots7' '0.08 ⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈' + 'dots8' '0.08 ⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈' + 'dots9' '0.08 ⢹ ⢺ ⢼ ⣸ ⣇ ⡧ ⡗ ⡏' + 'dots10' '0.08 ⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠' + 'dots11' '0.1 ⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈' + 'dots12' '0.08 "⢀⠀" "⡀⠀" "⠄⠀" "⢂⠀" "⡂⠀" "⠅⠀" "⢃⠀" "⡃⠀" "⠍⠀" "⢋⠀" "⡋⠀" "⠍⠁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⢈⠩" "⡀⢙" "⠄⡙" "⢂⠩" "⡂⢘" "⠅⡘" "⢃⠨" "⡃⢐" "⠍⡐" "⢋⠠" "⡋⢀" "⠍⡁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⠈⠩" "⠀⢙" "⠀⡙" "⠀⠩" "⠀⢘" "⠀⡘" "⠀⠨" "⠀⢐" "⠀⡐" "⠀⠠" "⠀⢀" "⠀⡀"' + 'line' '0.13 - \\ | /' + 'line2' '0.1 ⠂ - – — – -' + 'pipe' '0.1 ┤ ┘ ┴ └ ├ ┌ ┬ ┐' + 'simpleDots' '0.4 ". " ".. " "..." " "' + 'simpleDotsScrolling' '0.2 ". " ".. " "..." " .." " ." " "' + 'star' '0.07 ✶ ✸ ✹ ✺ ✹ ✷' + 'star2' '0.08 + x *' + 'flip' "0.07 _ _ _ - \` \` ' ´ - _ _ _" + 'hamburger' '0.1 ☱ ☲ ☴' + 'growVertical' '0.12 ▁ ▃ ▄ ▅ ▆ ▇ ▆ ▅ ▄ ▃' + 'growHorizontal' '0.12 ▏ ▎ ▍ ▌ ▋ ▊ ▉ ▊ ▋ ▌ ▍ ▎' + 'balloon' '0.14 " " "." "o" "O" "@" "*" " "' + 'balloon2' '0.12 . o O ° O o .' + 'noise' '▓ ▒ ░' + 'bounce' '0.1 ⠁ ⠂ ⠄ ⠂' + 'boxBounce' '0.12 ▖ ▘ ▝ ▗' + 'boxBounce2' '0.1 ▌ ▀ ▐ ▄' + 'triangle' '0.05 ◢ ◣ ◤ ◥' + 'arc' '0.1 ◜ ◠ ◝ ◞ ◡ ◟' + 'circle' '0.12 ◡ ⊙ ◠' + 'squareCorners' '0.18 ◰ ◳ ◲ ◱' + 'circleQuarters' '0.12 ◴ ◷ ◶ ◵' + 'circleHalves' '0.05 ◐ ◓ ◑ ◒' + 'squish' '0.1 ╫ ╪' + 'toggle' '0.25 ⊶ ⊷' + 'toggle2' '0.08 ▫ ▪' + 'toggle3' '0.12 □ ■' + 'toggle4' '0.1 ■ □ ▪ ▫' + 'toggle5' '0.1 ▮ ▯' + 'toggle6' '0.3 ဝ ၀' + 'toggle7' '0.08 ⦾ ⦿' + 'toggle8' '0.1 ◍ ◌' + 'toggle9' '0.1 ◉ ◎' + 'toggle10' '0.1 ㊂ ㊀ ㊁' + 'toggle11' '0.05 ⧇ ⧆' + 'toggle12' '0.12 ☗ ☖' + 'toggle13' '0.08 = * -' + 'arrow' '0.1 ← ↖ ↑ ↗ → ↘ ↓ ↙' + 'arrow2' '0.12 ▹▹▹▹▹ ▸▹▹▹▹ ▹▸▹▹▹ ▹▹▸▹▹ ▹▹▹▸▹ ▹▹▹▹▸' + 'bouncingBar' '0.08 "[ ]" "[ =]" "[ ==]" "[ ===]" "[====]" "[=== ]" "[== ]" "[= ]"' + 'bouncingBall' '0.08 "( ● )" "( ● )" "( ● )" "( ● )" "( ●)" "( ● )" "( ● )" "( ● )" "( ● )" "(● )"' + 'pong' '0.08 "▐⠂ ▌" "▐⠈ ▌" "▐ ⠂ ▌" "▐ ⠠ ▌" "▐ ⡀ ▌" "▐ ⠠ ▌" "▐ ⠂ ▌" "▐ ⠈ ▌" "▐ ⠂ ▌" "▐ ⠠ ▌" "▐ ⡀ ▌" "▐ ⠠ ▌" "▐ ⠂ ▌" "▐ ⠈ ▌" "▐ ⠂▌" "▐ ⠠▌" "▐ ⡀▌" "▐ ⠠ ▌" "▐ ⠂ ▌" "▐ ⠈ ▌" "▐ ⠂ ▌" "▐ ⠠ ▌" "▐ ⡀ ▌" "▐ ⠠ ▌" "▐ ⠂ ▌" "▐ ⠈ ▌" "▐ ⠂ ▌" "▐ ⠠ ▌" "▐ ⡀ ▌" "▐⠠ ▌"' + 'shark' '0.12 "▐|\\____________▌" "▐_|\\___________▌" "▐__|\\__________▌" "▐___|\\_________▌" "▐____|\\________▌" "▐_____|\\_______▌" "▐______|\\______▌" "▐_______|\\_____▌" "▐________|\\____▌" "▐_________|\\___▌" "▐__________|\\__▌" "▐___________|\\_▌" "▐____________|\\▌" "▐____________/|▌" "▐___________/|_▌" "▐__________/|__▌" "▐_________/|___▌" "▐________/|____▌" "▐_______/|_____▌" "▐______/|______▌" "▐_____/|_______▌" "▐____/|________▌" "▐___/|_________▌" "▐__/|__________▌" "▐_/|___________▌" "▐/|____________▌"' +) + +### +# Output usage information and exit +### +function _revolver_usage() { + echo "\033[0;33mUsage:\033[0;m" + echo " revolver [options] " + echo + echo "\033[0;33mOptions:\033[0;m" + echo " -h, --help Output help text and exit" + echo " -v, --version Output version information and exit" + echo " -s, --style Set the spinner style" + echo + echo "\033[0;33mCommands:\033[0;m" + echo " start Start the spinner" + echo " update Update the message" + echo " stop Stop the spinner" + echo " demo Display an demo of each style" +} + +### +# The main revolver process, which contains the loop +### +function _revolver_process() { + local dir statefile state msg pid="$1" spinner_index=0 + + # Find the directory and load the statefile + dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} + statefile="$dir/$pid" + + # The frames that, when animated, will make up + # our spinning indicator + frames=(${(@z)_revolver_spinners[$style]}) + interval=${(@z)frames[1]} + shift frames + + # Create a never-ending loop + while [[ 1 -eq 1 ]]; do + # If the statefile has been removed, exit the script + # to prevent it from being orphaned + if [[ ! -f $statefile ]]; then + exit 1 + fi + + # Check for the existence of the parent process + $(kill -s 0 $pid 2&>/dev/null) + + # If process doesn't exist, exit the script + # to prevent it from being orphaned + if [[ $? -ne 0 ]]; then + exit 1 + fi + + # Load the current state, and parse it to get + # the message to be displayed + state=($(cat $statefile)) + + msg="${(@)state:1}" + + # Output the current spinner frame, and add a + # slight delay before the next one + _revolver_spin + sleep ${interval:-"0.1"} + done +} + +### +# Output the spinner itself, along with a message +### +function _revolver_spin() { + local dir statefile state pid frame + + # ZSH arrays start at 1, so we need to bump the index if it's 0 + if [[ $spinner_index -eq 0 ]]; then + spinner_index+=1 + fi + + # Calculate the screen width + lim=$(tput cols) + + # Clear the line and move the cursor to the start + printf ' %.0s' {1..$lim} + echo -n "\r" + + # Echo the current frame and message, and overwrite + # the rest of the line with white space + msg="\033[0;38;5;242m${msg}\033[0;m" + frame="${${(@z)frames}[$spinner_index]//\"}" + printf '%*.*b' ${#msg} $lim "$frame $msg$(printf '%0.1s' " "{1..$lim})" + + # Return to the beginning of the line + echo -n "\r" + + # Set the spinner index to the next frame + spinner_index=$(( $(( $spinner_index + 1 )) % $(( ${#frames} + 1 )) )) +} + +### +# Stop the current spinner process +### +function _revolver_stop() { + local dir statefile state pid + + # Find the directory and load the statefile + dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} + statefile="$dir/$PPID" + + # If the statefile does not exist, raise an error. + # The spinner process itself performs the same check + # and kills itself, so it should never be orphaned + if [[ ! -f $statefile ]]; then + echo '\033[0;31mRevolver process could not be found\033[0;m' + exit 1 + fi + + # Get the current state, and parse it to find the PID + # of the spinner process + state=($(cat $statefile)) + pid="$state[1]" + + # Clear the line and move the cursor to the start + printf ' %.0s' {1..$(tput cols)} + echo -n "\r" + + # If a PID has been found, kill the process + [[ ! -z $pid ]] && kill "$pid" > /dev/null + unset pid + + # Remove the statefile + rm $statefile +} + +### +# Update the message being displayed +function _revolver_update() { + local dir statefile state pid msg="$1" + + # Find the directory and load the statefile + dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} + statefile="$dir/$PPID" + + # If the statefile does not exist, raise an error. + # The spinner process itself performs the same check + # and kills itself, so it should never be orphaned + if [[ ! -f $statefile ]]; then + echo '\033[0;31mRevolver process could not be found\033[0;m' + exit 1 + fi + + # Get the current state, and parse it to find the PID + # of the spinner process + state=($(cat $statefile)) + pid="$state[1]" + + # Clear the line and move the cursor to the start + printf ' %.0s' {1..$(tput cols)} + echo -n "\r" + + # Echo the new message to the statefile, to be + # picked up by the spinner process + echo "$pid $msg" >! $statefile +} + +### +# Create a new spinner with the specified message +### +function _revolver_start() { + local dir statefile msg="$1" + + # Find the directory and create it if it doesn't exist + dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} + if [[ ! -d $dir ]]; then + mkdir -p $dir + fi + + # Create the filename for the statefile + statefile="$dir/$PPID" + + touch $statefile + if [[ ! -f $statefile ]]; then + echo '\033[0;31mRevolver process could not create state file\033[0;m' + echo "Check that the directory $dir is writable" + exit 1 + fi + + # Start the spinner process in the background + _revolver_process $PPID &! + + # Save the current state to the statefile + echo "$! $msg" >! $statefile +} + +### +# Demonstrate each of the included spinner styles +### +function _revolver_demo() { + for style in "${(@k)_revolver_spinners[@]}"; do + revolver --style $style start $style + sleep 2 + revolver stop + done +} + +### +# Handle command input +### +function _revolver() { + # Get the context from the first parameter + local help version style ctx="$1" + + # Parse CLI options + zparseopts -D \ + h=help -help=help \ + v=version -version=version \ + s:=style -style:=style + + # Output usage information and exit + if [[ -n $help ]]; then + _revolver_usage + exit 0 + fi + + # Output version information and exit + if [[ -n $version ]]; then + echo '0.2.0' + exit 0 + fi + + if [[ -z $style ]]; then + style='dots' + fi + + if [[ -n $style ]]; then + shift style + ctx="$1" + fi + + if [[ -z $_revolver_spinners[$style] ]]; then + echo $(color red "Spinner '$style' is not recognised") + exit 1 + fi + + case $ctx in + start|update|stop|demo) + # Check if a valid command is passed, + # and if so, run it + _revolver_${ctx} "${(@)@:2}" + ;; + *) + # If the context is not recognised, + # throw an error and exit + echo "Command $ctx is not recognised" + exit 1 + ;; + esac +} + +_revolver "$@" diff --git a/tests/helpers.zsh b/tests/helpers.zsh index 625ae64..e716769 100644 --- a/tests/helpers.zsh +++ b/tests/helpers.zsh @@ -18,8 +18,8 @@ zi_test() { typeset -gxU path path=( \${HOME}/go/bin \$path ) typeset -gA ZI - ZI[HOME_DIR]=${_zi_data} - source ${_zi_bin}/zi.zsh + ZI[HOME_DIR]='${_zi_data}' + source '${_zi_bin}/zi.zsh' autoload -Uz _zi ${script} "