Skip to content

feat: Enhance alert service with new endpoints and statistics #15

feat: Enhance alert service with new endpoints and statistics

feat: Enhance alert service with new endpoints and statistics #15

Workflow file for this run

# Build and publish release artifacts when a version tag is pushed.
#
# Produces:
# - Python wheel (.whl) for the ai-horde-stats-exporter package
# - Dashboard tarball (horde-dashboards-<version>.tar.gz) with app/ and infra/ dirs
# - Release manifest JSON (release-manifest.json) with filenames, checksums, and version
# - SHA-256 checksums file (checksums.txt)
#
# All artifacts are attached to a GitHub Release created from the tag.
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv and Python
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Build wheel
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
# Ensure the wheel version matches the release tag.
# This is authoritative — pyproject.toml carries a development
# placeholder that is overwritten here at release time.
cd packages/ai-horde-stats-exporter
sed -i "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml
uv build --wheel --out-dir ../../dist/
- name: Assemble dashboard tarball
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
STAGING=$(mktemp -d)
# App dashboards (from the Python package source tree)
mkdir -p "$STAGING/app"
cp packages/ai-horde-stats-exporter/src/ai_horde_stats_exporter/dashboards/*.json \
"$STAGING/app/"
# Infra dashboards (from the top-level dashboards directory)
mkdir -p "$STAGING/infra"
find dashboards -name '*.json' | while read -r src; do
dest="$STAGING/infra/$(basename "$src")"
python3 scripts/convert_grafana_v2_to_classic.py "$src" "$dest"
done
# Guardrail: release artifact must contain classic dashboard JSON.
python3 - <<'PY'
import json
import pathlib
base = pathlib.Path("$STAGING") / "infra"
bad = []
for path in sorted(base.glob("*.json")):
data = json.loads(path.read_text())
if isinstance(data, dict) and str(data.get("apiVersion", "")).startswith("dashboard.grafana.app/"):
bad.append(f"{path.name}: still has apiVersion={data.get('apiVersion')}")
if "panels" not in data:
bad.append(f"{path.name}: missing panels")
if "templating" not in data:
bad.append(f"{path.name}: missing templating")
if bad:
raise SystemExit("\n".join(bad))
PY
tar -czf "dist/horde-dashboards-${VERSION}.tar.gz" -C "$STAGING" app infra
rm -rf "$STAGING"
- name: Generate checksums and manifest
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
cd dist
# checksums.txt — one line per file
sha256sum * > checksums.txt
# Structured manifest for automation consumers
WHEEL_FILE=$(ls *.whl)
DASHBOARD_FILE="horde-dashboards-${VERSION}.tar.gz"
WHEEL_SHA=$(sha256sum "$WHEEL_FILE" | cut -d' ' -f1)
DASHBOARD_SHA=$(sha256sum "$DASHBOARD_FILE" | cut -d' ' -f1)
cat > release-manifest.json <<EOF
{
"version": "${VERSION}",
"tag": "${GITHUB_REF_NAME}",
"artifacts": {
"wheel": {
"filename": "${WHEEL_FILE}",
"sha256": "${WHEEL_SHA}"
},
"dashboards": {
"filename": "${DASHBOARD_FILE}",
"sha256": "${DASHBOARD_SHA}"
}
}
}
EOF
# Compact the JSON (remove leading whitespace from heredoc)
python3 -c "
import json, pathlib
p = pathlib.Path('release-manifest.json')
p.write_text(json.dumps(json.loads(p.read_text()), indent=2) + '\n')
"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: dist/
images:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- package: ai-horde-stats-exporter
manifest_key: ai_horde_stats_exporter
image: ghcr.io/haidra-org/ai-horde-stats-exporter
- package: ai-horde-service-alerts
manifest_key: ai_horde_service_alerts
image: ghcr.io/haidra-org/ai-horde-service-alerts
steps:
- uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest
type=sha,format=short
- name: Build and push image
id: build
uses: docker/build-push-action@v6
with:
context: packages/${{ matrix.package }}
file: packages/${{ matrix.package }}/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: true
- name: Emit per-image manifest fragment
env:
MATRIX_KEY: ${{ matrix.manifest_key }}
IMAGE: ${{ matrix.image }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p image-manifests
python3 - <<'PY'
import json
import os
import pathlib
fragment = {
os.environ["MATRIX_KEY"]: {
"ref": f"{os.environ['IMAGE']}:{os.environ['GITHUB_REF_NAME'][1:]}",
"digest": os.environ["DIGEST"],
}
}
out = pathlib.Path("image-manifests") / f"{os.environ['MATRIX_KEY']}.json"
out.write_text(json.dumps(fragment, indent=2) + "\n")
PY
- name: Upload image manifest fragment
uses: actions/upload-artifact@v4
with:
name: image-manifest-${{ matrix.manifest_key }}
path: image-manifests/
manifest:
needs: [build, images]
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts
path: dist/
- name: Download image manifest fragments
uses: actions/download-artifact@v4
with:
pattern: image-manifest-*
path: image-manifests/
merge-multiple: true
- name: Augment release manifest with image refs
run: |
set -euo pipefail
python3 - <<'PY'
import json
import pathlib
manifest_path = pathlib.Path("dist/release-manifest.json")
data = json.loads(manifest_path.read_text())
images: dict = {}
for fragment in sorted(pathlib.Path("image-manifests").glob("*.json")):
images.update(json.loads(fragment.read_text()))
if not images:
raise SystemExit("no image manifest fragments found")
data["images"] = images
manifest_path.write_text(json.dumps(data, indent=2) + "\n")
PY
- name: Refresh checksums
run: |
set -euo pipefail
cd dist
sha256sum * > checksums.txt
- name: Upload updated artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts-final
path: dist/
overwrite: true
release:
needs: manifest
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts-final
path: dist/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--generate-notes \
dist/*