Skip to content

Commit 28e7bd9

Browse files
Merge pull request #4 from LukasNiessen/codex/p0-ci-metadata
Codex/p0 ci metadata
2 parents 8e0627c + 91ec10a commit 28e7bd9

7 files changed

Lines changed: 139 additions & 7 deletions

File tree

.github/workflows/integrate.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ jobs:
2626
pip-${{ runner.os }}-${{ matrix.python-version }}-
2727
- name: Install dependencies
2828
run: pip install -e ".[dev]"
29+
- name: Check release metadata
30+
run: python scripts/check_release_metadata.py
2931
- name: Lint
30-
run: ruff check src/
32+
run: ruff check src/ scripts/
3133
- name: Type check
3234
run: mypy src/archunitpython/ --ignore-missing-imports
3335
- name: Test
3436
run: pytest --tb=short -q
37+
- name: Build package
38+
run: python -m build
3539

3640
publish:
3741
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

.releaserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
[
1313
"@semantic-release/exec",
1414
{
15-
"prepareCmd": "python -c \"import re, pathlib; files=[('pyproject.toml', r'(?m)^version = \\\"[^\\\"]+\\\"$', 'version = \\\"${nextRelease.version}\\\"'), ('src/archunitpython/__init__.py', r'(?m)^__version__ = \\\"[^\\\"]+\\\"$', '__version__ = \\\"${nextRelease.version}\\\"')]; [pathlib.Path(path).write_text(re.sub(pattern, replacement, pathlib.Path(path).read_text())) for path, pattern, replacement in files]\""
15+
"prepareCmd": "python scripts/bump_release_version.py ${nextRelease.version}"
1616
}
1717
],
1818
[

BACKLOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ This backlog collects product and maintenance ideas from project research.
44

55
## P0 - Maintenance And Correctness
66

7-
- Keep package metadata synchronized across `pyproject.toml`, `CHANGELOG.md`, and `src/archunitpython/__init__.py`.
8-
- Keep tool configuration valid for the supported Python range, especially mypy and Ruff target versions.
9-
- Add a release metadata check that fails when the exported `__version__` differs from the project version.
10-
- Add CI jobs that run tests, Ruff, mypy, and a package build from a clean checkout.
7+
- [x] Keep package metadata synchronized across `pyproject.toml`, `CHANGELOG.md`, and `src/archunitpython/__init__.py`.
8+
- [x] Keep tool configuration valid for the supported Python range, especially mypy and Ruff target versions.
9+
- [x] Add a release metadata check that fails when the exported `__version__` differs from the project version.
10+
- [x] Add CI jobs that run tests, Ruff, mypy, and a package build from a clean checkout.
1111

1212
## P1 - Adoption Workflow
1313

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,12 @@ We use ourselves to ensure the architectural rules for this repository.
567567
568568
## 🦊 Contributing
569569
570-
We highly appreciate contributions. We use GitHub Flow, meaning that we use feature branches. As soon as something is merged or pushed to `main` it gets deployed. Versioning is automated via [Conventional Commits](https://www.conventionalcommits.org/). See more in [Contributing](CONTRIBUTING.md).
570+
We highly appreciate contributions. See [Contributing](CONTRIBUTING.md) for the full workflow.
571+
572+
- Use feature branches and open pull requests against `main`.
573+
- Use [Conventional Commits](https://www.conventionalcommits.org/) so releases can be versioned automatically.
574+
- Do not bump versions manually for normal feature or fix work; semantic-release updates `pyproject.toml`, `src/archunitpython/__init__.py`, and `CHANGELOG.md`.
575+
- CI checks linting, typing, tests, package builds, and release metadata sync.
571576

572577
## ℹ️ FAQ
573578

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Changelog = "https://github.com/LukasNiessen/ArchUnitPython/blob/main/CHANGELOG.
5454

5555
[project.optional-dependencies]
5656
dev = [
57+
"build>=1.0",
5758
"pytest>=7.0",
5859
"pytest-cov>=4.0",
5960
"mypy>=1.0",

scripts/bump_release_version.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Synchronize release version metadata for semantic-release."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
import sys
7+
from pathlib import Path
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
PYPROJECT = ROOT / "pyproject.toml"
11+
PACKAGE_INIT = ROOT / "src" / "archunitpython" / "__init__.py"
12+
13+
14+
def replace_once(pattern: str, replacement: str, content: str, path: Path) -> str:
15+
updated, count = re.subn(pattern, replacement, content, count=1, flags=re.MULTILINE)
16+
if count != 1:
17+
raise RuntimeError(f"Could not update version in {path}")
18+
return updated
19+
20+
21+
def bump_version(version: str) -> None:
22+
pyproject = PYPROJECT.read_text(encoding="utf-8")
23+
PYPROJECT.write_text(
24+
replace_once(r'^version = "[^"]+"$', f'version = "{version}"', pyproject, PYPROJECT),
25+
encoding="utf-8",
26+
)
27+
28+
package_init = PACKAGE_INIT.read_text(encoding="utf-8")
29+
PACKAGE_INIT.write_text(
30+
replace_once(
31+
r'^__version__ = "[^"]+"$',
32+
f'__version__ = "{version}"',
33+
package_init,
34+
PACKAGE_INIT,
35+
),
36+
encoding="utf-8",
37+
)
38+
39+
40+
def main() -> int:
41+
if len(sys.argv) != 2:
42+
print("Usage: python scripts/bump_release_version.py <version>", file=sys.stderr)
43+
return 2
44+
45+
bump_version(sys.argv[1])
46+
return 0
47+
48+
49+
if __name__ == "__main__":
50+
raise SystemExit(main())

scripts/check_release_metadata.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Check that release metadata stays synchronized."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
import re
7+
import sys
8+
from pathlib import Path
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
PYPROJECT = ROOT / "pyproject.toml"
12+
PACKAGE_INIT = ROOT / "src" / "archunitpython" / "__init__.py"
13+
CHANGELOG = ROOT / "CHANGELOG.md"
14+
15+
16+
def read_project_version() -> str:
17+
content = PYPROJECT.read_text(encoding="utf-8")
18+
match = re.search(r'^version = "([^"]+)"$', content, re.MULTILINE)
19+
if match is None:
20+
raise RuntimeError("Could not find project.version in pyproject.toml")
21+
return match.group(1)
22+
23+
24+
def read_package_version() -> str:
25+
module = ast.parse(PACKAGE_INIT.read_text(encoding="utf-8"))
26+
for statement in module.body:
27+
if (
28+
isinstance(statement, ast.Assign)
29+
and len(statement.targets) == 1
30+
and isinstance(statement.targets[0], ast.Name)
31+
and statement.targets[0].id == "__version__"
32+
and isinstance(statement.value, ast.Constant)
33+
and isinstance(statement.value.value, str)
34+
):
35+
return statement.value.value
36+
raise RuntimeError("Could not find __version__ in src/archunitpython/__init__.py")
37+
38+
39+
def changelog_contains_version(version: str) -> bool:
40+
content = CHANGELOG.read_text(encoding="utf-8")
41+
heading_pattern = re.compile(
42+
rf"^#+\s+(?:\[)?{re.escape(version)}(?:\])?(?:\s|\(|$)",
43+
re.MULTILINE,
44+
)
45+
return heading_pattern.search(content) is not None
46+
47+
48+
def main() -> int:
49+
project_version = read_project_version()
50+
package_version = read_package_version()
51+
52+
errors = []
53+
if package_version != project_version:
54+
errors.append(
55+
f"Package __version__ ({package_version}) does not match "
56+
f"pyproject.toml version ({project_version})."
57+
)
58+
if not changelog_contains_version(project_version):
59+
errors.append(f"CHANGELOG.md does not contain a heading for version {project_version}.")
60+
61+
if errors:
62+
print("Release metadata check failed:", file=sys.stderr)
63+
for error in errors:
64+
print(f"- {error}", file=sys.stderr)
65+
return 1
66+
67+
print(f"Release metadata is synchronized for version {project_version}.")
68+
return 0
69+
70+
71+
if __name__ == "__main__":
72+
raise SystemExit(main())

0 commit comments

Comments
 (0)