Skip to content

Commit 9499b73

Browse files
committed
improvement(ci): select affected task groups with Turborepo
1 parent ff3d930 commit 9499b73

15 files changed

Lines changed: 886 additions & 347 deletions

File tree

‎.github/CI.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# CI selection and acceptance ownership
2+
3+
The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installing application dependencies or provisioning services. `turbo.json` records package dependency propagation; `apps/sim/turbo.json` partitions the app into shared runtime, server, workspace UI, and acceptance tasks. `.github/ci-suites.json` turns those tasks into job matrices. The uploaded `ci-plan` artifact names changed files, selected tasks, attribution reasons, and any fallback.
4+
5+
A PR compares its merge base with the checked-out merge commit. Pushes and normal manual runs execute all checks. A manual `checks` run can supply 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.
6+
7+
| Change | Expensive checks selected |
8+
| --- | --- |
9+
| Independent SDK | That workspace's tests; no app build, services, or browsers |
10+
| App unit test | App test shards and test type check |
11+
| Integration test | Both provision paths; no HTTP/browser job merely for the test edit |
12+
| Workspace UI | Mobile and live Electron, plus app tests/build |
13+
| Landing content | SEO, app tests/build; no database integration or Electron |
14+
| Shared runtime or transitive dependency | All dependent boundaries |
15+
| One E2E harness | Its acceptance group and the shared production build |
16+
17+
The server boundary deliberately includes all API routes and backend domains. Shared modules and unclassified app source remain broad inputs: this is safer than pretending a route's static imports describe database, dynamic registry, or background-worker dependencies. Files read outside a package graph need explicit inputs: CLI acceptance imports `packages/sim-cli/src/embed.ts`, app tests read docs content, and live Electron and realtime acceptance cross app boundaries. When adding such a dependency, update the corresponding task inputs/dependencies and the real Git/Turbo selector scenarios.
18+
19+
`affectedTasks` already uses task inputs. Do not enable `futureFlags.affectedUsingTaskInputs` just for this planner: that flag also changes pruning, which can pull CI-only cross-app task dependencies into production Docker contexts.
20+
21+
Lightweight HTTP suites and database integration run on free standard GitHub runners. Memory-heavy builds and browser/Electron jobs retain appropriately sized Blacksmith runners.
22+
23+
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.
24+
25+
## Coverage removed or consolidated
26+
27+
- SEO is plain HTTP, independent of browser and viewport. Its complete suite runs once in the SEO job, instead of repeating in each of four mobile shards.
28+
- The MCP suite's homepage-returns-200 control duplicated SEO's stronger real-homepage Markdown/content-negotiation check. SEO owns that positive control; MCP retains host isolation, ownership challenge, discovery, API catalogs, unknown-page/API behavior, and refusal of Markdown.
29+
- CLI CI uses one cold/warm-up iteration and one measured iteration instead of three identical measured repetitions. Both retain run completion, selected output, file-reference size, trace, and omitted-workflow-snapshot assertions. The standalone benchmark keeps its configurable sample size.
30+
- A precompiled Electron app needs no route-compilation warm-up, which repeated read/import/chat-switch/Stop flows before each shard. The actual acceptance tests still exercise those behaviors.
31+
32+
The long-import and lease-expiration checks remain: they prove distinct survival/uncertain-outcome behavior across real process failure. Likewise, the two integration provision paths remain because migrations create constraints/triggers that schema push does not.
33+
34+
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.

