Skip to content

Commit fd055f0

Browse files
authored
[eas-build-job] Allow undefined env values (#3900)
## Summary Widens the shared build `Env` type to allow `undefined` values, matching Node/process env semantics where undefined entries can represent absent values. ## Details - Changes `Env` to `Record<string, string | undefined>` in `@expo/eas-build-job`. - Allows `ctx.reportError` extras to contain undefined values for diagnostic metadata. - Updates strict string boundaries to explicitly require values where needed, such as GraphQL IDs and static context API URLs. - Filters undefined values out of `displayRuntimeInfo` string-only display maps. ## Validation CI should pass.
1 parent 785d690 commit fd055f0

20 files changed

Lines changed: 136 additions & 42 deletions

File tree

packages/build-tools/src/common/projectSources.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ async function uploadProjectMetadataAsync(
233233
}
234234
`),
235235
{
236-
buildId: ctx.env.EAS_BUILD_ID,
236+
buildId: nullthrows(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set'),
237237
projectMetadataFile: {
238238
type: 'GCS',
239239
bucketKey: uploadSession.bucketKey,

packages/build-tools/src/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface BuildContextOptions {
6363
reportError?: (
6464
msg: string,
6565
err?: Error,
66-
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
66+
options?: { tags?: Record<string, string>; extras?: Record<string, string | undefined> }
6767
) => void;
6868
skipNativeBuild?: boolean;
6969
metadata?: Metadata;
@@ -80,7 +80,7 @@ export class BuildContext<TJob extends Job = Job> {
8080
public readonly reportError?: (
8181
msg: string,
8282
err?: Error,
83-
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
83+
options?: { tags?: Record<string, string>; extras?: Record<string, string | undefined> }
8484
) => void;
8585
public readonly skipNativeBuild?: boolean;
8686
public readonly expoApiV2BaseUrl?: string;

packages/build-tools/src/ios/fastlane.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export async function runFastlane(
9393
cwd,
9494
}: {
9595
logger?: bunyan;
96-
env?: Record<string, string>;
96+
env?: Env;
9797
cwd?: string;
9898
} = {}
9999
): Promise<SpawnResult> {

packages/build-tools/src/steps/functions/createSubmissionEntity.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { asyncResult } from '@expo/results';
22
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
33

4+
import { Sentry } from '../../sentry';
45
import { retryOnDNSFailure } from '../../utils/retryOnDNSFailure';
56

67
export function createSubmissionEntityFunction(): BuildFunction {
@@ -54,18 +55,42 @@ export function createSubmissionEntityFunction(): BuildFunction {
5455
const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
5556
if (!robotAccessToken) {
5657
stepsCtx.logger.error('Failed to create submission entity: no robot access token found');
58+
Sentry.capture('Failed to create submission entity: missing robot access token');
5759
return;
5860
}
5961

6062
const buildId = inputs.build_id.value;
6163
if (!buildId) {
6264
stepsCtx.logger.error('Failed to create submission entity: no build ID provided');
65+
Sentry.capture('Failed to create submission entity: missing build ID', {
66+
extras: {
67+
buildId,
68+
},
69+
});
6370
return;
6471
}
6572

6673
const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
6774
if (!workflowJobId) {
6875
stepsCtx.logger.error('Failed to create submission entity: no workflow job ID found');
76+
Sentry.capture('Failed to create submission entity: missing workflow job ID', {
77+
extras: {
78+
buildId,
79+
workflowJobId,
80+
},
81+
});
82+
return;
83+
}
84+
85+
const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
86+
if (!expoApiServerURL) {
87+
stepsCtx.logger.error('Failed to create submission entity: no Expo API server URL found');
88+
Sentry.capture('Failed to create submission entity: missing Expo API server URL', {
89+
extras: {
90+
buildId,
91+
workflowJobId,
92+
},
93+
});
6994
return;
7095
}
7196

@@ -83,7 +108,7 @@ export function createSubmissionEntityFunction(): BuildFunction {
83108

84109
try {
85110
const response = await retryOnDNSFailure(fetch)(
86-
new URL('/v2/app-store-submissions/', stepsCtx.global.staticContext.expoApiServerURL),
111+
new URL('/v2/app-store-submissions/', expoApiServerURL),
87112
{
88113
method: 'POST',
89114
headers: {

packages/build-tools/src/steps/functions/downloadArtifact.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UserError } from '@expo/eas-build-job';
1+
import { SystemError } from '@expo/eas-build-job';
22
import { bunyan } from '@expo/logger';
33
import { asyncResult } from '@expo/results';
44
import {
@@ -56,18 +56,23 @@ export function createDownloadArtifactFunction(): BuildFunction {
5656
const interpolationContext = stepsCtx.global.getInterpolationContext();
5757

5858
if (!('workflow' in interpolationContext)) {
59-
throw new UserError(
60-
'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
61-
'No workflow found in the interpolation context.'
62-
);
59+
throw new SystemError('No workflow found in the interpolation context.', {
60+
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
61+
});
6362
}
6463

6564
const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
6665
if (!robotAccessToken) {
67-
throw new UserError(
68-
'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
69-
'No robot access token found in the job secrets.'
70-
);
66+
throw new SystemError('No robot access token found in the job secrets.', {
67+
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
68+
});
69+
}
70+
71+
const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
72+
if (!expoApiServerURL) {
73+
throw new SystemError('Missing Expo API server URL.', {
74+
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_EXPO_API_SERVER_URL',
75+
});
7176
}
7277

7378
const workflowRunId = interpolationContext.workflow.id;
@@ -82,7 +87,7 @@ export function createDownloadArtifactFunction(): BuildFunction {
8287
const { artifactPath } = await downloadArtifactAsync({
8388
logger,
8489
workflowRunId,
85-
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
90+
expoApiServerURL,
8691
robotAccessToken,
8792
params,
8893
});

packages/build-tools/src/steps/functions/restoreCache.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export function createRestoreCacheFunction(): BuildFunction {
6969
.filter(key => key !== '');
7070

7171
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
72+
const expoApiServerURL = nullthrows(
73+
stepsCtx.global.staticContext.expoApiServerURL,
74+
'expoApiServerURL is not set'
75+
);
7276
const robotAccessToken = nullthrows(
7377
stepsCtx.global.staticContext.job.secrets?.robotAccessToken,
7478
'robotAccessToken is not set'
@@ -77,7 +81,7 @@ export function createRestoreCacheFunction(): BuildFunction {
7781
const { archivePath, matchedKey } = await downloadCacheAsync({
7882
logger,
7983
jobId,
80-
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
84+
expoApiServerURL,
8185
robotAccessToken,
8286
paths,
8387
key,

packages/build-tools/src/steps/functions/saveCache.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export function createSaveCacheFunction(): BuildFunction {
4343
.filter(path => path.length > 0);
4444
const key = z.string().parse(inputs.key.value);
4545
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
46+
const expoApiServerURL = nullthrows(
47+
stepsCtx.global.staticContext.expoApiServerURL,
48+
'expoApiServerURL is not set'
49+
);
4650
const robotAccessToken = nullthrows(
4751
stepsCtx.global.staticContext.job.secrets?.robotAccessToken,
4852
'robotAccessToken is not set'
@@ -61,7 +65,7 @@ export function createSaveCacheFunction(): BuildFunction {
6165
await uploadPublicCacheAsync({
6266
logger,
6367
jobId,
64-
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
68+
expoApiServerURL,
6569
robotAccessToken,
6670
archivePath,
6771
key,
@@ -73,7 +77,7 @@ export function createSaveCacheFunction(): BuildFunction {
7377
await uploadCacheAsync({
7478
logger,
7579
jobId,
76-
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
80+
expoApiServerURL,
7781
robotAccessToken,
7882
archivePath,
7983
key,

packages/build-tools/src/utils/__tests__/expoUpdates.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,33 @@ describe(expoUpdates.configureExpoUpdatesIfInstalledAsync, () => {
104104
expect(iosSetChannelNativelyAsync).toBeCalledTimes(1);
105105
expect(getExpoUpdatesPackageVersionIfInstalledAsync).toBeCalledTimes(1);
106106
});
107+
108+
it('does not fail on missing EAS_BUILD_ID when logging fingerprint diffs', async () => {
109+
jest.mocked(getExpoUpdatesPackageVersionIfInstalledAsync).mockResolvedValue('0.18.0');
110+
111+
const warn = jest.fn();
112+
const managedCtx: BuildContext<BuildJob> = {
113+
appConfig: {},
114+
env: {},
115+
job: {
116+
platform: Platform.IOS,
117+
},
118+
logger: { warn },
119+
metadata: {
120+
runtimeVersion: 'runtime-version-from-local-machine',
121+
},
122+
getReactNativeProjectDirectory: () => '/app',
123+
} as any;
124+
125+
await expect(
126+
expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx, {
127+
resolvedRuntimeVersion: 'runtime-version-from-eas-build',
128+
resolvedFingerprintSources: [],
129+
})
130+
).rejects.toThrow(
131+
'Runtime version calculated on local machine not equal to runtime version calculated during build.'
132+
);
133+
134+
expect(warn).toHaveBeenCalledWith('Skipping fingerprint diff because EAS_BUILD_ID is not set');
135+
});
107136
});

packages/build-tools/src/utils/expoUpdates.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ async function logDiffFingerprints({
243243
ctx: BuildContext<BuildJob>;
244244
}): Promise<void> {
245245
const { resolvedRuntimeVersion, resolvedFingerprintSources } = resolvedRuntime;
246+
const buildId = ctx.env.EAS_BUILD_ID;
247+
248+
if (!buildId) {
249+
ctx.logger.warn('Skipping fingerprint diff because EAS_BUILD_ID is not set');
250+
return;
251+
}
246252

247253
const fingerprintInfo = await ctx.graphqlClient
248254
.query(
@@ -257,7 +263,7 @@ async function logDiffFingerprints({
257263
}
258264
}
259265
`),
260-
{ id: ctx.env.EAS_BUILD_ID }
266+
{ id: buildId }
261267
)
262268
.toPromise();
263269

packages/eas-build-job/src/__tests__/common.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
import { StaticWorkflowInterpolationContextZ } from '../common';
1+
import { EnvSchema, StaticWorkflowInterpolationContextZ } from '../common';
2+
3+
describe('EnvSchema', () => {
4+
it('accepts explicit undefined values', () => {
5+
const env = {
6+
DEFINED_ENV: 'value',
7+
UNDEFINED_ENV: undefined,
8+
};
9+
10+
const { value, error } = EnvSchema.validate(env);
11+
12+
expect(error).toBeUndefined();
13+
expect(value).toEqual(env);
14+
});
15+
});
216

317
describe('StaticWorkflowInterpolationContextZ', () => {
418
it('accepts app and account context', () => {

0 commit comments

Comments
 (0)