Skip to content

Commit 3bb1722

Browse files
mydeaclaude
andauthored
chore(ci): Streamline CI setup to split bundle, layer, tarball generation (#20396)
## Summary Restructures the CI Build & Test pipeline to split the monolithic build step into parallel jobs and eliminate redundant work, reducing the critical path for test execution. ### Key changes **Split build into parallel jobs:** - The main `Build` job now only runs `build:transpile`, `build:types`, and `build:extension` (via `yarn build:ci`) - **Bundle builds** (`job_build_bundles`) run in a separate parallel job after `Build` — only when browser integration tests are affected - **Lambda layer build** (`job_build_layer`) runs in its own job — only when `@sentry/aws-serverless` or related E2E tests are affected - **Tarball packaging** (`job_build_tarballs`) is a dedicated job that replaces the old `job_e2e_prepare` step **Smarter build artifact handling:** - Replaced the hand-maintained `CACHED_BUILD_PATHS` list with a dynamic Nx-derived artifact path computation (`scripts/ci-print-build-artifact-paths.mjs`) that reads the Nx project graph to determine exactly which output directories to upload - Removed the tarball cache (`BUILD_CACHE_TARBALL_KEY`) — tarballs are now built once and passed as artifacts, not cached across runs - Each build stage uploads its own artifact (`build-output`, `build-bundle-output`, `build-layer-output`), so downstream jobs only download what they need **E2E test matrix generation moved into Build:** - The E2E test matrix (`ci:build-matrix`) is now computed inside the `Build` job instead of a separate `job_e2e_prepare` job - This removes `job_e2e_prepare` as a separate step and shaves off the sequential dependency **NX cache improvements:** - Moved NX cache directory from `.nxcache` to `.nx/cache` (includes `workspace-data` for better cache hits) - Changed from `actions/cache` (save+restore) to `actions/cache/save` in Build + `actions/cache/restore` in downstream jobs - Cache is now scoped per-PR (`${{ github.head_ref }}-${{ github.run_id }}`) for more predictable behavior **Other cleanups:** - `getTestMatrix.ts` → `getTestMatrix.mjs` — removed the `ts-node`, `glob`, and `yaml` dependencies by rewriting as plain ESM with `node:fs` and `node:util` - Fixed 1aws-serverless1 to include the lambda extension in the transpile step, to ensure it is properly cached and restored, and as we depend on this for the tarball as well. - Fixed `angular` dependency resolution for CI builds - Fixes `nestjs` types output for proper caching ### Before → After pipeline structure ``` BEFORE: Metadata → Build (transpile+types+bundles+layer+tarballs) → All tests AFTER: Metadata → Build (transpile+types only) ├── Build bundles ──→ Browser Playwright tests ├── Build layer ───→ (only when needed) ├── Build tarballs → E2E tests └── Unit/Integration/Remix tests (no extra build needed) ``` Somehow, cache restoration with nx seems hit or miss, it works sometimes but not other times. I opened an issue here: nrwl/nx#35403 - fixing this would improve this by a couple minutes, so would be nice to get there... --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5439cc5 commit 3bb1722

12 files changed

Lines changed: 418 additions & 197 deletions

File tree

.github/workflows/build.yml

Lines changed: 221 additions & 106 deletions
Large diffs are not rendered by default.

.github/workflows/flaky-test-detector.yml

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ on:
1010
env:
1111
HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }}
1212

13-
NX_CACHE_RESTORE_KEYS: |
14-
nx-Linux-${{ github.ref }}-${{ github.event.inputs.commit || github.sha }}
15-
nx-Linux-${{ github.ref }}
16-
nx-Linux
17-
1813
# Cancel in progress workflows on pull_requests.
1914
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
2015
concurrency:
@@ -36,16 +31,10 @@ jobs:
3631
with:
3732
node-version-file: 'package.json'
3833
cache: 'yarn'
34+
3935
- name: Install dependencies
4036
run: yarn install --ignore-engines --frozen-lockfile
4137

42-
- name: NX cache
43-
uses: actions/cache/restore@v5
44-
with:
45-
path: .nxcache
46-
key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }}
47-
restore-keys: ${{ env.NX_CACHE_RESTORE_KEYS }}
48-
4938
- name: Build packages
5039
run: yarn build
5140

