@expo/build-tools 21.4.0 → 21.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/android/gradleProfile.js +14 -2
  2. package/dist/builders/custom.js +20 -12
  3. package/dist/common/git.d.ts +4 -0
  4. package/dist/common/git.js +35 -2
  5. package/dist/common/jobHooks.js +10 -0
  6. package/dist/generic.js +15 -6
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +3 -1
  9. package/dist/logging/HttpLogStream.d.ts +28 -0
  10. package/dist/logging/HttpLogStream.js +137 -0
  11. package/dist/steps/compositeFunctions.d.ts +11 -0
  12. package/dist/steps/compositeFunctions.js +62 -0
  13. package/dist/steps/easFunctions.js +4 -0
  14. package/dist/steps/functions/checkout.js +26 -1
  15. package/dist/steps/functions/collectServeSimMetrics.d.ts +3 -0
  16. package/dist/steps/functions/collectServeSimMetrics.js +41 -0
  17. package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +1 -0
  18. package/dist/steps/functions/startAgentDeviceRemoteSession.js +56 -48
  19. package/dist/steps/functions/startArgentRemoteSession.js +17 -2
  20. package/dist/steps/functions/startServeSimMetrics.d.ts +2 -0
  21. package/dist/steps/functions/startServeSimMetrics.js +17 -0
  22. package/dist/steps/functions/startServeSimRemoteSession.js +19 -14
  23. package/dist/steps/utils/agentDeviceArtifacts.js +1 -1
  24. package/dist/steps/utils/argentArtifacts.js +1 -1
  25. package/dist/steps/utils/deviceRunSessionEvents.js +68 -10
  26. package/dist/steps/utils/remoteDeviceRunSession.d.ts +43 -5
  27. package/dist/steps/utils/remoteDeviceRunSession.js +249 -37
  28. package/dist/steps/utils/serveSimMetricsArtifacts.d.ts +9 -0
  29. package/dist/steps/utils/serveSimMetricsArtifacts.js +49 -0
  30. package/dist/steps/utils/serveSimMetricsRecorder.d.ts +30 -0
  31. package/dist/steps/utils/serveSimMetricsRecorder.js +232 -0
  32. package/package.json +4 -4
@@ -75,6 +75,15 @@ function formatSeconds(ms) {
75
75
  }
76
76
  return `${s.toFixed(1)}s`;
77
77
  }
