From 7b79ebf99a2cb6391e9486ed50467b6d859b3719 Mon Sep 17 00:00:00 2001 From: ldastey-dev Date: Sun, 6 Sep 2026 12:17:16 +0100 Subject: [PATCH 1/2] ci: enforce deploy.sh and deploy.ps1 cross-platform tests on every branch deploy.sh and deploy.ps1 are load-bearing for every consumer of this library, but CI only covered deploy.ps1, only on main and PRs into main, and only when specific paths changed. deploy.sh was not in the path filter and had no workflow at all, so it could break without any signal. Triggers - Both workflows now run on every push and every pull request, with no branch or path filters. Add concurrency groups so in-flight runs are superseded. New deploy.sh workflow - shellcheck (--severity=warning) and bash -n over every tracked .sh file, as a fast parallel gate. - End-to-end tests/test-deploy.sh on ubuntu-latest and macos-latest, plus an explicit run under macOS /bin/bash 3.2 and a cwd-independence check. deploy.ps1 workflow - Add a fast parse + PSScriptAnalyzer compatibility gate (5.1 and 7.0). - Add macos-pwsh to the matrix alongside Windows PowerShell 5.1. - Fail if PSScriptAnalyzer is missing, since deploy.Tests.ps1 silently skips its compatibility test in that case, hiding 5.1 incompatibilities. Portability fixes found by the new macOS coverage - tests/test-deploy.sh used sha256sum and find -perm /111, both GNU-only. macOS has neither, so the suite could never have passed there. Replaced with helpers that fall back to shasum -a 256 and use -exec test -x for listing. - deploy.sh: remove a dead 'sequence' assignment so shellcheck is clean at warning severity, allowing CI to gate on it. Docs - Record the portability constraints, CI enforcement, and the rule against narrowing these triggers in the maintainer AGENTS.md. - Ignore testResults.xml, produced by Invoke-Pester -CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deploy-ps1-tests.yml | 92 +++++++++++++++++++------ .github/workflows/deploy-sh-tests.yml | 95 ++++++++++++++++++++++++++ .gitignore | 3 + AGENTS.md | 7 ++ deploy.sh | 1 - tests/test-deploy.sh | 23 +++++-- 6 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/deploy-sh-tests.yml diff --git a/.github/workflows/deploy-ps1-tests.yml b/.github/workflows/deploy-ps1-tests.yml index 7f4efeb..bb3e891 100644 --- a/.github/workflows/deploy-ps1-tests.yml +++ b/.github/workflows/deploy-ps1-tests.yml @@ -1,30 +1,64 @@ name: deploy.ps1 tests +# deploy.ps1 is a critical script: it is the Windows entry point for every +# consumer of this library. It must work on Windows PowerShell 5.1 and on +# PowerShell 7+ across Windows, Linux and macOS, so it is tested on every push +# and every pull request regardless of branch or changed paths. on: push: - branches: [main] - paths: - - 'deploy.ps1' - - 'deploy.Tests.ps1' - - 'PSScriptAnalyzerSettings.psd1' - - 'tests/**' - - 'core/**' - - 'standards/**' - - 'playbooks/**' - - '.github/workflows/deploy-ps1-tests.yml' pull_request: - branches: [main] - paths: - - 'deploy.ps1' - - 'deploy.Tests.ps1' - - 'PSScriptAnalyzerSettings.psd1' - - 'tests/**' - - 'core/**' - - 'standards/**' - - 'playbooks/**' - - '.github/workflows/deploy-ps1-tests.yml' + +# Supersede in-flight runs for the same ref so feedback tracks the latest push. +concurrency: + group: deploy-ps1-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: + # Fast gate (seconds): parse and static compatibility analysis, so a syntax or + # 5.1-incompatibility break is reported without waiting for the Windows + # PowerShell 5.1 end-to-end job, which is the slowest in the matrix. + lint: + name: parse and compatibility analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + + - name: Parse check + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $failed = $false + foreach ($file in (Get-ChildItem -Path . -Recurse -Include '*.ps1','*.psd1' -File)) { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$errors) | Out-Null + if ($errors) { + Write-Host "::error::Parse errors in $($file.FullName)" + $errors | ForEach-Object { Write-Host " $($_.Message)" } + $failed = $true + } + } + if ($failed) { exit 1 } + Write-Host "All PowerShell files parse cleanly." + + - name: PSScriptAnalyzer compatibility rules (5.1 and 7.0) + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Import-Module PSScriptAnalyzer + $results = Invoke-ScriptAnalyzer -Path ./deploy.ps1 -Settings ./PSScriptAnalyzerSettings.psd1 + if ($results) { + $results | Format-Table -AutoSize | Out-String | Write-Host + throw "PSScriptAnalyzer reported $($results.Count) compatibility finding(s)." + } + Write-Host "No compatibility findings for Windows PowerShell 5.1 or PowerShell 7.0." + test: strategy: fail-fast: false @@ -32,7 +66,8 @@ jobs: include: # Windows PowerShell 5.1 is the host that reported both bugs this workflow guards # against (the BOM-less Windows-1252 parse break, and Add-Type's CurrentDirectory - # side effect) - it must be tested directly, not simulated. + # side effect) - it must be tested directly, not simulated. 5.1 is the oldest + # supported baseline; if it breaks here, the script is not shippable. - os: windows-latest shell: powershell name: windows-powershell-5.1 @@ -42,6 +77,9 @@ jobs: - os: ubuntu-latest shell: pwsh name: ubuntu-pwsh + - os: macos-latest + shell: pwsh + name: macos-pwsh runs-on: ${{ matrix.os }} name: ${{ matrix.name }} defaults: @@ -50,11 +88,23 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Report PowerShell version + run: $PSVersionTable | Format-List | Out-String | Write-Host + - name: Install test dependencies run: | Install-Module -Name Pester -MinimumVersion 5.0 -Force -SkipPublisherCheck -Scope CurrentUser Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + # deploy.Tests.ps1 skips its PSScriptAnalyzer compatibility test when the module + # is absent. A silent skip in CI would hide 5.1 incompatibilities, so fail loudly + # if the install step did not take effect. + - name: Verify PSScriptAnalyzer is available + run: | + if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) { + throw "PSScriptAnalyzer is not available; compatibility tests would silently skip." + } + - name: Run Pester unit tests run: Invoke-Pester -Path ./deploy.Tests.ps1 -CI diff --git a/.github/workflows/deploy-sh-tests.yml b/.github/workflows/deploy-sh-tests.yml new file mode 100644 index 0000000..bcef5a9 --- /dev/null +++ b/.github/workflows/deploy-sh-tests.yml @@ -0,0 +1,95 @@ +name: deploy.sh tests + +# deploy.sh is a critical script: every consumer of this library runs it to +# install the templates. It must work on both Linux and macOS, so it is tested +# on every push and every pull request regardless of branch or changed paths. +on: + push: + pull_request: + +# Supersede in-flight runs for the same ref so feedback tracks the latest push. +concurrency: + group: deploy-sh-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # Fast gate (seconds): catches syntax errors and shell bugs before the + # end-to-end matrix finishes. Runs in parallel with the matrix, not before it, + # so a green lint never delays real test results. + lint: + name: shellcheck and syntax + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install shellcheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Collect shell scripts + run: | + # Every tracked .sh file, so new playbook scripts are covered automatically. + git ls-files '*.sh' + + - name: Syntax check (bash -n) + run: | + status=0 + while IFS= read -r script; do + bash -n "$script" || status=1 + done < <(git ls-files '*.sh') + exit $status + + - name: ShellCheck + run: | + # --severity=warning fails on portability and correctness findings, not + # just hard errors. The tree is clean at this level; keep it that way. + status=0 + while IFS= read -r script; do + shellcheck --shell=bash --severity=warning "$script" || status=1 + done < <(git ls-files '*.sh') + exit $status + + test: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + # Ubuntu: GNU coreutils and GNU find. + - os: ubuntu-latest + name: ubuntu-bash + # macOS: BSD coreutils, BSD find, and /bin/bash 3.2. This combination + # is what breaks GNU-only constructs such as sha256sum and -perm /111, + # so it must be tested on a real macOS host rather than simulated. + - os: macos-latest + name: macos-bash + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + + - name: Report bash version + run: | + echo "default bash: $(bash --version | head -1)" + echo "/bin/bash: $(/bin/bash --version | head -1)" + + - name: Run end-to-end deploy tests + run: bash tests/test-deploy.sh + + # macOS ships bash 3.2 as /bin/bash and deploy.sh declares #!/bin/bash, + # so consumers on macOS run it under 3.2. Assert that explicitly instead of + # relying on whichever bash happens to be first on PATH. + - name: Run end-to-end deploy tests under /bin/bash + if: runner.os == 'macOS' + run: /bin/bash tests/test-deploy.sh + + - name: Verify deploy.sh runs from an arbitrary working directory + run: | + target="$(mktemp -d)" + cd "$(mktemp -d)" + "$GITHUB_WORKSPACE/deploy.sh" --agents all --overwrite "$target" + test -f "$target/AGENTS.md" + test -f "$target/.context/index.md" diff --git a/.gitignore b/.gitignore index dd5a7af..39e7d61 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ # Keep them locally under /engagements/ so they cannot be accidentally # committed back to this template repo. /engagements/ + +# Pester test output (Invoke-Pester -CI) +testResults.xml diff --git a/AGENTS.md b/AGENTS.md index 984a2cb..aaf8081 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,9 @@ The directory layout in this repo is **not** the layout in target repos. The dep - `deploy.sh` (bash) and `deploy.ps1` (PowerShell) must stay behaviour-equivalent. A change to one usually needs the matching change in the other. - Both must honour the overwrite guard (`--overwrite` / `--no-overwrite`, interactive prompt otherwise). - The interactive `--agents` menu must work on macOS, Linux, and Windows PowerShell. +- **`deploy.sh` must be portable to macOS and Linux.** macOS ships bash 3.2 as `/bin/bash` and BSD userland, so bash 4+ syntax (`declare -A`, `mapfile`, `${var,,}`) and GNU-only utilities are forbidden. Use portable equivalents: `shasum -a 256` or a `sha256sum` fallback rather than assuming `sha256sum`; `find … -exec test -x {} \; -print` rather than `find -perm /111`; `sed -i.bak` (then delete the backup) rather than bare `sed -i`. The same rule applies to every `.sh` file in `playbooks/`. +- **`deploy.ps1` must run on Windows PowerShell 5.1 and PowerShell 7+.** 5.1 is the oldest supported baseline and is where the historic parse and `Add-Type` bugs surfaced. PowerShell 7-only syntax (ternaries, `??`, `-Parallel`) is forbidden. `PSScriptAnalyzerSettings.psd1` encodes this and is enforced in CI. +- **CI enforces both.** `.github/workflows/deploy-sh-tests.yml` and `.github/workflows/deploy-ps1-tests.yml` run on every push and every pull request, on every branch, with no path filters — these scripts are load-bearing for every consumer, so they are never allowed to go untested. `deploy.sh` is tested on Ubuntu and macOS (including explicitly under `/bin/bash` 3.2) and linted with `shellcheck --severity=warning`; `deploy.ps1` is tested on Windows PowerShell 5.1, and on PowerShell 7 across Windows, Linux and macOS. Do not add path filters or branch filters to these workflows. - **Never run `deploy.sh` or `deploy.ps1` against this repository.** This repo is the source library, not a deploy target. Running the scripts here writes `/AGENTS.md`, `/CLAUDE.md`, `/.context/`, `/.cursor/`, `/.devin/`, `/.windsurfrules`, `/.github/copilot-instructions.md`, `/.claude/`, and `/.github/skills/` at the repo root — the `.gitignore` keeps those out of commits, but they overlay tracked source paths (`core/AGENTS.md` is the tracked source; `/AGENTS.md` is the tracked maintainer guide) and create confusing untracked state. To test a deploy-script change, run it against an empty scratch directory (`mkdir /tmp/agentic-context-test && ./deploy.sh /tmp/agentic-context-test`) or another repo entirely. --- @@ -178,6 +181,9 @@ Additional rules that apply specifically to maintainers of this template repo: - Keep authoring (markdown) and distribution (deploy scripts) separate in every change. - Never commit engagement artefacts or root-level deploy outputs. - `deploy.sh` and `deploy.ps1` must remain behaviour-equivalent. +- `deploy.sh` must run on macOS bash 3.2 with BSD userland as well as on Linux with GNU userland. No bash 4+ syntax, no GNU-only utilities. +- `deploy.ps1` must run on Windows PowerShell 5.1 as well as PowerShell 7+. No PowerShell 7-only syntax. +- The deploy-script workflows must run on every branch and every pull request with no path filters. Never narrow their triggers. ## Decision Checklist @@ -187,6 +193,7 @@ Before opening a PR, confirm: - [ ] Prose lives in `standards/`, `playbooks/`, or `core/.context/` — not in a per-agent file. - [ ] If a new standard or playbook: added to `core/.context/index.md` and (for standards) the table in `core/AGENTS.md`. - [ ] If a deploy script change: both `deploy.sh` and `deploy.ps1` updated, and tested against a scratch directory — never against this repo. +- [ ] If a deploy script change: `shellcheck --severity=warning` is clean, and no bash 4+ syntax, GNU-only utilities, or PowerShell 7-only syntax was introduced. - [ ] If a new agent: redirect file added under `core/`, both deploy scripts updated, README table updated. - [ ] British English, kebab-case, prescriptive language. - [ ] No engagement artefacts or generated deploy outputs in the diff. diff --git a/deploy.sh b/deploy.sh index 56a1fa9..e4a9aee 100755 --- a/deploy.sh +++ b/deploy.sh @@ -225,7 +225,6 @@ interactive_select_agents() { while true; do key="" - sequence="" IFS= read -rsn1 key || true if [[ "$key" == $'\x1b' ]]; then diff --git a/tests/test-deploy.sh b/tests/test-deploy.sh index 467452f..c6b009a 100644 --- a/tests/test-deploy.sh +++ b/tests/test-deploy.sh @@ -22,6 +22,21 @@ if [ -x "$REPO_DIR/README.md" ]; then PERMS_SUPPORTED=false fi +# Portable checksum and executable-file listing. GNU coreutils provides +# sha256sum and GNU find accepts -perm /111; macOS ships BSD equivalents that +# reject both. Resolve each once so the suite runs identically on Linux and macOS. +if command -v sha256sum >/dev/null 2>&1; then + checksum_tree() { find "$1" -type f -exec sha256sum {} + | sort; } +elif command -v shasum >/dev/null 2>&1; then + checksum_tree() { find "$1" -type f -exec shasum -a 256 {} + | sort; } +else + echo "ERROR: neither sha256sum nor shasum is available" >&2 + exit 1 +fi + +# -exec test -x is portable; GNU -perm /111 and BSD -perm +111 are not interchangeable. +list_executables() { find "$1" -type f -exec test -x {} \; -print | sort; } + PASSED=0 FAILED=0 @@ -260,12 +275,12 @@ TC5_PERMS1=$(mktemp) TC5_PERMS2=$(mktemp) "$REPO_DIR/deploy.sh" --agents all --overwrite "$TC5_DIR" >/dev/null 2>&1 -find "$TC5_DIR" -type f -exec sha256sum {} + | sort > "$TC5_CHECKSUMS1" -find "$TC5_DIR" -type f -perm /111 | sort > "$TC5_PERMS1" +checksum_tree "$TC5_DIR" > "$TC5_CHECKSUMS1" +list_executables "$TC5_DIR" > "$TC5_PERMS1" "$REPO_DIR/deploy.sh" --agents all --overwrite "$TC5_DIR" >/dev/null 2>&1 -find "$TC5_DIR" -type f -exec sha256sum {} + | sort > "$TC5_CHECKSUMS2" -find "$TC5_DIR" -type f -perm /111 | sort > "$TC5_PERMS2" +checksum_tree "$TC5_DIR" > "$TC5_CHECKSUMS2" +list_executables "$TC5_DIR" > "$TC5_PERMS2" if diff -q "$TC5_CHECKSUMS1" "$TC5_CHECKSUMS2" >/dev/null 2>&1; then pass "File checksums identical across both runs" From 750fdedb82237484bd0ec3a0fd30e4a11dc76dcf Mon Sep 17 00:00:00 2001 From: ldastey-dev Date: Sun, 6 Sep 2026 12:23:14 +0100 Subject: [PATCH 2/2] ci: scope push trigger to main to avoid duplicating every pull request run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pull_request already fires on the synchronize event, so a push to a branch with an open pull request was running the full matrix twice — including the ~4 minute Windows PowerShell 5.1 job. Coverage is unchanged: every pull request from any branch still runs both workflows on every commit, and main is still protected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deploy-ps1-tests.yml | 4 ++++ .github/workflows/deploy-sh-tests.yml | 4 ++++ AGENTS.md | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-ps1-tests.yml b/.github/workflows/deploy-ps1-tests.yml index bb3e891..88d1e36 100644 --- a/.github/workflows/deploy-ps1-tests.yml +++ b/.github/workflows/deploy-ps1-tests.yml @@ -5,7 +5,11 @@ name: deploy.ps1 tests # PowerShell 7+ across Windows, Linux and macOS, so it is tested on every push # and every pull request regardless of branch or changed paths. on: + # pull_request covers every PR from any head branch, and re-runs on each new + # commit pushed to that branch (the synchronize event). push is scoped to main + # so protecting the trunk does not double every PR run and slow the queue. push: + branches: [main] pull_request: # Supersede in-flight runs for the same ref so feedback tracks the latest push. diff --git a/.github/workflows/deploy-sh-tests.yml b/.github/workflows/deploy-sh-tests.yml index bcef5a9..e4e7558 100644 --- a/.github/workflows/deploy-sh-tests.yml +++ b/.github/workflows/deploy-sh-tests.yml @@ -4,7 +4,11 @@ name: deploy.sh tests # install the templates. It must work on both Linux and macOS, so it is tested # on every push and every pull request regardless of branch or changed paths. on: + # pull_request covers every PR from any head branch, and re-runs on each new + # commit pushed to that branch (the synchronize event). push is scoped to main + # so protecting the trunk does not double every PR run and slow the queue. push: + branches: [main] pull_request: # Supersede in-flight runs for the same ref so feedback tracks the latest push. diff --git a/AGENTS.md b/AGENTS.md index aaf8081..5d36863 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,7 +131,7 @@ The directory layout in this repo is **not** the layout in target repos. The dep - The interactive `--agents` menu must work on macOS, Linux, and Windows PowerShell. - **`deploy.sh` must be portable to macOS and Linux.** macOS ships bash 3.2 as `/bin/bash` and BSD userland, so bash 4+ syntax (`declare -A`, `mapfile`, `${var,,}`) and GNU-only utilities are forbidden. Use portable equivalents: `shasum -a 256` or a `sha256sum` fallback rather than assuming `sha256sum`; `find … -exec test -x {} \; -print` rather than `find -perm /111`; `sed -i.bak` (then delete the backup) rather than bare `sed -i`. The same rule applies to every `.sh` file in `playbooks/`. - **`deploy.ps1` must run on Windows PowerShell 5.1 and PowerShell 7+.** 5.1 is the oldest supported baseline and is where the historic parse and `Add-Type` bugs surfaced. PowerShell 7-only syntax (ternaries, `??`, `-Parallel`) is forbidden. `PSScriptAnalyzerSettings.psd1` encodes this and is enforced in CI. -- **CI enforces both.** `.github/workflows/deploy-sh-tests.yml` and `.github/workflows/deploy-ps1-tests.yml` run on every push and every pull request, on every branch, with no path filters — these scripts are load-bearing for every consumer, so they are never allowed to go untested. `deploy.sh` is tested on Ubuntu and macOS (including explicitly under `/bin/bash` 3.2) and linted with `shellcheck --severity=warning`; `deploy.ps1` is tested on Windows PowerShell 5.1, and on PowerShell 7 across Windows, Linux and macOS. Do not add path filters or branch filters to these workflows. +- **CI enforces both.** `.github/workflows/deploy-sh-tests.yml` and `.github/workflows/deploy-ps1-tests.yml` run on every pull request from any branch (and on every commit pushed to an open pull request), plus every push to `main`, with **no path filters** — these scripts are load-bearing for every consumer, so they are never allowed to go untested. `deploy.sh` is tested on Ubuntu and macOS (including explicitly under `/bin/bash` 3.2) and linted with `shellcheck --severity=warning`; `deploy.ps1` is tested on Windows PowerShell 5.1, and on PowerShell 7 across Windows, Linux and macOS. Do not add path filters to these workflows and do not restrict the `pull_request` trigger to specific branches. - **Never run `deploy.sh` or `deploy.ps1` against this repository.** This repo is the source library, not a deploy target. Running the scripts here writes `/AGENTS.md`, `/CLAUDE.md`, `/.context/`, `/.cursor/`, `/.devin/`, `/.windsurfrules`, `/.github/copilot-instructions.md`, `/.claude/`, and `/.github/skills/` at the repo root — the `.gitignore` keeps those out of commits, but they overlay tracked source paths (`core/AGENTS.md` is the tracked source; `/AGENTS.md` is the tracked maintainer guide) and create confusing untracked state. To test a deploy-script change, run it against an empty scratch directory (`mkdir /tmp/agentic-context-test && ./deploy.sh /tmp/agentic-context-test`) or another repo entirely. --- @@ -183,7 +183,7 @@ Additional rules that apply specifically to maintainers of this template repo: - `deploy.sh` and `deploy.ps1` must remain behaviour-equivalent. - `deploy.sh` must run on macOS bash 3.2 with BSD userland as well as on Linux with GNU userland. No bash 4+ syntax, no GNU-only utilities. - `deploy.ps1` must run on Windows PowerShell 5.1 as well as PowerShell 7+. No PowerShell 7-only syntax. -- The deploy-script workflows must run on every branch and every pull request with no path filters. Never narrow their triggers. +- The deploy-script workflows must run on every pull request from any branch, with no path filters. Never narrow their triggers. ## Decision Checklist