Skip to content

ci: publish cytnx to PyPI on tag (stable) and master push (.devN nightly)#866

Draft
IvanaGyro wants to merge 5 commits into
masterfrom
release-nightly-and-prod-pypi
Draft

ci: publish cytnx to PyPI on tag (stable) and master push (.devN nightly)#866
IvanaGyro wants to merge 5 commits into
masterfrom
release-nightly-and-prod-pypi

Conversation

@IvanaGyro
Copy link
Copy Markdown
Collaborator

@IvanaGyro IvanaGyro commented May 31, 2026

Summary

Replace the TestPyPI-on-PR/tag flow with two distinct release pipelines that share one PyPI project name, following the numpy / scipy convention:

  • stablerelease_pypi.yml is restricted to v* tag pushes and manual workflow_dispatch, and publishes the resulting wheels to production PyPI as cytnx X.Y.Z.
  • nightly — new release_pypi_nightly.yml triggers on every push to master (PR merges) and on manual workflow_dispatch, and publishes wheels to the same cytnx PyPI project but version-stamped MAJOR.MINOR.PATCH.devYYYYMMDDHHMM (PEP 440, UTC).

pip install cytnx continues to resolve to the latest stable release (PEP 440 excludes pre-releases by default). pip install --pre cytnx resolves to the most recent nightly. One PyPI project, one trusted-publisher entry, two channels distinguished by the version string — same pattern as numpy, scipy, cython, etc.

Commits

  1. build: refactor optional-deps; add release-tools group — collapse dev extras down to cytnx[test] + cytnx[coverage] (single source of truth per leaf group) and add a new release-tools extras group containing tomlkit, consumed only by the release pipeline.
  2. build: add CYTNX_VERSION_TAG env hook for dev-version suffixes — introduce CYTNX_VERSION_FULL in CMakeLists.txt that initialises to the numeric CYTNX_VERSION and is extended with $ENV{CYTNX_VERSION_TAG} when that variable is set. Used only for the CYTNX_VERSION compile definition (i.e. cytnx.__version__); numeric-only consumers (project(VERSION ...), target SOVERSION, libname) are untouched. Also adds CYTNX_VERSION_TAG to the cibuildwheel Linux environment-pass so the variable crosses into the manylinux container. No-op when the env var is unset.
  3. build: add nightly-release pyproject stamping helpertools/prepare_nightly_release.py uses tomlkit to rewrite pyproject.toml in place (dynamic = ["version"] removed, static version = "X.Y.Z.devYYYYMMDDHHMM", [tool.scikit-build.metadata.version] removed) and appends CYTNX_VERSION_TAG=.devYYYYMMDDHHMM to $GITHUB_ENV. Reuses the regex from [tool.scikit-build.metadata.version].regex to read version.cmake, so the regex lives in exactly one place. Stand-alone; not wired up yet.
  4. ci: switch release_pypi.yml from TestPyPI to production PyPI — triggers narrowed to v* tags + workflow_dispatch. Drops the pull_request trigger and the PR-merge / PR-keyed ccache steps that went with it. Job renamed ReleaseTestPyPIReleasePyPI. Publish destination is production PyPI (no repository-url override). Drops the login-shell defaults block. Every third-party action is pinned to a full commit SHA with the upstream tag retained as a trailing comment.
  5. ci: add nightly cytnx dev-release pipeline — new release_pypi_nightly.yml. Triggers on push to master + workflow_dispatch. Installs tomlkit, runs the prep script, builds wheels via cibuildwheel on the same 4-OS matrix as the stable workflow, and publishes to PyPI as a cytnx dev release via the same trusted-publisher entry. Forwards CYTNX_VERSION_TAG into the cibuildwheel build via the step's env: block. Shares the ccache-wheel-${OS}- cache prefix with release_pypi.yml so the two pipelines warm each other's caches.

Why a separate workflow file?

Different triggers, different version-stamping behavior, and a broken nightly should never block a real release (or vice versa). The shared C++ build logic lives in pyproject.toml ([tool.cibuildwheel.*]) and tools/cibuildwheel_*, so the workflow YAML duplication is small.

Version stamping — why timestamp instead of incremental

A monotonic timestamp (devYYYYMMDDHHMM) requires no external state. An incremental devN would need either querying PyPI for the last .devN or storing a counter, both of which race under parallel merges and break on repo moves. The minute-resolution stamp handles multiple merges per day; PyPI rejects duplicate filenames, but two merges within the same minute is extremely unlikely (and a re-run picks up a new minute).

