Skip to content

version release

version release #1

name: version release
# Manual workflow_dispatch to cut a moqx tagged release.
#
# Tags vM.m.p on the dispatched ref (main or release/vM.m). Promotes the
# corresponding rolling snapshot pre-release tarballs and, when feasible,
# also builds portable linux-{amd64,arm64} tarballs against an explicit
# moxygen release tag.
#
# Job graph (parallel):
#
# validate
# │
# ▼
# create-release (--draft)
# │
# ├──► portable-build (matrix amd64/arm64, conditional)
# │ └─► uploads directly to release
# ├──► promote-snapshot
# │ └─► xargs -P parallel uploads to release
# │
# ▼
# finalize (--draft=false)
#
# Inputs:
# version required. semver, e.g. 1.2.3
# moxygen_release optional. moxygen tag to consume (e.g. v1.2.3).
# When absent, falls back to moxygen `snapshot-latest`.
# The fallback is fine for everyday tagging but won't
# produce portable tarballs (snapshot-latest doesn't
# carry them — portable only ships in tagged moxygen
# releases).
#
# Behavior matrix:
# moxygen_release present + has portable assets → portable matrix runs
# moxygen_release present + no portable assets → portable skipped (no failure)
# moxygen_release absent → portable skipped (no failure)
#
# Snapshot source — derived from dispatched ref:
# dispatched on main → consume snapshot-latest
# dispatched on release/vM.m → consume snapshot-vM.m-latest
#
# Release branch creation is independent of this workflow. To maintain a
# patch line, devops cuts release/vM.m manually
# (`git checkout -b release/vM.m main && git push`); this workflow is
# then dispatched on that branch to tag vM.m.p.
on:
workflow_dispatch:
inputs:
version:
description: 'moqx version (e.g. 1.2.3)'
required: true
type: string
moxygen_release:
description: 'moxygen release tag (optional, e.g. v1.2.3). Default: moxygen snapshot-latest.'
required: false
type: string
# Serialize per-version dispatches: prevents two runs with the same
# version from both passing validate's tag-doesn't-exist check and
# then racing on create-release.
concurrency:
group: version-release-${{ inputs.version }}
cancel-in-progress: false
permissions:
contents: write
jobs:
# ════════════════════════════════════════════════════════════════════════════
# Validate: semver gate, tag-doesn't-exist, resolve snapshot tag/sha,
# probe moxygen reference for portable assets.
# ════════════════════════════════════════════════════════════════════════════
validate:
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.v.outputs.version }}
tag: ${{ steps.v.outputs.tag }}
snapshot_tag: ${{ steps.v.outputs.snapshot_tag }}
snapshot_sha: ${{ steps.v.outputs.snapshot_sha }}
moxygen_ref: ${{ steps.v.outputs.moxygen_ref }}
has_portable: ${{ steps.v.outputs.has_portable }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate inputs and resolve refs
id: v
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ inputs.version }}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "Error: version must be semver (e.g. 1.2.3 or 1.2.3-rc1)" >&2
exit 1
fi
TAG="v${VERSION}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Error: tag $TAG already exists" >&2
exit 1
fi
# Snapshot source from dispatched ref.
BRANCH="${{ github.ref_name }}"
if [ "$BRANCH" = "main" ]; then
SNAPSHOT_TAG="snapshot-latest"
else
LABEL="${BRANCH#release/}"
SNAPSHOT_TAG="snapshot-${LABEL}-latest"
fi
echo "==> Dispatched from $BRANCH → consuming $SNAPSHOT_TAG"
# Resolve snapshot SHA.
SNAPSHOT_SHA=$(gh api repos/{owner}/{repo}/releases \
--jq ".[] | select(.tag_name == \"$SNAPSHOT_TAG\") | .target_commitish")
if [[ -z "$SNAPSHOT_SHA" ]]; then
echo "Error: $SNAPSHOT_TAG release not found. Push to the dispatched branch first to produce a snapshot." >&2
exit 1
fi
# Moxygen reference: explicit input or snapshot-latest fallback.
MOX_REF="${{ inputs.moxygen_release }}"
if [ -z "$MOX_REF" ]; then
MOX_REF="snapshot-latest"
echo "==> No moxygen_release input — defaulting to moxygen snapshot-latest"
else
echo "==> Pinning to moxygen release: $MOX_REF"
fi
# Verify moxygen reference exists.
if ! gh release view "$MOX_REF" --repo openmoq/moxygen >/dev/null 2>&1; then
echo "Error: moxygen release $MOX_REF does not exist on openmoq/moxygen" >&2
exit 1
fi
# Probe portable assets.
ASSETS=$(gh release view "$MOX_REF" --repo openmoq/moxygen --json assets --jq '.assets[].name')
HAS_PORTABLE=true
for required in moxygen-linux-amd64.tar.gz moxygen-linux-arm64.tar.gz; do
if ! echo "$ASSETS" | grep -qx "$required"; then
HAS_PORTABLE=false
break
fi
done
if [ "$HAS_PORTABLE" = "true" ]; then
echo "==> moxygen $MOX_REF has portable assets — portable matrix will run"
else
echo "==> moxygen $MOX_REF lacks portable assets — portable matrix will be skipped"
fi
{
echo "version=$VERSION"
echo "tag=$TAG"
echo "snapshot_tag=$SNAPSHOT_TAG"
echo "snapshot_sha=$SNAPSHOT_SHA"
echo "moxygen_ref=$MOX_REF"
echo "has_portable=$HAS_PORTABLE"
} >> "$GITHUB_OUTPUT"
# ════════════════════════════════════════════════════════════════════════════
# Create-release: open a draft release that the upload jobs append to.
# ════════════════════════════════════════════════════════════════════════════
create-release:
needs: [validate]
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
- name: Create release (no assets yet)
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ needs.validate.outputs.version }}
TAG: ${{ needs.validate.outputs.tag }}
MOX_REF: ${{ needs.validate.outputs.moxygen_ref }}
HAS_PORTABLE: ${{ needs.validate.outputs.has_portable }}
SNAPSHOT_TAG: ${{ needs.validate.outputs.snapshot_tag }}
SNAPSHOT_SHA: ${{ needs.validate.outputs.snapshot_sha }}
run: |
if [ "$HAS_PORTABLE" = "true" ]; then
PORTABLE_NOTE="**Portable Linux tarballs** built against moxygen \`${MOX_REF}\`:
- \`moqx-linux-amd64.tar.gz\` — \`-march=x86-64-v2\` (≈ all cloud x86 since 2015)
- \`moqx-linux-arm64.tar.gz\` — \`-march=armv8-a\` (Graviton, Ampere, Apple M, RPi 3+, Rockchip)"
else
PORTABLE_NOTE="_Portable Linux tarballs not produced for this release: moxygen \`${MOX_REF}\` does not include the required portable assets._"
fi
gh release create "$TAG" \
--target "$SNAPSHOT_SHA" \
--title "moqx $VERSION" \
--notes "$(cat <<EOF
**Version:** \`${VERSION}\`
**Commit:** \`${SNAPSHOT_SHA}\`
**moxygen reference:** \`${MOX_REF}\`
Standard tarballs promoted from [\`${SNAPSHOT_TAG}\`](${{ github.server_url }}/${{ github.repository }}/releases/tag/${SNAPSHOT_TAG}).
${PORTABLE_NOTE}
EOF
)"
# ════════════════════════════════════════════════════════════════════════════
# Portable build: -O3, ISA-baselined, statically linked moqx-linux-{amd64,arm64}
# tarballs. Runs only when the moxygen reference carries portable assets.
# Each runner uploads its own tarball directly to the draft release.
# ════════════════════════════════════════════════════════════════════════════
portable-build:
needs: [validate, create-release]
if: needs.validate.outputs.has_portable == 'true'
strategy:
fail-fast: false
matrix:
include:
# cxx_flags MUST match what moxygen's portable build used —
# folly's F14 templates select an intrinsics mode based on
# compile-time -march macros. Mismatch causes
# F14LinkCheck<N>::check() undefined-reference link errors.
# Frame-pointer flags preserved at -O3 so signal-handler stack
# unwinding (folly::symbolizer) and post-mortem debugging via
# the moxygen .dbg sidecar work against optimized binaries.
- arch: amd64
runner: ubuntu-22.04
platform: linux-amd64
cxx_flags: "-march=x86-64-v2 -fno-omit-frame-pointer -fasynchronous-unwind-tables"
- arch: arm64
runner: ubuntu-22.04-arm
platform: linux-arm64
cxx_flags: "-march=armv8-a -fno-omit-frame-pointer -fasynchronous-unwind-tables"
name: portable (${{ matrix.arch }})
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: |
# Match docker/Dockerfile's build-stage apt list — picks up
# libdwarf-dev / libbrotli-dev that moxygen's install-system-deps
# doesn't install but proxygen needs.
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential ninja-build cmake git ca-certificates pkg-config \
libssl-dev libunwind-dev libgoogle-glog-dev libgflags-dev \
libdouble-conversion-dev libevent-dev libsodium-dev libzstd-dev \
libboost-all-dev libfmt-dev zlib1g-dev libc-ares-dev libdwarf-dev \
libbrotli-dev libgtest-dev libgmock-dev libxxhash-dev
- name: Download moxygen portable tarball
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
MOQX_PLATFORM: ${{ matrix.platform }}
MOX_REF: ${{ needs.validate.outputs.moxygen_ref }}
run: |
if [ "$MOX_REF" = "snapshot-latest" ]; then
bash scripts/build.sh setup --use-latest --no-fallback
else
export MOQX_MOXYGEN_RELEASE_TAG="$MOX_REF"
bash scripts/build.sh setup --no-fallback
fi
- name: Configure
run: |
# CMAKE_INSTALL_RPATH = $ORIGIN/../lib makes the binary look
# for bundled .so files next to itself before the system search
# path. Combined with the bundle step below this produces a
# self-contained tarball that runs on noble / RHEL 9 / AL2023
# without needing distro-specific packages installed.
cmake -S . -B _build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$(cat .scratch/cmake_prefix_path.txt)" \
-DCMAKE_MODULE_PATH="${GITHUB_WORKSPACE}/cmake;${GITHUB_WORKSPACE}/deps/moxygen/build/fbcode_builder/CMake" \
-DCMAKE_C_FLAGS="${{ matrix.cxx_flags }}" \
-DCMAKE_CXX_FLAGS="${{ matrix.cxx_flags }}" \
-DCMAKE_INSTALL_RPATH='$ORIGIN/../lib' \
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
-DCMAKE_DISABLE_FIND_PACKAGE_LibUnwind=TRUE \
-DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/_install"
- name: Build
run: cmake --build _build -j "$(nproc)"
- name: Install
run: cmake --install _build
- name: Bundle non-toolchain dynamic deps
run: |
# Bundle SONAME-versioned .so deps so the tarball runs on any
# modern Linux without distro-specific packages installed.
# Skips libstdc++/libgcc_s/libc/libm/ld-linux/linux-vdso (toolchain).
mkdir -p _install/lib
LIBARCH="$(uname -m)-linux-gnu"
# libboost_context: jammy=1.74, noble=1.83 — must bundle.
# libunwind intentionally NOT bundled — disabled via
# CMAKE_DISABLE_FIND_PACKAGE_LibUnwind to match moxygen portable.
for lib in \
libboost_context.so.1.74.0 \
libgflags.so.2.2 \
libsodium.so.23 \
libdouble-conversion.so.3 \
libevent-2.1.so.7 \
libzstd.so.1 \
libcares.so.2 \
libbrotlidec.so.1 \
libbrotlienc.so.1 \
libbrotlicommon.so.1 \
; do
src="/usr/lib/${LIBARCH}/${lib}"
if [ -f "$src" ]; then
cp -L "$src" _install/lib/
fi
done
echo "==> bundled libs:"
ls -lh _install/lib/
- name: Package
run: |
ARTIFACT="moqx-${{ matrix.platform }}.tar.gz"
tar czf "$ARTIFACT" -C _install .
ls -lh "$ARTIFACT"
- name: Smoke test
run: |
mkdir -p /tmp/moqx-smoke
tar xzf "moqx-${{ matrix.platform }}.tar.gz" -C /tmp/moqx-smoke
echo "==> ldd"
ldd /tmp/moqx-smoke/bin/moqx | tee /tmp/ldd.out
if grep -q "not found" /tmp/ldd.out; then
echo "ERROR: missing shared libraries"
exit 1
fi
- name: Upload to release
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
gh release upload "$TAG" "moqx-${{ matrix.platform }}.tar.gz" --clobber
# ════════════════════════════════════════════════════════════════════════════
# Promote-snapshot: copy the existing platform tarballs from the rolling
# snapshot to the new release. Runs in parallel with portable-build.
# ════════════════════════════════════════════════════════════════════════════
promote-snapshot:
needs: [validate, create-release]
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 }}
- name: Verify snapshot hasn't been replaced mid-flight
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
SNAPSHOT_TAG: ${{ needs.validate.outputs.snapshot_tag }}
EXPECTED_SHA: ${{ needs.validate.outputs.snapshot_sha }}
run: |
CURRENT_SHA=$(gh api "repos/$REPO/releases" \
--jq ".[] | select(.tag_name == \"$SNAPSHOT_TAG\") | .target_commitish")
if [ "$CURRENT_SHA" != "$EXPECTED_SHA" ]; then
echo "Error: $SNAPSHOT_TAG was replaced mid-release." >&2
echo " expected: $EXPECTED_SHA" >&2
echo " current: $CURRENT_SHA" >&2
echo "Retry the dispatch — ci-main published a new snapshot during this run." >&2
exit 1
fi
- name: Download snapshot tarballs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
SNAPSHOT_TAG: ${{ needs.validate.outputs.snapshot_tag }}
run: |
mkdir -p snapshot-assets
gh release download "$SNAPSHOT_TAG" --repo "$REPO" --dir snapshot-assets --pattern '*.tar.gz'
# Drop any portable tarballs from snapshot — those are built fresh
# in portable-build and uploaded directly there.
rm -f snapshot-assets/moqx-linux-amd64*.tar.gz \
snapshot-assets/moqx-linux-arm64*.tar.gz
echo "Snapshot tarballs to promote:"
ls -lh snapshot-assets/
- name: Upload to release (parallel)
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
# Fan out asset upload to N concurrent gh release upload calls.
find snapshot-assets -name '*.tar.gz' -print0 | \
xargs -0 -n1 -P 6 -I{} gh release upload "$TAG" --repo "$REPO" {} --clobber
# ════════════════════════════════════════════════════════════════════════════
# Finalize: mark release as latest once all uploads succeeded.
# Tolerates portable-build being skipped (has_portable=false).
# ════════════════════════════════════════════════════════════════════════════
finalize:
needs: [validate, create-release, portable-build, promote-snapshot]
if: |
always() &&
needs.validate.result == 'success' &&
needs.create-release.result == 'success' &&
needs.promote-snapshot.result == 'success' &&
needs.portable-build.result != 'failure' &&
needs.portable-build.result != 'cancelled'
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 }}
- name: Mark release as latest
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
gh release edit "$TAG" --repo "$REPO" --latest
echo "Released: $TAG"