Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 74 additions & 20 deletions .github/workflows/deploy-ps1-tests.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,77 @@
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:
# 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]
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
matrix:
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
Expand All @@ -42,6 +81,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:
Expand All @@ -50,11 +92,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

Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/deploy-sh-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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:
# 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.
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"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

---
Expand Down Expand Up @@ -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 pull request from any branch, with no path filters. Never narrow their triggers.

## Decision Checklist

Expand All @@ -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.
1 change: 0 additions & 1 deletion deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ interactive_select_agents() {

while true; do
key=""
sequence=""
IFS= read -rsn1 key || true

if [[ "$key" == $'\x1b' ]]; then
Expand Down
23 changes: 19 additions & 4 deletions tests/test-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Comment on lines +37 to +38

PASSED=0
FAILED=0

Expand Down Expand Up @@ -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"
Expand Down