‎.github/ci-suites.json‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"http": [
3+
{
4+
"group": "scim",
5+
"task": "ci:e2e-scim"
6+
},
7+
{
8+
"group": "cli",
9+
"task": "ci:e2e-cli"
10+
},
11+
{
12+
"group": "stop-after",
13+
"task": "ci:e2e-stop-after"
14+
},
15+
{
16+
"group": "desktop-inbox",
17+
"task": "ci:e2e-desktop-inbox"
18+
},
19+
{
20+
"group": "seo",
21+
"task": "ci:e2e-seo"
22+
}
23+
],
24+
"production": [
25+
{
26+
"group": "mobile",
27+
"name": "mobile, chromium, 1/2",
28+
"browser": "chromium",
29+
"shard": "1/2",
30+
"artifact": "mobile-chromium-1",
31+
"task": "ci:e2e-mobile"
32+
},
33+
{
34+
"group": "mobile",
35+
"name": "mobile, chromium, 2/2",
36+
"browser": "chromium",
37+
"shard": "2/2",
38+
"artifact": "mobile-chromium-2",
39+
"task": "ci:e2e-mobile"
40+
},
41+
{
42+
"group": "mobile",
43+
"name": "mobile, webkit, 1/2",
44+
"browser": "webkit",
45+
"shard": "1/2",
46+
"artifact": "mobile-webkit-1",
47+
"task": "ci:e2e-mobile"
48+
},
49+
{
50+
"group": "mobile",
51+
"name": "mobile, webkit, 2/2",
52+
"browser": "webkit",
53+
"shard": "2/2",
54+
"artifact": "mobile-webkit-2",
55+
"task": "ci:e2e-mobile"
56+
},
57+
{
58+
"group": "project-files",
59+
"name": "project-files",
60+
"browser": "",
61+
"shard": "",
62+
"artifact": "project-files",
63+
"task": "ci:e2e-project-files"
64+
}
65+
]
66+
}

‎.github/scripts/ci-changes.sh‎

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,6 @@
11
#!/usr/bin/env bash
2-
# Prints which checks a pull request's changes can affect, as `<flag>=true|false` lines for
3-
# $GITHUB_OUTPUT. A flag is `false` only when every changed file is on an allowlist whose jobs
4-
# fully cover it. A file on no allowlist sets every flag but `desktop_live` (which keeps its own
5-
# skip list) to `true`; a change under .github/, an empty diff, or any failure to work out the
6-
# diff sets all of them.
7-
#
8-
# full integration, e2e, e2e-build, e2e-mobile: any file not allowlisted below
9-
# test the Vitest shards and test-types: content, docs app, and desktop changes,
10-
# plus `full`
11-
# build the app's `next build`: content changes, plus `full`
12-
# desktop_live the live Electron suite: any file outside its own skip list
13-
#
14-
# lint always runs and is not gated.
15-
#
16-
# Usage: ci-changes.sh <base-sha>
17-
set -u
18-
19-
# Posts, author profiles, and the public assets only posts reference. The test shards read them
20-
# (lib/content/og-image.test.ts) and `next build` prerenders every post with its MDX components,
21-
# which check:library-content compiles but never renders. Components under content/ are code.
22-
content='^apps/sim/content/([^/]+/)*[^/]+\.mdx?$|^apps/sim/content/authors/[^/]+\.json$|^apps/sim/public/(authors|blog|changelog|library)/'
23-
# Its own workspace tests, its type-check and docs-manifest audit in lint, and apps/sim tests
24-
# that read its pages (tools/netsuite/netsuite.test.ts).
25-
docs='^apps/docs/'
26-
# Nothing outside the Electron app imports it; the desktop bridge and protocol packages are
27-
# consumed by apps/sim and stay unlisted.
28-
desktop='^apps/desktop/'
29-
# Agent guidance, read only by the lint job's audits.
30-
guidance='^(\.agents|\.claude|\.cursor)/|^([^/]+/)*(AGENTS|CLAUDE)\.md$|^README\.md$'
31-
# The live desktop suite drives apps/desktop against the app, so it skips more than the rest.
32-
desktop_live_skip="$guidance"'|^(apps/docs/|apps/pii/|apps/sim/content/|apps/sim/public/(authors|blog|changelog|library)/|packages/(python-sdk|ts-sdk)/)|\.mdx?$|(^|/)LICENSE$'
33-
34-
emit() {
35-
printf 'full=%s\ntest=%s\nbuild=%s\ndesktop_live=%s\n' "$1" "$2" "$3" "$4"
36-
}
37-
run_all() {
38-
emit true true true true
39-
echo "Running every check: $1" >&2
40-
exit 0
41-
}
42-
matches() { printf '%s\n' "$names" | grep -qE "$1"; }
43-
outside() { printf '%s\n' "$names" | grep -qvE "$1"; }
44-
45-
base=${1:-}
46-
[ -n "$base" ] || run_all 'no base commit'
47-
git fetch --quiet --depth=1 origin "$base" || run_all "could not fetch $base"
48-
names=$(git diff --no-renames --name-only "$base" HEAD) || run_all "could not diff against $base"
49-
[ -n "$names" ] || run_all 'no changed files listed'
50-
if matches '^\.github/'; then run_all 'a CI file changed'; fi
51-
52-
desktop_live=false
53-
if outside "$desktop_live_skip"; then desktop_live=true; fi
54-
if outside "$content|$docs|$desktop|$guidance"; then
55-
emit true true true "$desktop_live"
56-
unlisted=$(printf '%s\n' "$names" | grep -vE "$content|$docs|$desktop|$guidance" | head -n 1)
57-
echo "Running every check (desktop_live=$desktop_live): $unlisted is not on an allowlist" >&2
58-
exit 0
59-
fi
60-
61-
test=false build=false
62-
if matches "$content|$docs|$desktop"; then test=true; fi
63-
if matches "$content"; then build=true; fi
64-
emit false "$test" "$build" "$desktop_live"
65-
echo "Scoped checks: test=$test build=$build desktop_live=$desktop_live" >&2
2+
# Select tasks from the committed Turborepo graph; unknown files or failed attribution run all.
3+
# Usage: ci-changes.sh [base-sha]. No base runs the complete push/dispatch gate.
4+
set -euo pipefail
5+
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
6+
exec bun "$script_dir/../../scripts/ci-plan.ts" "${1:-}"

