Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/docs-sync/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Docs sync: document what merged to LibreChat `dev`

You are updating the librechat.ai documentation so it covers changes that recently merged to the LibreChat application's `dev` branch. Work in the current checkout of the docs repository.

## Inputs

- `.sync/commits.txt`: the LibreChat commits to review, one per line (`<sha> <author date> <subject>`), oldest first.
- `.sync/librechat/`: a clone of `LibreChat-AI/LibreChat` at `origin/dev`. It is the source of truth for behavior. Read it with `git -C .sync/librechat show <sha>`, `git -C .sync/librechat show origin/dev:<path>` and `git -C .sync/librechat grep -n <pattern> origin/dev -- <paths>`.
- Docs live in `content/docs/`. English sources are the `*.mdx` and `meta.json` files without a locale suffix.

Useful app paths:

| What | Where |
| ----------------------------------------- | ------------------------------------------------- |
| `librechat.yaml` schema, defaults, ranges | `packages/data-provider/src/config.ts` |
| Example config with comments | `librechat.example.yaml` |
| Environment variables | `.env.example` |
| English UI strings (exact labels) | `client/src/locales/en/translation.json` |
| Settings dialog entries | `client/src/components/Nav/Settings/registry.tsx` |

## Step 1: triage

For every commit in `.sync/commits.txt`, decide whether it needs documentation. It does when it:

- adds or changes an environment variable, a `librechat.yaml` key, a default, a range, or precedence between them;
- adds or changes a user-visible feature, setting, workflow or label;
- changes admin-facing behavior (auth, permissions, retention, rate limits, logging an admin acts on);
- makes existing docs wrong (for example a page that still describes a removed menu).

Internal refactors, tests, performance work, styling with no behavior change, and CI changes need no docs. Group related commits (a feature and its follow-up fixes) and document the final behavior on `origin/dev`, not the intermediate states.

## Step 2: check coverage

For each candidate, search the English docs for the keys, labels and concepts involved. If the docs already describe the current behavior correctly, record it as covered and move on.

## Step 3: write

- Verify every fact against `.sync/librechat` at `origin/dev` before writing it. A commit message is a lead, never evidence. Quote exact key names, defaults, ranges and UI labels from the source.
- Edit only English sources. Never create or edit locale copies such as `*.de.mdx` or `meta.de.json`; a separate workflow translates English changes. Write reader-facing prose inline in the MDX page, not in `components/repeated/`, because those partials are not translated.
- Put each change where a reader would look for it: feature behavior on the matching `content/docs/features/*.mdx` page, `librechat.yaml` keys on the matching page under `content/docs/configuration/librechat_yaml/object_structure/`, and environment variables in `content/docs/configuration/dotenv.mdx`. A genuinely new feature can get a new page; register it in the folder's `meta.json`.
- Match the page you are editing: its heading levels, tone and MDX components (`Callout`, `Steps`, `Tabs`, `OptionTable`). `OptionTable` cells render as plain text, so do not put backticks, bold or links inside them; put links in the surrounding prose.
- Lead with what the feature does and when to use it, then a minimal working example, then reference details, then caveats. Use exact UI labels in bold. Be complete but tight.
- The live docs follow `dev`. Do not add "available since" or "newer than vX" notes; released versions are served from `content/docs-archive/`, which you must never edit.
- Never use an em dash or an en dash as punctuation, and never use `--` as a substitute. Use commas, colons, semicolons, parentheses or separate sentences. No emojis.
- Do not add or replace images. If a change makes an existing screenshot outdated, list it in the report instead.
- Do not touch files outside `content/docs/`, except `.sync/report.md`.

## Step 4: verify

After writing, re-check each changed claim against the source, ideally with an independent subagent that reads only the diff (`git diff -- content/docs`) and `.sync/librechat`. Fix every confirmed problem. Also check that every internal link you added points to an existing page and heading anchor.

## Step 5: report

Write `.sync/report.md`, which becomes the pull request description. Keep it to one screen:

```markdown
## Summary

<One short paragraph: what this sync documents and the commit range.>

## Changes

- <page>: <what was added or corrected> (<LibreChat PR numbers>)

## Reviewed without docs changes

<One line per group, e.g. "Internal refactors and tests: #16592, #16610">

## Needs a human

- <Unverified claims, outdated screenshots, decisions you could not make from the source>
```

If nothing needs documentation, leave `content/docs/` untouched and say so in the report.
6 changes: 6 additions & 0 deletions .github/docs-sync/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"librechat": {
"branch": "dev",
"lastSyncedCommit": "92432e95d5bd9e1937117c40184bdba466f95426"
}
}
174 changes: 174 additions & 0 deletions .github/workflows/docs_sync_dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Docs Sync From LibreChat dev

