Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This is the log of notable changes to EAS CLI and related packages.

### 🐛 Bug fixes

- [steps] Coerce numeric env values to strings in workflow step configuration. ([#3583](https://github.com/expo/eas-cli/pull/3583) by [@szdziedzic](https://github.com/szdziedzic))
- [build-tools][eas-cli] Detect iOS Development provisioning profiles and set correct code signing identity instead of treating them as Ad Hoc. ([#3496](https://github.com/expo/eas-cli/pull/3496) by [@qwertey6](https://github.com/qwertey6))
- [build-tools] Prevent detecting Yarn Modern as Classic based on lockfile ([#3572](https://github.com/expo/eas-cli/pull/3572) by [@kitten](https://github.com/kitten))

Expand Down
7 changes: 6 additions & 1 deletion packages/steps/src/BuildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,12 @@ const BuildFunctionCallSchema = Joi.object({
name: Joi.string(),
workingDirectory: Joi.string(),
shell: Joi.string(),
env: Joi.object().pattern(Joi.string(), Joi.string().allow('')),
env: Joi.object().pattern(
Joi.string(),
Joi.alternatives()
.try(Joi.number().strict(), Joi.string().allow(''))
.custom(value => String(value))
),
if: Joi.string(),
timeout_minutes: Joi.number().positive(),
// Internal field for metrics collection. Not documented publicly.
Expand Down
79 changes: 75 additions & 4 deletions packages/steps/src/__tests__/BuildConfig-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe(validateConfig, () => {

expect(() => {
validateConfig(BuildConfigSchema, buildConfig);
}).toThrowError(/"build.steps\[0\].run.env.ENV1" must be a string/);
}).toThrowError(/"build.steps\[0\].run.env.ENV1" must be one of \[number, string\]/);
});
test('invalid env type', () => {
const buildConfig = {
Expand All @@ -278,7 +278,48 @@ describe(validateConfig, () => {

expect(() => {
validateConfig(BuildConfigSchema, buildConfig);
}).toThrowError(/"build.steps\[0\].run.env.ENV1" must be a string/);
}).toThrowError(/"build.steps\[0\].run.env.ENV1" must be one of \[number, string\]/);
});
test('env number coerced to string', () => {
const buildConfig = {
build: {
steps: [
{
run: {
command: 'echo 123',
env: {
HOMEBREW_NO_AUTO_UPDATE: 1,
},
},
Comment on lines +289 to +293
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a regression test that a numeric-like string env value (e.g. PORT: '001') remains unchanged after validation. This will catch unintended Joi conversion where strings are coerced into numbers and then stringified, potentially losing formatting.

Copilot uses AI. Check for mistakes.
},
],
},
};

const config = validateConfig(BuildConfigSchema, buildConfig);
assert(isBuildStepCommandRun(config.build.steps[0]));
expect(config.build.steps[0].run.env).toEqual({ HOMEBREW_NO_AUTO_UPDATE: '1' });
});
test('numeric-like string env value is not coerced', () => {
const buildConfig = {
build: {
steps: [
{
run: {
command: 'echo 123',
env: {
PORT: '001',
SCALE: '1e3',
},
},
},
],
},
};

const config = validateConfig(BuildConfigSchema, buildConfig);
assert(isBuildStepCommandRun(config.build.steps[0]));
expect(config.build.steps[0].run.env).toEqual({ PORT: '001', SCALE: '1e3' });
Comment on lines +283 to +322
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says there are two new test cases verifying numeric env coercion for both run steps and function calls, but this test file only adds coverage for run.env (and a separate case for numeric-like strings). Either add a test that asserts numeric env values are coerced for function-call steps (e.g. say_hi.env) or update the PR description/test plan to match what’s actually covered.

Copilot uses AI. Check for mistakes.
});
test('valid timeout_minutes', () => {
const buildConfig = {
Expand Down Expand Up @@ -471,6 +512,36 @@ describe(validateConfig, () => {
validateConfig(BuildConfigSchema, buildConfig);
}).not.toThrowError();
});
test('call with env number coerced to string', () => {
const buildConfig = {
build: {
steps: [
{
say_hi: {
env: {
HOMEBREW_NO_AUTO_UPDATE: 1,
PORT: 3000,
VERBOSE: 'true',
},
},
},
],
},
functions: {
say_hi: {
command: 'echo "Hi!"',
},
},
};

const config = validateConfig(BuildConfigSchema, buildConfig);
const step = config.build.steps[0] as BuildStepFunctionCall;
expect(step['say_hi'].env).toEqual({
HOMEBREW_NO_AUTO_UPDATE: '1',
PORT: '3000',
VERBOSE: 'true',
});
});
test('call with if statement', () => {
const buildConfig = {
build: {
Expand Down Expand Up @@ -518,7 +589,7 @@ describe(validateConfig, () => {

expect(() => {
validateConfig(BuildConfigSchema, buildConfig);
}).toThrowError(/"build.steps\[0\].say_hi.env.ENV1" must be a string/);
}).toThrowError(/"build.steps\[0\].say_hi.env.ENV1" must be one of \[number, string\]/);
});
test('invalid env structure', () => {
const buildConfig = {
Expand All @@ -544,7 +615,7 @@ describe(validateConfig, () => {

expect(() => {
validateConfig(BuildConfigSchema, buildConfig);
}).toThrowError(/"build.steps\[0\].say_hi.env.ENV1" must be a string/);
}).toThrowError(/"build.steps\[0\].say_hi.env.ENV1" must be one of \[number, string\]/);
});
test('call with inputs boolean', () => {
const buildConfig = {
Expand Down
Loading