Skip to content

ci(release): toggle-gated PyPI publishing for all release tags (+ docs)#319

Closed
gbrlcustodio wants to merge 6 commits into
devfrom
docs/reconcile-release-publish-list
Closed

ci(release): toggle-gated PyPI publishing for all release tags (+ docs)#319
gbrlcustodio wants to merge 6 commits into
devfrom
docs/reconcile-release-publish-list

Conversation

@gbrlcustodio

@gbrlcustodio gbrlcustodio commented Jun 19, 2026

Copy link
Copy Markdown
Member

What

Make PyPI publishing a separate, toggle-gated CI job that covers every release tag (betas included), and reconcile RELEASE.md with the workflow.

Why

RELEASE.md claimed only pipefy-cli and pipefy-mcp-server publish to PyPI, gated to v1. tags. Two problems:

  1. Which packages. release.yml stages and uploads all five wheels, and the dependency closure forces that: pipefy-cli/pipefy-mcp-server depend on pipefy-sdk/pipefy-auth, which depend on pipefy-infra. Published wheels declare these as bare, unpinned deps (the [tool.uv.sources] workspace mapping applies only in dev), so a public pip install only resolves if every package in the closure is on PyPI.
  2. When. The v1.-only gate was a stand-in for the PyPI account not being provisioned yet, not a deliberate policy. The intended end state publishes all release tags, including the v0.2.0-beta.* line.

Changes

.github/workflows/release.yml

  • Split publishing into a separate publish-pypi job (needs: release) under a dedicated pypi GitHub Actions environment, with id-token: write. The release job keeps contents: write and now uploads dist/ as an artifact the publish job consumes. Least-privilege per job; the top-level permissions block is removed.
  • Replace the startsWith('v1.') && !contains('-') step gate with a job-level toggle: if: vars.PYPI_PUBLISH_ENABLED == 'true'. Off by default (job skips), so betas and finals don't attempt PyPI until the account and the five pending publishers exist. The sec team flips the variable to turn it on, no code change.

RELEASE.md

  • Reframed around the toggle instead of the v0.x/v1 split: every tag builds + creates a GitHub Release; PyPI publishing layers on top when PYPI_PUBLISH_ENABLED is true, for all release tags.
  • Repository-setup section lists all five distributions, the single-batch failure mode, pending publishers for the first publish (owner pipefy, repo ai-toolkit, workflow release.yml, environment pypi), the toggle, and the optional environment protection rules.

Sec-team / maintainer prerequisites (before flipping the toggle)

  1. Provision the PyPI account/org and create five pending publishers (pipefy-sdk, pipefy-mcp-server, pipefy-cli, pipefy-auth, pipefy-infra), all pointing at owner pipefy, repo ai-toolkit, workflow release.yml, environment pypi. All five names are currently available on PyPI.
  2. Create the pypi GitHub Actions environment (add protection rules if you want an approval gate).
  3. Set repo variable PYPI_PUBLISH_ENABLED=true. One missing publisher fails the whole release, so do this only once all five exist.

RELEASE.md claimed only pipefy-cli and pipefy-mcp-server publish to PyPI
(with pipefy-sdk held back), but release.yml stages and uploads all five
wheels. The dependency closure forces this: cli and mcp depend on sdk and
auth, which depend on infra, so a public pip install only resolves when
every package is on PyPI. Reconcile the docs to match the workflow and
note the single-batch upload and pending-publisher requirements.
@gbrlcustodio
gbrlcustodio force-pushed the docs/reconcile-release-publish-list branch from 9da6d65 to 746b5f3 Compare June 19, 2026 17:22
… tags

Split PyPI publishing into a separate publish-pypi job that runs after the
GitHub Release, scoped to a dedicated pypi environment with id-token: write.
The release job keeps contents: write and uploads the built dist/ as an
artifact the publish job consumes.

Replace the v1.-only step gate with a job-level toggle,
vars.PYPI_PUBLISH_ENABLED == 'true'. The version-based gate was only ever a
stand-in for the PyPI account not being provisioned yet; the intended end
state publishes every release tag, betas included. Off by default so nothing
attempts PyPI until the account and the five pending publishers exist, then
the sec team flips the variable with no code change.

Reframe RELEASE.md around the toggle instead of the v0.x/v1 split.
@gbrlcustodio gbrlcustodio changed the title docs(release): publish all five wheels to PyPI on v1. tags ci(release): toggle-gated PyPI publishing for all release tags (+ docs) Jun 19, 2026
…upload

The publish-pypi staging step re-globbed and re-asserted exactly one wheel
per package, but the release job already verifies that before uploading the
immutable artifact. Collapse the step to a wheels-only copy (dist-pypi still
exists to exclude sdists from the PyPI upload).

Gate the dist artifact upload on PYPI_PUBLISH_ENABLED so non-publishing
releases don't pack and store an artifact nobody downloads.
- skip-existing on the PyPI upload so a re-pushed tag at an already-published
  version is idempotent instead of failing on PyPI's 400.
- retention-days: 1 on the dist artifact (a pure intra-run handoff).
- concurrency group so two tags pushed together don't race the artifact/version.
- Verify expected wheels now also asserts the total wheel count, so a future
  workspace member can't be silently published by the cp dist/*.whl staging.
- setup-python in publish-pypi (the smoke step no longer relies on the runner
  shim) and a nullglob guard with a clear message on the staging copy.
Published wheels declared bare, unpinned inter-package deps (pipefy-cli ->
pipefy-sdk/pipefy-auth -> pipefy-infra), so once on PyPI a user could resolve
pipefy-cli==X against a newer pipefy-sdk and get a mismatched, broken install.
Pin each sibling to ==<lockstep version> so the closure installs in lockstep.

bump_version.py now rewrites these pins on every bump and verify() asserts them,
keeping the pins in lockstep with the shared version. The [tool.uv.sources]
workspace mapping still overrides them for in-repo dev.
Publishing is now controlled by the PYPI_PUBLISH_ENABLED toggle, not the v1.0
tag, so docs that tied PyPI to v1.0 were stale. Fix the RELEASE.md post-publish
smoke command to allow pre-releases for beta tags, correct the sdk README's
'gated separately' claim to the single-batch model, and reword README/MIGRATION/
DEPRECATION/cli/mcp install notes to key on PyPI presence rather than v1.0.
@gbrlcustodio

Copy link
Copy Markdown
Member Author

Closing this in favor of the changes that already landed plus two focused follow-ups.

The core goal here (publish all five wheels to PyPI on every release tag) shipped in #365 via a simpler path: drop the v1.-only gate from the three PyPI steps, keep a single job, publish over OIDC/Trusted Publishing. It is live: 0.3.0-alpha.1 published all five distributions to PyPI, and trusted publishers are configured for each.

Given that, a rebase of this branch is not worth it: the diff is stale against the #361 SDK rename (pipefy_sdk -> pipefy, pins ==0.2.0-beta.3) and would need conflict resolution across ~14 files, and its off-by-default PYPI_PUBLISH_ENABLED toggle would re-gate publishing that now works (its rationale, "while the PyPI account is being provisioned", no longer holds).

The genuinely valuable pieces are carried forward, rewritten for the current name and version:

Not carried forward (can be a separate hardening PR if wanted): splitting publish-pypi into its own job under a dedicated pypi environment with per-job least-privilege permissions and an optional approval gate.

@gbrlcustodio
gbrlcustodio deleted the docs/reconcile-release-publish-list branch July 4, 2026 05:09
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