Skip to content

Commit c3f9d2d

Browse files
committed
docs: add RELEASE_PROCEDURE.md for LLM/agent dev+release workflow
Documents the full release pipeline for any LLM or human working in this repo: the src/ never-commit-directly rule, version-bump checklist across all 8 files, safe staging pattern (never git add -A), CI verification before tagging, injected-secret update procedure, tag/publish mechanics with irreversibility warning, and mandatory post-publish fresh-install verification. Written from and cites real incidents in this repo's history (v0.6.5 shipped a broken import, a wrong tag once pointed at a stray deleted branch). AGENTS.md updated to point here and stop hardcoding a version number that goes stale.
1 parent 6daa744 commit c3f9d2d

2 files changed

Lines changed: 364 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ QECTOR Decoder v3 is a high-performance, source-available Rust/Python platform f
1313
- **Ecosystem**: PyMatching-compatible, Stim/Sinter integration, belief-matching, Qiskit plugin, REST/gRPC + full-featured MCP server (25 tools in the QectorWorkbench companion).
1414
- **Emphasis**: Syndrome faithfulness, CPU/GPU bit-identical results, extensive regression + artifact-backed benchmarks, reproducibility.
1515

16-
**Current version**: 0.6.4 (Cargo + Python packaging).
16+
**Current version**: see `pyproject.toml` / `Cargo.toml` for the authoritative
17+
current version — do not trust a hardcoded number in this file, it goes stale.
18+
19+
**Release, versioning, and PyPI publish procedure**: see `RELEASE_PROCEDURE.md`
20+
at repo root. Read it before bumping any version number, touching `src/`,
21+
GitHub secrets, or git tags — it documents real incidents from this repo's
22+
history (broken secret uploads, a v0.6.5 release that failed to import for
23+
every user, stray branches) and the checklist that prevents repeating them.
1724

1825
**Workspace layout** (this checkout):
1926

