diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e88dee1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +# Release workflow +# +# develop: python-semantic-release on push → semver tag + GitHub Release → sync master + +name: release + +on: + push: + branches: + - develop + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: release-${{ github.repository }}-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: develop + fetch-depth: 0 + + - name: Reset to workflow SHA + run: git reset --hard ${{ github.sha }} + + - name: Semantic Release + id: semantic + uses: python-semantic-release/python-semantic-release@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: github-actions[bot] + git_committer_email: 41898282+github-actions[bot]@users.noreply.github.com + vcs_release: "false" + + - name: Create GitHub Release + if: steps.semantic.outputs.released == 'true' && steps.semantic.outputs.tag != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.semantic.outputs.tag }} + VERSION: ${{ steps.semantic.outputs.version }} + RELEASE_NOTES: ${{ steps.semantic.outputs.release_notes }} + run: | + set -euo pipefail + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release already exists for $TAG" + gh release view "$TAG" --json url -q .url + exit 0 + fi + NOTES_FILE="$(mktemp)" + if [[ -n "$RELEASE_NOTES" ]]; then + printf '%s\n' "$RELEASE_NOTES" > "$NOTES_FILE" + else + printf 'Release %s\n' "$VERSION" > "$NOTES_FILE" + fi + gh release create "$TAG" --title "$VERSION" --notes-file "$NOTES_FILE" + rm -f "$NOTES_FILE" + + - name: Verify GitHub Release + if: steps.semantic.outputs.released == 'true' && steps.semantic.outputs.tag != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.semantic.outputs.tag }} + run: gh release view "$TAG" + + - name: Sync master from develop + if: steps.semantic.outputs.released == 'true' + run: | + git fetch origin develop + git push origin origin/develop:refs/heads/master diff --git a/CHANGELOG.md b/CHANGELOG.md index b181d0e..a246c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to **adn-server** are documented here. + + ## [2.0.0-rc.4] - 2026-06-18 ### Added diff --git a/README.md b/README.md index b54f590..9800300 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ADN DMR Peer Server -**Version 2.0.0-rc.4** — pairs with **adn-monitor 2.0.0-rc.5** (report v2 slim wire + JSON HELLO). +**Version 2.0.0** — pairs with **adn-monitor 2.0.0** (report v2 slim wire + JSON HELLO). ADN DMR conference bridge server. Configuration is YAML; the codebase follows clean architecture (domain, application, infrastructure). v2 adds integrated **PROXY**, **SubscriptionStore** routing, report v2 to the monitor, and a unified **`adn-server.py`** entrypoint (`--echo`, `--doctor`, `--no-proxy`). See [CHANGELOG.md](CHANGELOG.md). diff --git a/pyproject.toml b/pyproject.toml index ce686db..7b0a1b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ # ADN DMR Peer Server # GPL v3. Derived from ADN DMR Server / HBlink. +# Version: bumped automatically by python-semantic-release (.github/workflows/release.yml). [build-system] requires = ["setuptools>=61", "wheel"] @@ -7,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "adn-server" -version = "2.0.0-rc.4" +version = "2.0.0" description = "ADN DMR Peer Server" readme = "README.md" license = { text = "GPL-3.0-or-later" } @@ -26,6 +27,9 @@ selfservice = ["mysqlclient>=2.0"] dev = ["pytest>=7.0", "jsonschema>=4.0", "paho-mqtt>=2.0"] docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "pymdown-extensions>=10.3", "mkdocs-mermaid2-plugin>=1.1"] +[project.scripts] +adn-server = "adn_server.main:main" + [tool.setuptools.packages.find] where = ["src"] @@ -43,5 +47,34 @@ markers = [ # Entrypoints adjust sys.path before importing the package (intentional E402). "src/adn_server/main.py" = ["E402"] -[project.scripts] -adn-server = "adn_server.main:main" +[tool.semantic_release] +build_command = "" +tag_format = "v{version}" +commit_message = "chore(release): {version}" +version_toml = ["pyproject.toml:project.version"] +version_variables = ["README.md:**Version"] + +[tool.semantic_release.branches.develop] +match = "develop" +prerelease = false + +[tool.semantic_release.branches.master] +match = "master" +prerelease = false + +[tool.semantic_release.commit_parser_options] +allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] + +[tool.semantic_release.changelog.default_templates] +mode = "update" +changelog_file = "CHANGELOG.md" + +[tool.semantic_release.remote] +name = "origin" +type = "github" +ignore_token_for_push = false + +[tool.semantic_release.remote.token] +env = "GH_TOKEN" diff --git a/src/adn_server/__init__.py b/src/adn_server/__init__.py index ca855fa..e2774c6 100644 --- a/src/adn_server/__init__.py +++ b/src/adn_server/__init__.py @@ -37,4 +37,6 @@ """ADN DMR Peer Server — conference bridge (rewrite of bridge_master).""" -__version__ = "2.0.0-rc.4" +from .version import __version__, read_version, repo_root + +__all__ = ["__version__", "read_version", "repo_root"] diff --git a/src/adn_server/version.py b/src/adn_server/version.py new file mode 100644 index 0000000..72a1465 --- /dev/null +++ b/src/adn_server/version.py @@ -0,0 +1,53 @@ +# ADN DMR Peer Server - package version +# +# Copyright (C) 2026 Rodrigo Pérez, CE5RPY +# +# Bumped by python-semantic-release in pyproject.toml; runtime reads checkout or metadata. + +"""Version helpers for CLI, HELLO, and deploy scripts.""" + +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path + + +def repo_root() -> Path: + """Repository root (parent of ``src/``).""" + return Path(__file__).resolve().parents[2] + + +@lru_cache(maxsize=1) +def _version_from_pyproject() -> str: + try: + import tomllib + except ModuleNotFoundError: + import tomli as tomllib # type: ignore[no-redef] + + data = tomllib.loads((repo_root() / "pyproject.toml").read_text(encoding="utf-8")) + ver = data.get("project", {}).get("version") + if isinstance(ver, str) and ver.strip(): + return ver.strip() + raise ValueError("project.version missing in pyproject.toml") + + +@lru_cache(maxsize=1) +def read_version() -> str: + """Human-readable version from ``pyproject.toml`` in checkout, else package metadata.""" + pyproject_path = repo_root() / "pyproject.toml" + if pyproject_path.is_file(): + try: + return _version_from_pyproject() + except ValueError: + pass + from importlib.metadata import PackageNotFoundError, version + + try: + return version("adn-server") + except PackageNotFoundError as e: + raise ValueError( + f"adn-server not installed and no pyproject.toml version: {pyproject_path}" + ) from e + + +__version__ = read_version() diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..5cb52da --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,28 @@ +"""pyproject.toml version and CLI --version.""" + +from __future__ import annotations + +import re +import subprocess +import sys + +from adn_server import __version__, read_version, repo_root + + +def test_read_version_matches_pyproject(): + text = (repo_root() / "pyproject.toml").read_text(encoding="utf-8") + m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE) + assert m, "project.version missing in pyproject.toml" + assert read_version() == m.group(1) + assert __version__ == read_version() + + +def test_adn_server_cli_version(): + proc = subprocess.run( + [sys.executable, "-m", "adn_server.main", "--version"], + capture_output=True, + text=True, + check=True, + cwd=str(repo_root()), + ) + assert proc.stdout.strip() == f"adn-server {read_version()}"