Retrospective: R021 (the final-release pull request for 0.2.0)#
Status: done on the branch release-0.2.0-final, ready for the final-release pull request (from main at 406c3a6, the merge
of pull request #11 with R020; the signed tag v0.2.0rc1 points there). Release 0.2.0 is M7 +
M6 (PLAN Appendix F); its release candidate 0.2.0rc1 is on TestPyPI since 2026-10-09 (dyng,
dyng-cu12 and dyng-cu13, release.yml run 37871452302, attempt 3). R021 smoke-tests that
release candidate from TestPyPI (docs/developer/release.md step 8), makes the final-release
pull request (step 9: the version, the CHANGELOG, CITATION.cff, the status texts, the
approvals log) and carries the release candidate’s certificate over to
benchmarks/results/0.2.0/. Nothing is published by this milestone: the orchestrator pushes the
branch; the author merges the pull request; the AI assistant pushes the signed tag v0.2.0 and
creates the GitHub Release on the author’s behalf (GOVERNANCE.md, 2026-10-06); the author
approves the PyPI deployments pypi-cu12 and pypi-cu13 (one dialog), then pypi. The planned
tag day is 2026-10-14 (UTC).
Step 1: the smoke test of 0.2.0rc1 from TestPyPI (rc-smoke)#
Result: passed, everywhere; nothing to fix. No defect of the code or of the documentation
was found, so the library and the release candidate’s documents are unchanged by this step and
the certificate can still be carried over (step 9 of release.md).
How it was run#
On 2026-10-13 between 19:28 and 19:37 UTC, on the GPU machine (Debian 12, glibc 2.36, Linux
6.1; 2x RTX A5000, NVIDIA driver 590.48.01, which reports CUDA 13.1; the GPU tests on GPU
1), under the shared lock of perf.lock. Every venv is new, made by python -m venv from a
plain interpreter (CPython 3.12.14 of the conda environment dyng-dev, used only as the base
interpreter, and CPython 3.13.15 of $DYNG_SCRATCH/tools/py313), and every command ran in a
clean environment (env -i with PATH=/usr/local/bin:/usr/bin:/bin, HOME, LANG=C.UTF-8,
OMP_NUM_THREADS=4, PIP_NO_CACHE_DIR=1): no dev environment, no CUDA toolkit and no build of
dynG on PATH, and no pip cache, so every file came from the indexes. The venvs’ own pip was
used (25.0.1 on 3.12, 26.2.1 on 3.13), as a user’s would be. The install commands are exactly
those of the release candidate’s README.md and docs/getting_started/install.md, and the
tests and ci/ scripts are those of the tag v0.2.0rc1 (git archive v0.2.0rc1; the tag is
406c3a6, this branch’s start).
The scripts and logs are in the work area (not committed; the logs go into the certificate, step 3 of this milestone):
File |
sha256 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The files tested are the ones the release candidate published. pip’s installation reports
(pip install --report) name the same three wheels in every venv, and their sha256 equal both
the digests TestPyPI serves (https://test.pypi.org/pypi/<name>/0.2.0rc1/json, uploaded
2026-10-09 02:24-02:25 UTC) and the files of the dist artifact of run 37871452302 (gh run download, read-only):
File (TestPyPI, uploaded by |
sha256 |
|---|---|
|
|
|
|
|
|
|
|
At the same time pypi.org has dyng 0.1.0 as its latest release and no project dyng-cu12 or
dyng-cu13 (HTTP 404), as the author’s decision of 2026-10-09 expects (the names are created
by the final release).
The CPU package dyng==0.2.0rc1 (release.md step 8)#
In fresh venvs on Python 3.12 and 3.13, the command of the README and of install.md:
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ dyng==0.2.0rc1
It installed dyng 0.2.0rc1 (the wheel above, from TestPyPI) and NumPy 2.5.4 (from PyPI), and
nothing else. Then, from an empty directory:
Check |
3.12.14 |
3.13.15 |
|---|---|---|
|
0.2.0rc1 |
0.2.0rc1 |
|
|
the same |
installed metadata |
|
the same |
|
five vertices: |
the same |
|
histogram |
the same |
|
path cost of |
the same |
|
every array above equal |
equal |
|
the BibTeX entries |
the same |
|
|
the same |
a CLI command: |
both output trees byte-identical ( |
the same |
beyond step 8: the release candidate’s |
546 passed, 67 skipped |
546 passed, 67 skipped |
Every run printed SMOKE OK. The 67 skips are the GPU tests (no plugin installed) and the
pandas round trip of the profiler (the venvs do not have pandas).
The plugins dyng[cu12]==0.2.0rc1 and dyng[cu13]==0.2.0rc1 (release.md step 8, “The GPU tests of the published plugin wheels”)#
Per plugin and per Python version (3.12, 3.13) a fresh venv, installed as install.md and the
README say for the release candidate (NumPy and the test tools from PyPI first, then dyng and
the plugin from TestPyPI alone, --no-deps):
pip install "numpy>=1.26" "pytest>=8" "hypothesis==6.167.1"
pip install -i https://test.pypi.org/simple/ --no-deps "dyng==0.2.0rc1" "dyng-cu13==0.2.0rc1" # or dyng-cu12
then the checks of ci/plugin_wheels.sh with the tag’s ci/plugin_smoke.py (from an empty
directory) and the tag’s python/tests (from the tag’s tree, against the installed packages):
Check |
cu12 / 3.12 |
cu12 / 3.13 |
cu13 / 3.12 |
cu13 / 3.13 |
|---|---|---|---|---|
installed |
|
the same |
|
the same |
|
passed: |
passed (the same) |
passed: |
passed (the same) |
the fallback without a device: |
passed: one |
passed |
passed |
passed |
the fallback without a driver: |
passed: “no CUDA driver: libcuda.so.1 cannot be loaded (install the NVIDIA driver, 525.60.13 or newer for CUDA 12)” |
passed |
passed (”… 580.65.06 or newer for CUDA 13”) |
passed |
|
55 passed, 7 skipped |
55 passed, 7 skipped |
55 passed, 7 skipped |
55 passed, 7 skipped |
|
546 passed, 67 skipped |
546 passed, 67 skipped |
546 passed, 67 skipped |
546 passed, 67 skipped |
The 7 skips of the GPU tests are the DLPack / CUDA array interface round trips with CuPy and
PyTorch, which these venvs do not install (the optional extras; ci/plugin_wheels.sh runs them
only with DYNG_PLUGIN_INTEROP_PYTHON; M6a recorded them on the local wheels).
The extras, as the final release will document them. In two more fresh venvs per plugin
(3.12, 3.13): NumPy from PyPI, then pip install -i https://test.pypi.org/simple/ "dyng[cu13]==0.2.0rc1" (and [cu12]), from TestPyPI alone. The extra resolved to the plugin
of the same version (dyng + dyng-cuN 0.2.0rc1, the files above), and ci/plugin_smoke.py --expect cuda passed on GPU 1 in all four. This is the install line of the final release
(pip install "dyng[cu13]", PyPI then), checked here on the release candidate’s metadata.
The step-10 procedure, rehearsed. The block of release.md “The GPU tests of the published
plugin wheels” that the AI assistant runs on the final tag’s release.yml run before the author
approves pypi-cu12 and pypi-cu13 was run once as written, from a clone of the tag, on the
dist artifact of run 37871452302 (with d=$DYNG_SCRATCH/r021/release-0.2.0rc1 instead of
$DYNG_SCRATCH/release-0.2.0rc1, to keep this milestone’s files together): twine check and
ci/wheel_check.py passed for the core and both plugin wheels, and for both plugins on 3.12 and
3.13 the smoke test on GPU 1, both fallbacks, pytest -m gpu (55 passed, 7 skipped) and the
DYNG_CPU_ONLY=1 suite (546 passed, 67 skipped) passed. The procedure works as documented; on
the tag day only run= changes.
What it means for the release#
Acceptance criterion 1 is met: no blocker for the final release from the release candidate.
No code, test or document changed in this step, so the library is still that of 0.2.0rc1’s measured commits and the certificate can be carried over (release.md step 9).
release.mdstep 8 names “a CPU machine and the GPU machine”. As for 0.1.0 (R011), only the GPU machine is available to this milestone; the CPU package’s host backends ran on it, and the plugins’ behaviour on a machine without a GPU or a driver was checked with the two fallbacks above (CUDA_VISIBLE_DEVICES=andci/without_cuda_driver.sh); the hosted runners of the release run checked the plugins’ install without a GPU too (its “Plugin cuN install test, no GPU” jobs, green).
Step 2: the final-release pull request (final-pr)#
The final-release changes (release.md step 9)#
Every item of docs/developer/release.md, “The final-release pull request (step 9)”, for 0.2.0
with the release date 2026-10-14, the planned tag day:
Item |
Commit |
What changed |
|---|---|---|
|
|
|
the status texts |
|
|
|
|
the approvals log: the author’s decision of 2026-10-09 on the plugin names (accepted window, no placeholders; the open decision R020 added is closed) and the publishers’ repository state of 2026-10-09; |
the hosted sanitizer record, this page |
this page’s commit |
|
python ci/wheel_check.py --release-metadata and --release-metadata --release-date 2026-10-14
pass (VERSION, CHANGELOG.md and CITATION.cff agree on 0.2.0 and the date); --release-date today fails until the tag day, as it should (release.md step 10 runs it on the tag day).
What is left of the release candidate’s wording. grep -rn "0.2.0rc1\|release candidate"
over the user-facing files (README.md, docs/index.md, docs/getting_started/, SECURITY.md,
CONTRIBUTING.md, SUPPORT.md, docs/citing.md, docs/roadmap.md, docs/developer/plan.md,
python/README.md) finds only history that stays true after the release: SECURITY.md’s
sentence that the release candidates were published on TestPyPI only and their rows “no; please
upgrade to 0.2.0”, its policy that problems found in a release candidate are fixed before the
release, the roadmap’s and the plan’s account of 0.1.0rc1 and 0.2.0rc1, and the install guide’s
“find_package(dyng 0.2) finds 0.2.x (the release candidates included)”. The documentation
site’s banner needs no change: docs/conf.py shows none for a final VERSION. That grep was too
narrow: it missed five lines that called 0.2.0’s own content “not released yet” (the maturity
lines of four algorithm pages and the tutorial’s “clone main, not the 0.1.0 release”), which the
review of the pull request found (step 3 below); step 9 of release.md now names those pages and
greps for not released and new in X.Y too.
The publishers’ failures, read back. The record in GOVERNANCE.md and release.md is taken
from the run itself (gh run view 37871452302 --attempt N, read-only): attempt 1 (01:47 UTC)
failed in both TestPyPI plugin jobs with invalid-publisher (“valid token, but no corresponding
publisher”, claims environment: testpypi-cu12 / testpypi-cu13); attempt 2 (02:20 UTC) with
“400 Non-user identities cannot create new projects. This was probably caused by successfully
using a pending publisher but specifying the project name incorrectly”; attempt 3 (02:24 UTC)
ran only the publish jobs (the builds, the install tests and collect keep their attempt-1 start
times) and uploaded the plugins, then dyng. That is why the files on TestPyPI are the first
attempt’s dist artifact (step 1’s hashes).
The certificate, carried over (release.md step 9)#
The library did not change since the release candidate’s measured commits: git diff from
b1a4869 (the gates, the replays, the golden mutations) and from d553b79 (the head of pull
request #11, whose hosted sanitizer jobs are recorded with the scope library) to this branch
touches no path of the library scope. So benchmarks/results/0.2.0/ is a copy of
benchmarks/results/0.2.0rc1/ (cp -r), and the certificate’s measured commits are b1a4869,
d553b79 and the commit of the re-run checks below; 616718a, where the release candidate’s
re-run checks of its tag day ran, is no longer measured (its records were replaced):
Carried over unchanged: the golden replays of sssp, cycle_count and mosp (
parity-*.json), the three suite summaries and the compacted records of the performance gates (ipdps25_*,ieee_tc_dyntrucy*), and the golden mutations (mutation-goldens.json,libraryscope).Run again on the final tree (they see the new
VERSION,CHANGELOG.md,CITATION.cffand the status texts), each from its own freshgit cloneof this branch at the last commit outsidebenchmarks/results/(the last commit of this page; a first run atc3512fewas replaced by a second one after the review fixes of step 3, which changedREADME.md,CHANGELOG.md,CONTRIBUTING.mdand documents), concurrently under the shared lock and never during a timing run, with the scripts of R020’s tag day adapted for the final tag ($DYNG_SCRATCH/r021/cert/), and recorded withparity/certify.py check --version 0.2.0under their existing names, replacing the release candidate’s records:packaging:distributions:release.yml’sselectstep forGITHUB_REF_NAME=v0.2.0(it must reportkind=package,version=0.2.0,prerelease=false,plugins=true, the three distributions;v0.2.0rc1andv0.2.0-rc.1refused),ci/wheel_check.py --release-metadata(and--release-date 2026-10-14),ci/wheels.sh,ci/plugin_wheels.shwith the pinned CI toolkits (CUDA 12.9 and 13.4),twine check --strict,ci/wheel_check.pyon each file and--release-set --split, then fresh Python 3.12 and 3.13 venvs: the core wheel alone and with each plugin (the wheel’s test subset, the README quickstart,dyng --version, and with a pluginci/plugin_smoke.py --expect cudaandpytest -m gpuon GPU 1), and the final release’s documented install lines,pip install dyng,pip install "dyng[cu13]"and"dyng[cu12]"without a version, against a local simple index holding the three distributions with PyPI as the extra index (PyPI has onlydyng0.1.0 and no plugin yet, so pip must pick 0.2.0 from the local index and the extras must resolve to the plugin of the same version); and, from the second run, the upgrade from 0.1.0 (step 3): a venv withdyng==0.1.0from PyPI keeps it forpip install "dyng[cuN]"and gets 0.2.0 and the plugin of the same version withpip install -U "dyng[cuN]", whose plugin smoke test then passes on GPU 1 (cu12 and cu13, 3.12 and 3.13).repo:check-parity(ci/check.sh --parity),api-check(ci/api_check.sh 406c3a6, the frozen C++ API and the Python API againstmainas pushed, the merge of 0.2.0rc1) andgpu_local(ci/gpu_local.sh, every step on GPU 1, the compute-sanitizer steps included).tests(README.md, whose C++ quickstart is a test, changed):asan,tsan,tsan-openmp(ci/sanitizers.sh;tsan-openmpwith R020’s local Clang 18 wrapper and Archer, deviation 5) andmutation-ctests(ctest --preset dev-cuda -L mutation, GPU 1).
Added:
sanitizers-hosted, the hostedsanitizers.ymljobs of the release candidate’s pull request #11 (run 37870378099 on its headd553b79, the eventpull_request, which tested the merge5694faaofd553b79into8b9067e, the same tree):asan / gcc-13839 of 839 tests,tsan / gcc-13745 of 745,tsan-openmp / clang-18839 of 839, all green (release step 3), with the job logs (gh run view --log, read-only) as evidence and the scopelibrary(deviation 4).The smoke test of 0.2.0rc1 from TestPyPI (step 1) is described in the certificate’s
README.md, its combined log copied tochecks/smoke-rc1.txt, as for 0.1.0.Then
parity/certify.py write --version 0.2.0on the committed tree, which checks every record against the final commit in the paths of its scope and writesparity.jsonnaming 0.2.0 and that commit, and the tables of the certificate’sREADME.md; it is the last commit of the branch.
The results of the re-run checks are in the certificate (benchmarks/results/0.2.0/README.md, its
“Checks” table generated from parity.json, every log named with its SHA-256 in checks.json and
excerpted under checks/): certify.py write passes only when every part passed on the release’s
code, so a certificate on this branch means they did.
Deviations from the plan#
The step-10 rehearsal’s directory (step 1):
$DYNG_SCRATCH/r021/release-0.2.0rc1instead of$DYNG_SCRATCH/release-0.2.0rc1, to keep the milestone’s files together.One machine for step 8 (step 1): release.md names a CPU machine and the GPU machine; only the GPU machine was available, as in R011. The CPU package’s host backends ran on it, and the plugins without a GPU or a driver were covered by the two fallbacks and by the release run’s hosted install tests without a GPU.
More than the docs ask (step 1): the CPU wheel’s whole pytest suite, the
dyng[cuN]extras from TestPyPI, and the step-10 rehearsal. Nothing failed.The hosted sanitizer record has the scope
library. Release step 3 records the hostedsanitizers.ymljobs of the release pull request with--scope tests. Those of pull request #11 testedd553b79, whosetestsscope differs from the final tree inREADME.md(only its status block and install lines; the C++ quickstart is unchanged), so atestsrecord would be stale andcertify.py writewould refuse it. The library is the release’s, so the record is made with--scope library, and thetestsscope is covered by the local presets run again on the final tree (which are the required checks).release.mdstep 3 now says so. The hosted jobs of the final-release pull request itself run once it is opened; they can be recorded with--scope testsin the tag-day pull request if one is needed (A tag day later than the final-release pull request).tsan-openmplocally uses R020’s wrapper of the conda Clang 18.1.8 (--gcc-install-dirGCC 12,--sysroot=/,-Wno-deprecated-declarations) and itslibarcher.so.bak(DYNG_ARCHER_LIBRARY), as R020 deviation 2 describes; the hostedtsan-openmp / clang-18job remains the reference.The documented install of the final release cannot run against PyPI before the release, so the
distributionscheck runspip install dyngandpip install "dyng[cuN]"(no version) against a local simple index with the three distributions and PyPI as the extra index, which is what PyPI will serve after the three uploads.CMakeLists.txtkeeps its outdated benchmark text in 0.2.0 (step 3, finding 7). The optionDYNG_BUILD_BENCHMARKSis described as “Build nvbench / Google Benchmark / dyng-bench”, a comment says the suites “come in 0.2”, and with the option ON configure warns “has no effect yet (the benchmark suites of 0.2)”.CMakeLists.txtis a path of thelibraryscope (it builds every measured program), so changing even these strings would make the carried-over gates, golden replays and golden mutations stale:certify.py writewould refuse them and 0.2.0 would have to be measured again (steps 1-3), for a message that only appears when a user turns the option ON. The text is fixed onmainafter the release (open items); the user-facing pages (CONTRIBUTING.md, the how-to) already say the suites are planned for a later release.The micro-benchmark suites and
dyng-bench(PLAN 8.5, item 1, and the targets of 4.2) are not in 0.2.0.CMakeLists.txtandCONTRIBUTING.mdhad placed them in 0.2 and in the 0.1 series; no milestone of 0.1 or 0.2 built them. The release gates run the paper suitesbenchmarks/paper/*.yamlwithparity/bench_suite.pyinstead ofdyng-bench run(as since R010), and the operators’ micro-benchmarks have no replacement. They move to a later release; the roadmap’s next releases do not list them yet.
Step 3: the review of the final-release pull request#
Independent reviewers read the branch at 0512066; each finding was checked by a second reader.
All were fixed in this branch, except the CMake part of finding 7 (deviation 7):
# |
Finding (severity) |
Fix |
Commit |
|---|---|---|---|
1 |
The maturity lines of |
the lines say “new in 0.2.0” / “since 0.2.0”; the tutorial works on the |
|
2 |
|
the block reads |
|
3 |
The who-does-what table named only |
both branches named: |
|
4 |
The roadmap’s “Where we are” opened with “0.1.0 is released” (low) |
it opens with 0.2.0 (released, how to install and upgrade, the certificate), then 0.1.0 as history |
|
5 |
The certificate README’s “What was run, and how (measured for 0.2.0rc1)” described check records that 0.2.0’s |
a 0.1.0-style opening paragraph says which of the release candidate’s records were replaced and by what |
the certificate commit |
6 |
The certificate README’s “Carried over” paragraph and this page named |
both name the measured commits |
this page, the certificate commit |
7 |
|
|
|
8 |
No upgrade path was documented: with |
|
|
These changes touch README.md (tests scope), CHANGELOG.md and CONTRIBUTING.md
(packaging) and documents (repo), none a path of the library scope, so the certificate still
carries over; every check of the tests, packaging and repo scopes was run again from fresh
clones of the last commit of this page and recorded in place of the first run’s records, and
parity/certify.py write ran again on the committed tree.
Readiness for the tag and the three PyPI approvals#
The orchestrator pushes the branch and opens the final-release pull request (base
main, headrelease-0.2.0-final), with the checklist of release.md. Required checks: the 27 of themainruleset. Before the author merges it, a manualwheels.ymlrun with the plugins, green on the pull request’s head commit, is linked in the pull request (release step 4: the only build of the published setup before the tag).The author merges the pull request (merge commit, ADR 0019).
The tag day, 2026-10-14 (UTC): on the merge commit
python ci/wheel_check.py --release-metadata --release-date todaypasses; if the tag falls on a later day, the dates move in a small pull request first and the packaging and repo checks run again (A tag day later than the final-release pull request). The AI assistant pushes the signed tagv0.2.0on the author’s behalf (git tag -s -m "dynG 0.2.0" v0.2.0, GOVERNANCE.md 2026-10-06).release.ymlbuilds the distributions anew from the tag (select: a final release, plugins on), uploadsdyng-cu12anddyng-cu13to TestPyPI (the projects exist there since 0.2.0rc1, so their publishers are ordinary ones now), thendyng, then waits forpypi-cu12andpypi-cu13.Before the author approves
pypi-cu12andpypi-cu13, the AI assistant runs release.md’s “The GPU tests of the published plugin wheels” on that run’sdistartifact on GPU 1 (step 1 rehearsed it: onlyrun=changes) and reports the result.The author approves
pypi-cu12andpypi-cu13in one dialog; their uploads create the projectsdyng-cu12anddyng-cu13on PyPI through the pending publishers the author checked on 2026-10-09. If a plugin job fails, the publisher is fixed on PyPI and the failed jobs of the same run are re-run (release.md, “If a publish job fails”);dyngstays unpublished until both plugins are there. Then the author approvespypi.The AI assistant creates the GitHub Release
v0.2.0from the CHANGELOG section with the run’s distributions (step 11); no DOI (step 12: Zenodo is not connected). After the merge thedeployjob ofdocs.ymlpublishes the site without the banner.Step 14: the announcement, the next
.dev0(0.3.0.dev0) inmain.
Open items#
For the author:
Merge the final-release pull request once it is green (and the
wheels.ymlrun is linked).The three PyPI approvals of the
v0.2.0run:pypi-cu12andpypi-cu13together, thenpypi, after the GPU tests of the run’s plugin wheels are reported green.Optional, not due for 0.2.0: connecting Zenodo before the GitHub Release would give 0.2.0 a DOI (checkpoint A4); otherwise 0.2.0 has none, as
docs/citing.mdnow says.
For the orchestrator:
Push the branch, open the pull request, dispatch
wheels.yml(with the plugins) on its head and link the run; report to the author when it is green and ready to merge.The tag day: if the tag is not pushed on 2026-10-14 (UTC), release.md’s late-tag-day procedure applies before the tag.
Optional: the hosted
sanitizers.ymljobs of this pull request can be recorded with--scope testsin a tag-day pull request (deviation 4); the certificate does not require them.mainof the development repository should followorigin/main(406c3a6); the API check of this milestone was run against406c3a6explicitly.After the release, on
main(0.3.0.dev0): bringCMakeLists.txt’sDYNG_BUILD_BENCHMARKSdescription, comment and warning to “planned for a later release” (deviation 7), and give the micro-benchmark suites anddyng-bencha release indocs/roadmap.mdanddocs/developer/plan.md(deviation 8).