Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
925db2e
🪞 feat: Expand Agent Cards Into Their Detail Dialog (#15798)
berry-13 Sep 22, 2026
339a323
🎨 chore: Enforce Design-System Rules With @shadcn/lint (#15981)
berry-13 Sep 22, 2026
5b0ff14
🧵 fix: Restore Classes tailwind-merge Was Silently Dropping (#15995)
berry-13 Sep 22, 2026
4519fe0
⚖️ refactor: Move Policy Links to the Signup Consent (#16138)
berry-13 Sep 23, 2026
4ffe382
📧 feat: add email address change (#14597)
berry-13 Sep 23, 2026
cb78d89
🌊 refactor: Upgrade the Client to Tailwind v4 (#15996)
berry-13 Sep 23, 2026
cd44344
🔔 feat: Unseen Reply Indicators, Reply Notifications, and Mark as Unr…
berry-13 Sep 23, 2026
4617302
🔑 feat: Passkey Authentication with WebAuthn Sign-In and Management (…
berry-13 Sep 23, 2026
1e7b4a9
🎨 feat: Check Color Tokens and Class Existence (#16041)
berry-13 Sep 23, 2026
33a11f3
🥧 feat: Show Last-Turn Token Usage Beside Conversation Totals (#16230)
lia-by-librechat[bot] Sep 23, 2026
f903cf3
🔑 fix: Keep Passkey Sign-In Reachable When Email Login Is Disabled (#…
berry-13 Sep 23, 2026
a27d6ec
🔑 fix: Refresh the Stored Passkey Backup State on Each Sign-In (#16255)
berry-13 Sep 23, 2026
7b27e79
🧩 feat: MCP Apps support (#13831)
dustinhealy Sep 24, 2026
7c60a20
🟨 feat: Add a ClickHouse Reference Theme (#16262)
berry-13 Sep 24, 2026
627ded7
🥼 ci: Gate MCP Apps on Executed Browser Tests (#16283)
lia-by-librechat[bot] Sep 24, 2026
a9571ee
🧹 refactor: Keep Mongoose Types Out of the Passkey Handler Contract (…
berry-13 Sep 24, 2026
55f3187
🥢 fix: Fit MCP App Snapshots Within Message Documents (#16299)
lia-by-librechat[bot] Sep 24, 2026
55ca911
🎨 style: Semantic Colour Roles for Builder, Tools and Sharing Surface…
berry-13 Sep 24, 2026
07bbbc0
📜 refactor: Declare the Chat Context Contract (#16288)
berry-13 Sep 24, 2026
be1178d
🧮 refactor: Pure Step Reducer Behind useStepHandler (#16289)
berry-13 Sep 24, 2026
27f78bc
🎨 feat: Pick the Deployment Theme in librechat.yaml (#16286)
berry-13 Sep 24, 2026
b89ed0a
🧹 chore: Remove the Unmounted Files Component Tree (#16243)
berry-13 Sep 24, 2026
ccae933
🔐 fix: Revoke Password Reset Credentials Independently (#16320)
berry-13 Sep 24, 2026
0173047
🧰 refactor: Pure Step Reducer for Tool Call Events (#16290)
berry-13 Sep 24, 2026
b0783e8
🔑 fix: Invalidate the Auth User Cache After a CLI Password Reset (#16…
berry-13 Sep 24, 2026
e396ea9
🫸 fix: Gate MCP App Views and Actions on User Intent (#16297)
lia-by-librechat[bot] Sep 24, 2026
41d7650
🛩️ ci: Allow More Time for Chrome Startup in Client Build Recovery (#…
lia-by-librechat[bot] Sep 24, 2026
c04bc7c
🧩 fix: Keep Streamed Parts Intact Across Replays and Early Completion…
berry-13 Sep 24, 2026
8239583
🔇 fix: Mute the Create Assistant Empty State in the Assistant Picker …
berry-13 Sep 24, 2026
91f78cd
♿ fix: Keep Identity Badge Labels at Text Contrast (#16337)
berry-13 Sep 24, 2026
c1fbeac
🖌️ refactor: Read Theme Roles in Prose, Settings Dialog and Composer …
berry-13 Sep 24, 2026
62a4e3b
🎨 feat: Theme the Shared Code Block With surface-code and a New surfa…
berry-13 Sep 24, 2026
36ba2d3
🫗 fix: Bound MCP App Responses and In-Flight Work (#16300)
lia-by-librechat[bot] Sep 25, 2026
3c4aa1a
🖼️ fix: Paint the Shared Link Tenant's Deployment Theme (#16333)
berry-13 Sep 25, 2026
13b6228
📐 refactor: Normalize Stream Events Behind a ChatTransport Type (#16335)
berry-13 Sep 25, 2026
4fb238d
🔌 refactor: Drive useSSE Through the ChatTransport (#16336)
berry-13 Sep 25, 2026
eefeb2a
⚙️ feat: Make the Per-User Passkey Cap Configurable (#16259)
berry-13 Sep 25, 2026
6e460b7
📐 feat: Radius and Font Scales Follow the Theme (#16340)
berry-13 Sep 25, 2026
1dc4e0d
🪶 style: Quieter Primitives, Shared Panel Headers, and Flat Managemen…
berry-13 Sep 25, 2026
33871c3
🌫️ feat: Shadow Scale Follows the Theme (#16341)
berry-13 Sep 25, 2026
fd1ca77
🟨 feat: ClickHouse Theme Shape Parity (#16342)
berry-13 Sep 25, 2026
3ae1157
🛶 fix: Reconcile Canary Sync Checks
lia-librechat Sep 25, 2026
733f5e6
🛶 test: Isolate Marketplace Retries and MCP OAuth Ownership
lia-librechat Sep 25, 2026
57e26fa
🛶 style: Match CI Tailwind Formatting in Canary Sync
lia-librechat Sep 25, 2026
a7bdfa7
🧩 feat: Let Theme Readers Ignore Unknown Appearance Tokens (#16373)
berry-13 Sep 25, 2026
bedfa78
🔲 a11y: Form-Control Outlines Clear 3:1 in Every Palette (#16377)
berry-13 Sep 25, 2026
c5c01df
🔌 refactor: Drive the Resumable Stream Through the ChatTransport (#16…
berry-13 Sep 25, 2026
bdd2d4e
🔤 feat: Bundle Inconsolata for the ClickHouse Theme (#16376)
berry-13 Sep 25, 2026
5eed7d5
🎛️ feat: Close the Data-Level Click UI Gaps in the ClickHouse Theme (…
berry-13 Sep 25, 2026
7f4bde8
🧩 feat: Map Message Content Parts to a UI Parts Union (#16374)
berry-13 Sep 26, 2026
c0ea79a
🔎 feat: Filter the Conversation List by Date, Endpoint, Files, and Sh…
berry-13 Sep 26, 2026
e232b1b
🔅 fix: Quiet Form-Control Outlines in the Stock Palettes (#16400)
berry-13 Sep 26, 2026
1cc4277
🔑 feat: Expose the Messages Key on the Chat Contract (#16395)
berry-13 Sep 27, 2026
3a73e50
🛶 fix: Reconcile Canary Server Startup and Static Gates
lia-librechat Sep 27, 2026
4406d34
🪝 feat: AI SDK-Shaped useChat Facade Over the Chat Contract (#16375)
berry-13 Sep 27, 2026
8ffdee3
🗂️ feat: Chat List Properties Menu and Project-Scoped Chats (#16246)
berry-13 Sep 27, 2026
2be35b2
💬 refactor: ChatForm UI redesign with unified palette (#14473)
berry-13 Sep 27, 2026
bb82aee
🛶 style: Match Schedule Recovery Theme Formatting
lia-librechat Sep 28, 2026
596c4dd
🛶 fix: Keep Revealed Queue Actions and E2E Flows After Composer Redesign
lia-librechat Sep 28, 2026
321dfb1
🛶 fix: Remove Obsolete Steer Recovery Translation Keys
lia-librechat Sep 28, 2026
c92ea0a
🧭 feat: Palette Show All Catalogs, MCP Status and Composer Polish (#1…
berry-13 Sep 28, 2026
f18db2e
🧪 test: Cover MCP App Operation Limits in Authenticated Startup Confi…
berry-13 Sep 28, 2026
ca8fb37
🔌 fix: Reattach a Resume Stream Whose Body Ends Before Final (#16438)
berry-13 Sep 28, 2026
04461d4
⛴️ fix: Keep MCP App and Speech Limits in One E2E Config
lia-librechat Sep 28, 2026
d98ca2b
⛴️ test: Check Code Steers in the Canary Thread
lia-librechat Sep 28, 2026
26b6988
🧯 fix: Outlast Failed Auth Cache Eviction on Credential Changes (#16435)
berry-13 Sep 28, 2026
79994cd
🧭 test: Pin the ClickHouse Theme to Click UI and Route Focus Through …
berry-13 Sep 28, 2026
15142ab
🎨 fix: Fall Back From an Invalid Theme in librechat.yaml Instead of E…
berry-13 Sep 28, 2026
3f7336c
🎛️ refactor: Supply App-Global Chat Settings From the Host (#16466)
berry-13 Sep 28, 2026
228191e
🎯 feat: Give Keyboard Focus Its Own Theme Roles (#16478)
berry-13 Sep 29, 2026
b2da59f
🚦 feat: Give Pressed and Disabled States Their Own Theme Roles (#16487)
berry-13 Sep 29, 2026
1b8801d
🔤 feat: Give Display Type and the Type Scale Their Own Theme Roles (#…
berry-13 Sep 29, 2026
67323e1
🌫️ feat: Give the Dialog Scrims Theme-Owned Opacity Roles (#16492)
berry-13 Sep 29, 2026
a51a0aa
🎨 fix: Draw Library Colours From Theme Roles and Record the Exception…
berry-13 Sep 29, 2026
dcd0689
🖌️ fix: Apply Controlled Themes Before Paint and Theme Prose Links, I…
berry-13 Sep 29, 2026
60b8bb9
test: Count Derived Scrim Tokens as Theme-Owned (#16512)
berry-13 Sep 29, 2026
9689ebf
⚡ perf: Keep the Mermaid Chunk Off the Boot Path (#16493)
berry-13 Sep 29, 2026
01f3e52
🪞 fix: Publish the Theme Defaults and Scale Mapping to Package Consum…
berry-13 Sep 29, 2026
5961f76
🔤 fix: Ship the Theme Font Faces With the Package (#16485)
berry-13 Sep 29, 2026
a50ea26
🫧 fix: Paint Chat Surfaces From Theme Roles (#16483)
berry-13 Sep 29, 2026
1d01178
🧩 fix: Paint Panel Surfaces From Theme Roles (#16484)
berry-13 Sep 29, 2026
5d39e0b
📐 fix: Draw Controls at the Theme Control Radius (#16481)
berry-13 Sep 29, 2026
f51b1a8
🎚️ feat: Give the Switch Theme Roles for Its Size and Knob (#16486)
berry-13 Sep 29, 2026
d8797b2
🗂️ feat: Give the Table Theme Roles for Header Text, Row Rules and De…
berry-13 Sep 29, 2026
fdef598
🧽 fix: Paint Freed Feature Surfaces From Theme Roles (#16517)
berry-13 Sep 29, 2026
e1e1d1b
🎒 feat: Carry Project Context Across Conversations (#15684)
berry-13 Sep 29, 2026
70bf641
⛴️ style: Match Theme Class Ordering in Canary Sync
lia-librechat Sep 30, 2026
206ef47
⛴️ fix: Align Canary Timing Types and CI Formatting
lia-librechat Sep 30, 2026
2d1dfae
⛴️ test: Make Passkey Ordering Fixtures Deterministic
lia-librechat Sep 30, 2026
5c6578f
📏 feat: Give Theme-Sized Controls Their Own Padding and Gap Roles (#1…
berry-13 Sep 30, 2026
0eccfbf
🎛️ feat: Give the Button Theme Roles for Its Label, Height, Fill and …
berry-13 Sep 30, 2026
d06e763
🧩 feat: Ignore Unknown Theme Color Tokens Instead of Dropping the The…
berry-13 Sep 30, 2026
dd1cb0e
🎚️ feat: Group Model Parameters Into Sections (#16247)
berry-13 Sep 30, 2026
37f02c2
test: Enumerate the Click UI Decisions the Theme Cannot Express (#16544)
berry-13 Sep 30, 2026
eb5d74d
🧑‍🎨 feat: Give the Default Avatar and Avatar Placeholder Theme Roles …
berry-13 Sep 30, 2026
0b7d49f
🪟 feat: Give OGDialog Theme Roles for Its Edge, Padding and Title (#1…
berry-13 Sep 30, 2026
0d8d8f6
🪟 feat: Expand, Copy and Tail Long Bash Output (#16555)
berry-13 Sep 30, 2026
ea938f6
🚦 feat: Show Exit Status and Stderr for Attached Workspace Commands (…
berry-13 Sep 30, 2026
385a648
⛴️ style: Match Canary Theme Class Ordering
lia-librechat Sep 30, 2026
2e06906
⛴️ test: Keep Executor and Timing Callback Adapters Aligned
lia-librechat Sep 30, 2026
6df4b8c
🔎 feat: Let Themes Focus Fields by Their Edge and Size Fields and Lab…
berry-13 Sep 30, 2026
5b84c4b
⏳ feat: Add Ephemeral Retention Mode for Forced Temporary Chats (#13811)
berry-13 Sep 30, 2026
cd27c01
🏷️ feat: Give the Badge Label Its Own Ink and Let the Legacy Dialog S…
berry-13 Sep 30, 2026
e6d6092
⛴️ fix: Reset Agent Search and Wait for Completed Diagram Exports
lia-librechat Oct 1, 2026
9dc8db0
🛶 fix: Preserve Canary Sync Resolutions (#16361)
lia-librechat Oct 1, 2026
bbb58a8
⛴️ fix: Preserve Canary Sync Resolutions (#16457)
lia-librechat Oct 1, 2026
443d27b
🎋 fix: Reconcile Canary Promotion Static Checks
lia-librechat Oct 1, 2026
f79ddcf
feat: Focus ring width and offset theme roles (#16587)
berry-13 Oct 1, 2026
5433d5f
🧾 test: Complete the Click UI Not-Expressible List (#16588)
berry-13 Oct 1, 2026
5b98963
🪆 fix: Resolve theme markers to the nearest themed root (#16589)
berry-13 Oct 1, 2026
b8cbc56
refactor: Let the Chat Contract Own Settings and Transport (#16592)
berry-13 Oct 1, 2026
379554a
🚦 refactor: Route Chat Send, Stop, Steer and Queue Through the Transp…
berry-13 Oct 1, 2026
f83c502
📐 feat: Menu, tooltip and tab corner theme roles (#16599)
berry-13 Oct 1, 2026
3d40b03
🌗 feat: Per-Mode Appearance Defaults for Menu and Tooltip Shadows (#1…
berry-13 Oct 1, 2026
82127a2
🌅 fix: Paint the Cached Deployment Theme on the First Frame (#16590)
berry-13 Oct 1, 2026
d016c5b
🧩 refactor: Message Parts Host Context (#16591)
berry-13 Oct 1, 2026
8b9c9a7
🖼️ feat: Give Media Overlays Their Own Theme Roles (#16596)
berry-13 Oct 1, 2026
92f7306
🔁 test: Reload Mid-Reply Reattaches and Finishes (#16598)
berry-13 Oct 1, 2026
c664eb8
📱 feat: Give the Mobile Drawer One Place for Each Control (#16248)
berry-13 Oct 1, 2026
d3d9bc9
🖍️ feat: Give the Remaining Component Colour Literals Theme Roles (#1…
berry-13 Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,10 @@ REGISTER_WINDOW=60
# VERIFY_EMAIL_SUBMISSION_MAX=2
# VERIFY_EMAIL_SUBMISSION_WINDOW=2

# Passkey enrollment, keyed by user id (the routes require a signed-in session).
PASSKEY_STEPUP_MAX=20
PASSKEY_STEPUP_WINDOW=15

LIMIT_CONCURRENT_MESSAGES=true
CONCURRENT_MESSAGE_MAX=2

Expand Down Expand Up @@ -892,6 +896,11 @@ ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
# ALLOW_EMAIL_CHANGE=true # note: enabled by default if omitted/commented out
# `emailChange` in librechat.yaml takes precedence over this, and also sets the link lifetime.
# Multi-node rollouts: keep this false until every node runs this version or newer. Older
# nodes issue and accept password reset links that carry no address binding, so one held by
# a previous address owner can outlive the change and reset the renamed account.
ALLOW_UNVERIFIED_EMAIL_LOGIN=true

SESSION_EXPIRY=1000 * 60 * 15
Expand Down Expand Up @@ -936,6 +945,23 @@ APPLE_KEY_ID=
APPLE_PRIVATE_KEY_PATH=
APPLE_CALLBACK_URL=/oauth/apple/callback

# Passkeys (WebAuthn)
# Requires HTTPS in production; localhost is exempt.
ALLOW_PASSKEY_LOGIN=false
# Optional. Passkeys each account may enroll (1-100). A librechat.yaml
# `passkeys.perUserMax` overrides this; both default to 20.
# MAX_PASSKEYS_PER_USER=20
# Optional. Defaults to the hostname of DOMAIN_CLIENT. Pin this if DOMAIN_CLIENT
# may change: existing passkeys stop working when the RP ID changes.
# PASSKEY_RP_ID=
# Optional. Name shown by the authenticator. Defaults to APP_TITLE.
# PASSKEY_RP_NAME=
# Optional. Comma-separated allowed origins. Defaults to DOMAIN_CLIENT and DOMAIN_SERVER.
# PASSKEY_ORIGINS=
# Adding a passkey requires re-entering the account password, since a passkey is a
# durable single-factor login. PASSKEY_STEPUP_MAX / PASSKEY_STEPUP_WINDOW in the
# Moderation section cap how many enrollment attempts an account may make.

# OpenID
OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
Expand Down
13 changes: 12 additions & 1 deletion .github/scripts/playwright-matrix.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ test('nightly and default manual runs retain full-suite coverage on both stores'
assertPartition(defaults[1], 'redis', 'full', 2);
});

test('PR and nightly matrices preserve both registered-email change modes on one memory shard', () => {
for (const matrix of [prMatrix, ...defaults]) {
const emailLanes = matrix.include.filter((lane) => lane.email_change === 'true');
assert.equal(emailLanes.length, 1);
assert.equal(emailLanes[0].stream_store, 'memory');
assert.match(emailLanes[0].shard, /^1\//);
}
assert.equal(shards.steps.filter((step) => step.if === "matrix.email_change == 'true'").length, 2);
});

test('manual PR coverage opts into the PR fallback matrix without bypassing the PR author gate', () => {
assert.deepEqual(workflow.on.workflow_dispatch.inputs.coverage.options, ['full', 'pr']);
assert.match(
Expand Down Expand Up @@ -188,11 +198,12 @@ test('graduated spec skips keep the same pool across shards and remain opt-in',
test('the required gate still waits for all shards and rejects failures without fighting cancellation', () => {
const gate = workflow.jobs.e2e;
assert.equal(gate.name, 'e2e');
assert.deepEqual(gate.needs, ['codegraph_select', 'e2e_shards', 'mcp_tool_list_changed']);
assert.deepEqual(gate.needs, ['codegraph_select', 'e2e_shards', 'mcp_tool_list_changed', 'mcp_apps']);
assert.equal(shards.strategy['fail-fast'], false);
assert.match(gate.if, /!cancelled\(\)/);
assert.match(shards.if, /!cancelled\(\)/);
assert.match(gate.steps[0].if, /needs\.e2e_shards\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.mcp_apps\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.mcp_tool_list_changed\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.codegraph_select\.outputs\.mcp_run == 'false'/);
assert.equal(gate.steps[0].run, 'exit 1');
Expand Down
109 changes: 101 additions & 8 deletions .github/workflows/playwright-mock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CHANGED: ${{ github.event.pull_request.changed_files }}
E2E_SKIP_ARMED: ${{ vars.CODEGRAPH_E2E_SKIP }}
FULL_INCLUDE: '{"include":[{"name":"memory, shard 1/6","stream_store":"memory","redis_image":"","suite":"full","shard":"1/6","artifact":"memory-1-of-6"},{"name":"memory, shard 2/6","stream_store":"memory","redis_image":"","suite":"full","shard":"2/6","artifact":"memory-2-of-6"},{"name":"memory, shard 3/6","stream_store":"memory","redis_image":"","suite":"full","shard":"3/6","artifact":"memory-3-of-6"},{"name":"memory, shard 4/6","stream_store":"memory","redis_image":"","suite":"full","shard":"4/6","artifact":"memory-4-of-6"},{"name":"memory, shard 5/6","stream_store":"memory","redis_image":"","suite":"full","shard":"5/6","artifact":"memory-5-of-6"},{"name":"memory, shard 6/6","stream_store":"memory","redis_image":"","suite":"full","shard":"6/6","artifact":"memory-6-of-6"},{"name":"redis transport, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"1/2","artifact":"redis-transport-1-of-2"},{"name":"redis transport, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"2/2","artifact":"redis-transport-2-of-2"}]}'
FULL_INCLUDE: '{"include":[{"name":"memory, shard 1/6","stream_store":"memory","redis_image":"","suite":"full","shard":"1/6","artifact":"memory-1-of-6","email_change":"true"},{"name":"memory, shard 2/6","stream_store":"memory","redis_image":"","suite":"full","shard":"2/6","artifact":"memory-2-of-6"},{"name":"memory, shard 3/6","stream_store":"memory","redis_image":"","suite":"full","shard":"3/6","artifact":"memory-3-of-6"},{"name":"memory, shard 4/6","stream_store":"memory","redis_image":"","suite":"full","shard":"4/6","artifact":"memory-4-of-6"},{"name":"memory, shard 5/6","stream_store":"memory","redis_image":"","suite":"full","shard":"5/6","artifact":"memory-5-of-6"},{"name":"memory, shard 6/6","stream_store":"memory","redis_image":"","suite":"full","shard":"6/6","artifact":"memory-6-of-6"},{"name":"redis transport, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"1/2","artifact":"redis-transport-1-of-2"},{"name":"redis transport, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"2/2","artifact":"redis-transport-2-of-2"}]}'
run: |
set +e
note() { echo "$1" >> "$GITHUB_STEP_SUMMARY"; }
Expand Down Expand Up @@ -165,7 +165,8 @@ jobs:
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request != null &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)))
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) ||
github.event.pull_request.head.repo.full_name == github.repository)))
runs-on: ubuntu-latest
timeout-minutes: 30
# Separate runners isolate each shard's user, MongoDB, Redis and fixture state.
Expand All @@ -177,8 +178,8 @@ jobs:
(github.event_name == 'pull_request' && needs.codegraph_select.outputs.e2e_include != '' &&
fromJSON(needs.codegraph_select.outputs.e2e_include)) ||
((github.event_name == 'pull_request' || inputs.coverage == 'pr') &&
fromJSON('{"include":[{"name":"memory, shard 1/6","stream_store":"memory","redis_image":"","suite":"full","shard":"1/6","artifact":"memory-1-of-6"},{"name":"memory, shard 2/6","stream_store":"memory","redis_image":"","suite":"full","shard":"2/6","artifact":"memory-2-of-6"},{"name":"memory, shard 3/6","stream_store":"memory","redis_image":"","suite":"full","shard":"3/6","artifact":"memory-3-of-6"},{"name":"memory, shard 4/6","stream_store":"memory","redis_image":"","suite":"full","shard":"4/6","artifact":"memory-4-of-6"},{"name":"memory, shard 5/6","stream_store":"memory","redis_image":"","suite":"full","shard":"5/6","artifact":"memory-5-of-6"},{"name":"memory, shard 6/6","stream_store":"memory","redis_image":"","suite":"full","shard":"6/6","artifact":"memory-6-of-6"},{"name":"redis transport, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"1/2","artifact":"redis-transport-1-of-2"},{"name":"redis transport, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"2/2","artifact":"redis-transport-2-of-2"}]}')) ||
fromJSON('{"include":[{"name":"memory, shard 1/2","stream_store":"memory","redis_image":"","suite":"full","shard":"1/2","artifact":"memory-1-of-2"},{"name":"memory, shard 2/2","stream_store":"memory","redis_image":"","suite":"full","shard":"2/2","artifact":"memory-2-of-2"},{"name":"redis, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"1/2","artifact":"redis-1-of-2"},{"name":"redis, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"2/2","artifact":"redis-2-of-2"}]}')
fromJSON('{"include":[{"name":"memory, shard 1/6","stream_store":"memory","redis_image":"","suite":"full","shard":"1/6","artifact":"memory-1-of-6","email_change":"true"},{"name":"memory, shard 2/6","stream_store":"memory","redis_image":"","suite":"full","shard":"2/6","artifact":"memory-2-of-6"},{"name":"memory, shard 3/6","stream_store":"memory","redis_image":"","suite":"full","shard":"3/6","artifact":"memory-3-of-6"},{"name":"memory, shard 4/6","stream_store":"memory","redis_image":"","suite":"full","shard":"4/6","artifact":"memory-4-of-6"},{"name":"memory, shard 5/6","stream_store":"memory","redis_image":"","suite":"full","shard":"5/6","artifact":"memory-5-of-6"},{"name":"memory, shard 6/6","stream_store":"memory","redis_image":"","suite":"full","shard":"6/6","artifact":"memory-6-of-6"},{"name":"redis transport, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"1/2","artifact":"redis-transport-1-of-2"},{"name":"redis transport, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"2/2","artifact":"redis-transport-2-of-2"}]}')) ||
fromJSON('{"include":[{"name":"memory, shard 1/2","stream_store":"memory","redis_image":"","suite":"full","shard":"1/2","artifact":"memory-1-of-2","email_change":"true"},{"name":"memory, shard 2/2","stream_store":"memory","redis_image":"","suite":"full","shard":"2/2","artifact":"memory-2-of-2"},{"name":"redis, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"1/2","artifact":"redis-1-of-2"},{"name":"redis, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"2/2","artifact":"redis-2-of-2"}]}')
}}
services:
redis:
Expand Down Expand Up @@ -372,6 +373,19 @@ jobs:
env:
CI: 'true'

- name: Run registered-email change e2e
if: matrix.email_change == 'true'
run: npx playwright test --config=e2e/playwright.config.email.ts
env:
CI: 'true'

- name: Run disabled registered-email change e2e
if: matrix.email_change == 'true'
run: npx playwright test --config=e2e/playwright.config.email.ts
env:
CI: 'true'
E2E_ALLOW_EMAIL_CHANGE: 'false'

- name: Upload Playwright HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
Expand Down Expand Up @@ -400,7 +414,8 @@ jobs:
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request != null &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)))
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) ||
github.event.pull_request.head.repo.full_name == github.repository)))
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
Expand Down Expand Up @@ -542,27 +557,105 @@ jobs:
./redis-config/stop-cluster.sh || true
redis-cli -p 6379 shutdown || true

mcp_apps:
name: MCP Apps (browser and server)
# Always run the dedicated fixture on eligible PRs, schedule, and manual runs. The regular
# mock shards skip it unless E2E_MCP_APPS=true; codegraph gating must not omit this lane.
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request != null &&
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) ||
github.event.pull_request.head.repo.full_name == github.repository))
runs-on: ubuntu-latest
timeout-minutes: 45
env:
CI: 'true'
E2E_CHROMIUM_CHANNEL: chrome
steps:
- uses: actions/checkout@v5

- name: Use Node.js 24.16.0
uses: actions/setup-node@v5
with:
node-version: '24.16.0'

- name: Restore node_modules cache
id: cache-node-modules
uses: actions/cache@v5
with:
path: |
node_modules
client/node_modules
packages/client/node_modules
packages/data-provider/node_modules
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: Verify Chrome is present
run: google-chrome --version

- name: Check MCP Apps result gating and same-repository PR eligibility
run: node --test e2e/mcp-apps/results.test.mjs e2e/mcp-apps/workflow.test.mjs

- name: Check real SDK server fixture
run: node --test e2e/mcp-apps/server.test.mjs

# The runner builds once, then runs all four browser policy phases against one disposable
# MongoDB. It also rejects a Playwright-successful phase if every test was skipped.
- name: Run MCP Apps browser fixture (enabled, disabled, omitted, quota)
run: node e2e/mcp-apps/run.mjs

- name: Upload per-phase Playwright reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: playwright-mcp-apps-reports
path: |
e2e/playwright-report/mcp-apps-*/**
e2e/.generated/mcp-apps-result-*.json
retention-days: 7
if-no-files-found: warn

- name: Upload MCP Apps traces and screenshots
if: failure()
uses: actions/upload-artifact@v6
with:
name: playwright-mcp-apps-results
path: e2e/specs/.test-results/mcp-apps-*/**
retention-days: 7
if-no-files-found: ignore

e2e:
name: e2e
# `!cancelled()`, not `always()`: the gate has to survive a failed shard to report it, but a
# run superseded by `cancel-in-progress` has nothing to adjudicate. Under `always()` it was
# still dispatched onto a fresh runner while every other job went `cancelled`, then read
# `needs.e2e_shards.result == 'cancelled'` and exited 1 — so each superseded push left this
# required check red instead of cancelled. Matches the two lanes it gates.
# required check red instead of cancelled. Matches all three lanes it gates.
if: >-
!cancelled() &&
(github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request != null &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)))
needs: [codegraph_select, e2e_shards, mcp_tool_list_changed]
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) ||
github.event.pull_request.head.repo.full_name == github.repository)))
needs: [codegraph_select, e2e_shards, mcp_tool_list_changed, mcp_apps]
runs-on: ubuntu-latest
steps:
# A codegraph-skipped MCP lane reports `skipped`; that is a decision, not a failure.
- name: Verify every Playwright job passed
if: >-
needs.e2e_shards.result != 'success' ||
needs.mcp_apps.result != 'success' ||
(needs.mcp_tool_list_changed.result != 'success' &&
!(needs.mcp_tool_list_changed.result == 'skipped' && needs.codegraph_select.outputs.mcp_run == 'false'))
run: exit 1
Loading
Loading