# Weekly: review what merged to LibreChat's dev branch since the last synced
# commit, let Claude document whatever needs it, and open (or refresh) one
# draft pull request. Nothing merges on its own.
#
# State: .github/docs-sync/state.json records the last LibreChat commit the
# docs were synced to. It only advances when the sync pull request merges, so
# an unmerged run is regenerated from main, covering the wider range, the next
# time the workflow runs.
#
# Secrets:
# ANTHROPIC_API_KEY required
# DOCS_SYNC_TOKEN optional; a token with contents and pull-requests write.
# Pull requests opened with the default GITHUB_TOKEN do not
# trigger other workflows, so CI only runs on the sync PR
# when this is set.

on:
schedule:
- cron: '17 6 * * 1'
workflow_dispatch:
inputs:
since:
description: 'LibreChat commit to start after (defaults to state.json)'
type: string
default: ''
max_commits:
description: 'Most commits to review in one run'
type: number
default: 150
model:
description: 'Claude model'
type: string
default: 'claude-opus-5-5'

permissions:
contents: write
pull-requests: write
id-token: write

env:
SYNC_BRANCH: automation/docs-sync-dev
STATE_FILE: .github/docs-sync/state.json

concurrency:
group: docs-sync-dev
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# No stored credentials: the agent below reads untrusted text (commit
# messages, app source), so nothing it can reach should hold a token.
persist-credentials: false

- name: Prepare sync branch
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git switch -C "$SYNC_BRANCH" origin/main

- name: Clone LibreChat dev
run: |
git clone --quiet --filter=blob:none --branch dev \
https://github.com/LibreChat-AI/LibreChat.git .sync/librechat

- name: Collect commits to review
id: range
env:
SINCE: ${{ inputs.since }}
MAX_COMMITS: ${{ inputs.max_commits || 150 }}
run: |
since="${SINCE:-$(jq -r '.librechat.lastSyncedCommit' "$STATE_FILE")}"
if ! git -C .sync/librechat cat-file -e "${since}^{commit}" 2>/dev/null; then
echo "::error::start commit $since is not in LibreChat dev"
exit 1
fi
git -C .sync/librechat log --reverse --no-merges \
--format='%H %as %s' "${since}..origin/dev" | head -n "$MAX_COMMITS" > .sync/commits.txt
Comment on lines +84 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve merge boundaries in the sync marker

When the range crosses an upstream merge with new commits on both parents, --no-merges leaves incomparable parent-side commits in this file, while last later selects only one of them as the persisted marker. Git's A..B revision range excludes only commits reachable from A, so the next run sees the other parent, saves that SHA, and subsequent runs can alternate between the two sides indefinitely. I reproduced this with a two-parent merge; the workflow repeatedly reviews the same commits and never converges. Derive the checkpoint from a merge or first-parent boundary that contains the entire reviewed set rather than the final line of the filtered log.

Useful? React with 👍 / 👎.

Comment on lines +84 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include changes made directly in merge commits

When a merge commit contains conflict-resolution or other manual edits not present in either parent, --no-merges removes the only commit whose diff exposes those changes. The agent therefore never triages them, and once a later non-merge commit advances the marker, the documentation change is permanently skipped; include nontrivial merge commits in the review input or separately inspect their merge-result diffs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid truncating the Git pipeline under pipefail

When the pending range is large enough for head to close the pipe before git log finishes, the Actions default Bash shell runs with -o pipefail, so Git's resulting SIGPIPE status (141) makes this step fail instead of processing the requested batch. I reproduced the exact git log | head shape with more than 150 commits; because the marker is never advanced, every scheduled retry encounters the same backlog and fails again. The Bash pipeline semantics specify that pipefail returns the rightmost nonzero command status, so apply the limit within Git (for example with -n) rather than truncating its output through head.

Useful? React with 👍 / 👎.

