Skip to content

ci: seed CodSpeed baseline on main pushes and workflow_dispatch #29

ci: seed CodSpeed baseline on main pushes and workflow_dispatch

ci: seed CodSpeed baseline on main pushes and workflow_dispatch #29

Workflow file for this run

name: PR checks
# Per-package parallel pipeline: each codec is built, tested, and benchmarked
# independently. The matrix mirrors what CircleCI was doing — every entry
# runs concurrently, any failure fails the workflow.
#
# CodSpeed's first-class GHA integration replaces the manual codspeed-bench
# job from CircleCI: `CodSpeedHQ/action@v3` installs valgrind, sets up
# instrumentation, and uploads results in one step.
on:
pull_request:
push:
# Run on every branch including main so each merge to main seeds a
# fresh CodSpeed baseline. Without a main run, PR comments stay stuck
# on "Congrats! CodSpeed is installed" with no before/after deltas.
branches:
- "**"
# workflow_dispatch lets CodSpeed trigger a backtest run from the
# dashboard (to seed initial perf data after the repo is connected).
workflow_dispatch:
# Cancel in-flight runs when a new push lands on the same PR / branch.
concurrency:
group: pr-checks-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write # CodSpeed action posts a sticky PR comment
id-token: write # OIDC token used by CodSpeedHQ/action for auth
jobs:
detect-changes:
# Build the matrix dynamically from the set of packages that actually
# changed since main. Mirrors CircleCI's --since main skip logic. On a
# PR where every README was touched (this branch) the list is the full
# 8 packages; on a docs-only PR the build matrix is empty.
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.list.outputs.packages }}
any: ${{ steps.list.outputs.any }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: list
name: List changed packages
env:
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
run: |
set -e
ALL=(charls libjpeg-turbo-8bit libjpeg-turbo-12bit openjpeg openjphjs little-endian big-endian dicom-codec)
# Baseline runs: on a manual dispatch or any commit landing on
# main, build/test/bench every package. The "diff vs main" trick
# only makes sense on PR/feature branches — when HEAD === main,
# `git diff origin/main..HEAD` is empty and would skip CodSpeed
# entirely, leaving the dashboard with no baseline data.
if [ "$EVENT_NAME" = "workflow_dispatch" ] || [ "$REF" = "refs/heads/main" ]; then
json=$(printf '%s\n' "${ALL[@]}" | jq -R . | jq -s -c .)
echo "Baseline run ($EVENT_NAME on $REF): forcing all packages"
echo "packages=$json" >> "$GITHUB_OUTPUT"
echo "any=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git fetch --no-tags --depth=50 origin main || true
BASE=$(git merge-base origin/main HEAD || echo "origin/main")
changed=()
for pkg in "${ALL[@]}"; do
if ! git diff --quiet "$BASE"..HEAD -- "packages/$pkg/"; then
changed+=("$pkg")
fi
done
if [ ${#changed[@]} -eq 0 ]; then
echo "No packages changed since $BASE."
echo 'packages=[]' >> "$GITHUB_OUTPUT"
echo "any=false" >> "$GITHUB_OUTPUT"
else
json=$(printf '%s\n' "${changed[@]}" | jq -R . | jq -s -c .)
echo "Changed packages: $json"
echo "packages=$json" >> "$GITHUB_OUTPUT"
echo "any=true" >> "$GITHUB_OUTPUT"
fi
build:
needs: detect-changes
if: needs.detect-changes.outputs.any == 'true'
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
runs-on: ubuntu-latest
container:
image: emscripten/emsdk:3.1.74
steps:
- name: Install yarn + cmake + C++ build deps
run: |
npm install --global yarn@1.22.22
apt-get update
apt-get -y install build-essential git
wget -qO- "https://cmake.org/files/v3.17/cmake-3.17.4-Linux-x86_64.tar.gz" \
| tar --strip-components=1 -xz -C /usr/local
apt-get autoremove -y
apt-get clean -y
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Allow git to operate on the workspace
# The container runs as root but the workspace is owned by the
# checkout action's user, which makes git complain about
# `dubious ownership`. Mark it safe.
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Init submodules for this package
run: |
if [ -d "packages/${{ matrix.package }}/extern" ]; then
git submodule update --init --recursive "packages/${{ matrix.package }}/extern"
else
echo "No extern/ submodule for ${{ matrix.package }}; skipping."
fi
- name: Restore yarn cache
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles('yarn.lock') }}
restore-keys: yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: cd "packages/${{ matrix.package }}" && yarn run build:ci
- name: Ensure dist exists (no-op packages still need a placeholder)
run: mkdir -p "packages/${{ matrix.package }}/dist"
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.package }}
path: packages/${{ matrix.package }}/dist
if-no-files-found: ignore
retention-days: 7
test:
needs: [detect-changes, build]
if: needs.detect-changes.outputs.any == 'true'
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Download all built dists
uses: actions/download-artifact@v4
with:
pattern: dist-*
path: tmp/
- name: Replay dists into packages/<pkg>/dist
# actions/download-artifact lands each artifact in tmp/<name>/.
# Move each into its proper packages/<pkg>/dist location so vitest
# finds them, mirroring how CircleCI workspace persist worked.
run: |
set -e
for d in tmp/dist-*; do
[ -d "$d" ] || continue
pkg=$(basename "$d" | sed 's/^dist-//')
mkdir -p "packages/$pkg/dist"
shopt -s dotglob nullglob
cp -r "$d"/* "packages/$pkg/dist/" 2>/dev/null || true
done
ls packages/*/dist 2>/dev/null | head
- name: Restore yarn cache
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles('yarn.lock') }}
restore-keys: yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test
run: cd "packages/${{ matrix.package }}" && yarn run test:ci
codspeed-bench:
needs: [detect-changes, build]
if: needs.detect-changes.outputs.any == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Download all built dists
uses: actions/download-artifact@v4
with:
pattern: dist-*
path: tmp/
- name: Replay dists into packages/<pkg>/dist
run: |
set -e
for d in tmp/dist-*; do
[ -d "$d" ] || continue
pkg=$(basename "$d" | sed 's/^dist-//')
mkdir -p "packages/$pkg/dist"
shopt -s dotglob nullglob
cp -r "$d"/* "packages/$pkg/dist/" 2>/dev/null || true
done
- name: Restore yarn cache
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles('yarn.lock') }}
restore-keys: yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run CodSpeed benchmarks
# CodSpeedHQ/action@v4 sets up CPU simulation (Cachegrind-based
# instruction counting on a modeled CPU + cache hierarchy), runs
# the inner command under valgrind, uploads to codspeed.io, and
# posts/updates a sticky PR comment with the per-bench deltas.
# Authenticates via GitHub OIDC (id-token: write above) so no
# CODSPEED_TOKEN secret is needed.
#
# mode: simulation (vs walltime) — we chose simulation because:
# - deterministic: <1% run-to-run drift (verified across 3
# runs of identical source)
# - free CI minutes (walltime needs CodSpeed macro-runners)
# - regression-detection signal is strong even though the
# headline numbers are MODELED instruction-time, not real
# wall-clock (JS-loop benches inflate 30-100x vs production
# V8 due to no JIT under Cachegrind; wasm decode kernels
# inflate ~5-15x; pure native ~1x)
# See BENCHMARKING.md at the repo root for the full measurement
# model, how to read the cold/warm bench split, and what the
# CodSpeed dashboard warnings mean.
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: yarn run bench