Skip to content

[steps][build-tools][eas-cli] Expand legacy command/path local functions to a single build step; rename references in the consumers - #4096

Open
sswrk wants to merge 15 commits into
szymonswierk/eng-25402-eas-build-job-legacy-function-shapefrom
szymonswierk/eng-25402-steps-expand-legacy-functions
Open

[steps][build-tools][eas-cli] Expand legacy command/path local functions to a single build step; rename references in the consumers#4096
sswrk wants to merge 15 commits into
szymonswierk/eng-25402-eas-build-job-legacy-function-shapefrom
szymonswierk/eng-25402-steps-expand-legacy-functions

Conversation

@sswrk

@sswrk sswrk commented Jul 27, 2026

Copy link
Copy Markdown
Member

Why

We want to grandfather the legacy reusable functions from custom builds (https://docs.expo.dev/custom-builds/functions/) into the new reusable functions that can be used in workflows. This is to make the migration from custom builds to workflows easier.

This PR adds support for parsing the legacy custom build functions into workflow steps.

How

First, across steps, and the consumers (build-tools and eas-cli):

  • Rename “composite local function” occurrences to “local function” in places where we support both composite and legacy functions (this is done in the first commit so I recommend taking a look and then reviewing the rest of the commits to avoid the renaming noise).

The actual feature in steps:

  • Load legacy functions in the local function catalog (utils/localFunctions.ts).
    • Functions that specify path: have the path resolved in this step.
  • Map a legacy config to BuildFunction (utils/legacyFunction.ts).
    • Inputs and outputs default to required, which match the legacy functions, but is different from composites.
  • Expand a legacy call into a single build step (LocalFunctionExpander). BuildStep already has the machinery to run such function.
    • Note: unlike composite functions, working_directory set by the caller is allowed, because there's just one step.

This affects consumers:

  • build-tools: legacy functions referenced in workflows, hooks or composite functions used in workflows are now loaded.
  • eas-cli: eas workflow:validate accepts legacy functions.

Test Plan

Added unit tests.

@linear-code

linear-code Bot commented Jul 27, 2026

Copy link
Copy Markdown

ENG-25402

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.51852% with 4 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (szymonswierk/eng-25402-eas-build-job-legacy-function-shape@cec1583). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...es/eas-cli/src/commandUtils/workflow/validation.ts 33.34% 2 Missing ⚠️
packages/steps/src/index.ts 0.00% 1 Missing ⚠️
packages/steps/src/utils/localFunctions.ts 99.19% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                                      Coverage Diff                                      @@
##             szymonswierk/eng-25402-eas-build-job-legacy-function-shape    #4096   +/-   ##
=============================================================================================
  Coverage                                                              ?   63.45%           
=============================================================================================
  Files                                                                 ?     1028           
  Lines                                                                 ?    46952           
  Branches                                                              ?     9914           
=============================================================================================
  Hits                                                                  ?    29790           
  Misses                                                                ?    15707           
  Partials                                                              ?     1455           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from b77819d to b43b302 Compare July 28, 2026 09:04
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch 2 times, most recently from 22be929 to a49aa69 Compare July 28, 2026 13:11
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from a49aa69 to 0df6118 Compare July 28, 2026 15:30
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from 0df6118 to 2e32e71 Compare July 29, 2026 09:01
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from 2e32e71 to 3ba680d Compare July 29, 2026 09:29
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from 3ba680d to 096a5fd Compare July 29, 2026 13:40
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from 096a5fd to 04ebb71 Compare July 30, 2026 15:09
@sswrk sswrk added the ai-review Commits pushed to PRs with this label be automatically reviewed. label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 AI code review

Decision: Ready for human review (with comments)

Overall PR risk: Medium. The change renames the legacy "composite local function" path to a general "local function" path across steps, build-tools, and eas-cli, and adds a new feature that expands legacy command/path functions into a single build step. This modifies existing, shared parsing paths (StepsConfigParser, LocalFunctionExpander, hooks construction) used by every custom build and EAS workflow job, though deprecated aliases keep old call shapes working and the change is confined to packages that already ship together, so rollback is straightforward. The only verified finding is a misplaced CHANGELOG entry: both new lines sit under the already-released ## [21.7.0] heading instead of ## main, so the next release notes would miss them.

🟡 Warning (1)

  • New changelog entries placed under an already-released versionCHANGELOG.md:34 (quality) · id:98a11ab57644
    Confidence: High — three reviewers independently traced the same heading structure and the changelog tool logic.
    Impact if shipped: Medium — the release-automation script will not carry these entries into the next release notes.
    Suggested remediation: Move both new lines into the ### 🎉 New features list under the ## main heading at the top of the file.

    Evidence and reasoning

    The two new lines for PR [steps][build-tools][eas-cli] Expand legacy command/path local functions to a single build step; rename references in the consumers #4096 sit under ## [21.7.0](https://github.com/expo/eas-cli/releases/tag/v21.7.0) - 2026-08-07. This heading already has a release date and a GitHub release tag link, so version 21.7.0 already shipped. New entries for work still under review belong under the ## main section near the top of the file, where the repo keeps an empty ### 🎉 New features list until the next release cutover.

    scripts/src/changelogEntry.ts shows the repo convention: addEntry scans for the first depth-3 heading matching the category and appends there. That first match is always the heading inside ## main, not inside a released version section. Because the entries live under a published version instead, the changelog bot will not move them into the next release notes, and the historical record for 21.7.0 is edited after the fact.


This review is advisory — it never blocks a merge and never auto-approves.

@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch from 2e21d7e to d544e0c Compare August 5, 2026 12:29
@sswrk sswrk changed the title [steps] Expand legacy command/path local functions to a single build step [steps][build-tools][eas-cli] Expand legacy command/path local functions to a single build step; rename references in the consumers Aug 5, 2026
@sswrk
sswrk force-pushed the szymonswierk/eng-25402-steps-expand-legacy-functions branch 5 times, most recently from d4e75da to 5b53032 Compare August 6, 2026 12:25
@sswrk

sswrk commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Re. AI review:

Local function load errors use plain Error, not BuildConfigError

Not something specific to this PR, ignoring

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Subscribed to pull request

File Patterns Mentions
packages/eas-cli/** @douglowder

Generated by CodeMention

Warning: The preamble and epilogue options in commentConfiguration are deprecated. Use template instead.

@hSATAC hSATAC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I took a pass through this and left three inline comments, mostly around compatibility with existing custom build functions. I don't have much context on the legacy behavior, so I'm calling out differences I noticed rather than making a call on which ones we need to preserve.

I'll leave the final approval to Stanley since he has more context on the intended compatibility scope.

return BuildStepInput.createProvider({
id: input.name,
required: input.required ?? true,
defaultValue: input.default_value,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like a compatibility gap for two kinds of function.yml that are accepted by custom builds today. Both pass eas workflow:validate because it only builds the catalog, then fail when the worker parses the step.

For typed scalars, the Joi schema coerces quoted defaults and allowed values to their declared type. Here the raw values are passed through, so something like default_value: "42" for a number input remains a string and fails parse-time validation, even though getValue() would coerce it correctly at runtime.

For JSON inputs, an object default can never match an object in allowed_values because isRawValueOneOfAllowedValues() uses Array.includes(), while Joi compares them structurally:

inputs:
  - name: payload
    type: json
    default_value: { a: 1 }
    allowed_values: [{ a: 1 }]
command: echo hi

Could we preserve the Joi behavior here by coercing typed scalars and comparing JSON values structurally? Otherwise, I think these should at least fail during catalog loading with a field-level error, so eas workflow:validate catches them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't realize it. Added coercion ce7b0c1


const result = LocalFunctionConfigZ.safeParse(parsed);
if (!result.success) {
throw new Error(`Invalid local function "${functionPath}": ${z.prettifyError(result.error)}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking, but parsing the union here seems to make the validation errors less useful for both shapes.

A composite function.yml with a typo like shel: used to report Unrecognized key: "shel". Now it gets the generic three-shapes message. Legacy functions lose their field-level errors too; for example:

command: echo hi
supported_platforms: [windows]

reports that it must declare one of runs.steps, command, or path, even though command is already present.

Could we use the key that is present (runs, command, or path) to re-parse the matching branch and report its errors, keeping the generic message for cases where the shape is actually ambiguous? Since this PR is where the union starts being used in production, I think it may be worth handling here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed it in PR lower in the stack, by implementing a custom error logic: #4095

name: call.name ?? config.name ?? functionPath,
callInputs: call.callWith,
workingDirectory: call.workingDirectory,
shell: config.shell,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking question: is honoring the function-level shell here intentional?

From what I can tell, custom builds don't use BuildFunction.shell; only the call-site shell is passed through. That means a shell: declared by the function is currently ignored and the command runs with the default /bin/bash -eo pipefail. After migration, the same function would use its declared shell and lose the default -eo pipefail behavior.

Honoring the declaration may be the better behavior, but it does mean migration can change how an existing function runs. If that's intentional, could we adjust the “runs it exactly like a custom build does” docstring in utils/legacyFunction.ts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, this was intentional even though it's a real behavior divergence. My thinking is: since we can easily match user's intent, let's do that instead of preserving the confusing behavior.

Clarified the module doc that stated that there are "no differences" in behavior.

@github-actions

Copy link
Copy Markdown

✅ Thank you for adding the changelog entry!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Commits pushed to PRs with this label be automatically reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants