Merge pull request #772 from openmoq/sync-moxygen/3fb94e4 #534
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
| name: ci main | |
| # Full pipeline on push to main: format, build/test, publish artifacts + Docker, | |
| # release snapshot, notify. PRs use ci-pr.yml (format + build/test only). | |
| # | |
| # Job graph: | |
| # | |
| # check-format ──────────────────────────────────────────────────────────────────┐ | |
| # build (linux, asan debug) ── publish (docker+smoke) ── release ────────────────┼── notify | |
| # microbenchmark (linux, macos) ────────────────────────────────────────────────-┤ | |
| # └── prune-caches | |
| on: | |
| push: | |
| branches: [main, release/**] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| # Cancel superseded runs on release/** branches (rapid-iteration churn). | |
| # Keep main's serialize-don't-cancel behavior so external-state steps | |
| # (Docker push, release create, deploy) aren't interrupted mid-flight. | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| permissions: | |
| contents: write | |
| checks: write | |
| packages: write | |
| jobs: | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Verify: format + build/test matrix | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| check-format: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Install clang-format | |
| run: pip install clang-format==19.1.7 | |
| # format.sh pins the ruff version; uv is what fetches it. | |
| - uses: astral-sh/setup-uv@v6 | |
| - name: Check formatting | |
| run: bash scripts/dev/format.sh --check | |
| build: | |
| needs: [check-format] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # `key` is the machine-readable lane id (ccache/cache keys, option | |
| # gating); `name` is display-only, safe to rename. The build dir is | |
| # always build/<preset> (the presets' binaryDir). | |
| include: | |
| - name: linux | |
| key: linux | |
| preset: default | |
| runner: ubuntu-22.04 | |
| # arm64 is validated by the publish-side docker build (publish arm64 | |
| # entry below); skipped here to avoid the ubuntu-runner ↔ bookworm-tarball | |
| # glog ABI mismatch. arm64 issues surface at the publish step. | |
| - name: macos | |
| key: macos | |
| preset: default | |
| runner: macos-15 | |
| # ASan on moqx's own TUs, over the uninstrumented prebuilt folly, for a | |
| # fast signal. Full-stack instrumentation needs a prebuilt instrumented | |
| # moxygen: openmoq/moqx#579. | |
| # RelWithDebInfo: the prebuilt is NDEBUG and folly's kIsDebug is ABI. | |
| - name: asan debug | |
| key: asan | |
| preset: san | |
| extra_cmake: -DCMAKE_BUILD_TYPE=RelWithDebInfo | |
| # The san preset demands an instrumented moxygen; this lane knowingly | |
| # takes the uninstrumented one. Without it configure.sh refuses. | |
| uninstrumented_deps: true | |
| leak_check: true | |
| runner: [self-hosted, build] | |
| # Sized by memory, not cores. See ci-pr.yml for the measurements. | |
| build_jobs: 10 | |
| name: ${{ matrix.name }} | |
| runs-on: ${{ matrix.runner }} | |
| # Ceiling for the from-source fallback, not the normal runtime. | |
| timeout-minutes: 180 | |
| env: | |
| # Empty falls through to build.sh's own default (scripts/lib/jobs.sh). | |
| MOQX_BUILD_JOBS: ${{ matrix.build_jobs || '' }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: ./.github/actions/setup-build | |
| with: | |
| ccache-key: ${{ matrix.key }} | |
| # The trilogy, not raw cmake: configure.sh picks the moxygen and build.sh | |
| # resolves a job count, where Ninja's own nproc + 2 default OOMs the | |
| # sanitizer lane. See BUILD.md#build. | |
| - name: Build | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| MOQX_ALLOW_UNINSTRUMENTED_DEPS: ${{ matrix.uninstrumented_deps && '1' || '' }} | |
| run: | | |
| scripts/configure.sh ${{ matrix.preset }} --moxygen prebuilt-with-fallback ${{ matrix.extra_cmake }} | |
| scripts/build.sh ${{ matrix.preset }} | |
| # test.sh, not raw ctest: it resolves the --parallel the suite needs (the | |
| # shell integration tests carry unique ports so they can share a run). | |
| # Hit rate is the only way to tell a warm cache from a restored one. | |
| - name: ccache stats | |
| if: always() | |
| run: ccache -s | |
| - name: Test | |
| env: | |
| ASAN_OPTIONS: ${{ matrix.leak_check && 'detect_leaks=1:abort_on_error=1' || '' }} | |
| run: scripts/test.sh ${{ matrix.preset }} --output-junit test-results.xml | |
| - name: Publish test results | |
| uses: dorny/test-reporter@v3 | |
| if: success() || failure() | |
| with: | |
| name: "test (${{ matrix.name }})" | |
| path: build/${{ matrix.preset }}/test-results.xml | |
| reporter: java-junit | |
| fail-on-empty: ${{ job.status == 'success' && 'true' || 'false' }} | |
| # Only list failing tests in the Check Run summary. Listing all | |
| # passing tests too blows GitHub's 64 KiB output-body cap on large | |
| # suites; the per-suite pass/fail counts at the top still render. | |
| list-tests: failed | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Conformance: {mvfst, pico} × {d16, d18} × {Q, WT} | |
| # (mirrors ci-pr.yml) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| conformance: | |
| needs: [check-format] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: mvfst d16 Q | |
| versions: "16" | |
| transport: "Q" | |
| stack: "mvfst" | |
| - name: mvfst d16 WT | |
| versions: "16" | |
| transport: "" | |
| stack: "mvfst" | |
| - name: mvfst d18 Q | |
| versions: "18" | |
| transport: "Q" | |
| stack: "mvfst" | |
| - name: mvfst d18 WT | |
| versions: "18" | |
| transport: "" | |
| stack: "mvfst" | |
| - name: pico d16 Q | |
| versions: "16" | |
| transport: "Q" | |
| stack: "pico" | |
| - name: pico d16 WT | |
| versions: "16" | |
| transport: "" | |
| stack: "pico" | |
| - name: pico d18 Q | |
| versions: "18" | |
| transport: "Q" | |
| stack: "pico" | |
| - name: pico d18 WT | |
| versions: "18" | |
| transport: "" | |
| stack: "pico" | |
| name: conformance (${{ matrix.name }}) | |
| runs-on: ubuntu-22.04 | |
| # Ceiling for the from-source fallback; see the build job. | |
| timeout-minutes: 180 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: ./.github/actions/setup-build | |
| with: | |
| ccache-key: conformance | |
| # Tests off: this job only drives the moqx binary; the ~25 gtest | |
| # executables would link the heavy static stack for nothing. | |
| - name: Build | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| scripts/configure.sh default --moxygen prebuilt-with-fallback -DMOQX_BUILD_TESTS=OFF | |
| scripts/build.sh default | |
| # Hit rate is the only way to tell a warm cache from a restored one. | |
| - name: ccache stats | |
| if: always() | |
| run: ccache -s | |
| - name: Run conformance tests | |
| env: | |
| SKIP_FETCH: 1 | |
| run: bash test/test_conformance.sh ./build/default/moqx ${{ matrix.versions }} ${{ matrix.transport }} ${{ matrix.stack }} | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Microbenchmark: run in-process micro-benchmarks (independent of build/publish pipeline) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| microbenchmark: | |
| needs: [check-format] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: linux | |
| runner: ubuntu-22.04 | |
| - name: macos | |
| # Pinned to the moxygen publish runner: releases ship | |
| # moxygen-macos-15-arm64.tar.gz and no macos-26 one, so macos-latest | |
| # 404s. Keep in lockstep with the build job above. | |
| runner: macos-15 | |
| name: microbenchmark (${{ matrix.name }}) | |
| runs-on: ${{ matrix.runner }} | |
| # Ceiling for the from-source fallback; see the build job. | |
| timeout-minutes: 180 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: ./.github/actions/setup-build | |
| with: | |
| ccache-key: microbench | |
| - name: Build microbenchmarks | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| scripts/configure.sh default --moxygen prebuilt-with-fallback \ | |
| -DMOQX_BUILD_BENCHMARKS=ON -DMOQX_BUILD_TESTS=OFF | |
| scripts/build.sh default | |
| - name: Run microbenchmarks | |
| run: | | |
| ./build/default/benchmark/moqx_benchmark \ | |
| --bm_json_verbose=microbench-results.json \ | |
| | tee microbench-output.txt | |
| - name: Render summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Microbenchmark results — ${{ matrix.name }}" | |
| echo "" | |
| echo '```' | |
| cat microbench-output.txt | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload microbenchmark artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: microbench-results-${{ matrix.name }} | |
| path: | | |
| microbench-results.json | |
| microbench-output.txt | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Publish: build moqx, Docker image + smoke test, push | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| publish: | |
| needs: [check-format, build] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-22.04 | |
| platform: bookworm-amd64 | |
| - arch: arm64 | |
| runner: ubuntu-22.04-arm | |
| platform: bookworm-arm64 | |
| name: publish (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.runner }} | |
| # The image's moxygen stage can fall back to compiling folly; see the | |
| # build job. Without this the default is 6 h. | |
| timeout-minutes: 180 | |
| steps: | |
| - name: Generate app token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.OMOQ_APP_ID }} | |
| private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} | |
| - uses: actions/checkout@v5 | |
| with: | |
| # Full history + tags so `git describe --tags` can version the | |
| # image (bare tag on release commits, tag-distance-sha otherwise). | |
| fetch-depth: 0 | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Compute image tags | |
| id: tags | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| BRANCH="${{ github.ref_name }}" | |
| if [[ "$BRANCH" == "main" ]]; then | |
| LABEL="main" | |
| else | |
| LABEL="${BRANCH#release/}" | |
| fi | |
| echo "short=$SHORT" >> "$GITHUB_OUTPUT" | |
| echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" | |
| - name: Resolve build version | |
| id: version | |
| run: | | |
| # Baked into the binary and emitted as an OCI label. Matching only | |
| # v-prefixed tags is required: the repo carries moving tags that are | |
| # not versions, and an unfiltered describe returns one of those. | |
| V="$(git describe --tags --match 'v[0-9]*' --always)" | |
| echo "version=$V" >> "$GITHUB_OUTPUT" | |
| echo "==> build version: $V" | |
| # The docker-container driver is what makes --cache-to work at all; the | |
| # default driver has no cache exporter. | |
| - uses: docker/setup-buildx-action@v4 | |
| # Caches the pin-keyed moxygen stage (docker/Dockerfile), so a source-only | |
| # push reuses the prefix. Registry, not type=gha: that backend shares one | |
| # 10 GB budget with the ccache and dependency caches, and mode=max evicts them. | |
| - name: Build Docker image | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| ARCH="${{ matrix.arch }}" | |
| docker buildx build -f docker/Dockerfile --target relay --load \ | |
| --secret id=github_token,env=GITHUB_TOKEN \ | |
| --build-arg MOQX_VERSION_STRING="${{ steps.version.outputs.version }}" \ | |
| --cache-from "type=registry,ref=${IMAGE}/buildcache:${ARCH}" \ | |
| --cache-to "type=registry,ref=${IMAGE}/buildcache:${ARCH},mode=max" \ | |
| -t "${IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ | |
| -t "${IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ | |
| . | |
| - name: Smoke test Docker image | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}:${{ steps.tags.outputs.rolling }}-${{ matrix.arch }}" | |
| echo "==> ldd check" | |
| docker run --rm --entrypoint ldd "${IMAGE}" /usr/local/bin/moqx | |
| if docker run --rm --entrypoint ldd "${IMAGE}" /usr/local/bin/moqx 2>&1 | grep -q "not found"; then | |
| echo "ERROR: missing shared libraries"; exit 1 | |
| fi | |
| echo "==> Start container with test config" | |
| docker run -d --name moqx-smoke --network host \ | |
| -v "$PWD/test/test.config.yaml:/etc/moqx/config.yaml:ro" \ | |
| "${IMAGE}" --config=/etc/moqx/config.yaml | |
| echo "==> Wait for admin /info" | |
| for i in $(seq 1 50); do | |
| if curl -sf http://[::1]:9669/info >/dev/null 2>&1; then break; fi | |
| sleep 0.1 | |
| if [ "$i" -eq 50 ]; then | |
| echo "ERROR: admin server did not start"; docker logs moqx-smoke; exit 1 | |
| fi | |
| done | |
| RESP=$(curl -sf http://[::1]:9669/info) | |
| echo "Response: $RESP" | |
| echo "$RESP" | grep -q '"service":"moqx"' || { echo "FAIL: bad /info response"; exit 1; } | |
| docker stop moqx-smoke && docker rm moqx-smoke | |
| echo "==> Smoke test passed" | |
| # Same Dockerfile, same moxygen stage — so the client binary and the relay | |
| # can never come from different moxygen builds. No --cache-to: the relay | |
| # build above already exported that stage. | |
| - name: Build interop client image | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| CLIENT_IMAGE="ghcr.io/${{ github.repository_owner }}/moqx-interop-client" | |
| ARCH="${{ matrix.arch }}" | |
| docker buildx build -f docker/Dockerfile --target interop-client --load \ | |
| --secret id=github_token,env=GITHUB_TOKEN \ | |
| --cache-from "type=registry,ref=${IMAGE}/buildcache:${ARCH}" \ | |
| -t "${CLIENT_IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ | |
| -t "${CLIENT_IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ | |
| . | |
| - name: Smoke test interop client | |
| run: | | |
| CLIENT_IMAGE="ghcr.io/${{ github.repository_owner }}/moqx-interop-client:${{ steps.tags.outputs.rolling }}-${{ matrix.arch }}" | |
| echo "==> ldd check" | |
| docker run --rm --entrypoint ldd "${CLIENT_IMAGE}" /usr/local/bin/moq_interop_client | |
| if docker run --rm --entrypoint ldd "${CLIENT_IMAGE}" /usr/local/bin/moq_interop_client 2>&1 | grep -q "not found"; then | |
| echo "ERROR: missing shared libraries"; exit 1 | |
| fi | |
| echo "==> List supported tests" | |
| docker run --rm "${CLIENT_IMAGE}" --list | |
| echo "==> Interop client smoke test passed" | |
| - name: Push Docker images | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| CLIENT_IMAGE="ghcr.io/${{ github.repository_owner }}/moqx-interop-client" | |
| ARCH="${{ matrix.arch }}" | |
| docker push "${IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" | |
| docker push "${IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" | |
| docker push "${CLIENT_IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" | |
| docker push "${CLIENT_IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" | |
| - name: Package | |
| id: package | |
| run: | | |
| ARTIFACT="moqx-${{ matrix.platform }}.tar.gz" | |
| docker cp "$(docker create --name extract ghcr.io/${{ github.repository }}:${{ steps.tags.outputs.rolling }}-${{ matrix.arch }}):/usr/local/bin/moqx" . | |
| docker rm extract | |
| mkdir -p install/bin && mv moqx install/bin/ | |
| tar czf "$ARTIFACT" -C install . | |
| echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" | |
| # Handoff to the release job within this run; the release then holds the | |
| # same tarball on free storage. The week is re-run headroom: a failed | |
| # release job can be retried without rebuilding, over a weekend included. | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ${{ steps.package.outputs.artifact }} | |
| path: ${{ steps.package.outputs.artifact }} | |
| retention-days: 7 | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Manifest: stitch per-arch images into multiplatform tags | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| manifest: | |
| needs: [publish] | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Compute image tags | |
| id: tags | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| BRANCH="${{ github.ref_name }}" | |
| # Every branch gets a rolling <label>-latest tag. main additionally | |
| # keeps the bare 'latest' for back-compat with anyone pulling it. | |
| if [[ "$BRANCH" == "main" ]]; then | |
| LABEL="main" | |
| ALIAS="latest" # back-compat: main is also ':latest' | |
| else | |
| LABEL="${BRANCH#release/}" | |
| ALIAS="" | |
| fi | |
| echo "short=$SHORT" >> "$GITHUB_OUTPUT" | |
| echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" | |
| echo "alias=$ALIAS" >> "$GITHUB_OUTPUT" | |
| - name: Create multiplatform manifests (moqx) | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| SHORT="${{ steps.tags.outputs.short }}" | |
| ROLLING="${{ steps.tags.outputs.rolling }}" | |
| for TAG in "$SHORT" "$ROLLING"; do | |
| docker buildx imagetools create -t "${IMAGE}:${TAG}" \ | |
| "${IMAGE}:${TAG}-amd64" \ | |
| "${IMAGE}:${TAG}-arm64" | |
| done | |
| ALIAS="${{ steps.tags.outputs.alias }}" | |
| if [[ -n "$ALIAS" ]]; then | |
| docker buildx imagetools create -t "${IMAGE}:${ALIAS}" \ | |
| "${IMAGE}:${SHORT}-amd64" \ | |
| "${IMAGE}:${SHORT}-arm64" | |
| fi | |
| - name: Create multiplatform manifests (interop-client) | |
| run: | | |
| CLIENT_IMAGE="ghcr.io/${{ github.repository_owner }}/moqx-interop-client" | |
| SHORT="${{ steps.tags.outputs.short }}" | |
| ROLLING="${{ steps.tags.outputs.rolling }}" | |
| for TAG in "$SHORT" "$ROLLING"; do | |
| docker buildx imagetools create -t "${CLIENT_IMAGE}:${TAG}" \ | |
| "${CLIENT_IMAGE}:${TAG}-amd64" \ | |
| "${CLIENT_IMAGE}:${TAG}-arm64" | |
| done | |
| ALIAS="${{ steps.tags.outputs.alias }}" | |
| if [[ -n "$ALIAS" ]]; then | |
| docker buildx imagetools create -t "${CLIENT_IMAGE}:${ALIAS}" \ | |
| "${CLIENT_IMAGE}:${SHORT}-amd64" \ | |
| "${CLIENT_IMAGE}:${SHORT}-arm64" | |
| fi | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Release: create/update snapshot-latest pre-release (all green) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| release: | |
| needs: [build, manifest] | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| tag: ${{ steps.publish.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: "*.tar.gz" | |
| path: artifacts/ | |
| - name: Publish snapshot | |
| id: publish | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| BRANCH="${{ github.ref_name }}" | |
| if [[ "$BRANCH" == "main" ]]; then | |
| LABEL="main" | |
| TAG="snapshot-latest" | |
| else | |
| # release/nab-demo → nab-demo → snapshot-nab-demo-latest | |
| LABEL="${BRANCH#release/}" | |
| TAG="snapshot-${LABEL}-latest" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| gh release delete "$TAG" --yes 2>/dev/null || true | |
| git tag -d "$TAG" 2>/dev/null || true | |
| git push origin ":refs/tags/$TAG" 2>/dev/null || true | |
| gh release create "$TAG" artifacts/**/* \ | |
| --target "$GITHUB_SHA" \ | |
| --title "Latest build — ${LABEL} (${SHORT})" \ | |
| --prerelease \ | |
| --notes "$(cat <<EOF | |
| Rolling snapshot of the latest build from \`${{ github.ref_name }}\`. | |
| **Commit:** \`${GITHUB_SHA}\` | |
| **Built:** $(date -u +%Y-%m-%dT%H:%M:%SZ) | |
| **Docker:** \`ghcr.io/${{ github.repository }}:${SHORT}\` | |
| This pre-release is automatically replaced on every push to \`${{ github.ref_name }}\`. | |
| EOF | |
| )" | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Deploy: auto-deploy main to moqx-main.ci.openmoq.org | |
| # (release branches are deployed manually via deploy-relay.yml with their | |
| # own branch-derived domain) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| deploy: | |
| if: github.ref_name == 'main' | |
| needs: [manifest, release] | |
| runs-on: [self-hosted, relay] | |
| env: | |
| DOMAIN: moqx-main.ci.openmoq.org | |
| RELAY_PORT: 4433 | |
| ADMIN_PORT: 8000 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Ensure DNS A record | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.OMOQ_CERTBOT_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.OMOQ_CERTBOT_SECRET_ACCESS_KEY }} | |
| AWS_DEFAULT_REGION: us-east-1 | |
| HOSTED_ZONE_ID: Z0079758316CI99B99FLR | |
| run: | | |
| IP=$(curl -sf https://checkip.amazonaws.com | tr -d '[:space:]') | |
| if [[ -z "$IP" ]]; then | |
| echo "::error::Could not determine runner public IP" | |
| exit 1 | |
| fi | |
| echo "Ensuring A record: $DOMAIN → $IP" | |
| CHANGE_FILE=$(mktemp) | |
| cat > "$CHANGE_FILE" <<EOF | |
| { | |
| "Comment": "moqx auto-deploy: $DOMAIN", | |
| "Changes": [ | |
| { | |
| "Action": "UPSERT", | |
| "ResourceRecordSet": { | |
| "Name": "$DOMAIN", | |
| "Type": "A", | |
| "TTL": 300, | |
| "ResourceRecords": [{"Value": "$IP"}] | |
| } | |
| } | |
| ] | |
| } | |
| EOF | |
| CHANGE_ID=$(aws route53 change-resource-record-sets \ | |
| --hosted-zone-id "$HOSTED_ZONE_ID" \ | |
| --change-batch "file://$CHANGE_FILE" \ | |
| --query 'ChangeInfo.Id' --output text) | |
| aws route53 wait resource-record-sets-changed --id "$CHANGE_ID" | |
| rm -f "$CHANGE_FILE" | |
| - name: Ensure TLS cert | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.OMOQ_CERTBOT_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.OMOQ_CERTBOT_SECRET_ACCESS_KEY }} | |
| run: | | |
| CERT="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" | |
| need_issue=false | |
| if [ ! -f "$CERT" ]; then | |
| echo "::warning::No cert for ${DOMAIN} — provisioning via Route53" | |
| need_issue=true | |
| elif ! sudo openssl x509 -in "$CERT" -noout -checkend 2592000 2>/dev/null; then | |
| echo "::warning::Cert for ${DOMAIN} expires within 30 days — renewing" | |
| need_issue=true | |
| else | |
| echo "Cert for ${DOMAIN} is valid" | |
| fi | |
| if $need_issue; then | |
| sudo -E certbot certonly --dns-route53 \ | |
| --cert-name "$DOMAIN" \ | |
| -d "$DOMAIN" \ | |
| --non-interactive --agree-tos \ | |
| --email gmarzot@openmoq.org | |
| fi | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| # Auto-deploy uses the shared core (docker/relay-deploy.sh) with stats ON, | |
| # so main keeps the stats stack + public read-only dashboard live. Pulls | |
| # the compose-pinned :latest (this run's just-published image). | |
| - name: Deploy (relay + stats + public dashboard) | |
| env: | |
| ENABLE_STATS: "true" | |
| STATS_USER: ${{ secrets.STATS_USER }} | |
| STATS_PASSWORD: ${{ secrets.STATS_PASSWORD }} | |
| GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} | |
| CRASH_WATCH: "true" | |
| CRASH_GH_TOKEN: ${{ secrets.OMOQ_CRASH_DEBUG_TOKEN }} | |
| CRASH_SLACK_WEBHOOK_URL: ${{ secrets.OMOQ_SLACK_WEBHOOK_URL }} | |
| run: bash docker/relay-deploy.sh | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Prune build caches: keep the newest generation of each cache family. | |
| # | |
| # Every key carries github.sha, so each run mints an entry rather than | |
| # replacing one. Nothing removed the superseded generations, and the repo | |
| # reached 27 GB against a 10 GB budget — six live generations per lane, which | |
| # GitHub then evicts by age, taking usable caches with them. Port of | |
| # openmoq/moxygen#383. | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| prune-caches: | |
| needs: [build, conformance, publish] | |
| if: always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| actions: write | |
| steps: | |
| # Runs after the cache-writing jobs so this run's entries are already | |
| # saved and are the generation kept. | |
| - name: Delete superseded generations | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| cat > family.jq <<'JQ' | |
| # Family = the key without its trailing hex suffix: ccache keys end in | |
| # -<sha>, dependency keys in -<content hash>. Anything else (setup-uv | |
| # and friends) is left alone. | |
| def family: | |
| if (.key | test("-[0-9a-f]{32,}$")) then | |
| (.key | capture("^(?<p>.*)-[0-9a-f]{32,}$").p) | |
| else null end; | |
| map(select(family != null)) | |
| | group_by(family) | |
| | map(sort_by(.createdAt) | reverse | .[1:]) | |
| | flatten | |
| | .[] | "\(.id)\t\(.sizeInBytes)\t\(.key)" | |
| JQ | |
| gh cache list --ref "$GITHUB_REF" --limit 100 \ | |
| --json id,key,sizeInBytes,createdAt > caches.json | |
| jq -r -f family.jq caches.json > stale.tsv | |
| if [ ! -s stale.tsv ]; then | |
| echo "==> nothing superseded" | |
| exit 0 | |
| fi | |
| FREED=0 | |
| while IFS=$'\t' read -r id size key; do | |
| echo " deleting $key ($((size / 1048576)) MB)" | |
| if gh cache delete "$id"; then | |
| FREED=$((FREED + size)) | |
| fi | |
| done < stale.tsv | |
| echo "==> freed $((FREED / 1048576)) MB" | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Notify: aggregate status (runs after everything) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| notify: | |
| needs: [check-format, build, conformance, publish, manifest, release, deploy] | |
| if: always() | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Notify Slack | |
| continue-on-error: true | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.OMOQ_SLACK_WEBHOOK_URL }} | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Map job results to status symbols | |
| fmt_status() { | |
| case "$1" in | |
| success) echo "✓" ;; | |
| failure) echo "✗" ;; | |
| cancelled) echo "⊘" ;; | |
| *) echo "—" ;; | |
| esac | |
| } | |
| # verify = worst of check-format + build | |
| if [ "${{ needs.check-format.result }}" = "failure" ] || [ "${{ needs.build.result }}" = "failure" ]; then | |
| VER_RESULT="failure" | |
| elif [ "${{ needs.check-format.result }}" = "cancelled" ] || [ "${{ needs.build.result }}" = "cancelled" ]; then | |
| VER_RESULT="cancelled" | |
| elif [ "${{ needs.check-format.result }}" = "success" ] && [ "${{ needs.build.result }}" = "success" ]; then | |
| VER_RESULT="success" | |
| else | |
| VER_RESULT="skipped" | |
| fi | |
| VER=$(fmt_status "$VER_RESULT") | |
| CONF=$(fmt_status "${{ needs.conformance.result }}") | |
| PUB=$(fmt_status "${{ needs.publish.result }}") | |
| REL=$(fmt_status "${{ needs.release.result }}") | |
| DEP=$(fmt_status "${{ needs.deploy.result }}") | |
| STATUS="verify:${VER} conformance:${CONF} publish:${PUB} release:${REL} deploy:${DEP}" | |
| if [ "${{ needs.release.result }}" = "success" ]; then | |
| REL_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.release.outputs.tag || 'snapshot-latest' }}" | |
| TEXT=":white_check_mark: *${{ github.repository }}* \`${{ github.ref_name }}\` \`${SHORT}\` — ${STATUS} <${RUN_URL}|#${{ github.run_number }}> artifacts: <${REL_URL}|view>" | |
| else | |
| TEXT=":x: *${{ github.repository }}* \`${{ github.ref_name }}\` \`${SHORT}\` — ${STATUS} <${RUN_URL}|#${{ github.run_number }}>" | |
| fi | |
| curl -s -X POST "$SLACK_WEBHOOK_URL" \ | |
| -H "Content-Type: application/json" \ | |
| --data "{\"text\": \"${TEXT}\"}" | |
| - name: Notify email | |
| continue-on-error: true | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.OMOQ_AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.OMOQ_AWS_SECRET_ACCESS_KEY }} | |
| AWS_DEFAULT_REGION: us-east-1 | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # verify = worst of check-format + build | |
| CF="${{ needs.check-format.result }}" | |
| BLD="${{ needs.build.result }}" | |
| if [ "$CF" = "failure" ] || [ "$BLD" = "failure" ]; then | |
| VER="failure" | |
| elif [ "$CF" = "cancelled" ] || [ "$BLD" = "cancelled" ]; then | |
| VER="cancelled" | |
| elif [ "$CF" = "success" ] && [ "$BLD" = "success" ]; then | |
| VER="success" | |
| else | |
| VER="skipped" | |
| fi | |
| PUB="${{ needs.publish.result }}" | |
| REL="${{ needs.release.result }}" | |
| DEP="${{ needs.deploy.result }}" | |
| STATUS="verify:${VER} publish:${PUB} release:${REL} deploy:${DEP}" | |
| if [ "${{ needs.release.result }}" = "success" ]; then | |
| REL_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.release.outputs.tag || 'snapshot-latest' }}" | |
| SUBJECT="[moqx] published ${{ github.ref_name }} ${SHORT}" | |
| BODY="Repository: ${{ github.repository }}\nBranch: ${{ github.ref_name }}\nCommit: ${GITHUB_SHA}\nStatus: ${STATUS}\nDocker: ghcr.io/${{ github.repository }}:${SHORT}\nArtifacts: ${REL_URL}\nRun: ${RUN_URL}" | |
| else | |
| SUBJECT="[moqx] pipeline failed ${{ github.ref_name }} ${SHORT}" | |
| BODY="Repository: ${{ github.repository }}\nBranch: ${{ github.ref_name }}\nCommit: ${GITHUB_SHA}\nStatus: ${STATUS}\nRun: ${RUN_URL}" | |
| fi | |
| aws ses send-email \ | |
| --from "noreply@ci.openmoq.org" \ | |
| --destination '{"ToAddresses":["github-notifications@openmoq.org"]}' \ | |
| --message "{ | |
| \"Subject\": {\"Data\": \"${SUBJECT}\"}, | |
| \"Body\": {\"Text\": {\"Data\": \"${BODY}\"}} | |
| }" |