name: Build & publish Docker images on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] workflow_dispatch: env: REGISTRY: ghcr.io jobs: companion: name: Companion image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set image namespace run: echo "IMAGE_NAMESPACE=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV" - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU (multi-arch) uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/gluetun-companion tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - name: Build and push uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=companion cache-to: type=gha,mode=max,scope=companion sidecar: name: Sidecar image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set image namespace run: echo "IMAGE_NAMESPACE=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV" - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU (multi-arch) uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/gluetun-companion-sidecar tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./sidecar platforms: linux/amd64,linux/arm64 push: ${{ github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=sidecar cache-to: type=gha,mode=max,scope=sidecar # ── Smoke test — PRs only, reuses GHA layer cache from companion/sidecar ─ # Builds amd64-only images (fast, via cache), starts them, checks HTTP. # A failed smoke test blocks merge. smoke-test: name: Smoke test runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: [companion, sidecar] # wait for cache to be written first steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # Reuses the GHA layer cache populated by companion/sidecar jobs above. # Only amd64 is built locally (arm64 not available on ubuntu-latest runners). - name: Build Companion (amd64, cache reuse) uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64 push: false load: true tags: companion-smoke:test cache-from: type=gha,scope=companion - name: Build Sidecar (amd64, cache reuse) uses: docker/build-push-action@v5 with: context: ./sidecar platforms: linux/amd64 push: false load: true tags: sidecar-smoke:test cache-from: type=gha,scope=sidecar # ── Companion smoke test ────────────────────────────────────────────── - name: Start Companion run: | mkdir -p /tmp/companion-data docker run -d --name companion-smoke \ -e SECRET_KEY=smoke-test-$(openssl rand -hex 16) \ -e GLUETUN_CONTAINER=gluetun \ -e GLUETUN_HOST=localhost \ -e GLUETUN_PROXY_PORT=8888 \ -e COMPOSE_DIR=/compose \ -v /tmp/companion-data:/data \ -p 8765:8765 \ companion-smoke:test - name: Companion health check (HTTP 2xx/3xx within 20s) run: | for i in $(seq 1 20); do CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8765/ 2>/dev/null || echo 0) if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 400 ]; then echo "✅ Companion responded HTTP $CODE after ${i}s" exit 0 fi echo " [${i}/20] waiting (last code: $CODE)..." sleep 1 done echo "❌ Companion did not respond in 20s" docker logs companion-smoke exit 1 # ── Sidecar smoke test ─────────────────────────────────────────────── - name: Start Sidecar run: | docker run -d --name sidecar-smoke \ -p 8766:8766 \ sidecar-smoke:test - name: Sidecar health check (HTTP on /health within 20s) run: | # /health returns 200 if VPN up, 503 if not (expected in CI). # Any non-zero HTTP code means the server is running. for i in $(seq 1 20); do CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8766/health 2>/dev/null || echo 0) if [ "$CODE" != "0" ]; then echo "✅ Sidecar HTTP server is up (code: $CODE) after ${i}s" exit 0 fi echo " [${i}/20] waiting..." sleep 1 done echo "❌ Sidecar did not respond in 20s" docker logs sidecar-smoke exit 1 - name: Cleanup if: always() run: | docker stop companion-smoke sidecar-smoke 2>/dev/null || true docker rm companion-smoke sidecar-smoke 2>/dev/null || true