chore: import upstream snapshot with attribution
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
name: Lint for accessibility issues
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'client/src/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_workflow:
|
||||
description: 'Set to true to run this workflow'
|
||||
required: true
|
||||
default: 'false'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
axe-linter:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_workflow == 'true')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dequelabs/axe-linter-action@v1
|
||||
with:
|
||||
api_key: ${{ secrets.AXE_LINTER_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Agents Integration Tests
|
||||
|
||||
# Runs the packages/api `src/agents/**` integration specs (e.g. the durable HITL
|
||||
# checkpointer against a real in-process MongoDB via mongodb-memory-server). These
|
||||
# are `*.integration.spec.ts`, which `test:ci` deliberately excludes — without this
|
||||
# job they run nowhere and their regressions guard nothing.
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-staging
|
||||
- release/*
|
||||
paths:
|
||||
- 'packages/api/src/agents/**'
|
||||
- 'packages/api/package.json'
|
||||
- '.github/workflows/agents-integration-tests.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
agents_integration_tests:
|
||||
name: Integration Tests that use in-process MongoDB
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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: Run agents integration tests (in-process MongoDB)
|
||||
working-directory: packages/api
|
||||
env:
|
||||
NODE_ENV: test
|
||||
run: npm run test:agents-integration
|
||||
@@ -0,0 +1,426 @@
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'packages/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_ENV: CI
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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: Upload data-provider build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload data-schemas build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload api build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-api
|
||||
path: packages/api/dist
|
||||
retention-days: 2
|
||||
|
||||
typecheck:
|
||||
name: TypeScript type checks
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Download api build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-api
|
||||
path: packages/api/dist
|
||||
|
||||
- name: Type check data-provider
|
||||
run: npx tsc --noEmit -p packages/data-provider/tsconfig.json
|
||||
|
||||
- name: Type check data-schemas
|
||||
run: npx tsc --noEmit -p packages/data-schemas/tsconfig.json
|
||||
|
||||
- name: Type check @librechat/api
|
||||
run: npx tsc --noEmit -p packages/api/tsconfig.json
|
||||
|
||||
- name: Type check @librechat/client
|
||||
run: npx tsc --noEmit -p packages/client/tsconfig.json
|
||||
|
||||
circular-deps:
|
||||
name: Circular dependency checks
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Rebuild @librechat/api and check for circular dependencies
|
||||
run: |
|
||||
output=$(npm run build:api 2>&1)
|
||||
echo "$output"
|
||||
if echo "$output" | grep -q "Circular depend"; then
|
||||
echo "Error: Circular dependency detected in @librechat/api!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Detect circular dependencies in rollup
|
||||
working-directory: ./packages/data-provider
|
||||
run: |
|
||||
output=$(npm run rollup:api)
|
||||
echo "$output"
|
||||
if echo "$output" | grep -q "Circular dependency"; then
|
||||
echo "Error: Circular dependency detected!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test-api:
|
||||
name: 'Tests: api (shard ${{ matrix.shard }}/3)'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
env:
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
BAN_VIOLATIONS: ${{ secrets.BAN_VIOLATIONS }}
|
||||
BAN_DURATION: ${{ secrets.BAN_DURATION }}
|
||||
BAN_INTERVAL: ${{ secrets.BAN_INTERVAL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Download api build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-api
|
||||
path: packages/api/dist
|
||||
|
||||
- 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 unit tests (shard ${{ matrix.shard }}/3)
|
||||
run: cd api && npm run test:ci -- --shard=${{ matrix.shard }}/3
|
||||
|
||||
test-data-provider:
|
||||
name: 'Tests: data-provider'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd packages/data-provider && npm run test:ci
|
||||
|
||||
test-data-schemas:
|
||||
name: 'Tests: data-schemas'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd packages/data-schemas && npm run test:ci
|
||||
|
||||
test-packages-api:
|
||||
name: 'Tests: @librechat/api (shard ${{ matrix.shard }}/4)'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# Suite typically completes in ~5 min on a warm runner, but tail-latency
|
||||
# cancellations have started showing up: tests are actively passing right
|
||||
# up to the timeout, then the job is killed mid-suite. Sharding splits the
|
||||
# suite across runners; per-shard headroom still absorbs runner variance.
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download data-schemas build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-schemas
|
||||
path: packages/data-schemas/dist
|
||||
|
||||
- name: Download api build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-api
|
||||
path: packages/api/dist
|
||||
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/4)
|
||||
run: cd packages/api && npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Linux_Container_Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Build GitHub Runner container image'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker build --build-arg RUNNER_VERSION=${{ env.RUNNER_VERSION }} -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }} .
|
||||
|
||||
- name: 'Push container image to ACR'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
@@ -0,0 +1,133 @@
|
||||
name: Cache Integration Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-staging
|
||||
- release/*
|
||||
paths:
|
||||
- 'packages/api/src/cache/**'
|
||||
- 'packages/api/src/cluster/**'
|
||||
- 'packages/api/src/mcp/**'
|
||||
- 'packages/api/src/stream/**'
|
||||
- 'redis-config/**'
|
||||
- '.github/workflows/cache-integration-tests.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cache_integration_tests:
|
||||
name: Integration Tests that use actual Redis Cache
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install Redis tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y redis-server redis-tools
|
||||
|
||||
- name: Start Single Redis Instance
|
||||
run: |
|
||||
redis-server --daemonize yes --port 6379
|
||||
sleep 2
|
||||
# Verify single Redis is running
|
||||
redis-cli -p 6379 ping || exit 1
|
||||
|
||||
- name: Start Redis Cluster
|
||||
working-directory: redis-config
|
||||
run: |
|
||||
chmod +x start-cluster.sh stop-cluster.sh
|
||||
./start-cluster.sh
|
||||
sleep 10
|
||||
# Verify cluster is running
|
||||
redis-cli -p 7001 cluster info || exit 1
|
||||
redis-cli -p 7002 cluster info || exit 1
|
||||
redis-cli -p 7003 cluster info || exit 1
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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 }}-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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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: Run all cache integration tests (Single Redis Node)
|
||||
working-directory: packages/api
|
||||
env:
|
||||
NODE_ENV: test
|
||||
USE_REDIS: true
|
||||
USE_REDIS_CLUSTER: false
|
||||
REDIS_URI: redis://127.0.0.1:6379
|
||||
run: npm run test:cache-integration
|
||||
|
||||
- name: Run all cache integration tests (Redis Cluster)
|
||||
working-directory: packages/api
|
||||
env:
|
||||
NODE_ENV: test
|
||||
USE_REDIS: true
|
||||
USE_REDIS_CLUSTER: true
|
||||
REDIS_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
||||
run: npm run test:cache-integration
|
||||
|
||||
- name: Stop Redis Cluster
|
||||
if: always()
|
||||
working-directory: redis-config
|
||||
run: ./stop-cluster.sh || true
|
||||
|
||||
- name: Stop Single Redis Instance
|
||||
if: always()
|
||||
run: redis-cli -p 6379 shutdown || true
|
||||
@@ -0,0 +1,94 @@
|
||||
name: Publish `@librechat/client` to NPM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/client/package.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: false
|
||||
default: 'Manual publish requested'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pack:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install client dependencies
|
||||
run: cd packages/client && npm ci
|
||||
|
||||
- name: Build client
|
||||
run: cd packages/client && npm run build
|
||||
|
||||
- name: Check version change
|
||||
id: check
|
||||
working-directory: packages/client
|
||||
run: |
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
PUBLISHED_VERSION=$(npm view @librechat/client version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
|
||||
echo "No version change, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version changed, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Pack package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
working-directory: packages/client
|
||||
run: |
|
||||
mkdir -p "$GITHUB_WORKSPACE/npm-package"
|
||||
npm pack --pack-destination "$GITHUB_WORKSPACE/npm-package"
|
||||
|
||||
- name: Upload package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librechat-client-package
|
||||
path: npm-package/*.tgz
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
|
||||
publish-npm:
|
||||
needs: pack
|
||||
if: github.ref == 'refs/heads/main' && needs.pack.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish # Must match npm trusted publisher config
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for OIDC trusted publishing
|
||||
steps:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install npm with OIDC support
|
||||
run: npm install -g npm@11.14.1 --ignore-scripts
|
||||
|
||||
- name: Download package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: librechat-client-package
|
||||
path: npm-package
|
||||
|
||||
- name: Publish
|
||||
working-directory: npm-package
|
||||
run: npm publish *.tgz --access public --provenance
|
||||
@@ -0,0 +1,88 @@
|
||||
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/**'
|
||||
|
||||
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@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Publish `librechat-data-provider` to NPM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/data-provider/package.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: false
|
||||
default: 'Manual publish requested'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pack:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
- run: cd packages/data-provider && npm ci
|
||||
- run: cd packages/data-provider && npm run build
|
||||
- name: Pack package
|
||||
run: |
|
||||
mkdir -p npm-package
|
||||
cd packages/data-provider
|
||||
npm pack --pack-destination "$GITHUB_WORKSPACE/npm-package"
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librechat-data-provider-package
|
||||
path: npm-package/*.tgz
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
|
||||
publish-npm:
|
||||
needs: pack
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish # Must match npm trusted publisher config
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for OIDC trusted publishing
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install npm with OIDC support
|
||||
run: npm install -g npm@11.14.1 --ignore-scripts
|
||||
|
||||
- name: Download package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: librechat-data-provider-package
|
||||
path: npm-package
|
||||
|
||||
- name: Publish package
|
||||
working-directory: npm-package
|
||||
run: npm publish *.tgz --provenance
|
||||
@@ -0,0 +1,94 @@
|
||||
name: Publish `@librechat/data-schemas` to NPM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/data-schemas/package.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: false
|
||||
default: 'Manual publish requested'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pack:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd packages/data-schemas && npm ci
|
||||
|
||||
- name: Build
|
||||
run: cd packages/data-schemas && npm run build
|
||||
|
||||
- name: Check version change
|
||||
id: check
|
||||
working-directory: packages/data-schemas
|
||||
run: |
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
PUBLISHED_VERSION=$(npm view @librechat/data-schemas version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
|
||||
echo "No version change, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version changed, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Pack package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
working-directory: packages/data-schemas
|
||||
run: |
|
||||
mkdir -p "$GITHUB_WORKSPACE/npm-package"
|
||||
npm pack --pack-destination "$GITHUB_WORKSPACE/npm-package"
|
||||
|
||||
- name: Upload package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librechat-data-schemas-package
|
||||
path: npm-package/*.tgz
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
|
||||
publish-npm:
|
||||
needs: pack
|
||||
if: github.ref == 'refs/heads/main' && needs.pack.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish # Must match npm trusted publisher config
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for OIDC trusted publishing
|
||||
steps:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install npm with OIDC support
|
||||
run: npm install -g npm@11.14.1 --ignore-scripts
|
||||
|
||||
- name: Download package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: librechat-data-schemas-package
|
||||
path: npm-package
|
||||
|
||||
- name: Publish
|
||||
working-directory: npm-package
|
||||
run: npm publish *.tgz --access public --provenance
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Update Test Server
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Docker Dev Branch Images Build"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'danny-avila/LibreChat' &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev'))
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install SSH Key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.DO_SSH_PRIVATE_KEY }}
|
||||
known_hosts: ${{ secrets.DO_KNOWN_HOSTS }}
|
||||
|
||||
- name: Run update script on DigitalOcean Droplet
|
||||
env:
|
||||
DO_HOST: ${{ secrets.DO_HOST }}
|
||||
DO_USER: ${{ secrets.DO_USER }}
|
||||
run: |
|
||||
ssh ${DO_USER}@${DO_HOST} << EOF
|
||||
sudo -i -u danny bash << 'EEOF'
|
||||
cd ~/LibreChat && \
|
||||
git fetch origin main && \
|
||||
sudo npm run stop:deployed && \
|
||||
sudo docker images --format "{{.Repository}}:{{.ID}}" | grep -E "lc-dev|librechat" | cut -d: -f2 | xargs -r sudo docker rmi -f || true && \
|
||||
sudo npm run update:deployed && \
|
||||
git checkout dev && \
|
||||
git pull origin dev && \
|
||||
git checkout do-deploy && \
|
||||
git rebase dev && \
|
||||
sudo npm run start:deployed && \
|
||||
echo "Update completed. Application should be running now."
|
||||
EEOF
|
||||
EOF
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Deploy_GHRunner_Linux_ACI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
ACI_RESOURCE_GROUP: 'Demo-ACI-GitHub-Runners-RG'
|
||||
ACI_NAME: 'gh-runner-linux-01'
|
||||
DNS_NAME_LABEL: 'gh-lin-01'
|
||||
GH_OWNER: ${{ github.repository_owner }}
|
||||
GH_REPOSITORY: 'LibreChat' #Change here to deploy self hosted runner ACI to another repo.
|
||||
|
||||
jobs:
|
||||
deploy-gh-runner-aci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Deploy to Azure Container Instances'
|
||||
uses: 'azure/aci-deploy@v1'
|
||||
with:
|
||||
resource-group: ${{ env.ACI_RESOURCE_GROUP }}
|
||||
image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
name: ${{ env.ACI_NAME }}
|
||||
dns-name-label: ${{ env.DNS_NAME_LABEL }}
|
||||
environment-variables: GH_TOKEN=${{ secrets.PAT_TOKEN }} GH_OWNER=${{ env.GH_OWNER }} GH_REPOSITORY=${{ env.GH_REPOSITORY }}
|
||||
location: 'eastus'
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Docker Dev Branch Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.multi'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 130
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: lc-dev-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: lc-dev
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Compute build metadata
|
||||
run: |
|
||||
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Docker Dev Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.multi'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 130
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-dev-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat-dev
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Compute build metadata
|
||||
run: |
|
||||
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Docker Dev Staging Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: lc-dev-staging-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: lc-dev-staging
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Compute build metadata
|
||||
run: |
|
||||
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
@@ -0,0 +1,128 @@
|
||||
name: Docker Build Smoke Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/docker-smoke.yml'
|
||||
- '.dockerignore'
|
||||
- 'Dockerfile.multi'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'config/**'
|
||||
- 'skill/**'
|
||||
- 'packages/api/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/data-provider/**'
|
||||
- 'packages/data-schemas/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-smoke-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
client-package-target:
|
||||
name: Build Docker client package target
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build client package target
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.multi
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
target: client-package-build
|
||||
|
||||
api-runtime-smoke:
|
||||
name: API runtime smoke (production image boots)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Build the real production image (final `api-build` stage), which installs
|
||||
# with `npm ci --omit=dev` — the same prune that, in prod, exposed runtime
|
||||
# dependencies the tsdown bundle externalizes but were never declared.
|
||||
- name: Build production image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.multi
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: librechat-api-smoke:ci
|
||||
cache-from: type=gha,scope=docker-smoke-api
|
||||
cache-to: type=gha,mode=max,scope=docker-smoke-api
|
||||
|
||||
# Loads the entire externalized require graph of the built @librechat/api
|
||||
# bundle inside the pruned production image. A missing or ESM-incompatible
|
||||
# runtime dependency (e.g. the `get-stream` regression) fails here with a
|
||||
# non-zero exit — deterministically, with no database required.
|
||||
- name: Verify production image resolves all runtime modules
|
||||
run: |
|
||||
docker run --rm librechat-api-smoke:ci \
|
||||
node -e "require('@librechat/api'); require('@librechat/api/telemetry'); console.log('module resolution OK')"
|
||||
|
||||
# Boot the real entrypoint against a real MongoDB so the *entire* server
|
||||
# require graph loads (api/db throws at module scope without MONGO_URI, and
|
||||
# is imported before models/services/routes), then gate on /readyz AND the
|
||||
# container staying alive. /readyz only returns 200 after the post-listen
|
||||
# startup (initializeMCPs + checkMigrations) sets serverReady, and those
|
||||
# steps process.exit(1) on failure — so ANY startup crash (missing module,
|
||||
# ReferenceError, bad config, post-listen failure) fails the smoke.
|
||||
- name: Boot production image against MongoDB and poll /readyz
|
||||
run: |
|
||||
set -u
|
||||
docker network create lc-smoke
|
||||
docker run -d --name lc-mongo --network lc-smoke mongo:8.0.20
|
||||
docker run -d --name lc-api --network lc-smoke -p 3080:3080 \
|
||||
-e HOST=0.0.0.0 -e PORT=3080 \
|
||||
-e NODE_ENV=production \
|
||||
-e MONGO_URI=mongodb://lc-mongo:27017/LibreChat \
|
||||
-e CREDS_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
|
||||
-e CREDS_IV=0123456789abcdef0123456789abcdef \
|
||||
-e JWT_SECRET=docker-smoke-jwt-secret \
|
||||
-e JWT_REFRESH_SECRET=docker-smoke-jwt-refresh-secret \
|
||||
-e SEARCH=false \
|
||||
librechat-api-smoke:ci
|
||||
|
||||
healthy=""
|
||||
for i in $(seq 1 60); do
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' lc-api 2>/dev/null)" != "true" ]; then
|
||||
echo "::error::API container exited during startup (exit code $(docker inspect -f '{{.State.ExitCode}}' lc-api 2>/dev/null))"
|
||||
break
|
||||
fi
|
||||
if [ "$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:3080/readyz 2>/dev/null || true)" = "200" ]; then
|
||||
healthy="yes"
|
||||
echo "/readyz returned 200 — server fully booted (post-listen startup complete)."
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "----- last 100 lines of api container logs -----"
|
||||
docker logs lc-api 2>&1 | tail -100 || true
|
||||
echo "------------------------------------------------"
|
||||
docker rm -f lc-api lc-mongo >/dev/null 2>&1 || true
|
||||
docker network rm lc-smoke >/dev/null 2>&1 || true
|
||||
|
||||
if [ -z "$healthy" ]; then
|
||||
echo "::error::Production image failed to reach a ready /readyz within timeout"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,128 @@
|
||||
name: ESLint Code Quality Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-staging
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- '.github/workflows/eslint-ci.yml'
|
||||
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
npx eslint --no-error-on-unmatched-pattern \
|
||||
--config eslint.config.mjs \
|
||||
--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
|
||||
@@ -0,0 +1,300 @@
|
||||
name: Frontend Unit Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/data-provider/**'
|
||||
- '.github/workflows/frontend-review.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload client-package build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
retention-days: 2
|
||||
|
||||
typecheck:
|
||||
name: TypeScript type checks (client)
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Type check client
|
||||
run: npm run typecheck
|
||||
working-directory: client
|
||||
|
||||
test-packages-client:
|
||||
name: 'Tests: @librechat/client'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:ci
|
||||
working-directory: packages/client
|
||||
|
||||
test-ubuntu:
|
||||
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/4)'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v4
|
||||
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
|
||||
|
||||
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@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v4
|
||||
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
|
||||
|
||||
build-verify:
|
||||
name: Vite build verification
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Build client
|
||||
run: cd client && npm run build:ci
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 'generate_embeddings'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supabase/embeddings-generator@v0.0.5
|
||||
with:
|
||||
supabase-url: ${{ secrets.SUPABASE_URL }}
|
||||
supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
openai-key: ${{ secrets.OPENAI_DOC_EMBEDDINGS_KEY }}
|
||||
docs-root-path: 'docs'
|
||||
@@ -0,0 +1,91 @@
|
||||
# Removes a PR's GitNexus index from the droplet when the PR is closed
|
||||
# (merged or not). The deploy workflow also prunes stale folders as a
|
||||
# safety net, but this gives us immediate cleanup without waiting for
|
||||
# the next deploy trigger.
|
||||
|
||||
name: GitNexus Cleanup PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: gitnexus-cleanup-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
# Skip fork PRs entirely. GitHub withholds repository secrets from
|
||||
# pull_request events originating on forks, so an SSH deploy job run
|
||||
# from a fork close would fail noisily. The deploy workflow's stale-
|
||||
# folder pruning step catches any fork-contributor indexes that
|
||||
# actually made it onto the droplet.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Skip the SSH round-trip entirely when no index artifact was ever
|
||||
# built for this PR (docs-only PRs, paths-ignored PRs, PRs closed
|
||||
# before indexing finished, etc). Eliminates ~95% of no-op SSH
|
||||
# sessions on a busy repo.
|
||||
- name: Check for index artifact
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: `gitnexus-index-pr-${context.payload.pull_request.number}`,
|
||||
per_page: 1,
|
||||
});
|
||||
const hasArtifact = data.total_count > 0;
|
||||
core.info(`Artifact exists: ${hasArtifact}`);
|
||||
core.setOutput('has_artifact', hasArtifact ? 'true' : 'false');
|
||||
|
||||
- name: Setup SSH
|
||||
if: steps.check.outputs.has_artifact == 'true'
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
|
||||
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
if [ -z "$KNOWN_HOST" ]; then
|
||||
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Remove PR index from droplet
|
||||
if: steps.check.outputs.has_artifact == 'true'
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" PR_NUM="$PR_NUM" bash <<'REMOTE'
|
||||
set -e
|
||||
TARGET="/opt/gitnexus/indexes/LibreChat-pr-$PR_NUM"
|
||||
if [ -d "$TARGET" ]; then
|
||||
echo "Removing $TARGET"
|
||||
rm -rf "$TARGET"
|
||||
cd /opt/gitnexus
|
||||
docker compose up -d --force-recreate gitnexus
|
||||
echo "GitNexus restarted without PR #$PR_NUM"
|
||||
else
|
||||
echo "No index to clean up for PR #$PR_NUM (artifact existed but droplet folder did not)"
|
||||
fi
|
||||
REMOTE
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
@@ -0,0 +1,583 @@
|
||||
# Deploys GitNexus indexes to a droplet via SSH + rsync.
|
||||
#
|
||||
# Architecture:
|
||||
# GitHub Actions (deploy)
|
||||
# 1. Resolves latest successful index runs for main and dev
|
||||
# 2. Downloads each matching .gitnexus/ artifact
|
||||
# 3. Rsyncs them into /opt/gitnexus/indexes/<name>/ on the droplet
|
||||
# 4. Removes any stale folders on the droplet that are not main/dev
|
||||
# 5. Pulls latest image, force-recreates gitnexus, reloads Caddy,
|
||||
# and polls docker health until the container reports healthy
|
||||
# The caddy container is untouched — no TLS churn.
|
||||
#
|
||||
# First-time droplet bootstrap (run once, manually):
|
||||
# 1. Create 2GB+ Ubuntu 24.04 droplet, add SSH key
|
||||
# 2. Point DNS A record for your subdomain at the droplet IP
|
||||
# 3. SSH in and run:
|
||||
# curl -fsSL https://get.docker.com | sh
|
||||
# systemctl enable --now docker
|
||||
# mkdir -p /opt/gitnexus/indexes
|
||||
# useradd -m -s /bin/bash deploy
|
||||
# usermod -aG docker deploy
|
||||
# mkdir -p /home/deploy/.ssh
|
||||
# # Add deploy pubkey to /home/deploy/.ssh/authorized_keys
|
||||
# chown -R deploy:deploy /home/deploy/.ssh /opt/gitnexus
|
||||
# chmod 700 /home/deploy/.ssh
|
||||
# ufw allow 22,80,443/tcp
|
||||
# ufw --force enable
|
||||
# 4. Copy .do/gitnexus/docker-compose.yml and Caddyfile into /opt/gitnexus/
|
||||
# 5. Create /opt/gitnexus/.env with: GITNEXUS_DOMAIN=... and API_TOKEN=...
|
||||
# 6. cd /opt/gitnexus && docker compose up -d
|
||||
#
|
||||
# Then capture the droplet's SSH host key from your workstation and
|
||||
# save it as the GITNEXUS_DO_KNOWN_HOST secret (below) so CI can pin it:
|
||||
# ssh-keyscan -H gitnexus.yourdomain.com
|
||||
#
|
||||
# GHCR image: the workflow runs `docker login ghcr.io` on the droplet
|
||||
# on every deploy using GITHUB_TOKEN, so the package can stay private.
|
||||
# If you'd rather not have CI manage droplet auth, make the package
|
||||
# public under repo Settings -> Packages.
|
||||
#
|
||||
# Required GitHub secrets:
|
||||
# GITNEXUS_DO_HOST — droplet IP or hostname
|
||||
# GITNEXUS_DO_USER — SSH user (e.g. "deploy")
|
||||
# GITNEXUS_DO_SSH_KEY — private key matching the authorized pubkey
|
||||
# GITNEXUS_DO_KNOWN_HOST — output of `ssh-keyscan -H <host>` pinning the
|
||||
# droplet's host keys (prevents MITM/TOFU risk)
|
||||
|
||||
name: GitNexus Deploy
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['GitNexus Index']
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Optional PR number for status comments from bot-triggered dispatches'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write # post status comments on PR command dispatches
|
||||
|
||||
# Global serialization. Earlier versions used per-ref concurrency with
|
||||
# cancel-in-progress so rapid pushes to the same ref coalesced but deploys
|
||||
# targeting different refs ran in parallel. That had a data race: the
|
||||
# prune-stale-indexes step computes its active_names up front, so if
|
||||
# deploy A is rsyncing /opt/gitnexus/indexes/LibreChat-pr-12580 while
|
||||
# deploy B (started slightly later with a different ref) prunes, B can
|
||||
# rm -rf a folder A is still uploading into.
|
||||
#
|
||||
# All deploys now queue behind a single group. cancel-in-progress is
|
||||
# false so a running rsync/docker-compose restart never gets killed
|
||||
# mid-operation (which would leave the droplet in a partial state).
|
||||
# The 20-minute job timeout bounds total queue depth.
|
||||
concurrency:
|
||||
group: gitnexus-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITNEXUS_VERSION: '1.6.7'
|
||||
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/librechat-gitnexus
|
||||
|
||||
jobs:
|
||||
# Rebuilds the long-lived image only when Dockerfile/entrypoint/extensions
|
||||
# change. Skipped on every other run, so index-only deploys are fast.
|
||||
build-image:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
(github.event.workflow_run.head_branch == 'main' ||
|
||||
github.event.workflow_run.head_branch == 'dev')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push image to GHCR
|
||||
outputs:
|
||||
image_tag: ${{ steps.tag.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect image changes
|
||||
id: changes
|
||||
run: |
|
||||
# Default to rebuild when we can't cleanly diff (first commit,
|
||||
# workflow_run from a PR branch where HEAD isn't the trigger, etc).
|
||||
# Rebuild on miss > skip when we should have rebuilt.
|
||||
if git rev-parse --verify HEAD~1 >/dev/null 2>&1 && \
|
||||
git diff --quiet HEAD~1 HEAD -- .do/gitnexus/Dockerfile .do/gitnexus/entrypoint.sh .do/gitnexus/install-extensions.js; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
run: echo "value=v${{ env.GITNEXUS_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .do/gitnexus
|
||||
file: .do/gitnexus/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.value }}
|
||||
build-args: |
|
||||
GITNEXUS_VERSION=${{ env.GITNEXUS_VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
needs: build-image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write # post deploy-complete comments on PR command dispatches
|
||||
steps:
|
||||
- name: Checkout deploy config
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .do/gitnexus
|
||||
fetch-depth: 1
|
||||
|
||||
# Resolve every index to serve. All resolutions go through
|
||||
# listArtifactsForRepo keyed by the expected artifact name, so a
|
||||
# run's branch or event type doesn't matter — we always pick the
|
||||
# freshest artifact that actually exists.
|
||||
#
|
||||
# Why this matters: a /gitnexus index command dispatches
|
||||
# gitnexus-index.yml with ref=main and an input pr_number, which
|
||||
# produces a run whose head_branch is "main" but whose artifact
|
||||
# is gitnexus-index-pr-<N>. listWorkflowRuns(branch='main') would
|
||||
# happily return that run, and we'd then try to download a
|
||||
# nonexistent gitnexus-index-main artifact from it. Querying by
|
||||
# artifact name directly avoids the whole mess.
|
||||
- name: Resolve indexes to serve
|
||||
id: resolve
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const serve = []; // [{ name, artifactName, runId }]
|
||||
|
||||
// Helper — pick the newest non-expired artifact matching a name.
|
||||
const latestArtifact = async (artifactName) => {
|
||||
const { data } = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: artifactName,
|
||||
per_page: 10,
|
||||
});
|
||||
return data.artifacts
|
||||
.filter((a) => !a.expired)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
|
||||
};
|
||||
|
||||
// --- main and dev branches ---
|
||||
for (const [branch, name] of [
|
||||
['main', 'LibreChat'],
|
||||
['dev', 'LibreChat-dev'],
|
||||
]) {
|
||||
const artifactName = `gitnexus-index-${branch}`;
|
||||
const fresh = await latestArtifact(artifactName);
|
||||
if (!fresh) {
|
||||
core.warning(`No artifact found for ${branch} (expected ${artifactName})`);
|
||||
continue;
|
||||
}
|
||||
serve.push({
|
||||
name,
|
||||
artifactName,
|
||||
runId: fresh.workflow_run.id,
|
||||
});
|
||||
core.info(`${branch}: run ${fresh.workflow_run.id} -> ${name}`);
|
||||
}
|
||||
|
||||
core.info('PR index deploys are paused; serving main and dev only.');
|
||||
|
||||
if (!serve.length) {
|
||||
core.setFailed('No indexes to serve');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('matrix', JSON.stringify(serve));
|
||||
core.setOutput('active_names', serve.map((s) => s.name).join(','));
|
||||
|
||||
- name: Download each index artifact
|
||||
env:
|
||||
MATRIX: ${{ steps.resolve.outputs.matrix }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p staging
|
||||
# main/dev artifact download failures are fatal — a missing
|
||||
# main/dev index is a real deploy failure. PR artifact failures
|
||||
# are soft — a PR artifact deleted mid-deploy shouldn't abort
|
||||
# the whole deploy and take main/dev down with it.
|
||||
echo "$MATRIX" | jq -c '.[]' | while read -r entry; do
|
||||
name=$(echo "$entry" | jq -r '.name')
|
||||
artifact=$(echo "$entry" | jq -r '.artifactName')
|
||||
runId=$(echo "$entry" | jq -r '.runId')
|
||||
target="staging/${name}/.gitnexus"
|
||||
echo "Downloading $artifact from run $runId -> $target"
|
||||
mkdir -p "$target"
|
||||
if ! gh run download "$runId" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--name "$artifact" \
|
||||
--dir "$target"; then
|
||||
case "$name" in
|
||||
LibreChat|LibreChat-dev)
|
||||
echo "::error::Failed to download critical artifact $artifact"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# The name stays in active_names so the prune step
|
||||
# won't remove the droplet's existing copy. The old
|
||||
# index keeps being served instead of being wiped to
|
||||
# nothing — stale beats empty — but observability
|
||||
# requires an explicit notice since this path is
|
||||
# invisible in the happy-path deploy log.
|
||||
echo "::warning::Failed to download PR artifact $artifact — skipping fresh sync; previous index (if any) will continue being served from the droplet"
|
||||
rm -rf "staging/${name}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Staged for rsync:"
|
||||
du -sh staging/*/.gitnexus/ 2>/dev/null || echo "(none)"
|
||||
|
||||
- name: Setup SSH
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
|
||||
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
# Pin the droplet's SSH host key from a repository secret instead
|
||||
# of trusting whatever ssh-keyscan returns at deploy time. The
|
||||
# secret is populated from `ssh-keyscan -H <host>` at bootstrap.
|
||||
if [ -z "$KNOWN_HOST" ]; then
|
||||
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty. Run ssh-keyscan -H <host> and paste the output as this secret."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Authenticate droplet with GHCR
|
||||
# GHCR packages pushed by GITHUB_TOKEN start private. The droplet
|
||||
# pulls the image on every deploy, so we re-authenticate it here
|
||||
# using the same short-lived token. If the package is public, this
|
||||
# step is redundant but harmless.
|
||||
#
|
||||
# The token MUST travel through SSH stdin (not as a command arg)
|
||||
# so it's never visible in the droplet's process table via
|
||||
# /proc/<pid>/cmdline. `printf '%s'` is preferred over `echo`
|
||||
# so the exact byte sequence sent is explicit — docker login
|
||||
# tolerates a trailing newline but `printf` makes the intent
|
||||
# obvious and portable across shells.
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
printf '%s' "$GH_TOKEN" | ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"docker login ghcr.io -u '$GH_ACTOR' --password-stdin"
|
||||
|
||||
- name: Upload config files
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
run: |
|
||||
rsync -az -e "ssh -i ~/.ssh/deploy_key" \
|
||||
.do/gitnexus/docker-compose.yml \
|
||||
.do/gitnexus/Caddyfile \
|
||||
"$SSH_USER@$SSH_HOST:/opt/gitnexus/"
|
||||
|
||||
- name: Prune stale indexes then sync fresh ones
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
ACTIVE_NAMES: ${{ steps.resolve.outputs.active_names }}
|
||||
run: |
|
||||
set -e
|
||||
# ── Step 1: prune FIRST ────────────────────────────────
|
||||
# Remove any folders on the droplet that aren't in the active set.
|
||||
# This frees disk BEFORE rsyncing new data, which matters on a
|
||||
# 10GB disk where each current index is ~400MB.
|
||||
echo "Pruning stale indexes (keeping: $ACTIVE_NAMES)"
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
ACTIVE_NAMES="$ACTIVE_NAMES" bash <<'REMOTE'
|
||||
set -e
|
||||
cd /opt/gitnexus/indexes || exit 0
|
||||
shopt -s nullglob
|
||||
IFS=',' read -ra ACTIVE <<< "$ACTIVE_NAMES"
|
||||
for dir in */; do
|
||||
dir="${dir%/}"
|
||||
keep=false
|
||||
for a in "${ACTIVE[@]}"; do
|
||||
if [ "$dir" = "$a" ]; then keep=true; break; fi
|
||||
done
|
||||
if [ "$keep" = false ]; then
|
||||
echo "Removing stale index: $dir"
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done
|
||||
echo "Disk after prune:"
|
||||
df -h / | tail -1
|
||||
REMOTE
|
||||
|
||||
# ── Step 2: rsync-then-swap ─────────────────────────────
|
||||
# Upload each index to a temp directory, then atomically swap
|
||||
# it into place. If rsync fails, the old index survives intact
|
||||
# and the partial temp dir is cleaned up — no production data
|
||||
# is lost. The brief period where both old + new exist costs
|
||||
# ~400MB of extra disk, but the prune step already freed
|
||||
# space from evicted indexes so this fits on a 10GB disk.
|
||||
for dir in staging/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
name=$(basename "$dir")
|
||||
echo "Syncing $name (rsync-then-swap)"
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"mkdir -p /opt/gitnexus/indexes/${name}.new"
|
||||
if rsync -az -e "ssh -i ~/.ssh/deploy_key" \
|
||||
"$dir" \
|
||||
"$SSH_USER@$SSH_HOST:/opt/gitnexus/indexes/${name}.new/"; then
|
||||
# Swap: remove old, rename new into place
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"rm -rf /opt/gitnexus/indexes/$name && mv /opt/gitnexus/indexes/${name}.new /opt/gitnexus/indexes/$name"
|
||||
echo " $name swapped successfully"
|
||||
else
|
||||
# Clean up the partial temp dir
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"rm -rf /opt/gitnexus/indexes/${name}.new"
|
||||
# main/dev are critical — abort the deploy so the failure
|
||||
# is visible and the container isn't restarted with stale
|
||||
# or missing data. PR indexes are best-effort.
|
||||
case "$name" in
|
||||
LibreChat|LibreChat-dev)
|
||||
echo "::error::rsync failed for critical index $name — aborting deploy"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "::warning::rsync failed for PR index $name — keeping previous index"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Pull image, restart gitnexus, reload Caddy, wait for healthy
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" bash <<'REMOTE'
|
||||
set -e
|
||||
cd /opt/gitnexus
|
||||
|
||||
# ── Disk cleanup ──────────────────────────────────────
|
||||
# Docker accumulates old image layers, dangling images, and
|
||||
# build cache across deploys. This droplet is only ~8.7GB
|
||||
# usable with a 700MB+ gitnexus image, so disk pressure is
|
||||
# constant. Prune everything not used by currently-running
|
||||
# containers BEFORE pulling the new image so the extract has
|
||||
# room; the post-recreate prune below reclaims the old image.
|
||||
echo "Disk before cleanup:"
|
||||
df -h / | tail -1
|
||||
# Omit --volumes: Caddy's caddy-data and caddy-config volumes
|
||||
# hold TLS certificates and ACME state. If Caddy happens to be
|
||||
# stopped when this runs (the workflow handles that case later),
|
||||
# --volumes would wipe them, forcing Let's Encrypt re-issuance
|
||||
# and risking rate-limit lockout (5 certs/domain/week).
|
||||
docker system prune -af 2>/dev/null || true
|
||||
echo "Disk after cleanup:"
|
||||
df -h / | tail -1
|
||||
|
||||
# Fail fast if disk is critically low even after prune. The
|
||||
# gitnexus image is ~700MB and shares most layers with the
|
||||
# running one, so an incremental pull needs well under 1GB.
|
||||
# 1536MB leaves headroom on this small droplet without the
|
||||
# over-conservative 2GB guard aborting on a healthy box.
|
||||
AVAIL_MB=$(df --output=avail -m / | tail -1 | tr -d ' ')
|
||||
if [ "$AVAIL_MB" -lt 1536 ]; then
|
||||
echo "::error::Disk critically low (${AVAIL_MB}MB free). Aborting deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose pull gitnexus
|
||||
docker compose up -d --force-recreate gitnexus
|
||||
|
||||
# The previous gitnexus image is now dangling (the running
|
||||
# container was recreated onto the freshly pulled image). The
|
||||
# pre-pull prune above couldn't touch it because it was still
|
||||
# in use at that point. Reclaim it now so the old generation
|
||||
# doesn't accumulate — critical on this 10GB droplet.
|
||||
docker image prune -f 2>/dev/null || true
|
||||
|
||||
# Reload Caddy in-place so a changed Caddyfile takes effect
|
||||
# without losing TLS certs or restarting connections. If caddy
|
||||
# isn't running yet (first-time bootstrap), bring it up.
|
||||
if docker compose ps --status running caddy 2>/dev/null | grep -q caddy; then
|
||||
echo "Reloading Caddy config"
|
||||
docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || {
|
||||
echo "Caddy reload failed — forcing restart"
|
||||
docker compose up -d --force-recreate caddy
|
||||
}
|
||||
else
|
||||
echo "Caddy not running — starting"
|
||||
docker compose up -d caddy
|
||||
fi
|
||||
|
||||
# Poll gitnexus health until ready or timeout. Docker's own
|
||||
# unhealthy detection takes up to 150s (start_period 60s +
|
||||
# retries 3 * interval 30s), so the poll ceiling must clear
|
||||
# that to avoid false negatives when gitnexus legitimately
|
||||
# takes ~2.5 min to warm up.
|
||||
# Max wait = 36 sleeps * 5s = 180s (final iteration exits
|
||||
# before its sleep on failure, so 37 iterations is the
|
||||
# correct upper bound for a true 180s ceiling).
|
||||
echo "Waiting for gitnexus to report healthy..."
|
||||
for i in $(seq 1 37); do
|
||||
STATUS=$(docker inspect --format='{{.State.Health.Status}}' gitnexus 2>/dev/null || echo unknown)
|
||||
echo "[$i/37] gitnexus health: $STATUS"
|
||||
if [ "$STATUS" = "healthy" ]; then
|
||||
echo "gitnexus is healthy"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 37 ]; then
|
||||
echo "ERROR: gitnexus failed to become healthy after 180s"
|
||||
docker compose ps
|
||||
docker compose logs --tail 80 gitnexus
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker compose ps
|
||||
echo "--- Caddy logs (last 20 lines) ---"
|
||||
docker compose logs --tail 20 caddy || true
|
||||
echo "--- GitNexus logs (last 30 lines) ---"
|
||||
docker compose logs --tail 30 gitnexus || true
|
||||
REMOTE
|
||||
|
||||
# When the deploy was triggered by a PR command path, post a
|
||||
# terminal status comment on that one PR only. Two sub-cases:
|
||||
#
|
||||
# 1. workflow_run trigger: the PR's native auto-index run fired
|
||||
# workflow_run, so github.event.workflow_run.id is the trigger.
|
||||
# Find the matching PR via the matrix entry whose runId matches.
|
||||
#
|
||||
# 2. workflow_dispatch trigger with inputs.pr_number set: the
|
||||
# index workflow's bot-fallback path dispatched us directly
|
||||
# because workflow_run is suppressed for GITHUB_TOKEN triggers.
|
||||
# Use inputs.pr_number as the comment target.
|
||||
#
|
||||
# Broadcast-commenting on every active PR would be noise — only the
|
||||
# PR that asked for a fresh index gets a reply.
|
||||
- name: Comment on PR — deploy complete
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
MATRIX: ${{ steps.resolve.outputs.matrix }}
|
||||
TRIGGER_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number }}
|
||||
DEPLOY_STATUS: ${{ job.status }}
|
||||
with:
|
||||
script: |
|
||||
const deployUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const matrix = JSON.parse(process.env.MATRIX || '[]');
|
||||
let prNum = null;
|
||||
|
||||
// Case 1: dispatched directly with pr_number (bot-fallback path)
|
||||
if (process.env.DISPATCH_PR_NUMBER && process.env.DISPATCH_PR_NUMBER !== '') {
|
||||
const dispatchPrRaw = process.env.DISPATCH_PR_NUMBER;
|
||||
if (!/^\d+$/.test(dispatchPrRaw)) {
|
||||
core.setFailed(`Invalid PR number: ${dispatchPrRaw}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatchPrNum = Number(dispatchPrRaw);
|
||||
const servedPr = matrix.some((m) => m.name === `LibreChat-pr-${dispatchPrNum}`);
|
||||
|
||||
if (!servedPr) {
|
||||
const body = [
|
||||
'### GitNexus: PR deploy skipped',
|
||||
'',
|
||||
'PR-specific deploys are paused; only `LibreChat` and `LibreChat-dev` are currently served.',
|
||||
`[Deploy run](${deployUrl})`,
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: dispatchPrNum,
|
||||
body,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
prNum = dispatchPrNum;
|
||||
}
|
||||
// Case 2: workflow_run trigger from a PR index run
|
||||
else if (context.eventName === 'workflow_run') {
|
||||
const triggerRunId = Number(process.env.TRIGGER_RUN_ID);
|
||||
const match = matrix.find(
|
||||
(m) => m.runId === triggerRunId && m.name.startsWith('LibreChat-pr-'),
|
||||
);
|
||||
if (match) {
|
||||
prNum = parseInt(match.name.replace('LibreChat-pr-', ''), 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (!prNum) {
|
||||
core.info('No PR to comment on (trigger was not a PR-scoped index); skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = process.env.DEPLOY_STATUS === 'success';
|
||||
const body = [
|
||||
`### GitNexus: ${ok ? '🚀 deployed' : '❌ deploy failed'}`,
|
||||
'',
|
||||
ok
|
||||
? `The \`LibreChat-pr-${prNum}\` index is now live on the MCP server.`
|
||||
: `The deploy failed — the previous index (if any) continues to be served.`,
|
||||
`[Deploy run](${deployUrl})`,
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNum,
|
||||
body,
|
||||
});
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
@@ -0,0 +1,323 @@
|
||||
name: GitNexus Index
|
||||
|
||||
on:
|
||||
# PR branches are NOT auto-indexed — an embeddings run is too slow to
|
||||
# spend on every PR push. Only main/dev are indexed automatically;
|
||||
# individual PRs are indexed on demand via the /gitnexus command or a
|
||||
# manual workflow_dispatch.
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
embeddings:
|
||||
description: 'Enable embedding generation (slow, increases index size)'
|
||||
type: boolean
|
||||
default: false
|
||||
force:
|
||||
description: 'Force full re-index'
|
||||
type: boolean
|
||||
default: false
|
||||
# When invoked from the /gitnexus index PR command, the command
|
||||
# workflow fills these so the index is built from the PR's head
|
||||
# ref and uploaded under the PR-numbered artifact name.
|
||||
pr_number:
|
||||
description: 'PR number to index (set by /gitnexus command)'
|
||||
type: string
|
||||
default: ''
|
||||
pr_ref:
|
||||
description: 'Optional PR head ref to check out; defaults to refs/pull/<pr_number>/head when pr_number is set'
|
||||
type: string
|
||||
default: ''
|
||||
deploy_after:
|
||||
description: 'Dispatch GitNexus Deploy after a successful index run'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# When triggered by the /gitnexus command, group by PR number so rapid
|
||||
# re-runs coalesce. Otherwise group by git ref as before.
|
||||
group: gitnexus-${{ inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITNEXUS_VERSION: '1.6.7'
|
||||
|
||||
jobs:
|
||||
index:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # read changed files to decide whether embeddings are needed
|
||||
# Push + dispatch run unconditionally. The pull_request trigger is
|
||||
# disabled (see `on:` above), so this never runs automatically on a
|
||||
# PR. PRs are indexed on demand instead:
|
||||
# - /gitnexus index (PR comment command, contributor-gated)
|
||||
# - workflow_dispatch (manual dispatch from Actions UI)
|
||||
# Both arrive as workflow_dispatch. The pull_request guard is kept as
|
||||
# a safety net should the trigger ever be re-added.
|
||||
if: |
|
||||
github.event_name != 'pull_request' ||
|
||||
github.event.pull_request.user.login == 'danny-avila'
|
||||
runs-on: ubuntu-latest
|
||||
# Embedding generation dominates the budget: ~45 min worst case on
|
||||
# standard runners since the 1.6.x graph (~23k nodes) doubled vs 1.5.x.
|
||||
timeout-minutes: 60
|
||||
# Best-effort index: a tool-internal crash must not block PRs. Fail soft on
|
||||
# PR events; push/dispatch runs still fail loudly so regressions stay visible.
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- name: Validate dispatch inputs
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
PR_REF: ${{ inputs.pr_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::pr_number must be numeric"
|
||||
exit 1
|
||||
fi
|
||||
EXPECTED_REF="refs/pull/${PR_NUMBER}/head"
|
||||
if [ -n "$PR_REF" ] && [ "$PR_REF" != "$EXPECTED_REF" ]; then
|
||||
echo "::error::pr_ref must match ${EXPECTED_REF}"
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "$PR_REF" ]; then
|
||||
echo "::error::pr_ref requires pr_number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve GitNexus flags
|
||||
id: flags
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
ENABLE_EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Decide whether to generate embeddings. Rules:
|
||||
# push (main/dev) -> always embed
|
||||
# pull_request -> embed ONLY when the PR changes files
|
||||
# under paths that also trigger backend
|
||||
# or frontend unit tests (api/, client/,
|
||||
# packages/). Docs/config-only PRs skip
|
||||
# embeddings to save ~3-5 min of CI.
|
||||
# workflow_dispatch -> respect the explicit `embeddings` input
|
||||
# (default false). This also covers the
|
||||
# /gitnexus index [embeddings] command.
|
||||
ENABLE_EMBEDDINGS=false
|
||||
case "$EVENT_NAME" in
|
||||
workflow_dispatch)
|
||||
[ "$ENABLE_EMBEDDINGS_INPUT" = "true" ] && ENABLE_EMBEDDINGS=true
|
||||
;;
|
||||
push)
|
||||
ENABLE_EMBEDDINGS=true
|
||||
;;
|
||||
pull_request)
|
||||
CHANGED=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM/files" \
|
||||
--paginate --jq '.[].filename' 2>/dev/null || echo "")
|
||||
if printf '%s\n' "$CHANGED" | grep -qE '^(api/|client/|packages/)'; then
|
||||
echo "PR #$PR_NUM touches unit-test paths (api|client|packages) — enabling embeddings"
|
||||
ENABLE_EMBEDDINGS=true
|
||||
else
|
||||
echo "PR #$PR_NUM does not touch unit-test paths — graph-only index"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
|
||||
echo "enable_embeddings=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "enable_embeddings=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install GitNexus CLI
|
||||
working-directory: ${{ runner.temp }}
|
||||
env:
|
||||
NPM_CONFIG_AUDIT: false
|
||||
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
|
||||
NPM_CONFIG_FUND: false
|
||||
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/gitnexus-cli" "$RUNNER_TEMP/gitnexus-npm-cache"
|
||||
: > "$RUNNER_TEMP/gitnexus-cli/global-npmrc"
|
||||
printf '%s\n' \
|
||||
'registry=https://registry.npmjs.org/' \
|
||||
'audit=false' \
|
||||
'fund=false' \
|
||||
> "$RUNNER_TEMP/gitnexus-cli/.npmrc"
|
||||
# Keep GitNexus' native DB dependency deterministic in fresh CI installs.
|
||||
npm install \
|
||||
--prefix "$RUNNER_TEMP/gitnexus-cli" \
|
||||
--no-save \
|
||||
--no-package-lock \
|
||||
"gitnexus@${{ env.GITNEXUS_VERSION }}" \
|
||||
"@ladybugdb/core@0.17.1"
|
||||
test -x "$RUNNER_TEMP/gitnexus-cli/node_modules/.bin/gitnexus"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# When the /gitnexus command dispatches us with a pr_ref, it's
|
||||
# a refs/pull/<N>/head ref that GitHub mirrors into the base
|
||||
# repo for every PR, so checkout works for fork PRs too. When
|
||||
# pr_ref is empty (native push/pull_request), fall back to the
|
||||
# default ref actions/checkout would use.
|
||||
ref: ${{ inputs.pr_ref || (inputs.pr_number != '' && format('refs/pull/{0}/head', inputs.pr_number) || '') }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
# HuggingFace throttles anonymous model downloads from shared GHA
|
||||
# runner IPs (429s or stalled transfers). Cache the embedding model
|
||||
# across runs so warm runs never touch HF at all.
|
||||
- name: Cache HuggingFace embedding model
|
||||
if: steps.flags.outputs.enable_embeddings == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/hf-cache
|
||||
key: hf-model-snowflake-arctic-embed-xs-v1
|
||||
|
||||
- name: Run GitNexus Analyze
|
||||
working-directory: ${{ runner.temp }}
|
||||
env:
|
||||
ENABLE_EMBEDDINGS: ${{ steps.flags.outputs.enable_embeddings }}
|
||||
FORCE: ${{ inputs.force }}
|
||||
GITNEXUS_BIN: ${{ runner.temp }}/gitnexus-cli/node_modules/.bin/gitnexus
|
||||
# Fail soft in ~2 min on stalled downloads instead of eating the
|
||||
# 25-min job budget; HF_TOKEN lifts the anonymous rate limit on
|
||||
# cold-cache runs (empty when the secret is unset — safe no-op).
|
||||
HF_DOWNLOAD_TIMEOUT_MS: '60000'
|
||||
HF_HOME: ${{ runner.temp }}/hf-cache
|
||||
HF_MAX_ATTEMPTS: '2'
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
NPM_CONFIG_AUDIT: false
|
||||
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
|
||||
NPM_CONFIG_FUND: false
|
||||
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
FLAGS=(--skip-agents-md --verbose)
|
||||
|
||||
if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
|
||||
FLAGS+=(--embeddings)
|
||||
fi
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
FLAGS+=(--force)
|
||||
fi
|
||||
"$GITNEXUS_BIN" analyze "$GITHUB_WORKSPACE" "${FLAGS[@]}"
|
||||
|
||||
- name: Verify index
|
||||
run: |
|
||||
if [ ! -d ".gitnexus" ] || [ ! -f ".gitnexus/meta.json" ]; then
|
||||
echo "::error::GitNexus index was not created"
|
||||
exit 1
|
||||
fi
|
||||
echo "::group::Index metadata"
|
||||
cat .gitnexus/meta.json
|
||||
echo ""
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Upload GitNexus index
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Artifact naming order of precedence:
|
||||
# 1. /gitnexus command dispatch: inputs.pr_number -> pr-<N>
|
||||
# 2. Native pull_request event: github.event.pull_request.number
|
||||
# 3. Push or manual dispatch without pr_number: github.ref_name
|
||||
name: >-
|
||||
gitnexus-index-${{
|
||||
inputs.pr_number != ''
|
||||
&& format('pr-{0}', inputs.pr_number)
|
||||
|| (github.event_name == 'pull_request'
|
||||
&& format('pr-{0}', github.event.pull_request.number)
|
||||
|| github.ref_name)
|
||||
}}
|
||||
path: .gitnexus/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
post-index:
|
||||
needs: index
|
||||
if: |
|
||||
always() &&
|
||||
(inputs.pr_number != '' ||
|
||||
inputs.deploy_after)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # dispatch gitnexus-deploy.yml when deploy_after is set
|
||||
pull-requests: write # post completion comments for /gitnexus command runs
|
||||
steps:
|
||||
# GitHub suppresses workflow_run events for workflow runs triggered
|
||||
# by GITHUB_TOKEN (to prevent recursive chaining). Dispatches without
|
||||
# a PR number can still opt into a deploy by setting deploy_after=true.
|
||||
- name: Trigger deploy workflow after non-PR dispatches
|
||||
if: inputs.deploy_after && inputs.pr_number == '' && needs.index.result == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.info('deploy_after=true; dispatching gitnexus-deploy.yml manually.');
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'gitnexus-deploy.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
pr_number: '',
|
||||
},
|
||||
});
|
||||
|
||||
# Reply on the PR when the /gitnexus command path runs so the
|
||||
# requester knows the index step finished. This fires when
|
||||
# inputs.pr_number is set and reports the index job result.
|
||||
- name: Comment on PR — index complete
|
||||
if: inputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
|
||||
INDEX_RESULT: ${{ needs.index.result }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const indexSucceeded = process.env.INDEX_RESULT === 'success';
|
||||
const outcome = indexSucceeded ? '✅ indexed' : '❌ index failed';
|
||||
const prNum = parseInt(process.env.PR_NUMBER || '', 10);
|
||||
if (!Number.isSafeInteger(prNum)) {
|
||||
core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`);
|
||||
return;
|
||||
}
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const embeddingsFlag = process.env.EMBEDDINGS_INPUT === 'true' ? 'with embeddings' : 'graph-only';
|
||||
const body = [
|
||||
`### GitNexus: ${outcome}`,
|
||||
``,
|
||||
`PR #${prNum} was indexed ${embeddingsFlag}.`,
|
||||
`[Index run](${runUrl})`,
|
||||
'',
|
||||
indexSucceeded
|
||||
? 'PR-specific deploys are paused; only `LibreChat` and `LibreChat-dev` are currently served.'
|
||||
: '_Index run failed — the previous index (if any) continues to be served._',
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNum,
|
||||
body,
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
# Responds to `/gitnexus index` comments on pull requests.
|
||||
#
|
||||
# Gated to the same author_association roles (OWNER, MEMBER, COLLABORATOR)
|
||||
# as the automatic PR index trigger, but applied to the COMMENTER, not
|
||||
# the PR author. This intentionally lets a contributor index a PR from
|
||||
# a non-contributor / first-time fork author — the contributor takes
|
||||
# responsibility for the trust boundary by typing the command.
|
||||
#
|
||||
# When a matching comment lands on a PR, this workflow dispatches
|
||||
# `gitnexus-index.yml` with the PR number and the `refs/pull/<N>/head`
|
||||
# ref so indexing works for fork PRs too (GitHub mirrors every PR's
|
||||
# head ref into the base repo regardless of which fork it originated
|
||||
# from, so actions/checkout can always resolve it).
|
||||
#
|
||||
# Use cases:
|
||||
# - Re-index a PR after a rebase without pushing a new commit
|
||||
# - Index a docs-only PR that was skipped by paths-ignore
|
||||
# - Index a non-contributor (fork) PR that the auto-trigger skipped
|
||||
# - Re-run a failed index
|
||||
#
|
||||
# Supported commands:
|
||||
# /gitnexus index — index the PR with embeddings (default)
|
||||
# /gitnexus index embeddings — explicit form of the above; same effect
|
||||
# /gitnexus index fast — graph-only index (skip embeddings), for
|
||||
# a quick re-index without waiting ~5 min
|
||||
# of embedding generation
|
||||
|
||||
name: GitNexus PR Command
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: write # needed to dispatch gitnexus-index.yml
|
||||
|
||||
concurrency:
|
||||
group: gitnexus-pr-command-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
# Only run for PR comments that start with /gitnexus from trusted
|
||||
# commenters. Intentionally checks the COMMENTER's association so a
|
||||
# contributor can index a non-contributor's PR on demand.
|
||||
if: |
|
||||
github.event.issue.pull_request != null &&
|
||||
startsWith(github.event.comment.body, '/gitnexus') &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Parse command and resolve PR head ref
|
||||
id: parse
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.comment.body.trim();
|
||||
const match = body.match(/^\/gitnexus\s+(\w+)(?:\s+(\w+))?/);
|
||||
if (!match) {
|
||||
core.setFailed(`Unrecognized command: ${body}. Try: /gitnexus index [fast]`);
|
||||
return;
|
||||
}
|
||||
const [, subcommand, modifier] = match;
|
||||
if (subcommand !== 'index') {
|
||||
core.setFailed(`Unknown subcommand: ${subcommand}. Only 'index' is supported.`);
|
||||
return;
|
||||
}
|
||||
// Default to embeddings on — a contributor typing the command
|
||||
// has already decided they want a full re-index. The `fast`
|
||||
// modifier is the explicit opt-out for graph-only runs.
|
||||
// `embeddings` is accepted as a no-op alias for backwards
|
||||
// compat with the previous command form.
|
||||
let embeddings = 'true';
|
||||
if (modifier === 'fast' || modifier === 'graph-only' || modifier === 'no-embeddings') {
|
||||
embeddings = 'false';
|
||||
}
|
||||
|
||||
// Use refs/pull/<N>/head instead of the raw head SHA. GitHub
|
||||
// mirrors every PR's head into the base repo as this ref, so
|
||||
// actions/checkout can always resolve it — even for PRs from
|
||||
// forks whose raw SHAs don't exist in the base repo.
|
||||
const prNum = context.payload.issue.number;
|
||||
core.setOutput('pr_number', String(prNum));
|
||||
core.setOutput('pr_ref', `refs/pull/${prNum}/head`);
|
||||
core.setOutput('embeddings', embeddings);
|
||||
core.info(
|
||||
`Dispatching index for PR #${prNum} at refs/pull/${prNum}/head (embeddings=${embeddings}, modifier=${modifier || '(none)'})`,
|
||||
);
|
||||
|
||||
- name: Dispatch gitnexus-index workflow
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EMBEDDINGS: ${{ steps.parse.outputs.embeddings }}
|
||||
PR_NUMBER: ${{ steps.parse.outputs.pr_number }}
|
||||
PR_REF: ${{ steps.parse.outputs.pr_ref }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = process.env.PR_NUMBER || '';
|
||||
const prRef = process.env.PR_REF || '';
|
||||
const embeddings = process.env.EMBEDDINGS || 'false';
|
||||
if (!/^[0-9]+$/.test(prNumber)) {
|
||||
core.setFailed(`Invalid PR number: ${prNumber}`);
|
||||
return;
|
||||
}
|
||||
if (prRef !== `refs/pull/${prNumber}/head`) {
|
||||
core.setFailed(`Invalid PR ref: ${prRef}`);
|
||||
return;
|
||||
}
|
||||
if (!['true', 'false'].includes(embeddings)) {
|
||||
core.setFailed(`Invalid embeddings value: ${embeddings}`);
|
||||
return;
|
||||
}
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'gitnexus-index.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
pr_number: prNumber,
|
||||
pr_ref: prRef,
|
||||
embeddings,
|
||||
force: 'false',
|
||||
deploy_after: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
- name: React to the comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket',
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Build Helm Charts on Tag
|
||||
|
||||
# The workflow is triggered when a tag is pushed
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "chart-*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
chart_tag:
|
||||
description: "Existing chart tag to release, for example chart-2.0.5"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CHART_REPOSITORY: ${{ github.repository_owner }}/librechat-chart
|
||||
steps:
|
||||
- name: Resolve chart tag
|
||||
id: chart-version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
INPUT_CHART_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.chart_tag || '' }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHART_TAG="$REF_NAME"
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
CHART_TAG="$INPUT_CHART_TAG"
|
||||
fi
|
||||
|
||||
CHART_VERSION="${CHART_TAG#chart-}"
|
||||
SEMVER_REGEX='^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?$'
|
||||
if [[ "$CHART_TAG" != chart-* || ! "$CHART_VERSION" =~ $SEMVER_REGEX ]]; then
|
||||
echo "::error::Chart tags must use the form chart-<semver>, for example chart-2.0.3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'CHART_REF=refs/tags/%s\n' "$CHART_TAG"
|
||||
printf 'CHART_TAG=%s\n' "$CHART_TAG"
|
||||
printf 'CHART_VERSION=%s\n' "$CHART_VERSION"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
ref: ${{ steps.chart-version.outputs.CHART_REF }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Build Subchart Deps
|
||||
run: |
|
||||
cd helm/librechat
|
||||
helm dependency build
|
||||
cd ../librechat-rag-api
|
||||
helm dependency build
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Run Helm OCI Charts Releaser
|
||||
# This is for the librechat chart
|
||||
- name: Release Helm OCI Charts for librechat
|
||||
uses: appany/helm-oci-chart-releaser@v0.4.2
|
||||
with:
|
||||
name: librechat
|
||||
repository: ${{ env.CHART_REPOSITORY }}
|
||||
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
|
||||
path: helm/librechat
|
||||
registry: ghcr.io
|
||||
registry_username: ${{ github.actor }}
|
||||
registry_password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# this is for the librechat-rag-api chart
|
||||
- name: Release Helm OCI Charts for librechat-rag-api
|
||||
uses: appany/helm-oci-chart-releaser@v0.4.2
|
||||
with:
|
||||
name: librechat-rag-api
|
||||
repository: ${{ env.CHART_REPOSITORY }}
|
||||
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
|
||||
path: helm/librechat-rag-api
|
||||
registry: ghcr.io
|
||||
registry_username: ${{ github.actor }}
|
||||
registry_password: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,150 @@
|
||||
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/**"
|
||||
|
||||
jobs:
|
||||
detect-unused-i18n-keys:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Langfuse Fanout
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/langfuse-fanout.yml'
|
||||
- 'otel/langfuse-fanout/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: langfuse-fanout-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
go-tests:
|
||||
name: Go tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: otel/langfuse-fanout/go.mod
|
||||
cache-dependency-path: otel/langfuse-fanout/go.sum
|
||||
|
||||
- name: Check Go formatting
|
||||
working-directory: otel/langfuse-fanout
|
||||
run: |
|
||||
unformatted="$(gofmt -l .)"
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "::error::Go files are not gofmt-formatted:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Go tests
|
||||
working-directory: otel/langfuse-fanout
|
||||
run: go test ./...
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Sync Locize Translations & Create Translation PR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
repository_dispatch:
|
||||
types: [locize/versionPublished]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-translations:
|
||||
name: Sync Translation Keys with Locize
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install locize CLI
|
||||
run: npm install -g locize-cli@12.2.0 --ignore-scripts --no-audit --no-fund
|
||||
|
||||
# Sync translations (Push missing keys & remove deleted ones)
|
||||
- name: Sync Locize with Repository
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
env:
|
||||
LOCIZE_API_KEY: ${{ secrets.LOCIZE_API_KEY }}
|
||||
LOCIZE_PROJECT_ID: ${{ secrets.LOCIZE_PROJECT_ID }}
|
||||
run: |
|
||||
cd client/src/locales
|
||||
locize sync --cdn-type pro --api-key "$LOCIZE_API_KEY" --project-id "$LOCIZE_PROJECT_ID" --language en
|
||||
|
||||
# When triggered by repository_dispatch, skip sync step.
|
||||
- name: Skip sync step on non-push events
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
run: echo "Skipping sync as the event is not a push."
|
||||
|
||||
create-pull-request:
|
||||
name: Create Translation PR on Version Published
|
||||
runs-on: ubuntu-latest
|
||||
needs: sync-translations
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
# 1. Check out the repository.
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# 2. Download translation files from locize.
|
||||
- name: Download Translations from locize
|
||||
uses: locize/download@v2
|
||||
with:
|
||||
project-id: ${{ secrets.LOCIZE_PROJECT_ID }}
|
||||
path: "client/src/locales"
|
||||
|
||||
# 3. Create a Pull Request using a dedicated fine-grained PAT so this
|
||||
# workflow does not depend on the global GITHUB_TOKEN PR-creation setting.
|
||||
- name: Create Pull Request
|
||||
id: create-pull-request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.LOCIZE_PR_TOKEN }}
|
||||
add-paths: |
|
||||
client/src/locales/**
|
||||
commit-message: "🌍 i18n: Update translation.json with latest translations"
|
||||
base: main
|
||||
branch: i18n/locize-translation-update
|
||||
title: "🌍 i18n: Update translation.json with latest translations"
|
||||
body: |
|
||||
**Description**:
|
||||
- 🎯 **Objective**: Update `translation.json` with the latest translations from locize.
|
||||
- 🔍 **Details**: This PR is automatically generated upon receiving a versionPublished event with version "latest". It reflects the newest translations provided by locize.
|
||||
- ✅ **Status**: Ready for review.
|
||||
labels: "🌍 i18n"
|
||||
|
||||
- name: Request Reviewer
|
||||
if: ${{ steps.create-pull-request.outputs.pull-request-number != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.LOCIZE_PR_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pull-request.outputs.pull-request-number }}
|
||||
REVIEWER: danny-avila
|
||||
run: |
|
||||
author="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author --jq '.author.login')"
|
||||
|
||||
if [ "$author" = "$REVIEWER" ]; then
|
||||
echo "Skipping reviewer request because $REVIEWER authored PR #$PR_NUMBER."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --add-reviewer "$REVIEWER"
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Docker Compose Build Latest Main Image Tag (Manual Dispatch)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch tags and set the latest tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force
|
||||
LATEST_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+[.][0-9]+[.][0-9]+$' | head -n 1)
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "::error::No stable v<semver> tag found"
|
||||
exit 1
|
||||
fi
|
||||
printf 'LATEST_TAG=%s\n' "$LATEST_TAG" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Compute build metadata
|
||||
run: |
|
||||
printf 'BUILD_COMMIT=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
printf 'BUILD_BRANCH=main\n' >> "$GITHUB_ENV"
|
||||
printf 'BUILD_DATE=%s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
@@ -0,0 +1,137 @@
|
||||
name: Playwright E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: false
|
||||
default: 'Manual e2e run'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: playwright-mock-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
E2E_CHROMIUM_CHANNEL: chrome
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
client/node_modules
|
||||
packages/client/node_modules
|
||||
packages/data-provider/node_modules
|
||||
packages/data-schemas/node_modules
|
||||
packages/api/node_modules
|
||||
api/node_modules
|
||||
key: node-modules-e2e-${{ 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@v4
|
||||
with:
|
||||
path: packages/data-provider/dist
|
||||
key: build-data-provider-${{ runner.os }}-${{ hashFiles('package-lock.json', '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@v4
|
||||
with:
|
||||
path: packages/data-schemas/dist
|
||||
key: build-data-schemas-${{ runner.os }}-${{ hashFiles('package-lock.json', '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@v4
|
||||
with:
|
||||
path: packages/api/dist
|
||||
key: build-api-${{ runner.os }}-${{ hashFiles('package-lock.json', '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: Restore client-package build cache
|
||||
id: cache-client-package
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/client/dist
|
||||
key: build-client-package-${{ runner.os }}-${{ hashFiles('package-lock.json', '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: Restore client app build cache
|
||||
id: cache-client-app
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: client/dist
|
||||
key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package-lock.json', 'client/src/**', 'client/public/**', 'client/scripts/post-build.cjs', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', '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 app
|
||||
if: steps.cache-client-app.outputs.cache-hit != 'true'
|
||||
run: npm run build:client
|
||||
|
||||
- name: Install Playwright runtime dependencies
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
google-chrome --version
|
||||
npx playwright install-deps chrome
|
||||
|
||||
- name: Run mock-LLM Tier-1 e2e
|
||||
run: npx playwright test --config=e2e/playwright.config.mock.ts
|
||||
env:
|
||||
CI: 'true'
|
||||
|
||||
- name: Upload Playwright HTML report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/**
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload traces & screenshots
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-test-results
|
||||
path: e2e/specs/.test-results/**
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,136 @@
|
||||
name: Retry Failed Docker Builds
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Docker Build Smoke Tests
|
||||
- Docker Compose Build Latest Main Image Tag (Manual Dispatch)
|
||||
- Docker Dev Branch Images Build
|
||||
- Docker Dev Images Build
|
||||
- Docker Dev Staging Images Build
|
||||
- Docker Images Build on Tag
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
retry-failed-jobs:
|
||||
name: Re-run failed jobs
|
||||
if: >
|
||||
(github.event.workflow_run.conclusion == 'failure' ||
|
||||
github.event.workflow_run.conclusion == 'timed_out') &&
|
||||
github.event.workflow_run.run_attempt < 3
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check failed run is still current
|
||||
id: current-run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
RUN_EVENT: ${{ github.event.workflow_run.event }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
WORKFLOW_ID: ${{ github.event.workflow_run.workflow_id }}
|
||||
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
encode_uri() {
|
||||
jq -nr --arg value "$1" '$value | @uri'
|
||||
}
|
||||
|
||||
workflow_runs_path="repos/${GITHUB_REPOSITORY}/actions/workflows/${WORKFLOW_ID}/runs"
|
||||
query="per_page=1&exclude_pull_requests=false"
|
||||
|
||||
if [ "$WORKFLOW_NAME" = "Docker Images Build on Tag" ]; then
|
||||
if [ -z "$HEAD_BRANCH" ]; then
|
||||
echo "No tag name found for ${WORKFLOW_NAME}; skipping retry."
|
||||
echo "should_retry=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! tag_ref=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/$(encode_uri "$HEAD_BRANCH")" --jq '.object'); then
|
||||
echo "Tag ${HEAD_BRANCH} no longer exists; skipping retry."
|
||||
echo "should_retry=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tag_object_type=$(jq -r '.type' <<< "$tag_ref")
|
||||
tag_object_sha=$(jq -r '.sha' <<< "$tag_ref")
|
||||
|
||||
if [ "$tag_object_type" = "tag" ]; then
|
||||
tag_head_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_object_sha}" --jq '.object.sha')
|
||||
else
|
||||
tag_head_sha="$tag_object_sha"
|
||||
fi
|
||||
|
||||
if [ "$tag_head_sha" = "$HEAD_SHA" ]; then
|
||||
echo "should_retry=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Skipping retry for stale ${WORKFLOW_NAME} run ${RUN_ID}; tag ${HEAD_BRANCH} now points to ${tag_head_sha}."
|
||||
echo "should_retry=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$RUN_EVENT" ]; then
|
||||
query="${query}&event=$(encode_uri "$RUN_EVENT")"
|
||||
fi
|
||||
|
||||
if [ -n "$HEAD_BRANCH" ]; then
|
||||
query_with_ref="${query}&branch=$(encode_uri "$HEAD_BRANCH")"
|
||||
latest_run=$(gh api "${workflow_runs_path}?${query_with_ref}" --jq '.workflow_runs[0] // empty')
|
||||
else
|
||||
latest_run=""
|
||||
fi
|
||||
|
||||
if [ -z "$latest_run" ]; then
|
||||
latest_run=$(gh api "${workflow_runs_path}?${query}" --jq '.workflow_runs[0] // empty')
|
||||
fi
|
||||
|
||||
if [ -z "$latest_run" ]; then
|
||||
echo "No matching workflow runs found for ${WORKFLOW_NAME}; skipping retry."
|
||||
echo "should_retry=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest_run_id=$(jq -r '.id' <<< "$latest_run")
|
||||
latest_head_sha=$(jq -r '.head_sha' <<< "$latest_run")
|
||||
|
||||
if [ "$latest_run_id" = "$RUN_ID" ] || [ "$latest_head_sha" = "$HEAD_SHA" ]; then
|
||||
echo "should_retry=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Skipping retry for stale ${WORKFLOW_NAME} run ${RUN_ID}; newer run ${latest_run_id} is at ${latest_head_sha}."
|
||||
echo "should_retry=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Re-run failed Docker jobs
|
||||
if: steps.current-run.outputs.should_retry == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cancelled_jobs=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?per_page=100" \
|
||||
| jq -s '[.[].jobs[]? | select(.conclusion == "cancelled")] | length')
|
||||
|
||||
if [ "$cancelled_jobs" -gt 0 ]; then
|
||||
endpoint="rerun"
|
||||
echo "Found ${cancelled_jobs} cancelled job(s); re-running the full workflow run."
|
||||
else
|
||||
endpoint="rerun-failed-jobs"
|
||||
echo "Re-running failed jobs only."
|
||||
fi
|
||||
|
||||
echo "Retrying ${WORKFLOW_NAME} (run ${RUN_ID}, attempt ${RUN_ATTEMPT})."
|
||||
gh api \
|
||||
--method POST \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/${endpoint}"
|
||||
@@ -0,0 +1,85 @@
|
||||
name: Sync Helm Chart Tags
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_existing_tag:
|
||||
description: "Existing chart-* tag to dispatch if tag creation succeeded but release dispatch failed"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: sync-helm-chart-tags
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
noop:
|
||||
name: Ignore non-main push
|
||||
if: github.event_name == 'push' && github.ref != 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Skip tag sync
|
||||
run: echo "Helm chart tag sync only runs on main pushes or manual dispatch."
|
||||
|
||||
sync:
|
||||
name: Sync chart tags
|
||||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
env:
|
||||
BASE_REF: refs/remotes/origin/main
|
||||
BACKFILL_FROM_VERSION: 1.9.0
|
||||
CHART_PATH: helm/librechat/Chart.yaml
|
||||
DEFAULT_BRANCH: main
|
||||
DISPATCH_WORKFLOW: helmcharts.yml
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
RELEASE_EXISTING_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_existing_tag || '' }}
|
||||
REPO_DIR: /tmp/librechat-sync
|
||||
TAG_PREFIX: chart-
|
||||
steps:
|
||||
- name: Fetch main and tags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
||||
echo "::error::Unexpected repository name: $GITHUB_REPOSITORY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$GITHUB_SERVER_URL" != "https://github.com" ]]; then
|
||||
echo "::error::Unexpected GitHub server URL: $GITHUB_SERVER_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$REPO_DIR"
|
||||
git init "$REPO_DIR"
|
||||
cd "$REPO_DIR"
|
||||
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
AUTH_HEADER="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
|
||||
git -c "http.extraheader=AUTHORIZATION: basic ${AUTH_HEADER}" \
|
||||
fetch --prune --force --tags origin \
|
||||
"+refs/heads/${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"
|
||||
git checkout --detach "$BASE_REF"
|
||||
|
||||
- name: Create missing chart tags
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PUSH_TAGS: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$REPO_DIR"
|
||||
.github/scripts/sync-helm-chart-tags.sh
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Docker Images Build on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate release tag
|
||||
id: release-tag
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG_REGEX='^v[0-9]+[.][0-9]+[.][0-9]+(-rc[0-9]+)?$'
|
||||
STABLE_TAG_REGEX='^v[0-9]+[.][0-9]+[.][0-9]+$'
|
||||
if [[ ! "$REF_NAME" =~ $TAG_REGEX ]]; then
|
||||
echo "::error::Docker release tags must use v<semver> or v<semver>-rcN, for example v0.8.5 or v0.8.5-rc1"
|
||||
exit 1
|
||||
fi
|
||||
printf 'image_tag=%s\n' "$REF_NAME" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$REF_NAME" =~ $STABLE_TAG_REGEX ]]; then
|
||||
echo "is_stable=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_stable=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Compute build metadata
|
||||
run: |
|
||||
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
|
||||
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- name: Resolve image tags
|
||||
id: image-tags
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
IMAGE_NAME: ${{ matrix.image_name }}
|
||||
IMAGE_TAG: ${{ steps.release-tag.outputs.image_tag }}
|
||||
IS_STABLE: ${{ steps.release-tag.outputs.is_stable }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force
|
||||
LATEST_STABLE_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+[.][0-9]+[.][0-9]+$' | head -n 1 || true)
|
||||
{
|
||||
echo 'tags<<EOF'
|
||||
printf 'ghcr.io/%s/%s:%s\n' "$GITHUB_REPOSITORY_OWNER" "$IMAGE_NAME" "$IMAGE_TAG"
|
||||
printf '%s/%s:%s\n' "$DOCKERHUB_USERNAME" "$IMAGE_NAME" "$IMAGE_TAG"
|
||||
if [ "$IS_STABLE" = "true" ] && [ "$IMAGE_TAG" = "$LATEST_STABLE_TAG" ]; then
|
||||
printf 'ghcr.io/%s/%s:latest\n' "$GITHUB_REPOSITORY_OWNER" "$IMAGE_NAME"
|
||||
printf '%s/%s:latest\n' "$DOCKERHUB_USERNAME" "$IMAGE_NAME"
|
||||
fi
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: ${{ steps.image-tags.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
@@ -0,0 +1,282 @@
|
||||
name: Detect Unused NPM Packages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'client/**'
|
||||
- 'api/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/api/**'
|
||||
|
||||
jobs:
|
||||
detect-unused-packages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
Reference in New Issue
Block a user