RELEASE_PROCEDURE.md

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# RELEASE_PROCEDURE.md — QECTOR Decoder v3
2+
3+
**Audience: any LLM/agent (or human) doing dev work, version bumps, or
4+
releases in this repository.** Read this in full before touching version
5+
numbers, `src/`, GitHub secrets, or git tags. It exists because every
6+
mistake described below has actually happened in this repo at least once.
7+
8+
Companion to `AGENTS.md` (general dev conventions, coding style, testing).
9+
This file is specifically about **the release/push/publish pipeline**
10+
the part with irreversible consequences (PyPI publishes cannot be deleted
11+
or overwritten; GitHub secrets, once wrong, silently break every build).
12+
13+
---
14+
15+
## 0. The one rule that matters most
16+
17+
**`src/*.rs` in this working checkout is the real, proprietary Rust source.
18+
It must never be committed directly to git on `main`.**
19+
20+
This repo's public GitHub history does NOT contain the real Rust source in
21+
plain form. Instead:
22+
23+
- The real source lives only in this local checkout (and wherever else the
24+
operator keeps it) and as three base64-encoded, chunked GitHub Actions
25+
secrets: `RUST_SRC_B64_1`, `RUST_SRC_B64_2`, `RUST_SRC_B64_3`.
26+
- CI (`.github/workflows/CI.yml`) reassembles and decodes these secrets at
27+
build time (`Inject Rust source` step) into `src/` before compiling.
28+
- `git status` in this checkout will normally show `src/lib.rs` and other
29+
`src/*.rs` files as modified or untracked. **This is expected and correct.**
30+
Do not `git add src/` and do not include `src/` in release commits.
31+
32+
If you ever need to update the source the secrets decode to, see §5
33+
("Updating the injected Rust source"). Never treat a modified/untracked
34+
`src/` as something to silently stage in a routine commit.
35+
36+
---
37+
38+
## 1. Repo / environment facts (verified, not assumed)
39+
40+
- Local checkout root (Windows): `C:\Users\Admin\Desktop\qector-build-rc-0.6.2`
41+
- Remote: `https://github.com/GuillaumeLessard/qector-decoder.git`, default
42+
branch `main` (an earlier stray `master` branch existed and was deleted —
43+
if you ever see a `master` branch again, do not assume it's authoritative;
44+
verify against `main` first).
45+
- PyPI project: `qector-decoder-v3` (https://pypi.org/project/qector-decoder-v3/)
46+
- Publish mechanism: OIDC Trusted Publisher via `pypa/gh-action-pypi-publish`
47+
**no API token is stored or needed.** Publishing is gated on:
48+
- tag matching `v*`
49+
- `github.repository == 'GuillaumeLessard/qector-decoder'` (forks can't publish)
50+
- not a `pull_request` event
51+
- all of `linux-x86_64`, `windows-x64`, `macos-arm` jobs succeeding first
52+
(`needs:` in CI.yml) — `macos-intel` is present but disabled
53+
(`if: false`, long queue times) and is NOT required for publish.
54+
- Build flags actually used in CI (`CI.yml``env.MATURIN_ARGS`):
55+
`--release --no-default-features --features cuda --out dist`
56+
**OpenCL is NOT compiled into public wheels.** CUDA is present as a
57+
feature but loads at runtime via `libloading`/NVRTC, so no CUDA toolkit
58+
is needed on CI runners. Any code path that unconditionally assumes an
59+
OpenCL symbol exists on the compiled module will crash on import for
60+
every user (this exact bug shipped in v0.6.5 — see CHANGELOG).
61+
- Local dev builds (`maturin develop`) commonly use **default features**,
62+
which include both `opencl` and `cuda`. This means a bug gated behind
63+
"only present without the opencl feature" will not reproduce locally
64+
under default `maturin develop` — you must rebuild with the exact CI
65+
flags to catch it (§6).
66+
67+
---
68+
69+
## 2. Version numbers — every location, no exceptions
70+
71+
A version bump touches **all** of the following. Miss one and you ship an
72+
inconsistent release (this has happened — a stale `0.6.4` citation block
73+
survived two version bumps before being caught).
74+
75+
| File | What to change |
76+
|---|---|
77+
| `pyproject.toml` | `[project] version = "X.Y.Z"` |
78+
| `Cargo.toml` | `[package] version = "X.Y.Z"` |
79+
| `Cargo.lock` | Auto-updates when you run `cargo check`/`cargo build` after bumping `Cargo.toml`. Verify the `qector_decoder_v3` package entry specifically — do NOT hand-edit; do not confuse it with unrelated third-party crate versions in the same lockfile (e.g. `rand_core 0.6.4` is unrelated and must not change). |
80+
| `python/qector_decoder_v3/__init__.py` | `__fallback_version__ = "X.Y.Z"`**this is a fallback only**, used if the compiled extension's own `__version__` can't be read. Never make this overwrite the real compiled `__version__`; the code should keep reporting the built value until the wheel is actually rebuilt. |
81+
| `CITATION.cff` | `version: X.Y.Z` |
82+
| `codemeta.json` | `"version":"X.Y.Z"` (single-line minified JSON in this repo — use exact string match, don't reformat the whole file) |
83+
| `README.md` | Citation/BibTeX block (`version = {X.Y.Z}`) + add a new `## New in vX.Y.Z` section ahead of the previous one |
84+
| `PYPI_README.md` | Citation/BibTeX block (`version = {X.Y.Z}`) + add a new `## vX.Y.Z Highlights` section ahead of the previous one |
85+
| `CHANGELOG.md` | New `## [X.Y.Z] - YYYY-MM-DD` entry at the top (below `## [Unreleased]`), following Keep a Changelog format already in use |
86+
87+
### Verifying you got them all
88+
89+
Before committing, grep every version-bearing file for the OLD version
90+
string and confirm zero hits outside of historical changelog entries
91+
(which should of course still mention old versions in their own sections):
92+
93+
```powershell
94+
Select-String -Path README.md,PYPI_README.md,CHANGELOG.md,CITATION.cff,codemeta.json,pyproject.toml,Cargo.toml,Cargo.lock -Pattern "OLD\.VERSION\.HERE"
95+
```
96+
97+
Then grep for the NEW version and confirm it appears in every file from
98+
the table above:
99+
100+
```powershell
101+
Select-String -Path README.md,PYPI_README.md,CHANGELOG.md,CITATION.cff,codemeta.json,pyproject.toml,Cargo.toml,Cargo.lock -Pattern "NEW\.VERSION\.HERE"
102+
```
103+
104+
Read the actual diff before committing, not just the file list:
105+
106+
```powershell
107+
git diff -- pyproject.toml Cargo.toml CITATION.cff codemeta.json Cargo.lock python/qector_decoder_v3/__init__.py README.md PYPI_README.md CHANGELOG.md
108+
```
109+
110+
---
111+
112+
## 3. Staging and committing — the safe pattern
113+
114+
**Never `git add -A` or `git add .` in this repo.** The working tree
115+
routinely contains real proprietary `src/*.rs` files (modified or
116+
untracked) plus scratch/debug files that should never be committed
117+
(`*.ps1` one-off scripts, `mypy_*.txt`, `pytest_*.txt`, `ruff_*.txt`,
118+
`cargo_check_*.txt`, `.venv*/`, `benchmark_*.py` ad hoc scripts, etc.).
119+
Blind `-A` staging has caused real incidents in this repo's history.
120+
121+
Correct pattern — stage exactly the files you intend to change, by name:
122+
123+
```powershell
124+
cd "C:\Users\Admin\Desktop\qector-build-rc-0.6.2"
125+
git status --short # review everything first
126+
git add pyproject.toml Cargo.toml Cargo.lock CITATION.cff codemeta.json `
127+
python/qector_decoder_v3/__init__.py README.md PYPI_README.md CHANGELOG.md
128+
git status --short # confirm ONLY intended files show staged (M in col 1)
129+
# confirm src/lib.rs is NOT staged
130+
```
131+
132+
If `src/lib.rs` or other `src/*.rs` files show as staged, unstage them
133+
immediately: `git restore --staged src/`. This should not normally happen
134+
if you only `git add` explicit filenames, but always verify.
135+
136+
Write a commit message that documents **root cause and verification**, not
137+
just "bump version" — future sessions (including future you, and future
138+
LLM agents with no memory of this session) need to know *why*, not just
139+
*what*:
140+
141+
```powershell
142+
git commit -m "vX.Y.Z: <one-line summary>" -m "<paragraph: root cause, what was verified, and how>"
143+
```
144+
145+
Push to `main` directly (this repo does not use a PR-gated release
146+
branch model for routine releases):
147+
148+
```powershell
149+
git push origin main
150+
```
151+
152+
---
153+
154+
## 4. Verify CI is green on `main` BEFORE tagging
155+
156+
Tagging triggers the real publish pipeline. Never tag on a red or unverified
157+
`main`. Use the GitHub CLI to watch the exact commit's runs:
158+
159+
```powershell
160+
gh run list --branch main --limit 5
161+
gh run watch <tests-run-id> --exit-status
162+
gh run watch <ci-run-id> --exit-status
163+
```
164+
165+
Confirm in the output:
166+
- `tests` workflow: `ruff-and-mypy`, `smoke-import-py3.9`...`py3.13` all pass
167+
(the `docker` job is informational only — `continue-on-error: true` — a
168+
red docker job is expected/harmless when `src/` in the checkout is a stub
169+
or doesn't match the Dockerfile's expectations; do not treat it as a
170+
release blocker)
171+
- `CI` workflow: all 15 real matrix jobs (5 Python versions × Linux/Windows,
172+
plus 5 × macOS ARM) show ✓ Complete job. `macos-x86_64-py${{ matrix.python-version }}`
173+
will show as **skipped**, not failed — this is the disabled Intel-macOS
174+
job (`if: false` in CI.yml, pre-existing, not something to fix as part
175+
of a release). A skip here is normal; a failure anywhere else is not.
176+
177+
Do not proceed to tagging on partial/incomplete runs. Wait for full completion.
178+
179+
---
180+
181+
## 5. Updating the injected Rust source (only when `src/` actually changed)
182+
183+
If your release includes real Rust changes (new decoder, bugfix inside
184+
`src/*.rs`), the GitHub secrets must be regenerated from the verified-correct
185+
local source **before** the CI build that will use them:
186+
187+
1. Confirm the local `src/` tree compiles and passes tests first:
188+
```powershell
189+
cargo check --no-default-features --features cuda # match CI's exact flags
190+
cargo test
191+
```
192+
2. Round-trip-verify the encoding BEFORE uploading anything — encode, then
193+
decode back locally, and diff against the original byte-for-byte. Never
194+
upload a secret you haven't verified decodes to exactly the source you
195+
intended. A prior incident in this repo involved uploading a stub/wrong
196+
`src/lib.rs` this way, which silently broke every subsequent build.
197+
3. Chunk the base64 into three parts sized to stay under GitHub's per-secret
198+
size limit, and upload as `RUST_SRC_B64_1`, `RUST_SRC_B64_2`, `RUST_SRC_B64_3`
199+
via `gh secret set <name> < chunk_file` (or the GitHub UI).
200+
4. After uploading, trigger a fresh CI run (push a commit or re-run an
201+
existing workflow) and manually confirm the "Inject Rust source" step
202+
in the Actions log actually extracted the expected files — check the
203+
file count and spot-check one distinctive file (e.g. confirm a specific
204+
new function name grep-matches inside the extracted `src/lib.rs` in the
205+
Actions log, or that `cargo check` succeeds in that job).
206+
5. Only after confirming a full green CI run against the NEW secrets should
207+
you proceed to tagging.
208+
209+
**Never assume a secret upload worked. Verify by observing the next real
210+
CI run use it successfully**, not by trusting the upload command's exit code.
211+
212+
---
213+
214+
## 6. Tagging and publishing to PyPI
215+
216+
**This step is irreversible.** PyPI does not allow re-uploading or deleting
217+
a version once published — a broken release stays broken forever at that
218+
version number; the only fix is shipping a new, higher version number
219+
(this happened with v0.6.5 → v0.6.6). Do not tag speculatively "to see
220+
what happens."
221+
222+
Before tagging:
223+
- [ ] All version numbers updated and verified (§2)
224+
- [ ] Docs (`README.md`, `PYPI_README.md`, `CHANGELOG.md`) updated and re-read
225+
in full, not just diffed (read the actual rendered section — stale
226+
surrounding prose or broken markdown tables are easy to miss in a diff)
227+
- [ ] Committed and pushed to `main`
228+
- [ ] CI fully green on that exact commit (§4)
229+
- [ ] **Check whether a tag with this version already exists** — if you're
230+
re-doing a release (e.g. after discovering the version was already
231+
tagged against a stale/wrong commit), delete and recreate deliberately:
232+
```powershell
233+
git tag -l "vX.Y.Z" # check local
234+
git ls-remote --tags origin | Select-String "vX.Y.Z" # check remote
235+
# if it exists and points at the wrong commit:
236+
git tag -d vX.Y.Z
237+
git push origin :refs/tags/vX.Y.Z # delete remote tag
238+
```
239+
240+
Create and push the tag against the exact commit you verified in §4:
241+
242+
```powershell
243+
git tag vX.Y.Z <commit-sha> # or omit sha to tag HEAD, but verify HEAD first
244+
git push origin vX.Y.Z
245+
```
246+
247+
Watch the tag-triggered run through to completion, including the publish job:
248+
249+
```powershell
250+
gh run list --branch main --limit 3
251+
gh run watch <tag-triggered-ci-run-id> --exit-status
252+
```
253+
254+
Confirm in the output that `publish-to-pypi` actually ran (not skipped)
255+
and succeeded. Then verify independently, from a fresh shell, that PyPI
256+
actually has it — don't trust the Actions log alone, and don't trust
257+
`pypi.org`'s JSON API's `info.version` field immediately after (it lags
258+
behind actual file availability due to CDN caching):
259+
260+
```powershell
261+
pip index versions qector-decoder-v3 # or check the Files page directly:
262+
# https://pypi.org/project/qector-decoder-v3/#files
263+
```
264+
265+
---
266+
267+
## 7. Post-publish: install and test the REAL published artifact
268+
269+
**Never consider a release verified based on local dev builds, editable
270+
installs, or `maturin develop` output.** You must install the actual thing
271+
a real user would get from `pip install`.
272+
273+
1. Uninstall any local/editable install first, and confirm it's actually gone:
274+
```powershell
275+
cd "C:\Users\Admin\Desktop\qector-build-rc-0.6.2"
276+
.\.venv\Scripts\Activate.ps1
277+
pip uninstall qector-decoder-v3 -y
278+
python -c "import qector_decoder_v3" # MUST raise ModuleNotFoundError
279+
```
280+
2. Create a **fresh, separate, throwaway venv** — not `.venv` — with no
281+
cache, so nothing local leaks in:
282+
```powershell
283+
python -m venv .venv_pypi_test
284+
.\.venv_pypi_test\Scripts\Activate.ps1
285+
python -m pip install --upgrade pip --quiet
286+
pip install qector-decoder-v3==X.Y.Z --no-cache-dir
287+
```
288+
3. Confirm what actually got installed — look for the absence of an
289+
"Editable project location" line, and confirm the `Location` path
290+
points inside the throwaway venv, not this checkout:
291+
```powershell
292+
pip show qector-decoder-v3
293+
```
294+
4. Actually run code against it — at minimum: import (catches the exact
295+
class of bug that broke v0.6.5), GPU-availability checks (these run
296+
after import and were previously unreachable), and one real decode
297+
through the public API with real array shapes, not just `assert True`.
298+
Write this as a disposable script, run it, delete it after (see §8) —
299+
don't leave ad hoc verification scripts littering the repo root.
300+
5. Only after this passes is the release actually done. "CI is green" and
301+
"PyPI shows the version" are necessary but not sufficient — a release
302+
that imports-fails for every user (as v0.6.5 did) can still pass CI,
303+
because CI's own smoke-import test in `tests.yml` runs on Linux only
304+
and may not exercise every feature-flag combination a real user hits.
305+
306+
---
307+
308+
## 8. Cleanup discipline
309+
310+
- Delete throwaway venvs (`.venv_pypi_test`, `.venv_clean_test`, etc.) and
311+
ad hoc verification scripts once you're done with them. They are not
312+
tracked in git (see `.gitignore`), but they clutter the working directory
313+
and can confuse the next session (human or LLM) about what's real vs.
314+
scratch.
315+
- Never delete `todo.md`, `todo2.md`, `howto.md`, or other operator-authored
316+
planning docs at repo root unless explicitly asked — these are working
317+
notes, not scratch output, even though they sit alongside actual scratch
318+
files.
319+
- If you're unsure whether a root-level file is scratch or intentional,
320+
ask, or check `git log --follow -- <file>` / whether it's tracked at all
321+
(`git ls-files <file>`) before deleting.
322+
323+
---
324+
325+
## 9. Quick reference — full release checklist
326+
327+
```
328+
[ ] Bump version in all 8 locations (§2), verify with grep
329+
[ ] Update README.md, PYPI_README.md, CHANGELOG.md — read full rendered sections
330+
[ ] git status --short — review everything, confirm no src/ surprises
331+
[ ] git add <explicit filenames only> -- never -A / .
332+
[ ] git status --short — confirm ONLY intended files staged
333+
[ ] git commit -m "vX.Y.Z: ..." -m "<root cause + verification detail>"
334+
[ ] git push origin main
335+
[ ] gh run watch <tests-run> --exit-status
336+
[ ] gh run watch <ci-run> --exit-status -- confirm all real jobs green
337+
[ ] (if src/ changed) regenerate + verify RUST_SRC_B64_1/2/3, confirm next
338+
CI run actually used the new secret successfully
339+
[ ] Check no stale/wrong tag already exists for this version
340+
[ ] git tag vX.Y.Z && git push origin vX.Y.Z
341+
[ ] gh run watch <tag-triggered-run> --exit-status -- confirm publish-to-pypi ran + succeeded
342+
[ ] pip index versions qector-decoder-v3 -- confirm live independent of Actions log
343+
[ ] Uninstall local install, confirm ModuleNotFoundError
344+
[ ] Fresh throwaway venv, pip install <pkg>==X.Y.Z --no-cache-dir
345+
[ ] pip show <pkg> -- confirm no "Editable project location", right venv path
346+
[ ] Run real code against it: import + GPU checks + one real decode
347+
[ ] Delete throwaway venv + ad hoc test script
348+
[ ] Done -- only now is the release actually verified
349+
```
350+
351+
---
352+
353+
*This document describes process, not code. If the actual workflow files
354+
(`.github/workflows/CI.yml`, `tests.yml`) change, re-verify this document
355+
against them rather than trusting it as historical truth — it was last
356+
verified against the live workflow files on 2026-07-12.*

0 commit comments

Comments
 (0)