Skip to content

chore(deps): bump the production-dependencies group across 1 directory with 9 updates #1835

chore(deps): bump the production-dependencies group across 1 directory with 9 updates

chore(deps): bump the production-dependencies group across 1 directory with 9 updates #1835

Workflow file for this run

name: CI
on:
pull_request:
branches:
- main
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild all images (ignore cache)'
required: false
default: false
type: boolean
env:
TEST_HOST: http://localhost
CI_DB_PORT: 5433
CI_PORT: 8081
PORT: 8081
CI_BETTER_AUTH_URL: http://localhost:8083/auth
BETTER_AUTH_URL: http://localhost:8083/auth
DB_USERNAME: test-user
DB_PASSWORD: test-pw
DB_NAME: wxyc_db
AUTH_BYPASS: true
AUTH_USERNAME: test_dj1
AUTH_PASSWORD: testpassword123
MOCK_API_URL: http://localhost:9090
# Workflow-level GITHUB_TOKEN scope. No job in this file pushes code,
# comments on PRs, or creates releases; all writes to external services
# use their own non-GITHUB_TOKEN secrets. contents:read is the safe floor.
permissions:
contents: read
jobs:
# Detect what changed to conditionally run jobs
detect-changes:
runs-on: ubuntu-latest
outputs:
apps: ${{ steps.changes.outputs.apps }}
jobs: ${{ steps.changes.outputs.jobs }}
shared: ${{ steps.changes.outputs.shared }}
tests: ${{ steps.changes.outputs.tests }}
src: ${{ steps.changes.outputs.src }}
run-integration: ${{ steps.should-run.outputs.run-integration }}
db-init: ${{ steps.changes.outputs.db-init }}
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Detect file changes
uses: dorny/paths-filter@v4
id: changes
with:
filters: |
apps:
- 'apps/**'
jobs:
- 'jobs/**'
shared:
- 'shared/**'
tests:
- 'tests/**'
db-init:
- 'dev_env/Dockerfile.init'
- 'dev_env/init-db.mjs'
- 'dev_env/format-pg-error.mjs'
- 'dev_env/package.init.json'
- 'dev_env/seed_db.sql'
- 'shared/database/src/migrations/**'
- 'shared/database/src/schema.ts'
docs-only:
- '**/*.md'
- 'docs/**'
mock-api:
- 'dev_env/mock-api-server/**'
src:
- 'apps/**'
- 'jobs/**'
- 'shared/**'
- 'tests/**'
- 'dev_env/mock-api-server/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.base.json'
- name: Determine if integration tests should run
id: should-run
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" || \
"${{ steps.changes.outputs.apps }}" == "true" || \
"${{ steps.changes.outputs.jobs }}" == "true" || \
"${{ steps.changes.outputs.shared }}" == "true" || \
"${{ steps.changes.outputs.tests }}" == "true" ]]; then
echo "run-integration=true" >> $GITHUB_OUTPUT
else
echo "run-integration=false" >> $GITHUB_OUTPUT
fi
# Fast lint and type check - always runs on code changes
lint-and-typecheck:
needs: detect-changes
if: needs.detect-changes.outputs.src == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Set Up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v5
with:
path: node_modules
key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }}
- name: Validate cached node_modules
id: validate-cache
if: steps.cache-node-modules.outputs.cache-hit == 'true'
run: |
if npm ls --depth=0 >/dev/null 2>&1; then
echo "valid=true" >> $GITHUB_OUTPUT
else
echo "valid=false" >> $GITHUB_OUTPUT
echo "⚠️ Cache corrupted, will reinstall"
rm -rf node_modules
fi
- name: Install Dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false'
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Cache tool artifacts (tsc, ESLint, Prettier)
uses: actions/cache@v5
with:
path: |
.cache
shared/database/.cache
shared/authentication/.cache
apps/backend/tsconfig.tsbuildinfo
apps/auth/tsconfig.tsbuildinfo
key: tool-cache-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', '.prettierrc.json', 'tsconfig.base.json', 'package-lock.json') }}-${{ github.sha }}
restore-keys: |
tool-cache-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', '.prettierrc.json', 'tsconfig.base.json', 'package-lock.json') }}-
tool-cache-${{ runner.os }}-
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Check formatting
run: npm run format:check
- name: Validate migration journal
run: npm run lint:migrations
- name: Cross-cache-identity flag-doc consistency
run: scripts/check-cross-cache-identity-flags.sh
- name: Migration guards (cross-cache-identity)
run: scripts/check-precondition-guards.sh
- name: legacy_entry_id writes (three-use invariant)
run: node scripts/check-legacy-entry-id-writes.mjs
- name: Bulk UPDATE + ANALYZE pairing (BS#934)
run: node scripts/check-bulk-update-analyze.mjs --strict
- name: Build
run: npm run build
# Unit tests - runs affected tests only
unit-tests:
needs: detect-changes
if: needs.detect-changes.outputs.src == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Need full history for --changedSince
- name: Set Up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v5
with:
path: node_modules
key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }}
- name: Validate cached node_modules
id: validate-cache
if: steps.cache-node-modules.outputs.cache-hit == 'true'
run: |
if npm ls --depth=0 >/dev/null 2>&1; then
echo "valid=true" >> $GITHUB_OUTPUT
else
echo "valid=false" >> $GITHUB_OUTPUT
echo "⚠️ Cache corrupted, will reinstall"
rm -rf node_modules
fi
- name: Install Dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false'
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Run affected unit tests
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
npx jest --config jest.unit.config.ts --changedSince=origin/${{ github.base_ref }} --coverage --passWithNoTests
else
# workflow_dispatch: run all unit tests
npm run test:unit:coverage
fi
- name: Upload Coverage
uses: actions/upload-artifact@v7
if: always()
with:
name: unit-test-coverage
path: coverage/
retention-days: 7
# Integration tests - only when backend/auth/shared/tests change
# Note: Job always runs to report status, but steps are skipped when no relevant changes.
# The services block starts PostgreSQL even when skipped -- this is unavoidable with GHA
# service containers but harmless (starts in seconds, job exits immediately).
Integration-Tests:
needs: [detect-changes, lint-and-typecheck]
# Run when lint-and-typecheck succeeds or was skipped (no src changes).
# This ensures the required check reports status on docs-only PRs.
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18.0-alpine
env:
POSTGRES_USER: test-user
POSTGRES_PASSWORD: test-pw
POSTGRES_DB: wxyc_db
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 2s
--health-timeout 5s
--health-retries 10
env:
RUN_TESTS: ${{ needs.detect-changes.outputs.run-integration }}
TEST_HOST: http://localhost
DB_HOST: localhost
DB_PORT: 5433
DB_USERNAME: test-user
DB_PASSWORD: test-pw
DB_NAME: wxyc_db
PORT: 8081
AUTH_PORT: 8083
AUTH_BYPASS: true
BETTER_AUTH_URL: http://localhost:8083/auth
BETTER_AUTH_JWKS_URL: http://localhost:8083/auth/jwks
BETTER_AUTH_ISSUER: http://localhost:8083
BETTER_AUTH_AUDIENCE: http://localhost:8083
BETTER_AUTH_SECRET: ci-test-secret-key-for-jwt-signing
steps:
- name: Skip notification
if: env.RUN_TESTS != 'true'
run: echo "No relevant changes detected, skipping integration tests"
- name: Checkout Code
if: env.RUN_TESTS == 'true'
uses: actions/checkout@v6
- name: Set Up Node.js
if: env.RUN_TESTS == 'true'
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Cache node_modules
if: env.RUN_TESTS == 'true'
id: cache-node-modules
uses: actions/cache@v5
with:
path: node_modules
key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }}
- name: Validate cached node_modules
id: validate-cache
if: env.RUN_TESTS == 'true' && steps.cache-node-modules.outputs.cache-hit == 'true'
run: |
if npm ls --depth=0 >/dev/null 2>&1; then
echo "valid=true" >> $GITHUB_OUTPUT
else
echo "valid=false" >> $GITHUB_OUTPUT
echo "Cache corrupted, will reinstall"
rm -rf node_modules
fi
- name: Install Dependencies
if: env.RUN_TESTS == 'true' && (steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false')
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Lint .env file
if: env.RUN_TESTS == 'true'
run: npm run lint:env
- name: Build
if: env.RUN_TESTS == 'true'
run: npm run build
- name: Build mock API server
if: env.RUN_TESTS == 'true'
run: cd dev_env/mock-api-server && npm ci && npm run build
- name: Initialize database
if: env.RUN_TESTS == 'true'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
touch .env
node dev_env/init-db.mjs
- name: Start services
if: env.RUN_TESTS == 'true'
env:
NODE_ENV: test
USE_MOCK_SERVICES: 'false'
ANON_DEVICE_JWT_SECRET: ci-test-secret-key-for-anonymous-devices
LIBRARY_METADATA_URL: http://localhost:9090
SLACK_WEBHOOK_URL: http://localhost:9090
SLACK_WXYC_REQUESTS_WEBHOOK: /services/T00/B00/ci-test-webhook
TUBAFRENZY_URL: http://localhost:9090
MIRROR_API_KEY: ci-test-mirror-key
MOCK_API_PORT: '9090'
# Internal endpoints (ETL notify, tubafrenzy webhook).
# authenticateInternal() fails closed on an empty key, so the
# tubafrenzy-webhook integration spec needs this wired explicitly.
# Matches the spec's INTERNAL_KEY fallback default.
ETL_NOTIFY_KEY: test-secret-key
# BS#1261 — request-line ban CRUD + check-request-ban bucketing.
# Same shape as ETL_NOTIFY_KEY but distinct env var.
ROM_INTERNAL_KEY: test-rom-secret-key
# LML client limiter: disable in CI. The integration suite runs
# hundreds of fire-and-forget /lookup calls across spec files under
# --runInBand; the module-level TokenBucket(50/min) would drain
# partway through and subsequent calls would queue ~1200ms each,
# timing out metadata-lml tests at 30s (BS#955). Mirrors the
# unit-test override convention at
# tests/unit/services/lml.client.test.ts:792-793.
LML_CLIENT_MAX_CONCURRENT: '10000'
LML_CLIENT_RATE_PER_MIN: '60000'
# Default org wiring: matches the slug seeded by dev_env/seed_db.sql.
# Consumed by the org-sync hooks and the user.create.after
# auto-membership hook in shared/authentication/src/auth.definition.ts.
# Without this, those hooks log a warning and no-op, which the
# auth-auto-membership integration spec specifically exercises.
DEFAULT_ORG_SLUG: test-org
# Catalog-search alias LATERAL: align GHA with the ci-profile
# docker-compose default (dev_env/docker-compose.yml:245). Without
# this, `getCatalogSearchAliasConfig().enabled` is false in CI and
# tests/integration/library-search-alias.spec.js's BS#1269 +
# BS#1383 cases either silently warn-skip (the OHSEES test) or
# fail-fast (the discogs_member test). The flag has been on by
# default in `npm run ci:testmock` since BS#1269 landed; this
# closes the GHA-vs-compose drift that surfaced when BS#1383
# promoted its case from warn-skip to fail-fast.
CATALOG_SEARCH_ALIAS_ENABLED: 'true'
run: |
node dev_env/mock-api-server/dist/server.js > /tmp/mock-api.log 2>&1 &
node apps/auth/dist/app.js > /tmp/auth.log 2>&1 &
node apps/backend/dist/app.js > /tmp/backend.log 2>&1 &
timeout 30 bash -c 'until curl -sf http://localhost:9090/_admin/health > /dev/null 2>&1; do sleep 1; done'
timeout 30 bash -c 'until curl -sf http://localhost:8083/healthcheck > /dev/null 2>&1; do sleep 1; done'
timeout 30 bash -c 'until curl -sf http://localhost:8081/healthcheck > /dev/null 2>&1; do sleep 1; done'
- name: Run Integration Tests
if: env.RUN_TESTS == 'true'
# Note: Tests must run sequentially (--runInBand) because they share
# show state, DJ sessions, and flowsheet entries. Parallel execution
# causes tests to interfere with each other's join/leave operations.
run: npm run ci:test
- name: Show service logs on failure
if: env.RUN_TESTS == 'true' && failure()
run: |
echo "=== Mock API Server Log ==="
cat /tmp/mock-api.log || true
echo ""
echo "=== Auth Service Log ==="
cat /tmp/auth.log || true
echo ""
echo "=== Backend Service Log ==="
cat /tmp/backend.log || true
- name: Upload Coverage Report
uses: actions/upload-artifact@v7
if: env.RUN_TESTS == 'true' && always()
with:
name: integration-test-coverage
path: coverage/
retention-days: 14
# Pre-flight migrate-dryrun: restores the most recent automated RDS
# snapshot to a sandbox DB instance and runs `node scripts/dryrun-migrate.mjs`
# against it. Catches preconditions that depend on prod data shape (e.g.
# the `RAISE EXCEPTION` guards added per WXYC/Backend-Service#705) at
# PR-review time rather than at deploy time. See WXYC/Backend-Service#726.
#
# Requires repo-level secrets (one-time ops setup):
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION — already present
# for the deploy workflow; needs additional rds:* permissions plus
# ec2:AuthorizeSecurityGroupIngress / ec2:RevokeSecurityGroupIngress
# on SGs tagged Purpose=migrate-dryrun (#757).
# PROD_DB_ID — RDS instance identifier of the prod DB.
# PROD_DB_NAME, PROD_DB_USERNAME, PROD_DB_PASSWORD — match the source
# instance's master credentials (snapshots restore with the source's
# master user).
# SG_DRYRUN_GHA — security group id. Created tagged Purpose=migrate-dryrun
# by scripts/provision-dryrun-aws.mjs. The job authorizes its
# runner's /32 just-in-time and revokes it in always() teardown,
# so the SG carries no persistent ingress rules between runs.
# See WXYC/Backend-Service#726 (preflight) and #757 (JIT ingress).
migrate-dryrun:
name: Migration Dry-Run (prod-shaped data)
needs: [detect-changes]
if: needs.detect-changes.outputs.db-init == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install minimal deps for dryrun script
# postgres + drizzle-orm are the only runtime imports the script
# needs. The `--workspaces=false` flag skips workspace resolution
# so npm doesn't try to fetch the `@wxyc/shared` workspace dep
# from GitHub Packages — that lookup needs NPM_TOKEN, and even
# with the token wired in, install + resolution costs ~30s of
# CI time we don't need here. The script imports
# `../dev_env/format-pg-error.mjs` by relative path, so it
# doesn't need anything inside dev_env to be npm-installed.
run: npm install --workspaces=false --no-save --no-package-lock --no-audit --no-fund postgres drizzle-orm
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Configure AWS credentials
# Long-lived AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY pattern
# already used by .github/workflows/deploy-base.yml (lines 246, 254,
# 265, 329, 337, 363, 454). Migrating to OIDC is a separate concern.
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Authorize runner IP on SG_DRYRUN_GHA
# GitHub Actions runners pull from a pool of ~3000 egress CIDRs that
# rotate monthly (api.github.com/meta `.actions`). The default AWS
# SG rule limit is 60, so bulk-authorizing the entire pool is not
# an option. Instead, authorize just this run's runner IP /32 here
# and revoke it in the always() teardown step below. Net result:
# SG carries one transient rule per concurrent run; quota usage is
# bounded by run concurrency (db-init paths change rarely, so this
# stays well under 60 in practice).
run: |
set -euo pipefail
RUNNER_IP=$(curl -sS --max-time 10 https://checkip.amazonaws.com | tr -d '[:space:]')
if [[ ! "$RUNNER_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "checkip.amazonaws.com returned unexpected payload: ${RUNNER_IP}" >&2
exit 1
fi
echo "RUNNER_IP=${RUNNER_IP}" >> "$GITHUB_ENV"
# Tolerate InvalidPermission.Duplicate so a re-run that hits the
# same runner is a no-op rather than a hard failure.
aws ec2 authorize-security-group-ingress \
--group-id "${{ secrets.SG_DRYRUN_GHA }}" \
--protocol tcp --port 5432 \
--cidr "${RUNNER_IP}/32" \
2> >(grep -v 'InvalidPermission.Duplicate' >&2) \
|| aws ec2 describe-security-groups \
--group-ids "${{ secrets.SG_DRYRUN_GHA }}" \
--query "SecurityGroups[0].IpPermissions[?FromPort==\`5432\`].IpRanges[?CidrIp=='${RUNNER_IP}/32']" \
--output text | grep -q "${RUNNER_IP}"
- name: Restore latest prod snapshot to ephemeral sandbox
id: restore
# The sandbox lands in the dedicated `wxyc-dryrun-public` subnet
# group rather than inheriting prod's. Prod's subnet group is all
# private — `--publicly-accessible` is silently ineffective there
# (no IGW route, no public IP). The dedicated group covers public
# subnets so the sandbox gets a real public IP and the GHA runner
# can reach it. Ingress is gated by SG_DRYRUN_GHA; the JIT-authorize
# step above adds this runner's /32 just before the connection.
# The sandbox, the SG rule, and the subnet-group reference are
# all torn down in the always-run steps below.
run: |
set -euo pipefail
# Filter to Status==available so we don't pick up a snapshot
# AWS is still mid-creating (RDS's automated-backup window
# raced this step on PR #1348 / run 27185185066 → exit 254
# 'DBSnapshot must have state available but actually has
# creating'). The trailing `[-1]` then gives the most-recent
# *available* snapshot.
SNAPSHOT_ID=$(aws rds describe-db-snapshots \
--db-instance-identifier "${{ secrets.PROD_DB_ID }}" \
--snapshot-type automated \
--query 'sort_by(DBSnapshots[?Status==`available`], &SnapshotCreateTime)[-1].DBSnapshotIdentifier' \
--output text)
SANDBOX_ID="dryrun-${{ github.run_id }}"
echo "SANDBOX_ID=$SANDBOX_ID" >> "$GITHUB_ENV"
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier "$SANDBOX_ID" \
--db-snapshot-identifier "$SNAPSHOT_ID" \
--db-instance-class db.t4g.micro \
--publicly-accessible \
--db-subnet-group-name wxyc-dryrun-public \
--vpc-security-group-ids "${{ secrets.SG_DRYRUN_GHA }}"
aws rds wait db-instance-available --db-instance-identifier "$SANDBOX_ID"
ENDPOINT=$(aws rds describe-db-instances \
--db-instance-identifier "$SANDBOX_ID" \
--query 'DBInstances[0].Endpoint.Address' --output text)
echo "DB_HOST=$ENDPOINT" >> "$GITHUB_ENV"
- name: Run drizzle:migrate against sandbox
env:
DB_HOST: ${{ env.DB_HOST }}
DB_PORT: '5432'
DB_NAME: ${{ secrets.PROD_DB_NAME }}
DB_USERNAME: ${{ secrets.PROD_DB_USERNAME }}
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
run: node scripts/dryrun-migrate.mjs
- name: Revoke runner IP from SG_DRYRUN_GHA
# Always runs so a failure in any earlier step still cleans up the
# transient ingress rule. The guard on RUNNER_IP handles the case
# where the authorize step failed before the env var was written.
if: always()
run: |
if [ -n "${RUNNER_IP:-}" ]; then
aws ec2 revoke-security-group-ingress \
--group-id "${{ secrets.SG_DRYRUN_GHA }}" \
--protocol tcp --port 5432 \
--cidr "${RUNNER_IP}/32" \
2> >(grep -v 'InvalidPermission.NotFound' >&2) \
|| true
fi
- name: Tear down sandbox
# The guard on SANDBOX_ID handles the case where restore failed
# before the env var was written; without it, delete-db-instance
# would error and obscure the actual failure.
if: always()
run: |
if [ -n "${SANDBOX_ID:-}" ]; then
aws rds delete-db-instance \
--db-instance-identifier "$SANDBOX_ID" \
--skip-final-snapshot \
--delete-automated-backups
fi