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
45 changes: 25 additions & 20 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ wads/
1. **`populate my-project`** reads the template at `wads/data/pyproject_toml_tpl.toml`
2. Loads it as TOML, merges user-provided values (name, description, author, license, etc.)
3. Writes the resulting `pyproject.toml` with Hatchling build system
4. Copies `wads/data/github_ci_uv_stub.yml` → `.github/workflows/ci.yml` (5-line stub
that calls the reusable workflow in `i2mint/wads/.github/workflows/uv-ci.yml@master`).
4. Renders `wads/data/github_ci_uv_stub.yml` → `.github/workflows/ci.yml` (5-line stub
that calls the reusable workflow in `i2mint/wads/.github/workflows/uv-ci.yml@master`),
switching its secrets block to the named transport (see "Secrets" below).
For repos that need to customize CI beyond `[tool.wads.ci.*]`, drop the stub and
copy `wads/data/github_ci_uv.yml` inline instead.
5. Creates README.md, LICENSE, .gitignore, .gitattributes, .editorconfig, package dir
Expand All @@ -101,7 +102,7 @@ consumer on their next CI run — no per-repo edit, no `wads-migrate` sweep.
| Tradeoff | Pin strategy |
|---|---|
| Float with wads (default) | `@master` — convenient; bad wads merge breaks CI everywhere on next run, but never reaches PyPI (publish is gated on workflow success) |
| Freeze | `@0.2.15` (or any later tag; no `v` prefix) — set via `wads-migrate ci-to-stub --pin @0.2.15`; the repo only picks up wads updates when re-pinned. A JSON-transport stub needs a tag whose uv-ci.yml declares `WADS_CI_SECRETS_JSON` (releases after 0.2.14); older pins need `--transport named` |
| Freeze | `@0.2.15` (or any later tag; no `v` prefix) — set via `wads-migrate ci-to-stub --pin @0.2.15`; the repo only picks up wads updates when re-pinned. A named-transport stub (the default) works with any tag; a JSON-transport stub needs a tag whose uv-ci.yml declares `WADS_CI_SECRETS_JSON` (releases after 0.2.14) |

The "CI failure ≠ broken release" property is what makes `@master` safe by
default: a botched wads update blocks publication of all downstream packages
Expand All @@ -119,19 +120,22 @@ A reusable workflow's secret *interface* (`on.workflow_call.secrets`) must be
static YAML and `secrets: inherit` is unreliable cross-owner, so secrets are
handled in two decoupled layers:

