Releases: tag each healthy deploy vMAJOR.MINOR.PATCH; /health and node checkin report versions (node-26#49)
Build & Deploy / Build & push images (push) Successful in 4m35s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 1m34s
Build & Deploy / Deploy to VM (push) Successful in 1m54s
Build & Deploy / Report a failed deploy (push) Skipped
Build & Deploy / Tag release (push) Successful in 5s
Build & Deploy / Build & push images (push) Successful in 4m35s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 1m34s
Build & Deploy / Deploy to VM (push) Successful in 1m54s
Build & Deploy / Report a failed deploy (push) Skipped
Build & Deploy / Tag release (push) Successful in 5s
VERSION holds MAJOR.MINOR (1.1). The build job works out the next patch (scripts/next-version.sh, integer count) and stamps it into c2-core as DRB_VERSION, which /health returns. The new "Tag release" job tags the commit only after the deploy job succeeds, so a failed deploy's number is reused. Node checkins carry `version`, stored on the node doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5cdde85d55
commit
b7cdb4452f
@@ -12,9 +12,27 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build & push images
|
name: Build & push images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -33,6 +51,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_SHA=${{ gitea.sha }}
|
GIT_SHA=${{ gitea.sha }}
|
||||||
|
DRB_VERSION=${{ steps.version.outputs.version }}
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.REGISTRY }}/c2-core:latest
|
${{ env.REGISTRY }}/c2-core:latest
|
||||||
${{ env.REGISTRY }}/c2-core:${{ gitea.sha }}
|
${{ env.REGISTRY }}/c2-core:${{ gitea.sha }}
|
||||||
@@ -459,3 +478,26 @@ jobs:
|
|||||||
PY
|
PY
|
||||||
curl -sS -X POST -H "Content-Type: application/json" \
|
curl -sS -X POST -H "Content-Type: application/json" \
|
||||||
--data @/tmp/payload.json "$WEBHOOK" || echo "notification POST failed"
|
--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"
|
||||||
|
|||||||
@@ -13,5 +13,8 @@ COPY tests/ ./tests/
|
|||||||
# -- exactly how production served two-day-old code for two days.
|
# -- exactly how production served two-day-old code for two days.
|
||||||
ARG GIT_SHA=unknown
|
ARG GIT_SHA=unknown
|
||||||
ENV GIT_SHA=$GIT_SHA
|
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"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class MQTTHandler:
|
|||||||
"sdr_count": payload.get("sdr_count"), # None until reported, never a guessed 1
|
"sdr_count": payload.get("sdr_count"), # None until reported, never a guessed 1
|
||||||
"disk_free_bytes": payload.get("disk_free_bytes"),
|
"disk_free_bytes": payload.get("disk_free_bytes"),
|
||||||
"disk_total_bytes": payload.get("disk_total_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),
|
"enforce_override_timeout": payload.get("enforce_override_timeout", True),
|
||||||
"is_overridden": False,
|
"is_overridden": False,
|
||||||
"override_system_id": None,
|
"override_system_id": None,
|
||||||
@@ -168,7 +169,7 @@ class MQTTHandler:
|
|||||||
# records to — a full disk silently stopped node-002 for 17 days.
|
# records to — a full disk silently stopped node-002 for 17 days.
|
||||||
for key in ("secondary_sdr_mode", "secondary_sdr_priority", "secondary_sdr_running",
|
for key in ("secondary_sdr_mode", "secondary_sdr_priority", "secondary_sdr_running",
|
||||||
"sdr_pins", "sdr_devices", "op25_sdr_serial", "radio_channels",
|
"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:
|
if key in payload:
|
||||||
updates[key] = payload[key]
|
updates[key] = payload[key]
|
||||||
if "sdr_count" in payload:
|
if "sdr_count" in payload:
|
||||||
|
|||||||
@@ -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
|
# 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.
|
# tunes, and keeping it out of Settings avoids implying it can be changed.
|
||||||
_GIT_SHA = os.getenv("GIT_SHA", "unknown")
|
_GIT_SHA = os.getenv("GIT_SHA", "unknown")
|
||||||
|
_VERSION = os.getenv("DRB_VERSION", "untagged")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
@@ -197,6 +198,7 @@ async def health():
|
|||||||
# CI asserts this equals the commit it just deployed. Without it a
|
# CI asserts this equals the commit it just deployed. Without it a
|
||||||
# deploy can "succeed" while the previous container is still serving.
|
# deploy can "succeed" while the previous container is still serving.
|
||||||
"git_sha": _GIT_SHA,
|
"git_sha": _GIT_SHA,
|
||||||
|
"version": _VERSION,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 ))"
|
||||||
Reference in New Issue
Block a user