weld-cli 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weld_cli-0.6.0/.claude/docs/DOCS_BEST_PRACTICES.md +209 -0
- weld_cli-0.6.0/.claude/docs/PYTHON_BEST_PRACTICES.md +215 -0
- weld_cli-0.6.0/.claude/docs/RELEASE_WORKFLOW.md +272 -0
- weld_cli-0.6.0/.claude/docs/SPEC.md +1228 -0
- weld_cli-0.6.0/.github/weld-logo.png +0 -0
- weld_cli-0.6.0/.github/workflows/ci.yml +59 -0
- weld_cli-0.6.0/.github/workflows/docs.yml +68 -0
- weld_cli-0.6.0/.github/workflows/release.yml +91 -0
- weld_cli-0.6.0/.gitignore +57 -0
- weld_cli-0.6.0/.pre-commit-config.yaml +29 -0
- weld_cli-0.6.0/.python-version +1 -0
- weld_cli-0.6.0/.secrets.baseline +127 -0
- weld_cli-0.6.0/CHANGELOG.md +312 -0
- weld_cli-0.6.0/CLAUDE.md +221 -0
- weld_cli-0.6.0/LICENSE +190 -0
- weld_cli-0.6.0/Makefile +351 -0
- weld_cli-0.6.0/PKG-INFO +84 -0
- weld_cli-0.6.0/README.md +52 -0
- weld_cli-0.6.0/RELEASE_NOTES.md +19 -0
- weld_cli-0.6.0/docs/assets/weld-logo.png +0 -0
- weld_cli-0.6.0/docs/commands/commit.md +88 -0
- weld_cli-0.6.0/docs/commands/discover.md +69 -0
- weld_cli-0.6.0/docs/commands/doctor.md +53 -0
- weld_cli-0.6.0/docs/commands/implement.md +231 -0
- weld_cli-0.6.0/docs/commands/index.md +57 -0
- weld_cli-0.6.0/docs/commands/init.md +44 -0
- weld_cli-0.6.0/docs/commands/interview.md +57 -0
- weld_cli-0.6.0/docs/commands/plan.md +61 -0
- weld_cli-0.6.0/docs/commands/research.md +66 -0
- weld_cli-0.6.0/docs/commands/review.md +91 -0
- weld_cli-0.6.0/docs/configuration.md +111 -0
- weld_cli-0.6.0/docs/development/architecture.md +127 -0
- weld_cli-0.6.0/docs/development/contributing.md +92 -0
- weld_cli-0.6.0/docs/development/index.md +90 -0
- weld_cli-0.6.0/docs/index.md +52 -0
- weld_cli-0.6.0/docs/installation.md +84 -0
- weld_cli-0.6.0/docs/overrides/.gitkeep +3 -0
- weld_cli-0.6.0/docs/quickstart.md +74 -0
- weld_cli-0.6.0/docs/reference/exit-codes.md +70 -0
- weld_cli-0.6.0/docs/reference/plan-format.md +122 -0
- weld_cli-0.6.0/docs/stylesheets/extra.css +51 -0
- weld_cli-0.6.0/docs/troubleshooting.md +140 -0
- weld_cli-0.6.0/docs/workflow.md +125 -0
- weld_cli-0.6.0/mkdocs.yml +167 -0
- weld_cli-0.6.0/pyproject.toml +112 -0
- weld_cli-0.6.0/scripts/assert_unreleased_empty.py +20 -0
- weld_cli-0.6.0/scripts/extract_release_notes.py +27 -0
- weld_cli-0.6.0/src/weld/__init__.py +3 -0
- weld_cli-0.6.0/src/weld/__main__.py +6 -0
- weld_cli-0.6.0/src/weld/cli.py +135 -0
- weld_cli-0.6.0/src/weld/commands/__init__.py +17 -0
- weld_cli-0.6.0/src/weld/commands/commit.py +821 -0
- weld_cli-0.6.0/src/weld/commands/discover.py +147 -0
- weld_cli-0.6.0/src/weld/commands/doc_review.py +439 -0
- weld_cli-0.6.0/src/weld/commands/doctor.py +88 -0
- weld_cli-0.6.0/src/weld/commands/implement.py +869 -0
- weld_cli-0.6.0/src/weld/commands/init.py +127 -0
- weld_cli-0.6.0/src/weld/commands/interview.py +68 -0
- weld_cli-0.6.0/src/weld/commands/plan.py +293 -0
- weld_cli-0.6.0/src/weld/commands/research.py +182 -0
- weld_cli-0.6.0/src/weld/config.py +322 -0
- weld_cli-0.6.0/src/weld/constants.py +34 -0
- weld_cli-0.6.0/src/weld/core/__init__.py +60 -0
- weld_cli-0.6.0/src/weld/core/discover_engine.py +196 -0
- weld_cli-0.6.0/src/weld/core/doc_review_engine.py +502 -0
- weld_cli-0.6.0/src/weld/core/history.py +112 -0
- weld_cli-0.6.0/src/weld/core/interview_engine.py +106 -0
- weld_cli-0.6.0/src/weld/core/plan_parser.py +342 -0
- weld_cli-0.6.0/src/weld/core/weld_dir.py +36 -0
- weld_cli-0.6.0/src/weld/logging.py +116 -0
- weld_cli-0.6.0/src/weld/models/__init__.py +24 -0
- weld_cli-0.6.0/src/weld/models/discover.py +30 -0
- weld_cli-0.6.0/src/weld/models/issues.py +54 -0
- weld_cli-0.6.0/src/weld/models/session.py +55 -0
- weld_cli-0.6.0/src/weld/output.py +94 -0
- weld_cli-0.6.0/src/weld/py.typed +2 -0
- weld_cli-0.6.0/src/weld/services/__init__.py +125 -0
- weld_cli-0.6.0/src/weld/services/claude.py +346 -0
- weld_cli-0.6.0/src/weld/services/filesystem.py +84 -0
- weld_cli-0.6.0/src/weld/services/gist_uploader.py +123 -0
- weld_cli-0.6.0/src/weld/services/git.py +222 -0
- weld_cli-0.6.0/src/weld/services/session_detector.py +100 -0
- weld_cli-0.6.0/src/weld/services/session_tracker.py +353 -0
- weld_cli-0.6.0/src/weld/services/transcript_renderer.py +194 -0
- weld_cli-0.6.0/src/weld/services/transcripts.py +91 -0
- weld_cli-0.6.0/src/weld/validation.py +54 -0
- weld_cli-0.6.0/tests/__init__.py +1 -0
- weld_cli-0.6.0/tests/conftest.py +98 -0
- weld_cli-0.6.0/tests/e2e_test.sh +312 -0
- weld_cli-0.6.0/tests/test_claude.py +514 -0
- weld_cli-0.6.0/tests/test_cli.py +1074 -0
- weld_cli-0.6.0/tests/test_commit.py +812 -0
- weld_cli-0.6.0/tests/test_commit_e2e.py +180 -0
- weld_cli-0.6.0/tests/test_config.py +478 -0
- weld_cli-0.6.0/tests/test_discover.py +147 -0
- weld_cli-0.6.0/tests/test_doc_review.py +779 -0
- weld_cli-0.6.0/tests/test_filesystem.py +139 -0
- weld_cli-0.6.0/tests/test_gist_uploader.py +341 -0
- weld_cli-0.6.0/tests/test_git.py +356 -0
- weld_cli-0.6.0/tests/test_history.py +370 -0
- weld_cli-0.6.0/tests/test_implement.py +750 -0
- weld_cli-0.6.0/tests/test_interview.py +138 -0
- weld_cli-0.6.0/tests/test_logging.py +131 -0
- weld_cli-0.6.0/tests/test_output.py +265 -0
- weld_cli-0.6.0/tests/test_plan.py +176 -0
- weld_cli-0.6.0/tests/test_plan_parser.py +442 -0
- weld_cli-0.6.0/tests/test_research.py +107 -0
- weld_cli-0.6.0/tests/test_session_detector.py +195 -0
- weld_cli-0.6.0/tests/test_session_models.py +196 -0
- weld_cli-0.6.0/tests/test_session_tracker.py +772 -0
- weld_cli-0.6.0/tests/test_transcript_renderer.py +431 -0
- weld_cli-0.6.0/tests/test_transcripts.py +146 -0
- weld_cli-0.6.0/tests/test_validation.py +71 -0
- weld_cli-0.6.0/tests/test_weld_dir.py +82 -0
- weld_cli-0.6.0/uv.lock +1427 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
## Best practices for user docs for a Python CLI tool (OSS-grade)
|
|
2
|
+
|
|
3
|
+
### 1) Define your doc set (minimum viable, then scale)
|
|
4
|
+
|
|
5
|
+
**Minimum**
|
|
6
|
+
|
|
7
|
+
* **README.md**: fast onboarding + core commands.
|
|
8
|
+
* **`mycli --help`**: authoritative CLI reference for flags/args.
|
|
9
|
+
* **Command reference** (docs site or `/docs`): one page per command + examples.
|
|
10
|
+
|
|
11
|
+
**Common additions**
|
|
12
|
+
|
|
13
|
+
* **Installation**: pipx / uv tool install / pip / brew (if you ship it).
|
|
14
|
+
* **Configuration**: file format, env vars, precedence, locations.
|
|
15
|
+
* **Tutorials**: 2–3 end-to-end workflows users actually do.
|
|
16
|
+
* **How-to**: targeted tasks (“use dry-run”, “debug logging”, “CI integration”).
|
|
17
|
+
* **Troubleshooting**: known errors, exit codes, common gotchas.
|
|
18
|
+
* **FAQ**: concise answers, links to deeper docs.
|
|
19
|
+
* **Changelog** + **Upgrade notes**: breaking changes and migration steps.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### 2) Optimize for “first success in 2 minutes”
|
|
24
|
+
|
|
25
|
+
Your README should contain, in this order:
|
|
26
|
+
|
|
27
|
+
1. **One-sentence value** (what it does, who for).
|
|
28
|
+
2. **Install** (one preferred method, others collapsed).
|
|
29
|
+
3. **Quick start**: 3–5 commands that produce a visible result.
|
|
30
|
+
4. **Common workflows**: 2–4 examples.
|
|
31
|
+
5. **Configuration**: where config lives + minimal example.
|
|
32
|
+
6. **Support**: how to file issues, debug info to include.
|
|
33
|
+
|
|
34
|
+
Pattern for quick start:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv tool install mycli
|
|
38
|
+
mycli init
|
|
39
|
+
mycli run plan.md
|
|
40
|
+
mycli status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### 3) Make the CLI self-documenting
|
|
46
|
+
|
|
47
|
+
**`--help` quality is part of docs quality.**
|
|
48
|
+
|
|
49
|
+
Best practices:
|
|
50
|
+
|
|
51
|
+
* Every command has:
|
|
52
|
+
|
|
53
|
+
* short summary
|
|
54
|
+
* longer help text
|
|
55
|
+
* examples (even 1 is enough)
|
|
56
|
+
* Flags:
|
|
57
|
+
|
|
58
|
+
* consistent naming (`--dry-run`, `--debug`, `--json`, `--config`, `--version`)
|
|
59
|
+
* clear defaults shown in help output
|
|
60
|
+
* Subcommands are discoverable:
|
|
61
|
+
|
|
62
|
+
* `mycli help`
|
|
63
|
+
* `mycli <cmd> --help`
|
|
64
|
+
|
|
65
|
+
If using Typer/Click, invest in:
|
|
66
|
+
|
|
67
|
+
* good parameter names
|
|
68
|
+
* `help=` strings everywhere
|
|
69
|
+
* sane defaults
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### 4) Document output contracts (users build automation)
|
|
74
|
+
|
|
75
|
+
If people will script your tool, document:
|
|
76
|
+
|
|
77
|
+
* **Exit codes** (table)
|
|
78
|
+
* **stdout vs stderr** behavior
|
|
79
|
+
* **JSON output schemas** (`--json`), stability guarantees:
|
|
80
|
+
|
|
81
|
+
* “stable fields” vs “best-effort fields”
|
|
82
|
+
* **Determinism** expectations (ordering, timestamps, concurrency)
|
|
83
|
+
|
|
84
|
+
Example exit code contract:
|
|
85
|
+
|
|
86
|
+
* `0` success
|
|
87
|
+
* `1` runtime error
|
|
88
|
+
* `2` usage error (bad args/config)
|
|
89
|
+
* `3` partial success / some checks failed (if relevant)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### 5) Configuration docs: be explicit and testable
|
|
94
|
+
|
|
95
|
+
Users fail most often on config.
|
|
96
|
+
|
|
97
|
+
Include:
|
|
98
|
+
|
|
99
|
+
* **Search path** (e.g. current dir → XDG → home)
|
|
100
|
+
* **Precedence**: flags > env > config file > defaults
|
|
101
|
+
* **Full config reference** (generated if possible)
|
|
102
|
+
* A “minimal config” and a “full config” example
|
|
103
|
+
* Validation behavior: what happens on unknown keys / wrong types
|
|
104
|
+
|
|
105
|
+
If you support env vars, document them in a dedicated table:
|
|
106
|
+
|
|
107
|
+
* name, type, default, example, related flags.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### 6) Examples that match real user intent
|
|
112
|
+
|
|
113
|
+
Prefer “task-based” pages over exhaustive prose.
|
|
114
|
+
|
|
115
|
+
* “Run in dry-run mode to preview changes”
|
|
116
|
+
* “Enable debug logs and collect diagnostics for an issue”
|
|
117
|
+
* “Integrate with CI (GitHub Actions)”
|
|
118
|
+
* “Use JSON output with jq”
|
|
119
|
+
* “Handle non-zero exit codes in bash”
|
|
120
|
+
|
|
121
|
+
Each example should show:
|
|
122
|
+
|
|
123
|
+
* command
|
|
124
|
+
* expected output snippet (short)
|
|
125
|
+
* what to do next
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 7) Keep docs versioned and tied to releases
|
|
130
|
+
|
|
131
|
+
Best practice for OSS:
|
|
132
|
+
|
|
133
|
+
* Host docs per version (or at least “latest” + “stable”).
|
|
134
|
+
* At minimum: docs in repo; release tags preserve exact docs state.
|
|
135
|
+
* For breaking changes, add a dedicated **Upgrade Guide** section and link it from release notes.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### 8) Choose a docs toolchain that fits CLI repos
|
|
140
|
+
|
|
141
|
+
Recommended:
|
|
142
|
+
|
|
143
|
+
* **MkDocs + mkdocs-material**: simple, fast, great UX.
|
|
144
|
+
* Keep docs in `docs/`, build on CI, deploy to GitHub Pages.
|
|
145
|
+
|
|
146
|
+
Doc site structure that scales:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
docs/
|
|
150
|
+
index.md
|
|
151
|
+
install.md
|
|
152
|
+
quickstart.md
|
|
153
|
+
commands/
|
|
154
|
+
init.md
|
|
155
|
+
run.md
|
|
156
|
+
status.md
|
|
157
|
+
checks.md
|
|
158
|
+
config.md
|
|
159
|
+
tutorials/
|
|
160
|
+
workflow-basic.md
|
|
161
|
+
ci-github-actions.md
|
|
162
|
+
troubleshooting.md
|
|
163
|
+
faq.md
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### 9) Troubleshooting: include a diagnostics checklist
|
|
169
|
+
|
|
170
|
+
Have a “copy/paste for issues” section:
|
|
171
|
+
|
|
172
|
+
* `mycli --version`
|
|
173
|
+
* `python --version`
|
|
174
|
+
* OS info
|
|
175
|
+
* config file used
|
|
176
|
+
* command invoked
|
|
177
|
+
* `--debug` logs (redaction guidance)
|
|
178
|
+
* relevant output files/artifacts paths
|
|
179
|
+
|
|
180
|
+
Also document:
|
|
181
|
+
|
|
182
|
+
* common permission errors
|
|
183
|
+
* path issues
|
|
184
|
+
* lockfile/state issues (if your tool uses locks)
|
|
185
|
+
* proxy/network behavior (if applicable)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### 10) Doc quality gates (enterprise habit)
|
|
190
|
+
|
|
191
|
+
Add CI that fails if docs break:
|
|
192
|
+
|
|
193
|
+
* Build docs on PRs (MkDocs build)
|
|
194
|
+
* Check links (optional)
|
|
195
|
+
* Spellcheck (optional, but useful at scale)
|
|
196
|
+
* Ensure `--help` output is updated if you snapshot it
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Recommended “minimum bar” for your CLI (given your features like `--dry-run`, `--debug`, checks)
|
|
201
|
+
|
|
202
|
+
* README with install + quick start + “how it works” paragraph
|
|
203
|
+
* Command docs: `init`, `plan`, `implement`, `review`, `checks`, `status`, `commit`
|
|
204
|
+
* Config docs including new checks categories and defaults
|
|
205
|
+
* “CI integration” how-to
|
|
206
|
+
* Troubleshooting page covering locks, stale locks, debug logs, non-zero exit codes
|
|
207
|
+
* Output contract page (`--json`, exit codes)
|
|
208
|
+
|
|
209
|
+
If you share your command list (or `mycli --help` output), I can propose an exact docs IA (page tree) and draft the README + 2–3 core docs pages in your style.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
## Enterprise-grade best practices for a `uv`-managed Python CLI tool
|
|
2
|
+
|
|
3
|
+
### Project layout (CLI-first, testable, packageable)
|
|
4
|
+
|
|
5
|
+
Use `src/` layout and keep CLI thin.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
mycli/
|
|
9
|
+
pyproject.toml
|
|
10
|
+
uv.lock
|
|
11
|
+
src/mycli/
|
|
12
|
+
__init__.py
|
|
13
|
+
__main__.py
|
|
14
|
+
cli.py
|
|
15
|
+
commands/
|
|
16
|
+
__init__.py
|
|
17
|
+
foo.py
|
|
18
|
+
core/
|
|
19
|
+
__init__.py
|
|
20
|
+
logic.py
|
|
21
|
+
services/
|
|
22
|
+
__init__.py
|
|
23
|
+
fs.py
|
|
24
|
+
net.py
|
|
25
|
+
tests/
|
|
26
|
+
test_cli.py
|
|
27
|
+
test_logic.py
|
|
28
|
+
README.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Principles
|
|
32
|
+
|
|
33
|
+
* **`cli.py`**: argument parsing + command dispatch only.
|
|
34
|
+
* **`core/`**: pure logic, easy unit tests.
|
|
35
|
+
* **`services/`**: side effects (filesystem/network/subprocess), injectable/mocked.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## `uv` workflow standard (local + CI)
|
|
40
|
+
|
|
41
|
+
### Commands developers run
|
|
42
|
+
|
|
43
|
+
* Install/sync: `uv sync`
|
|
44
|
+
* Run CLI: `uv run mycli ...`
|
|
45
|
+
* Run tests: `uv run pytest`
|
|
46
|
+
* Lint: `uv run ruff check .`
|
|
47
|
+
* Format: `uv run ruff format .` (or `uv run black .`)
|
|
48
|
+
* Typecheck: `uv run pyright`
|
|
49
|
+
|
|
50
|
+
### CI should run (no autofix)
|
|
51
|
+
|
|
52
|
+
* `uv sync --frozen`
|
|
53
|
+
* `uv run ruff format --check .`
|
|
54
|
+
* `uv run ruff check .`
|
|
55
|
+
* `uv run pyright`
|
|
56
|
+
* `uv run pytest -q --maxfail=1`
|
|
57
|
+
* `uv run pip-audit` (or `uv run pip-audit -r <exported requirements>` if needed)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## `pyproject.toml` baseline (uv + CLI + quality gates)
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[project]
|
|
65
|
+
name = "mycli"
|
|
66
|
+
version = "0.1.0"
|
|
67
|
+
description = "My CLI tool"
|
|
68
|
+
requires-python = ">=3.11"
|
|
69
|
+
dependencies = [
|
|
70
|
+
"typer>=0.12",
|
|
71
|
+
"rich>=13.7",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[project.scripts]
|
|
75
|
+
mycli = "mycli.cli:app"
|
|
76
|
+
|
|
77
|
+
[build-system]
|
|
78
|
+
requires = ["hatchling"]
|
|
79
|
+
build-backend = "hatchling.build"
|
|
80
|
+
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
target-version = "py311"
|
|
83
|
+
line-length = 100
|
|
84
|
+
src = ["src"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
select = ["E","F","I","UP","B","SIM","C4","RUF"]
|
|
88
|
+
ignore = []
|
|
89
|
+
|
|
90
|
+
[tool.ruff.format]
|
|
91
|
+
quote-style = "double"
|
|
92
|
+
indent-style = "space"
|
|
93
|
+
|
|
94
|
+
[tool.pyright]
|
|
95
|
+
typeCheckingMode = "standard"
|
|
96
|
+
pythonVersion = "3.11"
|
|
97
|
+
include = ["src"]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Notes
|
|
101
|
+
|
|
102
|
+
* `project.scripts` makes a proper entry point (`mycli`).
|
|
103
|
+
* `hatchling` is a clean default build backend for CLIs.
|
|
104
|
+
* Ruff rule-set chosen for CLI repos: correctness + modernization + import hygiene.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## CLI implementation best practices (enterprise-grade)
|
|
109
|
+
|
|
110
|
+
### Use a real CLI framework
|
|
111
|
+
|
|
112
|
+
* Prefer **Typer** (excellent UX, type-hints map well to args/options).
|
|
113
|
+
* Alternate: Click (mature), argparse (stdlib, minimal deps).
|
|
114
|
+
|
|
115
|
+
### Make failures predictable
|
|
116
|
+
|
|
117
|
+
* Exit codes: `0` success, `1` generic error, `2` usage error, etc.
|
|
118
|
+
* Print user-facing errors to **stderr**, and keep them concise.
|
|
119
|
+
* Provide `--json` for machine-readable output if automation matters.
|
|
120
|
+
|
|
121
|
+
### Logging strategy
|
|
122
|
+
|
|
123
|
+
* Default: quiet user output; add `--verbose/-v` for logs.
|
|
124
|
+
* For CI / automation: `--log-level` and `--no-color`.
|
|
125
|
+
* If you emit logs, separate them from primary output (stderr vs stdout).
|
|
126
|
+
|
|
127
|
+
### Configuration
|
|
128
|
+
|
|
129
|
+
* Support precedence:
|
|
130
|
+
|
|
131
|
+
1. CLI flags
|
|
132
|
+
2. environment variables
|
|
133
|
+
3. config file (e.g., `~/.config/mycli/config.toml`)
|
|
134
|
+
4. defaults
|
|
135
|
+
* Use `platformdirs` to locate config directories.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Testing strategy for a CLI tool
|
|
140
|
+
|
|
141
|
+
### Unit tests
|
|
142
|
+
|
|
143
|
+
* Pure logic in `core/` with straightforward tests.
|
|
144
|
+
|
|
145
|
+
### CLI tests
|
|
146
|
+
|
|
147
|
+
* For Typer: `typer.testing.CliRunner()`
|
|
148
|
+
* Assert:
|
|
149
|
+
|
|
150
|
+
* exit code
|
|
151
|
+
* stdout/stderr
|
|
152
|
+
* behavior under invalid args
|
|
153
|
+
* behavior with env vars / config
|
|
154
|
+
|
|
155
|
+
### Integration tests (optional but common)
|
|
156
|
+
|
|
157
|
+
* Use `tmp_path` for filesystem scenarios.
|
|
158
|
+
* Mock network/subprocess calls unless you explicitly test them behind a marker.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Security + supply chain for CLIs
|
|
163
|
+
|
|
164
|
+
* Dependency scanning: `pip-audit` in CI.
|
|
165
|
+
* Secret scanning: `detect-secrets` or `gitleaks` pre-commit + CI.
|
|
166
|
+
* If invoking subprocesses:
|
|
167
|
+
|
|
168
|
+
* avoid `shell=True`
|
|
169
|
+
* sanitize args
|
|
170
|
+
* timeouts everywhere
|
|
171
|
+
* If handling files:
|
|
172
|
+
|
|
173
|
+
* avoid unsafe path joins (validate inputs; use `pathlib`)
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Pre-commit (works perfectly with `uv run`)
|
|
178
|
+
|
|
179
|
+
`.pre-commit-config.yaml` recommended hooks:
|
|
180
|
+
|
|
181
|
+
* ruff (lint)
|
|
182
|
+
* ruff (format)
|
|
183
|
+
* pyright (optional; can be slow but doable)
|
|
184
|
+
* detect-secrets
|
|
185
|
+
|
|
186
|
+
Run hooks via:
|
|
187
|
+
|
|
188
|
+
* `uv run pre-commit install`
|
|
189
|
+
* `uv run pre-commit run -a`
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Release and packaging (CLI distribution)
|
|
194
|
+
|
|
195
|
+
* Build: `uv run python -m build`
|
|
196
|
+
* Publish: `uv run twine upload dist/*`
|
|
197
|
+
* Add `--version` and `mycli version` command (handy for support).
|
|
198
|
+
* Consider shipping a single-file binary (optional):
|
|
199
|
+
|
|
200
|
+
* `pyinstaller` / `shiv` / `pex` depending on target environment.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## CI (GitHub Actions skeleton for `uv`)
|
|
205
|
+
|
|
206
|
+
Core pattern:
|
|
207
|
+
|
|
208
|
+
* checkout
|
|
209
|
+
* install uv
|
|
210
|
+
* `uv sync --frozen`
|
|
211
|
+
* run quality gates listed above
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
If you want, I can output a complete ready-to-drop-in repo scaffold: `pyproject.toml`, `ruff/pyright` settings, `pre-commit` config, and a GitHub Actions workflow using `uv sync --frozen`, plus a minimal Typer-based CLI with one command and tests.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
## Best practices spec: OSS `uv` package releases via GitHub tag → CI → GitHub Release (+ PyPI)
|
|
2
|
+
|
|
3
|
+
### Goals
|
|
4
|
+
|
|
5
|
+
* Release is **triggered by pushing a git tag** `vX.Y.Z`.
|
|
6
|
+
* CI **creates a GitHub Release** and **publishes to PyPI**.
|
|
7
|
+
* GitHub Release notes are **pulled automatically from `CHANGELOG.md`**.
|
|
8
|
+
* Release is **reproducible** and **fails early** on versioning/changelog mistakes.
|
|
9
|
+
* Prefer **PyPI Trusted Publishing (OIDC)** over long-lived API tokens.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Repository requirements
|
|
14
|
+
|
|
15
|
+
### 1) Version source of truth
|
|
16
|
+
|
|
17
|
+
* `pyproject.toml` contains `project.version = "X.Y.Z"`.
|
|
18
|
+
* The git tag is `vX.Y.Z`.
|
|
19
|
+
* CI must assert: `tag_version == pyproject_version`.
|
|
20
|
+
|
|
21
|
+
### 2) Changelog format (Keep a Changelog compatible)
|
|
22
|
+
|
|
23
|
+
`CHANGELOG.md` must have:
|
|
24
|
+
|
|
25
|
+
* An `## [Unreleased]` section at the top.
|
|
26
|
+
* Released sections like: `## [0.1.0] - 2026-01-04`
|
|
27
|
+
* Content under headings (`###`, `####`, etc.) is allowed.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
|
|
31
|
+
```md
|
|
32
|
+
## [Unreleased]
|
|
33
|
+
### Added
|
|
34
|
+
- ...
|
|
35
|
+
|
|
36
|
+
## [0.1.0] - 2026-01-04
|
|
37
|
+
Initial release...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Release process (human + automation)
|
|
43
|
+
|
|
44
|
+
### Human steps (per release)
|
|
45
|
+
|
|
46
|
+
1. Move entries from `## [Unreleased]` into a new version section `## [X.Y.Z] - YYYY-MM-DD`.
|
|
47
|
+
2. Ensure `pyproject.toml` version is set to `X.Y.Z`.
|
|
48
|
+
3. Commit to `main`.
|
|
49
|
+
4. Create annotated tag: `git tag -a vX.Y.Z -m "vX.Y.Z"`.
|
|
50
|
+
5. Push tag: `git push origin vX.Y.Z`.
|
|
51
|
+
|
|
52
|
+
### CI responsibilities (on tag push)
|
|
53
|
+
|
|
54
|
+
1. Checkout with full history.
|
|
55
|
+
2. Install `uv`, sync dependencies with lockfile (`--frozen`).
|
|
56
|
+
3. Run quality gates (format, lint, typecheck, tests).
|
|
57
|
+
4. Extract version from tag.
|
|
58
|
+
5. Verify `pyproject.toml` version matches tag.
|
|
59
|
+
6. Verify `CHANGELOG.md`:
|
|
60
|
+
|
|
61
|
+
* `[X.Y.Z]` section exists.
|
|
62
|
+
* `## [Unreleased]` is **empty** (recommended gate).
|
|
63
|
+
7. Extract release notes from the `[X.Y.Z]` section and write `RELEASE_NOTES.md`.
|
|
64
|
+
8. Build sdist + wheel once.
|
|
65
|
+
9. Publish to PyPI (Trusted Publishing preferred).
|
|
66
|
+
10. Create GitHub Release using extracted notes and attach artifacts.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Changelog extraction spec (tailored to your structure)
|
|
71
|
+
|
|
72
|
+
### A) Extract release notes for version `X.Y.Z`
|
|
73
|
+
|
|
74
|
+
Rules:
|
|
75
|
+
|
|
76
|
+
* Find section header matching `^## \[X.Y.Z\]` (date suffix optional).
|
|
77
|
+
* Capture everything until the next `^## \[` section header or EOF.
|
|
78
|
+
* Exclude the header line itself.
|
|
79
|
+
* Preserve all markdown below (including nested headings).
|
|
80
|
+
|
|
81
|
+
Reference implementation (`scripts/extract_release_notes.py`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import re, sys, pathlib
|
|
85
|
+
|
|
86
|
+
version = sys.argv[1]
|
|
87
|
+
text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
pattern = rf"""
|
|
90
|
+
^##\s+\[{re.escape(version)}\][^\n]*\n
|
|
91
|
+
(.*?)
|
|
92
|
+
(?=^##\s+\[|\Z)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
m = re.search(pattern, text, re.S | re.M | re.X)
|
|
96
|
+
if not m:
|
|
97
|
+
raise SystemExit(f"CHANGELOG.md missing section for [{version}]")
|
|
98
|
+
|
|
99
|
+
notes = m.group(1).strip()
|
|
100
|
+
pathlib.Path("RELEASE_NOTES.md").write_text(notes + "\n", encoding="utf-8")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### B) Enforce “Unreleased is empty” on tag builds (recommended)
|
|
104
|
+
|
|
105
|
+
Rule:
|
|
106
|
+
|
|
107
|
+
* Extract the body of `## [Unreleased]`.
|
|
108
|
+
* If it contains non-whitespace content, fail the release.
|
|
109
|
+
|
|
110
|
+
Reference implementation (`scripts/assert_unreleased_empty.py`):
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import re, pathlib
|
|
114
|
+
|
|
115
|
+
text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
|
|
116
|
+
m = re.search(r"^##\s+\[Unreleased\]\n(.*?)(?=^##\s+\[|\Z)", text, re.S | re.M)
|
|
117
|
+
|
|
118
|
+
if m and m.group(1).strip():
|
|
119
|
+
raise SystemExit("Unreleased section is not empty — move entries into the release section before tagging.")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Policy note:
|
|
123
|
+
|
|
124
|
+
* This is a “hard gate” that prevents accidental partial releases.
|
|
125
|
+
* If you want a softer policy, change it to warn-only (not recommended for clean OSS releases).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## GitHub Actions workflow (tag push trigger)
|
|
130
|
+
|
|
131
|
+
Create: `.github/workflows/release.yml`
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
name: release
|
|
135
|
+
|
|
136
|
+
on:
|
|
137
|
+
push:
|
|
138
|
+
tags:
|
|
139
|
+
- "v*"
|
|
140
|
+
|
|
141
|
+
permissions:
|
|
142
|
+
contents: write
|
|
143
|
+
id-token: write
|
|
144
|
+
|
|
145
|
+
jobs:
|
|
146
|
+
release:
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/checkout@v4
|
|
150
|
+
with:
|
|
151
|
+
fetch-depth: 0
|
|
152
|
+
|
|
153
|
+
- uses: actions/setup-python@v5
|
|
154
|
+
with:
|
|
155
|
+
python-version: "3.11"
|
|
156
|
+
|
|
157
|
+
- name: Install uv
|
|
158
|
+
uses: astral-sh/setup-uv@v3
|
|
159
|
+
|
|
160
|
+
- name: Sync deps (locked)
|
|
161
|
+
run: uv sync --frozen
|
|
162
|
+
|
|
163
|
+
- name: Quality gates
|
|
164
|
+
run: |
|
|
165
|
+
uv run ruff format --check .
|
|
166
|
+
uv run ruff check .
|
|
167
|
+
uv run pyright
|
|
168
|
+
uv run pytest -q
|
|
169
|
+
|
|
170
|
+
- name: Extract version from tag
|
|
171
|
+
id: tag
|
|
172
|
+
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
173
|
+
|
|
174
|
+
- name: Verify pyproject version matches tag
|
|
175
|
+
run: |
|
|
176
|
+
uv run python - <<'PY'
|
|
177
|
+
import tomllib
|
|
178
|
+
v=tomllib.load(open("pyproject.toml","rb"))["project"]["version"]
|
|
179
|
+
tv="${{ steps.tag.outputs.version }}"
|
|
180
|
+
assert v==tv, f"pyproject version {v} != tag {tv}"
|
|
181
|
+
PY
|
|
182
|
+
|
|
183
|
+
- name: Ensure Unreleased is empty
|
|
184
|
+
run: uv run python scripts/assert_unreleased_empty.py
|
|
185
|
+
|
|
186
|
+
- name: Extract release notes from CHANGELOG.md
|
|
187
|
+
run: uv run python scripts/extract_release_notes.py "${{ steps.tag.outputs.version }}"
|
|
188
|
+
|
|
189
|
+
- name: Build (sdist + wheel)
|
|
190
|
+
run: uv run python -m build
|
|
191
|
+
|
|
192
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
193
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
194
|
+
with:
|
|
195
|
+
packages-dir: dist/
|
|
196
|
+
|
|
197
|
+
- name: Create GitHub Release + upload artifacts
|
|
198
|
+
env:
|
|
199
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
200
|
+
run: |
|
|
201
|
+
gh release create "${GITHUB_REF_NAME}" \
|
|
202
|
+
--title "${GITHUB_REF_NAME}" \
|
|
203
|
+
--notes-file RELEASE_NOTES.md \
|
|
204
|
+
dist/*
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## PyPI publishing (recommended: Trusted Publishing)
|
|
210
|
+
|
|
211
|
+
### Setup (one-time)
|
|
212
|
+
|
|
213
|
+
1. Create the project on PyPI (or publish once manually).
|
|
214
|
+
2. In PyPI project settings, add a **Trusted Publisher**:
|
|
215
|
+
|
|
216
|
+
* Provider: GitHub
|
|
217
|
+
* Repo: your org/repo
|
|
218
|
+
* Workflow: `.github/workflows/release.yml`
|
|
219
|
+
3. Ensure GitHub Actions has:
|
|
220
|
+
|
|
221
|
+
* `permissions: id-token: write`
|
|
222
|
+
|
|
223
|
+
### Fallback (token-based)
|
|
224
|
+
|
|
225
|
+
If Trusted Publishing is not possible:
|
|
226
|
+
|
|
227
|
+
* Store `PYPI_API_TOKEN` as a GitHub secret.
|
|
228
|
+
* Replace publish step:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
232
|
+
with:
|
|
233
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Hardening checklist (recommended for serious OSS)
|
|
239
|
+
|
|
240
|
+
### Reproducibility
|
|
241
|
+
|
|
242
|
+
* Commit `uv.lock`.
|
|
243
|
+
* Use `uv sync --frozen` in CI.
|
|
244
|
+
* Build artifacts only after tests pass.
|
|
245
|
+
|
|
246
|
+
### Security
|
|
247
|
+
|
|
248
|
+
* Add dependency audit on tag builds (or every PR):
|
|
249
|
+
|
|
250
|
+
* `uv run pip-audit`
|
|
251
|
+
* Add secret scanning:
|
|
252
|
+
|
|
253
|
+
* `gitleaks` or `detect-secrets` in pre-commit + CI
|
|
254
|
+
|
|
255
|
+
### Release integrity
|
|
256
|
+
|
|
257
|
+
* Fail if:
|
|
258
|
+
|
|
259
|
+
* tag version != pyproject version
|
|
260
|
+
* changelog section missing
|
|
261
|
+
* Unreleased is non-empty
|
|
262
|
+
* tests/lint/typecheck fail
|
|
263
|
+
* build fails
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Expected conventions
|
|
268
|
+
|
|
269
|
+
* Tags are `vX.Y.Z`.
|
|
270
|
+
* Changelog headers are `## [X.Y.Z] - YYYY-MM-DD`.
|
|
271
|
+
* `CHANGELOG.md` is authoritative release notes source.
|
|
272
|
+
* CI is authoritative publisher and release creator.
|