dev-packages/e2e-tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ try {
198198
```
199199

200200
Test apps in the folder `test-applications` will be automatically picked up by CI in the job `job_e2e_tests` (in `.github/workflows/build.yml`).
201-
The test matrix for CI is generated in `dev-packages/e2e-tests/lib/getTestMatrix.ts`.
201+
The test matrix for CI is generated in `dev-packages/e2e-tests/lib/getTestMatrix.mjs`.
202202

203203
For each test app, CI checks its dependencies (and devDependencies) to see if any of them have changed in the current PR (based on nx affected projects).
204204
For example, if something is changed in the browser package, only E2E test apps that depend on browser will run, while others will be skipped.

dev-packages/e2e-tests/lib/getTestMatrix.ts renamed to dev-packages/e2e-tests/lib/getTestMatrix.mjs

Lines changed: 31 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,21 @@
1-
import { execSync } from 'child_process';
2-
import * as fs from 'fs';
3-
import { sync as globSync } from 'glob';
4-
import * as path from 'path';
5-
import { dirname } from 'path';
6-
import { parseArgs } from 'util';
7-
8-
interface MatrixInclude {
9-
/** The test application (directory) name. */
10-
'test-application': string;
11-
/** Optional override for the build command to run. */
12-
'build-command'?: string;
13-
/** Optional override for the assert command to run. */
14-
'assert-command'?: string;
15-
/** Optional label for the test run. If not set, defaults to value of `test-application`. */
16-
label?: string;
17-
}
1+
import { execSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { parseArgs } from 'node:util';
186

19-
interface PackageJsonSentryTestConfig {
20-
/** If this is true, the test app is optional. */
21-
optional?: boolean;
22-
/** Variant configs that should be run in non-optional test runs. */
23-
variants?: Partial<MatrixInclude>[];
24-
/** Variant configs that should be run in optional test runs. */
25-
optionalVariants?: Partial<MatrixInclude>[];
26-
/** Skip this test app for matrix generation. */
27-
skip?: boolean;
28-
}
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
298

309
/**
31-
* This methods generates a matrix for the GitHub Actions workflow to run the E2E tests.
32-
* It checks which test applications are affected by the current changes in the PR and then generates a matrix
10+
* Generates a matrix for the GitHub Actions workflow to run the E2E tests.
11+
* Checks which test applications are affected by the current changes in the PR and then generates a matrix
3312
* including all test apps that have at least one dependency that was changed in the PR.
3413
* If no `--base=xxx` is provided, it will output all test applications.
3514
*
3615
* If `--optional=true` is set, it will generate a matrix of optional test applications only.
3716
* Otherwise, these will be skipped.
3817
*/
39-
function run(): void {
18+
function run() {
4019
const { values } = parseArgs({
4120
args: process.argv.slice(2),
4221
options: {
@@ -52,9 +31,7 @@ function run(): void {
5231
// eslint-disable-next-line no-console
5332
console.error(`Parsed command line arguments: base=${base}, head=${head}, optional=${optional}`);
5433

55-
const testApplications = globSync('*/package.json', {
56-
cwd: `${__dirname}/../test-applications`,
57-
}).map(filePath => dirname(filePath));
34+
const testApplications = discoverTestApplicationDirs();
5835

5936
// For GitHub Action debugging (using stderr the 'matrix=...' output is not polluted)
6037
// eslint-disable-next-line no-console
@@ -67,7 +44,7 @@ function run(): void {
6744
: testApplications;
6845

6946
const optionalMode = optional === 'true';
70-
const includes: MatrixInclude[] = [];
47+
const includes = [];
7148

7249
includedTestApplications.forEach(testApp => {
7350
addIncludesForTestApp(testApp, includes, { optionalMode });
@@ -78,11 +55,18 @@ function run(): void {
7855
console.log(`matrix=${JSON.stringify({ include: includes })}`);
7956
}
8057

81-
function addIncludesForTestApp(
82-
testApp: string,
83-
includes: MatrixInclude[],
84-
{ optionalMode }: { optionalMode: boolean },
85-
): void {
58+
/** Direct children of `test-applications/` that contain a `package.json` (replaces glob one-segment + package.json). */
59+
function discoverTestApplicationDirs() {
60+
const appsRoot = path.join(__dirname, '..', 'test-applications');
61+
return fs
62+
.readdirSync(appsRoot, { withFileTypes: true })
63+
.filter(entry => entry.isDirectory())
64+
.map(entry => entry.name)
65+
.filter(name => fs.existsSync(path.join(appsRoot, name, 'package.json')))
66+
.sort();
67+
}
68+
69+
function addIncludesForTestApp(testApp, includes, { optionalMode }) {
8670
const packageJson = getPackageJson(testApp);
8771

8872
const shouldSkip = packageJson.sentryTest?.skip || false;
@@ -108,7 +92,7 @@ function addIncludesForTestApp(
10892
});
10993
}
11094

111-
function getSentryDependencies(appName: string): string[] {
95+
function getSentryDependencies(appName) {
11296
const packageJson = getPackageJson(appName);
11397

11498
const dependencies = {
@@ -119,11 +103,7 @@ function getSentryDependencies(appName: string): string[] {
119103
return Object.keys(dependencies).filter(key => key.startsWith('@sentry'));
120104
}
121105

122-
function getPackageJson(appName: string): {
123-
dependencies?: { [key: string]: string };
124-
devDependencies?: { [key: string]: string };
125-
sentryTest?: PackageJsonSentryTestConfig;
126-
} {
106+
function getPackageJson(appName) {
127107
const fullPath = path.resolve(__dirname, '..', 'test-applications', appName, 'package.json');
128108

129109
if (!fs.existsSync(fullPath)) {
@@ -133,19 +113,14 @@ function getPackageJson(appName: string): {
133113
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
134114
}
135115

136-
run();
137-
138-
function getAffectedTestApplications(
139-
testApplications: string[],
140-
{ base = 'develop', head }: { base?: string; head?: string },
141-
): string[] {
116+
function getAffectedTestApplications(testApplications, { base = 'develop', head }) {
142117
const additionalArgs = [`--base=${base}`];
143118

144119
if (head) {
145120
additionalArgs.push(`--head=${head}`);
146121
}
147122

148-
let affectedProjects: string[] = [];
123+
let affectedProjects = [];
149124
try {
150125
affectedProjects = execSync(`yarn --silent nx show projects --affected ${additionalArgs.join(' ')}`)
151126
.toString()
@@ -201,7 +176,7 @@ function getAffectedTestApplications(
201176
return Array.from(testAppsToRun);
202177
}
203178

204-
function getChangedTestApps(base: string, head?: string): false | Set<string> {
179+
function getChangedTestApps(base, head) {
205180
const changedFiles = execSync(`git diff --name-only ${base}${head ? `..${head}` : ''} -- .`, {
206181
encoding: 'utf-8',
207182
})
@@ -214,7 +189,7 @@ function getChangedTestApps(base: string, head?: string): false | Set<string> {
214189
// eslint-disable-next-line no-console
215190
console.error(`Changed files since ${base}${head ? `..${head}` : ''}: ${JSON.stringify(changedFiles)}`);
216191

217-
const changedTestApps: Set<string> = new Set();
192+
const changedTestApps = new Set();
218193
const testAppsPrefix = 'dev-packages/e2e-tests/test-applications/';
219194

220195
for (const file of changedFiles) {
@@ -233,3 +208,5 @@ function getChangedTestApps(base: string, head?: string): false | Set<string> {
233208

234209
return changedTestApps;
235210
}
211+
212+
run();

dev-packages/e2e-tests/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"test:prepare": "ts-node prepare.ts",
1313
"test:validate": "ts-node validate-packed-tarball-setup.ts",
1414
"clean": "rimraf tmp node_modules packed && yarn clean:test-applications && yarn clean:pnpm",
15-
"ci:build-matrix": "ts-node ./lib/getTestMatrix.ts",
16-
"ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true",
15+
"ci:build-matrix": "node ./lib/getTestMatrix.mjs",
16+
"ci:build-matrix-optional": "node ./lib/getTestMatrix.mjs --optional=true",
1717
"ci:copy-to-temp": "ts-node ./ciCopyToTemp.ts",
1818
"ci:pnpm-overrides": "ts-node ./ciPnpmOverrides.ts",
1919
"clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular,event-dumps}",

nx.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
}
6262
},
6363
"$schema": "./node_modules/nx/schemas/nx-schema.json",
64-
"cacheDirectory": ".nxcache",
64+
"cacheDirectory": ".nx/cache",
6565
"tui": {
6666
"autoExit": true
6767
},

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{
22
"private": true,
33
"scripts": {
4-
"build": "node ./scripts/verify-packages-versions.js && nx run-many -t build:transpile build:types build:bundle build:layer",
4+
"build": "node ./scripts/verify-packages-versions.js && nx run-many -t build:transpile build:types build:bundle",
5+
"build:ci": "node ./scripts/verify-packages-versions.js && nx run-many -t build:transpile build:types",
56
"build:bundle": "nx run-many -t build:bundle",
7+
"build:layer": "nx run-many -t build:layer",
68
"build:dev": "nx run-many -t build:types build:transpile",
79
"build:dev:filter": "nx run-many -t build:dev -p",
810
"build:transpile": "nx run-many -t build:transpile",
@@ -11,6 +13,7 @@
1113
"build:dev:watch": "nx run-many -t build:dev:watch",
1214
"build:tarball": "run-s clean:tarballs build:tarballs",
1315
"build:tarballs": "nx run-many -t build:tarball",
16+
"ci:print-build-artifact-paths": "node ./scripts/ci-print-build-artifact-paths.mjs",
1417
"changelog": "ts-node ./scripts/get-commit-list.ts",
1518
"generate-changelog": "ts-node ./scripts/generate-changelog.ts",
1619
"circularDepCheck": "nx run-many -t circularDepCheck",

packages/angular/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,7 @@
7171
"^build:types"
7272
],
7373
"outputs": [
74-
"{projectRoot}/build/esm2015",
75-
"{projectRoot}/build/fesm2015",
76-
"{projectRoot}/build/fesm2020",
77-
"{projectRoot}/build/*.d.ts"
74+
"{projectRoot}/build"
7875
]
7976
}
8077
}

packages/aws-serverless/package.json

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@
8080
"@vercel/nft": "^1.3.0"
8181
},
8282
"scripts": {
83-
"build": "run-p build:transpile build:types build:extension && run-s build:layer",
83+
"build": "run-p build:transpile build:types",
8484
"build:extension": "rollup -c rollup.lambda-extension.config.mjs && yarn ts-node scripts/buildLambdaExtension.ts",
8585
"build:layer": "rimraf build/aws && yarn ts-node scripts/buildLambdaLayer.ts",
8686
"build:dev": "run-p build:transpile build:types",
87-
"build:transpile": "rollup -c rollup.npm.config.mjs",
87+
"build:transpile": "run-s build:transpile:npm build:extension",
88+
"build:transpile:npm": "rollup -c rollup.npm.config.mjs",
8889
"build:types": "run-s build:types:core build:types:downlevel",
8990
"build:types:core": "tsc -p tsconfig.types.json",
9091
"build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
@@ -117,20 +118,10 @@
117118
],
118119
"outputs": [
119120
"{projectRoot}/build/npm/esm",
120-
"{projectRoot}/build/npm/cjs"
121-
]
122-
},
123-
"build:extension": {
124-
"inputs": [
125-
"production",
126-
"^production"
127-
],
128-
"dependsOn": [
129-
"^build:transpile"
130-
],
131-
"outputs": [
121+
"{projectRoot}/build/npm/cjs",
132122
"{projectRoot}/build/lambda-extension"
133-
]
123+
],
124+
"cache": true
134125
},
135126
"build:layer": {
136127
"inputs": [
@@ -139,7 +130,7 @@
139130
],
140131
"dependsOn": [
141132
"build:transpile",
142-
"build:extension"
133+
"build:types"
143134
],
144135
"outputs": [
145136
"{projectRoot}/build/aws"

packages/gatsby/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,24 @@
8484
"volta": {
8585
"extends": "../../package.json"
8686
},
87+
"nx": {
88+
"targets": {
89+
"build:transpile": {
90+
"inputs": [
91+
"production",
92+
"^production"
93+
],
94+
"outputs": [
95+
"{projectRoot}/build/esm",
96+
"{projectRoot}/build/cjs",
97+
"{projectRoot}/*.d.ts"
98+
],
99+
"dependsOn": [
100+
"^build:transpile"
101+
],
102+
"cache": true
103+
}
104+
}
105+
},
87106
"sideEffects": false
88107
}

0 commit comments

Comments
 (0)