chore: add PyPI release workflow#26
Merged
Merged
Conversation
b80547a to
f5164e5
Compare
ce318bd to
09a5769
Compare
15 tasks
09a5769 to
1ecc94b
Compare
62d4e4d to
516af19
Compare
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
…ishing Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
`flowmesh stack build` and `flowmesh stack push` had no way to override `FLOWMESH_VERSION` / `FLOWMESH_BUILD_REF` per invocation. Setting them in the shell did not take effect because `_run_bake` calls `load_env` before reading `os.getenv`, and `load_env` overwrites `os.environ` from the env file — so a stale `.env` value clobbered the shell value. The flags now act as explicit overrides applied after the env-file load, matching the `--image-tag` pattern already on `pull`/`up`/`down`. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
The release flow ships container images alongside PyPI distributions, but the actual `flowmesh stack push` happens on a GPU build host outside CI (no self-hosted runners on the public repo). `release-images.yml` is the post-publish auditor: anonymously reads each of the six expected refs from GHCR, asserts the multi-arch index, OCI version label, and revision label match the GitHub Release tag and the tagged commit, then retags the set as `:latest` from the `ghcr` environment with a downgrade guard on existing `:latest`. The digest table is appended to the Release body idempotently via sentinel-marker replacement. Verifier accepts both OCI indexes and legacy Docker manifest lists so it doesn't reject valid buildx output. Retag aborts on unexpected inspect failures rather than silently treating them as a missing `:latest`. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
5943b8e to
95d1e2b
Compare
Zizmor's `template-injection` pedantic check flagged the `docker login`
step for expanding `${{ secrets.GITHUB_TOKEN }}` and `${{ github.actor }}`
directly into the shell script. Bind both to step-level env vars and
reference them as `$GHCR_TOKEN` / `$GHCR_USER` so the runner doesn't
splice attacker-controllable text into the script body.
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Both check_release_version.py and bump_version.py hand-rolled their own version regex, which disagreed on whether the leading 'v' was required and accepted strings PEP 440 itself would reject. Route both through packaging.version.Version: tags and pyproject versions are parsed and compared as Version objects, with directed "not PEP 440" errors on malformed inputs. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Catch InvalidVersion on the input --tag so a non-PEP-440 tag exits with a directed "::error::tag <x> is not PEP 440" rather than an uncaught stacktrace. Expand the warning printed when --force suppresses a TransientInspectError to spell out that the downgrade guard is OFF and that :latest may silently move backward — the previous wording read as "just a transient blip, proceeding" and hid the real consequence from the operator. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
A typo like NODE_ROLE=worke on a worker node previously fell through to the "default to root" branch, deploying Redis containers and routing as a root node. Treat unset as root (unchanged), but raise typer.Exit with a directed error on any other unrecognized value. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Pin the three return states of _existing_latest_version (missing reference, transient inspect failure, parsed Version) plus the label-parsing branches that distinguish "no version label" from "label is not PEP 440". Add a parametrized matrix over _is_release covering plain releases, post-releases (eligible) and pre/dev/local/non-PEP-440 tags (rejected). Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Note in docs/CLI.md that .env values always win over shell-set FLOWMESH_VERSION / FLOWMESH_BUILD_REF, so --image-tag / --build-ref are the only way to override without editing the file. Document in the release.yml package_pattern input help that patterns are split on commas and therefore cannot contain a literal comma. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
timzsu
requested changes
May 12, 2026
Collaborator
timzsu
left a comment
There was a problem hiding this comment.
Two inline comments, plus this test coverage issue:
The release script tests cover the helper predicates well, but the main release-safety paths are still lightly tested. It would be useful to add mocked tests for check_image_release._check_target() and retag_image_release._plan_retags(), especially around missing labels, platform mismatch, and newer/equal/older :latest behavior. That would also make the builder-label issue easier to catch in unit tests.
The cuda.builder Dockerfile was the only published target missing the ARG/LABEL block, so release-images.yml's _check_target would have failed verification for flowmesh_worker_builder:<tag>-gpu on its per- platform image.version / image.revision asserts. Match the pattern already used by Dockerfile.cuda, Dockerfile.cpu, Dockerfile.ssh.*, and the server Dockerfile: declare BUILD_VERSION / BUILD_REF / BUILD_CREATED ARGs (the bake target already passes them) and emit the three opencontainers labels. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
filter_distributions.py replaces the inline bash glob filter in release.yml. fnmatch.fnmatchcase keeps the same matching semantics without the IFS / case quirks, and the helper is now unit-testable for empty pattern sets, zero-match outcomes, and whitespace-only patterns — all paths that would have silently misbehaved in shell. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Pin the release-safety paths flagged in review. _check_target tests exercise OCI-vs-Docker manifest-list acceptance, non-index mediatype rejection, platform-set mismatch, attestation-manifest filtering, and per-platform image.version / image.revision drift. _plan_retags tests walk older / equal / newer :latest, --force override, MissingVersionLabel and TransientInspectError both with and without --force, and the invalid-tag exit. Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Add the release automation needed to publish FlowMesh's lightweight PyPI distributions and its GHCR container images from immutable release tags. The release pipeline builds and verifies every published distribution, validates synchronized versions and internal pins, smoke-tests the umbrella extras, publishes through PyPI Trusted Publishing, then verifies the matching container images and retags them as
:latestwith downgrade protection.Changes
.github/workflows/release.yml) — build/verify and publish jobs, manual TestPyPI/PyPI dispatch,package_patternfilter for staggered Trusted Publisher onboarding,twine checkdistribution metadata validation, and a tag-reachability guard that aborts unless the release tag is an ancestor oforigin/main..github/workflows/release-images.yml) — verify-and-retag pipeline for the six GHCR images: anonymous read of each published ref, multi-arch manifest + OCI version/revision label assertions against the release tag and commit SHA, then a:latestretag with downgrade-protection (overrideable viaforce_latestdispatch input). Idempotently appends a digest table to the GitHub Release body via sentinel markers.scripts/ci/,scripts/dev/) —check_release_version.py(synchronized versions + internal pin agreement),check_image_release.py(manifest/label verifier),retag_image_release.py(downgrade-protected:latestretag, accepts OCI indexes and legacy Docker manifest lists),append_release_digests.py(Release-body digest table writer with null-body normalization), andbump_version.py(synchronized version + pin bump helper).cli/stack/src/flowmesh_cli_stack/stack.py,docs/CLI.md) —--image-tagand--build-refonflowmesh stack build/stack push, applied afterload_envso they actually override the.envvalue. Matches the pre-existing--image-tagpattern onpull/up/down.docs/RELEASE.md,CONTRIBUTING.md) — PyPI Trusted Publishing setup, release prep, TestPyPI rehearsal, PyPI publishing, image push from the build host, GHCR first-time setup (visibility + repo link +ghcrenvironment), install verification, and an "If a release goes wrong" recovery section (PyPI immutability, yank, next-patch,.postN, image-only re-run).tests/scripts/test_append_release_digests.py) — regression coverage for_read_current_body'snull/ empty / whitespace normalization and the sentinel strip-and-replace contract.sdk/,sdk/stack/,cli/,cli/stack/,hook/pyproject.toml, plusLICENSEcopies under each sub-package) — bump[build-system].requirestosetuptools>=77, switch to SPDX-stringlicense = "Apache-2.0", declarelicense-files = ["LICENSE"]. Each published wheel now carries its own Apache-2.0 text under*.dist-info/licenses/LICENSEand declaresLicense-Expression: Apache-2.0instead ofLicense: UNKNOWN.Design
origin/main. PyPI versions and GHCR image refs are derived from the tag, never frommain.package-build.ymlstill smoke-tests PRs;release.ymlowns tag-based artifacts and PyPI upload.release-images.ymlis the post-publish auditor for the GHCR image set built from a GPU build host outside CI (no self-hosted runners on the public repo).pypi/testpypienvironments host the OIDC claim; production publishing can require manual approval without static upload tokens. Aghcrenvironment gates:latestretag with the same approval model.check_release_version.pyfails if any of the six package versions diverge from thevX.Y.Ztag or if any first-party dependency pin still points at an older version.bump_version.pyapplies the same policy locally so prep is mechanical.:latestpolicy._is_release(incheck_image_release.py) gates:latestto non-prerelease, non-dev, non-local versions;.postNis eligible.retag_image_release.pyparses the existing:latest's OCIimage.versionlabel and refuses to retag when it points at a newer or equal version (overrideable with--force). A transient inspect failure aborts rather than silently disabling the guard.flowmesh[sdk]/flowmesh[cli]/flowmesh[hook]resolve to multiple separately-published wheels; each declaresApache-2.0and ships LICENSE text for Apache § 4 redistribution and so PyPI / audit tools don't reportLicense: UNKNOWN.Test Plan
Test Result
check_release_version.py --tag v0.1.0→ exit 0, "Release package versions are synchronized at 0.1.0".bump_version.py 0.1.0 --check→ exit 0, "Package versions are already set to 0.1.0".pre-commit run --all-files→ all hooks pass.pytest tests --ignore=tests/worker/test_mp_executor_cleanup_gpu.py→ 759 passed.uv build --all-packages→ produced all six distributions (flowmesh,flowmesh-sdk,flowmesh-sdk-stack,flowmesh-cli,flowmesh-cli-stack,flowmesh-hook) as.whl+.tar.gz.check_package_build.py --dist <dist>→ smoke tests pass forflowmesh[sdk],flowmesh[hook],flowmesh[cli];flowmesh --helpruns from the built CLI wheel.twine check dist/*→ all distributions pass metadata validation.*.dist-info/licenses/LICENSE(11351 bytes, root Apache-2.0 text) and declaresLicense-Expression: Apache-2.0+License-File: LICENSEin METADATA.Pre-submission Checklist
pre-commit run --all-filesand fixed any issues.uv run pytest tests/passes locally.uv sync --all-packages --group ci --frozen).[BREAKING]and described migration steps above.