Skip to content
Closed
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
14 changes: 13 additions & 1 deletion .github/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,22 @@ SEO depends on workspace UI and server code: landing pages reuse workspace prici

Affected type-checks follow the package graph too; repository audits and lint run whenever the checks workflow runs. Workspace-only unit jobs use a free GitHub runner and do not start the app test shards.

Database integration and lightweight HTTP suites run on free standard GitHub runners with matching GitHub cache setup. A same-source integration comparison of 16 jobs per provider (32 total) had a slowest shard of 6m 04s on 2-vCPU Blacksmith versus 6m 55s on free GitHub, with identical coverage. GitHub trades the observed 51 seconds for zero runner compute cost. This is one paired sample, not a long-run percentile estimate. These explicitly selected GitHub jobs do not follow `CI_PROVIDER`.
Database integration uses 4-vCPU Blacksmith runners with sticky dependency disks, following `CI_PROVIDER` and falling back to standard GitHub runners with matching setup. A prior same-source comparison of 16 jobs per provider (32 total) had a slowest shard of 6m 04s on 2-vCPU Blacksmith versus 6m 55s on GitHub, with identical coverage. This is one paired sample, not a long-run percentile estimate. Evaluate runner changes against complete coverage and the overall CI gate, since a faster shard does not shorten a run when another job finishes later. Lightweight HTTP suites retain free standard GitHub runners and do not follow `CI_PROVIDER`.

GitHub runners install dependencies directly rather than transferring Bun and `node_modules` archives. In a same-source paired comparison, those restores took 26.2 seconds before an 82-millisecond install, while a clean install took 12.42 seconds. This is one paired observation; retain the policy only while repeated CI timings support it. Blacksmith keeps its sticky dependency disks. GitHub Turbo output caches remain enabled for jobs that request them, with a commit-specific key and a task/event/fork-scoped restore prefix, so a rerun can reuse an exact entry without creating another archive. Push jobs never restore pull-request entries.

Memory-heavy builds and browser jobs retain appropriately sized Blacksmith runners. Electron uses 4-vCPU Blacksmith: both shards passed within 14 seconds of the 8-vCPU control; the free GitHub rest shard was slower and its lease run stalled.

Every HTTP and browser group consumes the same production build in an isolated job/database. Electron also uses that build and skips its development-route warm-up. Local HTTP and Electron development runs retain their dev-server behavior. Build inputs exclude test-only files so a harness edit can reuse an unchanged production build.

Desktop inbox protocol, assistant actions UI, and organization search UI run as three parallel HTTP jobs with isolated databases, apps, and reports. They retain one acceptance task boundary, so changing any of their harnesses selects all three jobs. Desktop inbox needs no browser. The UI jobs and browser extension install Chromium while their app starts, waiting for both before executing assertions; browser-install failures still fail the job and preserve the install log. No acceptance assertions are removed by the split.

Browser extension acceptance runs native interaction and access-revalidation groups in parallel jobs with isolated databases, apps, browser profiles, and reports. Both retain the same task boundary, so an affected extension harness selects both groups. Native streaming and first-open capture checks retain real timers. Access checks install Playwright's clock before the app mounts: advancing production polling timers preserves real HTTP and database authorization checks without waiting through each polling interval. Capture timestamps originate in Chromium's isolated world, so the harness restores wall time after each advance. The local `browser-extension` group still runs the complete suite; live-provider runs require this complete mode and retain native timing throughout.

