On Merge | Validate Build #100
Workflow file for this run
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: "On Merge | Validate Build" | |
| on: | |
| # The merge queue gates trunk; this is the comprehensive tier. | |
| merge_group: | |
| # Allow this workflow to be executed manually from the GH UI. | |
| workflow_dispatch: | |
| env: | |
| CARGO_TERM_COLOR: always | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Single required-check context for the merge queue. Keep this job's name | |
| # ("⚡ PR Ready") identical to the one in on-push.yml. publish-edge is in the | |
| # needs list so the merge waits for the rolling edge prerelease to be | |
| # published before the candidate fast-forwards onto trunk. | |
| pr-ready: | |
| if: always() | |
| name: "⚡ PR Ready" | |
| runs-on: ubuntu-22.04 | |
| needs: | |
| - "format" | |
| - "build" | |
| - "dist-plan" | |
| - "dist-build" | |
| - "publish-edge" | |
| steps: | |
| - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }} | |
| run: | | |
| echo "One or more dependent jobs failed, was skipped, or was cancelled. All jobs must pass for the PR to be ready." | |
| exit 1 | |
| - run: echo "OK" | |
| # Fast-fail formatting gate (cheap; surfaces a formatting miss before the | |
| # heavier build job finishes). See on-push.yml for why we use setup-rust | |
| # without forcing a toolchain and without a CROSS_REPO_PAT. | |
| format: | |
| name: Check Formatting | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Setup Rust | |
| uses: "wack/gh-actions/setup-rust@trunk" | |
| - name: Check formatting | |
| run: cargo make check-format | |
| # Full validation. multitool's `ci-flow` bundles pre-build checks, format, | |
| # clippy, build, docs, the nextest suite, AND llvm-cov coverage in one task, | |
| # so there is no separate coverage job (unlike the Helm-based Wack repos that | |
| # split coverage out). | |
| build: | |
| name: Validate Rust Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Setup Rust | |
| uses: "wack/gh-actions/setup-rust@trunk" | |
| with: | |
| # ci-flow runs the suite with cargo-nextest and collects coverage with | |
| # cargo-llvm-cov, so both must be installed alongside cargo-make. | |
| shared-key: rust-ci | |
| extra-tools: cargo-nextest,cargo-llvm-cov | |
| - name: Cargo Make | |
| run: cargo make ci-flow | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Gating release build. Every merge-queue candidate is built for all Linux | |
| # dist targets before it can land, so trunk never holds a commit that fails to | |
| # produce a release artifact. The macOS targets are intentionally excluded | |
| # here to keep (10×-billed) macOS runner spend off every merge — they're built | |
| # and published only on a version tag, via release.yml. Consequently the | |
| # rolling `edge` prerelease published below carries Linux binaries only; macOS | |
| # binaries ship with tagged releases. | |
| # | |
| # The matrix is read at runtime from `dist plan` (then filtered to Linux), so | |
| # it tracks dist-workspace.toml automatically — no hand-maintained target list. | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| dist-plan: | |
| name: Plan release build | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| matrix: ${{ steps.plan.outputs.matrix }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| submodules: recursive | |
| - name: Install dist | |
| shell: bash | |
| run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh" | |
| - id: plan | |
| shell: bash | |
| run: | | |
| # Tagless plan (no GitHub Release is created); we only need the build | |
| # matrix that describes each target's runner and dist invocation. | |
| # Filter to Linux targets so the merge queue never pays for macOS | |
| # builds — those run on tags (release.yml) with the full target set. | |
| dist plan --output-format=json > plan-dist-manifest.json | |
| echo "matrix=$(jq -c '{include: [.ci.github.artifacts_matrix.include[] | select([.targets[] | contains("linux")] | any)]}' plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" | |
| dist-build: | |
| name: "Build (${{ join(matrix.targets, ', ') }})" | |
| needs: dist-plan | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.dist-plan.outputs.matrix) }} | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container && matrix.container.image || null }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| submodules: recursive | |
| - name: Install Rust non-interactively if not already installed | |
| if: ${{ matrix.container }} | |
| run: | | |
| if ! command -v cargo > /dev/null 2>&1; then | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| fi | |
| - name: Install OpenSSL and MUSL build dependencies | |
| if: ${{ startsWith(matrix.runner, 'ubuntu') }} | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -yq openssl libssl-dev musl-tools perl make | |
| # `usearch` (pulled in transitively via cersei-agent -> cersei-tools -> | |
| # cersei-embeddings) compiles C++ through `cxx`, so the musl target needs a | |
| # musl-targeting C++ compiler. `musl-tools` only ships `musl-gcc` (C), not | |
| # g++, so we install a full GNU musl-cross toolchain and point cc-rs + the | |
| # Rust linker at it. GNU (not zig/clang) deliberately: `link-cplusplus` | |
| # links GNU libstdc++, which this toolchain bundles statically — the ABI | |
| # match is what lets the fully-static musl binary link the C++ objects. | |
| # | |
| # Sourced from the cross-tools/musl-cross GitHub release (version-pinned, | |
| # checksum-verified) rather than musl.cc, which is unreachable from | |
| # GitHub's Azure-hosted runners; GitHub release assets always are. Kept in | |
| # sync with release-prebuild.yml.stub (which dist injects into release.yml). | |
| - name: Install MUSL C++ cross toolchain | |
| if: ${{ contains(join(matrix.targets, ','), '-linux-musl') }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ver=20260515 | |
| file=x86_64-unknown-linux-musl.tar.xz | |
| base="https://github.com/cross-tools/musl-cross/releases/download/${ver}" | |
| for attempt in 1 2 3; do | |
| if curl -fSL --retry 3 --connect-timeout 30 -o "/tmp/${file}" "${base}/${file}" \ | |
| && curl -fSL --retry 3 --connect-timeout 30 -o "/tmp/${file}.sha256" "${base}/${file}.sha256"; then | |
| break | |
| fi | |
| echo "download attempt $attempt failed; retrying in 10s" >&2 | |
| sleep 10 | |
| done | |
| expected=$(awk '{print $1}' "/tmp/${file}.sha256") | |
| actual=$(sha256sum "/tmp/${file}" | awk '{print $1}') | |
| if [ "$expected" != "$actual" ]; then | |
| echo "checksum mismatch for ${file}: got ${actual}, expected ${expected}" >&2 | |
| exit 1 | |
| fi | |
| sudo tar -xJf "/tmp/${file}" -C /opt | |
| bindir=/opt/x86_64-unknown-linux-musl/bin | |
| echo "$bindir" >> "$GITHUB_PATH" | |
| { | |
| echo "CC_x86_64_unknown_linux_musl=$bindir/x86_64-unknown-linux-musl-gcc" | |
| echo "CXX_x86_64_unknown_linux_musl=$bindir/x86_64-unknown-linux-musl-g++" | |
| echo "AR_x86_64_unknown_linux_musl=$bindir/x86_64-unknown-linux-musl-ar" | |
| echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=$bindir/x86_64-unknown-linux-musl-gcc" | |
| } >> "$GITHUB_ENV" | |
| - name: Install dist | |
| run: ${{ matrix.install_dist.run }} | |
| - name: Install dependencies | |
| run: | | |
| ${{ matrix.packages_install }} | |
| - id: build | |
| name: Build artifacts | |
| shell: bash | |
| run: | | |
| dist build --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json | |
| echo "dist ran successfully" | |
| # Collect the artifact paths dist just produced so publish-edge can | |
| # reuse them without rebuilding. | |
| echo "paths<<EOF" >> "$GITHUB_OUTPUT" | |
| dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: edge-artifacts-${{ join(matrix.targets, '_') }} | |
| path: ${{ steps.build.outputs.paths }} | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Publish the validated artifacts to the rolling `edge` prerelease. Because it | |
| # depends on format + build + dist-build, it runs LAST in the merge group — | |
| # only after every other check is green — so it publishes a candidate that has | |
| # already cleared validation and is the commit about to fast-forward onto | |
| # trunk. It reuses dist-build's (Linux-only) artifacts (no rebuild), so `edge` | |
| # carries Linux binaries; macOS binaries ship with tagged releases. It is in | |
| # pr-ready's needs, so the merge waits for it. | |
| # | |
| # The publish steps only run on merge_group, not workflow_dispatch, so a manual | |
| # dispatch (used for inspection) validates without touching the edge release — | |
| # but the job still succeeds, keeping pr-ready green. | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| publish-edge: | |
| name: Publish edge prerelease | |
| needs: | |
| - format | |
| - build | |
| - dist-build | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EDGE_TAG: edge | |
| steps: | |
| - name: Download built artifacts | |
| if: ${{ github.event_name == 'merge_group' }} | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: edge-artifacts-* | |
| path: edge/ | |
| merge-multiple: true | |
| - name: Generate checksums | |
| if: ${{ github.event_name == 'merge_group' }} | |
| working-directory: edge | |
| run: | | |
| shopt -s nullglob | |
| files=(*) | |
| if [ ${#files[@]} -eq 0 ]; then | |
| echo "No artifacts were downloaded; refusing to publish an empty edge release." >&2 | |
| exit 1 | |
| fi | |
| sha256sum "${files[@]}" > SHA256SUMS | |
| cat SHA256SUMS | |
| - name: Update rolling edge prerelease | |
| if: ${{ github.event_name == 'merge_group' }} | |
| env: | |
| # github.sha is the merge-queue candidate that fast-forwards onto trunk | |
| # when this run passes — the exact commit we want edge to point at. | |
| RELEASE_COMMIT: ${{ github.sha }} | |
| run: | | |
| # Recreate the rolling prerelease so its assets and target commit are | |
| # always the latest trunk candidate. Delete + recreate (rather than | |
| # uploading over the top) guarantees stale per-platform assets from a | |
| # previous build never linger. | |
| gh release delete "$EDGE_TAG" --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes 2>/dev/null || true | |
| # Retry create/upload so a transient API hiccup doesn't wedge the queue. | |
| n=0 | |
| until [ "$n" -ge 3 ]; do | |
| if gh release create "$EDGE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$RELEASE_COMMIT" \ | |
| --prerelease \ | |
| --title "Edge (latest trunk build)" \ | |
| --notes "Rolling pre-release built from the most recent trunk commit (${RELEASE_COMMIT}). Updated automatically on every merge; not a stable release." \ | |
| edge/*; then | |
| break | |
| fi | |
| n=$((n + 1)) | |
| echo "gh release create failed (attempt ${n}); retrying in 10s..." >&2 | |
| sleep 10 | |
| done | |
| if [ "$n" -ge 3 ]; then | |
| echo "Failed to publish edge release after 3 attempts." >&2 | |
| exit 1 | |
| fi |