Skip to content

Commit 5f0db0f

Browse files
committed
Use pyproject.toml as version source; CI bump/tag/publish
- Read __version__ from package metadata (importlib.metadata), fallback to "DEVELOPMENT" - Validate config version against buildrunner __version__; drop version.py and VERSION_FILE_PATH - CI: bump patch on push to main (skip for bot bump commits), tag and publish only when bump ran - Add VERSIONING.md; tests patch buildrunner.__version__ instead of version file
1 parent d909509 commit 5f0db0f

7 files changed

Lines changed: 92 additions & 98 deletions

File tree

.github/workflows/build.yaml

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,56 +57,79 @@ jobs:
5757
check_name: "Test Results ${{ matrix.python-version }}"
5858
github_retries: 10
5959
secondary_rate_limit_wait_seconds: 60.0
60+
# Version source of truth: pyproject.toml [project] version.
61+
# - Patch: CI bumps patch on every push to main (and commits it).
62+
# - Minor/major: Update version in pyproject.toml (e.g. uv version 3.22.0 or uv version --bump minor),
63+
# merge with message "Release 3.22.0"; CI will use that version as-is and tag it.
6064
get-version:
6165
if: github.repository == 'adobe/buildrunner'
6266
runs-on: ubuntu-latest
6367
needs: test
68+
permissions:
69+
contents: write
6470
outputs:
6571
current-version: ${{ steps.version-number.outputs.CURRENT_VERSION }}
72+
should-tag: ${{ steps.tag-decision.outputs.should_tag }}
6673
steps:
6774
- uses: actions/checkout@v2
6875
with:
69-
# Fetch all history instead of the latest commit
7076
fetch-depth: 0
7177
- name: Install uv
7278
uses: astral-sh/setup-uv@v6
7379
with:
7480
python-version: 3.9
7581
- name: Install dependencies
7682
run: uv sync --locked
83+
- name: Bump version (patch only when not the bot’s bump commit)
84+
id: bump
85+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, 'Bump version to')
86+
run: uv version --bump patch
7787
- name: Get current version
7888
id: version-number
79-
run: echo "CURRENT_VERSION=$(uv version --short).$(git rev-list --count HEAD)" >> $GITHUB_OUTPUT
89+
run: echo "CURRENT_VERSION=$(uv version --short)" >> $GITHUB_OUTPUT
8090
- name: Print current version
8191
run: echo CURRENT_VERSION ${{ steps.version-number.outputs.CURRENT_VERSION }}
92+
- name: Set tag/publish decision
93+
id: tag-decision
94+
run: |
95+
if [ "${{ steps.bump.outcome }}" = "success" ]; then echo "should_tag=true" >> $GITHUB_OUTPUT; exit 0; fi
96+
echo "should_tag=false" >> $GITHUB_OUTPUT
97+
- name: Print tag decision
98+
run: echo "should_tag=${{ steps.tag-decision.outputs.should_tag }}"
99+
- name: Commit and push version bump
100+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, 'Bump version to')
101+
run: |
102+
git config user.name "github-actions[bot]"
103+
git config user.email "github-actions[bot]@users.noreply.github.com"
104+
git add pyproject.toml uv.lock
105+
git diff --staged --quiet || (git commit -m "Bump version to ${{ steps.version-number.outputs.CURRENT_VERSION }}" && git push)
82106
tag-commit:
83-
if: github.repository == 'adobe/buildrunner' && github.event_name == 'push' && github.ref == 'refs/heads/main'
107+
if: github.repository == 'adobe/buildrunner' && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.get-version.outputs.should-tag == 'true'
84108
runs-on: ubuntu-latest
85109
needs: [test, get-version]
86110
steps:
87111
- uses: actions/checkout@v2
88112
with:
89-
# Fetch all history instead of the latest commit
90113
fetch-depth: 0
114+
ref: main
91115
- name: Tag commit
92116
run: git tag ${{ needs.get-version.outputs.current-version }} && git push --tags
93117
publish-pypi:
94-
if: github.repository == 'adobe/buildrunner'
118+
if: github.repository == 'adobe/buildrunner' && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.get-version.outputs.should-tag == 'true'))
95119
runs-on: ubuntu-latest
96-
needs: test
120+
needs: [test, get-version]
97121
steps:
98122
- uses: actions/checkout@v2
99123
with:
100-
# Fetch all history instead of the latest commit
101-
fetch-depth: 0
124+
ref: ${{ github.ref == 'refs/heads/main' && 'main' || github.sha }}
102125
- name: Install uv
103126
uses: astral-sh/setup-uv@v6
104127
with:
105128
python-version: 3.9
106129
- name: Install dependencies
107130
run: uv sync --locked
108131
- name: Set version
109-
run: uv version --no-sync "$(uv version --short).$(git rev-list --count HEAD)"
132+
run: uv version --no-sync "${{ needs.get-version.outputs.current-version }}"
110133
- name: Build
111134
run: uv build
112135
- name: Check upload
@@ -119,14 +142,13 @@ jobs:
119142
user: __token__
120143
password: ${{ secrets.ADOBE_BOT_PYPI_TOKEN }}
121144
publish-docker:
122-
if: github.repository == 'adobe/buildrunner'
145+
if: github.repository == 'adobe/buildrunner' && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.get-version.outputs.should-tag == 'true'))
123146
runs-on: ubuntu-latest
124147
needs: [test, get-version]
125148
steps:
126149
- uses: actions/checkout@v2
127150
with:
128-
# Fetch all history instead of the latest commit
129-
fetch-depth: 0
151+
ref: ${{ github.ref == 'refs/heads/main' && 'main' || github.sha }}
130152
- name: Docker Tags
131153
id: docker_tags
132154
uses: docker/metadata-action@v3

