Skip to content

Commit bfe8fed

Browse files
committed
build: extend the version scheme to minor and major releases
- `__version__` now states the release being worked toward and `make build` derives from it. Previously only a last-digit bump was expressible, so `1.1.0` or `2.0.0` could not be released at all. - `check-about-version` accepts any single number raised by one with the rest zeroed, and accepts a `release/<version>` branch on `origin` as evidence during the window between the cut and the tag — which is what lets `main` move on immediately. - A release branch sets `latest_released_version` to the release it produces, so a repo pinning `external/` to that branch resolves the right baseline instead of the previous release. - `make version` now prints the publishable version; `make version-dev` prints the on-tree `.dev0`.
1 parent af262ba commit bfe8fed

12 files changed

Lines changed: 457 additions & 128 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ jobs:
5252
# The published version comes from src/coreai_opt/_about.py, not the tag.
5353
# Fail early if they disagree so we never publish a mismatched/duplicate
5454
# version (PyPI uploads are immutable and cannot be overwritten).
55-
# `print_version.py --release` computes the version exactly as the
55+
# `make version` computes the version exactly as the
5656
# `make build` step below does, so this guard can't drift from what
5757
# actually gets published.
5858
# Skipped on manual dry runs, where the ref is a branch, not a vX.Y.Z tag.
59-
# Run via uv (installed above) so the interpreter satisfies
60-
# requires-python whatever the runner image ships; see the Makefile's
61-
# `version` target.
59+
# The target runs `uv run --no-project` internally, so uv (installed
60+
# above) supplies an interpreter satisfying requires-python whatever the
61+
# runner image ships; see the Makefile's `version` target.
6262
if: github.event_name == 'push'
6363
run: |
6464
tag="${GITHUB_REF_NAME}"
65-
version="$(uv run --no-config --no-project --python '>=3.11' scripts/make/print_version.py --release)"
65+
version="$(make version)"
6666
echo "tag=${tag} package version=${version}"
6767
if [ "${tag}" != "v${version}" ]; then
6868
echo "::error::Tag ${tag} does not match package version v${version} (src/coreai_opt/_about.py). Update latest_released_version so the release it implies matches the tag."

.pre-commit-config.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,10 @@ repos:
325325
name: Check _about.py version fields
326326
description: |
327327
Check that _about.py's latest_released_version matches the repo's
328-
latest release tag, and that __version__ is its last number plus
329-
one, plus .dev0. Catches a release candidate that looks like a
330-
release has already shipped when it hasn't.
328+
latest release tag (or has a release/<version> branch), and that
329+
__version__ raises exactly one of its numbers by one, zeroes the
330+
rest, and ends in .dev0. Catches a release candidate that looks
331+
like a release has already shipped when it hasn't.
331332
entry: python scripts/pre_commit/check_about_version.py
332333
language: system
333334
files: (^|/)_about\.py$

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525