One-time setup required before merge (manual on PyPI)

These workflows use trusted publishing — no API tokens are stored in the repo. The following must be configured on PyPI before either workflow can publish:

  • cytnx project — add a trusted publisher with repository Cytnx-dev/Cytnx, workflow release_pypi.yml, job ReleasePyPI, no environment.
  • same cytnx project — add a second trusted publisher entry with repository Cytnx-dev/Cytnx, workflow release_pypi_nightly.yml, job PublishNightlyPyPI, no environment.

Until both are configured, the pypa/gh-action-pypi-publish step will fail; this is intentional and safe.

Test plan

  • Both PyPI trusted-publisher entries created on the cytnx project.
  • Manual workflow_dispatch of release_pypi_nightly.yml from this branch (before merge) — verify the wheel filenames look like cytnx-1.0.0.devYYYYMMDDHHMM-* and the upload succeeds.
  • Manual workflow_dispatch of release_pypi.yml — verify it builds and publishes cytnx X.Y.Z to production PyPI.
  • After merge to master: confirm release_pypi_nightly.yml runs and a cytnx X.Y.Z.devN release appears on PyPI.
  • Tag a release (vX.Y.Z) and confirm release_pypi.yml runs and cytnx X.Y.Z lands on PyPI.
  • pip install cytnx resolves to the tagged stable; pip install --pre cytnx resolves to the latest nightly.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new script, tools/prepare_nightly_release.py, which automates updating pyproject.toml for nightly releases by parsing version information from version.cmake and appending a UTC timestamp. The code review feedback focuses on making the script more robust against formatting changes. Specifically, the reviewer suggests parsing version components individually rather than assuming they are adjacent, using flexible regular expressions instead of exact string replacement for updating package metadata, and refining the regex used to strip the scikit-build metadata block to prevent premature matching.

Comment thread tools/prepare_nightly_release.py Outdated
Comment thread tools/prepare_nightly_release.py Outdated
Comment thread tools/prepare_nightly_release.py Outdated
Comment thread .github/workflows/release_pypi.yml Outdated
@IvanaGyro IvanaGyro force-pushed the release-nightly-and-prod-pypi branch from 5db13b9 to f5dcea7 Compare May 31, 2026 12:31
@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 29.23%. Comparing base (8c7ee11) to head (d91029c).
⚠️ Report is 19 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #866      +/-   ##
==========================================
+ Coverage   29.05%   29.23%   +0.17%     
==========================================
  Files         241      241              
  Lines       35519    35559      +40     
  Branches    14807    14822      +15     
