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,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement here on GitHub or
|
||||
on the official [Discord Server](https://discord.librechat.ai).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../README.md)
|
||||
@@ -0,0 +1,159 @@
|
||||
# Contributor Guidelines
|
||||
|
||||
Thank you to all the contributors who have helped make this project possible! We welcome various types of contributions, such as bug reports, documentation improvements, feature requests, and code contributions.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
If the feature you would like to contribute has not already received prior approval from the project maintainers (i.e., the feature is currently on the [roadmap](https://github.com/users/danny-avila/projects/2)), please submit a request in the [Feature Requests & Suggestions category](https://github.com/danny-avila/LibreChat/discussions/new?category=feature-requests-suggestions) of the discussions board before beginning work on it. The requests should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/danny-avila/LibreChat/issues) and included in the corresponding pull request for tracking purposes.
|
||||
|
||||
Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected. We appreciate your understanding and cooperation.
|
||||
|
||||
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.librechat.ai), where you can engage with other contributors and seek guidance from the community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
We strive to maintain a positive and inclusive environment within our project community. We expect all contributors to adhere to the following standards:
|
||||
|
||||
- Using welcoming and inclusive language.
|
||||
- Being respectful of differing viewpoints and experiences.
|
||||
- Gracefully accepting constructive criticism.
|
||||
- Focusing on what is best for the community.
|
||||
- Showing empathy towards other community members.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that do not align with these standards.
|
||||
|
||||
## To contribute to this project, please adhere to the following guidelines:
|
||||
|
||||
## 1. Development Setup
|
||||
|
||||
1. Use Node.js v24.16.0.
|
||||
2. Run `npm run smart-reinstall` to install dependencies (uses Turborepo). Use `npm run reinstall` for a clean install, or `npm ci` for a fresh lockfile-based install.
|
||||
3. Build all compiled code: `npm run build`.
|
||||
4. Setup and run unit tests:
|
||||
- Copy `.env.test`: `cp api/test/.env.test.example api/test/.env.test`.
|
||||
- Run backend unit tests: `npm run test:api`.
|
||||
- Run frontend unit tests: `npm run test:client`.
|
||||
5. Setup and run integration tests:
|
||||
- Create `.env`: `cp .env.example .env`.
|
||||
- Install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/), ensure that `mongosh` connects to your local instance.
|
||||
- Run: `npx install playwright`, then `npx playwright install`.
|
||||
- Copy `config.local`: `cp e2e/config.local.example.ts e2e/config.local.ts`.
|
||||
- Copy `librechat.yaml`: `cp librechat.example.yaml librechat.yaml`.
|
||||
- Run: `npm run e2e`.
|
||||
|
||||
## 2. Development Notes
|
||||
|
||||
1. Before starting work, make sure your main branch has the latest commits with `npm run update`.
|
||||
2. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning.
|
||||
3. After your changes, reinstall packages in your current branch using `npm run reinstall` and ensure everything still works.
|
||||
- Restart the ESLint server ("ESLint: Restart ESLint Server" in VS Code command bar) and your IDE after reinstalling or updating.
|
||||
4. Clear web app localStorage and cookies before and after changes.
|
||||
5. To check for introduced errors, build all compiled code: `npm run build`.
|
||||
6. Run backend unit tests: `npm run test:api`.
|
||||
7. Run frontend unit tests: `npm run test:client`.
|
||||
8. Run integration tests: `npm run e2e`.
|
||||
|
||||
## 3. Git Workflow
|
||||
|
||||
We utilize a GitFlow workflow to manage changes to this project's codebase. Follow these general steps when contributing code:
|
||||
|
||||
1. Fork the repository and create a new branch with a descriptive slash-based name (e.g., `new/feature/x`).
|
||||
2. Implement your changes and ensure that all tests pass.
|
||||
3. Commit your changes using conventional commit messages with GitFlow flags. Begin the commit message with a tag indicating the change type, such as "feat" (new feature), "fix" (bug fix), "docs" (documentation), or "refactor" (code refactoring), followed by a brief summary of the changes (e.g., `feat: Add new feature X to the project`).
|
||||
4. Submit a pull request with a clear and concise description of your changes and the reasons behind them.
|
||||
5. We will review your pull request, provide feedback as needed, and eventually merge the approved changes into the main branch.
|
||||
|
||||
## 4. Commit Message Format
|
||||
|
||||
We follow the [semantic format](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) for commit messages.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
feat: add hat wobble
|
||||
^--^ ^------------^
|
||||
| |
|
||||
| +-> Summary in present tense.
|
||||
|
|
||||
+-------> Type: chore, docs, feat, fix, refactor, style, or test.
|
||||
```
|
||||
|
||||
### Commit Guidelines
|
||||
- Do your best to reduce the number of commits, organizing them as much possible. Look into [squashing commits](https://www.freecodecamp.org/news/git-squash-commits/) in order to keep a neat history.
|
||||
- For those that care about maximizing commits for stats, adhere to the above as I 'squash and merge' an unorganized and/or unformatted commit history, which reduces the number of your commits to 1,:
|
||||
```
|
||||
* Update Br.tsx
|
||||
|
||||
* Update Es.tsx
|
||||
|
||||
* Update Br.tsx
|
||||
```
|
||||
|
||||
|
||||
## 5. Pull Request Process
|
||||
|
||||
When submitting a pull request, please follow these guidelines:
|
||||
|
||||
- Ensure that any installation or build dependencies are removed before the end of the layer when doing a build.
|
||||
- Update the README.md with details of changes to the interface, including new environment variables, exposed ports, useful file locations, and container parameters.
|
||||
- Increase the version numbers in any example files and the README.md to reflect the new version that the pull request represents. We use [SemVer](http://semver.org/) for versioning.
|
||||
|
||||
Ensure that your changes meet the following criteria:
|
||||
|
||||
- All tests pass as highlighted [above](#1-development-notes).
|
||||
- The code is well-formatted and adheres to our coding standards.
|
||||
- The commit history is clean and easy to follow. You can use `git rebase` or `git merge --squash` to clean your commit history before submitting the pull request.
|
||||
- The pull request description clearly outlines the changes and the reasons behind them. Be sure to include the steps to test the pull request.
|
||||
|
||||
## 6. Naming Conventions
|
||||
|
||||
Apply the following naming conventions to branches, labels, and other Git-related entities:
|
||||
|
||||
- **Branch names:** Descriptive and slash-based (e.g., `new/feature/x`).
|
||||
- **Labels:** Descriptive and kebab case (e.g., `bug-fix`).
|
||||
- **JS/TS:** Directories and file names: Descriptive and camelCase. First letter uppercased for React files (e.g., `helperFunction.ts, ReactComponent.tsx`).
|
||||
- **Docs:** Directories and file names: Descriptive and snake_case (e.g., `config_files.md`).
|
||||
|
||||
## 7. Coding Standards
|
||||
|
||||
For detailed coding conventions, workspace boundaries, and architecture guidance, refer to the [`AGENTS.md`](../AGENTS.md) file at the project root. It covers code style, type safety, import ordering, iteration/performance expectations, frontend rules, testing, and development commands.
|
||||
|
||||
## 8. TypeScript Conversion
|
||||
|
||||
1. **Original State**: The project was initially developed entirely in JavaScript (JS).
|
||||
|
||||
2. **Frontend**: Fully transitioned to TypeScript.
|
||||
|
||||
3. **Backend**:
|
||||
- The legacy Express.js server remains in `/api` as JavaScript.
|
||||
- All new backend code is written in TypeScript under `/packages/api`, which is compiled and consumed by `/api`.
|
||||
- Shared database logic lives in `/packages/data-schemas` (TypeScript).
|
||||
- Shared frontend/backend API types and services live in `/packages/data-provider` (TypeScript).
|
||||
- Minimize direct changes to `/api`; prefer adding TypeScript code to `/packages/api` and importing it.
|
||||
|
||||
## 9. Module Import Conventions
|
||||
|
||||
Imports are organized into three sections (in order):
|
||||
|
||||
1. **Package imports** — sorted from shortest to longest line length.
|
||||
- `react` is always the first import.
|
||||
- Multi-line (stacked) imports count their total character length across all lines for sorting.
|
||||
|
||||
2. **`import type` imports** — sorted from longest to shortest line length.
|
||||
- Package type imports come first, then local type imports.
|
||||
- Line length sorting resets between the package and local sub-groups.
|
||||
|
||||
3. **Local/project imports** — sorted from longest to shortest line length.
|
||||
- Multi-line (stacked) imports count their total character length across all lines for sorting.
|
||||
- Imports with alias `~` are treated the same as relative imports with respect to line length.
|
||||
|
||||
- Consolidate value imports from the same module as much as possible.
|
||||
- Always use standalone `import type { ... }` for type imports; never use inline `type` keyword inside value imports (e.g., `import { Foo, type Bar }` is wrong).
|
||||
|
||||
**Note:** ESLint will automatically enforce these import conventions when you run `npm run lint --fix` or through pre-commit hooks.
|
||||
|
||||
For the full set of coding standards, see [`AGENTS.md`](../AGENTS.md).
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../README.md)
|
||||
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [danny-avila]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,93 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["🐛 bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
Before submitting, please:
|
||||
- Search existing [Issues and Discussions](https://github.com/danny-avila/LibreChat/discussions) to see if your bug has already been reported
|
||||
- Use [Discussions](https://github.com/danny-avila/LibreChat/discussions) instead of Issues for:
|
||||
- General inquiries
|
||||
- Help with setup
|
||||
- Questions about whether you're experiencing a bug
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Please give as many details as possible
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version-info
|
||||
attributes:
|
||||
label: Version Information
|
||||
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
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please list the steps needed to reproduce the issue.
|
||||
placeholder: "1. Step 1\n2. Step 2\n3. Step 3"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Mobile (iOS)
|
||||
- Mobile (Android)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please paste relevant logs that were created when reproducing the error.
|
||||
|
||||
Log locations:
|
||||
- Docker: Project root directory ./logs
|
||||
- npm: ./api/logs
|
||||
|
||||
There are two types of logs that can help diagnose the issue:
|
||||
- debug logs (debug-YYYY-MM-DD.log)
|
||||
- error logs (error-YYYY-MM-DD.log)
|
||||
|
||||
Error logs contain exact stack traces and are especially helpful, but both can provide valuable information.
|
||||
Please only include the relevant portions of logs that correspond to when you reproduced the error.
|
||||
|
||||
For UI-related issues, browser console logs can be very helpful. You can provide these as screenshots or paste the text here.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Feature Request
|
||||
description: File a feature request
|
||||
title: "[Enhancement]: "
|
||||
labels: ["✨ enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What features would you like to see added?
|
||||
description: Please provide as many details as possible.
|
||||
placeholder: Please provide as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: More details
|
||||
description: Please provide additional details if needed.
|
||||
placeholder: Please provide additional details if needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: subject
|
||||
attributes:
|
||||
label: Which components are impacted by your request?
|
||||
multiple: true
|
||||
options:
|
||||
- General
|
||||
- UI
|
||||
- Endpoints
|
||||
- Plugins
|
||||
- Other
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Pictures
|
||||
description: If relevant, please include images to help clarify your request. You can drag and drop images directly here, paste them, or provide a link to them.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Locize Translation Access Request
|
||||
description: Request access to an additional language in Locize for LibreChat translations.
|
||||
title: "Locize Access Request: "
|
||||
labels: ["🌍 i18n", "🔑 access request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for your interest in contributing to LibreChat translations!
|
||||
Please fill out the form below to request access to an additional language in **Locize**.
|
||||
|
||||
**🔗 Available Languages:** [View the list here](https://www.librechat.ai/docs/translation)
|
||||
|
||||
**📌 Note:** Ensure that the requested language is supported before submitting your request.
|
||||
- type: input
|
||||
id: account_name
|
||||
attributes:
|
||||
label: Locize Account Name
|
||||
description: Please provide your Locize account name (e.g., John Doe).
|
||||
placeholder: e.g., John Doe
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: language_requested
|
||||
attributes:
|
||||
label: Language Code (ISO 639-1)
|
||||
description: |
|
||||
Enter the **ISO 639-1** language code for the language you want to translate into.
|
||||
Example: `es` for Spanish, `zh-Hant` for Traditional Chinese.
|
||||
|
||||
**🔗 Reference:** [Available Languages](https://www.librechat.ai/docs/translation)
|
||||
placeholder: e.g., es
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: agreement
|
||||
attributes:
|
||||
label: Agreement
|
||||
description: By submitting this request, you confirm that you will contribute responsibly and adhere to the project guidelines.
|
||||
options:
|
||||
- label: I agree to use my access solely for contributing to LibreChat translations.
|
||||
required: true
|
||||
@@ -0,0 +1,33 @@
|
||||
name: New Language Request
|
||||
description: Request to add a new language for LibreChat translations.
|
||||
title: "New Language Request: "
|
||||
labels: ["✨ enhancement", "🌍 i18n"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to submit a new language request! Please fill out the following details so we can review your request.
|
||||
- type: input
|
||||
id: language_name
|
||||
attributes:
|
||||
label: Language Name
|
||||
description: Please provide the full name of the language (e.g., Spanish, Mandarin).
|
||||
placeholder: e.g., Spanish
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: iso_code
|
||||
attributes:
|
||||
label: ISO 639-1 Code
|
||||
description: Please provide the ISO 639-1 code for the language (e.g., es for Spanish). You can refer to [this list](https://www.w3schools.com/tags/ref_language_codes.asp) for valid codes.
|
||||
placeholder: e.g., es
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
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
|
||||
@@ -0,0 +1,63 @@
|
||||
# Security Policy
|
||||
|
||||
At LibreChat, we prioritize the security of our project and value the contributions of security researchers in helping us improve the security of our codebase. If you discover a security vulnerability within our project, we appreciate your responsible disclosure. Please follow the guidelines below to report any vulnerabilities to us:
|
||||
|
||||
**Note: Only report sensitive vulnerability details via the appropriate private communication channels mentioned below. Public channels, such as GitHub issues and Discord, should be used for initiating contact and establishing private communication channels.**
|
||||
|
||||
## Communication Channels
|
||||
|
||||
When reporting a security vulnerability, you have the following options to reach out to us:
|
||||
|
||||
- **Option 1: GitHub Security Advisory System**: We encourage you to use GitHub's Security Advisory system to report any security vulnerabilities you find. This allows us to receive vulnerability reports directly through GitHub. For more information on how to submit a security advisory report, please refer to the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
|
||||
|
||||
- **Option 2: GitHub Issues**: You can initiate first contact via GitHub Issues. However, please note that initial contact through GitHub Issues should not include any sensitive details.
|
||||
|
||||
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.librechat.ai) and initiate first contact in the `#issues` channel. However, please ensure that initial contact through Discord does not include any sensitive details.
|
||||
|
||||
_After the initial contact, we will establish a private communication channel for further discussion._
|
||||
|
||||
### When submitting a vulnerability report, please provide us with the following information:
|
||||
|
||||
- A clear description of the vulnerability, including steps to reproduce it.
|
||||
- The version(s) of the project affected by the vulnerability.
|
||||
- Any additional information that may be useful for understanding and addressing the issue.
|
||||
|
||||
We strive to acknowledge vulnerability reports within 72 hours and will keep you informed of the progress towards resolution.
|
||||
|
||||
## Security Updates and Patching
|
||||
|
||||
We are committed to maintaining the security of our open-source project, LibreChat, and promptly addressing any identified vulnerabilities. To ensure the security of our project, we adhere to the following practices:
|
||||
|
||||
- We prioritize security updates for the current major release of our software.
|
||||
- We actively monitor the GitHub Security Advisory system and the `#issues` channel on Discord for any vulnerability reports.
|
||||
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
|
||||
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
|
||||
|
||||
Please note that as a security-conscious community, we may not always disclose detailed information about security issues until we have determined that doing so would not put our users or the project at risk. We appreciate your understanding and cooperation in these matters.
|
||||
|
||||
## Scope
|
||||
|
||||
This security policy applies to the following GitHub repository:
|
||||
|
||||
- Repository: [LibreChat](https://github.librechat.ai)
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.librechat.ai) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/danny-avila/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We would like to express our gratitude to the security researchers and community members who help us improve the security of our project. Your contributions are invaluable, and we sincerely appreciate your efforts.
|
||||
|
||||
## Bug Bounty Program
|
||||
|
||||
We currently do not have a bug bounty program in place. However, we welcome and appreciate any
|
||||
|
||||
security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase. We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.
|
||||
|
||||
**Reference**
|
||||
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../README.md)
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"title": "### ✨ New Features",
|
||||
"labels": ["feat"]
|
||||
},
|
||||
{
|
||||
"title": "### 🌍 Internationalization",
|
||||
"labels": ["i18n"]
|
||||
},
|
||||
{
|
||||
"title": "### 👐 Accessibility",
|
||||
"labels": ["a11y"]
|
||||
},
|
||||
{
|
||||
"title": "### 🔧 Fixes",
|
||||
"labels": ["Fix", "fix"]
|
||||
},
|
||||
{
|
||||
"title": "### ⚙️ Other Changes",
|
||||
"labels": ["ci", "style", "docs", "refactor", "chore"]
|
||||
}
|
||||
],
|
||||
"ignore_labels": [
|
||||
"🔁 duplicate",
|
||||
"📊 analytics",
|
||||
"🌱 good first issue",
|
||||
"🔍 investigation",
|
||||
"🙏 help wanted",
|
||||
"❌ invalid",
|
||||
"❓ question",
|
||||
"🚫 wontfix",
|
||||
"🚀 release",
|
||||
"version"
|
||||
],
|
||||
"base_branches": ["main"],
|
||||
"sort": {
|
||||
"order": "ASC",
|
||||
"on_property": "mergedAt"
|
||||
},
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:",
|
||||
"target": "$1",
|
||||
"flags": "i",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
},
|
||||
{
|
||||
"pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*",
|
||||
"target": "version",
|
||||
"flags": "i",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
}
|
||||
],
|
||||
"template": "## [#{{TO_TAG}}] - #{{TO_TAG_DATE}}\n\nChanges from #{{FROM_TAG}} to #{{TO_TAG}}.\n\n#{{CHANGELOG}}\n\n[See full release details][release-#{{TO_TAG}}]\n\n[release-#{{TO_TAG}}]: https://github.com/#{{OWNER}}/#{{REPO}}/releases/tag/#{{TO_TAG}}\n\n---",
|
||||
"pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})",
|
||||
"empty_template": "- no changes"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"title": "### ✨ New Features",
|
||||
"labels": ["feat"]
|
||||
},
|
||||
{
|
||||
"title": "### 🌍 Internationalization",
|
||||
"labels": ["i18n"]
|
||||
},
|
||||
{
|
||||
"title": "### 👐 Accessibility",
|
||||
"labels": ["a11y"]
|
||||
},
|
||||
{
|
||||
"title": "### 🔧 Fixes",
|
||||
"labels": ["Fix", "fix"]
|
||||
},
|
||||
{
|
||||
"title": "### ⚙️ Other Changes",
|
||||
"labels": ["ci", "style", "docs", "refactor", "chore"]
|
||||
}
|
||||
],
|
||||
"ignore_labels": [
|
||||
"🔁 duplicate",
|
||||
"📊 analytics",
|
||||
"🌱 good first issue",
|
||||
"🔍 investigation",
|
||||
"🙏 help wanted",
|
||||
"❌ invalid",
|
||||
"❓ question",
|
||||
"🚫 wontfix",
|
||||
"🚀 release",
|
||||
"version",
|
||||
"action"
|
||||
],
|
||||
"base_branches": ["main"],
|
||||
"sort": {
|
||||
"order": "ASC",
|
||||
"on_property": "mergedAt"
|
||||
},
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:",
|
||||
"target": "$1",
|
||||
"flags": "i",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
},
|
||||
{
|
||||
"pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*",
|
||||
"target": "version",
|
||||
"flags": "i",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
},
|
||||
{
|
||||
"pattern": "^(?:[^A-Za-z0-9]*)(action)\\b.*",
|
||||
"target": "action",
|
||||
"flags": "i",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
}
|
||||
],
|
||||
"template": "## [Unreleased]\n\n#{{CHANGELOG}}\n\n---",
|
||||
"pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})",
|
||||
"empty_template": "- no changes"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# name: Playwright Tests
|
||||
# on:
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - release/*
|
||||
# paths:
|
||||
# - 'api/**'
|
||||
# - 'client/**'
|
||||
# - 'packages/**'
|
||||
# - 'e2e/**'
|
||||
# jobs:
|
||||
# tests_e2e:
|
||||
# name: Run Playwright tests
|
||||
# if: github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat'
|
||||
# timeout-minutes: 60
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# NODE_ENV: CI
|
||||
# CI: true
|
||||
# SEARCH: false
|
||||
# BINGAI_TOKEN: user_provided
|
||||
# CHATGPT_TOKEN: user_provided
|
||||
# MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
# E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
|
||||
# E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
|
||||
# JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
# JWT_REFRESH_SECRET: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
# CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
# CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
# DOMAIN_CLIENT: ${{ secrets.DOMAIN_CLIENT }}
|
||||
# DOMAIN_SERVER: ${{ secrets.DOMAIN_SERVER }}
|
||||
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 # Skip downloading during npm install
|
||||
# PLAYWRIGHT_BROWSERS_PATH: 0 # Places binaries to node_modules/@playwright/test
|
||||
# TITLE_CONVO: false
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 24.16.0
|
||||
# cache: 'npm'
|
||||
|
||||
# - name: Install global dependencies
|
||||
# run: npm ci
|
||||
|
||||
# # - name: Remove sharp dependency
|
||||
# # run: rm -rf node_modules/sharp
|
||||
|
||||
# # - name: Install sharp with linux dependencies
|
||||
# # run: cd api && SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp
|
||||
|
||||
# - name: Build Client
|
||||
# run: npm run frontend
|
||||
|
||||
# - name: Install Playwright
|
||||
# run: |
|
||||
# npx playwright install-deps
|
||||
# npm install -D @playwright/test@latest
|
||||
# npx playwright install chromium
|
||||
|
||||
# - name: Run Playwright tests
|
||||
# run: npm run e2e:ci
|
||||
|
||||
# - name: Upload playwright report
|
||||
# uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: e2e/playwright-report/
|
||||
# retention-days: 30
|
||||
@@ -0,0 +1,41 @@
|
||||
# Pull Request Template
|
||||
|
||||
⚠️ Before Submitting a PR, Please Review:
|
||||
- Please ensure that you have thoroughly read and understood the [Contributing Docs](https://github.com/danny-avila/LibreChat/blob/main/.github/CONTRIBUTING.md) before submitting your Pull Request.
|
||||
|
||||
⚠️ Documentation Updates Notice:
|
||||
- Kindly note that documentation updates are managed in this repository: [librechat.ai](https://github.com/LibreChat-AI/librechat.ai)
|
||||
|
||||
## Summary
|
||||
|
||||
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
|
||||
|
||||
## Change Type
|
||||
|
||||
Please delete any irrelevant options.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] Translation update
|
||||
|
||||
## Testing
|
||||
|
||||
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
|
||||
|
||||
### **Test Configuration**:
|
||||
|
||||
## Checklist
|
||||
|
||||
Please delete any irrelevant options.
|
||||
|
||||
- [ ] My code adheres to this project's style guidelines
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented in any complex areas of my code
|
||||
- [ ] I have made pertinent documentation changes
|
||||
- [ ] My changes do not introduce new warnings
|
||||
- [ ] I have written tests demonstrating that my changes are effective or that my feature works
|
||||
- [ ] Local unit tests pass with my changes
|
||||
- [ ] Any changes dependent on mine have been merged and published in downstream modules.
|
||||
- [ ] A pull request for updating the documentation has been submitted.
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CHART_PATH="${CHART_PATH:-helm/librechat/Chart.yaml}"
|
||||
DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
|
||||
BASE_REF="${BASE_REF:-refs/remotes/origin/${DEFAULT_BRANCH}}"
|
||||
BACKFILL_FROM_VERSION="${BACKFILL_FROM_VERSION:-1.9.0}"
|
||||
PUSH_TAGS="${PUSH_TAGS:-false}"
|
||||
TAG_PREFIX="${TAG_PREFIX:-chart-}"
|
||||
GITHUB_SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
DISPATCH_WORKFLOW="${DISPATCH_WORKFLOW:-}"
|
||||
RELEASE_EXISTING_TAG="${RELEASE_EXISTING_TAG:-}"
|
||||
SEMVER_REGEX='^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-[0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?([+][0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?$'
|
||||
|
||||
fail() {
|
||||
printf '::error::%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
git_auth_header() {
|
||||
token="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
|
||||
printf 'AUTHORIZATION: basic %s' "$token"
|
||||
}
|
||||
|
||||
git_with_auth() {
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
git -c "http.extraheader=$(git_auth_header)" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
git "$@"
|
||||
}
|
||||
|
||||
dispatch_release() {
|
||||
tag="$1"
|
||||
|
||||
if [ -z "$DISPATCH_WORKFLOW" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "${GITHUB_REPOSITORY:-}" ]; then
|
||||
fail "GITHUB_REPOSITORY is required to dispatch ${DISPATCH_WORKFLOW}"
|
||||
fi
|
||||
|
||||
if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
||||
fail "Unexpected repository name: ${GITHUB_REPOSITORY}"
|
||||
fi
|
||||
|
||||
if [[ ! "$DISPATCH_WORKFLOW" =~ ^[A-Za-z0-9_.-]+[.]ya?ml$ ]]; then
|
||||
fail "Unexpected workflow file: ${DISPATCH_WORKFLOW}"
|
||||
fi
|
||||
|
||||
token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
if [ -z "$token" ]; then
|
||||
fail "GH_TOKEN or GITHUB_TOKEN is required to dispatch ${DISPATCH_WORKFLOW}"
|
||||
fi
|
||||
|
||||
command -v gh >/dev/null ||
|
||||
fail "GitHub CLI is required to dispatch ${DISPATCH_WORKFLOW}"
|
||||
|
||||
GH_TOKEN="$token" gh workflow run "$DISPATCH_WORKFLOW" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--ref "$DEFAULT_BRANCH" \
|
||||
-f "chart_tag=${tag}"
|
||||
}
|
||||
|
||||
version_less_than() {
|
||||
left="${1%%[-+]*}"
|
||||
right="${2%%[-+]*}"
|
||||
|
||||
IFS=. read -r left_major left_minor left_patch <<<"$left"
|
||||
IFS=. read -r right_major right_minor right_patch <<<"$right"
|
||||
|
||||
if (( left_major != right_major )); then
|
||||
(( left_major < right_major ))
|
||||
return
|
||||
fi
|
||||
|
||||
if (( left_minor != right_minor )); then
|
||||
(( left_minor < right_minor ))
|
||||
return
|
||||
fi
|
||||
|
||||
(( left_patch < right_patch ))
|
||||
}
|
||||
|
||||
validate_chart_tag() {
|
||||
tag="$1"
|
||||
version="${tag#${TAG_PREFIX}}"
|
||||
|
||||
git check-ref-format "refs/tags/${tag}" >/dev/null ||
|
||||
fail "Refusing to use invalid tag ${tag}"
|
||||
|
||||
if [[ "$tag" != "${TAG_PREFIX}"* || ! "$version" =~ $SEMVER_REGEX ]]; then
|
||||
fail "Chart tags must use the form ${TAG_PREFIX}<semver>, for example ${TAG_PREFIX}2.0.5"
|
||||
fi
|
||||
}
|
||||
|
||||
dispatch_existing_tag() {
|
||||
tag="$1"
|
||||
|
||||
if [ -z "$tag" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
validate_chart_tag "$tag"
|
||||
|
||||
if [ "$PUSH_TAGS" != "true" ]; then
|
||||
printf 'Would dispatch release workflow for existing %s.\n' "$tag"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
||||
fail "Remote tag ${tag} does not exist"
|
||||
fi
|
||||
|
||||
printf 'Dispatching release workflow for existing %s.\n' "$tag"
|
||||
dispatch_release "$tag"
|
||||
}
|
||||
|
||||
chart_version_at() {
|
||||
git show "${1}:${CHART_PATH}" 2>/dev/null | awk '
|
||||
/^version:[[:space:]]*/ {
|
||||
value = $0
|
||||
sub(/^version:[[:space:]]*/, "", value)
|
||||
sub(/[[:space:]]*#.*/, "", value)
|
||||
gsub(/^[[:space:]"'\''"]+|[[:space:]"'\''"]+$/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
case "$PUSH_TAGS" in
|
||||
true | false) ;;
|
||||
*) fail "PUSH_TAGS must be true or false" ;;
|
||||
esac
|
||||
|
||||
if [[ ! "$BACKFILL_FROM_VERSION" =~ $SEMVER_REGEX ]]; then
|
||||
fail "BACKFILL_FROM_VERSION must be a valid SemVer value"
|
||||
fi
|
||||
|
||||
git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null ||
|
||||
fail "Unable to resolve ${BASE_REF}; fetch ${DEFAULT_BRANCH} before running this script"
|
||||
|
||||
history_file="$(mktemp)"
|
||||
versions_file="$(mktemp)"
|
||||
seen_file="$(mktemp)"
|
||||
missing_file="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "$history_file" "$versions_file" "$seen_file" "$missing_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
git log --first-parent --reverse --format=%H "$BASE_REF" -- "$CHART_PATH" >"$history_file"
|
||||
|
||||
if [ ! -s "$history_file" ]; then
|
||||
fail "No history found for ${CHART_PATH} on ${BASE_REF}"
|
||||
fi
|
||||
|
||||
while IFS= read -r commit; do
|
||||
version="$(chart_version_at "$commit")"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! "$version" =~ $SEMVER_REGEX ]]; then
|
||||
fail "${CHART_PATH} has invalid SemVer '${version}' at ${commit}"
|
||||
fi
|
||||
|
||||
if version_less_than "$version" "$BACKFILL_FROM_VERSION"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if grep -Fqx "$version" "$seen_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\n' "$version" >>"$seen_file"
|
||||
printf '%s\t%s\n' "$version" "$commit" >>"$versions_file"
|
||||
done <"$history_file"
|
||||
|
||||
if [ ! -s "$versions_file" ]; then
|
||||
fail "No chart versions found in ${CHART_PATH}"
|
||||
fi
|
||||
|
||||
while IFS="$(printf '\t')" read -r version commit; do
|
||||
tag="${TAG_PREFIX}${version}"
|
||||
|
||||
validate_chart_tag "$tag"
|
||||
|
||||
if git rev-parse --quiet --verify "refs/tags/${tag}" >/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$tag" "$commit" >>"$missing_file"
|
||||
done <"$versions_file"
|
||||
|
||||
if [ ! -s "$missing_file" ]; then
|
||||
printf 'All chart versions on %s already have %s tags.\n' "$BASE_REF" "$TAG_PREFIX"
|
||||
dispatch_existing_tag "$RELEASE_EXISTING_TAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while IFS="$(printf '\t')" read -r tag commit; do
|
||||
short_commit="$(git rev-parse --short "$commit")"
|
||||
|
||||
if [ "$PUSH_TAGS" != "true" ]; then
|
||||
printf 'Would create %s at %s.\n' "$tag" "$short_commit"
|
||||
continue
|
||||
fi
|
||||
|
||||
if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
||||
printf 'Remote tag %s already exists; dispatching release workflow.\n' "$tag"
|
||||
dispatch_release "$tag"
|
||||
continue
|
||||
fi
|
||||
|
||||
git tag "$tag" "$commit"
|
||||
|
||||
if git_with_auth push origin "refs/tags/${tag}"; then
|
||||
printf 'Created %s at %s.\n' "$tag" "$short_commit"
|
||||
dispatch_release "$tag"
|
||||
continue
|
||||
fi
|
||||
|
||||
if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
||||
printf 'Remote tag %s was created concurrently; dispatching release workflow.\n' "$tag"
|
||||
dispatch_release "$tag"
|
||||
continue
|
||||
fi
|
||||
|
||||
fail "Failed to push ${tag}"
|
||||
done <"$missing_file"
|
||||
|
||||
dispatch_existing_tag "$RELEASE_EXISTING_TAG"
|
||||
@@ -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