2626
- Initial release of `coreai-opt`. See the [GitHub Releases](https://github.com/apple/coreai-optimization/releases/) page for release notes.
2727

28-
[0.2.1]: https://github.com/apple/coreai-optimization/releases/tag/v0.2.1
29-
[0.2.0]: https://github.com/apple/coreai-optimization/releases/tag/v0.2.0
28+
[0.2.0]: https://github.com/apple/coreai-optimization/commits/v0.2.0/
29+
[0.2.1]: https://github.com/apple/coreai-optimization/compare/v0.2.0...v0.2.1/

Makefile

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Use of this source code is governed by a BSD-3-Clause license that can
44
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
55

6-
.PHONY: _maybe_patch_pyproject all api-list build build-dev check clean distclean distclean-all docs docs-clean docs-open env env-all env-docs env-highest-torch env-lowest-torch env-tutorial render-api-index set-auto-venv test test-cov test-fast test-highest-pytorch test-lowest-pytorch test-slow test-smoke test-tutorials version
6+
.PHONY: _maybe_patch_pyproject all api-list build build-dev check clean distclean distclean-all docs docs-clean docs-open env env-all env-docs env-highest-torch env-lowest-torch env-tutorial render-api-index set-auto-venv test test-cov test-fast test-highest-pytorch test-lowest-pytorch test-slow test-smoke test-tutorials version version-dev
77

88
SHELL := /bin/bash
99

@@ -356,12 +356,18 @@ distclean-all:
356356
set-auto-venv:
357357
@$(SCRIPTS)/make/set_auto_venv.sh $(DEFAULT_VENV) $(SHELL_RC)
358358

359-
# Show the development version carried on the tree (e.g. 0.2.2.dev0), including
360-
# any COREAI_OPT_VERSION_EXTENSION (e.g. 0.2.2.1.dev0). Reads _about.py as plain
361-
# text, so no venv is needed — but `uv run --no-project` is still what guarantees
362-
# a >= 3.11 interpreter (a bare `python3` is 3.9 on stock macOS, and `python` may
363-
# not exist at all) without requiring `make env` first.
359+
# Show the version a release would publish (e.g. 0.2.2), including any
360+
# COREAI_OPT_VERSION_EXTENSION (e.g. 0.2.2.1). This is the same computation the
361+
# release workflow's tag guard uses, so the two cannot drift. Reads _about.py as
362+
# plain text, so no venv is needed — but `uv run --no-project` is still what
363+
# guarantees a >= 3.11 interpreter (a bare `python3` is 3.9 on stock macOS, and
364+
# `python` may not exist at all) without requiring `make env` first.
364365
version:
366+
@uv run --no-config --no-project --python '>=3.11' $(SCRIPTS)/make/print_version.py --release
367+
368+
# Show the development version carried on the tree (e.g. 0.2.2.dev0), i.e.
369+
# `version` with the .dev0 marker the tree carries between releases.
370+
version-dev:
365371
@uv run --no-config --no-project --python '>=3.11' $(SCRIPTS)/make/print_version.py
366372

367373
# =============================================================================

RELEASE.md

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,92 @@
11
# Package Release Guide
22

3-
The OSS release process for Core AI Optimization is still being defined. This page will document the workflow for publishing to PyPI once the public release infrastructure is finalized.
3+
Releases are cut and published by the [release managers team](https://github.com/orgs/apple/teams/coreai-optimization-release-managers).
44

55
The following commands are available locally:
66

77
```bash
8-
make build # build the canonical, publishable wheel + sdist (uv build --no-sources)
9-
make build-dev # build a timestamped dev wheel (e.g. 0.2.2.dev202607231430+abc1234)
10-
make version # show the development version carried on the tree (e.g. 0.2.2.dev0)
8+
make build # build the artifacts for the current release to be published
9+
make build-dev # build a nightly or local development wheel (e.g. 1.1.0.dev202607231430+abc1234)
10+
make version # show the version a release would publish (e.g. 1.1.0)
11+
make version-dev # show the development version carried on the tree (e.g. 1.1.0.dev0)
1112
make clean # remove build artifacts
1213
```
1314

1415
## Version scheme
1516

16-
`main` always carries the version planned for the _next_ release. This ensures that ongoing development is never mistaken for an already-published version, and that a release can be stabilized, tested, and published on its own branch, independently of later changes on `main`. (The release-branch workflow itself — branch naming, tagging, and backporting fixes to `main` — will be documented separately in the release schedule doc; this section covers only the version-string mechanics.)
17+
`main` always carries the version of the *next* release, never the one that already shipped.
1718

18-
`src/coreai_opt/_about.py` stores `latest_released_version` (the last tagged release, e.g. `"0.2.1"`) and computes `__version__` from it by incrementing its last number by one and adding a `.dev0` suffix (e.g. `"0.2.2.dev0"`). A pre-commit hook (`check-about-version`) verifies that `__version__` always follows this rule and that `latest_released_version` matches the repo's latest release tag. As a result, `__version__` can never look as though a release has shipped when it hasn't. The `.dev0` suffix is only a marker on the tree; it never appears in a built wheel.
19+
There are three version formats:
1920

20-
- `make build` builds the release that `__version__` implies, e.g. `0.2.2`. A release is cut by tagging it (`v0.2.2`); `latest_released_version` is then hard-coded to `"0.2.2"`, which bumps `__version__` to the next candidate (`0.2.3.dev0`).
21-
- `make build-dev` builds that same release but with a unique `.dev<UTC-timestamp>+<short-sha>` suffix instead. It is used by contributors, smoke tests, and the nightly pipeline. `DEV_VERSION=<version>` uses that version exactly instead.
21+
| Format | Example | What it is |
22+
| -------------------------------------- | ------------------------------- | -------------------------------------------- |
23+
| `X.Y.Z` | `1.0.0` | a published release |
24+
| `X.Y.Z.dev0` | `1.1.0.dev0` | the version `main` carries in the repo |
25+
| `X.Y.Z.dev<UTC-timestamp>+<short-sha>` | `1.1.0.dev202607231430+abc1234` | a dev artifact, built from a specific commit |
2226

23-
Sorting is preserved: `0.2.2.dev0 < 0.2.2.dev202607231430+abc1234 < 0.2.2`.
27+
Say `1.0.0` has just been released. `main` then carries `1.0.1.dev0`. That reads as "working toward a release after `1.0.0`, which has not shipped": the `.dev0` suffix marks the tree as unreleased and never appears in a built wheel. Nothing on `main` can be mistaken for a published version.
2428

25-
### Extending the scheme downstream
29+
`main`'s `.dev0` always defaults to the last digit plus one, so after `1.0.0` it is `1.0.1.dev0`. Once the version of the next release is known — usually a minor — a PR sets `__version__` to it before the release branch is cut.
30+
31+
The flow below traces one cycle. At the cut, `main` and the release branch diverge and never rejoin: the branch keeps the version it was cut with, and only `main` moves on.
32+
33+
```mermaid
34+
---
35+
title: Version change flow
36+
---
37+
flowchart TB
38+
prev["previous release schedule"] --> m1["main: 1.0.1.dev0<br/>placeholder, last digit + 1"]
39+
prev --> r0("1.0.0 released")
40+
m1 -->|"ready for release"| ask{"is the placeholder the<br/>version we want?"}
41+
ask -->|"No, usually a minor"| pr["PR sets __version__"]
42+
ask -->|Yes| fin
43+
pr --> fin["main: 1.1.0.dev0<br/>assume we release 1.1.0;<br/>it could also stay 1.0.1"]
44+
fin --> nxt["main: 1.1.1.dev0<br/>placeholder, last digit + 1"]
45+
fin -->|cut| rb["release/1.1.0<br/>set latest_released_version = 1.1.0"]
46+
rb --> stab["stabilize"]
47+
stab -->|"tag v1.1.0"| rel("1.1.0 released")
48+
nxt --> nextsched["next release schedule"]
49+
rel --> nextsched
50+
nextsched -.->|"the process repeats"| prev
51+
```
52+
53+
`src/coreai_opt/_about.py` holds the last released version and `__version__`. Both are set together in one PR when `main` moves forward: the last released version becomes the release just branched, and `__version__` becomes the one after it. The `check-about-version` pre-commit hook enforces the relation between them: `__version__` must be the last released version with exactly one of its numbers raised by one, every number after that reset to zero, and `.dev0` on the end. From `1.0.0` it accepts `1.0.1.dev0`, `1.1.0.dev0`, or `2.0.0.dev0`, and nothing else. Any other value fails the commit, and the message prints the accepted ones, so there is nothing to work out by hand.
54+
55+
- `make build` builds the artifacts for the current release to be published.
56+
- `make build-dev` builds the wheel for the nightly build, and local wheels for development and testing, each carrying a unique `.dev<UTC-timestamp>+<short-sha>` suffix.
57+
58+
Therefore, we have the following order:
59+
60+
```text
61+
1.1.0.dev0 < 1.1.0.dev202607231430+abc1234 < 1.1.0
62+
```
63+
64+
This is the order we want. `1.1.0.dev0` is the bare marker `main` carries, so it sorts below every wheel actually built for `1.1.0`. Each nightly sorts above it, and above the nightly before it, because the timestamp only grows. The published `1.1.0` sorts highest of all, so installers pick it over any dev wheel.
65+
66+
A release branch is the one place where the two match: it sets `latest_released_version` to the release it produces, so `__version__` is that same version plus `.dev0` rather than a next candidate. On `main` they always differ, which is what tells a release branch apart — and what lets a repo that vendors this one pin to a release branch and still resolve the right baseline.
67+
68+
`__version__` must always be a literal string, never an expression.
2669

27-
A repo that uses this one as a submodule and includes this `Makefile` — building one combined wheel from both trees — can add its own 4th number. Set `COREAI_OPT_VERSION_EXTENSION` to the number it's about to release next (e.g. `"1"` for its first release off a given OSS release, then `"2"` for the one after that). Then call `make build`, `make build-dev`, or `make version` unchanged:
70+
### Release branches
71+
72+
1. `release/<version>` is created from `main`, once the version of the next release has been decided — that is, which digit gets one added to it. Its first commit sets `latest_released_version` to that version, so the branch names its own release.
73+
2. The tag is created on the `release/<version>` branch, never on `main`.
74+
3. After the cut, `main` continues on to the next release's `.dev0`.
75+
4. The `check-about-version` pre-commit hook enforces the version rules on every commit.
76+
5. After the cut, the release branch takes no new commits, unless a must-fix issue comes up. Those commits are later cherry-picked back to `main`.
77+
78+
Cut the branch before moving `main` to the next dev release.
79+
80+
### Extending the scheme downstream
2881

29-
- `latest_released_version` `"0.2.1"` + extension `"1"` -> candidate: `0.2.1.1.dev0`
30-
- `make build` -> `0.2.1.1`
31-
- `make build-dev` -> `0.2.1.1.dev<UTC-timestamp>+<short-sha>`
82+
A repo that uses this one as a submodule and includes this `Makefile` — building one combined wheel from both trees — can add its own 4th number. Set `COREAI_OPT_VERSION_EXTENSION` to the number it's about to release next, then call `make build`, `make build-dev`, or `make version` unchanged.
3283

33-
The extra number is used exactly as given (`scripts/release/release_utils.apply_version_extension`); `latest_released_version`'s own last number is only bumped for OSS's own `main`, when no extension is set.
84+
The extension anchors the release to the last *published* release instead of the one `__version__` is working toward:
3485

35-
There is still only one `_about.py` (this package's own); the extra number is a plain string handled entirely in `scripts/release/release_utils.next_release_base` — no other file or package is involved.
86+
| `latest_released_version` | `__version__` | `COREAI_OPT_VERSION_EXTENSION` | Release built |
87+
| ------------------------- | ------------- | ------------------------------ | ------------- |
88+
| `1.0.0` | `1.1.0.dev0` | unset | `1.1.0` |
89+
| `1.0.0` | `1.1.0.dev0` | `1` | `1.0.0.1` |
90+
| `1.0.0` | `1.1.0.dev0` | `2` | `1.0.0.2` |
3691

37-
<!-- TODO: Document the chosen OSS release workflow (PyPI trusted publishing, twine upload, or uv publish). -->
92+
The extension is used exactly as given, and starts at `1`, not `0`. `make build-dev` adds the usual `.dev<UTC-timestamp>+<short-sha>` suffix on top.

scripts/make/build.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
version. Called by `make build-dev`.
1515
1616
``_about.py`` stores ``latest_released_version`` (the last tagged release) by
17-
hand; ``__version__`` is computed from it — add one to its last number, add
18-
``.dev0``. This script computes the version to build from
19-
``latest_released_version``, not from the on-tree ``__version__`` (which must
20-
never be treated as already released), writes it into ``_about.py``, builds,
21-
then restores the file. A repo that uses this one as a submodule (building
17+
hand; ``__version__`` names the release the tree is working toward. This
18+
script takes the version to build from ``__version__``, writes it into
19+
``_about.py``, builds, then restores the file. A repo that uses this one as a submodule (building
2220
one combined wheel) can add its own extra number to the version with
2321
``COREAI_OPT_VERSION_EXTENSION``; see
2422
``scripts/release/release_utils.next_release_base``.
@@ -89,7 +87,9 @@ def main() -> None:
8987
repo_root = _find_repo_root(Path(__file__))
9088

9189
about = read_about(repo_root)
92-
release_base = next_release_base(about.latest_released_version, get_version_extension())
90+
release_base = next_release_base(
91+
about.latest_released_version, about.version, get_version_extension()
92+
)
9393
build_version = resolve_build_version(
9494
release_base,
9595
dev=args.dev,

scripts/make/print_version.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
Usage:
1111
print_version.py the ``.dev0`` candidate, e.g. ``0.2.2.dev0``
12-
(``make version``)
12+
(``make version-dev``)
1313
print_version.py --release the release itself, e.g. ``0.2.2`` — the
1414
version ``make build`` publishes
1515
@@ -25,7 +25,7 @@
2525
import sys
2626
from pathlib import Path
2727

28-
# `make version` exports PYTHONPATH, but the release workflow runs this script
28+
# `make version-dev` exports PYTHONPATH, but the release workflow runs this script
2929
# directly, which puts only `scripts/make/` on sys.path. Walk up to the project
3030
# root (the directory holding pyproject.toml, alongside `scripts/`) so this
3131
# keeps working if the script moves. It can't call
@@ -58,7 +58,9 @@ def main() -> None:
5858

5959
about = read_about(_repo_root)
6060
compute = next_release_base if args.release else next_candidate_version
61-
sys.stdout.write(f"{compute(about.latest_released_version, get_version_extension())}\n")
61+
sys.stdout.write(
62+
f"{compute(about.latest_released_version, about.version, get_version_extension())}\n"
63+
)
6264

6365

6466
if __name__ == "__main__":

0 commit comments

Comments
 (0)