Skip to content

Commit 1a14c87

Browse files
authored
improvement(ci): target acceptance and reuse validated build caches (#9583)
Select acceptance work from the task graph, separate docs site and platform checks, and preserve complete comparison history for push attribution. Build docs once and run isolated acceptance groups in parallel after bounded server readiness. Reuse validated content and incremental build inputs with bounded retention. Validate native source/toolchain fingerprints and restored output integrity, seed trusted branch caches, and balance desktop smoke across four whole-file shards. Bound organization-search diagnostics and reclaim closed-PR merge-ref caches. Validation: full root tests, all workspace types, lint and 62 audits; real cache invalidation and Git attribution controls; complete hosted docs, integration, organization-search, Electron and packaged smoke coverage; final-head bot reviews.
1 parent ebf29c0 commit 1a14c87

26 files changed

Lines changed: 1017 additions & 199 deletions

‎.github/CI.md‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installin
44

55
A PR compares its merge base with the checked-out merge commit. Main/staging pushes execute all checks; the parent `ci` workflow skips checks on `dev` pushes, while PRs targeting `dev` still run checks. A manual `checks` run executes all checks unless supplied a full base commit SHA to reproduce a scoped plan. Missing history, an empty diff, unknown files outside the workspace graph, root script/CI changes, or a planner error run every check. Missing selection outputs fail the aggregate gate. The gate also rejects an omitted dependency, any failed/cancelled job, and any skip that its explicit selection flag did not authorize.
66

7+
Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with their merge base. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Independent SDK and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip.
8+
9+
Docs production builds run once and publish an artifact to isolated HTTP, corpus, Chromium, and WebKit jobs. Source checks keep their own checkout because generator verification mutates inputs. Content caches use separate lint/source/build namespaces; generation validates every restore, repairs missing declarations, and removes obsolete modules. The Next build cache keeps its supported incremental behavior. Content and Next caches have size limits, and Next cache files expire after seven days; successful content eviction happens after all consumers finish.
10+
711
| Change | Expensive checks selected |
812
| --- | --- |
913
| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers |
@@ -28,11 +32,17 @@ Database integration uses 4-vCPU Blacksmith runners with sticky dependency disks
2832

2933
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.
3034

35+
Closing a same-repository pull request deletes only its GitHub merge-ref caches after verifying that it remains closed or merged. The regular `pull_request` workflow skips forks and Dependabot because their tokens cannot delete caches; those entries retain GitHub's normal cache eviction. Cleanup checks out no code and never selects a branch-wide or default-branch cache. Blacksmith sticky disks retain their separate lifecycle.
36+
3137
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.
3238

3339
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.
3440

35-
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.
41+
Desktop inbox protocol, assistant actions UI, and organization search UI run as three parallel HTTP jobs with isolated databases, apps, and reports. Each harness owns its acceptance task, so an edit selects its own job. The inbox protocol task depends on server and realtime code; the browser tasks also depend on workspace UI. 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.
42+
43+
Live Electron acceptance tracks its live-app suite, fixtures, and Playwright configuration separately from shell-only smoke tests. Changing a shell-only smoke case runs the dedicated desktop workflow without starting the app's production build and live lease checks.
44+
45+
Desktop smoke weights also stay outside the desktop runtime task. Editing weights reruns desktop partition coverage without selecting live-app acceptance.
3646

3747
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.
3848

@@ -53,9 +63,11 @@ The long-import and lease-expiration checks remain: they prove distinct survival
5363

5464
Local planner validation is `bun run test:scripts scripts/ci-changes.test.ts scripts/ci-gate.test.ts`. It executes real Turbo queries in small Git fixtures; no application build or database is required. Full builds, services, browser suites, and Electron checks run on hosted CI.
5565

56-
The separate `desktop-e2e` workflow retains its desktop-specific path filters. Its macOS shell smoke suite runs whole files across three isolated runners, with one worker per runner. `scripts/desktop-e2e.ts` asks Playwright to discover the complete suite, then assigns longer files first to the currently lightest shard using `apps/desktop/e2e-durations.json`. Exact escaped file filters preserve serial fixtures. Unmeasured files receive the median measured weight, deleted files cannot select tests, and invalid or empty selections fail. Normal local runs, packaging smoke and the weekly Electron canary keep their existing commands.
66+
The separate `desktop-e2e` workflow retains its desktop-specific path filters. Its macOS shell smoke suite runs whole files across four isolated runners, with one worker per runner. The matrix owns the partition count; job names and runner arguments use `strategy.job-total`. `scripts/desktop-e2e.ts` asks Playwright to discover the complete suite, then assigns longer files first to the currently lightest shard using `apps/desktop/e2e-durations.json`. Exact escaped file filters preserve serial fixtures. Unmeasured files receive the median measured weight, deleted files cannot select tests, and invalid or empty selections fail. Normal local runs, packaging smoke and the weekly Electron canary keep their existing commands.
67+
68+
The current weights are median test durations in seconds from runs [41496623284](https://github.com/simstudioai/sim/actions/runs/41496623284), [41497346251](https://github.com/simstudioai/sim/actions/runs/41497346251), and [41502340540](https://github.com/simstudioai/sim/actions/runs/41502340540). 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.
5769

58-
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.
70+
Desktop native outputs are keyed by source, build recipes, compiler, SDK, OS, architecture, and Node header bytes. Every build still validates restored checksums, executable modes, universal architectures, and the helper signature; invalid or incomplete outputs rebuild. Native-input pushes to main/staging seed trusted Node 22 and 24 caches without repeating the PR smoke suite. PR cache entries never populate trusted branch caches.
5971

6072
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.
6173

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Cache desktop native outputs
2+
description: Reuse validated native outputs only for matching source and toolchain inputs
3+
runs:
4+
using: composite
5+
steps:
6+
- name: Identify native build inputs
7+
id: inputs
8+
shell: bash
9+
working-directory: apps/desktop
10+
run: echo "key=$(bun run scripts/native-build-cache.ts)" >> "$GITHUB_OUTPUT"
11+
- name: Restore native compilation outputs
12+
continue-on-error: true
13+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
14+
with:
15+
path: apps/desktop/dist/native
16+
key: desktop-native-v1-${{ runner.os }}-${{ steps.inputs.outputs.key }}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Docs content cache
2+
description: Reuse validated incremental documentation compilation without sharing concurrent writer snapshots.
3+
inputs:
4+
provider:
5+
description: The CI_PROVIDER repo variable.
6+
required: false
7+
default: ''
8+
namespace:
9+
description: A distinct name for each concurrently writing job.
10+
required: true
11+
generate:
12+
description: Generate and validate content after restoring it. Set false only when the following build already runs content:generate.
13+
required: false
14+
default: 'true'
15+
next-cache:
16+
description: Also restore the incremental Next build cache.
17+
required: false
18+
default: 'false'
19+
runs:
20+
using: composite
21+
steps:
22+
- name: Identify the compiler runtime
23+
id: runtime
24+
shell: bash
25+
env:
26+
CACHE_NAMESPACE: ${{ inputs.namespace }}
27+
GENERATE: ${{ inputs.generate }}
28+
NEXT_CACHE: ${{ inputs.next-cache }}
29+
run: |
30+
[[ "$CACHE_NAMESPACE" =~ ^[a-z0-9][a-z0-9-]*$ ]]
31+
[[ "$GENERATE" == true || "$GENERATE" == false ]]
32+
[[ "$NEXT_CACHE" == true || "$NEXT_CACHE" == false ]]
33+
toolchain=$(node --version)
34+
toolchain="$toolchain-$(bun --version)"
35+
echo "toolchain=$toolchain" >> "$GITHUB_OUTPUT"
36+
compiler=$(git ls-files -s -- bun.lock package.json patches apps/docs/package.json apps/docs/scripts/compile-content.ts apps/docs/lib apps/docs/components/workflow-preview packages | sha256sum | awk '{ print $1 }')
37+
echo "compiler=$compiler" >> "$GITHUB_OUTPUT"
38+
- name: Restore incremental content
39+
continue-on-error: true
40+
uses: ./.github/actions/cache
41+
with:
42+
provider: ${{ inputs.provider }}
43+
key: ${{ github.repository }}-docs-content-v1-${{ inputs.namespace }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.toolchain }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && format('-fork-{0}', github.event.pull_request.number) || '' }}-${{ steps.runtime.outputs.compiler }}
44+
path: ./apps/docs/.content
45+
- name: Bound restored content
46+
shell: bash
47+
run: bash .github/actions/docs-content-cache/prune.sh content
48+
- name: Restore incremental Next build
49+
if: inputs.next-cache == 'true'
50+
continue-on-error: true
51+
uses: ./.github/actions/cache
52+
with:
53+
provider: ${{ inputs.provider }}
54+
key: ${{ github.repository }}-docs-next-v1-${{ inputs.namespace }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.toolchain }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && format('-fork-{0}', github.event.pull_request.number) || '' }}-${{ hashFiles('bun.lock') }}
55+
path: ./apps/docs/.next/cache
56+
- name: Bound the incremental build cache
57+
if: inputs.next-cache == 'true'
58+
shell: bash
59+
run: bash .github/actions/docs-content-cache/prune.sh next
60+
- name: Generate and validate documentation modules
61+
id: generate
62+
if: inputs.generate == 'true'
63+
shell: bash
64+
run: bun run --cwd apps/docs content:generate --prune
65+
- name: Bound content after an unsuccessful generation
66+
if: always() && inputs.generate == 'true' && steps.generate.outcome != 'success'
67+
shell: bash
68+
run: bash .github/actions/docs-content-cache/prune.sh content
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
case ${1:-} in
5+
content)
6+
cache=apps/docs/.content
7+
maximum_kilobytes=524288
8+
;;
9+
next)
10+
cache=apps/docs/.next/cache
11+
maximum_kilobytes=2097152
12+
;;
13+
*) echo 'Usage: prune.sh <content|next>' >&2; exit 2 ;;
14+
esac
15+
if [ ! -d "$cache" ]; then exit 0; fi
16+
if [ "$1" = next ]; then find "$cache" -type f -mtime +7 -delete; fi
17+
kilobytes=$(du -sk "$cache" | awk '{ print $1 }')
18+
if [ "$kilobytes" -gt "$maximum_kilobytes" ]; then
19+
find "$cache" -mindepth 1 -delete
20+
fi

