Docker images #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Runtime + mkcert images (nginx → uvicorn). Triggers: | |
| # - workflow_call: CI (main/testing) and release → always build, test, push (parallel jobs). | |
| # - workflow_dispatch: manual — test-only (no Hub) or full publish (parallel jobs, same as call). | |
| name: Docker images | |
| on: | |
| workflow_call: | |
| inputs: | |
| checkout_ref: | |
| description: Git ref to build (release tag); empty uses github.sha | |
| type: string | |
| required: false | |
| default: "" | |
| workflow_dispatch: | |
| inputs: | |
| git_ref: | |
| description: Branch, tag, or SHA to build | |
| type: string | |
| required: true | |
| default: main | |
| mode: | |
| description: >- | |
| build-and-test-only — amd64 build + in-container tests, no Docker Hub. | |
| publish — same as CI/release (push runtime + mkcert, multi-arch). | |
| type: choice | |
| required: true | |
| options: | |
| - build-and-test-only | |
| - publish | |
| default: build-and-test-only | |
| permissions: | |
| contents: read | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| # Sequential local test (manual); no registry login or push. | |
| docker-manual: | |
| if: github.event_name == 'workflow_dispatch' && inputs.mode == 'build-and-test-only' | |
| name: Docker — build and test only (manual) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.git_ref }} | |
| - name: Validate pyproject version vs tag ref | |
| id: version | |
| env: | |
| GIT_REF: ${{ inputs.git_ref }} | |
| run: | | |
| python3 <<'PY' | |
| import os | |
| import pathlib | |
| import tomllib | |
| git_ref = os.environ["GIT_REF"].strip() | |
| version = tomllib.loads( | |
| pathlib.Path("pyproject.toml").read_text(encoding="utf-8") | |
| )["project"]["version"] | |
| expected_tag = f"v{version}" | |
| if git_ref.startswith("v") and git_ref != expected_tag: | |
| raise SystemExit( | |
| f"git_ref/version mismatch: ref={git_ref!r}, expected {expected_tag!r}" | |
| ) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: | |
| fh.write(f"tag={version}\n") | |
| PY | |
| - name: Keep image tag in env | |
| run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV | |
| - name: Create Docker test script | |
| run: | | |
| cat <<'EOF' > /tmp/docker-tests.sh | |
| #!/bin/sh | |
| set -eux | |
| # tests/ is bind-mounted read-only; keep bytecode out of the tree | |
| export PYTHONPYCACHEPREFIX=/tmp/pycache | |
| mkdir -p "$PYTHONPYCACHEPREFIX" | |
| export PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 | |
| uv sync --frozen --extra test --group dev | |
| uv run ruff check . | |
| uv run ruff format --check . | |
| uv run python -m compileall proxbox_api scripts tests | |
| uv run python -c "import proxbox_api.main" | |
| uv run python -c "from proxbox_api.proxmox_to_netbox.proxmox_schema import load_proxmox_generated_openapi; assert load_proxmox_generated_openapi().get('paths')" | |
| uv run pytest --cov=proxbox_api --cov-report=term-missing --cov-report=xml tests --ignore=tests/e2e | |
| EOF | |
| chmod +x /tmp/docker-tests.sh | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver: docker-container | |
| - name: Build amd64 image for in-container tests | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: runtime | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-pre | |
| cache-from: type=gha,scope=runtime | |
| cache-to: type=gha,mode=max,scope=runtime | |
| - name: Run tests in local image | |
| run: | | |
| docker run --rm \ | |
| -v /tmp/docker-tests.sh:/tmp/docker-tests.sh \ | |
| -v "${{ github.workspace }}/tests:/app/tests:ro" \ | |
| emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-pre \ | |
| /tmp/docker-tests.sh | |
| - name: Smoke-test runtime image startup | |
| run: | | |
| set -eux | |
| docker rm -f proxbox-smoke-http 2>/dev/null || true | |
| docker run -d --name proxbox-smoke-http \ | |
| -p 127.0.0.1:18080:8000 \ | |
| -e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \ | |
| emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-pre | |
| for i in $(seq 1 40); do | |
| if curl -fsS --max-time 5 http://127.0.0.1:18080/ >/dev/null 2>&1; then | |
| docker rm -f proxbox-smoke-http | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| docker logs proxbox-smoke-http | |
| docker rm -f proxbox-smoke-http | |
| exit 1 | |
| - name: Build amd64 mkcert image for in-container tests | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: mkcert | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert-pre | |
| cache-from: type=gha,scope=mkcert | |
| cache-to: type=gha,mode=max,scope=mkcert | |
| - name: Run tests in mkcert local image | |
| run: | | |
| docker run --rm \ | |
| -v /tmp/docker-tests.sh:/tmp/docker-tests.sh \ | |
| -v "${{ github.workspace }}/tests:/app/tests:ro" \ | |
| emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert-pre \ | |
| /tmp/docker-tests.sh | |
| - name: Smoke-test mkcert image startup | |
| run: | | |
| set -eux | |
| docker rm -f proxbox-smoke-https 2>/dev/null || true | |
| docker run -d --name proxbox-smoke-https \ | |
| -p 127.0.0.1:18443:8000 \ | |
| -e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \ | |
| emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert-pre | |
| for i in $(seq 1 40); do | |
| if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then | |
| docker rm -f proxbox-smoke-https | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| docker logs proxbox-smoke-https | |
| docker rm -f proxbox-smoke-https | |
| exit 1 | |
| docker-runtime: | |
| if: github.event_name == 'workflow_call' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.mode == 'publish') | |
| name: Docker Hub — runtime (nginx + uvicorn) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || github.event_name == 'workflow_call' && (inputs.checkout_ref != '' && inputs.checkout_ref || github.sha) || github.sha }} | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| # Required for cache-to: gha together with load: true (docker driver cannot export cache). | |
| driver: docker-container | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Resolve image version from pyproject | |
| id: version | |
| env: | |
| GIT_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || github.event_name == 'workflow_call' && (inputs.checkout_ref != '' && inputs.checkout_ref || github.ref_name) || github.ref_name }} | |
| run: | | |
| python3 <<'PY' | |
| import os | |
| import pathlib | |
| import tomllib | |
| data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) | |
| version = data["project"]["version"] | |
| expected_tag = f"v{version}" | |
| git_ref = os.environ["GIT_REF"].strip() | |
| if git_ref.startswith("v") and git_ref != expected_tag: | |
| raise SystemExit( | |
| f"git ref / pyproject mismatch: ref={git_ref!r}, expected {expected_tag!r}" | |
| ) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: | |
| fh.write(f"tag={version}\n") | |
| PY | |
| - name: Keep image tag in env | |
| run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV | |
| - name: Create Docker test script | |
| run: | | |
| cat <<'EOF' > /tmp/docker-tests.sh | |
| #!/bin/sh | |
| set -eux | |
| # tests/ is bind-mounted read-only; keep bytecode out of the tree | |
| export PYTHONPYCACHEPREFIX=/tmp/pycache | |
| mkdir -p "$PYTHONPYCACHEPREFIX" | |
| export PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 | |
| uv sync --frozen --extra test --group dev | |
| uv run ruff check . | |
| uv run ruff format --check . | |
| uv run python -m compileall proxbox_api scripts tests | |
| uv run python -c "import proxbox_api.main" | |
| uv run python -c "from proxbox_api.proxmox_to_netbox.proxmox_schema import load_proxmox_generated_openapi; assert load_proxmox_generated_openapi().get('paths')" | |
| uv run pytest --cov=proxbox_api --cov-report=term-missing --cov-report=xml tests --ignore=tests/e2e | |
| EOF | |
| chmod +x /tmp/docker-tests.sh | |
| - name: Build amd64 image for pre-release tests | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: runtime | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-pre | |
| cache-from: type=gha,scope=runtime | |
| cache-to: type=gha,mode=max,scope=runtime | |
| - name: Extract metadata (labels, etc.) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: emersonfelipesp/proxbox-api | |
| tags: | | |
| type=raw,value=${{ steps.version.outputs.tag }} | |
| type=raw,value=latest | |
| type=ref,event=pr | |
| type=sha | |
| - name: Build and push Docker image (runtime) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: runtime | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=runtime | |
| cache-to: type=gha,mode=max,scope=runtime | |
| - name: Smoke-test pushed runtime images | |
| run: | | |
| set -eux | |
| for TAG in "${{ env.IMAGE_TAG }}" latest; do | |
| NAME=$(printf 'proxbox-smoke-%s' "$TAG" | tr '.:' '-') | |
| docker rm -f "$NAME" 2>/dev/null || true | |
| docker run -d --name "$NAME" \ | |
| -p 127.0.0.1:18080:8000 \ | |
| -e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \ | |
| emersonfelipesp/proxbox-api:"$TAG" | |
| ok=0 | |
| for i in $(seq 1 40); do | |
| if curl -fsS --max-time 5 http://127.0.0.1:18080/ >/dev/null 2>&1; then | |
| ok=1 | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if [ "$ok" -ne 1 ]; then | |
| docker logs "$NAME" | |
| docker rm -f "$NAME" | |
| exit 1 | |
| fi | |
| docker rm -f "$NAME" | |
| done | |
| docker-mkcert: | |
| if: github.event_name == 'workflow_call' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.mode == 'publish') | |
| name: Docker Hub — mkcert (nginx + HTTPS + uvicorn) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || github.event_name == 'workflow_call' && (inputs.checkout_ref != '' && inputs.checkout_ref || github.sha) || github.sha }} | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| # Required for cache-to: gha together with load: true (docker driver cannot export cache). | |
| driver: docker-container | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Resolve image version from pyproject | |
| id: version | |
| env: | |
| GIT_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || github.event_name == 'workflow_call' && (inputs.checkout_ref != '' && inputs.checkout_ref || github.ref_name) || github.ref_name }} | |
| run: | | |
| python3 <<'PY' | |
| import os | |
| import pathlib | |
| import tomllib | |
| data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) | |
| version = data["project"]["version"] | |
| expected_tag = f"v{version}" | |
| git_ref = os.environ["GIT_REF"].strip() | |
| if git_ref.startswith("v") and git_ref != expected_tag: | |
| raise SystemExit( | |
| f"git ref / pyproject mismatch: ref={git_ref!r}, expected {expected_tag!r}" | |
| ) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: | |
| fh.write(f"tag={version}\n") | |
| PY | |
| - name: Keep image tag in env | |
| run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV | |
| - name: Create Docker test script | |
| run: | | |
| cat <<'EOF' > /tmp/docker-tests.sh | |
| #!/bin/sh | |
| set -eux | |
| # tests/ is bind-mounted read-only; keep bytecode out of the tree | |
| export PYTHONPYCACHEPREFIX=/tmp/pycache | |
| mkdir -p "$PYTHONPYCACHEPREFIX" | |
| export PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 | |
| uv sync --frozen --extra test --group dev | |
| uv run ruff check . | |
| uv run ruff format --check . | |
| uv run python -m compileall proxbox_api scripts tests | |
| uv run python -c "import proxbox_api.main" | |
| uv run python -c "from proxbox_api.proxmox_to_netbox.proxmox_schema import load_proxmox_generated_openapi; assert load_proxmox_generated_openapi().get('paths')" | |
| uv run pytest --cov=proxbox_api --cov-report=term-missing --cov-report=xml tests --ignore=tests/e2e | |
| EOF | |
| chmod +x /tmp/docker-tests.sh | |
| - name: Build amd64 mkcert image for pre-release tests | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: mkcert | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert-pre | |
| cache-from: type=gha,scope=mkcert | |
| cache-to: type=gha,mode=max,scope=mkcert | |
| - name: Smoke-test mkcert image startup | |
| run: | | |
| set -eux | |
| docker rm -f proxbox-smoke-https 2>/dev/null || true | |
| docker run -d --name proxbox-smoke-https \ | |
| -p 127.0.0.1:18443:8000 \ | |
| -e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \ | |
| emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert-pre | |
| for i in $(seq 1 40); do | |
| if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then | |
| docker rm -f proxbox-smoke-https | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| docker logs proxbox-smoke-https | |
| docker rm -f proxbox-smoke-https | |
| exit 1 | |
| - name: Build and push mkcert Docker image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: mkcert | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-mkcert,emersonfelipesp/proxbox-api:latest-mkcert | |
| cache-from: type=gha,scope=mkcert | |
| cache-to: type=gha,mode=max,scope=mkcert | |
| - name: Smoke-test pushed mkcert images | |
| run: | | |
| set -eux | |
| for TAG in "${{ env.IMAGE_TAG }}-mkcert" latest-mkcert; do | |
| NAME=$(printf 'proxbox-smoke-%s' "$TAG" | tr '.:' '-') | |
| docker rm -f "$NAME" 2>/dev/null || true | |
| docker run -d --name "$NAME" \ | |
| -p 127.0.0.1:18443:8000 \ | |
| -e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \ | |
| emersonfelipesp/proxbox-api:"$TAG" | |
| ok=0 | |
| for i in $(seq 1 40); do | |
| if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then | |
| ok=1 | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if [ "$ok" -ne 1 ]; then | |
| docker logs "$NAME" | |
| docker rm -f "$NAME" | |
| exit 1 | |
| fi | |
| docker rm -f "$NAME" | |
| done |