Merge pull request #87 from openmoq/sync-moxygen/de598cd #36
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 | |
| on: | |
| push: | |
| branches: [main] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| checks: write | |
| packages: write | |
| jobs: | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Verify: format + build/test matrix | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| check-format: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install clang-format | |
| run: pip install clang-format==19.1.7 | |
| - name: Check formatting | |
| run: bash scripts/format.sh --check | |
| build: | |
| needs: [check-format] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: linux | |
| preset: default | |
| build_dir: build | |
| runner: ubuntu-22.04 | |
| - name: asan debug | |
| preset: san | |
| build_dir: build-san | |
| runner: [self-hosted, linode] | |
| name: ${{ matrix.name }} | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Generate app token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.OMOQ_APP_ID }} | |
| private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| - name: Install system dependencies | |
| run: sudo deps/moxygen/standalone/install-system-deps.sh | |
| - name: Setup dependencies | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: bash scripts/build.sh setup --from-release --no-fallback | |
| - name: Build | |
| run: bash scripts/build.sh --profile ${{ matrix.preset }} --build-dir ${{ matrix.build_dir }} | |
| - name: Test | |
| env: | |
| ASAN_OPTIONS: ${{ matrix.name == 'asan debug' && 'detect_leaks=1:abort_on_error=1' || '' }} | |
| run: bash scripts/build.sh test --build-dir ${{ matrix.build_dir }} -- --output-junit test-results.xml | |
| - name: Publish test results | |
| uses: dorny/test-reporter@v1.9.1 | |
| if: success() || failure() | |
| with: | |
| name: "test (${{ matrix.name }})" | |
| path: ${{ matrix.build_dir }}/test-results.xml | |
| reporter: java-junit | |
| fail-on-empty: ${{ job.status == 'success' && 'true' || 'false' }} | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Publish: build moqx, Docker image + smoke test, push | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| publish: | |
| needs: [check-format, build] | |
| name: publish | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Generate app token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.OMOQ_APP_ID }} | |
| private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| - name: Download bookworm moxygen tarball | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| MOQX_PLATFORM: bookworm-amd64 | |
| run: bash scripts/build.sh setup --from-release --no-fallback | |
| - name: Stage tarball for Docker build | |
| run: | | |
| mkdir -p .docker-deps | |
| cp -a .scratch/moxygen-install .docker-deps/moxygen | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Build Docker image | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| docker build -f docker/Dockerfile \ | |
| -t "${IMAGE}:${SHORT}" \ | |
| -t "${IMAGE}:latest" \ | |
| . | |
| - name: Smoke test Docker image | |
| run: | | |
| IMAGE="ghcr.io/${{ github.repository }}:latest" | |
| 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/tests/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" | |
| - name: Push Docker image | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| IMAGE="ghcr.io/${{ github.repository }}" | |
| docker push "${IMAGE}:${SHORT}" | |
| docker push "${IMAGE}:latest" | |
| - name: Package | |
| id: package | |
| run: | | |
| ARTIFACT="moqx-bookworm-amd64.tar.gz" | |
| docker cp "$(docker create --name extract ghcr.io/${{ github.repository }}:latest):/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" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ steps.package.outputs.artifact }} | |
| path: ${{ steps.package.outputs.artifact }} | |
| retention-days: 90 | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Release: create/update snapshot-latest pre-release (all green) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| release: | |
| needs: [build, publish] | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: "*.tar.gz" | |
| path: artifacts/ | |
| - name: Publish snapshot | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| SHORT="${GITHUB_SHA:0:7}" | |
| TAG="snapshot-latest" | |
| 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/**/* \ | |
| --title "Latest build ($SHORT)" \ | |
| --prerelease \ | |
| --notes "$(cat <<EOF | |
| Rolling snapshot of the latest build from \`main\`. | |
| **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 \`main\`. | |
| EOF | |
| )" | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Deploy: auto-deploy to moqx-000 after successful release | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| deploy: | |
| needs: [publish, release] | |
| runs-on: [self-hosted, linode] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Write .env | |
| working-directory: docker | |
| run: | | |
| cat > .env <<EOF | |
| DOMAIN=moqx-000.ci.openmoq.org | |
| CERTBOT_EMAIL=gmarzot@openmoq.org | |
| MOQX_PORT=4433 | |
| MOQX_ADMIN_PORT=8000 | |
| MOQX_LOG_LEVEL=0 | |
| MOQX_VERBOSE=0 | |
| EOF | |
| sed -i 's/^[[:space:]]*//' .env | |
| - name: Pull and deploy | |
| working-directory: docker | |
| run: | | |
| docker compose pull | |
| docker compose down --remove-orphans 2>/dev/null || true | |
| # Remove any stale containers with matching names (e.g. from manual docker run) | |
| docker rm -f moqx logmon 2>/dev/null || true | |
| docker compose up -d | |
| echo "==> Waiting for admin endpoint..." | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:8000/info >/dev/null 2>&1; then | |
| break | |
| fi | |
| sleep 1 | |
| if [ "$i" -eq 30 ]; then | |
| echo "::error::Admin endpoint did not respond within 30s" | |
| docker compose logs moqx | |
| exit 1 | |
| fi | |
| done | |
| RESP=$(curl -sf http://127.0.0.1:8000/info) | |
| echo "==> Relay deployed: $RESP" | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| # Notify: aggregate status (runs after everything) | |
| # ════════════════════════════════════════════════════════════════════════════ | |
| notify: | |
| needs: [check-format, build, publish, 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") | |
| PUB=$(fmt_status "${{ needs.publish.result }}") | |
| REL=$(fmt_status "${{ needs.release.result }}") | |
| DEP=$(fmt_status "${{ 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/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/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}\"}} | |
| }" |