Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
001ef8f
πŸ—οΈ refactor: Derive App Version from Root package.json + Add buildInf…
danny-avila Apr 21, 2026
c17f094
πŸ› οΈ feat: Add Build Metadata Resolver and Expose via /api/config
danny-avila Apr 21, 2026
6bcd697
ℹ️ feat: Add Settings β†’ About Panel with Diagnostics Copy
danny-avila Apr 21, 2026
b7dbb34
🐳 ci: Inject BUILD_COMMIT/BRANCH/DATE into Docker Images
danny-avila Apr 21, 2026
cfaf90e
πŸ“ docs: Direct Bug Reporters to Settings β†’ About for Version Info
danny-avila Apr 21, 2026
ef00d46
🐳 fix: Move BUILD_* ENV to End of Docker Stages to Preserve Layer Cache
danny-avila Apr 21, 2026
cd1680f
πŸ”§ fix: Propagate interface.buildInfo=false to Unauthenticated /api/co…
danny-avila Apr 21, 2026
844c0c2
πŸ”€ ci: Trigger Dev Image Workflows on Root package.json + Dockerfile C…
danny-avila Apr 21, 2026
ae8b070
🧽 fix: Harden About Panel Lifecycle, A11y, and Loading Gate
danny-avila Apr 21, 2026
3d250a0
⚑ perf: Eagerly Resolve Build Info at Module Load
danny-avila Apr 21, 2026
246a2ae
πŸ“ docs: Document rollup Version Placeholder Contract
danny-avila Apr 21, 2026
d08108d
πŸͺͺ fix: Only Toast "Copied" When Clipboard Copy Actually Succeeds
danny-avila Apr 21, 2026
27f3bfc
🐳 fix: Derive main image metadata from checkout
danny-avila May 19, 2026
a5ad655
πŸͺͺ fix: Keep About enabled until disabled
danny-avila May 19, 2026
198495a
βœ… test: Avoid literal Settings mock text
danny-avila May 19, 2026
f247bc5
🧱 refactor: Rename Build Info Module
danny-avila May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions .github/ISSUE_TEMPLATE/BUG-REPORT.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@ body:
id: version-info
attributes:
label: Version Information
description: |
If using Docker, please run and provide the output of:
```bash
docker images | grep librechat
```

If running from source, please run and provide the output of:
```bash
git rev-parse HEAD
```
placeholder: Paste the output here
description: |
In LibreChat, open **Settings β†’ About** and click **Copy diagnostics**, then paste the result here.
This captures the exact version, commit, branch, and build date so maintainers can pinpoint the build you're running.

If the About panel is unavailable (older version / self-hosted with it disabled), please provide as much of the following as possible instead:
- Docker: `docker images | grep librechat` (image tag) and `docker inspect <image> | grep -i "\"Commit\\|BUILD_"` if build args were set
- Source: `git rev-parse HEAD` and `git rev-parse --abbrev-ref HEAD`
placeholder: Paste the diagnostics block here
validations:
required: true
- type: textarea
Expand Down Expand Up @@ -93,4 +90,4 @@ body:
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
required: true
14 changes: 14 additions & 0 deletions .github/workflows/dev-branch-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
- 'api/**'
- 'client/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'Dockerfile'
- 'Dockerfile.multi'

permissions:
contents: read
Expand Down Expand Up @@ -65,6 +69,12 @@ jobs:
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
Expand All @@ -79,3 +89,7 @@ jobs:
${{ 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 }}
14 changes: 14 additions & 0 deletions .github/workflows/dev-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
- 'api/**'
- 'client/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'Dockerfile'
- 'Dockerfile.multi'

permissions:
contents: read
Expand Down Expand Up @@ -61,6 +65,12 @@ jobs:
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
Expand All @@ -75,3 +85,7 @@ jobs:
${{ 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 }}
10 changes: 10 additions & 0 deletions .github/workflows/dev-staging-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ jobs:
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
Expand All @@ -67,3 +73,7 @@ jobs:
${{ 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 }}
10 changes: 10 additions & 0 deletions .github/workflows/main-image-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ jobs:
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
Expand Down Expand Up @@ -80,3 +86,7 @@ jobs:
${{ 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 }}
10 changes: 10 additions & 0 deletions .github/workflows/tag-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ jobs:
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:
Expand Down Expand Up @@ -104,3 +110,7 @@ jobs:
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 }}
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ RUN \
npm prune --production; \
npm cache clean --force

# Optional build metadata surfaced in Settings -> About for support triage.
# Declared here (after the heavy install/build steps) so that commit/date
# changing on every CI run does not bust the cache for dependency install
# and frontend build layers. When unset, the backend falls back to local
# git resolution (if .git is present), and finally to empty values.
ARG BUILD_COMMIT=
ARG BUILD_BRANCH=
ARG BUILD_DATE=
ENV BUILD_COMMIT=${BUILD_COMMIT}
ENV BUILD_BRANCH=${BUILD_BRANCH}
ENV BUILD_DATE=${BUILD_DATE}

# Node API setup
EXPOSE 3080
ENV HOST=0.0.0.0
Expand Down
14 changes: 14 additions & 0 deletions Dockerfile.multi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
# Set configurable max-old-space-size with default
ARG NODE_MAX_OLD_SPACE_SIZE=6144

