diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 24f0713..2445bf9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -12,9 +12,27 @@ jobs: build: name: Build & push images runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # node-26#49: this build's release, vMAJOR.MINOR.PATCH. Tagged only once + # the deploy is healthy (job "tag" below), so a failed deploy's number is + # reused by the next push. + - name: Work out the release version + id: version + run: | + git fetch --force --tags origin + TAG="$(bash scripts/next-version.sh)" + if [ -z "$TAG" ]; then + TAG="$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -t. -k3,3n | tail -1)" + fi + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + echo "release ${TAG}" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -33,6 +51,7 @@ jobs: push: true build-args: | GIT_SHA=${{ gitea.sha }} + DRB_VERSION=${{ steps.version.outputs.version }} tags: | ${{ env.REGISTRY }}/c2-core:latest ${{ env.REGISTRY }}/c2-core:${{ gitea.sha }} @@ -459,3 +478,26 @@ jobs: PY curl -sS -X POST -H "Content-Type: application/json" \ --data @/tmp/payload.json "$WEBHOOK" || echo "notification POST failed" + + # node-26#49: a healthy deploy is a release. Tag it vMAJOR.MINOR.PATCH with + # the number the build stamped into /health. + tag: + name: Tag release + needs: [build, deploy] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Tag this commit + env: + VERSION: ${{ needs.build.outputs.version }} + run: | + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "no release version from build ('$VERSION')"; exit 1; } + git fetch --force --tags origin + if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then + echo "v$VERSION already exists"; exit 0 + fi + git tag "v$VERSION" + git push origin "refs/tags/v$VERSION" + echo "released v$VERSION" diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9459d4b --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.1 diff --git a/drb-c2-core/Dockerfile b/drb-c2-core/Dockerfile index 8982764..855ca44 100644 --- a/drb-c2-core/Dockerfile +++ b/drb-c2-core/Dockerfile @@ -13,5 +13,8 @@ COPY tests/ ./tests/ # -- exactly how production served two-day-old code for two days. ARG GIT_SHA=unknown ENV GIT_SHA=$GIT_SHA +# The release tag this build becomes (node-26#49), e.g. 1.1.4. +ARG DRB_VERSION=untagged +ENV DRB_VERSION=$DRB_VERSION CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/drb-c2-core/app/internal/mqtt_handler.py b/drb-c2-core/app/internal/mqtt_handler.py index a85bcd9..4c67f8f 100644 --- a/drb-c2-core/app/internal/mqtt_handler.py +++ b/drb-c2-core/app/internal/mqtt_handler.py @@ -131,6 +131,7 @@ class MQTTHandler: "sdr_count": payload.get("sdr_count"), # None until reported, never a guessed 1 "disk_free_bytes": payload.get("disk_free_bytes"), "disk_total_bytes": payload.get("disk_total_bytes"), + "version": payload.get("version"), # installed release (node-26#49) "enforce_override_timeout": payload.get("enforce_override_timeout", True), "is_overridden": False, "override_system_id": None, @@ -168,7 +169,7 @@ class MQTTHandler: # records to — a full disk silently stopped node-002 for 17 days. for key in ("secondary_sdr_mode", "secondary_sdr_priority", "secondary_sdr_running", "sdr_pins", "sdr_devices", "op25_sdr_serial", "radio_channels", - "disk_free_bytes", "disk_total_bytes"): + "disk_free_bytes", "disk_total_bytes", "version"): if key in payload: updates[key] = payload[key] if "sdr_count" in payload: diff --git a/drb-c2-core/app/main.py b/drb-c2-core/app/main.py index 26f16b6..1248132 100644 --- a/drb-c2-core/app/main.py +++ b/drb-c2-core/app/main.py @@ -187,6 +187,7 @@ app.include_router(discord_oauth.router) # auth per-endpoint (org owner / signe # build stamp baked in by the Dockerfile, not configuration anyone sets or # tunes, and keeping it out of Settings avoids implying it can be changed. _GIT_SHA = os.getenv("GIT_SHA", "unknown") +_VERSION = os.getenv("DRB_VERSION", "untagged") @app.get("/health") @@ -197,6 +198,7 @@ async def health(): # CI asserts this equals the commit it just deployed. Without it a # deploy can "succeed" while the previous container is still serving. "git_sha": _GIT_SHA, + "version": _VERSION, } diff --git a/drb-c2-core/tests/test_versioning.py b/drb-c2-core/tests/test_versioning.py new file mode 100644 index 0000000..c9cefc0 --- /dev/null +++ b/drb-c2-core/tests/test_versioning.py @@ -0,0 +1,24 @@ +"""node-26#49 — releases are visible: /health reports the server's, checkin carries the node's.""" +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi.testclient import TestClient + +from app.internal.mqtt_handler import MQTTHandler +from app.main import app + + +def test_health_reports_the_release(): + with patch("app.main._VERSION", "1.1.4"): + body = TestClient(app).get("/health").json() + assert body["version"] == "1.1.4" + assert "git_sha" in body + + +@pytest.mark.asyncio +async def test_checkin_stores_the_node_release(): + with patch("app.internal.mqtt_handler.fstore") as fs: + fs.doc_get = AsyncMock(return_value=None) + fs.doc_set = AsyncMock() + await MQTTHandler()._handle_checkin("n1", {"name": "Pi", "version": "1.1.10"}) + assert fs.doc_set.call_args[0][2]["version"] == "1.1.10" diff --git a/scripts/next-version.sh b/scripts/next-version.sh new file mode 100644 index 0000000..a94df95 --- /dev/null +++ b/scripts/next-version.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# node-26#49 (versioning, shared with Client/scripts) — the release tag this commit should get, or nothing if it has one. +# +# VERSION holds MAJOR.MINOR and is edited by hand. PATCH is automatic: one +# more than the highest existing vMAJOR.MINOR.PATCH tag, compared as an +# integer (so .10 follows .9), starting at 0 for a new MAJOR.MINOR. +# +# Usage: scripts/next-version.sh (from the repo root, tags fetched) +set -euo pipefail + +base="$(tr -d ' \r\n' < VERSION)" +[[ "$base" =~ ^[0-9]+\.[0-9]+$ ]] || { echo "VERSION must be MAJOR.MINOR, got '$base'" >&2; exit 1; } +pattern="^v${base//./\\.}\\.[0-9]+$" + +if git tag --points-at HEAD | grep -qE "$pattern"; then + exit 0 # this commit is already released +fi + +last="$(git tag -l "v${base}.*" | grep -E "$pattern" | sed "s/^v${base}\.//" | sort -n | tail -1 || true)" +echo "v${base}.$(( ${last:--1} + 1 ))"