‎.github/scripts/ci-gate.sh‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
#
66
# A skip passes only for a gated job whose `changes` flag is exactly `false`:
77
#
8-
# integration, e2e, e2e-build, e2e-production full
9-
# test, test-types test
10-
# build build
11-
# desktop-live desktop_live
8+
# Each job maps to its own explicit selection flag below. No inferred skip is accepted.
129
#
1310
# Every other skip fails: an ungated job (lint, `changes` itself), a gated job whose flag is
1411
# `true`, missing, or anything else (a failed or skipped `changes` leaves no outputs). A failure
@@ -24,13 +21,21 @@ if ! jq -e 'type == "object"' <<< "$needs" > /dev/null 2>&1; then
2421
fi
2522

2623
failed="$(jq -r '
27-
{"integration": "full", "e2e": "full", "e2e-build": "full", "e2e-production": "full", "test": "test",
28-
"test-types": "test", "build": "build", "desktop-live": "desktop_live"} as $gate
24+
{"integration": "integration", "e2e": "http", "e2e-build": "e2e_build", "e2e-production": "production", "test": "test",
25+
"test-types": "test_app", "build": "build", "desktop-live": "desktop_live"} as $gate
26+
| . as $needs
27+
| (($gate | keys) + ["changes", "lint"]) as $required
28+
| ($required - keys) as $missing
29+
| if ($missing | length) > 0 then
30+
$missing[] | "\(.): missing"
31+
else
32+
$needs
2933
| (.changes.outputs // {}) as $flags
3034
| to_entries[]
3135
| select(.value.result != "success")
3236
| select(.value.result != "skipped" or $gate[.key] == null or $flags[$gate[.key]] != "false")
3337
| "\(.key): \(.value.result)"
38+
end
3439
' <<< "$needs")"
3540
if [ -n "$failed" ]; then
3641
echo "::error::Checks did not pass:"

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
query($base: String!, $head: String!) {
2+
packages { items { name path } }
3+
affectedTasks(base: $base, head: $head, tasks: [
4+
"test", "build", "ci:integration", "ci:desktop-live",
5+
"ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox",
6+
"ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
7+
]) {
8+
items {
9+
name
10+
fullName
11+
package { name }
12+
reason {
13+
__typename
14+
... on TaskFileChanged { filePath }
15+
... on TaskGlobalFileChanged { filePath }
16+
... on TaskGlobalDepsChanged { filePath }
17+
... on TaskDependencyTaskChanged { taskName packageName }
18+
... on TaskPackageDependencyChanged { packageName }
19+
... on TaskAllChanged { description }
20+
}
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)