Skip to content

v1.0.0 Publish Execution Plan — 2026-09-05

Index · Publish Readiness Assessment - 2026-07-24 · V1 Release Roadmap · Publishing to PyPI · Publishing Docker Images

Purpose: The single working checklist that takes testo-core from unreleased 0.1.0 to a published, installable v1.0.0 on PyPI, GHCR, and JFrog Artifactory. Supersedes as the action list: Publish Readiness Assessment - 2026-07-24 (that note stays as the point-in-time analysis; this note is what we execute). Method: One step at a time, top to bottom. A step is not done until its Done when line is true. Check boxes as we go and commit the doc with the change it describes.

Owner legend

  • AI — Claude does it in-session.
  • YOU — needs your credentials, 2FA, or org admin. Cannot be delegated.
  • BOTH — AI drives, you approve or watch.

Step map

# Step Owner Est. Status
0 Green baseline AI ~~0.5–2h~~ 0.3h actual ✅ done 2026-09-05
1 Streamlit decision AI 0.5h ✅ done — option A
2 Version + CHANGELOG roll AI 0.25h ✅ done
3 PyPI publish auth YOU 0.75h ⬜
4 GHCR permissions YOU 0.25h ⬜
5 Tag, release, firefight BOTH 1h ⬜
6 Verify public install AI 0.5h ⬜
7 Artifactory repos YOU 1h ⬜
8 Artifactory publish workflow AI 1.5h ✅ drafted — gated, awaits Step 7
9 Verify Artifactory install AI 0.5h ⬜
10 Exedra — scope unknown YOU TBD 🚧 blocked
11 Close release gates + announce BOTH 0.5h ⬜

Total remaining (steps 1–9, 11): 6–7 hours. Step 0 came in far under estimate — the drift risk did not materialize. Step 10 is unestimated until scoped.


Step 0 — Green baseline ✅

Nothing else matters until the suite is green. The tree had not been exercised since 2026-07-24; six weeks of dependency drift was the single biggest schedule risk in this plan. It did not materialize.

  • [x] Full suite: python -m pytest -q --no-cov → 624 passed, 4 skipped, 18.8s
  • [x] Lint gate: ruff check . → 2 import-sort errors, auto-fixed, now All checks passed
  • [x] Type gate: mypy testo_core → 59 errors in 24 files (advisory only, non-blocking in ci.yml)
  • [x] Frontend build: npm --prefix frontend run build → built in 1.02s
  • [x] Frontend tests: npm --prefix frontend run test → 8 files, 14 tests passed

Failures found and fixed: ruff import-organization errors in tests/contract/api/test_analytics_contract.py and tests/unit/testo_core/test_pyramid_viz.py. Fixed with ruff check . --fix; both files re-tested green (14 passed).

Carried forward, not blocking: 59 advisory mypy errors, worst offender testo_core/reporting/reporters/orchestrate.py (object has no attribute print, plus stale type: ignore codes). Belongs with Technical Debt Tracker Workstream 5 — do not expand v1.0 to fix these.

Done when: ~~all five commands exit 0~~ ✅ — four gates green, mypy advisory-only as designed.


Step 1 — Streamlit decision

The React frontend is the official UI (Phase 3 gates 100%). testo_ui/, the testo-ui entrypoint, and streamlit>=1.36.0 are still shipped. See Streamlit to React Migration Guide.

Pick one:

  • [x] A — Deprecate (chosen 2026-09-05): emit a DeprecationWarning on testo-ui entry, note it in CHANGELOG.md and README.md, schedule removal for v1.1. ~30 min.
  • [ ] ~~B — Remove now:~~ (deferred to v1.1) drop testo_ui/, the entrypoint, the [ui] extra, and the streamlit dependency. Shrinks install surface but touches packaging contract tests. ~2h.

Done when: the choice is implemented, tests still green, and CHANGELOG.md records it.


Step 2 — Version + CHANGELOG roll

  • [x] pyproject.toml: version = "0.1.0" → "1.0.0"
  • [x] Confirm testo_core.__version__ resolves to 1.0.0 → testo --version prints testo 1.0.0
  • [x] CHANGELOG.md: rolled to ## [1.0.0] - 2026-09-05, fresh [Unreleased] added, compare links updated
  • [x] python scripts/check_changelog_format.py passes
  • [x] Contract tests that assert on version still pass → full suite 624 passed
  • [x] Commit as chore(release): bump to 1.0.0 (Conventional Commits — commitlint.yml enforces this)

Done when: testo --version prints 1.0.0 and the changelog gate passes.


Step 3 — PyPI publish auth — YOU

publish.yml is release-triggered and uses OIDC trusted publishing (no long-lived token). It has never fired. Without this step, the first release run fails at the publish job. See Publishing to PyPI.

  • [ ] Register/claim the testo-core project name on PyPI
  • [ ] Add a trusted publisher: repo taltal-beep/testosterone, workflow publish.yml, environment pypi
  • [ ] Repeat on Test PyPI with environment test-pypi
  • [ ] Create GitHub Actions environments test-pypi and pypi (Settings → Environments)

Done when: both environments exist in GitHub and both trusted publishers are registered.

Name availability — checked 2026-09-05: testo-core is free on PyPI (HTTP 404, unregistered). No fallback name needed. Note that testosterone is taken, so the distribution name stays testo-core as already configured.


Step 4 — GHCR permissions — YOU

docker-publish.yml pushes ghcr.io/<owner>/testo-runner on release. See Publishing Docker Images.

  • [ ] Confirm the release job has packages: write
  • [ ] Confirm Actions is allowed to publish packages for this repo/org
  • [ ] Decide package visibility (public for OSS)

