From dc50ac63b074ec326027ffd6fdf69a8156bc8b0b Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Sun, 6 Sep 2026 23:50:57 +0700 Subject: [PATCH 1/2] [eas-cli] Keep the branch name in `workflow:run --ref` `--ref` resolved the ref locally with `git rev-parse` and sent only the resulting commit hash, so the branch or tag name never reached the server. Runs dispatched from the CLI were labeled with a 40-character hash, which also filled the website's Git branch filter with hashes. Send branch and tag names through untouched instead. `HEAD`, revision expressions and remote-tracking refs keep being resolved locally, since they either mean nothing in the remote repository or mean something else there. Fixes #4311 --- CHANGELOG.md | 2 + .../commands/workflow/__tests__/run.test.ts | 120 +++++++++++++++++- packages/eas-cli/src/commands/workflow/run.ts | 73 ++++++++++- 3 files changed, 191 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bdfab93e..1c2b463b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [eas-cli] Send the branch or tag name to `eas workflow:run --ref`, instead of resolving it to a commit hash locally, so that runs are labeled with the ref that was requested. ([#4355](https://github.com/expo/eas-cli/pull/4355) by [@dennytosp](https://github.com/dennytosp)) + ### ๐Ÿงน Chores - Upgrade `typescript` to 6.0.3, `ts-jest` to 29.4.12, `@types/node` to 20.19.43, `gql.tada` to 1.11.3, and `https-proxy-agent` to 7.0.6. ([#XXXX](https://github.com/expo/eas-cli/pull/XXXX) by [@wschurman](https://github.com/wschurman)) diff --git a/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts b/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts index 0b6d16ea13..00ce34ecb1 100644 --- a/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts +++ b/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts @@ -3,7 +3,7 @@ import { parseJsonInputs, parseWorkflowInputsFromYaml, } from '../../../commandUtils/workflow/inputs'; -import { resolveWorkflowRunSshInput } from '../run'; +import { resolveWorkflowRunGitRef, resolveWorkflowRunSshInput } from '../run'; describe(resolveWorkflowRunSshInput, () => { it('is null when --ssh is not set', () => { @@ -29,6 +29,124 @@ describe(resolveWorkflowRunSshInput, () => { }); }); +describe(resolveWorkflowRunGitRef, () => { + const commitSha = '0e5c2f18c7e4d0e2f5a1b3c4d5e6f708192a3b4c'; + + it('passes a branch name through instead of the commit it points at', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'my-branch', + symbolicFullName: 'refs/heads/my-branch', + commitSha, + }) + ).toBe('my-branch'); + }); + + it('passes a fully qualified branch name through', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'refs/heads/my-branch', + symbolicFullName: 'refs/heads/my-branch', + commitSha, + }) + ).toBe('refs/heads/my-branch'); + }); + + it('passes a branch name containing slashes through', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'feat/my-branch', + symbolicFullName: 'refs/heads/feat/my-branch', + commitSha, + }) + ).toBe('feat/my-branch'); + }); + + it('passes a tag name through', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'v1.0.0', + symbolicFullName: 'refs/tags/v1.0.0', + commitSha, + }) + ).toBe('v1.0.0'); + }); + + it('passes a fully qualified tag name through', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'refs/tags/v1.0.0', + symbolicFullName: 'refs/tags/v1.0.0', + commitSha, + }) + ).toBe('refs/tags/v1.0.0'); + }); + + // HEAD resolves to the default branch server-side, so it has to stay a commit. + it('resolves HEAD to a commit', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'HEAD', + symbolicFullName: 'refs/heads/main', + commitSha, + }) + ).toBe(commitSha); + }); + + it('resolves @ to a commit', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: '@', + symbolicFullName: 'refs/heads/main', + commitSha, + }) + ).toBe(commitSha); + }); + + it('resolves a detached HEAD to a commit', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'HEAD', + symbolicFullName: 'HEAD', + commitSha, + }) + ).toBe(commitSha); + }); + + it('resolves a revision expression to a commit', () => { + expect( + resolveWorkflowRunGitRef({ requestedRef: 'HEAD~2', symbolicFullName: null, commitSha }) + ).toBe(commitSha); + }); + + it('resolves a commit hash to a commit', () => { + expect( + resolveWorkflowRunGitRef({ requestedRef: '0e5c2f1', symbolicFullName: null, commitSha }) + ).toBe(commitSha); + }); + + // `origin/my-branch` is a local name for a remote branch; the server would not resolve it. + it('resolves a remote-tracking ref to a commit', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'origin/my-branch', + symbolicFullName: 'refs/remotes/origin/my-branch', + commitSha, + }) + ).toBe(commitSha); + }); + + it('resolves a partially qualified branch name to a commit', () => { + expect( + resolveWorkflowRunGitRef({ + requestedRef: 'heads/my-branch', + symbolicFullName: 'refs/heads/my-branch', + commitSha, + }) + ).toBe(commitSha); + }); +}); + describe('parseInputs', () => { it('should parse single key=value pair', () => { const inputs = parseInputs(['key=value']); diff --git a/packages/eas-cli/src/commands/workflow/run.ts b/packages/eas-cli/src/commands/workflow/run.ts index 8ba5787ed3..eaf4a5b8f7 100644 --- a/packages/eas-cli/src/commands/workflow/run.ts +++ b/packages/eas-cli/src/commands/workflow/run.ts @@ -91,6 +91,62 @@ export function resolveWorkflowRunSshInput({ return ssh ? { idleTimeoutSeconds } : null; } +const PASSTHROUGH_REF_PREFIXES = ['refs/heads/', 'refs/tags/']; + +/** + * Pick the value to send as `gitRef` when running a workflow from `--ref`. + * + * The server resolves the ref itself and records it as the run's `requestedGitRef`, + * which is what the website shows in the "Branch" column and offers in the Git branch + * filter. Resolving the ref to a commit locally before dispatching therefore throws + * away the branch or tag name the user asked for. + * + * Branch and tag names are passed through untouched, whether they are given in short + * (`my-branch`) or fully qualified (`refs/heads/my-branch`) form. Everything else keeps + * being sent as the locally resolved commit, because it either has no meaning in the + * remote repository or does not mean the same thing there: `HEAD` and `@` resolve to the + * default branch server-side, revision expressions such as `HEAD~2` are not refs at all, + * and remote-tracking refs like `origin/my-branch` are local names for remote branches. + */ +export function resolveWorkflowRunGitRef({ + requestedRef, + symbolicFullName, + commitSha, +}: { + requestedRef: string; + symbolicFullName: string | null; + commitSha: string; +}): string { + const prefix = PASSTHROUGH_REF_PREFIXES.find(candidate => + symbolicFullName?.startsWith(candidate) + ); + if (!symbolicFullName || !prefix) { + return commitSha; + } + const shortName = symbolicFullName.slice(prefix.length); + return requestedRef === shortName || requestedRef === symbolicFullName ? requestedRef : commitSha; +} + +/** + * Full ref name (e.g. `refs/heads/my-branch`) that `ref` points at, or null when it does + * not name a ref (a commit hash, a revision expression, or a detached `HEAD`). + */ +async function maybeResolveSymbolicFullNameAsync( + ref: string, + projectDir: string +): Promise { + try { + const { output } = await spawnAsync( + 'git', + ['rev-parse', '--symbolic-full-name', '--verify', ref], + { cwd: projectDir } + ); + return output[0].trim() || null; + } catch { + return null; + } +} + export default class WorkflowRun extends EasCommand { static override description = 'run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow run, unless the --ref flag is used.'; @@ -184,15 +240,26 @@ export default class WorkflowRun extends EasCommand { // Run from git ref const fileName = path.basename(args.file); // Find the real commit, make sure the ref is valid - gitRef = ( + const commitSha = ( await spawnAsync('git', ['rev-parse', flags.ref], { cwd: projectDir, }) ).output[0].trim(); - if (!gitRef) { + if (!commitSha) { throw new Error('Failed to resolve git reference'); } - Log.log(`Using workflow file ${fileName} at ${gitRef}`); + // Send the branch or tag name the user asked for, so that the server records it as + // the run's requested git ref instead of a bare commit hash. + gitRef = resolveWorkflowRunGitRef({ + requestedRef: flags.ref, + symbolicFullName: await maybeResolveSymbolicFullNameAsync(flags.ref, projectDir), + commitSha, + }); + Log.log( + gitRef === commitSha + ? `Using workflow file ${fileName} at ${gitRef}` + : `Using workflow file ${fileName} at ${gitRef} (${commitSha})` + ); let revisionResult: WorkflowRevision | undefined; try { revisionResult = await WorkflowRevisionMutation.getOrCreateWorkflowRevisionFromGitRefAsync( From 09e17515dd9d488dbbe58aa1ecb25c480e72b197 Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Thu, 22 Oct 2026 02:01:08 +0700 Subject: [PATCH 2/2] [eas-cli] Send a confirmed, fully qualified ref in workflow:run --ref Addresses the review on #4355. Send the ref fully qualified instead of passing the short name through. `normalizeGitRef` in `workflow/insights/filters.ts` already qualifies a bare name before filtering runs by it, so `refs/heads/my-branch` is the form the server stores -- the short name would not match an `eas workflow:insights --git-ref` query for the same branch. It also says which ref was meant when a tag and a branch share a name. Confirm the name against the ref store rather than trusting it. `git rev-parse --symbolic-full-name` echoes the casing the caller typed, so on a case-insensitive filesystem `--ref My-Branch` resolved to the loose ref for `my-branch` and came back as `refs/heads/My-Branch`; sending that fails against a case-sensitive remote. `for-each-ref` reports the name the way the store spells it, and the result is required back exactly, because `for-each-ref` matches a pattern up to a slash as well and would report `refs/heads/feat/alpha` for `refs/heads/feat`. That collapses the resolver to `canonicalRefName ?? commitSha`. The prefix list, the short-name slice and both equality comparisons are gone, and with them the `HEAD` / `@` / remote-tracking special cases -- none of those confirm a `refs/heads/` or `refs/tags/` name, so they fall out for free. Log at a `debug` level when the ref lookup fails for a reason other than "not a ref", so a git or environment failure does not silently send the commit with no sign of why. Run the two git calls with `Promise.all`. A single `git rev-parse --symbolic-full-name ` was the other option, but it cannot carry `--verify` ("Needed a single revision"), it prints one line rather than two for a non-ref so the parse is positional, and it turns a path-like `--ref` into an opaque fatal. It would also still need the separate `for-each-ref` call. Report the commit the server resolved, and warn when it differs from the local one, so an unpushed commit is visible instead of the run quietly using a different revision than the hash printed locally. Changelog moves to a changeset, per #4580. --- .changeset/workflow-run-ref-branch-name.md | 5 + CHANGELOG.md | 1 - .../commands/workflow/__tests__/run.test.ts | 113 ++---------------- packages/eas-cli/src/commands/workflow/run.ts | 103 ++++++++++------ 4 files changed, 84 insertions(+), 138 deletions(-) create mode 100644 .changeset/workflow-run-ref-branch-name.md diff --git a/.changeset/workflow-run-ref-branch-name.md b/.changeset/workflow-run-ref-branch-name.md new file mode 100644 index 0000000000..8d9985abd7 --- /dev/null +++ b/.changeset/workflow-run-ref-branch-name.md @@ -0,0 +1,5 @@ +--- +'eas-cli': patch +--- + +Keep the fully qualified branch or tag name in `eas workflow:run --ref`, so a run is labeled with the ref that was requested instead of the commit hash it resolved to locally, and warn when the remote has that ref at a different commit. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f37ab2fb3..1c6f549345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,7 +95,6 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes -- [eas-cli] Send the branch or tag name to `eas workflow:run --ref`, instead of resolving it to a commit hash locally, so that runs are labeled with the ref that was requested. ([#4355](https://github.com/expo/eas-cli/pull/4355) by [@dennytosp](https://github.com/dennytosp)) - [build-tools] Report early Argent server exits during simulator startup instead of waiting for the full readiness timeout. ([#4454](https://github.com/expo/eas-cli/pull/4454) by [@sjkim-expo](https://github.com/sjkim-expo)) - [eas-cli] Stop spinners from printing a new line on every frame after the terminal gets narrower. ([#4470](https://github.com/expo/eas-cli/pull/4470) by [@brentvatne](https://github.com/brentvatne)) - [eas-cli] Exclude time waiting for simulator concurrency from the session startup timeout and show queue progress. ([#4411](https://github.com/expo/eas-cli/pull/4411) by [@szdziedzic](https://github.com/szdziedzic)) diff --git a/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts b/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts index 00ce34ecb1..cadbfc5fb4 100644 --- a/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts +++ b/packages/eas-cli/src/commands/workflow/__tests__/run.test.ts @@ -32,118 +32,29 @@ describe(resolveWorkflowRunSshInput, () => { describe(resolveWorkflowRunGitRef, () => { const commitSha = '0e5c2f18c7e4d0e2f5a1b3c4d5e6f708192a3b4c'; - it('passes a branch name through instead of the commit it points at', () => { + it('sends a confirmed branch ref instead of the commit it points at', () => { expect( - resolveWorkflowRunGitRef({ - requestedRef: 'my-branch', - symbolicFullName: 'refs/heads/my-branch', - commitSha, - }) - ).toBe('my-branch'); - }); - - it('passes a fully qualified branch name through', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'refs/heads/my-branch', - symbolicFullName: 'refs/heads/my-branch', - commitSha, - }) + resolveWorkflowRunGitRef({ canonicalRefName: 'refs/heads/my-branch', commitSha }) ).toBe('refs/heads/my-branch'); }); - it('passes a branch name containing slashes through', () => { + it('sends a confirmed tag ref instead of the commit it points at', () => { expect( - resolveWorkflowRunGitRef({ - requestedRef: 'feat/my-branch', - symbolicFullName: 'refs/heads/feat/my-branch', - commitSha, - }) - ).toBe('feat/my-branch'); - }); - - it('passes a tag name through', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'v1.0.0', - symbolicFullName: 'refs/tags/v1.0.0', - commitSha, - }) - ).toBe('v1.0.0'); - }); - - it('passes a fully qualified tag name through', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'refs/tags/v1.0.0', - symbolicFullName: 'refs/tags/v1.0.0', - commitSha, - }) + resolveWorkflowRunGitRef({ canonicalRefName: 'refs/tags/v1.0.0', commitSha }) ).toBe('refs/tags/v1.0.0'); }); - // HEAD resolves to the default branch server-side, so it has to stay a commit. - it('resolves HEAD to a commit', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'HEAD', - symbolicFullName: 'refs/heads/main', - commitSha, - }) - ).toBe(commitSha); - }); - - it('resolves @ to a commit', () => { + it('keeps a branch name containing slashes whole', () => { expect( - resolveWorkflowRunGitRef({ - requestedRef: '@', - symbolicFullName: 'refs/heads/main', - commitSha, - }) - ).toBe(commitSha); + resolveWorkflowRunGitRef({ canonicalRefName: 'refs/heads/feature/my-branch', commitSha }) + ).toBe('refs/heads/feature/my-branch'); }); - it('resolves a detached HEAD to a commit', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'HEAD', - symbolicFullName: 'HEAD', - commitSha, - }) - ).toBe(commitSha); - }); - - it('resolves a revision expression to a commit', () => { - expect( - resolveWorkflowRunGitRef({ requestedRef: 'HEAD~2', symbolicFullName: null, commitSha }) - ).toBe(commitSha); - }); - - it('resolves a commit hash to a commit', () => { - expect( - resolveWorkflowRunGitRef({ requestedRef: '0e5c2f1', symbolicFullName: null, commitSha }) - ).toBe(commitSha); - }); - - // `origin/my-branch` is a local name for a remote branch; the server would not resolve it. - it('resolves a remote-tracking ref to a commit', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'origin/my-branch', - symbolicFullName: 'refs/remotes/origin/my-branch', - commitSha, - }) - ).toBe(commitSha); - }); - - it('resolves a partially qualified branch name to a commit', () => { - expect( - resolveWorkflowRunGitRef({ - requestedRef: 'heads/my-branch', - symbolicFullName: 'refs/heads/my-branch', - commitSha, - }) - ).toBe(commitSha); + it('sends the commit when nothing named a ref', () => { + // `HEAD`, `@`, a detached `HEAD`, a revision expression such as `HEAD~2`, a commit + // hash, a remote-tracking ref, and a name whose casing the ref store rejected all + // arrive here the same way: the ref-name lookup could not confirm one. + expect(resolveWorkflowRunGitRef({ canonicalRefName: null, commitSha })).toBe(commitSha); }); }); diff --git a/packages/eas-cli/src/commands/workflow/run.ts b/packages/eas-cli/src/commands/workflow/run.ts index eaf4a5b8f7..4d2e95b1cb 100644 --- a/packages/eas-cli/src/commands/workflow/run.ts +++ b/packages/eas-cli/src/commands/workflow/run.ts @@ -91,8 +91,6 @@ export function resolveWorkflowRunSshInput({ return ssh ? { idleTimeoutSeconds } : null; } -const PASSTHROUGH_REF_PREFIXES = ['refs/heads/', 'refs/tags/']; - /** * Pick the value to send as `gitRef` when running a workflow from `--ref`. * @@ -101,48 +99,69 @@ const PASSTHROUGH_REF_PREFIXES = ['refs/heads/', 'refs/tags/']; * filter. Resolving the ref to a commit locally before dispatching therefore throws * away the branch or tag name the user asked for. * - * Branch and tag names are passed through untouched, whether they are given in short - * (`my-branch`) or fully qualified (`refs/heads/my-branch`) form. Everything else keeps - * being sent as the locally resolved commit, because it either has no meaning in the - * remote repository or does not mean the same thing there: `HEAD` and `@` resolve to the - * default branch server-side, revision expressions such as `HEAD~2` are not refs at all, - * and remote-tracking refs like `origin/my-branch` are local names for remote branches. + * A ref is sent fully qualified (`refs/heads/my-branch`), which is the form the server + * already stores -- see `normalizeGitRef` in `workflow/insights/filters.ts`, which + * qualifies a bare name before filtering runs by it. It is also the form that says which + * ref was meant when a tag and a branch share a name. Anything that does not name a ref + * keeps being sent as the locally resolved commit, because it either has no meaning in + * the remote repository or does not mean the same thing there: `HEAD` and `@` resolve to + * the default branch server-side, revision expressions such as `HEAD~2` are not refs at + * all, and remote-tracking refs like `origin/my-branch` are local names for remote + * branches. */ export function resolveWorkflowRunGitRef({ - requestedRef, - symbolicFullName, + canonicalRefName, commitSha, }: { - requestedRef: string; - symbolicFullName: string | null; + canonicalRefName: string | null; commitSha: string; }): string { - const prefix = PASSTHROUGH_REF_PREFIXES.find(candidate => - symbolicFullName?.startsWith(candidate) - ); - if (!symbolicFullName || !prefix) { - return commitSha; - } - const shortName = symbolicFullName.slice(prefix.length); - return requestedRef === shortName || requestedRef === symbolicFullName ? requestedRef : commitSha; + return canonicalRefName ?? commitSha; } /** - * Full ref name (e.g. `refs/heads/my-branch`) that `ref` points at, or null when it does - * not name a ref (a commit hash, a revision expression, or a detached `HEAD`). + * The fully qualified ref name (e.g. `refs/heads/my-branch`) that `ref` points at, spelled + * the way the ref store spells it, or null when `ref` does not unambiguously name one. + * + * The name is confirmed against the ref store rather than trusted, because + * `git rev-parse --symbolic-full-name` echoes the casing the caller typed: on a + * case-insensitive filesystem `My-Branch` resolves to the loose ref for `my-branch` and + * comes back as `refs/heads/My-Branch`. Remote ref names are case-sensitive, so sending + * that would fail a run that used to work. */ -async function maybeResolveSymbolicFullNameAsync( +async function maybeResolveCanonicalRefNameAsync( ref: string, projectDir: string ): Promise { + let symbolicFullName: string; try { const { output } = await spawnAsync( 'git', ['rev-parse', '--symbolic-full-name', '--verify', ref], { cwd: projectDir } ); - return output[0].trim() || null; - } catch { + symbolicFullName = output[0].trim(); + } catch (err) { + // `--verify` exits 0 with empty output for a revision expression, so this is a real + // git or environment failure rather than "not a ref". Falling back to the commit is + // still right, but it should not be silent. + Log.debug(`Could not resolve the full ref name for "${ref}": ${err}`); + return null; + } + if (!symbolicFullName) { + return null; + } + try { + const { output } = await spawnAsync( + 'git', + ['for-each-ref', '--count=1', '--format=%(refname)', symbolicFullName], + { cwd: projectDir } + ); + // `for-each-ref` matches a pattern up to a slash as well, so `refs/heads/feat` would + // report `refs/heads/feat/alpha`. Require the name back exactly. + return output[0].trim() === symbolicFullName ? symbolicFullName : null; + } catch (err) { + Log.debug(`Could not confirm "${symbolicFullName}" against the ref store: ${err}`); return null; } } @@ -239,26 +258,25 @@ export default class WorkflowRun extends EasCommand { if (flags.ref) { // Run from git ref const fileName = path.basename(args.file); - // Find the real commit, make sure the ref is valid - const commitSha = ( - await spawnAsync('git', ['rev-parse', flags.ref], { + // Find the real commit, make sure the ref is valid. The ref-name lookup swallows its + // own failures, so this can only reject from `rev-parse`, with the same error as before. + const [revParseResult, canonicalRefName] = await Promise.all([ + spawnAsync('git', ['rev-parse', flags.ref], { cwd: projectDir, - }) - ).output[0].trim(); + }), + maybeResolveCanonicalRefNameAsync(flags.ref, projectDir), + ]); + const commitSha = revParseResult.output[0].trim(); if (!commitSha) { throw new Error('Failed to resolve git reference'); } // Send the branch or tag name the user asked for, so that the server records it as // the run's requested git ref instead of a bare commit hash. - gitRef = resolveWorkflowRunGitRef({ - requestedRef: flags.ref, - symbolicFullName: await maybeResolveSymbolicFullNameAsync(flags.ref, projectDir), - commitSha, - }); + gitRef = resolveWorkflowRunGitRef({ canonicalRefName, commitSha }); Log.log( gitRef === commitSha ? `Using workflow file ${fileName} at ${gitRef}` - : `Using workflow file ${fileName} at ${gitRef} (${commitSha})` + : `Using workflow file ${fileName} at ${gitRef} (locally ${commitSha})` ); let revisionResult: WorkflowRevision | undefined; try { @@ -281,6 +299,19 @@ export default class WorkflowRun extends EasCommand { `Failed to find or create workflow revision for ${fileName} at ${flags.ref}` ); } + // The server resolves the ref against the remote, so a ref name can land on a commit + // the user does not have locally -- unpushed work, or a branch that moved. Say so + // rather than letting the commit printed above be the only one they ever see. + if ( + gitRef !== commitSha && + revisionResult.commitSha && + revisionResult.commitSha !== commitSha + ) { + Log.warn( + `${gitRef} is at ${commitSha} locally but ${revisionResult.commitSha} on the remote. ` + + 'The run uses the remote commit; push your local commits first if you meant those.' + ); + } yamlConfig = revisionResult.yamlConfig; workflowRevisionId = revisionResult.id; } else {