From 2572e31b21851a48e3a9a984513a9c9077de6bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Fri, 11 Oct 2024 12:22:23 +0200 Subject: [PATCH] Add `workflowInterpolationContext` --- .../build-tools/src/customBuildContext.ts | 8 ++-- packages/eas-build-job/src/android.ts | 5 +++ packages/eas-build-job/src/common.ts | 35 +++++++++++++++ packages/eas-build-job/src/context.ts | 16 ++++++- packages/eas-build-job/src/generic.ts | 8 +++- packages/eas-build-job/src/index.ts | 1 + packages/eas-build-job/src/ios.ts | 4 ++ packages/steps/src/BuildStep.ts | 45 ++++++++++++++++++- packages/steps/src/BuildStepContext.ts | 8 ++-- .../steps/src/__tests__/BuildStep-test.ts | 10 +++++ .../src/__tests__/BuildStepContext-test.ts | 12 ++--- .../src/__tests__/BuildStepInput-test.ts | 22 ++++----- .../my-custom-ts-function/src/index.ts | 20 +++------ .../fixtures/my-custom-ts-function/yarn.lock | 14 +++++- packages/steps/src/__tests__/utils/context.ts | 13 +++--- packages/steps/src/cli/cli.ts | 11 ++--- packages/steps/src/index.ts | 1 + packages/steps/src/interpolation.ts | 35 +++++++++++++++ 18 files changed, 216 insertions(+), 52 deletions(-) create mode 100644 packages/steps/src/interpolation.ts diff --git a/packages/build-tools/src/customBuildContext.ts b/packages/build-tools/src/customBuildContext.ts index 495359b26..d54c04f54 100644 --- a/packages/build-tools/src/customBuildContext.ts +++ b/packages/build-tools/src/customBuildContext.ts @@ -4,12 +4,12 @@ import path from 'path'; import { BuildJob, BuildPhase, - BuildStaticContext, BuildTrigger, Env, Job, Metadata, Platform, + StaticJobInterpolationContext, } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; import { ExternalBuildContextProvider, BuildRuntimePlatform } from '@expo/steps'; @@ -92,11 +92,13 @@ export class CustomBuildContext implements ExternalBuild return this._env; } - public staticContext(): BuildStaticContext { + public staticContext(): StaticJobInterpolationContext { return { + ...this.job.workflowInterpolationContext, job: this.job, - metadata: this.metadata ?? null, env: this.env, + metadata: this.metadata ?? null, + steps: {}, }; } diff --git a/packages/eas-build-job/src/android.ts b/packages/eas-build-job/src/android.ts index cf13ddc97..574cdd58f 100644 --- a/packages/eas-build-job/src/android.ts +++ b/packages/eas-build-job/src/android.ts @@ -14,6 +14,7 @@ import { EnvironmentSecret, BuildTrigger, BuildMode, + StaticWorkflowInterpolationContext, } from './common'; export interface Keystore { @@ -110,6 +111,8 @@ export interface Job { submitProfile?: string; }; loggerLevel?: LoggerLevel; + + workflowInterpolationContext?: StaticWorkflowInterpolationContext; } const SecretsSchema = Joi.object({ @@ -175,4 +178,6 @@ export const JobSchema = Joi.object({ submitProfile: Joi.string(), }), loggerLevel: Joi.string().valid(...Object.values(LoggerLevel)), + + workflowInterpolationContext: Joi.any(), }); diff --git a/packages/eas-build-job/src/common.ts b/packages/eas-build-job/src/common.ts index 69f2912d9..8c93f3df5 100644 --- a/packages/eas-build-job/src/common.ts +++ b/packages/eas-build-job/src/common.ts @@ -165,3 +165,38 @@ export interface BuildPhaseStats { result: BuildPhaseResult; durationMs: number; } + +export const StaticWorkflowInterpolationContextZ = z.object({ + needs: z.record( + z.string(), + z.object({ + status: z.string(), + outputs: z.record(z.string(), z.string().nullable()), + }) + ), + github: z + .object({ + event_name: z.enum(['push', 'pull_request', 'workflow_dispatch']), + sha: z.string(), + ref: z.string(), + }) + // We need to .optional() to support jobs that are not triggered by a GitHub event. + .optional(), + env: z.record(z.string()), +}); + +export type StaticWorkflowInterpolationContext = z.infer< + typeof StaticWorkflowInterpolationContextZ +>; + +export type DynamicInterpolationContext = { + success: () => boolean; + failure: () => boolean; + always: () => boolean; + never: () => boolean; + fromJSON: (value: string) => unknown; + toJSON: (value: unknown) => string; +}; + +export type WorkflowInterpolationContext = StaticWorkflowInterpolationContext & + DynamicInterpolationContext; diff --git a/packages/eas-build-job/src/context.ts b/packages/eas-build-job/src/context.ts index f4a31cdc7..1328cef60 100644 --- a/packages/eas-build-job/src/context.ts +++ b/packages/eas-build-job/src/context.ts @@ -1,9 +1,21 @@ -import { Env } from './common'; +import { DynamicInterpolationContext, Env, StaticWorkflowInterpolationContext } from './common'; import { Job } from './job'; import { Metadata } from './metadata'; -export type BuildStaticContext = { +type StaticJobOnlyInterpolationContext = { job: Job; metadata: Metadata | null; env: Env; + steps: Record< + string, + { + outputs: Record; + } + >; }; + +export type StaticJobInterpolationContext = + | (StaticWorkflowInterpolationContext & StaticJobOnlyInterpolationContext) + | StaticJobOnlyInterpolationContext; + +export type JobInterpolationContext = StaticJobInterpolationContext & DynamicInterpolationContext; diff --git a/packages/eas-build-job/src/generic.ts b/packages/eas-build-job/src/generic.ts index 10e7817d2..94b734935 100644 --- a/packages/eas-build-job/src/generic.ts +++ b/packages/eas-build-job/src/generic.ts @@ -1,7 +1,12 @@ import { z } from 'zod'; import { LoggerLevel } from '@expo/logger'; -import { ArchiveSourceSchemaZ, BuildTrigger, EnvironmentSecretZ } from './common'; +import { + ArchiveSourceSchemaZ, + BuildTrigger, + EnvironmentSecretZ, + StaticWorkflowInterpolationContextZ, +} from './common'; import { StepZ } from './step'; export namespace Generic { @@ -33,6 +38,7 @@ export namespace Generic { type: z.never().optional(), triggeredBy: z.literal(BuildTrigger.GIT_BASED_INTEGRATION), loggerLevel: z.nativeEnum(LoggerLevel).optional(), + workflowInterpolationContext: StaticWorkflowInterpolationContextZ.optional(), }); const PathJobZ = CommonJobZ.extend({ diff --git a/packages/eas-build-job/src/index.ts b/packages/eas-build-job/src/index.ts index c454ea471..951925de3 100644 --- a/packages/eas-build-job/src/index.ts +++ b/packages/eas-build-job/src/index.ts @@ -12,6 +12,7 @@ export { Workflow, Platform, Cache, + WorkflowInterpolationContext, } from './common'; export { Metadata, sanitizeMetadata, FingerprintSource, FingerprintSourceType } from './metadata'; export * from './job'; diff --git a/packages/eas-build-job/src/ios.ts b/packages/eas-build-job/src/ios.ts index 9942b9864..534133f27 100644 --- a/packages/eas-build-job/src/ios.ts +++ b/packages/eas-build-job/src/ios.ts @@ -14,6 +14,7 @@ import { EnvironmentSecret, BuildTrigger, BuildMode, + StaticWorkflowInterpolationContext, } from './common'; export type DistributionType = 'store' | 'internal' | 'simulator'; @@ -124,6 +125,8 @@ export interface Job { submitProfile?: string; }; loggerLevel?: LoggerLevel; + + workflowInterpolationContext?: StaticWorkflowInterpolationContext; } const SecretsSchema = Joi.object({ @@ -209,4 +212,5 @@ export const JobSchema = Joi.object({ submitProfile: Joi.string(), }), loggerLevel: Joi.string().valid(...Object.values(LoggerLevel)), + workflowInterpolationContext: Joi.any(), }); diff --git a/packages/steps/src/BuildStep.ts b/packages/steps/src/BuildStep.ts index df138743d..773a80682 100644 --- a/packages/steps/src/BuildStep.ts +++ b/packages/steps/src/BuildStep.ts @@ -3,6 +3,7 @@ import fs from 'fs/promises'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; +import { JobInterpolationContext } from '@expo/eas-build-job'; import { BuildStepContext, BuildStepGlobalContext } from './BuildStepContext.js'; import { BuildStepInput, BuildStepInputById, makeBuildStepInputByIdMap } from './BuildStepInput.js'; @@ -26,6 +27,7 @@ import { BuildStepRuntimeError } from './errors.js'; import { BuildStepEnv } from './BuildStepEnv.js'; import { BuildRuntimePlatform } from './BuildRuntimePlatform.js'; import { jsepEval } from './utils/jsepEval.js'; +import { interpolateJobContext } from './interpolation.js'; export enum BuildStepStatus { NEW = 'new', @@ -69,6 +71,10 @@ export class BuildStepOutputAccessor { protected readonly outputById: BuildStepOutputById ) {} + public get outputs(): BuildStepOutput[] { + return Object.values(this.outputById); + } + public getOutputValueByName(name: string): string | undefined { if (!this.executed) { throw new BuildStepRuntimeError( @@ -341,11 +347,48 @@ export class BuildStep extends BuildStepOutputAccessor { ); } + private getInterpolationContext(): JobInterpolationContext { + const hasAnyPreviousStepFailed = this.ctx.global.hasAnyPreviousStepFailed; + + return { + always: () => true, + never: () => false, + success: () => !hasAnyPreviousStepFailed, + failure: () => hasAnyPreviousStepFailed, + ...this.ctx.global.staticContext.job.workflowInterpolationContext, + env: this.getScriptEnv(), + steps: Object.fromEntries( + this.ctx.global.steps.map((step) => [ + step.id, + { + outputs: Object.fromEntries( + step.outputs.map((output) => { + try { + return [output.id, output.value]; + } catch { + return [output.id, undefined]; + } + }) + ), + }, + ]) + ), + job: this.ctx.global.staticContext.job, + metadata: this.ctx.global.staticContext.metadata, + fromJSON: (json: string) => JSON.parse(json), + toJSON: (value: unknown) => JSON.stringify(value), + }; + } private async executeCommandAsync(): Promise { assert(this.command, 'Command must be defined.'); + const interpolatedCommand = interpolateJobContext({ + target: this.command, + context: this.getInterpolationContext(), + }); + const command = this.interpolateInputsOutputsAndGlobalContextInTemplate( - this.command, + `${interpolatedCommand}`, this.inputs ); this.ctx.logger.debug(`Interpolated inputs in the command template`); diff --git a/packages/steps/src/BuildStepContext.ts b/packages/steps/src/BuildStepContext.ts index 19b315d37..9dc5dd51f 100644 --- a/packages/steps/src/BuildStepContext.ts +++ b/packages/steps/src/BuildStepContext.ts @@ -1,7 +1,7 @@ import os from 'os'; import path from 'path'; -import { BuildStaticContext } from '@expo/eas-build-job'; +import { StaticJobInterpolationContext } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; import { v4 as uuidv4 } from 'uuid'; @@ -25,7 +25,7 @@ interface SerializedExternalBuildContextProvider { defaultWorkingDirectory: string; buildLogsDirectory: string; runtimePlatform: BuildRuntimePlatform; - staticContext: BuildStaticContext; + staticContext: StaticJobInterpolationContext; env: BuildStepEnv; } @@ -37,7 +37,7 @@ export interface ExternalBuildContextProvider { readonly runtimePlatform: BuildRuntimePlatform; readonly logger: bunyan; - readonly staticContext: () => BuildStaticContext; + readonly staticContext: () => StaticJobInterpolationContext; readonly env: BuildStepEnv; updateEnv(env: BuildStepEnv): void; @@ -88,7 +88,7 @@ export class BuildStepGlobalContext { return this.provider.env; } - public get staticContext(): BuildStaticContext { + public get staticContext(): StaticJobInterpolationContext { return this.provider.staticContext(); } diff --git a/packages/steps/src/__tests__/BuildStep-test.ts b/packages/steps/src/__tests__/BuildStep-test.ts index c968bcd4c..112350d19 100644 --- a/packages/steps/src/__tests__/BuildStep-test.ts +++ b/packages/steps/src/__tests__/BuildStep-test.ts @@ -4,6 +4,7 @@ import path from 'path'; import { jest } from '@jest/globals'; import { instance, mock, verify, when } from 'ts-mockito'; import { v4 as uuidv4 } from 'uuid'; +import { StaticJobInterpolationContext } from '@expo/eas-build-job'; import { BuildStep, BuildStepFunction, BuildStepStatus } from '../BuildStep.js'; import { @@ -62,6 +63,7 @@ describe(BuildStep, () => { it('throws when neither command nor fn is set', () => { const mockCtx = mock(); when(mockCtx.baseLogger).thenReturn(createMockLogger()); + when(mockCtx.staticContext).thenReturn({ job: {} } as StaticJobInterpolationContext); const ctx = instance(mockCtx); expect(() => { const id = 'test1'; @@ -1047,7 +1049,15 @@ describe(BuildStep.prototype.shouldExecuteStep, () => { command: 'echo 123', ifCondition: '${ success() }', }); +<<<<<<< HEAD expect(step.shouldExecuteStep()).toBe(false); +||||||| parent of 38d191a (Add `workflowInterpolationContext`) + const hasAnyPreviousStepsFailed = true; + expect(step.shouldExecuteStep(hasAnyPreviousStepsFailed)).toBe(false); +======= + ctx.markAsFailed(); + expect(step.shouldExecuteStep()).toBe(false); +>>>>>>> 38d191a (Add `workflowInterpolationContext`) }); it('returns true when a dynamic expression matches', () => { diff --git a/packages/steps/src/__tests__/BuildStepContext-test.ts b/packages/steps/src/__tests__/BuildStepContext-test.ts index 0e93ea899..9c504f72a 100644 --- a/packages/steps/src/__tests__/BuildStepContext-test.ts +++ b/packages/steps/src/__tests__/BuildStepContext-test.ts @@ -1,6 +1,6 @@ import os from 'os'; -import { BuildStaticContext } from '@expo/eas-build-job'; +import { JobInterpolationContext } from '@expo/eas-build-job'; import { instance, mock, when } from 'ts-mockito'; import { BuildStep } from '../BuildStep.js'; @@ -23,7 +23,7 @@ describe(BuildStepGlobalContext, () => { '/another/non/existent/path', '/working/dir/path', '/non/existent/path', - {} as unknown as BuildStaticContext + {} as unknown as JobInterpolationContext ), false ); @@ -42,7 +42,7 @@ describe(BuildStepGlobalContext, () => { projectTargetDirectory, workingDirectory, '/non/existent/path', - {} as unknown as BuildStaticContext + {} as unknown as JobInterpolationContext ), false ); @@ -60,7 +60,7 @@ describe(BuildStepGlobalContext, () => { projectTargetDirectory, workingDirectory, '/non/existent/path', - {} as unknown as BuildStaticContext + {} as unknown as JobInterpolationContext ), false ); @@ -82,7 +82,7 @@ describe(BuildStepGlobalContext, () => { projectSourceDirectory: '/a/b/c', projectTargetDirectory: '/d/e/f', relativeWorkingDirectory: 'i', - staticContextContent: { a: 1 } as unknown as BuildStaticContext, + staticContextContent: { a: 1 } as unknown as JobInterpolationContext, }); expect(ctx.serialize()).toEqual( expect.objectContaining({ @@ -114,7 +114,7 @@ describe(BuildStepGlobalContext, () => { defaultWorkingDirectory: '/g/h/i', buildLogsDirectory: '/j/k/l', runtimePlatform: BuildRuntimePlatform.DARWIN, - staticContext: { a: 1 } as unknown as BuildStaticContext, + staticContext: { a: 1 } as unknown as JobInterpolationContext, env: {}, }, skipCleanup: true, diff --git a/packages/steps/src/__tests__/BuildStepInput-test.ts b/packages/steps/src/__tests__/BuildStepInput-test.ts index c90252e68..aef1f855b 100644 --- a/packages/steps/src/__tests__/BuildStepInput-test.ts +++ b/packages/steps/src/__tests__/BuildStepInput-test.ts @@ -1,4 +1,4 @@ -import { BuildStaticContext } from '@expo/eas-build-job'; +import { JobInterpolationContext } from '@expo/eas-build-job'; import { BuildStepRuntimeError } from '../errors.js'; import { BuildStep } from '../BuildStep.js'; @@ -126,7 +126,7 @@ describe(BuildStepInput, () => { foo: { bar: 'Line 1\nLine 2\n\nLine 3', }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -144,7 +144,7 @@ describe(BuildStepInput, () => { foo: { bar: 'Line 1\\nLine 2\\n\\nLine 3', }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -169,7 +169,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -196,7 +196,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -223,7 +223,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -250,7 +250,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -279,7 +279,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -308,7 +308,7 @@ describe(BuildStepInput, () => { }, ], }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -329,7 +329,7 @@ describe(BuildStepInput, () => { context_val_2: { in_val_1: 'in_val_1', }, - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', @@ -361,7 +361,7 @@ describe(BuildStepInput, () => { const ctx = createGlobalContextMock({ staticContextContent: { context_val_1: 'Line 1\nLine 2\n\nLine 3', - } as unknown as BuildStaticContext, + } as unknown as JobInterpolationContext, }); const i = new BuildStepInput(ctx, { id: 'foo', diff --git a/packages/steps/src/__tests__/fixtures/my-custom-ts-function/src/index.ts b/packages/steps/src/__tests__/fixtures/my-custom-ts-function/src/index.ts index acefbcd6e..cc31077be 100644 --- a/packages/steps/src/__tests__/fixtures/my-custom-ts-function/src/index.ts +++ b/packages/steps/src/__tests__/fixtures/my-custom-ts-function/src/index.ts @@ -1,21 +1,15 @@ -import { - BuildStepContext, - BuildStepInput, - BuildStepInputValueTypeName, - BuildStepOutput, - BuildStepEnv, -} from '@expo/steps'; +import { BuildStepContext, BuildStepInput, BuildStepOutput, BuildStepEnv } from '@expo/steps'; interface MyTsFunctionInputs { - name: BuildStepInput; - num: BuildStepInput; - obj: BuildStepInput; + name: BuildStepInput; + num: BuildStepInput; + obj: BuildStepInput; } interface MyTsFunctionOutputs { - name: BuildStepOutput; - num: BuildStepOutput; - obj: BuildStepOutput; + name: BuildStepOutput; + num: BuildStepOutput; + obj: BuildStepOutput; } async function myTsFunctionAsync( diff --git a/packages/steps/src/__tests__/fixtures/my-custom-ts-function/yarn.lock b/packages/steps/src/__tests__/fixtures/my-custom-ts-function/yarn.lock index 843b9b64d..143625bc4 100644 --- a/packages/steps/src/__tests__/fixtures/my-custom-ts-function/yarn.lock +++ b/packages/steps/src/__tests__/fixtures/my-custom-ts-function/yarn.lock @@ -67,11 +67,18 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^20.4.1": +"@types/node@*": version "20.4.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.1.tgz#a6033a8718653c50ac4962977e14d0f984d9527d" integrity sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg== +"@types/node@20.14.2": + version "20.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== + dependencies: + undici-types "~5.26.4" + arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" @@ -260,6 +267,11 @@ typescript@^5.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + uuid@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" diff --git a/packages/steps/src/__tests__/utils/context.ts b/packages/steps/src/__tests__/utils/context.ts index 2cfc7ae62..11893a798 100644 --- a/packages/steps/src/__tests__/utils/context.ts +++ b/packages/steps/src/__tests__/utils/context.ts @@ -1,7 +1,7 @@ import os from 'os'; import path from 'path'; -import { BuildStaticContext } from '@expo/eas-build-job'; +import { JobInterpolationContext, StaticJobInterpolationContext } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; import { v4 as uuidv4 } from 'uuid'; @@ -25,12 +25,12 @@ export class MockContextProvider implements ExternalBuildContextProvider { public readonly projectTargetDirectory: string, public readonly defaultWorkingDirectory: string, public readonly buildLogsDirectory: string, - public readonly staticContextContent: BuildStaticContext + public readonly staticContextContent: StaticJobInterpolationContext ) {} public get env(): BuildStepEnv { return this._env; } - public staticContext(): BuildStaticContext { + public staticContext(): StaticJobInterpolationContext { return { ...this.staticContextContent }; } public updateEnv(env: BuildStepEnv): void { @@ -46,7 +46,7 @@ interface BuildContextParams { projectSourceDirectory?: string; projectTargetDirectory?: string; relativeWorkingDirectory?: string; - staticContextContent?: BuildStaticContext; + staticContextContent?: JobInterpolationContext; } export function createStepContextMock({ @@ -96,7 +96,10 @@ export function createGlobalContextMock({ ? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory) : resolvedProjectTargetDirectory, '/non/existent/dir', - staticContextContent ?? ({} as BuildStaticContext) + staticContextContent ?? + ({ + job: {}, + } as JobInterpolationContext) ), skipCleanup ?? false ); diff --git a/packages/steps/src/cli/cli.ts b/packages/steps/src/cli/cli.ts index 55c108a08..07101413f 100644 --- a/packages/steps/src/cli/cli.ts +++ b/packages/steps/src/cli/cli.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { BuildStaticContext, Env, Job, Metadata } from '@expo/eas-build-job'; +import { Job, StaticJobInterpolationContext } from '@expo/eas-build-job'; import { bunyan, createLogger } from '@expo/logger'; import { BuildConfigParser } from '../BuildConfigParser.js'; @@ -28,11 +28,12 @@ export class CliContextProvider implements ExternalBuildContextProvider { public get env(): BuildStepEnv { return this._env; } - public staticContext(): BuildStaticContext { + public staticContext(): StaticJobInterpolationContext { return { - job: {} as Job, - metadata: {} as Metadata, - env: this.env as Env, + env: this.env as Record, + job: {} as unknown as Job, + metadata: null, + steps: {}, }; } public updateEnv(env: BuildStepEnv): void { diff --git a/packages/steps/src/index.ts b/packages/steps/src/index.ts index 1c029a15c..89bef3c0e 100644 --- a/packages/steps/src/index.ts +++ b/packages/steps/src/index.ts @@ -12,5 +12,6 @@ export { BuildStepEnv } from './BuildStepEnv.js'; export { BuildFunctionGroup } from './BuildFunctionGroup.js'; export { BuildStep } from './BuildStep.js'; export * as errors from './errors.js'; +export * from './interpolation.js'; export * from './utils/shell/spawn.js'; export * from './utils/jsepEval.js'; diff --git a/packages/steps/src/interpolation.ts b/packages/steps/src/interpolation.ts new file mode 100644 index 000000000..ef3794893 --- /dev/null +++ b/packages/steps/src/interpolation.ts @@ -0,0 +1,35 @@ +import { JobInterpolationContext } from '@expo/eas-build-job'; + +import { jsepEval } from './utils/jsepEval.js'; + +export function interpolateJobContext({ + target, + context, +}: { + target: unknown; + context: JobInterpolationContext; +}): unknown { + if (typeof target === 'string') { + // If the value is e.g. `build: ${{ inputs.build }}`, we will interpolate the value + // without changing `inputs.build` type, i.e. if it is an object it'll be like `build: {...inputs.build}`. + if (target.startsWith('${{') && target.endsWith('}}')) { + return jsepEval(target.slice(3, -2), context); + } + + // Otherwise we replace all occurrences of `${{...}}` with the result of the expression. + // e.g. `echo ${{ build.profile }}` becomes `echo production`. + return target.replace(/\$\{\{(.+?)\}\}/g, (_match, expression) => { + return `${jsepEval(expression, context)}`; + }); + } else if (Array.isArray(target)) { + return target.map((value) => interpolateJobContext({ target: value, context })); + } else if (typeof target === 'object' && target) { + return Object.fromEntries( + Object.entries(target).map(([key, value]) => [ + key, + interpolateJobContext({ target: value, context }), + ]) + ); + } + return target; +}