Skip to content

Monorepo

Monorepo #456

Workflow file for this run

name: Monorepo
permissions:
contents: read
env:
# Define supported runtime versions for matrix expansion once for the workflow.
NODE_VERSIONS_JSON: "[20,22,24]"
MONGODB_VERSIONS_JSON: '["7","8"]'
REDIS_VERSION: "7"
on:
push:
branches:
- main
pull_request:
branches:
- "*"
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
inputs:
run_all:
description: Force tests for every package
required: false
type: boolean
default: false
jobs:
setup:
name: Impacted packages
runs-on: ubuntu-latest
env:
# DO NOT CHANGE BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING
# DEFAULT_BRANCH tells the detector which branch to diff against when calculating impact.
DEFAULT_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.event.repository.default_branch || 'main' }}
# BASE_SHA narrows the diff to the merge-base (PR) or latest push SHA for branch builds.
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }}
# FORCE_ALL flips to true when the workflow_dispatch input is toggled so every package runs.
FORCE_ALL: ${{ (github.event_name == 'schedule' || inputs.run_all == true || inputs.run_all == 'true') && 'true' || 'false' }}
outputs:
matrix: ${{ steps.impact.outputs.matrix }}
runtime-matrix: ${{ steps.runtime-matrix.outputs.runtime_matrix }}
node-versions: ${{ steps.node-versions.outputs.node_versions }}
has-packages: ${{ steps.impact.outputs.has_packages }}
steps:
- name: Git checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Prepare merge-base context
run: git fetch origin "$DEFAULT_BRANCH" --depth=1 || true
- name: Detect impacted packages
id: impact
run: |
node .github/workflows/scripts/detect-impacted-packages.mjs > impact.json
echo "matrix=$(jq -c '.matrix' impact.json)" >> "$GITHUB_OUTPUT"
echo "has_packages=$(jq -r '.hasPackages' impact.json)" >> "$GITHUB_OUTPUT"
cat impact.json | jq '.'
- name: Expand runtime matrix
id: runtime-matrix
# Replace "forbidden" characters to prevent GitHub Actions parsing issues.
# It looks odd, but it's a safety measure for edge cases.
run: |
node .github/workflows/scripts/expand-runtime-matrix.mjs --impact impact.json > runtime-matrix.json
cat runtime-matrix.json | jq '.'
matrix_json=$(jq -c '.' runtime-matrix.json)
matrix_json=${matrix_json//'%'/'%25'}
matrix_json=${matrix_json//$'\n'/'%0A'}
matrix_json=${matrix_json//$'\r'/'%0D'}
echo "runtime_matrix=$matrix_json" >> "$GITHUB_OUTPUT"
- name: Export node versions
id: node-versions
run: echo "node_versions=${{ env.NODE_VERSIONS_JSON }}" >> "$GITHUB_OUTPUT"
shared-runtime:
name: Shared runtime
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.has-packages == 'true'
outputs:
docker-cache-key: ${{ steps.docker-key.outputs.key }}
steps:
- name: Git checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
- name: Grant private repositories access
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.AUTOMATIC_TRANSLATION_DEPLOYMENT_KEY }}
- name: Generate pnpm lockfile
run: pnpm install --lockfile-only --ignore-scripts
- name: Upload pnpm lockfile
uses: actions/upload-artifact@v4
with:
name: pnpm-lock
path: pnpm-lock.yaml
retention-days: 30
- name: Determine cache month
id: cache-month
run: echo "value=$(date -u +%Y-%m)" >> "$GITHUB_OUTPUT"
- name: Declare docker cache key
id: docker-key
run: |
month='${{ steps.cache-month.outputs.value }}'
mongo_suffix=$(jq -r '.[]' <<< '${{ env.MONGODB_VERSIONS_JSON }}' | paste -sd'-' -)
echo "key=docker-images-${month}-mongo${mongo_suffix}-redis${{ env.REDIS_VERSION }}" >> "$GITHUB_OUTPUT"
- name: Restore docker image cache
id: docker-cache
uses: actions/cache@v4
with:
path: .github/docker-cache
key: ${{ steps.docker-key.outputs.key }}
- name: Pull and save docker images
if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
mkdir -p .github/docker-cache
jq -r '.[]' <<< '${{ env.MONGODB_VERSIONS_JSON }}' | while read -r version; do
docker pull "mongo:${version}"
docker save "mongo:${version}" -o ".github/docker-cache/mongo-${version}.tar"
done
docker pull "redis:${{ env.REDIS_VERSION }}"
docker save "redis:${{ env.REDIS_VERSION }}" -o ".github/docker-cache/redis-${{ env.REDIS_VERSION }}.tar"
warm-sharp-cache:
name: Warm sharp/libvips cache (Node ${{ matrix.nodeVersion }})
runs-on: ubuntu-latest
needs:
- setup
- shared-runtime
if: needs.setup.outputs.has-packages == 'true'
strategy:
fail-fast: false
matrix:
nodeVersion: ${{ fromJson(needs.setup.outputs['node-versions']) }}
steps:
- name: Git checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Download pnpm lockfile
uses: actions/download-artifact@v4
with:
name: pnpm-lock
path: .
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
- name: Use Node.js ${{ matrix.nodeVersion }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.nodeVersion }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
# Provide headers so sharp can compile from source when
# prebuilt binaries fail to download.
# Disabled for now as it adds time for each job. If we
# keep seeing related issues we can re-enable it.
# - name: Install libvips build dependencies
# run: |
# sudo apt-get update
# sudo apt-get install -y --no-install-recommends libvips-dev
- name: Grant private repositories access
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.AUTOMATIC_TRANSLATION_DEPLOYMENT_KEY }}
- name: Cache sharp/libvips downloads
id: sharp-cache
uses: actions/cache@v4
with:
path: |
~/.npm/_libvips
key: sharp-libvips-${{ runner.os }}-node${{ matrix.nodeVersion }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
sharp-libvips-${{ runner.os }}-node${{ matrix.nodeVersion }}-
sharp-libvips-${{ runner.os }}-
- name: Warm cache by installing dependencies once
run: pnpm install --frozen-lockfile
- name: Debug sharp/libvips cache contents
if: always()
run: |
echo "-- ~/.npm/_libvips"
ls -alh ~/.npm/_libvips || true
du -sh ~/.npm/_libvips || true
package-tests:
name: ${{ format('{0} ({1}, {2})', matrix.package, matrix.nodeVersion, matrix.needsMongo && matrix.mongodbVersion || 'n/a') }}
needs:
- setup
- shared-runtime
- warm-sharp-cache
if: needs.setup.outputs.has-packages == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs['runtime-matrix']) }}
steps:
- name: Git checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Download pnpm lockfile
uses: actions/download-artifact@v4
with:
name: pnpm-lock
path: .
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
- name: Use Node.js ${{ matrix.nodeVersion }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.nodeVersion }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Cache sharp/libvips downloads
id: sharp-cache
uses: actions/cache@v4
with:
path: |
~/.npm/_libvips
key: sharp-libvips-${{ runner.os }}-node${{ matrix.nodeVersion }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
sharp-libvips-${{ runner.os }}-node${{ matrix.nodeVersion }}-
sharp-libvips-${{ runner.os }}-
- name: Grant private repositories access
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.AUTOMATIC_TRANSLATION_DEPLOYMENT_KEY }}
# Same safety net: ensures node-gyp builds succeed when
# sharp's prebuild-install hits the network fallback.
# Disabled for now as it adds time for each job. If we
# keep seeing related issues we can re-enable it.
# - name: Install libvips build dependencies
# run: |
# sudo apt-get update
# sudo apt-get install -y --no-install-recommends libvips-dev
- name: Restore docker image cache
id: docker-cache-restore
uses: actions/cache/restore@v4
if: matrix.needsMongo || matrix.needsRedis
with:
path: .github/docker-cache
key: ${{ needs.shared-runtime.outputs.docker-cache-key }}
- name: Load cached Mongo image
if: steps.docker-cache-restore.outputs.cache-hit == 'true' && matrix.needsMongo
run: docker load -i ".github/docker-cache/mongo-${{ matrix.mongodbVersion }}.tar"
- name: Load cached Redis image
if: steps.docker-cache-restore.outputs.cache-hit == 'true' && matrix.needsRedis
run: docker load -i .github/docker-cache/redis-${{ env.REDIS_VERSION }}.tar
- name: Pull Mongo image (cache miss)
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsMongo
run: docker pull mongo:${{ matrix.mongodbVersion }}
- name: Pull Redis image (cache miss)
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsRedis
run: docker pull redis:${{ env.REDIS_VERSION }}
- name: Start MongoDB
if: matrix.needsMongo
run: |
docker rm -f mongo >/dev/null 2>&1 || true
docker run -d \
--name mongo \
--publish 27017:27017 \
--health-cmd "mongosh --quiet --eval 'db.runCommand({ ping: 1 })'" \
--health-interval 5s \
--health-timeout 5s \
--health-retries 12 \
mongo:${{ matrix.mongodbVersion }}
echo "Waiting for MongoDB to report healthy..."
for attempt in $(seq 1 60); do
status=$(docker inspect --format='{{.State.Health.Status}}' mongo 2>/dev/null || echo "starting")
if [ "$status" = "healthy" ]; then
exit 0
fi
if [ "$status" = "unhealthy" ]; then
echo "MongoDB reported unhealthy" >&2
docker logs mongo
exit 1
fi
sleep 2
done
echo "MongoDB failed to become healthy in time" >&2
docker logs mongo
exit 1
- name: Start Redis
if: matrix.needsRedis
run: |
docker rm -f redis >/dev/null 2>&1 || true
docker run -d \
--name redis \
--publish 6379:6379 \
--health-cmd "redis-cli ping || exit 1" \
--health-interval 5s \
--health-timeout 5s \
--health-retries 12 \
redis:${{ env.REDIS_VERSION }}
echo "Waiting for Redis to report healthy..."
for attempt in $(seq 1 60); do
status=$(docker inspect --format='{{.State.Health.Status}}' redis 2>/dev/null || echo "starting")
if [ "$status" = "healthy" ]; then
exit 0
fi
if [ "$status" = "unhealthy" ]; then
echo "Redis reported unhealthy" >&2
docker logs redis
exit 1
fi
sleep 2
done
echo "Redis failed to respond in time" >&2
docker logs redis
exit 1
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
- name: Run package tests
run: pnpm run --filter "${{ matrix.package }}" --if-present test
env:
CI: true
# Yes we want import-export to test with automatic-translation
TEST_WITH_PRO: "1"
- name: Stop Redis
if: always() && matrix.needsRedis
run: docker rm -f redis || true
- name: Stop MongoDB
if: always() && matrix.needsMongo
run: docker rm -f mongo || true