1. **Transport** — the modern stub passes ONE statically-declared secret,
`WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}` — the caller's whole
secrets context, double-encoded so the value is single-line (a multiline
secret is masked per line, and the pretty-printed `{`/`}` lines would
become global masks mangling every brace in the log). Any secret name a
repo has reaches the workflow — nothing to enumerate, nothing to fall
outside of. The 62-name superset (`wads.ci_secrets.DEFAULT_CI_SECRETS`) is
still declared in `uv-ci.yml` but **frozen**: it exists only so old
named-transport stubs keep working (a test in `test_ci_secrets.py` pins the
YAML to `WORKFLOW_CALL_SECRETS` = JSON secret + superset). Named transport
remains available for minimal-secret-surface repos via
`wads-migrate ci-to-stub --transport named`, which warns loudly on
out-of-superset names (they make the workflow unstartable — issue #63).
1. **Transport** — every stub wads *writes* (`populate`, `wads-migrate
ci-to-stub` on an inline workflow) passes secrets **by name**:
`PYPI_PASSWORD` plus the backing secret of each `[tool.wads.ci.env]` var
(`wads.ci_secrets.stub_with_named_transport`). Names must be in the frozen
62-name superset (`wads.ci_secrets.DEFAULT_CI_SECRETS`, declared in
`uv-ci.yml`; a test in `test_ci_secrets.py` pins the YAML to
`WORKFLOW_CALL_SECRETS` = JSON secret + superset), or the workflow is
unstartable (issue #63), so the CLIs warn loudly on out-of-superset names.
The **opt-in** JSON transport (`--transport json`) passes ONE secret,
`WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}` — the whole secrets
context, double-encoded so the value is single-line (a multiline secret is
masked per line). Any name works, but GitHub's malicious-workflow scanner
holds its runs on NEW repos (`action_required`, zero jobs, no log; issues
#74, #88), which is why it stopped being the default. Re-rendering an
existing stub (`migrate_ci_to_stub(path)`, `ci-to-stub`, `ci-on-demand`,
`fleet-stub`) keeps the transport it already has, with one exception: when no `--transport` is given and a named stub would pass a name outside the superset (so it could never start), the JSON transport is used instead, with a note on stderr.
2. **Env-assignment** — *which* values become job env vars (and which are
required) is driven entirely by `[tool.wads.ci.env]` (`required_envvars`,
`test_envvars`, `extra_envvars`, `defaults`, and `secret_aliases` for
Expand All @@ -148,7 +152,7 @@ handled in two decoupled layers:
transport feeds it.

To use a secret: `wads-secrets add VAR_NAME [SECRET_NAME]` updates pyproject
(and, on legacy named stubs, the stub's pass-through) and can `gh secret set`
(and, on named-transport stubs, the stub's pass-through) and can `gh secret set`
the value. For non-sensitive values use `wads-secrets add NAME --variable`
(repo variable; no transport, no masking) or put a literal in
`[tool.wads.ci.env].defaults`.
Expand Down Expand Up @@ -298,15 +302,16 @@ wads-test-analyze results.xml
## Testing

```bash
pytest wads/tests/
uv pip install -e ".[create,docs,skills,test]" # the suite needs the create extra
python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' --ignore=examples --ignore=scrap
```

Tests are in `wads/tests/` (not the top-level `tests/` directory).
That is CI's exact command. With no path, `testpaths = ["wads"]` collects the tests in `wads/tests/` (not a top-level `tests/`) plus every package doctest; the root `conftest.py` keeps the `wads/data` templates out. The `-o` flag replaces the repo's `NORMALIZE_WHITESPACE`, so a wrapped doctest output needs an inline `# doctest: +NORMALIZE_WHITESPACE`. CI tests 3.10 and 3.12; run 3.11 too before merging. The maintainer skill `skills/wads-dev-workflow` (linked from `.claude/skills/`) has the full checklist: goldens, the push-back test harness, what must change together, and the dependent (`i2mint/isee`) gate.

## Common Pitfalls

- The `[tool.wads.ci]` section is **not** standard TOML metadata - it's wads-specific
- `testpaths` in the template defaults to `["tests"]` but wads itself uses `["wads/tests"]`
- `testpaths` in the template defaults to `["tests"]`; wads itself uses `["wads"]` so CI (which runs pytest with no path) also collects the package doctests (#56). The root `conftest.py` keeps `wads/data` templates out of collection
- The CI workflow uses `i2mint/wads/actions/*@master` - these must be on GitHub
- System deps in `[tool.wads.ops.*]` only run in CI, not locally
- Version bumping happens automatically in the publish job on main/master
1 change: 1 addition & 0 deletions .claude/skills/setup-py-project
1 change: 1 addition & 0 deletions .claude/skills/wads-dev-workflow
1 change: 1 addition & 0 deletions .claude/skills/wads-migrate
120 changes: 68 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
# wads

Modern Python project packaging and CI/CD tools for developers who want to focus on code, not configuration.
wads creates, configures and publishes Python packages: a new project gets a Hatchling `pyproject.toml` and a five-line GitHub Actions stub that calls one shared reusable workflow, and every CI setting lives in `[tool.wads.ci]`. It also migrates legacy `setup.cfg` projects, wires CI secrets, and diagnoses CI failures.

*Still typing code with your own fingers? Skip to [For carbon-based contributors](#for-carbon-based-contributors).*

[![PyPI version](https://img.shields.io/pypi/v/wads.svg)](https://pypi.org/project/wads/)
[![Python versions](https://img.shields.io/pypi/pyversions/wads.svg)](https://pypi.org/project/wads/)

## For AI agents

**Skills.** Twelve agent skills ship inside the package, in `wads/data/skills/`: `setup-py-project`, `wads-migrate`, `wads-repo-doctor`, `wads-ci-health`, `wads-changelog`, `wads-docs-coverage`, `wads-docstring-render`, `wads-import-time`, `wads-pypi-polish`, `wads-skillify`, `wads-test-coverage` and `wads-type-coverage`. Enable them either way:

```bash
pip install "wads[create]" && wads-install-skills # symlinks them into ~/.claude/skills/
gh skill install i2mint/wads wads-repo-doctor # one skill, from GitHub
```

`wads-install-skills --list` prints the available names. In a clone of this repo, `.claude/skills/` links every skill for Claude Code, plus the maintainer skill `wads-dev-workflow` (in `skills/`), which covers changing wads itself.

**Project instructions.** [`.claude/CLAUDE.md`](.claude/CLAUDE.md) explains the architecture (pyproject as the single source of truth, the reusable workflow, the two-layer secrets model), the config sections, and the conventions.

**What an agent can do with wads:**

- Scaffold a package with `populate`, or a whole repo with the `setup-py-project` skill.
- Move a legacy repo to `pyproject.toml` and the CI stub with `wads-migrate`.
- Declare CI secrets and env vars with `wads-secrets add NAME`.
- Read or render a repo's CI configuration from Python, as below.
- Run the CI plan locally with `wads ci-local`, and audit dependency licences with `wads-licence-check`.
- Diagnose a failed run with `wads-ci-debug owner/repo`.

A minimal example, runnable with only the light core (`pip install wads`):

```python
from wads.ci_config import CIConfig
from wads.migration import migrate_ci_to_stub

config = CIConfig(
{
"project": {"name": "mypkg", "optional-dependencies": {"dev": ["httpx"]}},
"tool": {"wads": {"ci": {"testing": {"python_versions": ["3.12"]}}}},
}
)
assert config.python_versions == ["3.12"]
assert config.project_name == "mypkg"
# A dev extra CI would never install (no [tool.wads.ci.install].extras):
assert config.uninstalled_test_extras == {"dev": ["httpx"]}

stub = migrate_ci_to_stub() # the ci.yml a new repo gets
assert "uses: i2mint/wads/.github/workflows/uv-ci.yml@master" in stub
assert "PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}" in stub
```

## What is Wads?

Wads helps you:
Expand Down Expand Up @@ -136,26 +182,9 @@ wads-secrets add TEST_LEVEL --variable # non-sensitive value -> repo variab
wads-secrets list # show what's configured
```

`wads-secrets add` (a) records the variable in `[tool.wads.ci.env]` and (b)
runs `gh secret set` (or `gh variable set` with `--variable`) if `gh` is
installed (value taken from `$VAR_NAME` or `--value`). Under the hood there
are two layers: a **transport** — the stub passes your repo's whole secrets
context to the reusable workflow as one `WADS_CI_SECRETS_JSON` secret, so any
secret name works — and an **env policy** (`[tool.wads.ci.env]` —
`required_envvars` / `test_envvars` / `extra_envvars` / `defaults` /
`secret_aliases`) that decides which values become job env vars. Each declared
name resolves against secrets first, then repository *variables* (the right
home for non-sensitive values); committed constants can go straight into
`[tool.wads.ci.env].defaults`. A `required` name that resolves to nothing
fails the build; an undeclared secret is never written to the environment.
Note the JSON transport hands **every** secret the repo can read — including
org-level ones — to the reusable workflow (which only exports the declared
ones). If you want the workflow to receive *only* the names you list, use
`wads-migrate ci-to-stub --transport named` — that mode is limited to the
frozen superset in `wads.ci_secrets.DEFAULT_CI_SECRETS`, and is also the
right choice for orgs with very large shared secrets (the serialized context
must fit in one secret value). Older stubs pass secrets by name the same way;
regenerate with `wads-migrate ci-to-stub` to switch to the JSON transport.
`wads-secrets add` (a) records the variable in `[tool.wads.ci.env]`, (b) adds its secret to the stub's `secrets:` list, and (c) runs `gh secret set` (or `gh variable set` with `--variable`) if `gh` is installed (value taken from `$VAR_NAME` or `--value`). Under the hood there are two layers: a **transport**, where the stub passes secrets to the reusable workflow by name (`PYPI_PASSWORD` plus each declared one), and an **env policy** (`[tool.wads.ci.env]`: `required_envvars` / `test_envvars` / `extra_envvars` / `defaults` / `secret_aliases`) that decides which values become job env vars. Each declared name resolves against secrets first, then repository *variables* (the right home for non-sensitive values); committed constants can go straight into `[tool.wads.ci.env].defaults`. A `required` name that resolves to nothing fails the build; an undeclared secret is never written to the environment.

Named secrets must be in the frozen superset in `wads.ci_secrets.DEFAULT_CI_SECRETS`, or GitHub rejects the workflow at parse time; `wads-secrets` and `wads-migrate` warn about such names. The opt-in alternative, `wads-migrate ci-to-stub --transport json`, passes the repo's whole secrets context as one `WADS_CI_SECRETS_JSON` secret, so any name works. It is not the default because GitHub's malicious-workflow scanner holds its runs on new repositories: every run ends `action_required` with zero jobs and no log ([#74](https://github.com/i2mint/wads/issues/74)). Re-rendering an existing stub keeps whichever transport it already uses.

### Declare System Dependencies

Expand Down Expand Up @@ -563,37 +592,14 @@ note = "On Alpine: apk add unixodbc unixodbc-dev"
alternatives = ["iodbc"]
```

See [docs/SYSTEM_DEPENDENCIES.md](docs/SYSTEM_DEPENDENCIES.md) for comprehensive examples.

## Claude Code Skills

Wads ships with [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills for AI-assisted workflows. Install them globally so they're available in every project:

```bash
wads-install-skills
```

This symlinks skills to `~/.claude/skills/`, so they stay in sync when wads is updated:

| Command | Description |
|---------|-------------|
| `/setup-py-project` | AI-guided Python project creation: name suggestions, PyPI/GitHub availability checking, repo creation, file population |
| `/wads-migrate` | Migrate projects to modern wads setup (pyproject.toml + uv CI) |

**Example:**
```
/setup-py-project "a tool for audio signal processing"
```

To list available skills without installing: `wads-install-skills --list`
To update existing skills: `wads-install-skills --force`
See [misc/docs/SYSTEM_DEPENDENCIES.md](misc/docs/SYSTEM_DEPENDENCIES.md) for comprehensive examples.

## Documentation

- **[System Dependencies Guide](misc/docs/SYSTEM_DEPENDENCIES.md)** - `[tool.wads.ops.*]` format and examples
- **[Migration Guide](misc/docs/MIGRATION.md)** - Migrate from setup.cfg to pyproject.toml
- **[Utilities Reference](misc/docs/UTILITIES.md)** - CLI tools (`wads-ci-debug`, `wads-migrate`)
- **[CLAUDE.md](CLAUDE.md)** - AI agent guide for working with this project
- **[.claude/CLAUDE.md](.claude/CLAUDE.md)** - AI agent guide for working with this project

## Troubleshooting

Expand Down Expand Up @@ -630,21 +636,31 @@ Common issues:
- Python version incompatibilities → Check `python_versions` in `[tool.wads.ci.testing]`
- Test failures → Review generated fix instructions

## Development
## For carbon-based contributors

Everything above is for users of wads, human or not. This part is for working on wads itself.

### Running Tests
**Dev setup.** The test suite scaffolds and builds packages, so it needs the `create` extra:

```bash
pytest wads/tests/
uv venv && . .venv/bin/activate
uv pip install -e ".[create,docs,skills,test]"
```

### Building Documentation
**Run the tests the way CI does.** CI calls pytest with no path, so `testpaths = ["wads"]` collects both `wads/tests` and every doctest in the package:

```bash
pip install -e ".[docs]"
epythet build
python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' --ignore=examples --ignore=scrap
```

CI tests Python 3.10 and 3.12; run 3.11 as well before merging, because a dataclass default once broke there and nowhere else ([#100](https://github.com/i2mint/wads/issues/100)).

**Build the docs** with `pip install -e ".[docs]"` and then `epythet build`.

**Why it is built this way.** wads is a foundation package. Its reusable workflow and actions run from `@master` in every wads-managed repo, and every merge to `master` publishes a release to PyPI. So changes to `.github/workflows/uv-ci.yml`, `actions/*` or the stub template are fleet-wide changes: they come with tests (the push-back script, for example, is exercised against real throwaway git repos), and the populate output is pinned by golden files. [`.claude/CLAUDE.md`](.claude/CLAUDE.md) has the design rationale, and the `wads-dev-workflow` skill has the maintainer checklist.

**Contributing and questions.** Open an issue or a pull request at [github.com/i2mint/wads](https://github.com/i2mint/wads/issues).

## License

Apache Software License 2.0
Expand Down
Loading
Loading