Files
codebase-memory-mcp/.github/workflows/_smoke.yml
T
Martin Vogel 9bb891b669 ci: move macOS runners to macos-15 and pin the deployment target to 14.0
GitHub retires the macos-14 runner image on 2026-11-02, and its brownouts
already cancel our macos-14 jobs. Move every macos-14 runner reference to
macos-15 (arm64): the release darwin-arm64 build, the unix-macos14 and
tsan-mac test legs, the smoke-mac PR leg (its two matrix.os guards move in
lockstep), the release smoke, both soak legs, the memwaste event lane, the
bug-repro board, the cache warmer and the all-platform smoke. Lane names
stay unchanged: select-lanes.sh, ci-ok and the lane contracts key on them,
never on the runner label.

Two broad legs already ran on macos-15 (_test.yml BROAD_UNIX, _smoke.yml
BROAD_UNIX) and smoke.yml listed both images, so the moved legs would have
duplicated them. Those duplicates are dropped; PR CI keeps exactly the
same legs.

Without an explicit minimum, clang stamps the binary with the SDK's
default (LC_BUILD_VERSION minos), so a release built on macos-15 would
refuse to start on macOS 14. Makefile.cbm now folds
-mmacosx-version-min=14.0 into CC/CXX next to ARCHFLAGS for Apple targets
(detected from the compiler's target, not the host), so it reaches every
compile and link, vendored objects included, on every build path.

Proof on macOS 26.6 / SDK 26.5: the production binary built from
origin/main reports minos 26.0; with this change it reports minos 14.0,
with zero warnings under -Werror, no weak imports and an identical
undefined-symbol set. The test runner (direct make, no env.sh) is minos
14.0 too; platform and daemon_ipc suites pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-30 23:44:25 +01:00

372 lines
18 KiB
YAML

# Reusable: smoke test shipped release binaries on all platforms
name: Smoke
on:
workflow_call:
inputs:
broad_platforms:
description: 'Smoke the shipped binaries on the broad platform matrix (extra OS versions) instead of the core set'
type: boolean
default: false
permissions:
contents: read
jobs:
# Emit the platform matrices as JSON. The CORE set is the default (fast,
# unchanged); the BROAD set adds extra free runners (additional OS versions)
# that download the SAME shipped artifact for their goos/goarch and verify it
# runs on a wider range of OS versions. No new artifacts are built — broad
# legs reuse the exact binaries produced by _build.yml.
setup-matrix:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
unix: ${{ steps.set.outputs.unix }}
windows: ${{ steps.set.outputs.windows }}
portable: ${{ steps.set.outputs.portable }}
steps:
- name: Compute matrices
id: set
env:
BROAD: ${{ inputs.broad_platforms }}
run: |
CORE_UNIX='[
{"os":"ubuntu-latest","goos":"linux","goarch":"amd64"},
{"os":"ubuntu-24.04-arm","goos":"linux","goarch":"arm64"},
{"os":"macos-15","goos":"darwin","goarch":"arm64"},
{"os":"macos-15-intel","goos":"darwin","goarch":"amd64"}
]'
# Broad legs reuse existing goos/goarch artifacts on additional OS
# versions to widen the run-anywhere signal without building new targets.
# Broad legs are REQUIRED gates (no optional / continue-on-error):
# every smoke leg must pass. No `optional` flags anywhere.
#
# NOTE: the *dynamic* linux binary links glibc 2.38+ and cannot run on
# older distros by design — older-glibc coverage is the -portable (static)
# binary's job, exercised green by the smoke-linux-portable broad legs
# (ubuntu-22.04 / 22.04-arm). So the dynamic broad legs stay on
# forward-compatible OSes only (macOS); running the dynamic binary on
# ubuntu-22.04 would fail Phase 1 (glibc too old), not a real regression.
# Empty since the move off macos-14 (retired 2026-11-02): the darwin-arm64
# artifact is now built AND smoked on macos-15, the core leg, which was
# this broad leg.
BROAD_UNIX='[]'
CORE_WIN='[{"os":"windows-latest","arch":"amd64"}]'
# windows-11-arm now runs the NATIVE arm64 binary (build-windows-arm64),
# not the x86_64 binary under emulation — we smoke the artifact we ship.
BROAD_WIN='[{"os":"windows-2025","arch":"amd64"},{"os":"windows-11-arm","arch":"arm64"}]'
CORE_PORTABLE='[
{"arch":"amd64","runner":"ubuntu-latest"},
{"arch":"arm64","runner":"ubuntu-24.04-arm"}
]'
BROAD_PORTABLE='[
{"arch":"amd64","runner":"ubuntu-22.04"},
{"arch":"arm64","runner":"ubuntu-22.04-arm"}
]'
if [ "$BROAD" = "true" ]; then
UNIX=$(jq -cn --argjson a "$CORE_UNIX" --argjson b "$BROAD_UNIX" '$a + $b')
WIN=$(jq -cn --argjson a "$CORE_WIN" --argjson b "$BROAD_WIN" '$a + $b')
PORTABLE=$(jq -cn --argjson a "$CORE_PORTABLE" --argjson b "$BROAD_PORTABLE" '$a + $b')
else
UNIX=$(jq -cn --argjson a "$CORE_UNIX" '$a')
WIN=$(jq -cn --argjson a "$CORE_WIN" '$a')
PORTABLE=$(jq -cn --argjson a "$CORE_PORTABLE" '$a')
fi
# One composition ships; the wrappers assert the embedded UI themselves
# (SMOKE_REQUIRE_UI in scripts/ci/smoke-artifact.sh and below).
UNIX_M=$(jq -cn --argjson a "$UNIX" '{include:$a}')
WIN_M=$(jq -cn --argjson a "$WIN" '{include:$a}')
PORTABLE_M=$(jq -cn --argjson a "$PORTABLE" '{include:$a}')
echo "unix=$UNIX_M" >> "$GITHUB_OUTPUT"
echo "windows=$WIN_M" >> "$GITHUB_OUTPUT"
echo "portable=$PORTABLE_M" >> "$GITHUB_OUTPUT"
smoke-unix:
needs: setup-matrix
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup-matrix.outputs.unix) }}
runs-on: ${{ matrix.os }}
# 30, not 15: the unified smoke legitimately runs MORE (artifact mode,
# real download/update phases, a Phase 15 that actually executes), and
# v4's PASSING legs already took 12-14 min — v5 lost 13 green-path jobs
# to the 15-min wall (GitHub reports job timeouts as 'cancelled').
# Budget doctrine: the timeout sits above the worst case, twice over.
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
# macOS runner images have none on PATH.
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: pkg/go/go.mod
cache: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Extract release artifact
run: |
mkdir -p "$RUNNER_TEMP/cbm-artifact"
tar -xzf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C "$RUNNER_TEMP/cbm-artifact"
chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
# ONE harness for every venue. This is the same wrapper local CI and PR CI
# run, handed the DOWNLOADED artifact instead of a local build: it stages
# the release fixture, serves it on a kernel-assigned port, and runs
# smoke-test.sh under the isolated profile/TEMP the other venues use.
# It replaces an inline copy of that staging logic which had drifted from
# the wrapper (no env isolation, no user-PATH guard, fixed port).
- name: Smoke test (shared wrapper)
run: |
scripts/smoke-local.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
env:
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
SMOKE_REQUIRE_UI: "1"
SMOKE_REQUIRE_WRAPPERS: "1"
- name: Security audits
run: |
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-strings.sh "$BIN"
scripts/security-install.sh "$BIN"
scripts/security-network.sh "$BIN"
- name: MCP robustness test (selected linux-amd64 runtime)
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: |
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-fuzz.sh "$BIN"
scripts/security-fuzz-random.sh "$BIN" 60
- name: ClamAV scan (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
# apt auto-starts the clamav-freshclam daemon, which holds a lock on
# freshclam's log/db; stop it so the manual freshclam below can run
# (else: "Failed to lock the log file ... Resource temporarily unavailable").
sudo systemctl stop clamav-freshclam 2>/dev/null || true
sudo sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf 2>/dev/null || true
grep -q "DatabaseMirror" /etc/clamav/freshclam.conf 2>/dev/null || \
echo "DatabaseMirror database.clamav.net" | sudo tee -a /etc/clamav/freshclam.conf > /dev/null
sudo freshclam --quiet
clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
- name: ClamAV scan (macOS)
if: startsWith(matrix.os, 'macos')
run: |
brew install clamav > /dev/null 2>&1
CLAMAV_ETC=$(brew --prefix)/etc/clamav
if [ ! -f "$CLAMAV_ETC/freshclam.conf" ]; then
cp "$CLAMAV_ETC/freshclam.conf.sample" "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
sed -i '' 's/^Example/#Example/' "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
echo "DatabaseMirror database.clamav.net" >> "$CLAMAV_ETC/freshclam.conf"
fi
freshclam --quiet --no-warnings 2>/dev/null || freshclam --quiet 2>/dev/null || echo "WARNING: freshclam update failed"
clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
smoke-windows:
needs: setup-matrix
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup-matrix.outputs.windows) }}
runs-on: ${{ matrix.os }}
# 30, not 15: the unified smoke legitimately runs MORE (artifact mode,
# real download/update phases, a Phase 15 that actually executes), and
# v4's PASSING legs already took 12-14 min — v5 lost 13 green-path jobs
# to the 15-min wall (GitHub reports job timeouts as 'cancelled').
# Budget doctrine: the timeout sits above the worst case, twice over.
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
# MSYS2 shell below inherits it (path-type: inherit).
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: pkg/go/go.mod
cache: false
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
with:
msystem: ${{ matrix.arch == 'arm64' && 'CLANGARM64' || 'CLANG64' }}
path-type: inherit
install: >-
mingw-w64-clang-${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }}-python3
mingw-w64-clang-${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }}-curl
unzip
zip
coreutils
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-windows-${{ matrix.arch }}
- name: Extract release artifact
shell: msys2 {0}
run: |
ARCH=${{ matrix.arch }}
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
mkdir -p "$ARTIFACT_DIR"
unzip -o "codebase-memory-mcp-windows-${ARCH}.zip" -d "$ARTIFACT_DIR"
test -s "$ARTIFACT_DIR/codebase-memory-mcp.exe"
# ONE executable per runtime set: a payload sibling would mean the
# removed launcher split came back.
test ! -e "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
# Pre-flight the image from a profile-rooted directory: the shared
# runner workspace is deliberately not a valid install root.
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")"
# Best-effort cleanup with one retry: Windows holds the just-exited
# image's file busy for ~100ms (section teardown / first-touch AV),
# and a temp dir on an ephemeral runner must never fail the job.
trap 'rm -rf "$LAUNCH_DIR" 2>/dev/null || { sleep 2; rm -rf "$LAUNCH_DIR" 2>/dev/null || true; }' EXIT
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" "$LAUNCH_DIR/"
"$LAUNCH_DIR/codebase-memory-mcp.exe" --version
# ONE harness for every venue: the same script win.sh smoke-install and
# pr.yml run, handed the DOWNLOADED artifact instead of a local build. It
# stages the release fixture under a disposable profile root, serves it on
# a kernel-assigned port, guards AND verifies the user-PATH registry
# mutation, and neutralizes every agent-config destination before running
# smoke-test.sh. It replaces an inline copy of that staging which had none
# of the isolation and never verified the PATH mutation it performed.
- name: Smoke test (shared wrapper)
shell: msys2 {0}
env:
SMOKE_ARCH: ${{ matrix.arch }}
SMOKE_REQUIRE_UI: "1"
SMOKE_REQUIRE_WRAPPERS: "1"
run: |
export CBM_SMOKE_ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
bash test-infrastructure/vm/vm-smoke.sh
- name: Security audits
shell: msys2 {0}
run: |
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.exe"
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")"
# Best-effort cleanup with one retry (see the version preflight).
trap 'rm -rf "$SECURITY_DIR" 2>/dev/null || { sleep 2; rm -rf "$SECURITY_DIR" 2>/dev/null || true; }' EXIT
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" "$SECURITY_DIR/"
TMPDIR="$SECURITY_DIR" \
scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"
- name: Windows Defender scan
shell: pwsh
run: |
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
$artifactDir = Join-Path $env:RUNNER_TEMP "cbm-artifact"
foreach ($binary in @("codebase-memory-mcp.exe")) {
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File (Join-Path $artifactDir $binary) -DisableRemediation
$code = $LASTEXITCODE
Write-Host $result
# MpCmdRun -Scan exit codes: 0 = clean, 2 = threat found. Any OTHER non-zero
# means the scan engine could not run at all (e.g. hr=0x800106ba: the Defender
# antimalware service is unavailable on the runner) — that is NOT a detection.
# Fail soft on an engine failure so a transient runner-side AV outage can't
# false-fail a release; only a real detection (exit 2) hard-blocks.
if ($code -eq 2) {
Write-Host "BLOCKED: Windows Defender flagged $binary!"; exit 1
} elseif ($code -ne 0) {
Write-Host "::warning::Windows Defender scan could not run for $binary (exit $code) - skipping AV gate on this runner"
} else {
Write-Host "=== Windows Defender: $binary clean ==="
}
}
smoke-linux-portable:
needs: setup-matrix
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup-matrix.outputs.portable) }}
runs-on: ${{ matrix.runner }}
# 30, not 15: the unified smoke legitimately runs MORE (artifact mode,
# real download/update phases, a Phase 15 that actually executes), and
# v4's PASSING legs already took 12-14 min — v5 lost 13 green-path jobs
# to the 15-min wall (GitHub reports job timeouts as 'cancelled').
# Budget doctrine: the timeout sits above the worst case, twice over.
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper.
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: pkg/go/go.mod
cache: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-linux-${{ matrix.arch }}-portable
- name: Extract release artifact
run: |
mkdir -p "$RUNNER_TEMP/cbm-artifact"
tar -xzf codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz \
-C "$RUNNER_TEMP/cbm-artifact"
chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
# Same wrapper as every other venue. This leg previously called
# smoke-test.sh with no fixture server, which silently SKIPPED the
# download/checksum/install-script phases (12-13) — the portable binary is
# what every linux install and update actually delivers, so those are
# exactly the phases it most needs to run.
- name: Smoke test (shared wrapper)
run: |
scripts/smoke-local.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
env:
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
SMOKE_REQUIRE_UI: "1"
SMOKE_REQUIRE_WRAPPERS: "1"
# The -portable binary is what all linux install/update paths now deliver;
# it MUST start on old glibc (Debian 11 / RHEL 8 / Ubuntu 20.04). Runs it
# in debian:bullseye (glibc 2.31) — the standard dynamic binary would fail
# here with `GLIBC_2.38 not found`.
- name: Old-glibc compatibility
run: scripts/ci/check-glibc-compat.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
- name: Security audits
run: |
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-strings.sh "$BIN"
scripts/security-install.sh "$BIN"
scripts/security-network.sh "$BIN"
# ── Packaging check (non-gating) ──────────────────────────────────
# Builds pkg/glama/Dockerfile — the image Glama builds to score the
# server — and runs an MCP initialize + tools/list handshake to confirm the
# containerized stdio server still starts and introspects. Guards the Glama
# listing integration against drift (Dockerfile breakage, release-asset
# renames, introspection regressions).
#
# Also tests the brew tap/install flow.
#
# continue-on-error: a broken *directory* image must never block a release —
# the shipped product binaries are unaffected. The red X is the signal to fix
# the integration, not a release gate.
smoke-packages:
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build Glama image and verify MCP introspection
run: bash pkg/glama/verify.sh
- name: Test homebrew installation
env:
HOMEBREW_NO_AUTO_UPDATE: 1
run: |
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew tap deusdata/codebase-memory-mcp "$GITHUB_WORKSPACE"
brew trust deusdata/codebase-memory-mcp
brew install codebase-memory-mcp
codebase-memory-mcp --version