VERSIONING.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Versioning
2+
3+
The **source of truth** for the version is `pyproject.toml``[project]``version`.
4+
CI uses that value and the resulting git tag as the release version.
5+
6+
## Where to update version
7+
8+
- **Single place:** `pyproject.toml` — set `version = "X.Y.Z"` (e.g. `"3.21"` or `"3.21.0"`).
9+
10+
## How versions are produced
11+
12+
| You want | What to do |
13+
|----------|------------|
14+
| **Patch** (e.g. 3.21.0 → 3.21.1) | Nothing. Push to `main`; CI bumps patch, commits it, tags that version, and publishes. |
15+
| **Minor** (e.g. 3.21 → 3.22.0) | Update version in `pyproject.toml` (e.g. `uv version 3.22.0` or `uv version --bump minor`). Commit with message **`Release 3.22.0`** and merge to `main`. CI will use that version as-is (no bump), tag it, and publish. |
16+
| **Major** (e.g. 3.21 → 4.0.0) | Same as minor: set `version = "4.0.0"` in `pyproject.toml`, commit **`Release 4.0.0`**, merge to `main`. |
17+
18+
## Summary
19+
20+
- **Track/update** major.minor.patch in **`pyproject.toml`**.
21+
- **Patch releases:** automatic on every push to `main` (CI bumps and commits).
22+
- **Minor/major releases:** set the version in `pyproject.toml` and use a commit message starting with **`Release X.Y.Z`** so CI does not bump again and tags that version.

buildrunner/__init__.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from collections import OrderedDict
1111
import fnmatch
12-
import importlib.machinery
12+
import importlib.metadata
1313
import inspect
1414
import json
1515
import logging
@@ -19,7 +19,6 @@
1919
import tarfile
2020
import tempfile
2121
import traceback
22-
import types
2322
from typing import List, Optional
2423

2524
import requests
@@ -45,18 +44,10 @@
4544

4645
LOGGER = logging.getLogger(__name__)
4746

48-
__version__ = "DEVELOPMENT"
4947
try:
50-
_VERSION_FILE = os.path.join(os.path.dirname(__file__), "version.py")
51-
if os.path.exists(_VERSION_FILE):
52-
loader = importlib.machinery.SourceFileLoader(
53-
"buildrunnerversion", _VERSION_FILE
54-
)
55-
_VERSION_MOD = types.ModuleType(loader.name)
56-
loader.exec_module(_VERSION_MOD)
57-
__version__ = getattr(_VERSION_MOD, "__version__", __version__)
58-
except Exception: # pylint: disable=broad-except
59-
pass
48+
__version__ = importlib.metadata.version("buildrunner")
49+
except importlib.metadata.PackageNotFoundError:
50+
__version__ = "DEVELOPMENT"
6051

6152
SOURCE_DOCKERFILE = os.path.join(os.path.dirname(__file__), "SourceDockerfile")
6253

buildrunner/config/loader.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@
3737

3838

3939
MASTER_GLOBAL_CONFIG_FILE = "/etc/buildrunner/buildrunner.yaml"
40-
VERSION_FILE_PATH = (
41-
f"{os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))}/version.py"
42-
)
4340
RESULTS_DIR = "buildrunner.results"
4441
LOGGER = logging.getLogger(__name__)
4542

