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 @@ -15,6 +15,7 @@ This is the log of notable changes to EAS CLI and related packages.

- [build-tools] Allow expo.test subdomains to open local and staging simulator previews. ([#4480](https://github.com/expo/eas-cli/pull/4480) by [@hirbod](https://github.com/hirbod))
- [build-tools] Retry App Store Connect connection failures and safely recover interrupted status reads and upload commits. ([#4520](https://github.com/expo/eas-cli/pull/4520) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Fail `eas deploy --prod` and `eas deploy:alias` when the alias was not moved to the new deployment, instead of reporting success and exiting 0. ([#4443](https://github.com/expo/eas-cli/pull/4443) by [@LizunovSergey](https://github.com/LizunovSergey))

### 🧹 Chores

Expand Down
104 changes: 104 additions & 0 deletions packages/eas-cli/src/worker/__tests__/deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { promptAsync } from '../../prompts';
import {
assertValidDevDomainName,
assignDevDomainNameAsync,
assignWorkerDeploymentAliasAsync,
assignWorkerDeploymentProductionAsync,
getSignedDeploymentUrlAsync,
} from '../deployment';
import { DeploymentsMutation } from '../mutations';
Expand Down Expand Up @@ -248,3 +250,105 @@ describe(getSignedDeploymentUrlAsync, () => {
expect(DeploymentsMutation.assignDevDomainNameAsync).not.toHaveBeenCalled();
});
});

function createAliasResult({
aliasName,
deploymentIdentifier,
}: {
aliasName: string | null;
deploymentIdentifier: string;
}): any {
return {
id: 'alias-id',
aliasName,
url: 'https://warp-nexus.expo.app',
workerDeployment: {
id: 'worker-deployment-id',
url: `https://warp-nexus--${deploymentIdentifier}.expo.app`,
deploymentIdentifier,
deploymentDomain: 'warp-nexus',
createdAt: '2026-09-11T00:00:00.000Z',
},
};
}

describe(assignWorkerDeploymentProductionAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('returns the alias when production was moved to the requested deployment', async () => {
const graphqlClient = {} as ExpoGraphqlClient;
const alias = createAliasResult({ aliasName: null, deploymentIdentifier: 'y3iw49p3qz' });
jest.mocked(DeploymentsMutation.assignAliasAsync).mockResolvedValueOnce(alias);

await expect(
assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: 'test-app-id',
deploymentId: 'y3iw49p3qz',
})
).resolves.toBe(alias);
});

it('throws when production still points at the previous deployment', async () => {
// Repros expo/eas-cli#4388: the mutation resolves, so the command printed
// "Promoted deployment to production" and exited 0 while production kept
// serving the previous bundle.
const graphqlClient = {} as ExpoGraphqlClient;
jest
.mocked(DeploymentsMutation.assignAliasAsync)
.mockResolvedValueOnce(
createAliasResult({ aliasName: null, deploymentIdentifier: 'c0547b2ed2' })
);

await expect(
assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: 'test-app-id',
deploymentId: 'y3iw49p3qz',
})
).rejects.toThrow(
'Production was not moved to deployment "y3iw49p3qz" and still points to "c0547b2ed2".'
);
});
});

describe(assignWorkerDeploymentAliasAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('returns the alias when it was moved to the requested deployment', async () => {
const graphqlClient = {} as ExpoGraphqlClient;
const alias = createAliasResult({ aliasName: 'staging', deploymentIdentifier: 'y3iw49p3qz' });
jest.mocked(DeploymentsMutation.assignAliasAsync).mockResolvedValueOnce(alias);

await expect(
assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: 'test-app-id',
deploymentId: 'y3iw49p3qz',
aliasName: 'staging',
})
).resolves.toBe(alias);
});

it('names the alias when it still points at the previous deployment', async () => {
const graphqlClient = {} as ExpoGraphqlClient;
jest
.mocked(DeploymentsMutation.assignAliasAsync)
.mockResolvedValueOnce(
createAliasResult({ aliasName: 'staging', deploymentIdentifier: 'c0547b2ed2' })
);

await expect(
assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: 'test-app-id',
deploymentId: 'y3iw49p3qz',
aliasName: 'staging',
})
).rejects.toThrow('Alias "staging" was not moved to deployment "y3iw49p3qz"');
});
});
31 changes: 29 additions & 2 deletions packages/eas-cli/src/worker/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,13 @@ export async function assignWorkerDeploymentAliasAsync({
deploymentId: string;
aliasName: string;
}): ReturnType<typeof DeploymentsMutation.assignAliasAsync> {
return await DeploymentsMutation.assignAliasAsync(graphqlClient, {
const alias = await DeploymentsMutation.assignAliasAsync(graphqlClient, {
appId,
deploymentId,
aliasName,
});
assertAliasPointsToDeployment(alias, deploymentId);
return alias;
}

export async function assignWorkerDeploymentProductionAsync({
Expand All @@ -266,11 +268,36 @@ export async function assignWorkerDeploymentProductionAsync({
appId: string;
deploymentId: string;
}): ReturnType<typeof DeploymentsMutation.assignAliasAsync> {
return await DeploymentsMutation.assignAliasAsync(graphqlClient, {
const alias = await DeploymentsMutation.assignAliasAsync(graphqlClient, {
appId,
deploymentId,
aliasName: null, // this will assign the deployment as production
});
assertAliasPointsToDeployment(alias, deploymentId);
return alias;
}

/**
* The mutation resolves with the alias as it now stands, which is not necessarily the deployment
* that was requested. Without this check the caller reports success and exits 0 while the alias
* still resolves to the previous deployment, which is what expo/eas-cli#4388 describes for
* `--prod`. `assignDevDomainNameAsync` already compares its own mutation's result against what it
* asked for; this does the same for aliases.
*/
function assertAliasPointsToDeployment(
alias: Awaited<ReturnType<typeof DeploymentsMutation.assignAliasAsync>>,
requestedDeploymentId: string
): void {
const assignedDeploymentId = alias.workerDeployment.deploymentIdentifier;
if (String(assignedDeploymentId) === String(requestedDeploymentId)) {
return;
}
const target = alias.aliasName ? `Alias "${alias.aliasName}"` : 'Production';
throw new Error(
`${target} was not moved to deployment "${requestedDeploymentId}" and still points to ` +
`"${assignedDeploymentId}". The deployment itself was created and is reachable at its own ` +
`URL, so retrying the command is safe.`
);
}

export async function selectWorkerDeploymentOnAppAsync({
Expand Down
Loading