Skip to content

Cover the case a real adopter is actually in #56

Cover the case a real adopter is actually in

Cover the case a real adopter is actually in #56

Workflow file for this run

name: verify
on:
push:
branches: [main]
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The gate needs jq for the JSON checks and the install dry run. plutil does not exist
# on Linux, so the launchd check skips itself and says so.
- name: Install jq
run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
# Check 19 runs `claude plugin validate --strict`; without a working CLI it skips, and the
# falsifiability suite skips its case 19 with it. Install it so CI proves the real thing.
#
# `npm install -g` alone was not enough and produced a false green for as long as the step
# has existed. The npm package is a shim that fetches a platform-native binary in its
# postinstall; that did not land here, so every `claude plugin validate` exited non-zero
# with "claude native binary not installed" — and check 19's old output parsing read that
# error as no findings and printed ok. CI has never once validated these manifests.
#
# So install it, then prove the binary answers before trusting the step. `claude
# --version` failing here is a setup failure and should stop the run, not be discovered
# later as a check that silently measures nothing.
- name: Install Claude Code CLI
run: |
npm install -g @anthropic-ai/claude-code || true
root="$(npm root -g)/@anthropic-ai/claude-code"
node "$root/install.cjs" || true
# The native package resolves as an optional dependency and does not always arrive on
# a runner. The package ships a documented pure-node fallback for exactly this, so
# prefer it over failing the job: put a shim ahead of the shim.
if ! claude --version >/dev/null 2>&1 && [ -f "$root/cli-wrapper.cjs" ]; then
printf '#!/bin/sh\nexec node "%s/cli-wrapper.cjs" "$@"\n' "$root" | sudo tee /usr/local/bin/claude >/dev/null
sudo chmod +x /usr/local/bin/claude
fi
# Report, do not fail. Check 19 runs its own control and skips with a reason when the
# validator is unusable, so an unavailable CLI degrades to a visible skip in the gate
# output rather than a red run over something this repo does not control.
claude --version || echo "::warning::claude CLI unusable on this runner; check 19 will skip"
- name: Run the verification gate
run: ./.claude/verify.sh
# A gate that cannot fail proves nothing. This used to break one skill description and
# call it done, which left the other checks unproven — and two of them were not enforcing
# anything at the time. Every declared check now gets its own mutation, and check 16 of
# the gate fails if a check is added without one.
- name: Prove every check is falsifiable
run: ./tests/gate-falsifiability.sh
- name: Confirm the tree is restored
run: git diff --exit-code
# Everything above runs the gate. This installs for real, on Linux, into a disposable HOME.
# It exists because the worst bug this repo has shipped was Linux-only and invisible on the
# machine it was written on: hooks called /usr/bin/jq by absolute path, which is a macOS
# path, so off macOS the verify gate silently stopped blocking and the session hook injected
# nothing at all. A dry run cannot catch that. Only installing and firing the hooks can.
install-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
# Runs install.sh into throwaway HOMEs and asserts the resulting tree each time: a plain
# install, CLAUDE_CONFIG_DIR pointed elsewhere, a home path with a space, jq removed from
# PATH, a bash user, a second run for idempotency, both uninstall paths, and an install
# over a home that already holds somebody else's skills, settings and MCP servers. Two
# further cases exercise the curl bootstrap and the plugin marketplace against the
# published repo. This runner is the environment the matrix exists for — bash by default,
# no Homebrew, the shape of a cloud VM or a container.
- name: Install matrix (all environments, plus the network lanes)
run: ./tests/install-matrix.sh
- name: install.sh, for real
run: ./install.sh
- name: The installed tree matches the repo
run: ./bin/doctor --drift
# Expected counts are derived from the repo at run time, not hardcoded — a literal here
# went stale the first time a skill was added and failed a green tree.
- name: Everything landed
run: |
set -e
count() { ls -d $1 2>/dev/null | wc -l | tr -d ' '; }
for spec in "$HOME/.claude/skills/*/:claude/skills/*/" \
"$HOME/.claude/agents/*.md:claude/agents/*.md" \
"$HOME/.claude/commands/*.md:claude/commands/*.md" \
"$HOME/.claude/hooks/*.sh:claude/hooks/*.sh"; do
pat="${spec%:*}"; src="${spec##*:}"
got=$(count "$pat"); want=$(count "$src")
[ "$got" = "$want" ] && [ "$want" != 0 ] || { echo "$pat: got $got, repo has $want"; exit 1; }
echo "ok $pat -> $got (repo: $want)"
done
test -x "$HOME/.claude/hooks/verify-gate.sh"
- name: The Stop gate blocks on Linux, with and without jq on PATH
run: |
set -e
mkdir -p /tmp/g/repo/.claude /tmp/g/home/.config/agents /tmp/g/tmp /tmp/g/bin
printf '#!/usr/bin/env bash\necho "seeded failure"\nexit 1\n' > /tmp/g/repo/.claude/verify.sh
chmod +x /tmp/g/repo/.claude/verify.sh
printf '%s %s\n' "$(sha256sum /tmp/g/repo/.claude/verify.sh | cut -d' ' -f1)" \
/tmp/g/repo/.claude/verify.sh > /tmp/g/home/.config/agents/verify-trust
for t in bash sh cat cut grep sed awk tr rm mkdir env dirname basename sha256sum; do
ln -sf "$(command -v $t)" /tmp/g/bin/$t
done
out=$(printf '{"session_id":"ci"}' | env HOME=/tmp/g/home TMPDIR=/tmp/g/tmp \
CLAUDE_PROJECT_DIR=/tmp/g/repo bash claude/hooks/verify-gate.sh)
echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block with jq"; exit 1; }
echo "ok blocks with jq"
sed 's#/usr/bin/jq#/nonexistent/jq#' claude/hooks/verify-gate.sh > /tmp/g/nojq.sh
out=$(printf '{"session_id":"ci2"}' | env PATH=/tmp/g/bin HOME=/tmp/g/home \
TMPDIR=/tmp/g/tmp CLAUDE_PROJECT_DIR=/tmp/g/repo bash /tmp/g/nojq.sh)
echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block without jq"; exit 1; }
echo "ok blocks with no jq reachable"
- name: The session hook injects the routing block on Linux
run: |
set -e
n=$(printf '{"hook_event_name":"SessionStart"}' | bash claude/hooks/inject-session-context.sh | wc -c)
[ "$n" -gt 2000 ] || { echo "session hook injected only $n bytes"; exit 1; }
echo "ok session hook injected $n bytes"
- name: uninstall.sh --dry-run
run: ./uninstall.sh --dry-run
# macOS is the platform this repo calls primary, and until now CI never touched it. Every
# macOS-only assumption — BSD sed and find, `shasum` instead of `sha256sum`, `stat -f` — was
# only ever exercised on the author's laptop, which is the definition of works-on-my-machine.
install-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install matrix on macOS
run: ./tests/install-matrix.sh
- name: Run the verification gate
run: ./.claude/verify.sh
# Windows via Git Bash, which is what `shell: bash` selects on a windows runner and what a
# Windows user running these scripts actually has. Native PowerShell is not a target and the
# README says so; this proves the Git Bash and WSL-shaped path, which is the one people use.
#
# It gates like every other platform. It started as continue-on-error because Windows had
# never been supported and the job existed to report the truth rather than block merges; it
# went green in three commits, so the crutch is gone.
install-windows:
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: What this runner actually has
run: |
bash --version | head -1
uname -s
for t in jq git sed find cp chmod mktemp shasum sha256sum; do
printf '%-12s %s\n' "$t" "$(command -v $t || echo MISSING)"
done
- name: Install matrix on Windows (Git Bash)
run: ./tests/install-matrix.sh
- name: Run the verification gate
run: ./.claude/verify.sh
# Alpine, because BusyBox is not GNU. Its sed, grep, find and stat take different flags, and
# there is no bash until you install it — which is exactly the shape of a slim container
# image. Nothing else in this workflow would catch a GNU-ism.
install-alpine:
runs-on: ubuntu-latest
container: alpine:latest
steps:
- name: Tools a slim image does not ship
run: apk add --no-cache bash git jq curl findutils coreutils grep sed nodejs npm
- uses: actions/checkout@v4
- name: What this container actually has
run: |
bash --version | head -1
for t in jq git sed find cp chmod mktemp sha256sum shasum; do
printf '%-12s %s\n' "$t" "$(command -v $t || echo MISSING)"
done
- name: Install matrix on Alpine
run: bash ./tests/install-matrix.sh
- name: Run the verification gate
run: bash ./.claude/verify.sh