release: 0.5.1 #72
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
| # Aether-gate — build the container images, and smoke-test one. | |
| # | |
| # Builds both targets for amd64 + arm64 on every PR so the Dockerfile cannot | |
| # rot. Publishing is OFF by default and needs no secrets: the push step only | |
| # arms itself on the upstream repository's main branch, so a fork (where this | |
| # will also run) builds and verifies but never publishes. | |
| name: docker | |
| on: | |
| pull_request: | |
| paths: | |
| - "Dockerfile" | |
| - ".dockerignore" | |
| - "docker-compose.yml" | |
| - "aether_gate/**" | |
| - ".github/workflows/docker.yml" | |
| push: | |
| branches: [main] | |
| # Cutting a vX.Y.Z tag publishes immutable version tags alongside the | |
| # floating ones, so a deployment can pin an image instead of silently | |
| # following main. | |
| tags: ["v[0-9]+.[0-9]+.[0-9]+*"] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| # NB: deliberately NOT `github.repository` — that is `nigelfenton/Aether-gate`, | |
| # and an OCI repository name must be lowercase, so buildx rejects the tag before | |
| # it builds anything. The build job lowercases it at runtime (see "Resolve the | |
| # image name"); GHCR paths are case-insensitive on pull, so consumers are | |
| # unaffected. | |
| jobs: | |
| # The lan target is quick, so it gets built natively and actually RUN. | |
| smoke: | |
| name: build lan + smoke-test (sim adapter, no hardware) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # `load: true` needs an image in the local docker daemon, and exporting a | |
| # gha cache needs the docker-container driver — the default `docker` driver | |
| # supports neither combination ("Cache export is not supported for the | |
| # docker driver"). setup-buildx gives us the container driver, which does | |
| # both. | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Build lan image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: lan | |
| load: true | |
| tags: aether-gate:ci | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # The sim adapter needs no radio, so CI can prove the container actually | |
| # works rather than merely that it builds: it must advertise itself on the | |
| # discovery socket and answer on its control panel. | |
| - name: Smoke-test (discovery + control panel + rx-only) | |
| run: | | |
| set -eux | |
| docker run -d --name gate --network host \ | |
| -e AETHER_GATE_ADAPTER=sim \ | |
| -e AETHER_GATE_CTL_PORT=8731 \ | |
| -e AETHER_GATE_NO_UPDATE_CHECK=1 \ | |
| -e AETHER_GATE_RX_ONLY=1 \ | |
| -e AETHER_GATE_SERIAL=CISMOKE01 \ | |
| aether-gate:ci | |
| # give it a moment to bind and start advertising | |
| for i in $(seq 1 30); do | |
| curl -fsS http://127.0.0.1:8731/ >/dev/null 2>&1 && break | |
| sleep 1 | |
| done | |
| echo "--- control panel answers ---" | |
| curl -fsS http://127.0.0.1:8731/ >/dev/null | |
| echo "--- a discovery broadcast arrives on :4992 ---" | |
| timeout 10 python3 - <<'PY' | |
| import socket | |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| s.bind(("", 4992)); s.settimeout(8) | |
| import time | |
| deadline = time.time() + 8 | |
| data = b"" | |
| while time.time() < deadline: | |
| try: | |
| data, addr = s.recvfrom(2048) | |
| except socket.timeout: | |
| break | |
| if b"CISMOKE01" in data: | |
| break | |
| text = data.decode("utf-8", "replace") | |
| print("discovery:", text[:200]) | |
| # Match OUR serial specifically: any real FlexRadio on the same LAN | |
| # also advertises here, so a generic "serial=" check would pass even | |
| # if the container never started. | |
| assert "serial=CISMOKE01" in text, text | |
| PY | |
| echo "--- rx-only is reported ---" | |
| docker logs gate 2>&1 | tail -20 | |
| - name: Container stops gracefully (SIGTERM reaches PID 1) | |
| run: | | |
| set -eux | |
| # exec-form ENTRYPOINT means python is PID 1; a shell-form regression | |
| # would swallow SIGTERM and force docker to SIGKILL after the timeout. | |
| start=$(date +%s) | |
| docker stop -t 10 gate | |
| took=$(( $(date +%s) - start )) | |
| echo "stop took ${took}s" | |
| test "$took" -lt 10 | |
| test "$(docker inspect -f '{{.State.ExitCode}}' gate)" != "137" | |
| - name: Logs on failure | |
| if: failure() | |
| run: docker logs gate || true | |
| build: | |
| name: build ${{ matrix.target }} (amd64 + arm64) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [lan, full] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| # Actions expressions have no lowercase function, so do it in the shell. | |
| # `${VAR,,}` is a bash lowercase expansion; the default shell here is bash. | |
| # PUBLISH is decided once here so the login and build steps cannot drift. | |
| - name: Resolve the image name (must be lowercase for OCI) | |
| run: | | |
| echo "IMAGE=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" | |
| echo "PUBLISH=${{ github.repository_owner == 'nigelfenton' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}" >> "$GITHUB_ENV" | |
| # What each ref publishes (both targets share one image name, so a version | |
| # is always prefixed with its target -- otherwise `full` would overwrite | |
| # `lan`'s :1.2.3): | |
| # | |
| # push to main -> :lan :full floating | |
| # tag v1.2.3 -> :lan-1.2.3 :full-1.2.3 immutable, pin this | |
| # :lan-1.2 :full-1.2 moves with patches | |
| # :lan :full also refreshed | |
| # tag v1.2.3-rc1 -> :lan-1.2.3-rc1 prerelease only | |
| # | |
| # A prerelease deliberately does NOT move :lan or :lan-1.2. | |
| - name: Derive the image tags | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE }} | |
| flavor: latest=false | |
| tags: | | |
| type=raw,value=${{ matrix.target }},enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')) }} | |
| type=semver,pattern={{version}},prefix=${{ matrix.target }}- | |
| type=semver,pattern={{major}}.{{minor}},prefix=${{ matrix.target }}- | |
| # Publishing arms itself only on the upstream repo's main branch or a | |
| # version tag, so this workflow is safe to run anywhere (a fork builds and | |
| # verifies but never publishes). Nothing needs a secret until then. | |
| - name: Log in to GHCR | |
| if: env.PUBLISH == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build (and publish only from upstream main or a version tag) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: ${{ matrix.target }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: ${{ env.PUBLISH == 'true' }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=${{ matrix.target }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.target }} |