‎.github/ci-suites.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
},
1919
{
2020
"group": "assistant-actions",
21-
"task": "ci:e2e-desktop-inbox"
21+
"task": "ci:e2e-assistant-actions"
2222
},
2323
{
2424
"group": "organization-search",
25-
"task": "ci:e2e-desktop-inbox"
25+
"task": "ci:e2e-organization-search"
2626
},
2727
{
2828
"group": "browser-extension-native",

‎.github/scripts/ci-plan.gql‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ query($base: String!, $head: String!) {
33
affectedTasks(base: $base, head: $head, tasks: [
44
"test", "build", "type-check", "ci:integration", "ci:desktop-live",
55
"ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox",
6+
"ci:e2e-assistant-actions", "ci:e2e-organization-search",
7+
"ci:e2e-docs-platform",
68
"ci:e2e-browser-extension", "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
79
]) {
810
items {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: cache cleanup
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: cache-cleanup-${{ github.event.pull_request.number }}
12+
cancel-in-progress: false
13+
14+
jobs:
15+
cleanup:
16+
if: >-
17+
github.event.pull_request.head.repo.full_name == github.repository
18+
&& github.event.pull_request.user.login != 'dependabot[bot]'
19+
&& github.actor != 'dependabot[bot]'
20+
runs-on: ubuntu-24.04
21+
timeout-minutes: 5
22+
permissions:
23+
actions: write
24+
pull-requests: read
25+
steps:
26+
- name: Remove caches scoped to the closed pull request
27+
env:
28+
GH_TOKEN: ${{ github.token }}
29+
GH_REPO: ${{ github.repository }}
30+
PR_NUMBER: ${{ github.event.pull_request.number }}
31+
run: |
32+
state=$(gh pr view "$PR_NUMBER" --json state --jq .state)
33+
case "$state" in
34+
OPEN) exit 0 ;;
35+
CLOSED|MERGED) ;;
36+
*) echo "::error::Unexpected pull request state: $state"; exit 1 ;;
37+
esac
38+
gh cache delete --all --ref "refs/pull/${PR_NUMBER}/merge" --succeed-on-no-caches

‎.github/workflows/checks.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,10 @@ jobs:
685685
"$RUNNER_TEMP/actionlint" -color -shellcheck= -pyflakes=
686686
687687
- name: Generate documentation modules for import audits
688-
run: bun run --cwd apps/docs content:generate
688+
uses: ./.github/actions/docs-content-cache
689+
with:
690+
provider: ${{ vars.CI_PROVIDER }}
691+
namespace: lint
689692

690693
# Every zero-argument `check:*` script, run concurrently. The list is derived in
691694
# scripts/run-audits.ts, which also writes the per-audit timing table to the job
@@ -727,6 +730,10 @@ jobs:
727730
fi
728731
echo "✅ Schema and migrations are in sync"
729732
733+
- name: Bound documentation content cache
734+
if: always()
735+
run: bash .github/actions/docs-content-cache/prune.sh content
736+
730737
# Type errors in apps/sim's unit tests, ratcheted per file and TS code against a committed
731738
# baseline (scripts/check-test-types.ts). It type-checks the whole app with its ~3.4k test files:
732739
# lint's uncached apps/sim type-check alone takes ~35 s on this runner, so it gets its own job

0 commit comments

Comments
 (0)