# Optional build metadata surfaced in Settings -> About for support triage.
ARG BUILD_COMMIT=
ARG BUILD_BRANCH=
ARG BUILD_DATE=

# Base for all builds
FROM node:20-alpine AS base-min
ARG NPM_CI_TIMEOUT_SECONDS=1500
Expand Down Expand Up @@ -108,6 +113,15 @@ COPY --from=data-provider-build /app/packages/data-provider/dist ./packages/data
COPY --from=data-schemas-build /app/packages/data-schemas/dist ./packages/data-schemas/dist
COPY --from=api-package-build /app/packages/api/dist ./packages/api/dist
COPY --from=client-build /app/client/dist ./client/dist
# Propagate build metadata into runtime env so /api/config can expose it.
# Declared here (after the heavy install/copy steps) so that commit/date
# changing on every CI run does not bust the cache for those layers.
ARG BUILD_COMMIT
ARG BUILD_BRANCH
ARG BUILD_DATE
ENV BUILD_COMMIT=${BUILD_COMMIT}
ENV BUILD_BRANCH=${BUILD_BRANCH}
ENV BUILD_DATE=${BUILD_DATE}
WORKDIR /app/api
EXPOSE 3080
ENV HOST=0.0.0.0
Expand Down
121 changes: 121 additions & 0 deletions api/server/routes/__tests__/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,16 @@ jest.mock('@librechat/data-schemas', () => ({
}));

const mockGetCloudFrontConfig = jest.fn(() => null);
const mockResolveBuildInfo = jest.fn(() => ({
commit: null,
commitShort: null,
branch: null,
buildDate: null,
}));
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
getCloudFrontConfig: (...args) => mockGetCloudFrontConfig(...args),
resolveBuildInfo: (...args) => mockResolveBuildInfo(...args),
}));

const request = require('supertest');
Expand Down Expand Up @@ -63,6 +70,12 @@ const mockUser = {

afterEach(() => {
jest.resetAllMocks();
mockResolveBuildInfo.mockReturnValue({
commit: null,
commitShort: null,
branch: null,
buildDate: null,
});
delete process.env.APP_TITLE;
delete process.env.CHECK_BALANCE;
delete process.env.START_BALANCE;
Expand Down Expand Up @@ -446,4 +459,112 @@ describe('GET /api/config', () => {
expect(response.body).toHaveProperty('error');
});
});

describe('buildInfo payload', () => {
const populatedBuildInfo = {
commit: 'abcdef1234567890abcdef1234567890abcdef12',
commitShort: 'abcdef1',
branch: 'dev',
buildDate: '2026-04-20T12:00:00Z',
};

it('includes buildInfo in authenticated response when interface flag is not explicitly disabled', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
const app = createApp(mockUser);

const response = await request(app).get('/api/config');

expect(response.body.buildInfo).toEqual(populatedBuildInfo);
});

it('omits buildInfo when interface.buildInfo is false', async () => {
mockGetAppConfig.mockResolvedValue({
...baseAppConfig,
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
});
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
const app = createApp(mockUser);

const response = await request(app).get('/api/config');

expect(response.body).not.toHaveProperty('buildInfo');
});

it('omits buildInfo when all resolver fields are null', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
mockResolveBuildInfo.mockReturnValue({
commit: null,
commitShort: null,
branch: null,
buildDate: null,
});
const app = createApp(mockUser);

const response = await request(app).get('/api/config');

expect(response.body).not.toHaveProperty('buildInfo');
});

it('includes buildInfo in unauthenticated response when flag is not disabled', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
const app = createApp(null);

const response = await request(app).get('/api/config');

expect(response.body.buildInfo).toEqual(populatedBuildInfo);
});

it('omits buildInfo in unauthenticated response when interface.buildInfo is false', async () => {
mockGetAppConfig.mockResolvedValue({
...baseAppConfig,
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
});
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
const app = createApp(null);

const response = await request(app).get('/api/config');

expect(response.body).not.toHaveProperty('buildInfo');
});

it('propagates interface.buildInfo=false in unauthenticated response so clients can hide About tab', async () => {
mockGetAppConfig.mockResolvedValue({
...baseAppConfig,
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
});
const app = createApp(null);

const response = await request(app).get('/api/config');

expect(response.body.interface).toBeDefined();
expect(response.body.interface.buildInfo).toBe(false);
});

it('does not add interface.buildInfo=true to unauthenticated response (default stays implicit)', async () => {
mockGetAppConfig.mockResolvedValue({
...baseAppConfig,
interfaceConfig: { privacyPolicy: { externalUrl: 'https://x' }, buildInfo: true },
});
const app = createApp(null);

const response = await request(app).get('/api/config');

expect(response.body.interface).toBeDefined();
expect(response.body.interface).not.toHaveProperty('buildInfo');
});

it('includes interface block with only buildInfo=false when nothing else is set', async () => {
mockGetAppConfig.mockResolvedValue({
...baseAppConfig,
interfaceConfig: { buildInfo: false },
});
const app = createApp(null);

const response = await request(app).get('/api/config');

expect(response.body.interface).toEqual({ buildInfo: false });
});
});
});
Loading
Loading