@expo/build-tools 21.0.2 → 21.1.0

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.
@@ -41,7 +41,18 @@ async function androidBuilder(ctx) {
41
41
  }
42
42
  }
43
43
  async function buildAsync(ctx) {
44
- await (0, setup_1.setupAsync)(ctx);
44
+ const jobHooksRef = { current: null };
45
+ try {
46
+ await buildInnerAsync(ctx, jobHooksRef);
47
+ }
48
+ finally {
49
+ // Native builds have no other drain path for the hook context's queued
50
+ // step-metric uploads.
51
+ await jobHooksRef.current?.customBuildContext.drainPendingMetricUploads();
52
+ }
53
+ }
54
+ async function buildInnerAsync(ctx, jobHooksRef) {
55
+ await (0, setup_1.setupAsync)(ctx, { wrappedAnchors: ['install_node_modules'], jobHooksRef });
45
56
  const evictUsedBefore = new Date();
46
57
  const workingDirectory = ctx.getReactNativeProjectDirectory();
47
58
  const hasNativeCode = ctx.job.type === eas_build_job_1.Workflow.GENERIC;
@@ -50,7 +50,19 @@ async function iosBuilder(ctx) {
50
50
  }
51
51
  }
52
52
  async function buildAsync(ctx) {
53
- await (0, setup_1.setupAsync)(ctx);
53
+ const jobHooksRef = { current: null };
54
+ try {
55
+ await buildInnerAsync(ctx, jobHooksRef);
56
+ }
57
+ finally {
58
+ // Native builds have no other drain path for the hook context's queued
59
+ // step-metric uploads. Must wrap the WHOLE body — iOS's existing inner
60
+ // try/finally only starts after setupAsync returns.
61
+ await jobHooksRef.current?.customBuildContext.drainPendingMetricUploads();
62
+ }
63
+ }
64
+ async function buildInnerAsync(ctx, jobHooksRef) {
65
+ await (0, setup_1.setupAsync)(ctx, { wrappedAnchors: ['install_node_modules'], jobHooksRef });
54
66
  const hasNativeCode = ctx.job.type === eas_build_job_1.Workflow.GENERIC;
55
67
  const evictUsedBefore = new Date();
56
68
  const credentialsManager = new manager_1.default(ctx);
@@ -111,6 +111,9 @@ function validateEasBuildInternalResult({ oldJob, result, }) {
111
111
  // We want to retain values that we have set on the job.
112
112
  appId: oldJob.appId,
113
113
  initiatingUserId: oldJob.initiatingUserId,
114
+ // Hooks are resolved server-side at submit; the mid-build eas.json
115
+ // regeneration must not add, drop, or change them (the original wins).
116
+ hooks: oldJob.hooks,
114
117
  ...(oldJob.platform === eas_build_job_1.Platform.IOS && oldJob.refreshAdHocProvisioningProfile === true
115
118
  ? { refreshAdHocProvisioningProfile: true }
116
119
  : null),
@@ -0,0 +1,18 @@
1
+ import { BuildJob, BuildPhase, HookAnchorId } from '@expo/eas-build-job';
2
+ import { BuildContext } from '../context';
3
+ import { ParsedJobHooks } from './jobHooks';
4
+ /**
5
+ * Runs a native build phase with its `before_<anchor>` / `after_<anchor>` hooks
6
+ * wrapped around it: before-entries -> phase (in a try) -> after-entries, with
7
+ * after-on-failure guaranteed. A failing before sequence skips the phase AND the
8
+ * after-hooks (steps-engine parity). The phase error always wins over an
9
+ * after-hook error. Passthrough (no hooks or no keys for this anchor) runs the
10
+ * phase exactly once with zero overhead.
11
+ */
12
+ export declare function runHookableBuildPhaseAsync<TJob extends BuildJob, T>({ ctx, hooks, buildPhase, anchor, fn, }: {
13
+ ctx: BuildContext<TJob>;
14
+ hooks: ParsedJobHooks | null;
15
+ buildPhase: BuildPhase;
16
+ anchor: HookAnchorId;
17
+ fn: () => Promise<T>;
18
+ }): Promise<T>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runHookableBuildPhaseAsync = runHookableBuildPhaseAsync;
4
+ const eas_build_job_1 = require("@expo/eas-build-job");
5
+ const steps_1 = require("@expo/steps");
6
+ /**
7
+ * Runs a native build phase with its `before_<anchor>` / `after_<anchor>` hooks
8
+ * wrapped around it: before-entries -> phase (in a try) -> after-entries, with
9
+ * after-on-failure guaranteed. A failing before sequence skips the phase AND the
10
+ * after-hooks (steps-engine parity). The phase error always wins over an
11
+ * after-hook error. Passthrough (no hooks or no keys for this anchor) runs the
12
+ * phase exactly once with zero overhead.
13
+ */
14
+ async function runHookableBuildPhaseAsync({ ctx, hooks, buildPhase, anchor, fn, }) {
15
+ // runHookEntriesAsync no-ops when the key has no entries.
16
+ if (hooks) {
17
+ await runHookEntriesAsync({ ctx, hooks, timing: 'before', anchor });
18
+ }
19
+ let result;
20
+ try {
21
+ result = await ctx.runBuildPhase(buildPhase, fn);
22
+ }
23
+ catch (phaseError) {
24
+ if (hooks) {
25
+ try {
26
+ await runHookEntriesAsync({ ctx, hooks, timing: 'after', anchor, anchorFailed: true });
27
+ }
28
+ catch (hookError) {
29
+ // The phase error wins; still surface the after-hook failure in the log.
30
+ ctx.logger.error({ err: hookError }, `after_${anchor} hook failed`);
31
+ }
32
+ }
33
+ throw phaseError;
34
+ }
35
+ if (hooks) {
36
+ await runHookEntriesAsync({ ctx, hooks, timing: 'after', anchor, anchorFailed: false });
37
+ }
38
+ return result;
39
+ }
40
+ async function runHookEntriesAsync({ ctx, hooks, timing, anchor, anchorFailed = false, }) {
41
+ const hookKey = `${timing}_${anchor}`;
42
+ const entries = hooks.hookEntriesByKey[hookKey];
43
+ if (!entries?.length) {
44
+ return;
45
+ }
46
+ // Refresh the hook context from the live BuildContext before running: env
47
+ // moves as the build runs (set-env written back below; profile env), and
48
+ // job/metadata are replaced by eas build:internal.
49
+ hooks.customBuildContext.updateEnv(ctx.env);
50
+ // Overwrite the job/metadata snapshot; NOT updateJobInformation, whose
51
+ // delta-merge would duplicate environmentSecrets on each refresh.
52
+ hooks.customBuildContext.job = ctx.job;
53
+ hooks.customBuildContext.metadata = ctx.metadata;
54
+ if (timing === 'after' && anchorFailed) {
55
+ // Mirror the engine: the anchor's failure is marked on the shared context so
56
+ // `failure()` / `success()` on after-hook steps read the phase outcome.
57
+ hooks.globalContext.markAsFailed();
58
+ }
59
+ const { failedLocally, firstError } = await (0, steps_1.executeHookStepsAsync)(hooks.globalContext, entries, {
60
+ anchor,
61
+ timing,
62
+ ...(timing === 'after' ? { anchorResult: anchorFailed ? 'failed' : 'success' } : null),
63
+ });
64
+ // Reverse write-back BEFORE propagating a failure: env set by the entries that
65
+ // did run must reach the build context even when a later entry failed, so the
66
+ // native phases (and the outer lifecycle hooks) see it. Hook-carrying builds
67
+ // are git-based, so updateEnv's gate does not fire here.
68
+ ctx.updateEnv(hooks.globalContext.env);
69
+ if (failedLocally) {
70
+ throw new eas_build_job_1.UserError(eas_build_job_1.ErrorCode.HOOKS_ERROR, `Hook "${hookKey}" failed: ${firstError instanceof Error ? firstError.message : String(firstError)}. Check the failing hook step's logs above.`, { cause: firstError });
71
+ }
72
+ }
@@ -0,0 +1,16 @@
1
+ import { BuildJob, HookAnchorId, HookKey } from '@expo/eas-build-job';
2
+ import { BuildStepGlobalContext, HookEntry } from '@expo/steps';
3
+ import { BuildContext } from '../context';
4
+ import { CustomBuildContext } from '../customBuildContext';
5
+ export interface ParsedJobHooks {
6
+ globalContext: BuildStepGlobalContext;
7
+ customBuildContext: CustomBuildContext;
8
+ hookEntriesByKey: Partial<Record<HookKey, HookEntry[]>>;
9
+ }
10
+ /**
11
+ * Parses the native job's `hooks` into engine-ready entries, once, at setup
12
+ * time. Shape-validates every registered key (reachable or not); constructs
13
+ * entries only for the anchors this build actually wraps. Returns null when the
14
+ * job declares no hooks.
15
+ */
16
+ export declare function parseJobHooksAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>, wrappedAnchors: readonly HookAnchorId[]): Promise<ParsedJobHooks | null>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseJobHooksAsync = parseJobHooksAsync;
4
+ const eas_build_job_1 = require("@expo/eas-build-job");
5
+ const steps_1 = require("@expo/steps");
6
+ const customBuildContext_1 = require("../customBuildContext");
7
+ const easFunctionGroups_1 = require("../steps/easFunctionGroups");
8
+ const easFunctions_1 = require("../steps/easFunctions");
9
+ /**
10
+ * Parses the native job's `hooks` into engine-ready entries, once, at setup
11
+ * time. Shape-validates every registered key (reachable or not); constructs
12
+ * entries only for the anchors this build actually wraps. Returns null when the
13
+ * job declares no hooks.
14
+ */
15
+ async function parseJobHooksAsync(ctx, wrappedAnchors) {
16
+ const hooks = ctx.job.hooks;
17
+ if (!hooks || Object.keys(hooks).length === 0) {
18
+ return null;
19
+ }
20
+ const customBuildContext = new customBuildContext_1.CustomBuildContext(ctx);
21
+ const globalContext = new steps_1.BuildStepGlobalContext(customBuildContext, false);
22
+ // Hook steps run in the checked-out project directory, not the pre-checkout
23
+ // target directory the global context defaults to.
24
+ globalContext.markAsCheckedOut(ctx.logger);
25
+ const externalFunctions = (0, easFunctions_1.getEasFunctions)(customBuildContext);
26
+ const externalFunctionGroups = (0, easFunctionGroups_1.getEasFunctionGroups)(customBuildContext);
27
+ // Shape-validate every REGISTERED key, reachable or not (steps-world parity:
28
+ // a malformed `before_submit` fails a steps build, so it fails here too), and
29
+ // warn on keys this build won't run. An unknown key is inert — a key newer
30
+ // than this worker must be ignored, never fail the job.
31
+ for (const [key, steps] of Object.entries(hooks)) {
32
+ const parsed = (0, eas_build_job_1.parseHookKey)(key);
33
+ if (parsed === null) {
34
+ ctx.logger.warn(`Ignoring unknown hook key "${key}".`);
35
+ continue;
36
+ }
37
+ if (Array.isArray(steps) && steps.length === 0) {
38
+ // `[]` is an explicit opt-out; validateSteps requires >= 1 step.
39
+ continue;
40
+ }
41
+ try {
42
+ (0, eas_build_job_1.validateSteps)(steps);
43
+ }
44
+ catch (err) {
45
+ throw new eas_build_job_1.UserError(eas_build_job_1.ErrorCode.HOOKS_ERROR, `Invalid steps in "hooks.${key}": ${err instanceof Error ? err.message : String(err)}`, { cause: err });
46
+ }
47
+ // Registered but not wrapped by this build: shape-checked above, then
48
+ // skipped. Function existence is deliberately NOT checked (that happens only
49
+ // during construction) — an unknown `uses:` under an unwrapped key warns
50
+ // rather than failing the build.
51
+ if (!wrappedAnchors.includes(parsed.anchorId)) {
52
+ ctx.logger.warn(`Ignoring "hooks.${key}": this build does not run the "${parsed.anchorId}" step.`);
53
+ }
54
+ }
55
+ // Construct entries for the wrapped anchors in EXECUTION order (per anchor,
56
+ // before_ then after_) so generated step ids and output references follow the
57
+ // order steps actually run. All entries share one globalContext, so env and
58
+ // outputs accumulate across keys.
59
+ const hookEntriesByKey = {};
60
+ const orderedSteps = [];
61
+ for (const anchor of wrappedAnchors) {
62
+ for (const side of ['before', 'after']) {
63
+ const key = `${side}_${anchor}`;
64
+ const steps = hooks[key];
65
+ if (!Array.isArray(steps) || steps.length === 0) {
66
+ continue;
67
+ }
68
+ let entries;
69
+ try {
70
+ entries = await (0, steps_1.constructHookEntriesAsync)(globalContext, steps, {
71
+ externalFunctions,
72
+ externalFunctionGroups,
73
+ });
74
+ }
75
+ catch (err) {
76
+ throw new eas_build_job_1.UserError(eas_build_job_1.ErrorCode.HOOKS_ERROR, `Failed to parse hooks.${key}: ${err instanceof Error ? err.message : String(err)}. Hooks use the same syntax as workflow steps.`, { cause: err });
77
+ }
78
+ hookEntriesByKey[key] = entries;
79
+ for (const entry of entries) {
80
+ orderedSteps.push(...entry.steps);
81
+ }
82
+ }
83
+ }
84
+ // One aggregate pass over every constructed step in execution order: unique
85
+ // ids, output references, and platform allowance. Per-key validation misses
86
+ // cross-key id collisions and would reject a legal after -> before output
87
+ // reference as a future reference.
88
+ try {
89
+ await (0, steps_1.validateHookStepsAsync)(globalContext, orderedSteps);
90
+ }
91
+ catch (err) {
92
+ throw new eas_build_job_1.UserError(eas_build_job_1.ErrorCode.HOOKS_ERROR, `The job's hooks are invalid: ${err instanceof Error ? err.message : String(err)}. Fix the reported step ids or output references.`, { cause: err });
93
+ }
94
+ return { globalContext, customBuildContext, hookEntriesByKey };
95
+ }
@@ -1,3 +1,15 @@
1
- import { BuildJob } from '@expo/eas-build-job';
1
+ import { BuildJob, HookAnchorId } from '@expo/eas-build-job';
2
+ import { ParsedJobHooks } from './jobHooks';
2
3
  import { BuildContext } from '../context';
3
- export declare function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>): Promise<void>;
4
+ /**
5
+ * Mutable holder the builder passes into setupAsync. setupAsync assigns
6
+ * `current` the moment hooks are parsed (before any hook runs), so the builder's
7
+ * finally can drain queued metric uploads even if setup fails partway through.
8
+ */
9
+ export interface JobHooksRef {
10
+ current: ParsedJobHooks | null;
11
+ }
12
+ export declare function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>, { wrappedAnchors, jobHooksRef, }: {
13
+ wrappedAnchors: readonly HookAnchorId[];
14
+ jobHooksRef: JobHooksRef;
15
+ }): Promise<void>;
@@ -10,7 +10,9 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  const nullthrows_1 = __importDefault(require("nullthrows"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const easBuildInternal_1 = require("./easBuildInternal");
13
+ const hookableBuildPhase_1 = require("./hookableBuildPhase");
13
14
  const installDependencies_1 = require("./installDependencies");
15
+ const jobHooks_1 = require("./jobHooks");
14
16
  const projectSources_1 = require("./projectSources");
15
17
  const xcodeEnv_1 = require("../ios/xcodeEnv");
16
18
  const hooks_1 = require("../utils/hooks");
@@ -26,7 +28,7 @@ class DoctorTimeoutError extends Error {
26
28
  }
27
29
  class InstallDependenciesTimeoutError extends Error {
28
30
  }
29
- async function setupAsync(ctx) {
31
+ async function setupAsync(ctx, { wrappedAnchors, jobHooksRef, }) {
30
32
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.PREPARE_PROJECT, async () => {
31
33
  await (0, retry_1.retryAsync)(async () => {
32
34
  await fs_extra_1.default.rm(ctx.buildDirectory, { recursive: true, force: true });
@@ -64,27 +66,42 @@ async function setupAsync(ctx) {
64
66
  ctx.logger.error({ err }, `Failed to read eas.json.`);
65
67
  }
66
68
  });
67
- const packageJson = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.READ_PACKAGE_JSON, async () => {
68
- return (0, project_1.readAndLogPackageJson)(ctx.logger, ctx.getReactNativeProjectDirectory());
69
+ await ctx.runBuildPhase(eas_build_job_1.BuildPhase.READ_PACKAGE_JSON, async () => {
70
+ (0, project_1.readAndLogPackageJson)(ctx.logger, ctx.getReactNativeProjectDirectory());
69
71
  });
70
- await ctx.runBuildPhase(eas_build_job_1.BuildPhase.INSTALL_DEPENDENCIES, async () => {
71
- const expoVersion = ctx.metadata?.sdkVersion ??
72
- (0, packageManager_1.getPackageVersionFromPackageJson)({
73
- packageJson,
74
- packageName: 'expo',
72
+ // Parse hooks unconditionally, after every pre-install env mutation
73
+ // (PREPARE_PROJECT, the git-based profile-env block, PRE_INSTALL_HOOK) and
74
+ // before the first wrapped phase.
75
+ jobHooksRef.current = await (0, jobHooks_1.parseJobHooksAsync)(ctx, wrappedAnchors);
76
+ await (0, hookableBuildPhase_1.runHookableBuildPhaseAsync)({
77
+ ctx,
78
+ hooks: jobHooksRef.current,
79
+ buildPhase: eas_build_job_1.BuildPhase.INSTALL_DEPENDENCIES,
80
+ anchor: 'install_node_modules',
81
+ fn: async () => {
82
+ // Read package.json fresh here rather than reusing the READ_PACKAGE_JSON
83
+ // capture: a before_install_node_modules hook may have patched it, and the
84
+ // steps-world install already reads it fresh. Silent read — READ_PACKAGE_JSON
85
+ // stays the only logging site.
86
+ const freshPackageJson = (0, project_1.readPackageJson)(ctx.getReactNativeProjectDirectory());
87
+ const expoVersion = ctx.metadata?.sdkVersion ??
88
+ (0, packageManager_1.getPackageVersionFromPackageJson)({
89
+ packageJson: freshPackageJson,
90
+ packageName: 'expo',
91
+ });
92
+ const reactNativeVersion = ctx.metadata?.reactNativeVersion ??
93
+ (0, packageManager_1.getPackageVersionFromPackageJson)({
94
+ packageJson: freshPackageJson,
95
+ packageName: 'react-native',
96
+ });
97
+ await runInstallDependenciesAsync(ctx, {
98
+ useFrozenLockfile: (0, packageManager_1.shouldUseFrozenLockfile)({
99
+ env: ctx.env,
100
+ sdkVersion: expoVersion,
101
+ reactNativeVersion,
102
+ }),
75
103
  });
76
- const reactNativeVersion = ctx.metadata?.reactNativeVersion ??
77
- (0, packageManager_1.getPackageVersionFromPackageJson)({
78
- packageJson,
79
- packageName: 'react-native',
80
- });
81
- await runInstallDependenciesAsync(ctx, {
82
- useFrozenLockfile: (0, packageManager_1.shouldUseFrozenLockfile)({
83
- env: ctx.env,
84
- sdkVersion: expoVersion,
85
- reactNativeVersion,
86
- }),
87
- });
104
+ },
88
105
  });
89
106
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.READ_APP_CONFIG, async () => {
90
107
  const appConfig = await ctx.appConfig;
@@ -111,7 +128,9 @@ async function setupAsync(ctx) {
111
128
  ctx.updateJobInformation(newJob, newMetadata);
112
129
  });
113
130
  }
114
- const hasExpoPackage = !!packageJson.dependencies?.expo;
131
+ // Read fresh: a before_install_node_modules hook may have added or removed
132
+ // the expo dependency.
133
+ const hasExpoPackage = !!(0, project_1.readPackageJson)(ctx.getReactNativeProjectDirectory()).dependencies?.expo;
115
134
  if (!ctx.env.EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP && hasExpoPackage) {
116
135
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.RUN_EXPO_DOCTOR, async () => {
117
136
  try {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as Builders from './builders';
2
- import GCSLoggerStream from './gcs/LoggerStream';
3
- import { GCS } from './gcs/client';
4
- export { Builders, GCS, GCSLoggerStream };
2
+ import RemoteLoggerStream from './logging/RemoteLoggerStream';
3
+ export { Builders, RemoteLoggerStream };
4
+ export { uploadWithSignedUrl } from './storage/uploadWithSignedUrl';
5
+ export type { SignedUrl, UploadWithSignedUrlParams } from './storage/uploadWithSignedUrl';
5
6
  export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
6
7
  export { PackageManager } from './utils/packageManager';
7
8
  export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
package/dist/index.js CHANGED
@@ -39,13 +39,13 @@ 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.GCSLoggerStream = exports.GCS = 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.Builders = void 0;
43
43
  const Builders = __importStar(require("./builders"));
44
44
  exports.Builders = Builders;
45
- const LoggerStream_1 = __importDefault(require("./gcs/LoggerStream"));
46
- exports.GCSLoggerStream = LoggerStream_1.default;
47
- const client_1 = require("./gcs/client");
48
- Object.defineProperty(exports, "GCS", { enumerable: true, get: function () { return client_1.GCS; } });
45
+ const RemoteLoggerStream_1 = __importDefault(require("./logging/RemoteLoggerStream"));
46
+ exports.RemoteLoggerStream = RemoteLoggerStream_1.default;
47
+ var uploadWithSignedUrl_1 = require("./storage/uploadWithSignedUrl");
48
+ Object.defineProperty(exports, "uploadWithSignedUrl", { enumerable: true, get: function () { return uploadWithSignedUrl_1.uploadWithSignedUrl; } });
49
49
  var context_1 = require("./context");
50
50
  Object.defineProperty(exports, "BuildContext", { enumerable: true, get: function () { return context_1.BuildContext; } });
51
51
  Object.defineProperty(exports, "SkipNativeBuildError", { enumerable: true, get: function () { return context_1.SkipNativeBuildError; } });
@@ -1,7 +1,7 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { Writable } from 'stream';
3
- import { GCS } from './client';
4
- declare class GCSLoggerStream extends Writable {
3
+ import { type SignedUrl } from '../storage/uploadWithSignedUrl';
4
+ declare class RemoteLoggerStream extends Writable {
5
5
  writable: boolean;
6
6
  private readonly logger;
7
7
  private readonly uploadMethod?;
@@ -16,7 +16,7 @@ declare class GCSLoggerStream extends Writable {
16
16
  private buffer;
17
17
  private writePromise?;
18
18
  private cleanUpCalled;
19
- constructor({ logger, uploadMethod, options }: GCSLoggerStream.Config);
19
+ constructor({ logger, uploadMethod, options }: RemoteLoggerStream.Config);
20
20
  private findNormalizedHeader;
21
21
  init(): Promise<string>;
22
22
  cleanUp(): Promise<void>;
@@ -27,7 +27,7 @@ declare class GCSLoggerStream extends Writable {
27
27
  private upload;
28
28
  private createCompressedStream;
29
29
  }
30
- declare namespace GCSLoggerStream {
30
+ declare namespace RemoteLoggerStream {
31
31
  enum CompressionMethod {
32
32
  GZIP = "gzip",
33
33
  BR = "br"
@@ -37,7 +37,7 @@ declare namespace GCSLoggerStream {
37
37
  compress?: CompressionMethod;
38
38
  }
39
39
  type UploadMethod = {
40
- signedUrl: GCS.SignedUrl;
40
+ signedUrl: SignedUrl;
41
41
  };
42
42
  interface Config {
43
43
  logger: bunyan;
@@ -45,4 +45,4 @@ declare namespace GCSLoggerStream {
45
45
  options: Options;
46
46
  }
47
47
  }
48
- export default GCSLoggerStream;
48
+ export default RemoteLoggerStream;
@@ -11,9 +11,9 @@ const path_1 = __importDefault(require("path"));
11
11
  const stream_1 = require("stream");
12
12
  const util_1 = require("util");
13
13
  const zlib_1 = __importDefault(require("zlib"));
14
- const client_1 = require("./client");
14
+ const uploadWithSignedUrl_1 = require("../storage/uploadWithSignedUrl");
15
15
  const pipe = (0, util_1.promisify)(stream_1.pipeline);
16
- class GCSLoggerStream extends stream_1.Writable {
16
+ class RemoteLoggerStream extends stream_1.Writable {
17
17
  writable = true;
18
18
  logger;
19
19
  uploadMethod;
@@ -74,7 +74,7 @@ class GCSLoggerStream extends stream_1.Writable {
74
74
  await this.flush(true);
75
75
  await fs_extra_1.default.remove(this.temporaryLogsPath);
76
76
  await fs_extra_1.default.remove(this.temporaryCompressedLogsPath);
77
- this.logger.info('Cleaning up GCS log stream');
77
+ this.logger.info('Cleaning up log stream');
78
78
  }
79
79
  write(rec) {
80
80
  if (!this.fileHandle) {
@@ -102,7 +102,7 @@ class GCSLoggerStream extends stream_1.Writable {
102
102
  this.hasChangesToUpload = true;
103
103
  }
104
104
  catch (err) {
105
- this.logger.error({ err, origin: 'gcs-logger' }, 'Failed to write logs to file');
105
+ this.logger.error({ err, origin: 'remote-logger' }, 'Failed to write logs to file');
106
106
  }
107
107
  finally {
108
108
  this.writePromise = undefined;
@@ -129,7 +129,7 @@ class GCSLoggerStream extends stream_1.Writable {
129
129
  return result;
130
130
  })
131
131
  .catch(err => {
132
- this.logger.error({ err }, 'Failed to upload logs file to GCS');
132
+ this.logger.error({ err }, 'Failed to upload logs file');
133
133
  })
134
134
  .then(result => {
135
135
  this.uploadingPromise = undefined;
@@ -145,7 +145,7 @@ class GCSLoggerStream extends stream_1.Writable {
145
145
  const srcGeneratorAsync = async () => {
146
146
  return await this.createCompressedStream(fs_extra_1.default.createReadStream(this.temporaryLogsPath, { end: size }));
147
147
  };
148
- return await client_1.GCS.uploadWithSignedUrl({
148
+ return await (0, uploadWithSignedUrl_1.uploadWithSignedUrl)({
149
149
  signedUrl: this.uploadMethod.signedUrl,
150
150
  srcGeneratorAsync,
151
151
  });
@@ -170,11 +170,11 @@ class GCSLoggerStream extends stream_1.Writable {
170
170
  return fs_extra_1.default.createReadStream(this.temporaryCompressedLogsPath, { end: size });
171
171
  }
172
172
  }
173
- (function (GCSLoggerStream) {
173
+ (function (RemoteLoggerStream) {
174
174
  let CompressionMethod;
175
175
  (function (CompressionMethod) {
176
176
  CompressionMethod["GZIP"] = "gzip";
177
177
  CompressionMethod["BR"] = "br";
178
- })(CompressionMethod = GCSLoggerStream.CompressionMethod || (GCSLoggerStream.CompressionMethod = {}));
179
- })(GCSLoggerStream || (GCSLoggerStream = {}));
180
- exports.default = GCSLoggerStream;
178
+ })(CompressionMethod = RemoteLoggerStream.CompressionMethod || (RemoteLoggerStream.CompressionMethod = {}));
179
+ })(RemoteLoggerStream || (RemoteLoggerStream = {}));
180
+ exports.default = RemoteLoggerStream;
@@ -17,6 +17,7 @@ export interface JUnitTestCaseResult {
17
17
  tags: string[];
18
18
  properties: Record<string, string>;
19
19
  }
20
+ export declare function parseFailedFlowNamesFromJUnitFile(junitFile: string): Promise<Set<string>>;
20
21
  export declare function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]>;
21
22
  export declare function isFileAttrRun(testcases: JUnitTestCaseResult[]): testcases is (JUnitTestCaseResult & {
22
23
  file: string;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseFailedFlowNamesFromJUnitFile = parseFailedFlowNamesFromJUnitFile;
6
7
  exports.parseJUnitTestCases = parseJUnitTestCases;
7
8
  exports.isFileAttrRun = isFileAttrRun;
8
9
  exports.junitFileHasFileAttrs = junitFileHasFileAttrs;
@@ -99,6 +100,17 @@ async function parseJUnitFile(filePath) {
99
100
  return [];
100
101
  }
101
102
  }
103
+ // Raw names (slashes kept) of the testcases a single JUnit file marks failed. Best-effort: an
104
+ // unreadable or malformed file yields an empty set rather than throwing.
105
+ async function parseFailedFlowNamesFromJUnitFile(junitFile) {
106
+ const failed = new Set();
107
+ for (const testcase of await parseJUnitFile(junitFile)) {
108
+ if (testcase.status === 'failed') {
109
+ failed.add(testcase.name);
110
+ }
111
+ }
112
+ return failed;
113
+ }
102
114
  async function parseJUnitTestCases(junitDirectory) {
103
115
  let entries;
104
116
  try {
@@ -18,6 +18,7 @@ export declare function harvestFailureScreenshotsAsync(args: {
18
18
  testsDirectory: string;
19
19
  capturedSinceMs: number;
20
20
  attemptIndex: number;
21
+ failedFlowNames: ReadonlySet<string>;
21
22
  logger: bunyan;
22
23
  }): Promise<HarvestedScreenshot[]>;
23
24
  export declare function computePureFailureFlowNames(testCases: readonly {