Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 45 additions & 14 deletions .github/actions/run-release-prep-script/release_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,46 @@
from pyproject_parser import PyProject


def get_latest_published_version(repo: str) -> Version:
"""Return the version of the latest GitHub Release for *repo* (owner/repo)."""
url = f"https://api.github.com/repos/{repo}/releases/latest"
def _auth_headers() -> dict:
headers = {"Accept": "application/vnd.github+json"}
token = os.getenv("GH_API_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
resp = requests.get(url, headers=headers, timeout=5)
return headers


def _get_all_releases(repo: str):
"""Return all GitHub Release tags as Version objects for *repo*."""
url = f"https://api.github.com/repos/{repo}/releases"
resp = requests.get(url, headers=_auth_headers(), params={"per_page": 100}, timeout=10)
if resp.status_code == 404:
return None
return []
resp.raise_for_status()
tag = resp.json().get("tag_name", "").lstrip("v")
return Version(tag) if tag else None
versions = []
for release in resp.json():
tag = release["tag_name"].lstrip("v")
try:
versions.append(Version(tag))
except Exception:
pass
return versions


def get_latest_version_for_base(repo: str, base: str) -> Version:
"""Return the highest GitHub Release whose base version matches *base* (e.g. '2.0.0').
Used to support concurrent pre-release streams without cross-stream pollution."""
matching = [v for v in _get_all_releases(repo) if v.base_version == base]
return max(matching) if matching else None


def get_latest_version_for_minor_stream(repo: str, major: int, minor: int) -> Version:
"""Return the highest stable GitHub Release matching major.minor.*.
Used to support concurrent stable streams (e.g. 2.1.x while 2.2.x is active)."""
matching = [
v for v in _get_all_releases(repo)
if v.major == major and v.minor == minor and not v.pre
]
return max(matching) if matching else None


def increment_version(current: Version, branch: str) -> str:
Expand Down Expand Up @@ -87,16 +114,20 @@ def update_pyproject():
github_repo = os.getenv("GITHUB_REPO")
if not github_repo:
raise ValueError("GITHUB_REPO environment variable must be set (e.g. bcgov/bcrhp)")
latest_published_version = get_latest_published_version(github_repo)
current_toml_version = Version(str(pyproject.project["version"]))
if current_toml_version.pre:
# Pre-release version in pyproject.toml represents explicit developer intent
# — use it directly rather than letting a stable GitHub release override it
base_version = current_toml_version
elif latest_published_version is not None:
base_version = max(latest_published_version, current_toml_version)
# Pre-release in pyproject.toml signals explicit developer intent for this stream.
# Query only releases matching this base version to avoid cross-stream pollution
# (e.g. 2.1.0a5 should not affect the 2.0.0 stream).
stream_version = get_latest_version_for_base(github_repo, current_toml_version.base_version)
base_version = max(stream_version, current_toml_version) if stream_version else current_toml_version
else:
base_version = current_toml_version
# Stable release: scope lookup to same major.minor to avoid cross-stream pollution
# (e.g. 2.2.0 should not affect a 2.1.x patch stream).
stream_version = get_latest_version_for_minor_stream(
github_repo, current_toml_version.major, current_toml_version.minor
)
base_version = max(stream_version, current_toml_version) if stream_version else current_toml_version
pyproject.project["version"] = increment_version(base_version, current_branch)
pyproject.dump(toml_file)
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
if: >
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'on merge: github release of version')
uses: bcgov/bcgov-arches-common/.github/workflows/release_deploy_common.yml@brf/feat/add_release_workflows
uses: bcgov/bcgov-arches-common/.github/workflows/release_deploy_common.yml@main
20 changes: 17 additions & 3 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,12 @@ Pushing to a trigger branch increments the version, opens a PR, and publishes a
### How It Works

1. **Set the initial version** — for a new pre-release cycle, manually set the starting version in `pyproject.toml` to signal the scope (e.g. `1.3.1a0` for a patch alpha, `1.4.0a0` for a minor alpha)
2. **Push to a trigger branch** — the workflow reads both the latest GitHub Release and the current `pyproject.toml` version, uses whichever is higher as the base, and increments from there
2. **Push to a trigger branch** — the workflow reads the current `pyproject.toml` version and the relevant GitHub Releases for that stream, uses whichever is higher as the base, and increments from there
3. **A PR is opened automatically** — targeting the appropriate branch (`release/*` for alpha/beta, `main` for patch/minor/major), titled `on merge: github release of version X.Y.Z`
4. **Review and merge the PR** — once merged, the release workflow builds the package and publishes a GitHub Release with the wheel and sdist attached

The automation scopes its GitHub Release lookup to the current stream so that concurrent streams do not interfere with each other (see [Concurrent release streams](#concurrent-release-streams) below).

### Version Transition Scenarios

#### Starting a new pre-release cycle from stable
Expand Down Expand Up @@ -196,7 +198,19 @@ git merge <your-feature-branch>
git push origin release_patch
```

The automation increments from the latest GitHub Release and targets `main`.
The automation increments from the latest GitHub Release in the same `major.minor` stream and targets `main`.

---

#### Concurrent release streams

Multiple release streams can be active simultaneously without interfering with each other. The automation always scopes its GitHub Release lookup to the stream identified by `pyproject.toml`:

- **Concurrent pre-release streams** (e.g. `2.0.0ax` and `2.1.0ax`): the automation only considers releases whose base version matches exactly (e.g. only `2.0.0*` releases when `pyproject.toml` is `2.0.0a0`). Pushing `2.0.0a0` to `release_alpha` produces `2.0.0a1`, not `2.1.0a6`.

- **Concurrent stable streams** (e.g. backporting a `2.1.x` patch while `2.2.x` is active): the automation only considers releases in the same `major.minor` series. With `pyproject.toml` at `2.1.1` and `2.2.0` already released, pushing to `release_patch` produces `2.1.2`, not `2.2.1`.

In both cases, set `pyproject.toml` to the appropriate version on the trigger branch before pushing to signal which stream is being worked on.

---

Expand All @@ -210,7 +224,7 @@ The automation increments from the latest GitHub Release and targets `main`.

### Notes

- The automation uses whichever is higher — the latest GitHub Release or the current `pyproject.toml` version — as the base for incrementing. Set `pyproject.toml` manually only when starting a new pre-release cycle to signal scope (patch/minor/major)
- The automation uses whichever is higher — the latest GitHub Release in the current stream or the current `pyproject.toml` version — as the base for incrementing. The stream is determined by `pyproject.toml`: pre-release versions scope to an exact base version (e.g. `2.0.0*`), stable versions scope to `major.minor` (e.g. `2.1.x`). Set `pyproject.toml` manually only when starting a new pre-release cycle to signal scope (patch/minor/major)
- If the `Prepare release` workflow runs but no PR appears, check the Actions log for errors
- If a PR already exists for a trigger branch, the workflow will skip PR creation and exit cleanly — merge or close the existing PR first
- The release notes are auto-generated by GitHub based on PRs merged into the target branch since the previous release
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"

[project]
name = "bcgov-arches-common"
version = "2.1.0b0"
version = "2.1.0b1"
readme = "README.md"
requires-python = ">=3.11"
authors = []
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Intended Audience :: Developers",
Expand Down
Loading