From 0005bf7375322d12e64e2723202e62d365e50095 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Jun 2026 19:09:51 +0800 Subject: [PATCH] feat: generated libraries are zsh-ready (test scaffold + full shell CI) Make `cookiecutter` produce libraries that are zsh-ready out of the box: - New `{{cookiecutter.lib_name}}/.github/workflows/test.yml`: the canonical CI mirroring alexzhangs/xsh's shell matrix (bash 3.2/4.4/5.x + zsh 5.x). It is self-describing (loads `${{ github.repository }}`), so it needs no per-library edits; listed in `_copy_without_render` so GitHub's `${{ }}` expressions survive Jinja rendering. - New `{{cookiecutter.lib_name}}/test.sh`: a runnable skeleton that self-sources ~/.xshrc (so it runs under bash or zsh), derives the library name from xsh.lib, and import-smokes the utilities (a no-op until the author adds some). - setup.yml now relocates the generated `.github/workflows/test.yml` (the hidden dir isn't matched by the `mv ... *` glob). - Skeleton README notes zsh support. Verified locally: `cookiecutter` generation renders xsh.lib/README and copies the workflow verbatim (GHA expressions intact, valid YAML); the skeleton test.sh parses under bash 3.2 and zsh. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/setup.yml | 4 + cookiecutter.json | 3 +- .../.github/workflows/test.yml | 81 +++++++++++++++++++ {{cookiecutter.lib_name}}/README.md | 5 +- {{cookiecutter.lib_name}}/test.sh | 46 +++++++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 {{cookiecutter.lib_name}}/.github/workflows/test.yml create mode 100644 {{cookiecutter.lib_name}}/test.sh diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml index fab53bd..7043112 100644 --- a/.github/workflows/setup.yml +++ b/.github/workflows/setup.yml @@ -64,6 +64,10 @@ jobs: mv /tmp/$REPO_NAME/* . # And .gitignore too: mv /tmp/$REPO_NAME/.gitignore . + # And the generated CI workflow (hidden .github dir isn't matched by + # the glob above); the template's own .github/workflows/ already + # exists here (setup.yml was just removed from it). + mv /tmp/$REPO_NAME/.github/workflows/test.yml .github/workflows/ - name: Force push new repo contents uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/cookiecutter.json b/cookiecutter.json index e2eeeb1..7d82ac9 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,5 +2,6 @@ "lib_name": "", "description": "", "github_username": "", - "author_name": "" + "author_name": "", + "_copy_without_render": [".github/workflows/test.yml"] } diff --git a/{{cookiecutter.lib_name}}/.github/workflows/test.yml b/{{cookiecutter.lib_name}}/.github/workflows/test.yml new file mode 100644 index 0000000..382a047 --- /dev/null +++ b/{{cookiecutter.lib_name}}/.github/workflows/test.yml @@ -0,0 +1,81 @@ +name: Test + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +# This workflow is copied verbatim by cookiecutter (see _copy_without_render in +# cookiecutter.json) so GitHub's ${{ ... }} expressions survive generation. It +# is self-describing — it loads whatever repo it lives in via +# ${{ github.repository }}, so no per-library edits are needed. + +jobs: + test: + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + name: ${{ matrix.label }} + defaults: + run: + shell: bash + # Mirror alexzhangs/xsh's shell matrix: bash 3.2 / 4.4 / 5.x + zsh 5.x. + # Utilities run under zsh's ksh emulation (provided by xsh >= 0.7.0). + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, container: '', shell_path: /bin/bash, label: bash-5.x-linux } + - { os: macos-latest, container: '', shell_path: /bin/bash, label: bash-3.2-macos } + - { os: macos-latest, container: '', shell_path: brew, label: bash-5.x-macos } + - { os: ubuntu-latest, container: rockylinux:8, shell_path: /bin/bash, label: bash-4.4-linux } + - { os: macos-latest, container: '', shell_path: /bin/zsh, label: zsh-5.x-macos } + - { os: ubuntu-latest, container: '', shell_path: /usr/bin/zsh, label: zsh-5.x-linux } + + steps: + - name: Pre-install git (rockylinux container) + if: matrix.container != '' + run: dnf install -y git + + - name: Install dependencies (rockylinux container) + if: matrix.container != '' + run: | + dnf install -y --allowerasing coreutils + dnf install -y gawk sed file findutils diffutils procps-ng which tar gzip python3 + command -v python >/dev/null 2>&1 || ln -sf "$(command -v python3)" /usr/local/bin/python + git config --global --add safe.directory '*' + + - name: Install zsh (Linux) + if: matrix.container == '' && runner.os == 'Linux' && contains(matrix.shell_path, 'zsh') + run: sudo apt-get update && sudo apt-get install -y zsh + + - name: Install Homebrew bash (macOS bash 5.x) + if: matrix.shell_path == 'brew' + run: | + brew install bash + echo "SHELL_PATH=$(brew --prefix)/bin/bash" >> "$GITHUB_ENV" + + - name: Set shell path + if: matrix.shell_path != 'brew' + run: echo "SHELL_PATH=${{ matrix.shell_path }}" >> "$GITHUB_ENV" + + - name: Install xsh + run: | + git clone --depth=50 --branch=master https://github.com/alexzhangs/xsh.git /tmp/xsh + bash /tmp/xsh/install.sh + + - name: Load dependency library xsh-lib/core + run: | + source ~/.xshrc + xsh load xsh-lib/core + + - name: Load library from current branch + run: | + source ~/.xshrc + xsh load -b "${{ github.head_ref || github.ref_name }}" "${{ github.repository }}" + + - name: Run tests (${{ matrix.label }}) + # test.sh self-sources ~/.xshrc, so running it under $SHELL_PATH makes + # the utilities execute under that shell (bash, or zsh's ksh emulation). + run: | + "$SHELL_PATH" ~/.xsh/repo/"${{ github.repository }}"/test.sh diff --git a/{{cookiecutter.lib_name}}/README.md b/{{cookiecutter.lib_name}}/README.md index 19c8abb..cab3498 100644 --- a/{{cookiecutter.lib_name}}/README.md +++ b/{{cookiecutter.lib_name}}/README.md @@ -6,12 +6,15 @@ About xsh and its libraries, check out [xsh document](https://github.com/alexzha ## Requirements -1. bash +1. bash or zsh Tested with bash: * 4.3.48 on Linux * 3.2.57 on macOS + The utilities also run under **zsh** (the default shell on modern macOS); + xsh executes them under zsh's ksh emulation. Requires xsh >= 0.7.0 for zsh. + ## Dependency 1. xsh-lib/core diff --git a/{{cookiecutter.lib_name}}/test.sh b/{{cookiecutter.lib_name}}/test.sh new file mode 100644 index 0000000..b35930c --- /dev/null +++ b/{{cookiecutter.lib_name}}/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# +# Tests for this xsh library. Plain assertions, no external test framework +# (mirrors xsh-lib/core/test.sh). Runs under bash and zsh. +# +# Usage: +# xsh load xsh-lib/core / # one-time +# bash test.sh # or: zsh test.sh +# + +# Make the `xsh` function available when run as a child process. Under bash it +# is inherited as an exported function (no-op here); zsh cannot export functions, +# so a child `zsh test.sh` sources ~/.xshrc to define xsh as a real zsh function +# (otherwise it would only see the bin/xsh shim, which runs bash). This is what +# lets the utilities execute under zsh's ksh emulation. +if ! type xsh 2>/dev/null | grep -q 'function'; then + # shellcheck source=/dev/null + . ~/.xshrc +fi + +set -e -o pipefail + +# Derive the library's LPUE prefix from xsh.lib (its `name=` field). +__dir=$(cd "$(dirname "$0")" && pwd) +__lib=$(awk -F= '/^name=/{print $2; exit}' "$__dir/xsh.lib") +[ -n "$__lib" ] || { echo "test.sh: could not read library name from xsh.lib" >&2; exit 1; } + +xsh log info "xsh list ${__lib}/" +xsh list "${__lib}/*" >/dev/null + +# import-smoke: every function utility sources cleanly (under zsh each is +# sourced with the injected `emulate -L ksh`). A brand-new library has no +# utilities yet, so this loop is a no-op until you add some. +xsh log info "import-smoke: all ${__lib} function utilities" +while read -r __lpue; do + [ -z "$__lpue" ] && continue + xsh import "$__lpue" +done < <(xsh list "${__lib}/*" | awk '$1 == "[functions]" {print $2}') + +# TODO: add functional assertions for your utilities, e.g. +# [[ $(xsh ${__lib}/some/util arg) == expected ]] + +echo +echo "All tests passed." + +exit