Integration shard weights come from successful reports for both push and migration provisioning paths. Refresh them with `scripts/update-integration-durations.ts`, then check the new partition against a separate complete run before adopting it. Weights only change scheduling; discovery still owns coverage. The current refresh uses runs [41348786822](https://github.com/simstudioai/sim/actions/runs/41348786822) and [41377145857](https://github.com/simstudioai/sim/actions/runs/41377145857), with [41377810718](https://github.com/simstudioai/sim/actions/runs/41377810718) held out for validation.

Desktop pickup checks overlap their real grace periods using distinct calls and waiters under fixed authorization and parent-run fixtures. Recovery exhaustion checks keep worker responses, injected faults, and observed requests scoped to each stream before overlapping their real backoff schedules. Their immediate-takeover checks remain serial. File isolation and serial execution between integration files remain unchanged; these suites still verify actual timeout, lease, retry-floor, and late-claim behavior against PostgreSQL and Redis.

SCIM's production app uses a `sim.ai` fixture hostname mapped to loopback, since `NEXT_PUBLIC_FORCE_HOSTED` is ignored in production. Readiness, HTTP tests, and internal API requests still connect to loopback. The SCIM suite verifies that ending an Enterprise subscription rejects both owner administration and an existing bearer token, and that restoring it restores access.

## Coverage removed or consolidated
Expand All @@ -48,3 +58,5 @@ The separate `desktop-e2e` workflow retains its desktop-specific path filters. I
The initial weights are median test durations in seconds from runs [39734295953](https://github.com/simstudioai/sim/actions/runs/39734295953), [39833640524](https://github.com/simstudioai/sim/actions/runs/39833640524), and [39836596108](https://github.com/simstudioai/sim/actions/runs/39836596108). Refresh them from complete `desktop-smoke-reports-*` artifacts when the workload changes: sum `tests[].results[].duration` by `spec.file` in each run, take the median across runs, and divide milliseconds by 1,000. The weights steer scheduling only; Playwright discovery owns coverage. Before adopting new weights, compare repeated paired runs and the complete case/status multiset, including skipped cases and retries.

Each shard uploads its coverage/timing report, the complete discovery report, and its deterministic shard plan. `bun run test:scripts scripts/duration-shards.test.ts scripts/desktop-e2e.test.ts` checks partition coverage, unknown files, deterministic ordering, and balancing. The CLI regression executes tiny browser-free Playwright fixtures to prove that imported test factories and paths containing regex characters are selected correctly; it launches no Electron or browser.

Local pushes run `lint:check` and `type-check` through Husky before Git sends commits. These gates reuse Turborepo's task cache; a failure stops the push. The pre-commit hook still formats staged files, and hosted CI owns tests, audits, builds, and acceptance checks.
21 changes: 16 additions & 5 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: setup
description: Install the pinned Bun and Node toolchain, mount the dependency (and optionally Turbo) caches, and install workspace dependencies.
description: Install the pinned toolchain and workspace dependencies, using sticky dependency disks on Blacksmith and optional Turbo output caching on either provider.

inputs:
provider:
Expand All @@ -19,12 +19,13 @@ inputs:
required: false
default: ''

# Cache keys are scoped by event name, and fork PRs get their own namespace on
# Sticky disk keys are scoped by event name, and fork PRs get their own namespace on
# top: untrusted fork runs must never share a cache with push runs (whose caches
# feed production image builds) or with trusted internal-PR runs.
#
# node_modules also keys on the lockfile hash: a sticky disk is a mutable volume,
# and `bun install --frozen-lockfile` adds what the lockfile needs without
# GitHub runners install directly: restoring both dependency archives takes longer
# than a clean Bun install. node_modules on sticky disks keys on the lockfile hash:
# `bun install --frozen-lockfile` adds what the lockfile needs without
# pruning what it dropped, so branches on different lockfiles were contaminating
# each other (a stale @next/swc 16.2.6 outlived the 16.2.11 bump). The bun and
# Turbo caches are content/hash-addressed, so they stay shared — that is what
Expand All @@ -45,27 +46,37 @@ runs:
registry-url: ${{ inputs.registry-url }}

- name: Mount Bun cache
if: inputs.provider == '' || inputs.provider == 'blacksmith'
uses: ./.github/actions/cache
with:
provider: ${{ inputs.provider }}
key: ${{ github.repository }}-bun-cache-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
path: ~/.bun/install/cache

- name: Mount node_modules
if: inputs.provider == '' || inputs.provider == 'blacksmith'
uses: ./.github/actions/cache
with:
provider: ${{ inputs.provider }}
key: ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-${{ hashFiles('bun.lock') }}
path: ./node_modules

- name: Mount Turbo cache
if: inputs.turbo-cache-key != ''
if: inputs.turbo-cache-key != '' && (inputs.provider == '' || inputs.provider == 'blacksmith')
uses: ./.github/actions/cache
with:
provider: ${{ inputs.provider }}
key: ${{ github.repository }}-${{ inputs.turbo-cache-key }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
path: ./.turbo

- name: Restore and save Turbo cache
if: inputs.turbo-cache-key != '' && inputs.provider != '' && inputs.provider != 'blacksmith'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
key: ${{ github.repository }}-${{ inputs.turbo-cache-key }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-${{ github.sha }}
restore-keys: ${{ github.repository }}-${{ inputs.turbo-cache-key }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-
path: ./.turbo

# Bun retries a failed request itself, but a registry connection that stalls
# without failing leaves `bun install` waiting until the job's own timeout
# cancels it, the whole budget spent on one hung step. Healthy installs finish
Expand Down
14 changes: 13 additions & 1 deletion .github/ci-suites.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@
"task": "ci:e2e-desktop-inbox"
},
{
"group": "browser-extension",
"group": "assistant-actions",
"task": "ci:e2e-desktop-inbox"
},
{
"group": "organization-search",
"task": "ci:e2e-desktop-inbox"
},
{
"group": "browser-extension-native",
"task": "ci:e2e-browser-extension"
},
{
"group": "browser-extension-access",
"task": "ci:e2e-browser-extension"
},
{
Expand Down
99 changes: 61 additions & 38 deletions .github/scripts/http-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# CI groups, which reuse the production build from `e2e-build`.
# The job extracts it into apps/sim/.next before running these suites.
#
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|browser-extension|project-files|mobile|seo> (run from apps/sim)
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-access|project-files|mobile|seo> (run from apps/sim)
#
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
Expand All @@ -20,7 +20,7 @@
# telemetry flush runs detached and still writes .next/dev).
set -euo pipefail

group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|browser-extension|project-files|mobile|seo>}
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-access|project-files|mobile|seo>}
report_dir="$RUNNER_TEMP/e2e"
ready_timeout_seconds=600
mkdir -p "$report_dir"
Expand Down Expand Up @@ -71,6 +71,24 @@ finish() {
}
trap finish EXIT

start_browser_install() {
local name=$1
shift
browsers_log="$report_dir/$name-playwright-install.log"
setsid --wait bunx --no-install playwright install --with-deps "$@" > "$browsers_log" 2>&1 &
browsers_pid=$!
}

wait_for_browsers() {
if ! wait "$browsers_pid"; then
browsers_pid=''
tail -n 100 "$browsers_log"
echo "::error::Playwright browser install failed."
exit 1
fi
browsers_pid=''
}

# start_app <name> <port> <label> [record-http-status]
start_app() {
local name=$1 port=$2 label=$3
Expand Down Expand Up @@ -209,43 +227,55 @@ case "$group" in
bun run test:workflow-stop-after:e2e
;;

# The desktop background executor's protocol: device registration, the SSE doorbell over Redis
# pub/sub, presence, leased claims, Stop and isolation.
desktop-inbox)
desktop-inbox|assistant-actions|organization-search)
export REDIS_URL=redis://127.0.0.1:6379
export NEXT_PUBLIC_FORCE_HOSTED=false
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
bunx --no-install playwright install --with-deps chromium
if [ "$group" != desktop-inbox ]; then start_browser_install "$group" chromium; fi
export INTERNAL_API_SECRET=desktop-inbox-http-ci-local-secret-at-least-32-characters
export DB_TX_TRIPWIRE=throw
start_app desktop-inbox 3019 'desktop executor' record-http-status
DESKTOP_INBOX_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
DESKTOP_INBOX_E2E_DATABASE_URL="$DATABASE_URL" \
DESKTOP_INBOX_E2E_REDIS_URL="$REDIS_URL" \
DESKTOP_INBOX_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
DESKTOP_INBOX_E2E_REPORT_PATH="$report_dir/desktop-inbox-http-report.json" \
bun run test:desktop-inbox:e2e
ASSISTANT_ACTIONS_E2E_BASE_URL="$local_app_url" \
ASSISTANT_ACTIONS_E2E_DATABASE_URL="$DATABASE_URL" \
ASSISTANT_ACTIONS_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ASSISTANT_ACTIONS_E2E_REPORT_PATH="$report_dir/assistant-actions-ui-report.json" \
bun --no-env-file scripts/test-assistant-actions-e2e.ts
ORGANIZATION_SEARCH_E2E_BASE_URL="$local_app_url" \
ORGANIZATION_SEARCH_E2E_DATABASE_URL="$DATABASE_URL" \
ORGANIZATION_SEARCH_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ORGANIZATION_SEARCH_E2E_REPORT_PATH="$report_dir/organization-search-ui-report.json" \
bun --no-env-file scripts/test-organization-search-e2e.ts
start_app "$group" 3019 "$group" record-http-status
if [ "$group" != desktop-inbox ]; then wait_for_browsers; fi
case "$group" in
desktop-inbox)
DESKTOP_INBOX_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
DESKTOP_INBOX_E2E_DATABASE_URL="$DATABASE_URL" \
DESKTOP_INBOX_E2E_REDIS_URL="$REDIS_URL" \
DESKTOP_INBOX_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
DESKTOP_INBOX_E2E_REPORT_PATH="$report_dir/desktop-inbox-http-report.json" \
bun run test:desktop-inbox:e2e
;;
assistant-actions)
ASSISTANT_ACTIONS_E2E_BASE_URL="$local_app_url" \
ASSISTANT_ACTIONS_E2E_DATABASE_URL="$DATABASE_URL" \
ASSISTANT_ACTIONS_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ASSISTANT_ACTIONS_E2E_REPORT_PATH="$report_dir/assistant-actions-ui-report.json" \
bun --no-env-file scripts/test-assistant-actions-e2e.ts
;;
organization-search)
ORGANIZATION_SEARCH_E2E_BASE_URL="$local_app_url" \
ORGANIZATION_SEARCH_E2E_DATABASE_URL="$DATABASE_URL" \
ORGANIZATION_SEARCH_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ORGANIZATION_SEARCH_E2E_REPORT_PATH="$report_dir/organization-search-ui-report.json" \
bun --no-env-file scripts/test-organization-search-e2e.ts
;;
esac
;;