==========================================
+ Hits        10319    10394      +75     
+ Misses      18039    17941      -98     
- Partials     7161     7224      +63     
Flag Coverage Δ
cpp 28.82% <ø> (+0.15%) ⬆️
python 52.71% <ø> (+1.64%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 30.43% <ø> (+0.15%) ⬆️
Python bindings 17.09% <ø> (+0.20%) ⬆️
Python package 52.71% <ø> (+1.64%) ⬆️
see 13 files with indirect coverage changes

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8c7ee11...d91029c. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@IvanaGyro IvanaGyro force-pushed the release-nightly-and-prod-pypi branch from f5dcea7 to 80fa62c Compare May 31, 2026 17:42
@IvanaGyro IvanaGyro changed the title ci: split cytnx (tag → PyPI) and cytnx-nightly (master → PyPI) release pipelines ci: publish cytnx to PyPI on tag (stable) and master push (.devN nightly) May 31, 2026
Comment thread .github/workflows/release_pypi.yml Outdated
Comment thread .github/workflows/release_pypi_nightly.yml Outdated
IvanaGyro and others added 5 commits May 31, 2026 18:13
Two related cleanups to dependency declarations in pyproject.toml:

1. The `dev` aggregate under `[project.optional-dependencies]`
   previously duplicated the contents of the `test` and `coverage`
   groups verbatim, so any change to either leaf group required a
   second edit to keep `dev` in sync. Replace the duplicated dep
   list with self-references:

       dev = ["cytnx[test]", "cytnx[coverage]"]

   `pip install --editable .[dev]` resolves the references and pulls
   the same packages as before, but the source of truth for each
   group is now its own definition.

2. Add a new PEP 735 `[dependency-groups]` table with a
   `release-tools` group containing `tomlkit`. The group is consumed
   by release pipelines that programmatically rewrite pyproject.toml
   (e.g. tools/prepare_nightly_release.py).

   `[dependency-groups]` is used rather than
   `[project.optional-dependencies]` because tools listed here are
   needed at *build pipeline* time only, not at install or run time
   of cytnx itself. `pip install --group release-tools` installs the
   listed packages without invoking the project build backend, so a
   release pipeline can install the helpers on a runner without
   accidentally triggering a scikit-build-core compile of cytnx
   before cibuildwheel runs in its own isolated environment.

Co-Authored-By: Claude <noreply@anthropic.com>
`set_target_properties(VERSION ...)`, `project(VERSION ...)`, and the
shared-library SONAME require a strict MAJOR.MINOR.PATCH, so
CYTNX_VERSION cannot itself carry a PEP 440 dev/local suffix.

Introduce a separate string `CYTNX_VERSION_FULL`, initialised to the
numeric `CYTNX_VERSION` and extended with the contents of the
`CYTNX_VERSION_TAG` environment variable when that variable is set
and non-empty. Use `CYTNX_VERSION_FULL` for the `CYTNX_VERSION`
compile definition consumed by pybind, which becomes the runtime
`cytnx.__version__`. Numeric-only consumers (project version,
target VERSION/SOVERSION, libname suffix) keep reading
`CYTNX_VERSION`.

With no environment variable set, `CYTNX_VERSION_FULL` and
`CYTNX_VERSION` are identical, so non-release builds are unaffected
and `pytests/version_test.py` continues to compare
`cytnx.__version__` against the numeric version.cmake values
without change.

Also append `CYTNX_VERSION_TAG` to the cibuildwheel Linux
`environment-pass` list so the variable, when set on the host
runner, is forwarded into the manylinux container that performs the
actual wheel compilation. On macOS the variable is inherited from
the runner environment directly and needs no allow-list entry.

The variable itself is never set by a normal contributor or by any
existing CI job in this commit; nothing downstream consumes it yet.
A subsequent commit adds the release tooling that produces the
suffix.

Co-Authored-By: Claude <noreply@anthropic.com>
Add `tools/prepare_nightly_release.py`, a single-shot script intended
to run in CI before cibuildwheel on every push to `master`. It

  - reuses the regex declared in
    `[tool.scikit-build.metadata.version]` of pyproject.toml as the
    sole parser for version.cmake, so the two never disagree on what
    "the version" is;
  - derives a PEP 440 dev version of the form
    MAJOR.MINOR.PATCH.devYYYYMMDDHHMM (UTC), giving each push a
    monotonically increasing, deterministic identifier;
  - rewrites pyproject.toml in place to a static `version = "..."`
    on the `cytnx` project, removing the `dynamic = ["version"]`
    entry from `[project]` and the now-redundant
    `[tool.scikit-build.metadata.version]` table; and
  - appends `CYTNX_VERSION_TAG=.devYYYYMMDDHHMM` to `$GITHUB_ENV`
    when running under GitHub Actions, so the surrounding CI job can
    forward the suffix into the cibuildwheel build via the
    `CYTNX_VERSION_TAG` hook in CMakeLists.txt, keeping the wheel
    filename's version string and `cytnx.__version__` in lockstep.

The rewrite uses `tomlkit` (declared in the `release-tools`
dependency-group) for round-trip formatting preservation, so the
comments and section ordering of pyproject.toml survive the stamping
intact.

The script is idempotent only against a clean checkout; CI is
expected to run it once on a fresh tree before cibuildwheel.

No workflow invokes the script yet; the nightly publishing workflow
is added in a follow-up commit so that this commit can be reviewed
on its own.

Co-Authored-By: Claude <noreply@anthropic.com>
The workflow previously ran on every pull request and every push to
master to upload a non-version-suffixed wheel set to TestPyPI for
smoke-testing, and re-uploaded tagged releases to TestPyPI rather
than PyPI. The tag-push wheels never reached production PyPI.

This commit narrows the workflow to the role its name implies and
makes it the canonical production release pipeline:

- Triggers are reduced to `push` of `v*` tags and manual
  `workflow_dispatch`. The `pull_request` trigger and the
  `push` trigger on `master` are removed; the PR-validation and
  per-PR ccache warm-up jobs run in dedicated workflows
  (build_test.yml, ccache_seed.yml).
- The PR-only "Merge with latest target branch" step and the
  pull_request-keyed ccache cache step are dropped together with the
  PR trigger; both were unreachable.
- The publish step uploads to production PyPI (no
  `repository-url` override). Because the workflow itself only
  triggers on `v*` tag pushes and `workflow_dispatch`, the publish
  job needs no additional `if:` guard.
- Job name renamed from `ReleaseTestPyPI` (`ReleaseWheel-TestPyPI`)
  to `ReleasePyPI` to reflect the destination.
- Drop `defaults.run.shell: bash -el {0}`; the workflow does not
  rely on login-shell `.bashrc` initialisation, so the implicit
  `bash` shell is sufficient and avoids the extra interactive shell
  setup cost per step.
- Pin every third-party action to a full commit SHA with the
  upstream tag retained as a trailing comment, so an upstream tag
  re-point cannot silently change what runs in a release job that
  has PyPI publish permissions.
- Add a `ccache-wheel-${OS}-` restore-keys fallback so that the
  first build of a new ref does not start with a cold ccache.

The wheel build matrix itself is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Add `release_pypi_nightly.yml`, which builds and publishes a cytnx
wheel set to production PyPI on every push to `master` (i.e. every
merged pull request) and on manual workflow_dispatch.

The pipeline follows the numpy / scipy convention of using one PyPI
project name for both stable and dev channels, distinguished by the
PEP 440 version string:

  - tagged releases publish `cytnx X.Y.Z` (handled by
    release_pypi.yml);
  - nightlies publish `cytnx X.Y.Z.devYYYYMMDDHHMM`.

`pip install cytnx` continues to resolve to the latest stable
release; `pip install --pre cytnx` resolves to the most recent
nightly. No separate PyPI project, trusted-publisher entry, or
install command is needed.

Workflow shape:

1. Install the `release-tools` PEP 735 dependency-group from
   pyproject.toml via `pip install --group release-tools`. Doing so
   keeps the section name in pyproject.toml as the actual source of
   truth for the workflow's tooling deps without invoking
   scikit-build-core to compile cytnx on the host before cibuildwheel
   does so under its own isolated environment, which
   `pip install .[release-tools]` would have forced. The
   runner-bundled pip is upgraded first because `--group` requires
   pip 25.1+.
2. Run `tools/prepare_nightly_release.py` to rewrite pyproject.toml
   (static dev version, `dynamic = ["version"]` removed,
   `[tool.scikit-build.metadata.version]` removed) and to append
   `CYTNX_VERSION_TAG=.devYYYYMMDDHHMM` to `$GITHUB_ENV`. This same
   tag is consumed by the `CYTNX_VERSION_TAG` env hook in
   CMakeLists.txt so the wheel filename and `cytnx.__version__`
   agree.
3. Build wheels with cibuildwheel on the same matrix as
   release_pypi.yml (ubuntu-24.04, ubuntu-24.04-arm, macos-14,
   macos-15-intel). The ccache key prefix `ccache-wheel-${OS}-` is
   shared with release_pypi.yml so a cache primed by either pipeline
   can prime the other.
4. Forward `CYTNX_VERSION_TAG` into the cibuildwheel build via the
   step's `env:` block; the Linux manylinux container additionally
   needs the variable on the `environment-pass` list in
   pyproject.toml (added earlier in this branch).
5. Upload the wheels to PyPI via OIDC trusted publishing using the
   same SHA-pinned action versions as release_pypi.yml.

The `prepare_nightly_release.py` rewrite is destructive to the
checkout, so it must run on a fresh runner working tree; the
workflow is structured accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro IvanaGyro force-pushed the release-nightly-and-prod-pypi branch from 1c0abab to d91029c Compare May 31, 2026 18:14
@pcchen pcchen added this to the v1.1.0 milestone Jun 1, 2026
@IvanaGyro
Copy link
Copy Markdown
Collaborator Author

I plan to follow SPEC 4 to release the nightly build. This is adopted by numpy, scipy and some scientific Python packages. I am requesting access of Scientific Python Nightly Wheels to release the nightly build on scientific-python/upload-nightly-action#167. We will not merge this PR until we get access.

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.

2 participants