mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-03-28 17:42:55 +01:00
Compare commits
2 Commits
v0.2.14
...
chore/rebu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be6a506fb | ||
|
|
97ebba4f2d |
@@ -1,42 +1,51 @@
|
||||
# Git
|
||||
# VCS
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv
|
||||
venv
|
||||
ENV
|
||||
env
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
# Editor/IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Project directories (generated/runtime)
|
||||
data/
|
||||
out/
|
||||
|
||||
# Configuration (use environment variables in Docker)
|
||||
# Local env/config
|
||||
.env
|
||||
.env.*
|
||||
.envrc
|
||||
.direnv
|
||||
.direnv/
|
||||
meshcore.conf
|
||||
|
||||
# Documentation
|
||||
# Generated/runtime data
|
||||
.data/
|
||||
data/
|
||||
out/
|
||||
.buildx-cache/
|
||||
|
||||
# Docs/tests (not needed in image)
|
||||
docs/
|
||||
tests/
|
||||
*.md
|
||||
!README.md
|
||||
!LICENSE
|
||||
|
||||
# Development files
|
||||
.codex/
|
||||
*.log
|
||||
|
||||
# macOS
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
.codex/
|
||||
|
||||
276
.github/workflows/docker-publish.yml
vendored
276
.github/workflows/docker-publish.yml
vendored
@@ -1,276 +0,0 @@
|
||||
# Build and publish Docker images to GitHub Container Registry
|
||||
#
|
||||
# Triggers:
|
||||
# - On release: Build with version tags (X.Y.Z, X.Y, latest)
|
||||
# - On schedule: Rebuild all tags with fresh base image (OS patches)
|
||||
# - Manual: For testing, optional push
|
||||
#
|
||||
# Security:
|
||||
# - All actions pinned by SHA
|
||||
# - Vulnerability scanning with Trivy
|
||||
# - SBOM and provenance attestation
|
||||
|
||||
name: Docker Build and Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
schedule:
|
||||
# Daily at 4 AM UTC - rebuild with fresh base image
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- docker/**
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- src/**
|
||||
- scripts/**
|
||||
- .github/workflows/docker-publish.yml
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push:
|
||||
description: "Push image to registry"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
|
||||
concurrency:
|
||||
group: docker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# For nightly builds, get the latest release version
|
||||
- name: Get latest release version
|
||||
id: get-version
|
||||
if: github.event_name == 'schedule'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Get latest release tag
|
||||
LATEST_TAG=$(gh release view --json tagName -q '.tagName' 2>/dev/null || echo "")
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "No releases found, skipping nightly build"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
# Strip 'v' prefix if present
|
||||
VERSION=$(echo "$LATEST_TAG" | sed 's/^v//')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Skip if no releases
|
||||
if: github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true'
|
||||
run: |
|
||||
echo "No releases found, skipping nightly build"
|
||||
exit 0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to Container Registry
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Generate tags based on event type
|
||||
- name: Extract metadata (release)
|
||||
id: meta-release
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# X.Y.Z
|
||||
type=semver,pattern={{version}}
|
||||
# X.Y
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
# latest
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Extract metadata (nightly)
|
||||
id: meta-nightly
|
||||
if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true'
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# Rebuild version tags with OS patches
|
||||
type=raw,value=${{ steps.get-version.outputs.version }}
|
||||
# Nightly tags
|
||||
type=raw,value=nightly
|
||||
type=raw,value=nightly-{{date 'YYYYMMDD'}}
|
||||
# Also update latest with security patches
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Extract metadata (manual)
|
||||
id: meta-manual
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,prefix=sha-
|
||||
|
||||
# Build image (release - with cache)
|
||||
- name: Build and push (release)
|
||||
id: build-release
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta-release.outputs.tags }}
|
||||
labels: ${{ steps.meta-release.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
# Build image (nightly - no cache, fresh base)
|
||||
- name: Build and push (nightly)
|
||||
id: build-nightly
|
||||
if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta-nightly.outputs.tags }}
|
||||
labels: ${{ steps.meta-nightly.outputs.labels }}
|
||||
pull: true
|
||||
no-cache: true
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
# Build image (manual)
|
||||
- name: Build and push (manual)
|
||||
id: build-manual
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.meta-manual.outputs.tags }}
|
||||
labels: ${{ steps.meta-manual.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
# Determine image tag for scanning and testing
|
||||
- name: Determine image tag
|
||||
id: image-tag
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
# Strip 'v' prefix to match semver tag format from metadata-action
|
||||
echo "tag=$(echo '${{ github.event.release.tag_name }}' | sed 's/^v//')" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "tag=nightly" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=sha-${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Vulnerability scanning
|
||||
- name: Run Trivy vulnerability scanner
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image-tag.outputs.tag }}
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
severity: "CRITICAL,HIGH"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload Trivy scan results
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
continue-on-error: true
|
||||
|
||||
# Smoke test - verify image runs correctly
|
||||
# Skip for manual runs when push is disabled (image not available to pull)
|
||||
- name: Smoke test
|
||||
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true') && !(github.event_name == 'workflow_dispatch' && inputs.push == false)"
|
||||
run: |
|
||||
IMAGE_TAG="${{ steps.image-tag.outputs.tag }}"
|
||||
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}
|
||||
|
||||
# Test that Python and key modules are available
|
||||
docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \
|
||||
python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')"
|
||||
|
||||
# Attestation (releases only)
|
||||
- name: Generate attestation
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build-release.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
build-pr:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Build image (PR)
|
||||
id: build-pr
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
push: false
|
||||
tags: meshcore-stats:pr-${{ github.event.pull_request.number }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Smoke test (PR)
|
||||
run: |
|
||||
docker run --rm meshcore-stats:pr-${{ github.event.pull_request.number }} \
|
||||
python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')"
|
||||
97
.github/workflows/nightly.yml
vendored
Normal file
97
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Nightly Container
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: nightly
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Build metadata
|
||||
id: vars
|
||||
run: |
|
||||
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=nightly" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push nightly image
|
||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
CACHE_FROM: type=gha
|
||||
CACHE_TO: type=gha,mode=max
|
||||
PLATFORMS: linux/amd64,linux/arm64
|
||||
VERSION: ${{ steps.vars.outputs.version }}
|
||||
VCS_REF: ${{ github.sha }}
|
||||
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: nightly
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Verify multi-arch manifest
|
||||
run: |
|
||||
docker buildx imagetools inspect "${IMAGE_NAME}:nightly" | tee /tmp/imagetools.txt
|
||||
grep -q "linux/amd64" /tmp/imagetools.txt
|
||||
grep -q "linux/arm64" /tmp/imagetools.txt
|
||||
|
||||
- name: Resolve manifest digest
|
||||
id: digest
|
||||
run: |
|
||||
DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:nightly" --format '{{.Digest}}')
|
||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Trivy image scan (report-only)
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.IMAGE_NAME }}:nightly
|
||||
format: sarif
|
||||
output: trivy-nightly.sarif
|
||||
exit-code: '0'
|
||||
vuln-type: os,library
|
||||
ignore-unfixed: true
|
||||
|
||||
- name: Upload Trivy report
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: trivy-nightly
|
||||
path: trivy-nightly.sarif
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Cosign sign image (keyless)
|
||||
run: |
|
||||
cosign sign --yes "${IMAGE_NAME}@${{ steps.digest.outputs.digest }}"
|
||||
137
.github/workflows/pr.yml
vendored
Normal file
137
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: PR Container Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- docker-bake.hcl
|
||||
- docker/**
|
||||
- .github/workflows/**
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- src/**
|
||||
- scripts/**
|
||||
|
||||
concurrency:
|
||||
group: pr-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Lint workflows (actionlint)
|
||||
uses: rhysd/actionlint@0933c147c9d6587653d45fdcb4c497c57a65f9af # v1.7.10
|
||||
|
||||
- name: Lint Dockerfile (hadolint)
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
needs: [lint]
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
|
||||
PLATFORMS: linux/amd64,linux/arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Build metadata
|
||||
id: vars
|
||||
run: |
|
||||
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=pr-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Build multi-arch (amd64+arm64)
|
||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
CACHE_FROM: type=gha
|
||||
CACHE_TO: type=gha,mode=max
|
||||
PLATFORMS: ${{ env.PLATFORMS }}
|
||||
VERSION: ${{ steps.vars.outputs.version }}
|
||||
VCS_REF: ${{ github.sha }}
|
||||
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: pr-multiarch
|
||||
|
||||
- name: Build native image (load)
|
||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
CACHE_FROM: type=gha
|
||||
CACHE_TO: type=gha,mode=max
|
||||
LOCAL_PLATFORM: linux/amd64
|
||||
VERSION: ${{ steps.vars.outputs.version }}
|
||||
VCS_REF: ${{ github.sha }}
|
||||
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: pr-native
|
||||
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: [lint]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Build metadata
|
||||
id: scan-vars
|
||||
run: |
|
||||
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=pr-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build native image for scan (load)
|
||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
CACHE_FROM: type=gha
|
||||
CACHE_TO: type=gha,mode=max
|
||||
LOCAL_PLATFORM: linux/amd64
|
||||
VERSION: ${{ steps.scan-vars.outputs.version }}
|
||||
VCS_REF: ${{ github.sha }}
|
||||
BUILD_DATE: ${{ steps.scan-vars.outputs.build_date }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: pr-native
|
||||
|
||||
- name: Trivy image scan (report-only)
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: meshcore-stats:pr
|
||||
format: sarif
|
||||
output: trivy-report.sarif
|
||||
exit-code: '0'
|
||||
vuln-type: os,library
|
||||
ignore-unfixed: true
|
||||
|
||||
- name: Upload Trivy report
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: trivy-report-pr
|
||||
path: trivy-report.sarif
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
104
.github/workflows/release.yml
vendored
Normal file
104
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Release Container
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Validate tag and set metadata
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ ! "$GITHUB_REF_NAME" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "Tag must match vMAJOR.MINOR.PATCH (e.g., v1.2.3)." >&2
|
||||
exit 1
|
||||
fi
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
PATCH="${BASH_REMATCH[3]}"
|
||||
VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tags=${IMAGE_NAME}:${MAJOR},${IMAGE_NAME}:${MAJOR}.${MINOR},${IMAGE_NAME}:${VERSION},${IMAGE_NAME}:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push release image
|
||||
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
CACHE_FROM: type=gha
|
||||
CACHE_TO: type=gha,mode=max
|
||||
PLATFORMS: linux/amd64,linux/arm64
|
||||
VERSION: ${{ steps.vars.outputs.version }}
|
||||
VCS_REF: ${{ github.sha }}
|
||||
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
|
||||
RELEASE_TAGS: ${{ steps.vars.outputs.release_tags }}
|
||||
with:
|
||||
files: docker-bake.hcl
|
||||
targets: release
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Verify multi-arch manifest
|
||||
run: |
|
||||
docker buildx imagetools inspect "${IMAGE_NAME}:${{ steps.vars.outputs.version }}" | tee /tmp/imagetools.txt
|
||||
grep -q "linux/amd64" /tmp/imagetools.txt
|
||||
grep -q "linux/arm64" /tmp/imagetools.txt
|
||||
|
||||
- name: Resolve manifest digest
|
||||
id: digest
|
||||
run: |
|
||||
DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:${{ steps.vars.outputs.version }}" --format '{{.Digest}}')
|
||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Trivy image scan (report-only)
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.version }}
|
||||
format: sarif
|
||||
output: trivy-release.sarif
|
||||
exit-code: '0'
|
||||
vuln-type: os,library
|
||||
ignore-unfixed: true
|
||||
|
||||
- name: Upload Trivy report
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: trivy-release
|
||||
path: trivy-release.sarif
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Cosign sign image (keyless)
|
||||
run: |
|
||||
cosign sign --yes "${IMAGE_NAME}@${{ steps.digest.outputs.digest }}"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,6 +25,7 @@ meshcore.conf
|
||||
|
||||
# Docker local overrides
|
||||
docker-compose.override.yml
|
||||
.buildx-cache/
|
||||
|
||||
# Data directories (keep structure, ignore content)
|
||||
data/snapshots/companion/**/*.json
|
||||
@@ -48,4 +49,4 @@ out/*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.direnv
|
||||
.direnv
|
||||
|
||||
13
AGENTS.md
13
AGENTS.md
@@ -359,16 +359,11 @@ Jobs configured in `docker/ofelia.ini`:
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
`.github/workflows/docker-publish.yml` builds and publishes Docker images for `linux/amd64`, `linux/arm64`, and `linux/arm/v7`:
|
||||
Container workflows (amd64 + arm64 only):
|
||||
|
||||
| Trigger | Tags Created |
|
||||
|---------|--------------|
|
||||
| Release | `X.Y.Z`, `X.Y`, `latest` |
|
||||
| Nightly (4 AM UTC) | Rebuilds all version tags + `nightly`, `nightly-YYYYMMDD` |
|
||||
| Manual | `sha-xxxxxx` |
|
||||
| Pull request | Builds image (linux/amd64) without pushing and runs a smoke test |
|
||||
|
||||
**Nightly rebuilds** ensure version tags always include the latest OS security patches. This is a common pattern used by official Docker images (nginx, postgres, node). Users needing reproducibility should pin by SHA digest or use dated nightly tags.
|
||||
- `pr.yml`: pull_request lint (actionlint/hadolint), container-relevant path filter, multi-arch build (no push), native image build for report-only Trivy scan.
|
||||
- `release.yml`: tag push `vMAJOR.MINOR.PATCH` → push `MAJOR`, `MAJOR.MINOR`, `MAJOR.MINOR.PATCH`, `latest` with SBOM + provenance attestations, Trivy report artifact, cosign keyless signing, and manifest platform verification.
|
||||
- `nightly.yml`: scheduled + workflow_dispatch → push `nightly` with SBOM + provenance attestations, Trivy report artifact, cosign keyless signing, and manifest platform verification.
|
||||
|
||||
GitHub Actions use version tags in workflows, and Renovate is configured in `renovate.json` to pin action digests, maintain lockfiles, and auto-merge patch + digest updates once required checks pass (with automatic rebases when behind `main`).
|
||||
|
||||
|
||||
141
Dockerfile
141
Dockerfile
@@ -1,155 +1,100 @@
|
||||
# =============================================================================
|
||||
# Stage 0: Ofelia binary
|
||||
# =============================================================================
|
||||
FROM golang:1.25-bookworm@sha256:2c7c65601b020ee79db4c1a32ebee0bf3d6b298969ec683e24fcbea29305f10e AS ofelia-builder
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Ofelia version (built from source for multi-arch support)
|
||||
FROM ghcr.io/astral-sh/uv:0.9.24@sha256:816fdce3387ed2142e37d2e56e1b1b97ccc1ea87731ba199dc8a25c04e4997c5 AS uv
|
||||
|
||||
FROM golang:1.25-bookworm@sha256:2c7c65601b020ee79db4c1a32ebee0bf3d6b298969ec683e24fcbea29305f10e AS ofelia-builder
|
||||
ARG OFELIA_VERSION=0.3.12
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
ca-certificates \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src/ofelia
|
||||
RUN git clone --depth 1 --branch "v${OFELIA_VERSION}" https://github.com/mcuadros/ofelia.git /src/ofelia
|
||||
RUN git clone --depth 1 --branch "v${OFELIA_VERSION}" https://github.com/mcuadros/ofelia.git .
|
||||
|
||||
RUN set -ex; \
|
||||
if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
GOARCH="amd64"; \
|
||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||
GOARCH="arm64"; \
|
||||
elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \
|
||||
GOARCH="arm"; \
|
||||
GOARM="7"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \
|
||||
fi; \
|
||||
if [ -n "${GOARM:-}" ]; then \
|
||||
export GOARM; \
|
||||
fi; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" go build -o /usr/local/bin/ofelia .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
set -eux; \
|
||||
case "$TARGETARCH" in \
|
||||
amd64|arm64) GOARCH="$TARGETARCH" ;; \
|
||||
*) echo "Unsupported architecture: $TARGETARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" \
|
||||
go build -trimpath -ldflags "-s -w" -o /usr/local/bin/ofelia .
|
||||
|
||||
# =============================================================================
|
||||
# Stage 1: Build dependencies
|
||||
# =============================================================================
|
||||
FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7 AS builder
|
||||
|
||||
# uv version and checksums (verified from GitHub releases)
|
||||
ARG UV_VERSION=0.9.24
|
||||
ARG UV_SHA256_AMD64=fb13ad85106da6b21dd16613afca910994446fe94a78ee0b5bed9c75cd066078
|
||||
ARG UV_SHA256_ARM64=9b291a1a4f2fefc430e4fc49c00cb93eb448d41c5c79edf45211ceffedde3334
|
||||
ARG UV_SHA256_ARMV7=8d05b55fe2108ecab3995c2b656679a72c543fd9dc72eeb3a525106a709cfdcb
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
# Install build dependencies for Python packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libfreetype6-dev \
|
||||
libpng-dev \
|
||||
curl \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download and verify uv binary in builder stage
|
||||
RUN set -ex; \
|
||||
if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
UV_ARCH="x86_64"; \
|
||||
UV_SHA256="$UV_SHA256_AMD64"; \
|
||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||
UV_ARCH="aarch64"; \
|
||||
UV_SHA256="$UV_SHA256_ARM64"; \
|
||||
elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \
|
||||
UV_ARCH="armv7"; \
|
||||
UV_SHA256="$UV_SHA256_ARMV7"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \
|
||||
fi; \
|
||||
curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}-unknown-linux-gnu.tar.gz" \
|
||||
-o /tmp/uv.tar.gz \
|
||||
&& echo "${UV_SHA256} /tmp/uv.tar.gz" | sha256sum -c - \
|
||||
&& tar -xzf /tmp/uv.tar.gz -C /usr/local/bin --strip-components=1 --wildcards "*/uv" \
|
||||
&& rm /tmp/uv.tar.gz \
|
||||
&& chmod +x /usr/local/bin/uv
|
||||
COPY --from=uv /uv /uvx /usr/local/bin/
|
||||
|
||||
# Create virtual environment
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH" \
|
||||
UV_PROJECT_ENVIRONMENT=/opt/venv
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-install-project
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime
|
||||
# =============================================================================
|
||||
FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7
|
||||
|
||||
# OCI Labels
|
||||
LABEL org.opencontainers.image.source="https://github.com/jorijn/meshcore-stats"
|
||||
LABEL org.opencontainers.image.description="MeshCore Stats - LoRa mesh network monitoring"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
ARG BUILD_DATE=1970-01-01T00:00:00Z
|
||||
ARG VCS_REF=unknown
|
||||
ARG VERSION=dev
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/jorijn/meshcore-stats" \
|
||||
org.opencontainers.image.description="MeshCore Stats - LoRa mesh network monitoring" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.version=$VERSION
|
||||
|
||||
# Install runtime dependencies
|
||||
# - tini: init system for proper signal handling
|
||||
# - libfreetype6, libpng16-16: matplotlib runtime libraries
|
||||
# - fontconfig, fonts-dejavu-core: fonts for chart text rendering
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tini \
|
||||
libfreetype6 \
|
||||
libpng16-16 \
|
||||
ca-certificates \
|
||||
fontconfig \
|
||||
fonts-dejavu-core \
|
||||
libfreetype6 \
|
||||
libpng16-16 \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
# Build font cache for matplotlib
|
||||
&& fc-cache -f \
|
||||
# Remove setuid/setgid binaries for security
|
||||
&& find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true
|
||||
&& find /usr /bin /sbin /usr/local /usr/lib -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
|
||||
|
||||
# Create non-root user with dialout group for serial access
|
||||
RUN groupadd -g 1000 meshmon \
|
||||
&& useradd -u 1000 -g meshmon -G dialout -s /sbin/nologin meshmon \
|
||||
&& mkdir -p /data/state /out /tmp/matplotlib \
|
||||
&& chown -R meshmon:meshmon /data /out /tmp/matplotlib
|
||||
RUN set -eux; \
|
||||
if ! getent group dialout >/dev/null; then groupadd -r dialout; fi; \
|
||||
groupadd -g 1000 meshmon; \
|
||||
useradd -u 1000 -g 1000 -G dialout -m -s /usr/sbin/nologin meshmon; \
|
||||
mkdir -p /app /data/state /out /tmp/matplotlib; \
|
||||
chown -R meshmon:meshmon /app /data /out /tmp/matplotlib
|
||||
|
||||
# Copy Ofelia binary from builder (keeps curl out of runtime image)
|
||||
COPY --from=ofelia-builder /usr/local/bin/ofelia /usr/local/bin/ofelia
|
||||
|
||||
# Copy virtual environment from builder
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=meshmon:meshmon src/ /app/src/
|
||||
COPY --chown=meshmon:meshmon scripts/ /app/scripts/
|
||||
COPY --chown=meshmon:meshmon docker/ofelia.ini /app/ofelia.ini
|
||||
|
||||
# Environment configuration
|
||||
# - PATH: Include venv so Ofelia can run Python
|
||||
# - PYTHONPATH: Allow imports from src/meshmon
|
||||
# - PYTHONUNBUFFERED: Ensure logs are output immediately
|
||||
# - PYTHONDONTWRITEBYTECODE: Don't create .pyc files
|
||||
# - MPLCONFIGDIR: Matplotlib font cache directory
|
||||
ENV PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/app/src \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
MPLCONFIGDIR=/tmp/matplotlib
|
||||
MPLCONFIGDIR=/tmp/matplotlib \
|
||||
XDG_CACHE_HOME=/tmp \
|
||||
HOME=/tmp
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Run as non-root user
|
||||
USER meshmon
|
||||
USER 1000:1000
|
||||
|
||||
# Use tini as init system for proper signal handling
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
# Run Ofelia scheduler
|
||||
CMD ["ofelia", "daemon", "--config=/app/ofelia.ini"]
|
||||
|
||||
# Health check - verify database is accessible
|
||||
HEALTHCHECK --interval=5m --timeout=30s --start-period=60s --retries=3 \
|
||||
CMD python -c "import sqlite3; sqlite3.connect('/data/state/metrics.db').execute('SELECT 1')" || exit 1
|
||||
|
||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
SHELL := /bin/sh
|
||||
|
||||
IMAGE_NAME ?= ghcr.io/jorijn/meshcore-stats
|
||||
PLATFORMS ?= linux/amd64,linux/arm64
|
||||
LOCAL_PLATFORM ?= $(shell (docker info -f '{{.OSType}}/{{.Architecture}}' 2>/dev/null || echo linux/amd64) | tr -d '[:space:]')
|
||||
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
VCS_REF ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
VERSION ?= dev
|
||||
CACHE_FROM ?= type=local,src=.buildx-cache
|
||||
CACHE_TO ?= type=local,dest=.buildx-cache,mode=max
|
||||
|
||||
.PHONY: docker-build docker-buildx-all
|
||||
|
||||
docker-build:
|
||||
LOCAL_PLATFORM=$(LOCAL_PLATFORM) docker buildx bake pr-native \
|
||||
--set *.args.BUILD_DATE=$(BUILD_DATE) \
|
||||
--set *.args.VCS_REF=$(VCS_REF) \
|
||||
--set *.args.VERSION=$(VERSION) \
|
||||
--set *.cache-from=$(CACHE_FROM) \
|
||||
--set *.cache-to=$(CACHE_TO)
|
||||
|
||||
docker-buildx-all:
|
||||
PLATFORMS=$(PLATFORMS) docker buildx bake pr-multiarch \
|
||||
--set *.args.BUILD_DATE=$(BUILD_DATE) \
|
||||
--set *.args.VCS_REF=$(VCS_REF) \
|
||||
--set *.args.VERSION=$(VERSION) \
|
||||
--set *.cache-from=$(CACHE_FROM) \
|
||||
--set *.cache-to=$(CACHE_TO)
|
||||
42
README.md
42
README.md
@@ -56,7 +56,7 @@ docker compose logs meshcore-stats | head -20
|
||||
- Remote repeater node reachable via LoRa from the companion
|
||||
|
||||
**Resource requirements:** ~100MB memory, ~100MB disk per year of data.
|
||||
**Container architectures:** `linux/amd64`, `linux/arm64`, and `linux/arm/v7` (32-bit).
|
||||
**Container architectures:** `linux/amd64`, `linux/arm64`.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -157,6 +157,46 @@ cp data/state/metrics.db data/state/metrics.db.backup
|
||||
|
||||
> **Note**: `docker compose down` preserves your data. Use `docker compose down -v` only if you want to delete everything.
|
||||
|
||||
## Container Images & CI
|
||||
|
||||
### Tags and Platforms
|
||||
|
||||
- **Release tags** (on `vMAJOR.MINOR.PATCH`): `MAJOR`, `MAJOR.MINOR`, `MAJOR.MINOR.PATCH`, `latest`
|
||||
- **Nightly tag**: `nightly`
|
||||
- **Platforms**: `linux/amd64`, `linux/arm64`
|
||||
|
||||
### Security Artifacts
|
||||
|
||||
- **SBOM and provenance** are attached to the pushed images in GHCR (view in the image details/attestations UI).
|
||||
- **Vulnerability reports** are uploaded as GitHub Actions artifacts for each release/nightly build.
|
||||
|
||||
Example cosign verification (release tag):
|
||||
|
||||
```bash
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp 'https://github.com/jorijn/meshcore-stats/.github/workflows/release.yml@refs/tags/v1.2.3' \
|
||||
ghcr.io/jorijn/meshcore-stats:1.2.3
|
||||
```
|
||||
|
||||
### Local Workflow Testing
|
||||
|
||||
Prerequisites:
|
||||
- Docker Desktop (or Docker Engine) with Buildx enabled
|
||||
- QEMU/binfmt installed for multi-arch builds
|
||||
|
||||
Commands:
|
||||
|
||||
```bash
|
||||
# Native-arch build (loads into local Docker)
|
||||
make docker-build
|
||||
|
||||
# Multi-arch build validation (amd64 + arm64, no push)
|
||||
make docker-buildx-all
|
||||
```
|
||||
|
||||
Use `make docker-buildx-all` for full multi-arch validation.
|
||||
|
||||
### Manual Installation (Alternative)
|
||||
|
||||
For environments where Docker is not available.
|
||||
|
||||
72
docker-bake.hcl
Normal file
72
docker-bake.hcl
Normal file
@@ -0,0 +1,72 @@
|
||||
variable "IMAGE_NAME" {
|
||||
default = "ghcr.io/jorijn/meshcore-stats"
|
||||
}
|
||||
|
||||
variable "PLATFORMS" {
|
||||
default = "linux/amd64,linux/arm64"
|
||||
}
|
||||
|
||||
variable "LOCAL_PLATFORM" {
|
||||
default = "linux/amd64"
|
||||
}
|
||||
|
||||
variable "CACHE_FROM" {
|
||||
default = "type=local,src=.buildx-cache"
|
||||
}
|
||||
|
||||
variable "CACHE_TO" {
|
||||
default = "type=local,dest=.buildx-cache,mode=max"
|
||||
}
|
||||
|
||||
variable "VERSION" {
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
variable "VCS_REF" {
|
||||
default = "unknown"
|
||||
}
|
||||
|
||||
variable "BUILD_DATE" {
|
||||
default = "1970-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
variable "RELEASE_TAGS" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
target "base" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
platforms = split(",", PLATFORMS)
|
||||
args = {
|
||||
VERSION = VERSION
|
||||
VCS_REF = VCS_REF
|
||||
BUILD_DATE = BUILD_DATE
|
||||
}
|
||||
cache-from = [CACHE_FROM]
|
||||
cache-to = [CACHE_TO]
|
||||
}
|
||||
|
||||
target "pr-multiarch" {
|
||||
inherits = ["base"]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "pr-native" {
|
||||
inherits = ["base"]
|
||||
platforms = [LOCAL_PLATFORM]
|
||||
tags = ["meshcore-stats:pr"]
|
||||
output = ["type=docker"]
|
||||
}
|
||||
|
||||
target "release" {
|
||||
inherits = ["base"]
|
||||
tags = RELEASE_TAGS != "" ? split(",", RELEASE_TAGS) : []
|
||||
push = true
|
||||
}
|
||||
|
||||
target "nightly" {
|
||||
inherits = ["base"]
|
||||
tags = ["${IMAGE_NAME}:nightly"]
|
||||
push = true
|
||||
}
|
||||
@@ -17,6 +17,12 @@
|
||||
],
|
||||
"pinDigests": true
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"dockerfile"
|
||||
],
|
||||
"pinDigests": true
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"docker-compose"
|
||||
|
||||
@@ -36,6 +36,7 @@ ThemeName = Literal["light", "dark"]
|
||||
BIN_30_MINUTES = 1800 # 30 minutes in seconds
|
||||
BIN_2_HOURS = 7200 # 2 hours in seconds
|
||||
BIN_1_DAY = 86400 # 1 day in seconds
|
||||
MIN_COUNTER_INTERVAL_RATIO = 0.9 # Allow small scheduling jitter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -223,25 +224,38 @@ def load_timeseries_from_db(
|
||||
# For counter metrics, calculate rate of change
|
||||
if is_counter:
|
||||
rate_points: list[tuple[datetime, float]] = []
|
||||
cfg = get_config()
|
||||
min_interval = max(
|
||||
1.0,
|
||||
(cfg.companion_step if role == "companion" else cfg.repeater_step)
|
||||
* MIN_COUNTER_INTERVAL_RATIO,
|
||||
)
|
||||
|
||||
for i in range(1, len(raw_points)):
|
||||
prev_ts, prev_val = raw_points[i - 1]
|
||||
curr_ts, curr_val = raw_points[i]
|
||||
|
||||
delta_val = curr_val - prev_val
|
||||
prev_ts, prev_val = raw_points[0]
|
||||
for curr_ts, curr_val in raw_points[1:]:
|
||||
delta_secs = (curr_ts - prev_ts).total_seconds()
|
||||
|
||||
if delta_secs <= 0:
|
||||
continue
|
||||
if delta_secs < min_interval:
|
||||
log.debug(
|
||||
f"Skipping counter sample for {metric} at {curr_ts} "
|
||||
f"({delta_secs:.1f}s < {min_interval:.1f}s)"
|
||||
)
|
||||
continue
|
||||
|
||||
delta_val = curr_val - prev_val
|
||||
|
||||
# Skip negative deltas (device reboot)
|
||||
if delta_val < 0:
|
||||
log.debug(f"Counter reset detected for {metric} at {curr_ts}")
|
||||
prev_ts, prev_val = curr_ts, curr_val
|
||||
continue
|
||||
|
||||
# Calculate per-second rate, then apply scaling (typically x60 for per-minute)
|
||||
rate = (delta_val / delta_secs) * scale
|
||||
rate_points.append((curr_ts, rate))
|
||||
prev_ts, prev_val = curr_ts, curr_val
|
||||
|
||||
raw_points = rate_points
|
||||
else:
|
||||
|
||||
@@ -67,10 +67,49 @@ class TestCounterToRateConversion:
|
||||
assert ts.points[0].value == pytest.approx(expected_rate)
|
||||
assert ts.points[1].value == pytest.approx(expected_rate)
|
||||
|
||||
def test_applies_scale_factor(self, initialized_db, configured_env):
|
||||
def test_counter_rate_short_interval_under_step_is_skipped(
|
||||
self,
|
||||
initialized_db,
|
||||
configured_env,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Short sampling intervals are skipped to avoid rate spikes."""
|
||||
base_ts = 1704067200
|
||||
|
||||
monkeypatch.setenv("REPEATER_STEP", "900")
|
||||
import meshmon.env
|
||||
|
||||
meshmon.env._config = None
|
||||
|
||||
insert_metrics(base_ts, "repeater", {"nb_recv": 0.0}, initialized_db)
|
||||
insert_metrics(base_ts + 900, "repeater", {"nb_recv": 100.0}, initialized_db)
|
||||
insert_metrics(base_ts + 904, "repeater", {"nb_recv": 110.0}, initialized_db)
|
||||
insert_metrics(base_ts + 1800, "repeater", {"nb_recv": 200.0}, initialized_db)
|
||||
|
||||
ts = load_timeseries_from_db(
|
||||
role="repeater",
|
||||
metric="nb_recv",
|
||||
end_time=datetime.fromtimestamp(base_ts + 1800),
|
||||
lookback=timedelta(hours=2),
|
||||
period="day",
|
||||
)
|
||||
|
||||
expected_rate = (100.0 / 900.0) * 60.0
|
||||
assert len(ts.points) == 2
|
||||
assert ts.points[0].timestamp == datetime.fromtimestamp(base_ts + 900)
|
||||
assert ts.points[1].timestamp == datetime.fromtimestamp(base_ts + 1800)
|
||||
for point in ts.points:
|
||||
assert point.value == pytest.approx(expected_rate)
|
||||
|
||||
def test_applies_scale_factor(self, initialized_db, configured_env, monkeypatch):
|
||||
"""Counter rate is scaled (typically x60 for per-minute)."""
|
||||
base_ts = 1704067200
|
||||
|
||||
monkeypatch.setenv("REPEATER_STEP", "60")
|
||||
import meshmon.env
|
||||
|
||||
meshmon.env._config = None
|
||||
|
||||
# Insert values 60 seconds apart for easy math
|
||||
insert_metrics(base_ts, "repeater", {"nb_recv": 0.0}, initialized_db)
|
||||
insert_metrics(base_ts + 60, "repeater", {"nb_recv": 60.0}, initialized_db)
|
||||
|
||||
Reference in New Issue
Block a user