name: Production Deployment on: push: branches: - main # Image hosted on Codeberg's container registry. The shared # REGISTRY_USER / REGISTRY_PASSWORD secrets carry Codeberg credentials; # login URL is the hardcoded env.REGISTRY rather than secrets.REGISTRY_URL # so a stale URL secret can't push to the wrong registry. env: REGISTRY: codeberg.org IMAGE_NAME: gmcintire/towerops jobs: test-exunit: name: Run ExUnit Tests runs-on: ubuntu-22.04 env: MIX_ENV: test DATABASE_URL: ecto://postgres:postgres@postgres/towerops_test services: postgres: image: timescale/timescaledb-ha:pg17-all env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: towerops_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: https://github.com/actions/checkout@v4 - name: Set up Elixir uses: https://github.com/erlef/setup-beam@v1 with: version-type: strict elixir-version: '1.18.1' otp-version: '27.2' - name: Cache Mix uses: https://github.com/actions/cache@v4 with: path: ~/.mix key: ${{ runner.os }}-mix-1.18.1-27.2 restore-keys: ${{ runner.os }}-mix- - name: Cache deps uses: https://github.com/actions/cache@v4 with: path: deps key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} restore-keys: ${{ runner.os }}-deps- - name: Cache _build uses: https://github.com/actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-v3-${{ hashFiles('lib/**/*.ex') }}-${{ hashFiles('mix.lock') }} restore-keys: ${{ runner.os }}-build-v3- - name: Install system dependencies run: | . /etc/os-release if [ "$ID" = "debian" ]; then if [ -f /etc/apt/sources.list ] && grep -q "^deb " /etc/apt/sources.list; then sed -i 's/ main$/ main contrib non-free non-free-firmware/' /etc/apt/sources.list fi for f in /etc/apt/sources.list.d/*.sources; do [ -f "$f" ] || continue sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' "$f" done fi apt-get update apt-get install -y libssl-dev libsnmp-dev snmp-mibs-downloader postgresql-client - name: Install dependencies run: mix deps.get - name: Compile C NIF run: make -C c_src - name: Compile (warnings as errors) run: mix compile --warnings-as-errors - name: Run tests run: mix test build-and-push: name: Build and Push Docker Image runs-on: ubuntu-22.04 needs: [test-exunit] steps: - name: Checkout code uses: https://github.com/actions/checkout@v4 - name: Generate image tag id: tag run: | BRANCH=${GITHUB_REF#refs/heads/} TIMESTAMP=$(date +%s) SHORT_SHA=$(git rev-parse --short=7 HEAD | cut -c1-7) TAG="${BRANCH}-${TIMESTAMP}-${SHORT_SHA}" echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "Full image tag: ${TAG}" # Static docker + buildx download. The runner image's bookworm # apt repos have broken GPG signatures, so we bypass apt entirely. # buildx is needed because k8s/Dockerfile uses `# syntax=` and # pulls FROM the prebuilt towerops-base — buildx authenticates # against Codeberg via the docker login below. - name: Install Docker CLI and buildx run: | curl -fsSL -o /tmp/docker.tgz \ https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz tar xzf /tmp/docker.tgz -C /tmp install -m 0755 /tmp/docker/docker /usr/local/bin/docker rm -rf /tmp/docker /tmp/docker.tgz mkdir -p /usr/libexec/docker/cli-plugins curl -fsSL -o /usr/libexec/docker/cli-plugins/docker-buildx \ https://github.com/docker/buildx/releases/download/v0.19.3/buildx-v0.19.3.linux-amd64 chmod +x /usr/libexec/docker/cli-plugins/docker-buildx docker --version docker buildx version - name: Wait for Docker daemon run: | for i in $(seq 1 30); do if docker info >/dev/null 2>&1; then echo "Docker daemon reachable after ${i}s" exit 0 fi echo "Waiting for Docker daemon... ($i/30)" sleep 1 done echo "Docker daemon never became reachable" >&2 exit 1 # Login BEFORE the build so buildx can pull the private base image. - name: Log in to Codeberg container registry run: | attempt=1 max_attempts=3 while : ; do if echo "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login "${{ env.REGISTRY }}" \ -u "${{ secrets.REGISTRY_USER }}" \ --password-stdin; then exit 0 fi if [ "$attempt" -ge "$max_attempts" ]; then echo "docker login failed after $attempt attempts" >&2 exit 1 fi delay=$((2 ** attempt)) echo "docker login attempt $attempt failed; retrying in ${delay}s" sleep "$delay" attempt=$((attempt + 1)) done - name: Build and push Docker image run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" TAG="${{ steps.tag.outputs.tag }}" attempt=1 max_attempts=3 while : ; do if docker buildx build \ --build-arg MIX_ENV=prod \ --file k8s/Dockerfile \ -t "${IMAGE}:${TAG}" \ -t "${IMAGE}:production" \ --push \ .; then exit 0 fi if [ "$attempt" -ge "$max_attempts" ]; then echo "docker buildx build failed after $attempt attempts" >&2 exit 1 fi delay=$((2 ** attempt)) echo "docker buildx build attempt $attempt failed; retrying in ${delay}s" sleep "$delay" attempt=$((attempt + 1)) done - name: Deployment summary run: | echo "### ✅ Image Built and Pushed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "FluxCD will automatically detect and deploy the new image." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Monitor deployment:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "kubectl rollout status deployment/towerops -n towerops" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY