mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 12:13:30 +00:00
🚦 ci: Reduce GitHub Actions Runner Pressure (#14716)
* ci: reduce GitHub Actions runner pressure * ci: enforce static check path exclusions
This commit is contained in:
parent
1bd4455c2d
commit
82de3bc422
12 changed files with 857 additions and 710 deletions
4
.github/workflows/a11y.yml
vendored
4
.github/workflows/a11y.yml
vendored
|
|
@ -16,6 +16,10 @@ permissions:
|
|||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
axe-linter:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
agents_integration_tests:
|
||||
name: Integration Tests that use in-process MongoDB
|
||||
|
|
|
|||
4
.github/workflows/backend-review.yml
vendored
4
.github/workflows/backend-review.yml
vendored
|
|
@ -11,6 +11,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_ENV: CI
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cache_integration_tests:
|
||||
name: Integration Tests that use actual Redis Cache
|
||||
|
|
|
|||
89
.github/workflows/config-review.yml
vendored
89
.github/workflows/config-review.yml
vendored
|
|
@ -1,89 +0,0 @@
|
|||
name: Config Migration Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'config/**'
|
||||
- 'api/models/**'
|
||||
- 'api/db/**'
|
||||
- 'packages/data-schemas/src/**'
|
||||
- 'packages/data-provider/src/**'
|
||||
- 'packages/api/src/acl/**'
|
||||
- 'packages/api/src/shared-links/**'
|
||||
- '!**.md'
|
||||
|
||||
env:
|
||||
NODE_ENV: CI
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
jobs:
|
||||
test-config:
|
||||
name: 'Tests: config migrations'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
api/node_modules
|
||||
packages/api/node_modules
|
||||
packages/data-provider/node_modules
|
||||
packages/data-schemas/node_modules
|
||||
key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Restore data-provider build cache
|
||||
id: cache-data-provider
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/data-provider/dist
|
||||
key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-provider
|
||||
if: steps.cache-data-provider.outputs.cache-hit != 'true'
|
||||
run: npm run build:data-provider
|
||||
|
||||
- name: Restore data-schemas build cache
|
||||
id: cache-data-schemas
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/data-schemas/dist
|
||||
key: build-data-schemas-${{ runner.os }}-${{ hashFiles('packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/tsdown.config.mjs', 'packages/data-schemas/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-schemas
|
||||
if: steps.cache-data-schemas.outputs.cache-hit != 'true'
|
||||
run: npm run build:data-schemas
|
||||
|
||||
- name: Restore api build cache
|
||||
id: cache-api
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/api/dist
|
||||
key: build-api-${{ runner.os }}-${{ hashFiles('packages/api/src/**', 'packages/api/tsconfig*.json', 'packages/api/tsdown.config.mjs', 'packages/api/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/tsdown.config.mjs', 'packages/data-schemas/package.json') }}
|
||||
|
||||
- name: Build api
|
||||
if: steps.cache-api.outputs.cache-hit != 'true'
|
||||
run: npm run build:api
|
||||
|
||||
- name: Create empty auth.json file
|
||||
run: |
|
||||
mkdir -p api/data
|
||||
echo '{}' > api/data/auth.json
|
||||
|
||||
- name: Prepare .env.test file
|
||||
run: cp api/test/.env.test.example api/test/.env.test
|
||||
|
||||
- name: Run config migration tests
|
||||
run: npm run test:config
|
||||
132
.github/workflows/eslint-ci.yml
vendored
132
.github/workflows/eslint-ci.yml
vendored
|
|
@ -1,132 +0,0 @@
|
|||
name: ESLint Code Quality Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-staging
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- '.github/workflows/eslint-ci.yml'
|
||||
- '!**.md'
|
||||
|
||||
jobs:
|
||||
eslint_checks:
|
||||
name: Run ESLint Linting
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Run ESLint on changed files within the api/, client/, and packages/ directories.
|
||||
- name: Run ESLint on changed files
|
||||
run: |
|
||||
# Extract the base commit SHA from the pull_request event payload.
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
echo "Base commit SHA: $BASE_SHA"
|
||||
|
||||
# Get changed files (only JS/TS files in api/, client/, or packages/)
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
# Debug output
|
||||
echo "Changed files:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# Ensure there are files to lint before running ESLint
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping ESLint."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run ESLint
|
||||
# --no-warn-ignored: changed files under config-ignored paths
|
||||
# (e.g. packages/data-schemas/misc/**) must not fail --max-warnings=0
|
||||
npx eslint --no-error-on-unmatched-pattern \
|
||||
--config eslint.config.mjs \
|
||||
--no-warn-ignored \
|
||||
--max-warnings=0 \
|
||||
-- "${CHANGED_FILES[@]}"
|
||||
|
||||
# Run Prettier --check on the same set of changed files to catch
|
||||
# formatting drift in PRs that bypassed the local pre-commit hook
|
||||
# (e.g. GitHub UI edit-and-merge, `git commit --no-verify`).
|
||||
- name: Run Prettier --check on changed files
|
||||
run: |
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping Prettier."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to check:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# `prettier --check` exits non-zero if any file would be reformatted.
|
||||
# Suggest the local fix in the failure message so contributors aren't
|
||||
# left guessing how to resolve.
|
||||
if ! npx prettier --check --no-error-on-unmatched-pattern -- "${CHANGED_FILES[@]}"; then
|
||||
echo ""
|
||||
echo "::error::Prettier formatting drift detected. Fix locally with:"
|
||||
echo "::error:: npx prettier --write <files>"
|
||||
echo "::error::Or rely on the lint-staged pre-commit hook (do not bypass with --no-verify)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify import ordering on the same set of changed files. The script
|
||||
# only sorts files under known source roots, so unrelated changed files
|
||||
# (configs, etc.) are ignored. Matches the lint-staged pre-commit hook.
|
||||
- name: Check import sorting on changed files
|
||||
run: |
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping import-sort check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to check:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# `--check` lists offending files and exits non-zero without writing.
|
||||
if ! node scripts/sort-imports.mts --check "${CHANGED_FILES[@]}"; then
|
||||
echo ""
|
||||
echo "::error::Import order drift detected. Fix locally with:"
|
||||
echo "::error:: npm run sort-imports"
|
||||
echo "::error::For specific files:"
|
||||
echo "::error:: npm run sort-imports -- packages/api/src/app/metrics.ts packages/api/src/rum/proxy.ts"
|
||||
echo "::error::To check without writing files:"
|
||||
echo "::error:: npm run sort-imports:check"
|
||||
echo "::error::Or rely on the lint-staged pre-commit hook (do not bypass with --no-verify)."
|
||||
exit 1
|
||||
fi
|
||||
60
.github/workflows/frontend-review.yml
vendored
60
.github/workflows/frontend-review.yml
vendored
|
|
@ -12,6 +12,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
|
|
@ -162,14 +166,14 @@ jobs:
|
|||
working-directory: packages/client
|
||||
|
||||
test-ubuntu:
|
||||
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/4)'
|
||||
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/2)'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
shard: [1, 2]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
|
|
@ -205,56 +209,8 @@ jobs:
|
|||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/4)
|
||||
run: npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
working-directory: client
|
||||
|
||||
test-windows:
|
||||
name: 'Tests: Windows (shard ${{ matrix.shard }}/4)'
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
client/node_modules
|
||||
packages/client/node_modules
|
||||
packages/data-provider/node_modules
|
||||
key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Download data-provider build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/4)
|
||||
run: npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/2)
|
||||
run: npm run test:ci -- --shard=${{ matrix.shard }}/2
|
||||
working-directory: client
|
||||
|
||||
build-verify:
|
||||
|
|
|
|||
139
.github/workflows/frontend-windows-nightly.yml
vendored
Normal file
139
.github/workflows/frontend-windows-nightly.yml
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
name: Frontend Windows Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build packages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
source-sha: ${{ steps.source.outputs.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event_name == 'schedule' && 'dev' || github.sha }}
|
||||
|
||||
- name: Capture source SHA
|
||||
id: source
|
||||
shell: bash
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
client/node_modules
|
||||
packages/client/node_modules
|
||||
packages/data-provider/node_modules
|
||||
key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Restore data-provider build cache
|
||||
id: cache-data-provider
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/data-provider/dist
|
||||
key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-provider
|
||||
if: steps.cache-data-provider.outputs.cache-hit != 'true'
|
||||
run: npm run build:data-provider
|
||||
|
||||
- name: Restore client-package build cache
|
||||
id: cache-client-package
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/client/dist
|
||||
key: build-client-package-${{ runner.os }}-${{ hashFiles('packages/client/src/**', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build client-package
|
||||
if: steps.cache-client-package.outputs.cache-hit != 'true'
|
||||
run: npm run build:client-package
|
||||
|
||||
- name: Upload data-provider build
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload client-package build
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
retention-days: 2
|
||||
|
||||
test-windows:
|
||||
name: 'Tests: Windows (shard ${{ matrix.shard }}/4)'
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.build.outputs.source-sha }}
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
client/node_modules
|
||||
packages/client/node_modules
|
||||
packages/data-provider/node_modules
|
||||
key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Download data-provider build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/4)
|
||||
run: npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
working-directory: client
|
||||
151
.github/workflows/i18n-unused-keys.yml
vendored
151
.github/workflows/i18n-unused-keys.yml
vendored
|
|
@ -1,151 +0,0 @@
|
|||
name: Detect Unused i18next Strings
|
||||
|
||||
# This workflow checks for unused i18n keys in translation files.
|
||||
# It has special handling for:
|
||||
# - com_ui_special_var_* keys that are dynamically constructed
|
||||
# - com_agents_category_* keys that are stored in the database and used dynamically
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "client/src/**"
|
||||
- "api/**"
|
||||
- "packages/data-provider/src/**"
|
||||
- "packages/client/**"
|
||||
- "packages/data-schemas/src/**"
|
||||
- "!**.md"
|
||||
|
||||
jobs:
|
||||
detect-unused-i18n-keys:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Find unused i18next keys
|
||||
id: find-unused
|
||||
run: |
|
||||
echo "🔍 Scanning for unused i18next keys..."
|
||||
|
||||
# Define paths
|
||||
I18N_FILE="client/src/locales/en/translation.json"
|
||||
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
|
||||
|
||||
# Check if translation file exists
|
||||
if [[ ! -f "$I18N_FILE" ]]; then
|
||||
echo "::error title=Missing i18n File::Translation file not found: $I18N_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract all keys from the JSON file
|
||||
KEYS=$(jq -r 'keys[]' "$I18N_FILE")
|
||||
|
||||
# Track unused keys
|
||||
UNUSED_KEYS=()
|
||||
|
||||
# Check if each key is used in the source code
|
||||
for KEY in $KEYS; do
|
||||
FOUND=false
|
||||
|
||||
# Special case for dynamically constructed special variable keys
|
||||
if [[ "$KEY" == com_ui_special_var_* ]]; then
|
||||
# Check if TSpecialVarLabel is used in the codebase
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "TSpecialVarLabel" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Special case for agent category keys that are dynamically used from database
|
||||
elif [[ "$KEY" == com_agents_category_* ]]; then
|
||||
# Check if agent category localization is being used
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
# Check for dynamic category label/description usage
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -E "category\.(label|description).*startsWith.*['\"]com_" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for the method that defines these keys
|
||||
grep -r --include=\*.{js,jsx,ts,tsx} "ensureDefaultCategories" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for direct usage in agentCategory.ts
|
||||
grep -r --include=\*.ts -E "label:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1 || \
|
||||
grep -r --include=\*.ts -E "description:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Regular check for other keys
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
UNUSED_KEYS+=("$KEY")
|
||||
fi
|
||||
done
|
||||
|
||||
# Output results
|
||||
if [[ ${#UNUSED_KEYS[@]} -gt 0 ]]; then
|
||||
echo "🛑 Found ${#UNUSED_KEYS[@]} unused i18n keys:"
|
||||
echo "unused_keys=$(echo "${UNUSED_KEYS[@]}" | jq -R -s -c 'split(" ")')" >> $GITHUB_ENV
|
||||
for KEY in "${UNUSED_KEYS[@]}"; do
|
||||
echo "::warning title=Unused i18n Key::'$KEY' is defined but not used in the codebase."
|
||||
done
|
||||
else
|
||||
echo "✅ No unused i18n keys detected!"
|
||||
echo "unused_keys=[]" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Post verified comment on PR
|
||||
if: env.unused_keys != '[]'
|
||||
run: |
|
||||
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||
|
||||
# Format the unused keys list as checkboxes for easy manual checking.
|
||||
FILTERED_KEYS=$(echo "$unused_keys" | jq -r '.[]' | grep -v '^\s*$' | sed 's/^/- [ ] `/;s/$/`/' )
|
||||
|
||||
COMMENT_BODY=$(cat <<EOF
|
||||
### 🚨 Unused i18next Keys Detected
|
||||
|
||||
The following translation keys are defined in \`translation.json\` but are **not used** in the codebase:
|
||||
|
||||
$FILTERED_KEYS
|
||||
|
||||
⚠️ **Please remove these unused keys to keep the translation files clean.**
|
||||
EOF
|
||||
)
|
||||
|
||||
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
-f body="$COMMENT_BODY" \
|
||||
-H "Authorization: token $GITHUB_TOKEN"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fail workflow if unused keys found
|
||||
if: env.unused_keys != '[]'
|
||||
run: exit 1
|
||||
6
.github/workflows/playwright-mock.yml
vendored
6
.github/workflows/playwright-mock.yml
vendored
|
|
@ -27,7 +27,7 @@ env:
|
|||
|
||||
jobs:
|
||||
e2e_shards:
|
||||
name: e2e (${{ matrix.stream_store }}, shard ${{ matrix.shard }}/4)
|
||||
name: e2e (${{ matrix.stream_store }}, shard ${{ matrix.shard }}/2)
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
stream_store: [memory, redis]
|
||||
shard: [1, 2, 3, 4]
|
||||
shard: [1, 2]
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
|
@ -142,7 +142,7 @@ jobs:
|
|||
npx playwright install-deps chrome
|
||||
|
||||
- name: Run mock-LLM Tier-1 e2e
|
||||
run: npx playwright test --config=e2e/playwright.config.mock.ts --shard=${{ matrix.shard }}/4
|
||||
run: npx playwright test --config=e2e/playwright.config.mock.ts --shard=${{ matrix.shard }}/2
|
||||
env:
|
||||
CI: 'true'
|
||||
|
||||
|
|
|
|||
691
.github/workflows/static-checks.yml
vendored
Normal file
691
.github/workflows/static-checks.yml
vendored
Normal file
|
|
@ -0,0 +1,691 @@
|
|||
name: Static Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'config/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/static-checks.yml'
|
||||
- '!**.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_ENV: CI
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
jobs:
|
||||
static-checks:
|
||||
name: Static checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect affected checks
|
||||
id: paths
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
predicate-quantifier: 'some-with-excludes'
|
||||
filters: |
|
||||
eslint:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- '.github/workflows/static-checks.yml'
|
||||
- '!**.md'
|
||||
config:
|
||||
- 'api/**'
|
||||
- 'config/**'
|
||||
- 'packages/**'
|
||||
- '.github/workflows/static-checks.yml'
|
||||
- '!**.md'
|
||||
i18n:
|
||||
- 'api/**'
|
||||
- 'client/src/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/data-provider/src/**'
|
||||
- 'packages/data-schemas/src/**'
|
||||
- '.github/workflows/static-checks.yml'
|
||||
- '!**.md'
|
||||
unused_packages:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/api/**'
|
||||
- 'packages/client/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/static-checks.yml'
|
||||
- '!**.md'
|
||||
|
||||
- name: Set up Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
id: install_dependencies
|
||||
continue-on-error: true
|
||||
run: npm ci
|
||||
|
||||
# Run ESLint on changed files within the api/, client/, and packages/ directories.
|
||||
- name: Run ESLint on changed files
|
||||
id: eslint
|
||||
if: always() && steps.paths.outputs.eslint == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Extract the base commit SHA from the pull_request event payload.
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
echo "Base commit SHA: $BASE_SHA"
|
||||
|
||||
# Get changed files (only JS/TS files in api/, client/, or packages/)
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
# Debug output
|
||||
echo "Changed files:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# Ensure there are files to lint before running ESLint
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping ESLint."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run ESLint
|
||||
# --no-warn-ignored: changed files under config-ignored paths
|
||||
# (e.g. packages/data-schemas/misc/**) must not fail --max-warnings=0
|
||||
npx eslint --no-error-on-unmatched-pattern \
|
||||
--config eslint.config.mjs \
|
||||
--no-warn-ignored \
|
||||
--max-warnings=0 \
|
||||
-- "${CHANGED_FILES[@]}"
|
||||
|
||||
# Run Prettier --check on the same set of changed files to catch
|
||||
# formatting drift in PRs that bypassed the local pre-commit hook
|
||||
# (e.g. GitHub UI edit-and-merge, `git commit --no-verify`).
|
||||
- name: Run Prettier --check on changed files
|
||||
id: prettier
|
||||
if: always() && steps.paths.outputs.eslint == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping Prettier."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to check:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# `prettier --check` exits non-zero if any file would be reformatted.
|
||||
# Suggest the local fix in the failure message so contributors aren't
|
||||
# left guessing how to resolve.
|
||||
if ! npx prettier --check --no-error-on-unmatched-pattern -- "${CHANGED_FILES[@]}"; then
|
||||
echo ""
|
||||
echo "::error::Prettier formatting drift detected. Fix locally with:"
|
||||
echo "::error:: npx prettier --write <files>"
|
||||
echo "::error::Or rely on the lint-staged pre-commit hook (do not bypass with --no-verify)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify import ordering on the same set of changed files. The script
|
||||
# only sorts files under known source roots, so unrelated changed files
|
||||
# (configs, etc.) are ignored. Matches the lint-staged pre-commit hook.
|
||||
- name: Check import sorting on changed files
|
||||
id: import_sort
|
||||
if: always() && steps.paths.outputs.eslint == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
|
||||
mapfile -d '' -t CHANGED_FILES < <(
|
||||
git diff -z --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD |
|
||||
grep -zE '^(api|client|packages)/.*\.(js|jsx|ts|tsx)$' || true
|
||||
)
|
||||
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "No matching files changed. Skipping import-sort check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to check:"
|
||||
printf '%s\n' "${CHANGED_FILES[@]}"
|
||||
|
||||
# `--check` lists offending files and exits non-zero without writing.
|
||||
if ! node scripts/sort-imports.mts --check "${CHANGED_FILES[@]}"; then
|
||||
echo ""
|
||||
echo "::error::Import order drift detected. Fix locally with:"
|
||||
echo "::error:: npm run sort-imports"
|
||||
echo "::error::For specific files:"
|
||||
echo "::error:: npm run sort-imports -- packages/api/src/app/metrics.ts packages/api/src/rum/proxy.ts"
|
||||
echo "::error::To check without writing files:"
|
||||
echo "::error:: npm run sort-imports:check"
|
||||
echo "::error::Or rely on the lint-staged pre-commit hook (do not bypass with --no-verify)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Restore data-provider build cache
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
id: cache-data-provider
|
||||
continue-on-error: true
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/data-provider/dist
|
||||
key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-provider
|
||||
id: config_data_provider
|
||||
if: always() && steps.paths.outputs.config == 'true' && steps.cache-data-provider.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
run: npm run build:data-provider
|
||||
|
||||
- name: Restore data-schemas build cache
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
id: cache-data-schemas
|
||||
continue-on-error: true
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/data-schemas/dist
|
||||
key: build-data-schemas-${{ runner.os }}-${{ hashFiles('packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/tsdown.config.mjs', 'packages/data-schemas/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-schemas
|
||||
id: config_data_schemas
|
||||
if: always() && steps.paths.outputs.config == 'true' && steps.cache-data-schemas.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
run: npm run build:data-schemas
|
||||
|
||||
- name: Restore api build cache
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
id: cache-api
|
||||
continue-on-error: true
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: packages/api/dist
|
||||
key: build-api-${{ runner.os }}-${{ hashFiles('packages/api/src/**', 'packages/api/tsconfig*.json', 'packages/api/tsdown.config.mjs', 'packages/api/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/tsdown.config.mjs', 'packages/data-schemas/package.json') }}
|
||||
|
||||
- name: Build api
|
||||
id: config_api
|
||||
if: always() && steps.paths.outputs.config == 'true' && steps.cache-api.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
run: npm run build:api
|
||||
|
||||
- name: Create empty auth.json file
|
||||
id: config_auth
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
mkdir -p api/data
|
||||
echo '{}' > api/data/auth.json
|
||||
|
||||
- name: Prepare .env.test file
|
||||
id: config_env
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
continue-on-error: true
|
||||
run: cp api/test/.env.test.example api/test/.env.test
|
||||
|
||||
- name: Run config migration tests
|
||||
id: config_tests
|
||||
if: always() && steps.paths.outputs.config == 'true'
|
||||
continue-on-error: true
|
||||
run: npm run test:config
|
||||
|
||||
- name: Find unused i18next keys
|
||||
id: find_unused_i18n
|
||||
if: always() && steps.paths.outputs.i18n == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "🔍 Scanning for unused i18next keys..."
|
||||
|
||||
# Define paths
|
||||
I18N_FILE="client/src/locales/en/translation.json"
|
||||
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
|
||||
|
||||
# Check if translation file exists
|
||||
if [[ ! -f "$I18N_FILE" ]]; then
|
||||
echo "::error title=Missing i18n File::Translation file not found: $I18N_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract all keys from the JSON file
|
||||
KEYS=$(jq -r 'keys[]' "$I18N_FILE")
|
||||
|
||||
# Track unused keys
|
||||
UNUSED_KEYS=()
|
||||
|
||||
# Check if each key is used in the source code
|
||||
for KEY in $KEYS; do
|
||||
FOUND=false
|
||||
|
||||
# Special case for dynamically constructed special variable keys
|
||||
if [[ "$KEY" == com_ui_special_var_* ]]; then
|
||||
# Check if TSpecialVarLabel is used in the codebase
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "TSpecialVarLabel" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Special case for agent category keys that are dynamically used from database
|
||||
elif [[ "$KEY" == com_agents_category_* ]]; then
|
||||
# Check if agent category localization is being used
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
# Check for dynamic category label/description usage
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -E "category\.(label|description).*startsWith.*['\"]com_" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for the method that defines these keys
|
||||
grep -r --include=\*.{js,jsx,ts,tsx} "ensureDefaultCategories" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for direct usage in agentCategory.ts
|
||||
grep -r --include=\*.ts -E "label:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1 || \
|
||||
grep -r --include=\*.ts -E "description:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Regular check for other keys
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
UNUSED_KEYS+=("$KEY")
|
||||
fi
|
||||
done
|
||||
|
||||
# Output results
|
||||
if [[ ${#UNUSED_KEYS[@]} -gt 0 ]]; then
|
||||
echo "🛑 Found ${#UNUSED_KEYS[@]} unused i18n keys:"
|
||||
echo "unused_keys=$(echo "${UNUSED_KEYS[@]}" | jq -R -s -c 'split(" ")')" >> $GITHUB_ENV
|
||||
for KEY in "${UNUSED_KEYS[@]}"; do
|
||||
echo "::warning title=Unused i18n Key::'$KEY' is defined but not used in the codebase."
|
||||
done
|
||||
else
|
||||
echo "✅ No unused i18n keys detected!"
|
||||
echo "unused_keys=[]" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Fail workflow if unused keys found
|
||||
id: i18n
|
||||
if: >
|
||||
always() &&
|
||||
steps.paths.outputs.i18n == 'true' &&
|
||||
(steps.find_unused_i18n.outcome == 'failure' || env.unused_keys != '[]')
|
||||
continue-on-error: true
|
||||
run: exit 1
|
||||
|
||||
- name: Install depcheck
|
||||
id: install_depcheck
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
continue-on-error: true
|
||||
run: npm install -g depcheck
|
||||
|
||||
- name: Validate JSON files
|
||||
id: validate_package_json
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
for FILE in package.json client/package.json api/package.json packages/client/package.json; do
|
||||
if [[ -f "$FILE" ]]; then
|
||||
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Extract Dependencies Used in Scripts
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: extract-used-scripts
|
||||
continue-on-error: true
|
||||
run: |
|
||||
extract_deps_from_scripts() {
|
||||
local package_file=$1
|
||||
if [[ -f "$package_file" ]]; then
|
||||
jq -r '.scripts | to_entries[].value' "$package_file" | \
|
||||
grep -oE '([a-zA-Z0-9_-]+)' | sort -u > used_scripts.txt
|
||||
else
|
||||
touch used_scripts.txt
|
||||
fi
|
||||
}
|
||||
|
||||
extract_deps_from_scripts "package.json"
|
||||
mv used_scripts.txt root_used_deps.txt
|
||||
|
||||
extract_deps_from_scripts "client/package.json"
|
||||
mv used_scripts.txt client_used_deps.txt
|
||||
|
||||
extract_deps_from_scripts "api/package.json"
|
||||
mv used_scripts.txt api_used_deps.txt
|
||||
|
||||
- name: Extract Dependencies Used in Source Code
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: extract-used-code
|
||||
continue-on-error: true
|
||||
run: |
|
||||
extract_deps_from_code() {
|
||||
local folder=$1
|
||||
local output_file=$2
|
||||
|
||||
# Initialize empty output file
|
||||
> "$output_file"
|
||||
|
||||
if [[ -d "$folder" ]]; then
|
||||
# Extract require() statements (use explicit includes for portability)
|
||||
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" >> "$output_file" || true
|
||||
|
||||
# Extract ES6 imports - import x from 'module'
|
||||
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# import 'module' (side-effect imports)
|
||||
grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# export { x } from 'module' or export * from 'module'
|
||||
grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# import type { x } from 'module' (TypeScript)
|
||||
grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.ts' --include='*.tsx' 2>/dev/null | \
|
||||
sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# Remove subpath imports but keep the base package
|
||||
# For scoped packages: '@scope/pkg/subpath' -> '@scope/pkg'
|
||||
# For regular packages: 'pkg/subpath' -> 'pkg'
|
||||
# Scoped packages (must keep @scope/package, strip anything after)
|
||||
sed -i -E 's|^(@[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
|
||||
# Non-scoped packages (keep package name, strip subpath)
|
||||
sed -i -E 's|^([a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
|
||||
|
||||
sort -u "$output_file" -o "$output_file"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_deps_from_code "." root_used_code.txt
|
||||
extract_deps_from_code "client" client_used_code.txt
|
||||
extract_deps_from_code "api" api_used_code.txt
|
||||
|
||||
# Extract dependencies used by workspace packages
|
||||
# These packages are used in the workspace but dependencies are provided by parent package.json
|
||||
extract_deps_from_code "packages/client" packages_client_used_code.txt
|
||||
extract_deps_from_code "packages/api" packages_api_used_code.txt
|
||||
|
||||
- name: Get @librechat/client dependencies
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: get-librechat-client-deps
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [[ -f "packages/client/package.json" ]]; then
|
||||
# Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
|
||||
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
|
||||
# Combine all dependencies
|
||||
echo "$DEPS" > librechat_client_deps.txt
|
||||
echo "$DEV_DEPS" >> librechat_client_deps.txt
|
||||
echo "$PEER_DEPS" >> librechat_client_deps.txt
|
||||
|
||||
# Also include dependencies that are imported in packages/client
|
||||
cat packages_client_used_code.txt >> librechat_client_deps.txt
|
||||
|
||||
# Remove empty lines and sort
|
||||
grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
|
||||
mv temp_deps.txt librechat_client_deps.txt
|
||||
else
|
||||
touch librechat_client_deps.txt
|
||||
fi
|
||||
|
||||
- name: Get @librechat/api dependencies
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: get-librechat-api-deps
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [[ -f "packages/api/package.json" ]]; then
|
||||
# Get all dependencies from @librechat/api (dependencies, devDependencies, and peerDependencies)
|
||||
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
|
||||
# Combine all dependencies
|
||||
echo "$DEPS" > librechat_api_deps.txt
|
||||
echo "$DEV_DEPS" >> librechat_api_deps.txt
|
||||
echo "$PEER_DEPS" >> librechat_api_deps.txt
|
||||
|
||||
# Also include dependencies that are imported in packages/api
|
||||
cat packages_api_used_code.txt >> librechat_api_deps.txt
|
||||
|
||||
# Remove empty lines and sort
|
||||
grep -v '^$' librechat_api_deps.txt | sort -u > temp_deps.txt
|
||||
mv temp_deps.txt librechat_api_deps.txt
|
||||
else
|
||||
touch librechat_api_deps.txt
|
||||
fi
|
||||
|
||||
- name: Extract Workspace Dependencies
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: extract-workspace-deps
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Function to get dependencies from a workspace package that are used by another package
|
||||
get_workspace_package_deps() {
|
||||
local package_json=$1
|
||||
local output_file=$2
|
||||
|
||||
# Get all workspace dependencies (starting with @librechat/)
|
||||
if [[ -f "$package_json" ]]; then
|
||||
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
|
||||
|
||||
# For each workspace dependency, get its dependencies
|
||||
for dep in $workspace_deps; do
|
||||
# Convert @librechat/api to packages/api
|
||||
local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
|
||||
local workspace_package_json="${workspace_path}/package.json"
|
||||
|
||||
if [[ -f "$workspace_package_json" ]]; then
|
||||
# Extract all dependencies from the workspace package
|
||||
jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
|
||||
# Also extract peerDependencies
|
||||
jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -f "$output_file" ]]; then
|
||||
sort -u "$output_file" -o "$output_file"
|
||||
else
|
||||
touch "$output_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get workspace dependencies for each package
|
||||
get_workspace_package_deps "package.json" root_workspace_deps.txt
|
||||
get_workspace_package_deps "client/package.json" client_workspace_deps.txt
|
||||
get_workspace_package_deps "api/package.json" api_workspace_deps.txt
|
||||
|
||||
- name: Run depcheck for root package.json
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: check-root
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [[ -f "package.json" ]]; then
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, and workspace packages
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "")
|
||||
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Run depcheck for client/package.json
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: check-client
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [[ -f "client/package.json" ]]; then
|
||||
chmod -R 755 client
|
||||
cd client
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/client imports
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../librechat_client_deps.txt 2>/dev/null | sort -u) || echo "")
|
||||
# Filter out false positives
|
||||
UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
|
||||
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
cd ..
|
||||
fi
|
||||
|
||||
- name: Run depcheck for api/package.json
|
||||
if: always() && steps.paths.outputs.unused_packages == 'true'
|
||||
id: check-api
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [[ -f "api/package.json" ]]; then
|
||||
chmod -R 755 api
|
||||
cd api
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/api imports
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../librechat_api_deps.txt 2>/dev/null | sort -u) || echo "")
|
||||
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
cd ..
|
||||
fi
|
||||
|
||||
- name: Fail workflow if unused dependencies found
|
||||
id: unused_packages
|
||||
if: >
|
||||
always() &&
|
||||
steps.paths.outputs.unused_packages == 'true' &&
|
||||
(env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != '')
|
||||
continue-on-error: true
|
||||
run: exit 1
|
||||
|
||||
- name: Summarize static check failures
|
||||
if: always()
|
||||
env:
|
||||
INSTALL_DEPENDENCIES_OUTCOME: ${{ steps.install_dependencies.outcome }}
|
||||
ESLINT_OUTCOME: ${{ steps.eslint.outcome }}
|
||||
PRETTIER_OUTCOME: ${{ steps.prettier.outcome }}
|
||||
IMPORT_SORT_OUTCOME: ${{ steps.import_sort.outcome }}
|
||||
CACHE_DATA_PROVIDER_OUTCOME: ${{ steps.cache-data-provider.outcome }}
|
||||
CONFIG_DATA_PROVIDER_OUTCOME: ${{ steps.config_data_provider.outcome }}
|
||||
CACHE_DATA_SCHEMAS_OUTCOME: ${{ steps.cache-data-schemas.outcome }}
|
||||
CONFIG_DATA_SCHEMAS_OUTCOME: ${{ steps.config_data_schemas.outcome }}
|
||||
CACHE_API_OUTCOME: ${{ steps.cache-api.outcome }}
|
||||
CONFIG_API_OUTCOME: ${{ steps.config_api.outcome }}
|
||||
CONFIG_AUTH_OUTCOME: ${{ steps.config_auth.outcome }}
|
||||
CONFIG_ENV_OUTCOME: ${{ steps.config_env.outcome }}
|
||||
CONFIG_TESTS_OUTCOME: ${{ steps.config_tests.outcome }}
|
||||
FIND_I18N_OUTCOME: ${{ steps.find_unused_i18n.outcome }}
|
||||
I18N_OUTCOME: ${{ steps.i18n.outcome }}
|
||||
INSTALL_DEPCHECK_OUTCOME: ${{ steps.install_depcheck.outcome }}
|
||||
VALIDATE_PACKAGE_JSON_OUTCOME: ${{ steps.validate_package_json.outcome }}
|
||||
EXTRACT_USED_SCRIPTS_OUTCOME: ${{ steps.extract-used-scripts.outcome }}
|
||||
EXTRACT_USED_CODE_OUTCOME: ${{ steps.extract-used-code.outcome }}
|
||||
GET_CLIENT_DEPS_OUTCOME: ${{ steps.get-librechat-client-deps.outcome }}
|
||||
GET_API_DEPS_OUTCOME: ${{ steps.get-librechat-api-deps.outcome }}
|
||||
EXTRACT_WORKSPACE_DEPS_OUTCOME: ${{ steps.extract-workspace-deps.outcome }}
|
||||
CHECK_ROOT_OUTCOME: ${{ steps.check-root.outcome }}
|
||||
CHECK_CLIENT_OUTCOME: ${{ steps.check-client.outcome }}
|
||||
CHECK_API_OUTCOME: ${{ steps.check-api.outcome }}
|
||||
UNUSED_PACKAGES_OUTCOME: ${{ steps.unused_packages.outcome }}
|
||||
run: |
|
||||
failures=()
|
||||
|
||||
record_failure() {
|
||||
if [[ "$2" == "failure" ]]; then
|
||||
failures+=("$1")
|
||||
fi
|
||||
}
|
||||
|
||||
record_failure "Dependency installation" "$INSTALL_DEPENDENCIES_OUTCOME"
|
||||
record_failure "ESLint" "$ESLINT_OUTCOME"
|
||||
record_failure "Prettier" "$PRETTIER_OUTCOME"
|
||||
record_failure "Import sorting" "$IMPORT_SORT_OUTCOME"
|
||||
record_failure "Config data-provider cache" "$CACHE_DATA_PROVIDER_OUTCOME"
|
||||
record_failure "Config data-provider build" "$CONFIG_DATA_PROVIDER_OUTCOME"
|
||||
record_failure "Config data-schemas cache" "$CACHE_DATA_SCHEMAS_OUTCOME"
|
||||
record_failure "Config data-schemas build" "$CONFIG_DATA_SCHEMAS_OUTCOME"
|
||||
record_failure "Config API cache" "$CACHE_API_OUTCOME"
|
||||
record_failure "Config API build" "$CONFIG_API_OUTCOME"
|
||||
record_failure "Config auth preparation" "$CONFIG_AUTH_OUTCOME"
|
||||
record_failure "Config environment preparation" "$CONFIG_ENV_OUTCOME"
|
||||
record_failure "Config migration tests" "$CONFIG_TESTS_OUTCOME"
|
||||
record_failure "Unused i18n scan" "$FIND_I18N_OUTCOME"
|
||||
record_failure "Unused i18n keys" "$I18N_OUTCOME"
|
||||
record_failure "depcheck installation" "$INSTALL_DEPCHECK_OUTCOME"
|
||||
record_failure "Package JSON validation" "$VALIDATE_PACKAGE_JSON_OUTCOME"
|
||||
record_failure "Package script dependency extraction" "$EXTRACT_USED_SCRIPTS_OUTCOME"
|
||||
record_failure "Source dependency extraction" "$EXTRACT_USED_CODE_OUTCOME"
|
||||
record_failure "Client dependency collection" "$GET_CLIENT_DEPS_OUTCOME"
|
||||
record_failure "API dependency collection" "$GET_API_DEPS_OUTCOME"
|
||||
record_failure "Workspace dependency extraction" "$EXTRACT_WORKSPACE_DEPS_OUTCOME"
|
||||
record_failure "Root depcheck" "$CHECK_ROOT_OUTCOME"
|
||||
record_failure "Client depcheck" "$CHECK_CLIENT_OUTCOME"
|
||||
record_failure "API depcheck" "$CHECK_API_OUTCOME"
|
||||
record_failure "Unused NPM packages" "$UNUSED_PACKAGES_OUTCOME"
|
||||
|
||||
if [[ "$UNUSED_PACKAGES_OUTCOME" == "failure" ]]; then
|
||||
[[ -n "$ROOT_UNUSED" ]] && printf 'Root unused dependencies:\n%s\n' "$ROOT_UNUSED"
|
||||
[[ -n "$CLIENT_UNUSED" ]] && printf 'Client unused dependencies:\n%s\n' "$CLIENT_UNUSED"
|
||||
[[ -n "$API_UNUSED" ]] && printf 'API unused dependencies:\n%s\n' "$API_UNUSED"
|
||||
fi
|
||||
|
||||
if [[ ${#failures[@]} -eq 0 ]]; then
|
||||
echo "All affected static checks passed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "::error::Static checks failed:"
|
||||
printf ' - %s\n' "${failures[@]}"
|
||||
exit 1
|
||||
283
.github/workflows/unused-packages.yml
vendored
283
.github/workflows/unused-packages.yml
vendored
|
|
@ -1,283 +0,0 @@
|
|||
name: Detect Unused NPM Packages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'client/**'
|
||||
- 'api/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/api/**'
|
||||
- '!**.md'
|
||||
|
||||
jobs:
|
||||
detect-unused-packages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install depcheck
|
||||
run: npm install -g depcheck
|
||||
|
||||
- name: Validate JSON files
|
||||
run: |
|
||||
for FILE in package.json client/package.json api/package.json packages/client/package.json; do
|
||||
if [[ -f "$FILE" ]]; then
|
||||
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Extract Dependencies Used in Scripts
|
||||
id: extract-used-scripts
|
||||
run: |
|
||||
extract_deps_from_scripts() {
|
||||
local package_file=$1
|
||||
if [[ -f "$package_file" ]]; then
|
||||
jq -r '.scripts | to_entries[].value' "$package_file" | \
|
||||
grep -oE '([a-zA-Z0-9_-]+)' | sort -u > used_scripts.txt
|
||||
else
|
||||
touch used_scripts.txt
|
||||
fi
|
||||
}
|
||||
|
||||
extract_deps_from_scripts "package.json"
|
||||
mv used_scripts.txt root_used_deps.txt
|
||||
|
||||
extract_deps_from_scripts "client/package.json"
|
||||
mv used_scripts.txt client_used_deps.txt
|
||||
|
||||
extract_deps_from_scripts "api/package.json"
|
||||
mv used_scripts.txt api_used_deps.txt
|
||||
|
||||
- name: Extract Dependencies Used in Source Code
|
||||
id: extract-used-code
|
||||
run: |
|
||||
extract_deps_from_code() {
|
||||
local folder=$1
|
||||
local output_file=$2
|
||||
|
||||
# Initialize empty output file
|
||||
> "$output_file"
|
||||
|
||||
if [[ -d "$folder" ]]; then
|
||||
# Extract require() statements (use explicit includes for portability)
|
||||
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" >> "$output_file" || true
|
||||
|
||||
# Extract ES6 imports - import x from 'module'
|
||||
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# import 'module' (side-effect imports)
|
||||
grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# export { x } from 'module' or export * from 'module'
|
||||
grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
|
||||
sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# import type { x } from 'module' (TypeScript)
|
||||
grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
|
||||
--include='*.ts' --include='*.tsx' 2>/dev/null | \
|
||||
sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
|
||||
|
||||
# Remove subpath imports but keep the base package
|
||||
# For scoped packages: '@scope/pkg/subpath' -> '@scope/pkg'
|
||||
# For regular packages: 'pkg/subpath' -> 'pkg'
|
||||
# Scoped packages (must keep @scope/package, strip anything after)
|
||||
sed -i -E 's|^(@[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
|
||||
# Non-scoped packages (keep package name, strip subpath)
|
||||
sed -i -E 's|^([a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
|
||||
|
||||
sort -u "$output_file" -o "$output_file"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_deps_from_code "." root_used_code.txt
|
||||
extract_deps_from_code "client" client_used_code.txt
|
||||
extract_deps_from_code "api" api_used_code.txt
|
||||
|
||||
# Extract dependencies used by workspace packages
|
||||
# These packages are used in the workspace but dependencies are provided by parent package.json
|
||||
extract_deps_from_code "packages/client" packages_client_used_code.txt
|
||||
extract_deps_from_code "packages/api" packages_api_used_code.txt
|
||||
|
||||
- name: Get @librechat/client dependencies
|
||||
id: get-librechat-client-deps
|
||||
run: |
|
||||
if [[ -f "packages/client/package.json" ]]; then
|
||||
# Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
|
||||
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
|
||||
|
||||
# Combine all dependencies
|
||||
echo "$DEPS" > librechat_client_deps.txt
|
||||
echo "$DEV_DEPS" >> librechat_client_deps.txt
|
||||
echo "$PEER_DEPS" >> librechat_client_deps.txt
|
||||
|
||||
# Also include dependencies that are imported in packages/client
|
||||
cat packages_client_used_code.txt >> librechat_client_deps.txt
|
||||
|
||||
# Remove empty lines and sort
|
||||
grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
|
||||
mv temp_deps.txt librechat_client_deps.txt
|
||||
else
|
||||
touch librechat_client_deps.txt
|
||||
fi
|
||||
|
||||
- name: Get @librechat/api dependencies
|
||||
id: get-librechat-api-deps
|
||||
run: |
|
||||
if [[ -f "packages/api/package.json" ]]; then
|
||||
# Get all dependencies from @librechat/api (dependencies, devDependencies, and peerDependencies)
|
||||
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
|
||||
|
||||
# Combine all dependencies
|
||||
echo "$DEPS" > librechat_api_deps.txt
|
||||
echo "$DEV_DEPS" >> librechat_api_deps.txt
|
||||
echo "$PEER_DEPS" >> librechat_api_deps.txt
|
||||
|
||||
# Also include dependencies that are imported in packages/api
|
||||
cat packages_api_used_code.txt >> librechat_api_deps.txt
|
||||
|
||||
# Remove empty lines and sort
|
||||
grep -v '^$' librechat_api_deps.txt | sort -u > temp_deps.txt
|
||||
mv temp_deps.txt librechat_api_deps.txt
|
||||
else
|
||||
touch librechat_api_deps.txt
|
||||
fi
|
||||
|
||||
- name: Extract Workspace Dependencies
|
||||
id: extract-workspace-deps
|
||||
run: |
|
||||
# Function to get dependencies from a workspace package that are used by another package
|
||||
get_workspace_package_deps() {
|
||||
local package_json=$1
|
||||
local output_file=$2
|
||||
|
||||
# Get all workspace dependencies (starting with @librechat/)
|
||||
if [[ -f "$package_json" ]]; then
|
||||
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
|
||||
|
||||
# For each workspace dependency, get its dependencies
|
||||
for dep in $workspace_deps; do
|
||||
# Convert @librechat/api to packages/api
|
||||
local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
|
||||
local workspace_package_json="${workspace_path}/package.json"
|
||||
|
||||
if [[ -f "$workspace_package_json" ]]; then
|
||||
# Extract all dependencies from the workspace package
|
||||
jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
|
||||
# Also extract peerDependencies
|
||||
jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -f "$output_file" ]]; then
|
||||
sort -u "$output_file" -o "$output_file"
|
||||
else
|
||||
touch "$output_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get workspace dependencies for each package
|
||||
get_workspace_package_deps "package.json" root_workspace_deps.txt
|
||||
get_workspace_package_deps "client/package.json" client_workspace_deps.txt
|
||||
get_workspace_package_deps "api/package.json" api_workspace_deps.txt
|
||||
|
||||
- name: Run depcheck for root package.json
|
||||
id: check-root
|
||||
run: |
|
||||
if [[ -f "package.json" ]]; then
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, and workspace packages
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "")
|
||||
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Run depcheck for client/package.json
|
||||
id: check-client
|
||||
run: |
|
||||
if [[ -f "client/package.json" ]]; then
|
||||
chmod -R 755 client
|
||||
cd client
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/client imports
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../librechat_client_deps.txt 2>/dev/null | sort -u) || echo "")
|
||||
# Filter out false positives
|
||||
UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
|
||||
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
cd ..
|
||||
fi
|
||||
|
||||
- name: Run depcheck for api/package.json
|
||||
id: check-api
|
||||
run: |
|
||||
if [[ -f "api/package.json" ]]; then
|
||||
chmod -R 755 api
|
||||
cd api
|
||||
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
|
||||
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/api imports
|
||||
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../librechat_api_deps.txt 2>/dev/null | sort -u) || echo "")
|
||||
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
|
||||
echo "$UNUSED" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
cd ..
|
||||
fi
|
||||
|
||||
- name: Post comment on PR if unused dependencies are found
|
||||
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
|
||||
run: |
|
||||
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||
|
||||
ROOT_LIST=$(echo "$ROOT_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||
CLIENT_LIST=$(echo "$CLIENT_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||
API_LIST=$(echo "$API_UNUSED" | awk '{print "- `" $0 "`"}')
|
||||
|
||||
COMMENT_BODY=$(cat <<EOF
|
||||
### 🚨 Unused NPM Packages Detected
|
||||
|
||||
The following **unused dependencies** were found:
|
||||
|
||||
$(if [[ ! -z "$ROOT_UNUSED" ]]; then echo "#### 📂 Root \`package.json\`"; echo ""; echo "$ROOT_LIST"; echo ""; fi)
|
||||
|
||||
$(if [[ ! -z "$CLIENT_UNUSED" ]]; then echo "#### 📂 Client \`client/package.json\`"; echo ""; echo "$CLIENT_LIST"; echo ""; fi)
|
||||
|
||||
$(if [[ ! -z "$API_UNUSED" ]]; then echo "#### 📂 API \`api/package.json\`"; echo ""; echo "$API_LIST"; echo ""; fi)
|
||||
|
||||
⚠️ **Please remove these unused dependencies to keep your project clean.**
|
||||
EOF
|
||||
)
|
||||
|
||||
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
-f body="$COMMENT_BODY" \
|
||||
-H "Authorization: token $GITHUB_TOKEN"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fail workflow if unused dependencies found
|
||||
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
|
||||
run: exit 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue