Skip to content

PRODUCE-PY P2r: release 0.1.0, and the owner's publish script (typedstandards#135) - #4

Merged
npstorey merged 6 commits into
mainfrom
ts135/p2r-release
Oct 4, 2026
Merged

npstorey merged 6 commits into
mainfrom
ts135/p2r-release

Conversation

@npstorey

@npstorey npstorey commented Oct 4, 2026

Copy link
Copy Markdown
Owner

This is the release PR for PRODUCE-PY phase P2r (npstorey/typedstandards#135; G0 D3 = A: one release, 0.1.0, after P2; D4 = A: the owner publishes from their terminal with a tested script). It cuts typedstandards 0.1.0, which pins @typedstandards/cli 0.2.0, and adds the script that publishes it. The ORCH wrote it, and the owner publishes after the merge.

Branch ts135/p2r-release, head 3db7f61ecfb5573a681a659afb4642bbfa53bd4e, on main at 36b59f3 (the P2f merge). git diff --numstat main...HEAD:

1    1  CHANGELOG.md
201  0  scripts/publish.sh
1    1  scripts/smoke_wheel.py
1    1  src/typedstandards/__init__.py

What lands

  • The version cut (d02e59e): __version__ = "0.1.0". The ## Unreleased entries in CHANGELOG.md move, verbatim, under ## 0.1.0 — 2026-10-04, the publish day the owner named. uv.lock is unchanged: uv lock --check passes, since the lock records no version for the project. CLI_VERSION stays 0.2.0.
  • scripts/publish.sh, which the owner runs from a clean checkout of main at the release merge. It never prints the token.
    • DRY_RUN=1 scripts/publish.sh makes these checks and uploads nothing:

      • the checkout: it is clean, and HEAD equals origin/main;
      • the version is X.Y.Z;
      • a ## <version> — <date> heading exists;
      • PyPI answers 404 for the version.

      It then:

      • builds the sdist, and the wheel from the sdist (the build vendors the CLI with npm ci);
      • runs twine check --strict;
      • checks that the wheel carries the vendored CLI at CLI_VERSION;
      • installs the wheel into a fresh environment and runs scripts/smoke_wheel.py;
      • runs uv publish --dry-run, then checks that PyPI still answers 404.

      It records dist/SHA256SUMS and dist/COMMIT, and prints both files' SHA-256.

    • op run --env-file=pypi.env -- scripts/publish.sh is the live run. It refuses in five cases: the token is missing, the token does not start with pypi- (an op:// reference op run did not resolve, or a quoted value; 3db7f61, asked by the seat; the value is never printed), the CHANGELOG heading is not today's date, dist/ was built at another commit, or a file does not match SHA256SUMS. Otherwise it uploads exactly the two files the dry run built and checked, with uv publish --check-url https://pypi.org/simple/. Then:

      • It reads https://pypi.org/pypi/typedstandards/<v>/json back for up to 300 seconds and compares every file's SHA-256.
      • It installs typedstandards==<v> from PyPI into a fresh environment, which must print CLI_VERSION 0.2.0.
      • A failed upload says to read back before any retry.
      • A read-back that times out says to run it again, never that the upload did not land.
      • A file PyPI lists with a different SHA-256 stops it at once (exit 2), since a version cannot be uploaded twice.
    • READ_BACK=1 scripts/publish.sh runs only the read-back and the install check.

  • scripts/smoke_wheel.py (one word): it now resolves the CLI's entry path before comparing it with the resolved package path. It failed whenever the environment sat behind a symlink, which is what macOS's default temporary directory is (/var → /private/var). The wheel was correct; the check was not. The publish script's first full dry run found it.

Evidence (driven on 2026-10-04 by the ORCH, macOS, Node 24.21.0, uv 0.11.25)

Red: a development version is refused (at c8bccdb, __version__ 0.1.0.dev0):

$ DRY_RUN=1 RELEASE_REF=HEAD scripts/publish.sh
publish: __version__ is '0.1.0.dev0', not a release version (X.Y.Z)
exit 1

Red: the smoke check failed on a throwaway 0.1.0 candidate commit, at assert entry.is_relative_to(package / "_vendor") (AssertionError: /var/folders/…/typedstandards/_vendor/node_modules/@typedstandards/cli/dist/bin/main.js). Green: after f9d7711, smoke check passed.

The read-back was driven against a release that exists on PyPI, httpx 0.28.1, with its two files downloaded and their SHA-256 recorded:

--- green: correct hashes
publish:   httpx-0.28.1-py3-none-any.whl  d909fccc…59ad  PyPI: same SHA-256
publish:   httpx-0.28.1.tar.gz  75e98c5f…42fc  PyPI: same SHA-256
publish: read back: PyPI lists httpx 0.28.1 with the SHA-256 of every file built and checked
exit 0
--- red: one hash altered (after e38da72; before it, the script waited out its window instead)
publish:   httpx-0.28.1-py3-none-any.whl  0909fccc…59ad  PyPI: DIFFERENT SHA-256 d909fccc…59ad
publish: PyPI lists a file of httpx 0.28.1 with a different SHA-256 from the file built here,
publish: so what PyPI serves is not this build. A version cannot be uploaded twice: do not retry.
exit 2
--- red: a version PyPI does not have
publish: PyPI does not list httpx 0.28.99 yet   (three tries)
publish: PyPI has not listed every file with these hashes within 20 seconds.
publish: An upload can take longer to appear; this alone does not mean it failed.
publish: Run the read-back again: READ_BACK=1 scripts/publish.sh
exit 1

The live run refuses before any upload. This was driven on a throwaway 0.1.0 commit after a dry run there, with scripts/publish.sh as at d02e59e. The only change to the script since is the token check below (3db7f61).

--- no token:                 publish: UV_PUBLISH_TOKEN is not set: run through op run --env-file=pypi.env          exit 1
--- a tampered wheel:         publish: dist does not match SHA256SUMS; run the dry run again                       exit 1
--- dist from another commit: publish: dist was built at 1900138…, not HEAD; run the dry run again                exit 1
--- CHANGELOG dated 10-03:    publish: CHANGELOG.md dates 0.1.0 2026-10-03, and today is 2026-10-04: the heading names the publish day   exit 1
PyPI after all of this: 404

Red, then green: a token that is not a PyPI token (asked by the seat). Red at d02e59e: an op:// string and a quoted value both passed the token check, and each run stopped only at the next check:

$ UV_PUBLISH_TOKEN='op://Private/PyPI/credential' RELEASE_REF=HEAD scripts/publish.sh
publish: run DRY_RUN=1 scripts/publish.sh first: the live run uploads what it built
$ UV_PUBLISH_TOKEN='"pypi-AgEIcHlwaS5vcmc"' RELEASE_REF=HEAD scripts/publish.sh
publish: run DRY_RUN=1 scripts/publish.sh first: the live run uploads what it built

Green at 3db7f61, after a dry run in the same clean clone:

$ UV_PUBLISH_TOKEN='op://Private/PyPI/credential' RELEASE_REF=HEAD scripts/publish.sh
publish: UV_PUBLISH_TOKEN does not start with "pypi-", so it is not a PyPI API token: an op:// reference op run did not resolve, or a quoted value, reads this way. pypi.env holds one unquoted line, UV_PUBLISH_TOKEN=op://<vault>/<item>/<field>; run: op run --env-file=<path to pypi.env> -- scripts/publish.sh
exit 1
$ UV_PUBLISH_TOKEN='"pypi-AgEIcHlwaS5vcmc"' RELEASE_REF=HEAD scripts/publish.sh
publish: UV_PUBLISH_TOKEN does not start with "pypi-", … (the same message)
exit 1
--- a pypi- dummy passes every check up to the upload step; a test shim on PATH intercepted `uv publish`, so nothing was sent
$ PATH=<shim>:$PATH UV_PUBLISH_TOKEN='pypi-dummy-not-a-token' RELEASE_REF=HEAD READ_BACK_SECONDS=20 scripts/publish.sh
typedstandards-0.1.0.tar.gz: OK
typedstandards-0.1.0-py3-none-any.whl: OK
publish: uploading typedstandards-0.1.0.tar.gz and typedstandards-0.1.0-py3-none-any.whl
uv-shim: intercepted uv publish --check-url https://pypi.org/simple/ dist/typedstandards-0.1.0.tar.gz dist/typedstandards-0.1.0-py3-none-any.whl (test only; nothing sent)
publish: reading typedstandards 0.1.0 back from https://pypi.org, for up to 20 seconds
publish: PyPI does not list typedstandards 0.1.0 yet   (three tries)
publish: Run the read-back again: READ_BACK=1 scripts/publish.sh
exit 1
PyPI typedstandards after all of this: 404

Green: the release dry run from a clean clone at this head, 3db7f61:

publish: checkout 3db7f61 = HEAD; typedstandards 0.1.0 (CHANGELOG dated 2026-10-04); not on PyPI yet
Successfully built dist/typedstandards-0.1.0.tar.gz
Successfully built dist/typedstandards-0.1.0-py3-none-any.whl
Checking dist/typedstandards-0.1.0-py3-none-any.whl: PASSED
Checking dist/typedstandards-0.1.0.tar.gz: PASSED
publish: the wheel vendors @typedstandards/cli 0.2.0 (232 files)
typedstandards 0.1.0 imported from …/site-packages/typedstandards
CLI_VERSION 0.2.0; the vendored CLI's --version prints 0.2.0
6 vendored packages, each with its licence file
signed 4a4f4b0e…3765; verify ok=True status=active
helpers: pin, badge_cell, comparison_cell, sidecar and show ran from the installed wheel
smoke check passed
publish: uv publish --dry-run (its output says "Uploading"; it sends nothing):
publish: PyPI still answers 404 for typedstandards 0.1.0
publish:   e56454add4e876631b0ec34ebcc988938b88e5d0f6286fc00b3407158d3e019d  typedstandards-0.1.0.tar.gz
publish:   ab96e211f92e9f49ec00fd8fbb8ff9b11afb81068458e1e946696cd5f6bbd469  typedstandards-0.1.0-py3-none-any.whl
publish: DRY_RUN: nothing was uploaded.
exit 0

RELEASE_REF=HEAD stands in for origin/main, because this commit reaches main only at the merge. The owner's dry run uses the default.

Also at the head:

  • uv run pytest: 238 passed (Python 3.12.13).
  • ruff check: clean.
  • gitleaks over main..HEAD: no leaks.
  • Commits: six, each signed (G) with one Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>.
  • The wheel's SHA-256 (ab96e211…) is the same at d02e59e and 3db7f61: the build is reproducible, and only the sdist, which carries the script, changed.

After the merge: the owner's publish (today, 2026-10-04)

From a clean checkout of main at the merge, with node, npm and uv on PATH:

git -C ~/code/typedstandards-python checkout main && git -C ~/code/typedstandards-python pull --ff-only
cd ~/code/typedstandards-python
DRY_RUN=1 scripts/publish.sh
op run --env-file="$HOME/.config/typedstandards/pypi.env" -- scripts/publish.sh

~/.config/typedstandards/pypi.env sits outside the repository and holds a reference, never a value: UV_PUBLISH_TOKEN=op://<vault>/<item>/<field>. Use an account-scoped token for this first upload, then replace it with a token scoped to the typedstandards project.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u

npstorey and others added 6 commits October 4, 2026 08:34
…read-back

DRY_RUN=1 checks the checkout (clean, HEAD equal to origin/main), the version
(X.Y.Z), the CHANGELOG heading and that PyPI does not have the version yet;
builds the sdist and the wheel, checks them (twine check --strict, the vendored
CLI's version), installs the wheel into a fresh environment and runs the smoke
check, runs uv publish --dry-run, and records dist/SHA256SUMS and dist/COMMIT.
It uploads nothing.

The live run (under op run, UV_PUBLISH_TOKEN) uploads exactly the files the dry
run built and checked, then reads the release back from PyPI for about five
minutes and installs it into a fresh environment, which must print CLI_VERSION.
A failed upload says to read back before any retry; a read-back that times out
says to run it again and never that the upload did not land.

Refs npstorey/typedstandards#135 (G0 D4 = A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
A file PyPI lists with a different SHA-256 is final, since a version cannot be
uploaded twice. The read-back now exits 2 at once and says so, where it waited
out its window and said the upload might still appear. A file not listed yet
still waits, and a timeout still says to run the read-back again.

Refs npstorey/typedstandards#135.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
…ckage

The check compared the resolved package directory with the unresolved CLI entry,
so it failed whenever the environment sat behind a symlink, as a venv under
macOS's default temporary directory (/var -> /private/var) does. The wheel was
correct; the check was not. Found by the publish script's dry run.

Refs npstorey/typedstandards#135.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
uv publish --dry-run prints "Uploading" for each file while sending nothing.
The dry run now says so before it runs uv, and afterwards checks that PyPI still
answers 404 for the version.

Refs npstorey/typedstandards#135.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
__version__ 0.1.0, and CHANGELOG.md's Unreleased entries, verbatim, under
"## 0.1.0 — 2026-10-04", the publish day the owner named. uv.lock is unchanged:
`uv lock --check` passes, since the lock records no version for the project.
It pins @typedstandards/cli 0.2.0 (CLI_VERSION), as the version tests check.

Refs npstorey/typedstandards#135 (G0 D3 = A: one release, 0.1.0, after P2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
An op:// reference that op run did not resolve, or a value kept with its quotes,
was sent to PyPI as the token. The live run now refuses any UV_PUBLISH_TOKEN
that does not start with "pypi-", right after checking that it is set, with a
message naming the env file's one unquoted line and the op run command. The
value is never printed. Asked by the seat at the P2r check.

Refs npstorey/typedstandards#135.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru4PYga7Zf8HANgotZKs4u
Signed-off-by: Nathan Storey <npstorey@users.noreply.github.com>
@npstorey

npstorey commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

GO — CAT PLAN (the program seat), bound to head 3db7f61ecfb5573a681a659afb4642bbfa53bd4e, with one condition.

Read from GitHub and disk, 2026-10-04:

  • The PR API: head 3db7f61…, base main 36b59f3 (the P2f merge), 4 files, +204 −3, 6 commits, merge state clean; equal to the three-dot diff.
  • Check runs on that exact head: 18, all success from GitHub Actions (15368), nine distinct names, the nine protect-main requires.
  • Before the push: six commits, each signed (G) with one Signed-off-by read by the trailer parser; the guard's keyword scan and gitleaks over the outgoing range clean; dist/ gitignored.
  • The release: __version__ = "0.1.0", CLI_VERSION = "0.2.0", the CHANGELOG heading ## 0.1.0 — 2026-10-04; scripts/publish.sh mode 100755. The seat read publish.sh whole: it prints no token; it refuses unless the checkout is clean and at origin/main, PyPI answers 404 for the version, the heading names today, the files equal what the dry run built at this commit, and (since 3db7f61) the token starts with pypi-; a stop after the upload says the upload may have landed and points at READ_BACK=1; the read-back retries for about five minutes.

Condition: the merge and the publish land on the day the heading names, 2026-10-04 in the owner's local time; publish.sh's live run refuses on any other day. If the publish cannot happen today, do not merge: re-date the heading on the branch first, which makes a new head and needs a new GO.

The owner merges with --match-head-commit 3db7f61ecfb5573a681a659afb4642bbfa53bd4e through the seat's tested script, which also tags the merge; then DRY_RUN=1 scripts/publish.sh and the live run from the main checkout at the merge.

@npstorey
npstorey merged commit 7513583 into main Oct 4, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant