towerops-agent/.github/workflows/ci.yml
Graham McIntire a9bf0b0f8b
Switch from Alpine (musl) to Debian slim (glibc) to fix SNMP SIGSEGV
musl libc has weak fork-safety guarantees in multi-threaded processes.
When Tokio's runtime is running and we fork() for SNMP operations, musl's
internal state can be inconsistent in the child, causing SIGSEGV on every
SNMP walk to Ubiquiti devices. glibc handles this via pthread_atfork.

- Dockerfile: Alpine → rust:1.93-bookworm build + debian:12-slim runtime
- CI: Remove ensure-netsnmp-base/manifest jobs, simplify build deps
- Delete Dockerfile.netsnmp and netsnmp-base.yml workflow
- Add NULL pointer guards in snmp_helper.c for fork-safety
- Add SNMP exception type handling (NoSuchObject/Instance/EndOfMibView)
- Add crash reproduction tests
2026-02-11 08:41:06 -06:00

327 lines
10 KiB
YAML

# GitHub Actions CI/CD Configuration for Towerops Agent
# Builds and publishes Docker image to GitHub Container Registry
name: CI
on:
push:
branches:
- main
- "**"
tags:
- "v*"
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_IMAGE: gmcintire/towerops-agent
CARGO_TERM_COLOR: always
RUSTFLAGS: -C link-arg=-fuse-ld=lld
jobs:
test:
name: Test
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Mount Cargo target
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-cargo-target
path: ./target
- name: Mount Cargo registry
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-cargo-registry
path: ~/.cargo/registry
- name: Mount Cargo git
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-cargo-git
path: ~/.cargo/git
- name: Mount Rust toolchain
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-rustup
path: ~/.rustup
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.93"
components: rustfmt, clippy
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: protobuf-compiler lld libsnmp-dev
version: 1.0
- name: Format check
run: cargo fmt -- --check
- name: Check
run: cargo check --release
- name: Test
run: cargo test
# Temporarily disabled:
# - name: Clippy
# run: cargo clippy -- -D warnings
build-branch:
name: Build (Branch)
needs: [test]
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event_name == 'push' && github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/')
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract branch name
id: branch
run: |
BRANCH_NAME=${GITHUB_REF#refs/heads/}
# Sanitize branch name for Docker tag (replace / with -)
SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/\//-/g')
echo "name=$SAFE_BRANCH" >> $GITHUB_OUTPUT
- name: Build and push
uses: useblacksmith/build-push-action@v2
with:
context: .
push: true
provenance: false
build-args: |
BUILDKIT_INLINE_CACHE=1
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.branch.outputs.name }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
# Generate version info for release builds
version:
name: Version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
outputs:
version: ${{ steps.version.outputs.version }}
short_sha: ${{ steps.version.outputs.short_sha }}
is_tag: ${{ steps.version.outputs.is_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate version info
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
TAG=${GITHUB_REF#refs/tags/}
VERSION=${TAG#v}
echo "is_tag=true" >> $GITHUB_OUTPUT
else
VERSION=$(git describe --tags --always --dirty=-modified | sed 's/^v//')
echo "is_tag=false" >> $GITHUB_OUTPUT
fi
SHORT_SHA=${GITHUB_SHA::7}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
# Build architecture-specific images in parallel (native runners)
build-release:
name: Build (${{ matrix.arch }})
needs: [test, version]
runs-on: ${{ matrix.runner }}
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: blacksmith-4vcpu-ubuntu-2404
- platform: linux/arm64
arch: arm64
runner: blacksmith-4vcpu-ubuntu-2404-arm # Use Blacksmith ARM64 runners
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: gmcintire
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: ${{ matrix.platform }}
push: true
provenance: false
build-args: |
VERSION=${{ needs.version.outputs.version }}
BUILDKIT_INLINE_CACHE=1
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}-${{ matrix.arch }}
docker.io/${{ env.DOCKERHUB_IMAGE }}:${{ needs.version.outputs.version }}-${{ matrix.arch }}
- name: Extract standalone binary
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}-${{ matrix.arch }}"
CONTAINER=$(docker create $IMAGE)
docker cp $CONTAINER:/usr/local/bin/towerops-agent ./towerops-agent-linux-${{ matrix.arch }}
docker rm $CONTAINER
sha256sum towerops-agent-linux-${{ matrix.arch }} > towerops-agent-linux-${{ matrix.arch }}.sha256
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.arch }}
path: |
towerops-agent-linux-${{ matrix.arch }}
towerops-agent-linux-${{ matrix.arch }}.sha256
# Create multi-arch manifest after both builds complete
manifest:
name: Create Manifest
needs: [version, build-release]
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: gmcintire
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifests
run: |
VERSION="${{ needs.version.outputs.version }}"
SHORT_SHA="${{ needs.version.outputs.short_sha }}"
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
DH_IMAGE="docker.io/${{ env.DOCKERHUB_IMAGE }}"
# Create GHCR manifests
docker buildx imagetools create -t ${IMAGE}:${VERSION} \
${IMAGE}:${VERSION}-amd64 \
${IMAGE}:${VERSION}-arm64
docker buildx imagetools create -t ${IMAGE}:latest \
${IMAGE}:${VERSION}-amd64 \
${IMAGE}:${VERSION}-arm64
docker buildx imagetools create -t ${IMAGE}:sha-${SHORT_SHA} \
${IMAGE}:${VERSION}-amd64 \
${IMAGE}:${VERSION}-arm64
# Create Docker Hub manifests
docker buildx imagetools create -t ${DH_IMAGE}:${VERSION} \
${DH_IMAGE}:${VERSION}-amd64 \
${DH_IMAGE}:${VERSION}-arm64
docker buildx imagetools create -t ${DH_IMAGE}:latest \
${DH_IMAGE}:${VERSION}-amd64 \
${DH_IMAGE}:${VERSION}-arm64
docker buildx imagetools create -t ${DH_IMAGE}:sha-${SHORT_SHA} \
${DH_IMAGE}:${VERSION}-amd64 \
${DH_IMAGE}:${VERSION}-arm64
- name: Summary
run: |
echo "### Released version: ${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "Multi-architecture build (amd64, arm64) - built in parallel" >> $GITHUB_STEP_SUMMARY
echo "Tags pushed to GHCR and Docker Hub:" >> $GITHUB_STEP_SUMMARY
echo "- \`latest\`" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ needs.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`sha-${{ needs.version.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY
# Create GitHub Release with standalone binaries (only on tags)
release:
name: GitHub Release
needs: [version, manifest]
runs-on: blacksmith-4vcpu-ubuntu-2404
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download amd64 binary
uses: actions/download-artifact@v4
with:
name: binary-amd64
- name: Download arm64 binary
uses: actions/download-artifact@v4
with:
name: binary-arm64
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: v${{ needs.version.outputs.version }}
generate_release_notes: true
files: |
towerops-agent-linux-amd64
towerops-agent-linux-amd64.sha256
towerops-agent-linux-arm64
towerops-agent-linux-arm64.sha256
- name: Notify agents of new release
if: success()
run: |
curl -sf -X POST \
-H "Authorization: Bearer ${{ secrets.AGENT_WEBHOOK_SECRET }}" \
https://towerops.net/api/v1/webhooks/agent-release
continue-on-error: true