Skip to content

ci: stop node_modules cache restore-keys poisoning #9

ci: stop node_modules cache restore-keys poisoning

ci: stop node_modules cache restore-keys poisoning #9

Workflow file for this run

# MDK private monorepo (mdk-prv): backend/core/*, backend/workers/**, ui (all npm).
# Aligned with MiningOS CI Checks: per-package lockfile check, node cache actions, parallel jobs, PR audits.
name: CI Checks
on:
push:
branches: [main, develop, staging, "release/**"]
pull_request:
branches: [main, develop, staging, "release/**"]
types: [opened, reopened, synchronize]
workflow_dispatch:
inputs:
target:
description: "Which pipeline to run"
required: true
default: auto
type: choice
options:
- auto
- ui
- core
- workers
- all
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "lts/*"
UI_DIR: ui
jobs:
detect-target:
name: Detect changed areas
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
run_ui: ${{ steps.flags.outputs.run_ui }}
run_core: ${{ steps.flags.outputs.run_core }}
run_workers: ${{ steps.flags.outputs.run_workers }}
reason: ${{ steps.flags.outputs.reason }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Detect changed paths
id: changed
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
ui=false; core=false; workers=false
if [ "$EVENT_NAME" != "workflow_dispatch" ]; then
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
git fetch origin main --depth=1 2>/dev/null || true
BASE_SHA=$(git merge-base HEAD origin/main 2>/dev/null || echo "")
fi
[ -n "$BASE_SHA" ] && CHANGED=$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}") || CHANGED=""
echo "$CHANGED" | grep -q "^ui/" && ui=true || true
echo "$CHANGED" | grep -q "^backend/core/" && core=true || true
echo "$CHANGED" | grep -q "^backend/workers/" && workers=true || true
fi
echo "ui=$ui" >> "$GITHUB_OUTPUT"
echo "core=$core" >> "$GITHUB_OUTPUT"
echo "workers=$workers" >> "$GITHUB_OUTPUT"
- name: Resolve which pipelines should run
id: flags
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.target || 'auto' }}
UI_CHANGED: ${{ steps.changed.outputs.ui }}
CORE_CHANGED: ${{ steps.changed.outputs.core }}
WORKERS_CHANGED: ${{ steps.changed.outputs.workers }}
run: |
run_ui=false
run_core=false
run_workers=false
reason=""
has_ui_lock=false
[ -f "${{ env.UI_DIR }}/package-lock.json" ] && has_ui_lock=true
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$INPUT_TARGET" != "auto" ]; then
case "$INPUT_TARGET" in
ui) run_ui=true; run_core=false; run_workers=false; reason="manual=ui" ;;
core) run_ui=false; run_core=true; run_workers=false; reason="manual=core" ;;
workers) run_ui=false; run_core=false; run_workers=true; reason="manual=workers" ;;
all) run_ui=true; run_core=true; run_workers=true; reason="manual=all" ;;
*) run_ui=true; run_core=true; run_workers=true; reason="manual=unknown-fallback-all" ;;
esac
else
if [ "$UI_CHANGED" = "true" ] && [ "$has_ui_lock" = "true" ]; then run_ui=true; fi
if [ "$CORE_CHANGED" = "true" ]; then run_core=true; fi
if [ "$WORKERS_CHANGED" = "true" ]; then run_workers=true; fi
if [ "$run_ui" = "false" ] && [ "$run_core" = "false" ] && [ "$run_workers" = "false" ]; then
run_ui=$([ "$has_ui_lock" = "true" ] && echo true || echo false)
run_core=true
run_workers=true
reason="auto=no-targeted-change->run-core-workers"
else
reason="auto=changed-paths"
fi
fi
echo "run_ui=$run_ui" >> "$GITHUB_OUTPUT"
echo "run_core=$run_core" >> "$GITHUB_OUTPUT"
echo "run_workers=$run_workers" >> "$GITHUB_OUTPUT"
echo "reason=$reason" >> "$GITHUB_OUTPUT"
echo "Selected: ui=$run_ui core=$run_core workers=$run_workers ($reason)"
list-workers:
name: List worker packages
runs-on: ubuntu-latest
needs: [detect-target]
if: needs.detect-target.outputs.run_workers == 'true'
outputs:
dirs: ${{ steps.list.outputs.dirs }}
buildable_dirs: ${{ steps.list.outputs.buildable_dirs }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Discover worker package directories
id: list
shell: bash
run: |
set -euo pipefail
mapfile -t dirs < <(
find backend/workers -name package.json -not -path '*/node_modules/*' \
| xargs -I{} dirname {} | sort -u
)
if [ "${#dirs[@]}" -eq 0 ]; then
echo "::error::No package.json under backend/workers/"
exit 1
fi
json=$(printf '%s\n' "${dirs[@]}" | jq -R . | jq -s -c .)
echo "dirs=${json}" >> "$GITHUB_OUTPUT"
echo "Worker packages: ${json}"
buildable=()
for dir in "${dirs[@]}"; do
node -e "const s=require('./${dir}/package.json').scripts||{}; process.exit(s.build?0:1)" 2>/dev/null \
&& buildable+=("$dir") || true
done
if [ "${#buildable[@]}" -gt 0 ]; then
buildable_json=$(printf '%s\n' "${buildable[@]}" | jq -R . | jq -s -c .)
else
buildable_json='[]'
fi
echo "buildable_dirs=${buildable_json}" >> "$GITHUB_OUTPUT"
echo "Buildable worker packages: ${buildable_json}"
list-core:
name: List core packages
runs-on: ubuntu-latest
needs: [detect-target]
if: needs.detect-target.outputs.run_core == 'true'
outputs:
dirs: ${{ steps.list.outputs.dirs }}
buildable_dirs: ${{ steps.list.outputs.buildable_dirs }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Discover core package directories
id: list
shell: bash
run: |
set -euo pipefail
mapfile -t dirs < <(
find backend/core -name package.json -not -path '*/node_modules/*' \
| xargs -I{} dirname {} | sort -u
)
if [ "${#dirs[@]}" -eq 0 ]; then
echo "::error::No package.json under backend/core/"
exit 1
fi
json=$(printf '%s\n' "${dirs[@]}" | jq -R . | jq -s -c .)
echo "dirs=${json}" >> "$GITHUB_OUTPUT"
echo "Core packages: ${json}"
buildable=()
for dir in "${dirs[@]}"; do
node -e "const s=require('./${dir}/package.json').scripts||{}; process.exit(s.build?0:1)" 2>/dev/null \
&& buildable+=("$dir") || true
done
if [ "${#buildable[@]}" -gt 0 ]; then
buildable_json=$(printf '%s\n' "${buildable[@]}" | jq -R . | jq -s -c .)
else
buildable_json='[]'
fi
echo "buildable_dirs=${buildable_json}" >> "$GITHUB_OUTPUT"
echo "Buildable core packages: ${buildable_json}"
setup-ui:
name: 📦 Setup UI (ui)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target]
if: needs.detect-target.outputs.run_ui == 'true'
outputs:
cache-key: ${{ steps.setup-deps.outputs.cache-key }}
lockfile-outcome: ${{ steps.lockfile.outcome }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Lockfile consistency check
id: lockfile
shell: bash
working-directory: ${{ env.UI_DIR }}
run: |
set -euo pipefail
if [ ! -f package-lock.json ]; then
echo "No package-lock.json; skipping lockfile check"
exit 0
fi
npm install --package-lock-only --ignore-scripts --no-audit --no-fund
git -C "${GITHUB_WORKSPACE}" diff --exit-code "${{ env.UI_DIR }}/package-lock.json" || (
echo "::error::package-lock.json is out of sync with package.json."
echo "::error::Regenerate with: npm install --package-lock-only --ignore-scripts --no-audit --no-fund"
exit 1
)
- name: Install dependencies and cache node_modules
id: setup-deps
uses: ./.github/actions/node-setup-cache
with:
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
lint-ui:
name: ✨ Lint UI
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [detect-target, setup-ui]
if: needs.detect-target.outputs.run_ui == 'true'
defaults:
run:
working-directory: ${{ env.UI_DIR }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- run: npm run lint --if-present
typecheck-ui:
name: 🔎 Typecheck UI
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, setup-ui]
if: needs.detect-target.outputs.run_ui == 'true'
defaults:
run:
working-directory: ${{ env.UI_DIR }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- run: npm run typecheck --if-present
# 🧪 Test UI — sharded matrix.
# react-devkit is the bottleneck (~13 min, 296 test files), so it is split
# across 4 parallel runners via `vitest --shard`. Each shard writes a blob
# report; the coverage-ui job merges them and enforces thresholds. The smaller
# packages (ui-core, react-adapter, cli) run once on shard 1 via turbo.
test-ui:
name: 🧪 Test UI (shard ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, setup-ui]
if: needs.detect-target.outputs.run_ui == 'true'
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
NODE_OPTIONS: --max-old-space-size=4096
defaults:
run:
working-directory: ${{ env.UI_DIR }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- name: Cache Turbo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ env.UI_DIR }}/.turbo
key: turbo-ui-test-${{ runner.os }}-${{ hashFiles('ui/package-lock.json') }}-${{ github.sha }}-shard-${{ matrix.shard }}
restore-keys: |
turbo-ui-${{ runner.os }}-${{ hashFiles('ui/package-lock.json') }}-
turbo-ui-${{ runner.os }}-
- name: Cache Vitest transforms
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
# All tested packages set `cacheDir: '.cache'` in vitest.config.js.
path: |
ui/packages/react-devkit/.cache
ui/packages/ui-core/.cache
ui/packages/react-adapter/.cache
ui/packages/cli/.cache
key: vitest-ui-${{ runner.os }}-${{ hashFiles('ui/packages/**/*.ts', 'ui/packages/**/*.tsx', 'ui/packages/**/vitest.config.js') }}-shard-${{ matrix.shard }}
restore-keys: |
vitest-ui-${{ runner.os }}-
- name: Restore react-devkit build from Turbo cache
run: |
# node_modules is restored with --ignore-scripts, so workspace dist/
# outputs are absent; vitest needs them to resolve sibling packages.
npx turbo run build --filter=@tetherto/mdk-react-devkit
- name: Test smaller packages (shard 1 only)
if: matrix.shard == 1
# Only react-devkit is sharded; the rest run here once. Packages without
# a test:coverage task (fonts, catalog app) are skipped by turbo.
run: |
npx turbo run test:coverage --filter='!@tetherto/mdk-react-devkit'
- name: Test react-devkit shard ${{ matrix.shard }}/4
working-directory: ${{ env.UI_DIR }}/packages/react-devkit
# Thresholds are disabled per-shard (each shard covers only a subset of
# files); they are enforced on the merged report in the coverage-ui job.
run: |
npx vitest run \
--shard=${{ matrix.shard }}/4 \
--coverage \
--reporter=blob \
--coverage.thresholds.lines=0 \
--coverage.thresholds.functions=0 \
--coverage.thresholds.branches=0 \
--coverage.thresholds.statements=0
- name: Upload blob report
uses: actions/upload-artifact@v4
with:
name: ui-blob-report-${{ matrix.shard }}
path: ${{ env.UI_DIR }}/packages/react-devkit/.vitest-reports/
retention-days: 1
if-no-files-found: error
include-hidden-files: true
coverage-ui:
name: 📊 Coverage Merge UI
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [detect-target, setup-ui, test-ui]
if: needs.detect-target.outputs.run_ui == 'true'
defaults:
run:
working-directory: ${{ env.UI_DIR }}/packages/react-devkit
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- name: Download all blob reports
uses: actions/download-artifact@v4
with:
path: ${{ env.UI_DIR }}/packages/react-devkit/.vitest-reports
pattern: ui-blob-report-*
merge-multiple: true
- name: Merge coverage and enforce thresholds
run: |
ls -la .vitest-reports/
npx vitest run --mergeReports --coverage --coverage.reporter=text-summary --reporter=default
build-ui:
name: 🏗️ Build UI
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [detect-target, setup-ui]
if: needs.detect-target.outputs.run_ui == 'true'
defaults:
run:
working-directory: ${{ env.UI_DIR }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- run: npm run build --if-present
setup-core:
name: 📦 Setup Core (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-core]
if: needs.detect-target.outputs.run_core == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.dirs) }}
outputs:
cache-key: ${{ steps.setup-deps.outputs.cache-key }}
lockfile-outcome: ${{ steps.lockfile.outcome }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Lockfile consistency check
id: lockfile
shell: bash
working-directory: ${{ matrix.dir }}
run: |
set -euo pipefail
if [ ! -f package-lock.json ]; then
echo "No package-lock.json; skipping lockfile check"
exit 0
fi
npm install --package-lock-only --ignore-scripts --no-audit --no-fund
git -C "${GITHUB_WORKSPACE}" diff --exit-code "${{ matrix.dir }}/package-lock.json" || (
echo "::error::package-lock.json is out of sync with package.json in ${{ matrix.dir }}."
echo "::error::Regenerate with: npm install --package-lock-only --ignore-scripts --no-audit --no-fund"
exit 1
)
- name: Install dependencies and cache node_modules
id: setup-deps
uses: ./.github/actions/node-setup-cache
with:
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
lint-core:
name: ✨ Lint Core (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [detect-target, list-core, setup-core]
if: needs.detect-target.outputs.run_core == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-core.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run lint --if-present
typecheck-core:
name: 🔎 Typecheck Core (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-core, setup-core]
if: needs.detect-target.outputs.run_core == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-core.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run typecheck --if-present
test-core:
name: 🧪 Test Core (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [detect-target, list-core, setup-core]
if: needs.detect-target.outputs.run_core == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-core.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- name: Install cross-package dependencies
shell: bash
working-directory: ${{ github.workspace }}
run: |
# Core packages use relative requires into sibling core packages
# (e.g. mdk -> ork) and into backend/workers/** (base, lib,
# http.node.wrk, containers). Each test runner only restores the
# matrix package's node_modules, so the reachable sibling packages
# must be installed here too.
find backend/core backend/workers -name package.json \
-not -path '*/node_modules/*' -not -path '*/examples/*' \
| xargs -I{} dirname {} \
| grep -v "^${{ matrix.dir }}$" \
| while read -r dir; do
echo "Installing deps: $dir"
if [ -f "$dir/package-lock.json" ]; then
(cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund)
else
(cd "$dir" && npm install --ignore-scripts --no-audit --no-fund)
fi
done
- run: npm run test:coverage --if-present || npm test --if-present
build-core:
name: 🏗️ Build Core (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [detect-target, list-core, setup-core]
if: needs.detect-target.outputs.run_core == 'true' && needs.list-core.outputs.buildable_dirs != '[]'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.buildable_dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-core.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run build
setup-workers:
name: 📦 Setup Worker (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-workers]
if: needs.detect-target.outputs.run_workers == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.dirs) }}
outputs:
cache-key: ${{ steps.setup-deps.outputs.cache-key }}
lockfile-outcome: ${{ steps.lockfile.outcome }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Lockfile consistency check
id: lockfile
shell: bash
working-directory: ${{ matrix.dir }}
run: |
set -euo pipefail
if [ ! -f package-lock.json ]; then
echo "No package-lock.json; skipping lockfile check"
exit 0
fi
npm install --package-lock-only --ignore-scripts --no-audit --no-fund
git -C "${GITHUB_WORKSPACE}" diff --exit-code "${{ matrix.dir }}/package-lock.json" || (
echo "::error::package-lock.json is out of sync with package.json in ${{ matrix.dir }}."
echo "::error::Regenerate with: npm install --package-lock-only --ignore-scripts --no-audit --no-fund"
exit 1
)
- name: Install dependencies and cache node_modules
id: setup-deps
uses: ./.github/actions/node-setup-cache
with:
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
lint-workers:
name: ✨ Lint Worker (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [detect-target, list-workers, setup-workers]
if: needs.detect-target.outputs.run_workers == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-workers.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run lint --if-present
typecheck-workers:
name: 🔎 Typecheck Worker (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-workers, setup-workers]
if: needs.detect-target.outputs.run_workers == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-workers.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run typecheck --if-present
test-workers:
name: 🧪 Test Worker (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [detect-target, list-workers, setup-workers]
if: needs.detect-target.outputs.run_workers == 'true'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-workers.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- name: Install shared base package dependencies
shell: bash
working-directory: ${{ github.workspace }}
run: |
# Worker packages use relative requires into sibling base packages.
# Each test runner only restores the matrix package's node_modules, so
# shared packages must be installed here too.
find backend/workers -name package.json -not -path '*/node_modules/*' \
| xargs -I{} dirname {} \
| grep -E '(/base$|/base/)' \
| grep -v "^${{ matrix.dir }}$" \
| while read -r dir; do
echo "Installing deps: $dir"
(cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund)
done
for dir in backend/core/mock-control-service; do
[ -f "$dir/package.json" ] && \
(cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund) || true
done
- run: npm run test:coverage --if-present || npm test --if-present
build-workers:
name: 🏗️ Build Worker (${{ matrix.dir }})
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [detect-target, list-workers, setup-workers]
if: needs.detect-target.outputs.run_workers == 'true' && needs.list-workers.outputs.buildable_dirs != '[]'
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.buildable_dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-workers.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- run: npm run build
security_checks:
name: 🔐 Security Checks
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: moderate
vulnerability-check: true
license-check: true
security_audit_ui:
name: 🔐 Security Audit UI
if: github.event_name == 'pull_request' && needs.detect-target.outputs.run_ui == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, setup-ui]
permissions:
contents: read
defaults:
run:
working-directory: ${{ env.UI_DIR }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-ui.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ env.UI_DIR }}
- name: Security audit (npm)
run: |
set +e
npm audit signatures 2>&1
SIGNATURES_EXIT=$?
npm audit --audit-level=moderate 2>&1
MODERATE_EXIT=$?
npm exec --yes -- audit-ci@7 --config "${GITHUB_WORKSPACE}/.github/scripts/audit-ci.jsonc" 2>&1
HIGH_EXIT=$?
set -e
[ $SIGNATURES_EXIT -eq 0 ] && SIGNATURES_STATUS="PASS" || SIGNATURES_STATUS="WARN"
[ $MODERATE_EXIT -eq 0 ] && MODERATE_STATUS="PASS" || MODERATE_STATUS="WARN"
[ $HIGH_EXIT -eq 0 ] && HIGH_STATUS="PASS" || HIGH_STATUS="WARN"
# Only emit a summary block when the audit found something; a clean
# PR should not produce a wall of PASS tables.
if [ $MODERATE_EXIT -ne 0 ] || [ $HIGH_EXIT -ne 0 ]; then
AUD_JSON="${RUNNER_TEMP:-/tmp}/npm-audit-ui.json"
npm audit --json > "${AUD_JSON}" 2>/dev/null || true
{
echo "## Security Audit — UI (\`${{ env.UI_DIR }}\`)"
echo ""
echo "| Signatures | Moderate | High/Critical (audit-ci) |"
echo "|---|---|---|"
echo "| ${SIGNATURES_STATUS} | ${MODERATE_STATUS} | ${HIGH_STATUS} |"
} >> "$GITHUB_STEP_SUMMARY"
bash "${GITHUB_WORKSPACE}/.github/scripts/github-summary-npm-audit-high-critical.sh" "${AUD_JSON}" >> "$GITHUB_STEP_SUMMARY" || true
fi
[ $HIGH_EXIT -eq 0 ]
security_audit_core:
name: 🔐 Security Audit Core (${{ matrix.dir }})
if: github.event_name == 'pull_request' && needs.detect-target.outputs.run_core == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-core, setup-core]
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-core.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-core.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- name: Security audit (npm)
run: |
set +e
npm audit signatures 2>&1
SIGNATURES_EXIT=$?
npm audit --audit-level=moderate 2>&1
MODERATE_EXIT=$?
npm exec --yes -- audit-ci@7 --config "${GITHUB_WORKSPACE}/.github/scripts/audit-ci.jsonc" 2>&1
HIGH_EXIT=$?
set -e
[ $SIGNATURES_EXIT -eq 0 ] && SIGNATURES_STATUS="PASS" || SIGNATURES_STATUS="WARN"
[ $MODERATE_EXIT -eq 0 ] && MODERATE_STATUS="PASS" || MODERATE_STATUS="WARN"
[ $HIGH_EXIT -eq 0 ] && HIGH_STATUS="PASS" || HIGH_STATUS="WARN"
# Only emit a per-package summary block when the audit found
# something; a clean PR should not produce a wall of PASS tables.
if [ $MODERATE_EXIT -ne 0 ] || [ $HIGH_EXIT -ne 0 ]; then
AUD_JSON="${RUNNER_TEMP:-/tmp}/npm-audit-${{ matrix.dir }}.json"
npm audit --json > "${AUD_JSON}" 2>/dev/null || true
{
echo "## Security Audit — Core (\`${{ matrix.dir }}\`)"
echo ""
echo "| Signatures | Moderate | High/Critical (audit-ci) |"
echo "|---|---|---|"
echo "| ${SIGNATURES_STATUS} | ${MODERATE_STATUS} | ${HIGH_STATUS} |"
} >> "$GITHUB_STEP_SUMMARY"
bash "${GITHUB_WORKSPACE}/.github/scripts/github-summary-npm-audit-high-critical.sh" "${AUD_JSON}" >> "$GITHUB_STEP_SUMMARY" || true
fi
[ $HIGH_EXIT -eq 0 ]
security_audit_workers:
name: 🔐 Security Audit Worker (${{ matrix.dir }})
if: github.event_name == 'pull_request' && needs.detect-target.outputs.run_workers == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-target, list-workers, setup-workers]
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.list-workers.outputs.dirs) }}
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup-workers.outputs.cache-key }}
node-version: ${{ env.NODE_VERSION }}
working-directory: ${{ matrix.dir }}
- name: Security audit (npm)
run: |
set +e
npm audit signatures 2>&1
SIGNATURES_EXIT=$?
npm audit --audit-level=moderate 2>&1
MODERATE_EXIT=$?
npm exec --yes -- audit-ci@7 --config "${GITHUB_WORKSPACE}/.github/scripts/audit-ci.jsonc" 2>&1
HIGH_EXIT=$?
set -e
[ $SIGNATURES_EXIT -eq 0 ] && SIGNATURES_STATUS="PASS" || SIGNATURES_STATUS="WARN"
[ $MODERATE_EXIT -eq 0 ] && MODERATE_STATUS="PASS" || MODERATE_STATUS="WARN"
[ $HIGH_EXIT -eq 0 ] && HIGH_STATUS="PASS" || HIGH_STATUS="WARN"
# Only emit a per-package summary block when the audit found
# something; a clean PR should not produce a wall of PASS tables.
if [ $MODERATE_EXIT -ne 0 ] || [ $HIGH_EXIT -ne 0 ]; then
AUD_JSON="${RUNNER_TEMP:-/tmp}/npm-audit-${{ matrix.dir }}.json"
npm audit --json > "${AUD_JSON}" 2>/dev/null || true
{
echo "## Security Audit — Workers (\`${{ matrix.dir }}\`)"
echo ""
echo "| Signatures | Moderate | High/Critical (audit-ci) |"
echo "|---|---|---|"
echo "| ${SIGNATURES_STATUS} | ${MODERATE_STATUS} | ${HIGH_STATUS} |"
} >> "$GITHUB_STEP_SUMMARY"
bash "${GITHUB_WORKSPACE}/.github/scripts/github-summary-npm-audit-high-critical.sh" "${AUD_JSON}" >> "$GITHUB_STEP_SUMMARY" || true
fi
[ $HIGH_EXIT -eq 0 ]
summary:
name: 📋 CI Summary
runs-on: ubuntu-latest
timeout-minutes: 5
if: always() && !cancelled()
needs:
- detect-target
- security_checks
- security_audit_ui
- security_audit_core
- security_audit_workers
- setup-ui
- lint-ui
- typecheck-ui
- test-ui
- coverage-ui
- build-ui
- list-core
- setup-core
- lint-core
- typecheck-core
- test-core
- build-core
- list-workers
- setup-workers
- lint-workers
- typecheck-workers
- test-workers
- build-workers
steps:
- name: Write job summary
shell: bash
env:
REASON: ${{ needs.detect-target.outputs.reason }}
RUN_UI: ${{ needs.detect-target.outputs.run_ui }}
RUN_CORE: ${{ needs.detect-target.outputs.run_core }}
RUN_WORKERS: ${{ needs.detect-target.outputs.run_workers }}
SEC: ${{ needs.security_checks.result || 'skipped' }}
SEC_UI: ${{ needs.security_audit_ui.result || 'skipped' }}
SEC_CORE: ${{ needs.security_audit_core.result || 'skipped' }}
SEC_WRK: ${{ needs.security_audit_workers.result || 'skipped' }}
UI_SETUP: ${{ needs.setup-ui.result || 'skipped' }}
CORE_SETUP: ${{ needs.setup-core.result || 'skipped' }}
WRK_SETUP: ${{ needs.setup-workers.result || 'skipped' }}
run: |
ic() {
case "$1" in
success) echo "✅" ;;
failure) echo "❌" ;;
cancelled) echo "🚫" ;;
skipped) echo "⏭️" ;;
*) echo "❔" ;;
esac
}
{
echo "## CI Summary (mdk-prv monorepo)"
echo ""
echo "**Selection:** ${REASON} (ui=${RUN_UI} core=${RUN_CORE} workers=${RUN_WORKERS})"
echo ""
echo "| Area | Setup | Security |"
echo "|---|---|---|"
echo "| 🔐 Dependency review | — | $(ic "${SEC}") \`${SEC}\` |"
echo "| 🎨 UI | $(ic "${UI_SETUP}") \`${UI_SETUP}\` | $(ic "${SEC_UI}") \`${SEC_UI}\` |"
echo "| 🧱 Core | $(ic "${CORE_SETUP}") \`${CORE_SETUP}\` | $(ic "${SEC_CORE}") \`${SEC_CORE}\` |"
echo "| ⚙️ Workers | $(ic "${WRK_SETUP}") \`${WRK_SETUP}\` | $(ic "${SEC_WRK}") \`${SEC_WRK}\` |"
echo ""
echo "### What ran"
echo "- Per-package **lockfile consistency** in setup jobs (npm \`package-lock.json\`)"
echo "- **One install per package** via cache actions; downstream jobs restore \`node_modules\`"
echo "- **PR:** dependency review + per-package npm audits (audit-ci for high/critical)"
} >> "$GITHUB_STEP_SUMMARY"