@@ -124,41 +121,23 @@ def _validate_version(config: dict) -> None:
124121
buildrunner. If the config version is greater than the buildrunner version or any parsing error occurs
125122
it will raise a buildrunner exception.
126123
"""
127-
buildrunner_version = None
124+
from buildrunner import __version__
125+
126+
parts = __version__.strip().split(".")
127+
buildrunner_version = f"{parts[0]}.{parts[1]}" if len(parts) >= 2 else None
128128

129-
if not os.path.exists(VERSION_FILE_PATH):
129+
if not buildrunner_version:
130+
if __version__ != "DEVELOPMENT":
131+
raise BuildRunnerVersionError("unable to determine buildrunner version")
130132
LOGGER.warning(
131-
f"File {VERSION_FILE_PATH} does not exist. This could indicate an error with "
132-
f"the buildrunner installation. Unable to validate version."
133+
"Unable to determine buildrunner version for validation. Skipping config version check."
133134
)
134135
return
135136

136-
with open(VERSION_FILE_PATH, "r", encoding="utf-8") as version_file:
137-
for line in version_file.readlines():
138-
if "__version__" in line:
139-
try:
140-
version_values = (
141-
line.split("=")[1]
142-
.strip()
143-
.replace("'", "")
144-
.replace('"', "")
145-
.split(".")
146-
)
147-
buildrunner_version = f"{version_values[0]}.{version_values[1]}"
148-
except IndexError as exception:
149-
raise ConfigVersionFormatError(
150-
f'couldn\'t parse version from "{line}"'
151-
) from exception
152-
153-
if not buildrunner_version:
154-
raise BuildRunnerVersionError("unable to determine buildrunner version")
155-
156-
# version is optional and is valid to not have it in the config
157-
if "version" not in config.keys():
137+
if "version" not in config:
158138
return
159139

160140
config_version = config["version"]
161-
162141
try:
163142
if float(config_version) > float(buildrunner_version):
164143
raise ConfigVersionFormatError(
@@ -168,8 +147,7 @@ def _validate_version(config: dict) -> None:
168147
except ValueError as exception:
169148
raise ConfigVersionTypeError(
170149
f'unable to convert config version "{config_version}" '
171-
f'or buildrunner version "{buildrunner_version}" '
172-
f"to a float"
150+
f'or buildrunner version "{buildrunner_version}" to a float'
173151
) from exception
174152

175153

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "uv_build"
44

55
[project]
66
name = "buildrunner"
7-
version = "3.21"
7+
version = "3.22"
88
description = "Docker-based build tool"
99
readme = "README.rst"
1010
requires-python = ">=3.9"

tests/test_version.py

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import OrderedDict
2+
from unittest.mock import patch
23

34
import pytest
45
from buildrunner.config import loader
@@ -18,63 +19,43 @@ def fixture_config_file():
1819
yield config
1920

2021

21-
@pytest.fixture(name="version_file", autouse=True)
22-
def fixture_setup_version_file(tmp_path):
23-
version_file = tmp_path / "version.py"
24-
version_file.write_text(f"__version__ = '{buildrunner_version}'")
25-
original_path = loader.VERSION_FILE_PATH
26-
loader.VERSION_FILE_PATH = str(version_file)
27-
yield str(version_file)
28-
loader.VERSION_FILE_PATH = original_path
29-
30-
22+
@patch("buildrunner.__version__", buildrunner_version)
3123
def test_valid_version_file(config):
3224
loader._validate_version(config=config)
3325

3426

27+
@patch("buildrunner.__version__", "DEVELOPMENT")
3528
def test_missing_version_file(config):
36-
# No exception for a missing version file it just prints a warning
37-
loader.VERSION_FILE_PATH = "bogus"
29+
# When version is DEVELOPMENT, validation is skipped (no exception)
3830
loader._validate_version(config=config)
3931

4032

41-
def test_missing_version_in_version_file(config, version_file):
42-
with open(version_file, "w") as file:
43-
file.truncate()
44-
33+
@patch("buildrunner.__version__", "")
34+
def test_missing_version_in_version_file(config):
4535
with pytest.raises(BuildRunnerVersionError):
4636
loader._validate_version(config=config)
4737

4838

49-
def test_invalid_delim_version(config, version_file):
50-
with open(version_file, "w") as file:
51-
file.truncate()
52-
file.write("__version__: '1.3.4'")
53-
54-
with pytest.raises(ConfigVersionFormatError):
55-
loader._validate_version(config=config)
56-
39+
@patch("buildrunner.__version__", "1.3.4")
40+
def test_valid_version_parsing(config):
41+
# 1.3.4 parses to 1.3; config 2.0 > 1.3 so use config without version
42+
loader._validate_version(config=OrderedDict({}))
5743

58-
def test_invalid_config_number_version(config, version_file):
59-
with open(version_file, "w") as file:
60-
file.truncate()
61-
file.write("__version__ = '1'")
6244

63-
with pytest.raises(ConfigVersionFormatError):
45+
@patch("buildrunner.__version__", "1")
46+
def test_invalid_single_component_version(config):
47+
with pytest.raises(BuildRunnerVersionError):
6448
loader._validate_version(config=config)
6549

6650

67-
def test_invalid_config_version_type(config, version_file):
68-
with open(version_file, "w") as file:
69-
file.truncate()
70-
file.write("__version__ = 'two.zero.five'")
71-
51+
@patch("buildrunner.__version__", "2.0.701")
52+
def test_invalid_config_version_type(config):
7253
with pytest.raises(ConfigVersionTypeError):
73-
loader._validate_version(config=config)
54+
loader._validate_version(config={"version": "two.zero.five"})
7455

7556

7657
def test_bad_version(config):
77-
config = OrderedDict({"version": 2.1})
78-
79-
with pytest.raises(ConfigVersionFormatError):
80-
loader._validate_version(config=config)
58+
bad_config = OrderedDict({"version": 2.1})
59+
with patch("buildrunner.__version__", buildrunner_version):
60+
with pytest.raises(ConfigVersionFormatError):
61+
loader._validate_version(config=bad_config)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)