Skip to content

Commit 9ee3737

Browse files
authored
EAS Build Hooks (#5666)
* EAS Build Hooks * Fixes * Code separation * Better code separation * Fixes, yarn fix
1 parent 3541ced commit 9ee3737

7 files changed

Lines changed: 960 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
```
3232
- Add expo constants on event context ([#5748](https://github.com/getsentry/sentry-react-native/pull/5748))
3333
- Capture dynamic route params as span attributes for Expo Router navigations ([#5750](https://github.com/getsentry/sentry-react-native/pull/5750))
34+
- EAS Build Hooks ([#5666](https://github.com/getsentry/sentry-react-native/pull/5666))
35+
- Capture EAS build events in Sentry. Add the following to your `package.json`:
36+
```json
37+
{
38+
"scripts": {
39+
"eas-build-on-complete": "sentry-eas-build-on-complete"
40+
}
41+
}
42+
```
43+
Set `SENTRY_DSN` in your EAS secrets, and optionally `SENTRY_EAS_BUILD_CAPTURE_SUCCESS=true` to also capture successful builds.
44+
3445

3546
### Fixes
3647

packages/core/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
"lint:prettier": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --check \"{src,test,scripts,plugin/src}/**/**.ts\""
4646
},
4747
"bin": {
48+
"sentry-eas-build-on-complete": "scripts/eas-build-hook.js",
49+
"sentry-eas-build-on-error": "scripts/eas-build-hook.js",
50+
"sentry-eas-build-on-success": "scripts/eas-build-hook.js",
4851
"sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js"
4952
},
5053
"keywords": [
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env node
2+
/**
3+
* EAS Build Hook
4+
*
5+
* Unified entry point for all EAS build hooks (on-complete, on-error, on-success).
6+
* The hook name is determined from the bin command name in process.argv[1]
7+
* (e.g. sentry-eas-build-on-error → on-error) or can be passed as a CLI argument.
8+
*
9+
* Required environment variables:
10+
* - SENTRY_DSN: Your Sentry DSN
11+
*
12+
* Optional environment variables:
13+
* - SENTRY_EAS_BUILD_CAPTURE_SUCCESS: Set to 'true' to also capture successful builds
14+
* - SENTRY_EAS_BUILD_TAGS: JSON string of additional tags
15+
* - SENTRY_EAS_BUILD_ERROR_MESSAGE: Custom error message for failed builds
16+
* - SENTRY_EAS_BUILD_SUCCESS_MESSAGE: Custom success message for successful builds
17+
*
18+
* @see https://docs.expo.dev/build-reference/npm-hooks/
19+
* @see https://docs.sentry.io/platforms/react-native/
20+
*/
21+
22+
/* eslint-disable no-console */
23+
24+
const path = require('path');
25+
const fs = require('fs');
26+
27+
// ─── Environment loading ─────────────────────────────────────────────────────
28+
29+
/**
30+
* Merges parsed env vars into process.env without overwriting existing values.
31+
* This preserves EAS secrets and other pre-set environment variables.
32+
* @param {object} parsed - Parsed environment variables from dotenv
33+
*/
34+
function mergeEnvWithoutOverwrite(parsed) {
35+
for (const key of Object.keys(parsed)) {
36+
if (process.env[key] === undefined) {
37+
process.env[key] = parsed[key];
38+
}
39+
}
40+
}
41+
42+
/**
43+
* Loads environment variables from various sources:
44+
* - @expo/env (if available)
45+
* - .env file (via dotenv, if available)
46+
* - .env.sentry-build-plugin file
47+
*
48+
* NOTE: Existing environment variables (like EAS secrets) are NOT overwritten.
49+
*/
50+
function loadEnv() {
51+
// Try @expo/env first
52+
try {
53+
require('@expo/env').load('.');
54+
} catch (_e) {
55+
// Fallback to dotenv if available
56+
try {
57+
const dotenvPath = path.join(process.cwd(), '.env');
58+
if (fs.existsSync(dotenvPath)) {
59+
const dotenvFile = fs.readFileSync(dotenvPath, 'utf-8');
60+
const dotenv = require('dotenv');
61+
mergeEnvWithoutOverwrite(dotenv.parse(dotenvFile));
62+
}
63+
} catch (_e2) {
64+
// No dotenv available, continue with existing env vars
65+
}
66+
}
67+
68+
// Also load .env.sentry-build-plugin if it exists
69+
try {
70+
const sentryEnvPath = path.join(process.cwd(), '.env.sentry-build-plugin');
71+
if (fs.existsSync(sentryEnvPath)) {
72+
const dotenvFile = fs.readFileSync(sentryEnvPath, 'utf-8');
73+
const dotenv = require('dotenv');
74+
mergeEnvWithoutOverwrite(dotenv.parse(dotenvFile));
75+
}
76+
} catch (_e) {
77+
// Continue without .env.sentry-build-plugin
78+
}
79+
}
80+
81+
// ─── Hooks module & options ──────────────────────────────────────────────────
82+
83+
/**
84+
* Loads the EAS build hooks module from the compiled output.
85+
* @returns {object} The hooks module exports
86+
* @throws {Error} If the module cannot be loaded
87+
*/
88+
function loadHooksModule() {
89+
try {
90+
return require('../dist/js/tools/easBuildHooks.js');
91+
} catch (_e) {
92+
console.error('[Sentry] Could not load EAS build hooks module. Make sure @sentry/react-native is properly installed.');
93+
process.exit(1);
94+
}
95+
}
96+
97+
/**
98+
* Parses common options from environment variables.
99+
* @returns {object} Parsed options object
100+
*/
101+
function parseBaseOptions() {
102+
const options = {
103+
dsn: process.env.SENTRY_DSN,
104+
};
105+
106+
// Parse additional tags if provided
107+
if (process.env.SENTRY_EAS_BUILD_TAGS) {
108+
try {
109+
const parsed = JSON.parse(process.env.SENTRY_EAS_BUILD_TAGS);
110+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
111+
options.tags = parsed;
112+
} else {
113+
console.warn('[Sentry] SENTRY_EAS_BUILD_TAGS must be a JSON object (e.g., {"key":"value"}). Ignoring.');
114+
}
115+
} catch (_e) {
116+
console.warn('[Sentry] Could not parse SENTRY_EAS_BUILD_TAGS as JSON. Ignoring.');
117+
}
118+
}
119+
120+
return options;
121+
}
122+
123+
// ─── Hook configuration & execution ─────────────────────────────────────────
124+
125+
/**
126+
* Hook configuration keyed by hook name.
127+
*
128+
* Each entry defines which extra env vars to read and which hooks module
129+
* method to call.
130+
*/
131+
const HOOK_CONFIGS = {
132+
'on-complete': {
133+
envKeys: {
134+
errorMessage: 'SENTRY_EAS_BUILD_ERROR_MESSAGE',
135+
successMessage: 'SENTRY_EAS_BUILD_SUCCESS_MESSAGE',
136+
captureSuccessfulBuilds: 'SENTRY_EAS_BUILD_CAPTURE_SUCCESS',
137+
},
138+
method: 'captureEASBuildComplete',
139+
},
140+
'on-error': {
141+
envKeys: {
142+
errorMessage: 'SENTRY_EAS_BUILD_ERROR_MESSAGE',
143+
},
144+
method: 'captureEASBuildError',
145+
},
146+
'on-success': {
147+
envKeys: {
148+
successMessage: 'SENTRY_EAS_BUILD_SUCCESS_MESSAGE',
149+
captureSuccessfulBuilds: 'SENTRY_EAS_BUILD_CAPTURE_SUCCESS',
150+
},
151+
method: 'captureEASBuildSuccess',
152+
},
153+
};
154+
155+
/**
156+
* Runs an EAS build hook by name.
157+
*
158+
* Loads the environment, resolves hook-specific options from env vars,
159+
* and calls the corresponding hooks module method.
160+
*
161+
* @param {'on-complete' | 'on-error' | 'on-success'} hookName
162+
*/
163+
async function runEASBuildHook(hookName) {
164+
const config = HOOK_CONFIGS[hookName];
165+
if (!config) {
166+
throw new Error(`Unknown EAS build hook: ${hookName}`);
167+
}
168+
169+
loadEnv();
170+
171+
const hooks = loadHooksModule();
172+
const options = parseBaseOptions();
173+
174+
for (const [optionKey, envKey] of Object.entries(config.envKeys)) {
175+
if (optionKey === 'captureSuccessfulBuilds') {
176+
options[optionKey] = process.env[envKey] === 'true';
177+
} else if (process.env[envKey] !== undefined) {
178+
options[optionKey] = process.env[envKey];
179+
}
180+
}
181+
182+
try {
183+
await hooks[config.method](options);
184+
console.log(`[Sentry] EAS build ${hookName} hook completed.`);
185+
} catch (error) {
186+
console.error(`[Sentry] Error in eas-build-${hookName} hook:`, error);
187+
// Don't fail the build hook itself
188+
}
189+
}
190+
191+
// ─── Hook name resolution & entry point ─────────────────────────────────────
192+
193+
const HOOK_NAME_RE = /(?:sentry-eas-build-|build-)(on-(?:complete|error|success))/;
194+
195+
/**
196+
* Resolves which hook to run.
197+
*
198+
* 1. Explicit CLI argument: `node build-hook.js on-error`
199+
* 2. Derived from the script path in process.argv[1]
200+
*/
201+
function resolveHookName() {
202+
const arg = process.argv[2];
203+
if (arg && /^on-(complete|error|success)$/.test(arg)) {
204+
return arg;
205+
}
206+
207+
const caller = path.basename(process.argv[1] || '', '.js');
208+
const match = caller.match(HOOK_NAME_RE);
209+
if (match) {
210+
return match[1];
211+
}
212+
213+
console.error(
214+
'[Sentry] Could not determine EAS build hook name. ' +
215+
'Pass one of: on-complete, on-error, on-success',
216+
);
217+
process.exit(1);
218+
}
219+
220+
const hookName = resolveHookName();
221+
222+
runEASBuildHook(hookName).catch(error => {
223+
console.error(`[Sentry] Unexpected error in eas-build-${hookName} hook:`, error);
224+
process.exit(1);
225+
});

0 commit comments

Comments
 (0)