78
+ function truncateMiddle(value, width) {
79
+ if (value.length <= width) {
80
+ return value;
81
+ }
82
+ const availableWidth = width - 1;
83
+ const startWidth = Math.ceil(availableWidth / 2);
84
+ const endWidth = Math.floor(availableWidth / 2);
85
+ return `${value.slice(0, startWidth)}…${value.slice(-endWidth)}`;
86
+ }
78
87
  function formatGradleProfileReport(tasks) {
79
88
  // Filter out tasks under 1 second
80
89
  const significantTasks = tasks.filter(t => t.durationMs >= 1000);
@@ -119,7 +128,10 @@ function formatGradleProfileReport(tasks) {
119
128
  // Compute totals from individual tasks only (avoid double-counting)
120
129
  const totalMs = individualTasks.reduce((sum, t) => sum + t.durationMs, 0);
121
130
  const maxMs = totalMs || 1;
122
- const nameWidth = Math.max(4, ...rows.map(r => r.displayName.length)) + 2;
131
+ // Keep the report close to the xclogparser table width. Gradle task names can be very long,
132
+ // so allowing them to grow the first column without a limit makes the table difficult to read.
133
+ const maxNameWidth = 48;
134
+ const nameWidth = Math.min(maxNameWidth, Math.max(4, ...rows.map(row => row.displayName.length)) + 2);
123
135
  const barMaxWidth = 20;
124
136
  const header = '┌─' +
125
137
  '─'.repeat(nameWidth) +
@@ -163,7 +175,7 @@ function formatGradleProfileReport(tasks) {
163
175
  const bar = '█'.repeat(barLength) + '░'.repeat(barMaxWidth - barLength);
164
176
  const result = row.task.result === '(total)' ? 'total' : row.task.result;
165
177
  lines.push('│ ' +
166
- row.displayName.padEnd(nameWidth) +
178
+ truncateMiddle(row.displayName, nameWidth).padEnd(nameWidth) +
167
179
  ' │ ' +
168
180
  formatSeconds(row.task.durationMs).padStart(10) +
169
181
  ' │ ' +
@@ -15,6 +15,7 @@ const projectSources_1 = require("../common/projectSources");
15
15
  const customBuildContext_1 = require("../customBuildContext");
16
16
  const datadog_1 = require("../datadog");
17
17
  const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
18
+ const compositeFunctions_1 = require("../steps/compositeFunctions");
18
19
  const easFunctionGroups_1 = require("../steps/easFunctionGroups");
19
20
  const easFunctions_1 = require("../steps/easFunctions");
20
21
  const retry_1 = require("../utils/retry");
@@ -44,20 +45,27 @@ async function runCustomBuildAsync(ctx) {
44
45
  const globalContext = new steps_1.BuildStepGlobalContext(customBuildCtx, false);
45
46
  const easFunctions = (0, easFunctions_1.getEasFunctions)(customBuildCtx);
46
47
  const easFunctionGroups = (0, easFunctionGroups_1.getEasFunctionGroups)(customBuildCtx);
47
- const parser = ctx.job.steps
48
- ? new steps_1.StepsConfigParser(globalContext, {
49
- externalFunctions: easFunctions,
50
- externalFunctionGroups: easFunctionGroups,
51
- steps: ctx.job.steps,
52
- hooks: ctx.job.hooks,
53
- })
54
- : new steps_1.BuildConfigParser(globalContext, {
55
- externalFunctions: easFunctions,
56
- externalFunctionGroups: easFunctionGroups,
57
- configPath: path_1.default.join(ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory), (0, nullthrows_1.default)(ctx.job.customBuildConfig?.path, 'Steps or custom build config path are required in custom jobs')),
58
- });
59
48
  const workflow = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
60
49
  try {
50
+ const projectRoot = ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory);
51
+ const parser = ctx.job.steps
52
+ ? new steps_1.StepsConfigParser(globalContext, {
53
+ externalFunctions: easFunctions,
54
+ externalFunctionGroups: easFunctionGroups,
55
+ steps: ctx.job.steps,
56
+ hooks: ctx.job.hooks,
57
+ // Eager for job steps (always run), lazy loader for hooks (running anchors only).
58
+ compositeFunctionCatalog: await (0, compositeFunctions_1.buildCompositeFunctionCatalogAsync)(projectRoot, {
59
+ steps: ctx.job.steps,
60
+ logger: ctx.logger,
61
+ }),
62
+ loadCompositeFunction: (0, compositeFunctions_1.createCompositeFunctionLoader)(projectRoot, ctx.logger),
63
+ })
64
+ : new steps_1.BuildConfigParser(globalContext, {
65
+ externalFunctions: easFunctions,
66
+ externalFunctionGroups: easFunctionGroups,
67
+ configPath: path_1.default.join(projectRoot, (0, nullthrows_1.default)(ctx.job.customBuildConfig?.path, 'Steps or custom build config path are required in custom jobs')),
68
+ });
61
69
  return await parser.parseAsync();
62
70
  }
63
71
  catch (parseError) {
@@ -7,3 +7,7 @@ export declare function shallowCloneRepositoryAsync({ logger, archiveSource, des
7
7
  };
8
8
  destinationDirectory: string;
9
9
  }): Promise<void>;
10
+ export declare function fetchAndCheckoutRefAsync({ ref, repositoryDirectory, }: {
11
+ ref: string;
12
+ repositoryDirectory: string;
13
+ }): Promise<void>;
@@ -4,7 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.shallowCloneRepositoryAsync = shallowCloneRepositoryAsync;
7
+ exports.fetchAndCheckoutRefAsync = fetchAndCheckoutRefAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const path_1 = __importDefault(require("path"));
8
12
  async function shallowCloneRepositoryAsync({ logger, archiveSource, destinationDirectory, }) {
9
13
  const { repositoryUrl } = archiveSource;
10
14
  try {
@@ -47,6 +51,35 @@ async function shallowCloneRepositoryAsync({ logger, archiveSource, destinationD
47
51
  throw err;
48
52
  }
49
53
  }
54
+ async function fetchAndCheckoutRefAsync({ ref, repositoryDirectory, }) {
55
+ if (!(await fs_extra_1.default.pathExists(path_1.default.join(repositoryDirectory, '.git')))) {
56
+ throw new eas_build_job_1.UserError('EAS_CHECKOUT_NOT_A_GIT_REPOSITORY', `Cannot check out ref "${ref}": ${repositoryDirectory} is not a Git repository.`);
57
+ }
58
+ const { name, type } = getStrippedBranchOrTagName(ref);
59
+ const isCommitHash = type === 'other' && /^([0-9a-f]{40}|[0-9a-f]{64})$/.test(name);
60
+ const refToFetch = type === 'branch' ? `refs/heads/${name}` : type === 'tag' ? `refs/tags/${name}` : name;
61
+ try {
62
+ await (0, turtle_spawn_1.default)('git', ['fetch', 'origin', '--depth', '1', '--no-tags', refToFetch], {
63
+ cwd: repositoryDirectory,
64
+ });
65
+ if (type === 'tag') {
66
+ await (0, turtle_spawn_1.default)('git', ['checkout', 'FETCH_HEAD'], { cwd: repositoryDirectory });
67
+ // --force because the initial clone may have already created this tag.
68
+ await (0, turtle_spawn_1.default)('git', ['tag', '--force', name], { cwd: repositoryDirectory });
69
+ }
70
+ else if (isCommitHash) {
71
+ await (0, turtle_spawn_1.default)('git', ['checkout', 'FETCH_HEAD'], { cwd: repositoryDirectory });
72
+ }
73
+ else {
74
+ // -B because a branch with this name may exist from the initial clone.
75
+ await (0, turtle_spawn_1.default)('git', ['checkout', '-B', name, 'FETCH_HEAD'], { cwd: repositoryDirectory });
76
+ }
77
+ }
78
+ catch (err) {
79
+ // Git output is not relayed because it may contain the credentialed repository URL.
80
+ throw new eas_build_job_1.UserError('EAS_CHECKOUT_FAILED_TO_CHECKOUT_REF', `Failed to fetch and check out ref "${ref}". Make sure it is a branch, tag, or commit SHA reachable in the source repository.`, { cause: err });
81
+ }
82
+ }
50
83
  function getSanitizedGitUrl(maybeGitUrl) {
51
84
  try {
52
85
  const url = new URL(maybeGitUrl);
@@ -60,7 +93,7 @@ function getSanitizedGitUrl(maybeGitUrl) {
60
93
  }
61
94
  }
62
95
  function getStrippedBranchOrTagName(ref) {
63
- const branchRegex = /(\/?refs)?\/?heads\/(.+)/;
96
+ const branchRegex = /^\/?(refs\/)?heads\/(.+)$/;
64
97
  const branchMatch = ref.match(branchRegex);
65
98
  if (branchMatch) {
66
99
  return {
@@ -68,7 +101,7 @@ function getStrippedBranchOrTagName(ref) {
68
101
  type: 'branch',
69
102
  };
70
103
  }
71
- const tagRegex = /(\/?refs)?\/?tags\/(.+)/;
104
+ const tagRegex = /^\/?(refs\/)?tags\/(.+)$/;
72
105
  const tagMatch = ref.match(tagRegex);
73
106
  if (tagMatch) {
74
107
  return {
@@ -4,6 +4,7 @@ exports.parseJobHooksAsync = parseJobHooksAsync;
4
4
  const eas_build_job_1 = require("@expo/eas-build-job");
5
5
  const steps_1 = require("@expo/steps");
6
6
  const customBuildContext_1 = require("../customBuildContext");
7
+ const compositeFunctions_1 = require("../steps/compositeFunctions");
7
8
  const easFunctionGroups_1 = require("../steps/easFunctionGroups");
8
9
  const easFunctions_1 = require("../steps/easFunctions");
9
10
  /**
@@ -58,6 +59,8 @@ async function parseJobHooksAsync(ctx, wrappedAnchors) {
58
59
  // outputs accumulate across keys.
59
60
  const hookEntriesByKey = {};
60
61
  const orderedSteps = [];
62
+ const compositeFunctionCatalog = {};
63
+ const loadCompositeFunction = (0, compositeFunctions_1.createCompositeFunctionLoader)(ctx.getReactNativeProjectDirectory(), ctx.logger);
61
64
  for (const anchor of wrappedAnchors) {
62
65
  for (const side of ['before', 'after']) {
63
66
  const key = `${side}_${anchor}`;
@@ -67,9 +70,16 @@ async function parseJobHooksAsync(ctx, wrappedAnchors) {
67
70
  }
68
71
  let entries;
69
72
  try {
73
+ // Extended per key so a bad `uses:` path is attributed to that hook key.
74
+ await (0, steps_1.extendCompositeFunctionCatalogFromStepsAsync)({
75
+ catalog: compositeFunctionCatalog,
76
+ rootSteps: steps,
77
+ loadCompositeFunction,
78
+ });
70
79
  entries = await (0, steps_1.constructHookEntriesAsync)(globalContext, steps, {
71
80
  externalFunctions,
72
81
  externalFunctionGroups,
82
+ compositeFunctionCatalog,
73
83
  });
74
84
  }
75
85
  catch (err) {
package/dist/generic.js CHANGED
@@ -11,6 +11,7 @@ const promises_1 = __importDefault(require("fs/promises"));
11
11
  const nullthrows_1 = __importDefault(require("nullthrows"));
12
12
  const projectSources_1 = require("./common/projectSources");
13
13
  const customBuildContext_1 = require("./customBuildContext");
14
+ const compositeFunctions_1 = require("./steps/compositeFunctions");
14
15
  const easFunctionGroups_1 = require("./steps/easFunctionGroups");
15
16
  const easFunctions_1 = require("./steps/easFunctions");
16
17
  const outputs_1 = require("./utils/outputs");
@@ -33,14 +34,22 @@ async function runGenericJobAsync(ctx) {
33
34
  });
34
35
  });
35
36
  const globalContext = new steps_1.BuildStepGlobalContext(customBuildCtx, false);
36
- const parser = new steps_1.StepsConfigParser(globalContext, {
37
- externalFunctions: (0, easFunctions_1.getEasFunctions)(customBuildCtx),
38
- externalFunctionGroups: (0, easFunctionGroups_1.getEasFunctionGroups)(customBuildCtx),
39
- steps: ctx.job.steps,
40
- hooks: ctx.job.hooks,
41
- });
42
37
  const workflow = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
43
38
  try {
39
+ const projectRoot = ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory);
40
+ // Eager for job steps (always run), lazy loader for hooks (running anchors only).
41
+ const compositeFunctionCatalog = await (0, compositeFunctions_1.buildCompositeFunctionCatalogAsync)(projectRoot, {
42
+ steps: ctx.job.steps,
43
+ logger: ctx.logger,
44
+ });
45
+ const parser = new steps_1.StepsConfigParser(globalContext, {
46
+ externalFunctions: (0, easFunctions_1.getEasFunctions)(customBuildCtx),
47
+ externalFunctionGroups: (0, easFunctionGroups_1.getEasFunctionGroups)(customBuildCtx),
48
+ steps: ctx.job.steps,
49
+ hooks: ctx.job.hooks,
50
+ compositeFunctionCatalog,
51
+ loadCompositeFunction: (0, compositeFunctions_1.createCompositeFunctionLoader)(projectRoot, ctx.logger),
52
+ });
44
53
  return await parser.parseAsync();
45
54
  }
46
55
  catch (parseError) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as Builders from './builders';
2
+ import HttpLogStream from './logging/HttpLogStream';
2
3
  import RemoteLoggerStream from './logging/RemoteLoggerStream';
3
- export { Builders, RemoteLoggerStream };
4
+ export { Builders, HttpLogStream, RemoteLoggerStream };
4
5
  export { uploadWithSignedUrl } from './storage/uploadWithSignedUrl';
5
6
  export type { SignedUrl, UploadWithSignedUrlParams } from './storage/uploadWithSignedUrl';
6
7
  export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
package/dist/index.js CHANGED
@@ -39,9 +39,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.Builders = void 0;
42
+ exports.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.HttpLogStream = exports.Builders = void 0;
43
43
  const Builders = __importStar(require("./builders"));
44
44
  exports.Builders = Builders;
45
+ const HttpLogStream_1 = __importDefault(require("./logging/HttpLogStream"));
46
+ exports.HttpLogStream = HttpLogStream_1.default;
45
47
  const RemoteLoggerStream_1 = __importDefault(require("./logging/RemoteLoggerStream"));
46
48
  exports.RemoteLoggerStream = RemoteLoggerStream_1.default;
47
49
  var uploadWithSignedUrl_1 = require("./storage/uploadWithSignedUrl");
@@ -0,0 +1,28 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { Writable } from 'stream';
3
+ /**
4
+ * A bunyan-compatible writable stream for sending logs over HTTP.
5
+ */
6
+ export default class HttpLogStream extends Writable {
7
+ writable: boolean;
8
+ private readonly buffer;
9
+ private readonly url;
10
+ private readonly headers;
11
+ private readonly logger;
12
+ private readonly maxBatchBytes;
13
+ private readonly bufferRetentionMs;
14
+ private inFlightRequest;
15
+ constructor({ url, headers, logger, maxBatchBytes, bufferRetentionMs, }: {
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ logger: bunyan;
19
+ maxBatchBytes?: number;
20
+ bufferRetentionMs?: number | null;
21
+ });
22
+ _write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
23
+ cleanUp(): Promise<void>;
24
+ private flush;
25
+ private takeBatch;
26
+ private trimExpiredBufferedLogs;
27
+ private sendBatch;
28
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const results_1 = require("@expo/results");
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ const stream_1 = require("stream");
9
+ const retry_1 = require("../utils/retry");
10
+ const MAX_BATCH_BYTES = 200_000;
11
+ /**
12
+ * A bunyan-compatible writable stream for sending logs over HTTP.
13
+ */
14
+ class HttpLogStream extends stream_1.Writable {
15
+ writable = true;
16
+ buffer = [];
17
+ url;
18
+ headers;
19
+ logger;
20
+ maxBatchBytes;
21
+ bufferRetentionMs;
22
+ inFlightRequest = null;
23
+ constructor({ url, headers, logger, maxBatchBytes = MAX_BATCH_BYTES, bufferRetentionMs = null, }) {
24
+ super({ objectMode: true });
25
+ this.url = url;
26
+ this.headers = headers;
27
+ this.logger = logger;
28
+ this.maxBatchBytes = maxBatchBytes;
29
+ this.bufferRetentionMs = bufferRetentionMs;
30
+ }
31
+ _write(chunk, _encoding, callback) {
32
+ if (typeof chunk !== 'object' || chunk === null) {
33
+ callback(new Error('Invalid log entry: expected an object'));
34
+ return;
35
+ }
36
+ this.trimExpiredBufferedLogs();
37
+ this.buffer.push({
38
+ enqueuedAt: Date.now(),
39
+ serialized: JSON.stringify(chunk),
40
+ });
41
+ this.flush();
42
+ callback();
43
+ }
44
+ async cleanUp() {
45
+ this.writable = false;
46
+ await this.flush({ isCleanup: true });
47
+ }
48
+ flush({ isCleanup = false } = {}) {
49
+ if (this.inFlightRequest) {
50
+ if (isCleanup) {
51
+ return this.inFlightRequest.then(() => {
52
+ if (this.buffer.length > 0) {
53
+ return this.flush({ isCleanup: true });
54
+ }
55
+ });
56
+ }
57
+ return;
58
+ }
59
+ if (this.buffer.length === 0) {
60
+ return;
61
+ }
62
+ this.trimExpiredBufferedLogs();
63
+ if (this.buffer.length === 0) {
64
+ return;
65
+ }
66
+ const batch = this.takeBatch();
67
+ this.inFlightRequest = this.sendBatch(batch)
68
+ .catch(err => {
69
+ if (!isCleanup) {
70
+ // Keep logs in memory if upload fails so a later flush can retry.
71
+ this.buffer.unshift(...batch);
72
+ this.logger.error({ err }, 'Failed to send logs batch over HTTP');
73
+ }
74
+ })
75
+ .finally(() => {
76
+ this.inFlightRequest = null;
77
+ });
78
+ if (isCleanup) {
79
+ return this.inFlightRequest.then(() => {
80
+ if (this.buffer.length > 0) {
81
+ return this.flush({ isCleanup: true });
82
+ }
83
+ });
84
+ }
85
+ void this.inFlightRequest.then(() => {
86
+ if (this.writable && this.buffer.length > 0) {
87
+ this.flush();
88
+ }
89
+ });
90
+ }
91
+ takeBatch() {
92
+ const batch = [];
93
+ let batchBytes = 0;
94
+ while (this.buffer.length > 0) {
95
+ const nextEntry = this.buffer[0];
96
+ const nextEntryBytes = Buffer.byteLength(nextEntry.serialized) + (batch.length > 0 ? Buffer.byteLength('\n') : 0);
97
+ if (batch.length > 0 && batchBytes + nextEntryBytes > this.maxBatchBytes) {
98
+ break;
99
+ }
100
+ batch.push(this.buffer.shift());
101
+ batchBytes += nextEntryBytes;
102
+ }
103
+ return batch;
104
+ }
105
+ trimExpiredBufferedLogs() {
106
+ if (this.bufferRetentionMs === null) {
107
+ return;
108
+ }
109
+ const cutoff = Date.now() - this.bufferRetentionMs;
110
+ while (this.buffer.length > 0 && this.buffer[0].enqueuedAt < cutoff) {
111
+ this.buffer.shift();
112
+ }
113
+ }
114
+ async sendBatch(logs) {
115
+ await (0, retry_1.retryAsync)(async () => {
116
+ const response = await (0, node_fetch_1.default)(this.url, {
117
+ method: 'POST',
118
+ headers: {
119
+ ...this.headers,
120
+ 'Content-Type': 'application/x-ndjson',
121
+ },
122
+ body: logs.map(log => log.serialized).join('\n'),
123
+ });
124
+ if (!response.ok) {
125
+ const responseText = await (0, results_1.asyncResult)(response.text());
126
+ throw new Error(`Failed to upload logs: status=${response.status} statusText=${response.statusText} body=${responseText.value ?? '<error reading response body>'}`);
127
+ }
128
+ }, {
129
+ retryOptions: {
130
+ retries: 2,
131
+ retryIntervalMs: 1000,
132
+ },
133
+ logger: this.logger,
134
+ });
135
+ }
136
+ }
137
+ exports.default = HttpLogStream;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Loads local function.yml files from the project and builds the catalog consumed by
3
+ * {@link StepsConfigParser}. Keeps filesystem I/O in build-tools; expansion logic lives in @expo/steps.
4
+ */
5
+ import { bunyan } from '@expo/logger';
6
+ import { CompositeFunctionCatalog, CompositeFunctionConfig, Step } from '@expo/eas-build-job';
7
+ export declare function createCompositeFunctionLoader(projectRoot: string, logger?: bunyan): (compositeFunctionPath: string) => Promise<CompositeFunctionConfig>;
8
+ export declare function buildCompositeFunctionCatalogAsync(projectRoot: string, { steps, logger }: {
9
+ steps: readonly Step[];
10
+ logger?: bunyan;
11
+ }): Promise<CompositeFunctionCatalog>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCompositeFunctionLoader = createCompositeFunctionLoader;
7
+ exports.buildCompositeFunctionCatalogAsync = buildCompositeFunctionCatalogAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
+ const steps_1 = require("@expo/steps");
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const yaml_1 = __importDefault(require("yaml"));
13
+ const zod_1 = require("zod");
14
+ async function loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger } = {}) {
15
+ const resolvedPath = (0, steps_1.resolveLocalCompositeFunctionPath)(projectRoot, compositeFunctionPath);
16
+ for (const ext of ['yml', 'yaml']) {
17
+ const absolutePath = path_1.default.join(resolvedPath, `function.${ext}`);
18
+ let rawContents;
19
+ try {
20
+ rawContents = await promises_1.default.readFile(absolutePath, 'utf-8');
21
+ }
22
+ catch (err) {
23
+ if (err?.code === 'ENOENT') {
24
+ continue;
25
+ }
26
+ throw new Error(`Failed to read local composite function "${compositeFunctionPath}" from ${absolutePath}`, {
27
+ cause: err,
28
+ });
29
+ }
30
+ let parsed;
31
+ try {
32
+ parsed = yaml_1.default.parse(rawContents);
33
+ }
34
+ catch (err) {
35
+ throw new Error(`Failed to parse local composite function "${compositeFunctionPath}" YAML at ${absolutePath}`, {
36
+ cause: err,
37
+ });
38
+ }
39
+ let config;
40
+ try {
41
+ config = eas_build_job_1.CompositeFunctionConfigZ.parse(parsed);
42
+ }
43
+ catch (err) {
44
+ if (err instanceof zod_1.ZodError) {
45
+ throw new Error(`Invalid composite function "${compositeFunctionPath}": ${zod_1.z.prettifyError(err)}`);
46
+ }
47
+ throw err;
48
+ }
49
+ logger?.debug(`Loaded local composite function "${compositeFunctionPath}" from ${path_1.default.relative(projectRoot, absolutePath)}`);
50
+ return config;
51
+ }
52
+ throw new Error(`Local composite function "${compositeFunctionPath}" was referenced by a step but no such composite function exists. A local composite function is resolved from a "function.yml" (or "function.yaml") file at the referenced path relative to the EAS project root (e.g. "uses: ${compositeFunctionPath}" resolves "${compositeFunctionPath}/function.yml"). The recommended convention is to keep composite functions under ".eas/functions/<name>".`);
53
+ }
54
+ function createCompositeFunctionLoader(projectRoot, logger) {
55
+ return compositeFunctionPath => loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger });
56
+ }
57
+ async function buildCompositeFunctionCatalogAsync(projectRoot, { steps, logger }) {
58
+ return (0, steps_1.buildCompositeFunctionCatalogFromStepsAsync)({
59
+ rootSteps: steps,
60
+ loadCompositeFunction: createCompositeFunctionLoader(projectRoot, logger),
61
+ });
62
+ }
@@ -47,6 +47,8 @@ const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
47
47
  const startIosSimulator_1 = require("./functions/startIosSimulator");
48
48
  const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorRecordings");
49
49
  const startServeSimRemoteSession_1 = require("./functions/startServeSimRemoteSession");
50
+ const startServeSimMetrics_1 = require("./functions/startServeSimMetrics");
51
+ const collectServeSimMetrics_1 = require("./functions/collectServeSimMetrics");
50
52
  const uploadArtifact_1 = require("./functions/uploadArtifact");
51
53
  const uploadDeviceRunSessionScreenRecordings_1 = require("./functions/uploadDeviceRunSessionScreenRecordings");
52
54
  const uploadToAsc_1 = require("./functions/uploadToAsc");
@@ -95,6 +97,8 @@ function getEasFunctions(ctx) {
95
97
  (0, finishIosSimulatorRecordings_1.createFinishIosSimulatorRecordingsBuildFunction)(),
96
98
  (0, uploadDeviceRunSessionScreenRecordings_1.createUploadDeviceRunSessionScreenRecordingsBuildFunction)(ctx),
97
99
  (0, startServeSimRemoteSession_1.createStartServeSimRemoteSessionBuildFunction)(ctx),
100
+ (0, startServeSimMetrics_1.createStartServeSimMetricsBuildFunction)(),
101
+ (0, collectServeSimMetrics_1.createCollectServeSimMetricsBuildFunction)(ctx),
98
102
  (0, installMaestro_1.createInstallMaestroBuildFunction)(),
99
103
  (0, installPods_1.createInstallPodsBuildFunction)(),
100
104
  (0, sendSlackMessage_1.createSendSlackMessageFunction)(),
@@ -4,8 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createCheckoutBuildFunction = createCheckoutBuildFunction;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
8
  const steps_1 = require("@expo/steps");
8
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const zod_1 = require("zod");
11
+ const git_1 = require("../../common/git");
9
12
  function createCheckoutBuildFunction() {
10
13
  return new steps_1.BuildFunction({
11
14
  namespace: 'eas',
@@ -13,15 +16,37 @@ function createCheckoutBuildFunction() {
13
16
  name: 'Checkout',
14
17
  __metricsId: 'eas/checkout',
15
18
  __hookId: 'checkout',
16
- fn: async (stepsCtx) => {
19
+ inputProviders: [
20
+ steps_1.BuildStepInput.createProvider({
21
+ id: 'ref',
22
+ required: false,
23
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
24
+ }),
25
+ ],
26
+ fn: async (stepsCtx, { inputs }) => {
27
+ const ref = zod_1.z.string().min(1).optional().parse(inputs.ref.value);
17
28
  if (stepsCtx.global.wasCheckedOut()) {
29
+ if (ref) {
30
+ throw new eas_build_job_1.UserError('EAS_CHECKOUT_REF_AFTER_CHECKOUT', `Project directory is already checked out, so the "ref" input (${ref}) would have no effect. Move this eas/checkout step before any other step that checks out the project (e.g. before eas/build).`);
31
+ }
18
32
  stepsCtx.logger.info('Project directory is already checked out');
19
33
  return;
20
34
  }
35
+ const archiveType = stepsCtx.global.staticContext.job.projectArchive.type;
36
+ if (ref && archiveType !== eas_build_job_1.ArchiveSourceType.GIT) {
37
+ throw new eas_build_job_1.UserError('EAS_CHECKOUT_REF_REQUIRES_GIT_SOURCES', `The "ref" input requires project sources to come from a git repository, e.g. a job triggered through the GitHub integration. It is not supported for local builds or uploaded project tarballs (this job's project archive type is "${archiveType}").`);
38
+ }
21
39
  stepsCtx.logger.info('Checking out project directory');
22
40
  await fs_extra_1.default.move(stepsCtx.global.projectSourceDirectory, stepsCtx.global.projectTargetDirectory, {
23
41
  overwrite: true,
24
42
  });
43
+ if (ref) {
44
+ stepsCtx.logger.info(`Checking out ref "${ref}"`);
45
+ await (0, git_1.fetchAndCheckoutRefAsync)({
46
+ ref,
47
+ repositoryDirectory: stepsCtx.global.projectTargetDirectory,
48
+ });
49
+ }
25
50
  stepsCtx.global.markAsCheckedOut(stepsCtx.logger);
26
51
  },
27
52
  });
@@ -0,0 +1,3 @@
1
+ import { BuildFunction } from '@expo/steps';
2
+ import { type CustomBuildContext } from '../../customBuildContext';
3
+ export declare function createCollectServeSimMetricsBuildFunction(ctx: CustomBuildContext): BuildFunction;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCollectServeSimMetricsBuildFunction = createCollectServeSimMetricsBuildFunction;
4
+ const steps_1 = require("@expo/steps");
5
+ const sentry_1 = require("../../sentry");
6
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
7
+ const serveSimMetricsArtifacts_1 = require("../utils/serveSimMetricsArtifacts");
8
+ const serveSimMetricsRecorder_1 = require("../utils/serveSimMetricsRecorder");
9
+ function createCollectServeSimMetricsBuildFunction(ctx) {
10
+ return new steps_1.BuildFunction({
11
+ namespace: 'eas',
12
+ id: 'collect_serve_sim_metrics',
13
+ name: 'Collect serve-sim metrics',
14
+ __metricsId: 'eas/collect_serve_sim_metrics',
15
+ supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
16
+ fn: async ({ logger }, { env }) => {
17
+ const collected = await serveSimMetricsRecorder_1.ServeSimMetricsRecorder.finishAsync({ logger });
18
+ if (collected.length === 0) {
19
+ logger.info('No serve-sim metrics collected; skipping upload.');
20
+ return;
21
+ }
22
+ try {
23
+ const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
24
+ for (const { udid, filePath, metadata } of collected) {
25
+ await (0, serveSimMetricsArtifacts_1.uploadServeSimMetricsFileAsync)(ctx, {
26
+ deviceRunSessionId,
27
+ udid,
28
+ filePath,
29
+ metadata,
30
+ logger,
31
+ });
32
+ }
33
+ }
34
+ catch (err) {
35
+ const error = err instanceof Error ? err : new Error(String(err));
36
+ sentry_1.Sentry.capture('Could not upload serve-sim metrics', error);
37
+ logger.warn({ err: error }, 'Could not upload serve-sim metrics.');
38
+ }
39
+ },
40
+ });
41
+ }
@@ -1,6 +1,7 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
3
  import { type CustomBuildContext } from '../../customBuildContext';
4
+ export declare const DEFAULT_AGENT_DEVICE_VERSION = "0.20.3";
4
5
  export declare function createStartAgentDeviceRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
5
6
  export declare function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }: {
6
7
  eventCollection: {