count=$(wc -l < .sync/commits.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
if [ "$count" -eq 0 ]; then
echo "No new commits on LibreChat dev since $since."
exit 0
fi
last=$(tail -n 1 .sync/commits.txt | cut -d' ' -f1)
echo "since=$since" >> "$GITHUB_OUTPUT"
echo "last=$last" >> "$GITHUB_OUTPUT"
echo "Reviewing $count commits: ${since:0:10}..${last:0:10}"
sha256sum .git/config > .sync/git-config.sha256
ls -la .git/hooks > .sync/git-hooks.list

- name: Document changes with Claude
id: claude
if: steps.range.outputs.count != '0'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Follow the instructions in .github/docs-sync/prompt.md.
Review the ${{ steps.range.outputs.count }} LibreChat commits listed in .sync/commits.txt
(range ${{ steps.range.outputs.since }}..${{ steps.range.outputs.last }}).
claude_args: |
--model ${{ inputs.model || 'claude-opus-5-5' }}
--max-turns 400
--allowedTools "Read,Glob,Grep,Task,TodoWrite,Edit(./content/docs/**),Write(./content/docs/**),Edit(./.sync/report.md),Write(./.sync/report.md),Bash(git -C .sync/librechat show:*),Bash(git -C .sync/librechat log:*),Bash(git -C .sync/librechat grep:*),Bash(git -C .sync/librechat ls-tree:*),Bash(git -C .sync/librechat diff:*),Bash(git diff:*),Bash(git status:*),Bash(ls:*),Bash(wc:*)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the API credential out of agent-readable subprocesses

Because this step deliberately processes untrusted commit messages and source, an injected instruction can use the permitted Bash rule to run git diff --no-index /dev/null - < /proc/self/environ; --no-index accepts arbitrary inputs, and the shell redirection exposes its inherited environment, including ANTHROPIC_API_KEY, in the tool result. The model can then place the recovered value in the permitted .sync/report.md, which the later gh pr create or gh pr edit publishes as the PR body. persist-credentials: false protects the GitHub credential but not this API key, so run the agent tools with secrets scrubbed from their subprocess environment or in a sandbox that prevents access to the parent environment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

P1 Badge Security: Remove command-executing options from the agent allowlist

When ANTHROPIC_API_KEY is configured, an attacker whose text reaches LibreChat dev can prompt-inject the scheduled agent into invoking git -C .sync/librechat grep --open-files-in-pager=<command> .... This matches the allowed git ... grep:* prefix, but Git executes the supplied pager as a shell command. It can persist BASH_ENV through $GITHUB_ENV; the later PR step starts Bash with GH_TOKEN (the optional PAT or contents/PR-write job token), enabling credential theft and repository writes. The workspace checks do not inspect runner environment files. Replace general Git Bash access with a fixed argument-validating wrapper.

Useful? React with 👍 / 👎.


- name: Check the result
id: check
if: steps.claude.outputs.conclusion == 'success'
run: |
# Refuse to go on if anything tampered with git's own config or hooks.
sha256sum -c --quiet .sync/git-config.sha256
ls -la .git/hooks | diff -q - .sync/git-hooks.list
git config --local core.hooksPath /dev/null
# Only English docs sources may change.
changed=$(git status --porcelain --untracked-files=all -- . ':!.sync' | awk '{print $2}')
bad=$(printf '%s\n' "$changed" | grep -v -E '^content/docs/' || true)
locale=$(printf '%s\n' "$changed" | grep -E '\.[a-z]{2}(-[A-Z]{2})?\.(mdx|json)$' || true)
if [ -n "$bad$locale" ]; then
echo "::error::unexpected files changed:"; printf '%s\n' $bad $locale
exit 1
fi
# No em dashes, en dashes or " -- " in added lines.
git add -N content/docs
if git diff -U0 -- content/docs | grep -E '^\+[^+]' | grep -n -E '—|–| -- '; then
echo "::error::added lines contain a dash used as punctuation"
exit 1
fi
if [ ! -s .sync/report.md ]; then
echo "::error::Claude did not write .sync/report.md"
exit 1
fi
if [ -z "$changed" ]; then
echo "docs_changed=false" >> "$GITHUB_OUTPUT"
else
echo "docs_changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Open or update the sync pull request
if: steps.check.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.DOCS_SYNC_TOKEN || github.token }}
LAST: ${{ steps.range.outputs.last }}
DOCS_CHANGED: ${{ steps.check.outputs.docs_changed }}
run: |
jq --arg sha "$LAST" '.librechat.lastSyncedCommit = $sha' "$STATE_FILE" > "$STATE_FILE.tmp"
mv "$STATE_FILE.tmp" "$STATE_FILE"
git add content/docs "$STATE_FILE"
git commit -q --no-verify -m "docs: Sync with LibreChat dev up to ${LAST:0:10}"
auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w0)
git -c http.https://github.com/.extraheader="AUTHORIZATION: basic $auth" \
push --no-verify --force origin "HEAD:$SYNC_BRANCH"

title="📚 docs: Sync With LibreChat dev"
[ "$DOCS_CHANGED" = "true" ] || title="🔖 chore: Advance Docs Sync Marker (No Docs Changes)"
existing=$(gh pr list --head "$SYNC_BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "$title" --body-file .sync/report.md
echo "Updated #$existing"
else
gh pr create --draft --base main --head "$SYNC_BRANCH" --title "$title" --body-file .sync/report.md
fi

- name: Summary
if: always() && steps.range.outputs.count != '0'
run: |
[ -f .sync/report.md ] && cat .sync/report.md >> "$GITHUB_STEP_SUMMARY" || true
Loading