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
6 changes: 5 additions & 1 deletion .github/workflows/integrate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ jobs:
pip-${{ runner.os }}-${{ matrix.python-version }}-
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Check release metadata
run: python scripts/check_release_metadata.py
- name: Lint
run: ruff check src/
run: ruff check src/ scripts/
- name: Type check
run: mypy src/archunitpython/ --ignore-missing-imports
- name: Test
run: pytest --tb=short -q
- name: Build package
run: python -m build

publish:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
Expand Down
2 changes: 1 addition & 1 deletion .releaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[
"@semantic-release/exec",
{
"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]\""
"prepareCmd": "python scripts/bump_release_version.py ${nextRelease.version}"
}
],
[
Expand Down
8 changes: 4 additions & 4 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ This backlog collects product and maintenance ideas from project research.

## P0 - Maintenance And Correctness

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

## P1 - Adoption Workflow

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,12 @@ We use ourselves to ensure the architectural rules for this repository.

## 🦊 Contributing

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).
We highly appreciate contributions. See [Contributing](CONTRIBUTING.md) for the full workflow.

- Use feature branches and open pull requests against `main`.
- Use [Conventional Commits](https://www.conventionalcommits.org/) so releases can be versioned automatically.
- Do not bump versions manually for normal feature or fix work; semantic-release updates `pyproject.toml`, `src/archunitpython/__init__.py`, and `CHANGELOG.md`.
- CI checks linting, typing, tests, package builds, and release metadata sync.

## ℹ️ FAQ

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Changelog = "https://github.com/LukasNiessen/ArchUnitPython/blob/main/CHANGELOG.

[project.optional-dependencies]
dev = [
"build>=1.0",
"pytest>=7.0",
"pytest-cov>=4.0",
"mypy>=1.0",
Expand Down
50 changes: 50 additions & 0 deletions scripts/bump_release_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Synchronize release version metadata for semantic-release."""

from __future__ import annotations

import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = ROOT / "pyproject.toml"
PACKAGE_INIT = ROOT / "src" / "archunitpython" / "__init__.py"


def replace_once(pattern: str, replacement: str, content: str, path: Path) -> str:
updated, count = re.subn(pattern, replacement, content, count=1, flags=re.MULTILINE)
if count != 1:
raise RuntimeError(f"Could not update version in {path}")
return updated


def bump_version(version: str) -> None:
pyproject = PYPROJECT.read_text(encoding="utf-8")
PYPROJECT.write_text(
replace_once(r'^version = "[^"]+"$', f'version = "{version}"', pyproject, PYPROJECT),
encoding="utf-8",
)

package_init = PACKAGE_INIT.read_text(encoding="utf-8")
PACKAGE_INIT.write_text(
replace_once(
r'^__version__ = "[^"]+"$',
f'__version__ = "{version}"',
package_init,
PACKAGE_INIT,
),
encoding="utf-8",
)


def main() -> int:
if len(sys.argv) != 2:
print("Usage: python scripts/bump_release_version.py <version>", file=sys.stderr)
return 2

bump_version(sys.argv[1])
return 0


if __name__ == "__main__":
raise SystemExit(main())
72 changes: 72 additions & 0 deletions scripts/check_release_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Check that release metadata stays synchronized."""

from __future__ import annotations

import ast
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = ROOT / "pyproject.toml"
PACKAGE_INIT = ROOT / "src" / "archunitpython" / "__init__.py"
CHANGELOG = ROOT / "CHANGELOG.md"


def read_project_version() -> str:
content = PYPROJECT.read_text(encoding="utf-8")
match = re.search(r'^version = "([^"]+)"$', content, re.MULTILINE)
if match is None:
raise RuntimeError("Could not find project.version in pyproject.toml")
return match.group(1)


def read_package_version() -> str:
module = ast.parse(PACKAGE_INIT.read_text(encoding="utf-8"))
for statement in module.body:
if (
isinstance(statement, ast.Assign)
and len(statement.targets) == 1
and isinstance(statement.targets[0], ast.Name)
and statement.targets[0].id == "__version__"
and isinstance(statement.value, ast.Constant)
and isinstance(statement.value.value, str)
):
return statement.value.value
raise RuntimeError("Could not find __version__ in src/archunitpython/__init__.py")


def changelog_contains_version(version: str) -> bool:
content = CHANGELOG.read_text(encoding="utf-8")
heading_pattern = re.compile(
rf"^#+\s+(?:\[)?{re.escape(version)}(?:\])?(?:\s|\(|$)",
re.MULTILINE,
)
return heading_pattern.search(content) is not None


def main() -> int:
project_version = read_project_version()
package_version = read_package_version()

errors = []
if package_version != project_version:
errors.append(
f"Package __version__ ({package_version}) does not match "
f"pyproject.toml version ({project_version})."
)
if not changelog_contains_version(project_version):
errors.append(f"CHANGELOG.md does not contain a heading for version {project_version}.")

if errors:
print("Release metadata check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print(f"Release metadata is synchronized for version {project_version}.")
return 0


if __name__ == "__main__":
raise SystemExit(main())