Skip to content

Merge pull request #772 from openmoq/sync-moxygen/3fb94e4 #534

Merge pull request #772 from openmoq/sync-moxygen/3fb94e4

Merge pull request #772 from openmoq/sync-moxygen/3fb94e4 #534

Workflow file for this run

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}\"}}
}"