Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[steps] Add support for ${{ ... }} interpolations to function inputs #503

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/steps/src/BuildStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,9 @@ export class BuildStep extends BuildStepOutputAccessor {
inputs:
this.inputs?.reduce(
(acc, input) => {
acc[input.id] = input.value;
acc[input.id] = input.getValue({
interpolationContext: this.getInterpolationContext(),
});
return acc;
},
{} as Record<string, unknown>
Expand All @@ -346,7 +348,7 @@ export class BuildStep extends BuildStepOutputAccessor {
);
}

private getInterpolationContext(): JobInterpolationContext {
public getInterpolationContext(): JobInterpolationContext {
const hasAnyPreviousStepFailed = this.ctx.global.hasAnyPreviousStepFailed;

return {
Expand Down Expand Up @@ -415,10 +417,14 @@ export class BuildStep extends BuildStepOutputAccessor {
}
const vars = inputs.reduce(
(acc, input) => {
const inputValue = input.getValue({
interpolationContext: this.getInterpolationContext(),
});

acc[input.id] =
typeof input.value === 'object'
? JSON.stringify(input.value)
: input.value?.toString() ?? '';
typeof inputValue === 'object'
? JSON.stringify(inputValue)
: inputValue?.toString() ?? '';
return acc;
},
{} as Record<string, string>
Expand Down
46 changes: 25 additions & 21 deletions packages/steps/src/BuildStepInput.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import assert from 'assert';

import { bunyan } from '@expo/logger';
import { JobInterpolationContext } from '@expo/eas-build-job';

import { BuildStepGlobalContext, SerializedBuildStepGlobalContext } from './BuildStepContext.js';
import { BuildStepRuntimeError } from './errors.js';
import {
BUILD_STEP_OR_BUILD_GLOBAL_CONTEXT_REFERENCE_REGEX,
interpolateWithOutputs,
} from './utils/template.js';
import { interpolateJobContext } from './interpolation.js';

export enum BuildStepInputValueTypeName {
STRING = 'string',
Expand Down Expand Up @@ -95,34 +97,44 @@ export class BuildStepInput<
this.allowedValueTypeName = allowedValueTypeName;
}

public get value(): R extends true
? BuildStepInputValueType<T>
: BuildStepInputValueType<T> | undefined {
public getValue({
interpolationContext,
}: {
interpolationContext: JobInterpolationContext;
}): R extends true ? BuildStepInputValueType<T> : BuildStepInputValueType<T> | undefined {
const rawValue = this._value ?? this.defaultValue;
if (this.required && rawValue === undefined) {
throw new BuildStepRuntimeError(
`Input parameter "${this.id}" for step "${this.stepDisplayName}" is required but it was not set.`
);
}

const interpolatedValue = interpolateJobContext({
target: rawValue,
context: interpolationContext,
});

const valueDoesNotRequireInterpolation =
rawValue === undefined ||
rawValue === null ||
typeof rawValue === 'boolean' ||
typeof rawValue === 'number';
interpolatedValue === undefined ||
interpolatedValue === null ||
typeof interpolatedValue === 'boolean' ||
typeof interpolatedValue === 'number';
let returnValue;
if (valueDoesNotRequireInterpolation) {
if (typeof rawValue !== this.allowedValueTypeName && rawValue !== undefined) {
if (
typeof interpolatedValue !== this.allowedValueTypeName &&
interpolatedValue !== undefined
) {
throw new BuildStepRuntimeError(
`Input parameter "${this.id}" for step "${this.stepDisplayName}" must be of type "${this.allowedValueTypeName}".`
);
}
returnValue = rawValue as BuildStepInputValueType<T>;
returnValue = interpolatedValue as BuildStepInputValueType<T>;
} else {
// `valueDoesNotRequireInterpolation` checks that `rawValue` is not undefined
// `valueDoesNotRequireInterpolation` checks that `interpolatedValue` is not undefined
// so this will never be true.
assert(rawValue !== undefined);
const valueInterpolatedWithGlobalContext = this.ctx.interpolate(rawValue);
assert(interpolatedValue !== undefined);
const valueInterpolatedWithGlobalContext = this.ctx.interpolate(interpolatedValue);
const valueInterpolatedWithOutputsAndGlobalContext = interpolateWithOutputs(
valueInterpolatedWithGlobalContext,
(path) => this.ctx.getStepOutputValue(path) ?? ''
Expand Down Expand Up @@ -208,15 +220,7 @@ export class BuildStepInput<
}

private parseInputValueToString(value: string): string {
let parsedValue = value;
try {
parsedValue = JSON.parse(`"${value}"`);
} catch (err) {
if (!(err instanceof SyntaxError)) {
throw err;
}
}
return parsedValue;
return `${value}`;
}

private parseInputValueToNumber(value: string): number {
Expand Down
12 changes: 8 additions & 4 deletions packages/steps/src/BuildWorkflowValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ export class BuildWorkflowValidator {
typeof currentStepInput.rawValue === 'object'
? BuildStepInputValueTypeName.JSON
: typeof currentStepInput.rawValue;
const rawValueMayRequireInterpolation =
typeof currentStepInput.rawValue === 'string' && currentStepInput.rawValue.includes('${');
if (
currentStepInput.rawValue !== undefined &&
!currentStepInput.isRawValueStepOrContextReference() &&
!rawValueMayRequireInterpolation &&
currentType !== currentStepInput.allowedValueTypeName
) {
const error = new BuildConfigError(
Expand All @@ -81,9 +83,11 @@ export class BuildWorkflowValidator {
const error = new BuildConfigError(
`Input parameter "${currentStepInput.id}" for step "${
currentStep.displayName
}" is set to "${
currentStepInput.value
}" which is not one of the allowed values: ${nullthrows(currentStepInput.allowedValues)
}" is set to "${currentStepInput.getValue({
interpolationContext: currentStep.getInterpolationContext(),
})}" which is not one of the allowed values: ${nullthrows(
currentStepInput.allowedValues
)
.map((i) => `"${i}"`)
.join(', ')}.`
);
Expand Down
43 changes: 33 additions & 10 deletions packages/steps/src/__tests__/BuildConfigParser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,26 +224,37 @@ describe(BuildConfigParser, () => {
// property3: value4
// command: echo "Hi, ${ inputs.name }, ${ inputs.boolean_value }!"
const step1 = buildSteps[0];
const step1InterpolationContext = step1.getInterpolationContext();
expect(step1.id).toMatch(UUID_REGEX);
expect(step1.name).toBe('Say HI');
expect(step1.command).toBe('echo "Hi, ${ inputs.name }, ${ inputs.boolean_value }!"');
expect(step1.ctx.workingDirectory).toBe(ctx.defaultWorkingDirectory);
expect(step1.shell).toBe(getDefaultShell());
expect(step1.inputs).toBeDefined();
expect(step1.inputs?.[0].id).toBe('name');
expect(step1.inputs?.[0].value).toBe('Dominik Sokal');
expect(step1.inputs?.[0].getValue({ interpolationContext: step1InterpolationContext })).toBe(
'Dominik Sokal'
);
expect(step1.inputs?.[0].allowedValueTypeName).toBe(BuildStepInputValueTypeName.STRING);
expect(step1.inputs?.[1].id).toBe('country');
expect(step1.inputs?.[1].value).toBe('Poland');
expect(step1.inputs?.[1].getValue({ interpolationContext: step1InterpolationContext })).toBe(
'Poland'
);
expect(step1.inputs?.[1].allowedValueTypeName).toBe(BuildStepInputValueTypeName.STRING);
expect(step1.inputs?.[2].id).toBe('boolean_value');
expect(step1.inputs?.[2].value).toBe(true);
expect(step1.inputs?.[2].getValue({ interpolationContext: step1InterpolationContext })).toBe(
true
);
expect(step1.inputs?.[2].allowedValueTypeName).toBe(BuildStepInputValueTypeName.BOOLEAN);
expect(step1.inputs?.[3].id).toBe('number_value');
expect(step1.inputs?.[3].value).toBe(123);
expect(step1.inputs?.[3].getValue({ interpolationContext: step1InterpolationContext })).toBe(
123
);
expect(step1.inputs?.[3].allowedValueTypeName).toBe(BuildStepInputValueTypeName.NUMBER);
expect(step1.inputs?.[4].id).toBe('json_value');
expect(step1.inputs?.[4].value).toMatchObject({
expect(
step1.inputs?.[4].getValue({ interpolationContext: step1InterpolationContext })
).toMatchObject({
property1: 'value1',
property2: ['value2', { value3: { property3: 'value4' } }],
});
Expand Down Expand Up @@ -328,19 +339,24 @@ describe(BuildConfigParser, () => {
// - aaa
// - bbb
const step1 = buildSteps[0];
const step1InterpolationContext = step1.getInterpolationContext();
expect(step1.id).toMatch(UUID_REGEX);
expect(step1.name).toBe('Hi!');
expect(step1.command).toBe('echo "Hi, ${ inputs.name }!"');
expect(step1.ctx.workingDirectory).toBe(ctx.defaultWorkingDirectory);
expect(step1.shell).toBe(getDefaultShell());
expect(step1.inputs?.[0].id).toBe('name');
expect(step1.inputs?.[0].value).toBe('Dominik');
expect(step1.inputs?.[0].getValue({ interpolationContext: step1InterpolationContext })).toBe(
'Dominik'
);
expect(step1.inputs?.[0].allowedValueTypeName).toBe(BuildStepInputValueTypeName.STRING);
expect(step1.inputs?.[1].id).toBe('build_number');
expect(step1.inputs?.[1].rawValue).toBe('${ eas.job.version.buildNumber }');
expect(step1.inputs?.[1].allowedValueTypeName).toBe(BuildStepInputValueTypeName.NUMBER);
expect(step1.inputs?.[2].id).toBe('json_input');
expect(step1.inputs?.[2].value).toMatchObject({
expect(
step1.inputs?.[2].getValue({ interpolationContext: step1InterpolationContext })
).toMatchObject({
property1: 'value1',
property2: ['aaa', 'bbb'],
});
Expand All @@ -356,19 +372,26 @@ describe(BuildConfigParser, () => {
// name: Szymon
// build_number: 122
const step2 = buildSteps[1];
const step2InterpolationContext = step2.getInterpolationContext();
expect(step2.id).toMatch(UUID_REGEX);
expect(step2.name).toBe('Hi, Szymon!');
expect(step2.command).toBe('echo "Hi, ${ inputs.name }!"');
expect(step2.ctx.workingDirectory).toBe(ctx.defaultWorkingDirectory);
expect(step2.shell).toBe(getDefaultShell());
expect(step2.inputs?.[0].id).toBe('name');
expect(step2.inputs?.[0].value).toBe('Szymon');
expect(step2.inputs?.[0].getValue({ interpolationContext: step2InterpolationContext })).toBe(
'Szymon'
);
expect(step2.inputs?.[0].allowedValueTypeName).toBe(BuildStepInputValueTypeName.STRING);
expect(step2.inputs?.[1].id).toBe('build_number');
expect(step2.inputs?.[1].value).toBe(122);
expect(step2.inputs?.[1].getValue({ interpolationContext: step2InterpolationContext })).toBe(
122
);
expect(step2.inputs?.[1].allowedValueTypeName).toBe(BuildStepInputValueTypeName.NUMBER);
expect(step2.inputs?.[2].id).toBe('json_input');
expect(step2.inputs?.[2].value).toMatchObject({
expect(
step2.inputs?.[2].getValue({ interpolationContext: step2InterpolationContext })
).toMatchObject({
property1: 'value1',
property2: ['value2', { value3: { property3: 'value4' } }],
});
Expand Down
9 changes: 5 additions & 4 deletions packages/steps/src/__tests__/BuildFunction-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,11 @@ describe(BuildFunction, () => {
},
workingDirectory: ctx.defaultWorkingDirectory,
});
expect(step.inputs?.[0].value).toBe('abc');
expect(step.inputs?.[1].value).toBe('def');
expect(step.inputs?.[2].value).toBe(false);
expect(step.inputs?.[3].value).toMatchObject({
const interpolationContext = step.getInterpolationContext();
expect(step.inputs?.[0].getValue({ interpolationContext })).toBe('abc');
expect(step.inputs?.[1].getValue({ interpolationContext })).toBe('def');
expect(step.inputs?.[2].getValue({ interpolationContext })).toBe(false);
expect(step.inputs?.[3].getValue({ interpolationContext })).toMatchObject({
b: 2,
});
});
Expand Down
19 changes: 11 additions & 8 deletions packages/steps/src/__tests__/BuildStep-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { jest } from '@jest/globals';
import { instance, mock, verify, when } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';

import { BuildStep, BuildStepFunction, BuildStepStatus } from '../BuildStep.js';
import { BuildStep, BuildStepStatus } from '../BuildStep.js';
import {
BuildStepInput,
BuildStepInputById,
Expand Down Expand Up @@ -623,18 +623,21 @@ describe(BuildStep, () => {
}),
];

const fn: BuildStepFunction = (_ctx, { inputs, outputs }) => {
outputs.abc.set(
`${inputs.foo1.value} ${inputs.foo2.value} ${inputs.foo3.value} ${inputs.foo4.value}`
);
};

const step = new BuildStep(baseStepCtx, {
id,
displayName,
inputs,
outputs,
fn,
fn: (_ctx, { inputs, outputs }) => {
const interpolationContext = step.getInterpolationContext();
outputs.abc.set(
`${inputs.foo1.getValue({ interpolationContext })} ${inputs.foo2.getValue({
interpolationContext,
})} ${inputs.foo3.getValue({ interpolationContext })} ${inputs.foo4.getValue({
interpolationContext,
})}`
);
},
});

await step.executeAsync();
Expand Down
Loading
Loading