browser-extension)
browser-extension|browser-extension-native|browser-extension-access)
extension_group=all
case "$group" in
browser-extension-native) extension_group=native ;;
browser-extension-access) extension_group=access ;;
esac
export REDIS_URL=redis://127.0.0.1:6379
export NEXT_PUBLIC_FORCE_HOSTED=false
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true
export API_ENCRYPTION_KEY="$ENCRYPTION_KEY"
bunx --no-install playwright install --with-deps chromium
start_browser_install "$group" chromium
extension_identity=$(bun - <<'JS'
import { generateKeyPairSync, createHash } from 'node:crypto'
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
Expand All @@ -257,10 +287,12 @@ JS
)
export SIM_EXTENSION_KEY="${extension_identity%%$'\n'*}"
export BROWSER_EXTENSION_IDS="${extension_identity#*$'\n'}"
start_app browser-extension 3007 'browser extension' record-http-status
start_app "$group" 3007 'browser extension' record-http-status
wait_for_browsers
EXTENSION_APP_E2E_BASE_URL="$local_app_url" \
EXTENSION_APP_E2E_DATABASE_URL="$DATABASE_URL" \
EXTENSION_APP_E2E_REPORT_PATH="$report_dir/browser-extension/report.json" \
EXTENSION_APP_E2E_GROUP="$extension_group" \
EXTENSION_APP_E2E_REPORT_PATH="$report_dir/$group/report.json" \
bun run test:browser-extension:e2e
;;

Expand All @@ -280,19 +312,10 @@ JS
export INTERNAL_API_SECRET=mobile-http-ci-local-secret-at-least-32-characters
export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3025 SOCKET_SERVER_URL=http://127.0.0.1:3025
read -ra browsers <<< "${MOBILE_E2E_BROWSER:-chromium webkit}"
# The browsers install while the app boots, in their own process group so finish() can stop them.
browsers_log="$report_dir/mobile-playwright-install.log"
setsid --wait bunx --no-install playwright install --with-deps "${browsers[@]}" > "$browsers_log" 2>&1 &
browsers_pid=$!
start_browser_install mobile "${browsers[@]}"
start_app mobile 3024 'mobile browser' record-http-status
start_realtime mobile 3025
if ! wait "$browsers_pid"; then
browsers_pid=''
tail -n 100 "$browsers_log"
echo "::error::Playwright browser install failed."
exit 1
fi
browsers_pid=''
wait_for_browsers
MOBILE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
MOBILE_E2E_DATABASE_URL="$DATABASE_URL" \
MOBILE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
Expand Down
Loading