Skip to content
Open
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
5 changes: 3 additions & 2 deletions .config/wt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ url = "http://localhost:{{ branch | hash_port }}"
[[pre-remove]]
server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/dev/null || true"

# The Python package lives under generator/; this alias runs pytest from the right cwd.
# Tests live in four places (generator/, proxy/, the install-tend scripts,
# worker/); dev/test.sh runs the lot, mirroring ci.yaml's test jobs.
[aliases]
test = "cd generator && uv run pytest {{ args }}"
test = "dev/test.sh {{ args }}"
9 changes: 5 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ repos:
hooks:
- id: actionlint
# The composite actions' shared step bodies live in standalone scripts
# (shared/steps/, proxy/) rather than inline `run:` blocks, so
# actionlint's built-in shellcheck doesn't see them. Run shellcheck directly
# to keep that coverage. -S warning matches actionlint's default severity.
# (shared/steps/, proxy/, generator/src/tend/templates/) rather than inline
# `run:` blocks, so actionlint's built-in shellcheck doesn't see them. Run
# shellcheck directly to keep that coverage — and over dev/, which actionlint
# never sees at all. -S warning matches actionlint's default severity.
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
args: ["-S", "warning"]
files: ^(shared/steps/.*\.sh|generator/src/tend/templates/.*\.sh|proxy/setup-sandbox\.sh)$
files: ^(shared/steps/.*\.sh|generator/src/tend/templates/.*\.sh|proxy/setup-sandbox\.sh|dev/.*\.sh)$
- repo: local
hooks:
# The Claude Code slash-command preprocessor treats a backticked token
Expand Down
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ completely — old formats should fail with a clear error, not silently parse.
## Commands

```bash
wt test # run pytest in generator/
wt test # every suite (generator/, proxy/, install-tend scripts, worker/)
uvx tend@latest init # regenerate workflows from .config/tend.yaml
uvx tend@latest init --dry-run # preview without writing
uvx tend@latest check # verify branch protection, secrets, bot access
pre-commit run --all-files # lint: ruff, typos, actionlint, uv-lock
pre-commit run --all-files # lint: ruff, typos, actionlint, shellcheck, uv-lock
```

`wt test` runs [`dev/test.sh`](dev/test.sh), which mirrors the test jobs in
`ci.yaml`; arguments go to the pytest suites (`wt test -k render`).

## Architecture

Four pieces:
Expand Down Expand Up @@ -106,8 +109,8 @@ from `.config/tend.yaml`. Edit the generator or config, not the workflow files
directly.

The generator is a Python package under `generator/` — uses the uv_build
backend, requires Python 3.11+. Runtime dependencies: click, ruamel.yaml.
Dev dependencies: pytest, pytest-regtest.
backend, requires Python 3.11+. Runtime dependencies: click, jinja2,
ruamel.yaml. Dev dependencies: pytest, pytest-regtest.

Consuming repos regenerate their `tend-*.yaml` workflows nightly (tend itself
included — it dogfoods its own workflows). Changes to the generator do not
Expand Down
65 changes: 65 additions & 0 deletions dev/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Every test suite in the repo, mirroring the test jobs in
# .github/workflows/ci.yaml. `wt test` runs this (see .config/wt.toml), so one
# command covers generator/, proxy/, the install-tend scripts, and worker/.
#
# Arguments are forwarded to the pytest suites (`wt test -k render`); a filtered
# run skips worker/, whose vitest CLI takes different flags. Every suite runs
# even if an earlier one fails, and the failures are listed at the end.
set -o pipefail

cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1

failed=()

# suite <dir> <cmd>...
suite() {
local dir=$1 rc=0
shift
printf '\n==> %s: %s\n' "$dir" "$*"
(cd "$dir" && "$@") || rc=$?
# A filtered run gets slack: pytest exits 5 for "no tests collected" and 4 for
# a path that only exists in another suite. generator/ stays strict, so a bad
# flag — 4 everywhere — still fails the run.
if [ ${#pytest_args[@]} -gt 0 ] &&
{ [ "$rc" -eq 5 ] || { [ "$rc" -eq 4 ] && [ "$dir" != generator ]; }; }; then
rc=0
fi
if [ "$rc" -ne 0 ]; then failed+=("$dir"); fi
}

pytest_args=("$@")

suite generator uv run pytest "${pytest_args[@]}"

# The proxy addon isn't part of the generator package, and it imports
# mitmproxy.test, so it runs standalone against the version production runs
# rather than whatever mitmproxy is latest.
if mitmproxy_version=$(yq -e '.inputs.mitmproxy_version.default' claude/action.yaml); then
suite proxy uv run --no-project --with pytest \
--with "mitmproxy==$mitmproxy_version" pytest "${pytest_args[@]}"
else
echo "==> proxy: cannot read mitmproxy_version from claude/action.yaml (yq installed?)" >&2
failed+=(proxy)
fi

suite plugins/install-tend/skills/install-tend/scripts \
uv run --no-project --with pytest pytest "${pytest_args[@]}"

if [ ${#pytest_args[@]} -eq 0 ]; then
# Install when the tree is missing or older than the lockfile (`npm ci` writes
# node_modules/.package-lock.json), and with `npm ci` rather than `npm install`
# — an older local npm reruns resolution and rewrites package-lock.json,
# leaving churn in the diff that has nothing to do with the change under test.
if [ ! -d worker/node_modules ] ||
[ worker/package-lock.json -nt worker/node_modules/.package-lock.json ]; then
suite worker npm ci --prefer-offline --no-audit --no-fund
fi
suite worker npm run typecheck
suite worker npm test
fi

if [ ${#failed[@]} -gt 0 ]; then
printf '\nfailed: %s\n' "${failed[*]}" >&2
exit 1
fi
Loading