Done when: repo settings permit a GHCR push from Actions.


Step 5 — Tag, release, firefight

This is the step that actually ships. Both publish workflows fire off the GitHub Release event.

  • [ ] Create annotated tag v1.0.0 on main
  • [ ] Push the tag
  • [ ] Publish a GitHub Release with the [1.0.0] changelog section as notes
  • [ ] Watch publish.yml — Test PyPI dry-run → PyPI
  • [ ] Watch docker-publish.yml — GHCR push
  • [ ] Fix whatever fails on first run and re-run

Done when: both workflows are green.

Two release-blocking workflow bugs were already found and fixed on 2026-09-05 (see Session log) — the first-run failure budget is now smaller, but not zero.

Expect one failure cycle. These workflows have literally never executed. Budget for it rather than being surprised. If a fix requires a code change, the tag must be deleted, moved, and the release re-cut.


Step 6 — Verify public install

  • [ ] pip install testo-core==1.0.0 into a clean venv
  • [ ] testo --version → 1.0.0
  • [ ] testo cycles list against a sample repo
  • [ ] docker pull ghcr.io/<owner>/testo-runner:1.0.0
  • [ ] docker run --rm ghcr.io/<owner>/testo-runner:1.0.0 run --help

Done when: a fresh machine can install and run Testo from public registries.


Step 7 — Artifactory repositories — YOU

Net-new: there is currently zero JFrog/Artifactory configuration in the repo or the vault.

  • [ ] Create a PyPI-type local repository (e.g. testo-pypi-local)
  • [ ] Create a Docker local repository (e.g. testo-docker-local)
  • [ ] Choose auth: JFrog OIDC integration with GitHub Actions (preferred, no stored secret) or an identity token stored as a GitHub secret
  • [ ] Record the base URL, repo keys, and auth method below

Instance URL: (fill in) Repo keys: (fill in) Auth method: (fill in)

Done when: you can pip install and docker pull from those repos manually with your own credentials.


Step 8 — Artifactory publish workflow

  • [x] New .github/workflows/artifactory-publish.yml, release-triggered plus workflow_dispatch, gated on vars.ARTIFACTORY_URL so it no-ops until Step 7 lands
  • [x] Wheel + sdist upload via twine against the Artifactory PyPI index
  • [x] docker/login-action + push to the Artifactory Docker repo
  • [ ] ~~Reuse the build artifacts from publish.yml~~ — rejected: cross-workflow artifact passing adds coupling for a 40s build. Rebuilds instead.
  • [x] New vault note Publishing to Artifactory
  • [x] Linked from Index
  • [x] CHANGELOG.md entry

Done when: ~~exists, documented, dry run~~ — exists and documented ✅; dry run still pending Step 7 credentials, via workflow_dispatch with version 1.0.0.


Step 9 — Verify Artifactory install

  • [ ] pip install --index-url <artifactory-pypi-url> testo-core==1.0.0
  • [ ] docker pull <artifactory-docker-url>/testo-runner:1.0.0
  • [ ] Both smoke-tested with --version / run --help

Done when: Artifactory is a working distribution channel.


Step 10 — Exedra — 🚧 BLOCKED, needs scoping

"Exedra" appears nowhere in the codebase, the vault, or project memory. It cannot be planned or estimated until identified. Answer one of:

  • [ ] It is a package registry (which protocol — PyPI index, Docker, generic?) → treat as a clone of Steps 7–9, +1.5–2h
  • [ ] It is a deployment/runtime platform (needs Helm charts, k8s manifests, an app definition) → this is a different project, +6–10h, and should get its own plan note
  • [ ] It is an internal corporate platform → needs access details and its onboarding docs before anything can be estimated
  • [ ] It was a typo → strike this step

Done when: the step is either scoped into real tasks or struck from the plan.


Step 11 — Close gates and announce

Done when: the vault reflects a shipped v1.0.0 rather than a pending one.


Explicitly out of scope for v1.0.0

Deferred by design — do not let these expand the release:

  • Exception-handling hardening in Docker/S3/reporter paths (Technical Debt Tracker, Workstream 5)
  • External action repo (ariel-evn/uqo-action) wiring
  • trivy/grype image scanning
  • Baseline latency metrics
  • MySQL optional driver validation
  • Full Streamlit removal (if Step 1 chose option A)

Session log

Date Steps completed Notes
2026-09-05 Plan created Steps 0–11 defined; Step 10 blocked on scoping
2026-09-05 Steps 1, 2, 8 ✅ Streamlit option A (stderr notice, removal v1.1). Bumped to 1.0.0, changelog rolled. Artifactory workflow + guide landed, gated. Fixed 3 latent bugs in publish.yml / docker-publish.yml that would have failed the first release: testo-core==v1.0.0 invalid specifier, image refs pointing at a nonexistent v-prefixed tag, and latest gated on is_default_branch (never true on release). Remaining: Steps 3, 4, 7 (all YOU), then 5, 6, 9, 11.
2026-09-05 Step 0 ✅ Suite green first try (624 passed). Ruff auto-fixed 2 files. Mypy 59 advisory errors deferred to tech debt. Next: Step 1 Streamlit decision.
2026-09-27 Step 5 pre-flight Dockerfile.testo-runner ran as root, which the blocking Trivy config scan in docker-publish.yml rejects (DS002, HIGH); now runs as user testo with testo as entrypoint. [Unreleased] folded into [1.0.0] and redated, since the tag ships those changes.