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

### 🎉 New features

- [eas-cli] Add `--link-project-id` flag to `eas build` to link the directory to an existing project before building, like `eas init --id`. ([#4138](https://github.com/expo/eas-cli/pull/4138) by [@williamgrosset](https://github.com/williamgrosset))

### 🐛 Bug fixes

- [build-tools] Revert "Pin the default `agent-device` version for remote sessions" now that `agent-device` 0.20.5 fixes the broken release. ([#4144](https://github.com/expo/eas-cli/pull/4144) by [@gwdp](https://github.com/gwdp))
Expand Down
12 changes: 8 additions & 4 deletions packages/eas-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,10 @@ start a build

```
USAGE
$ eas build [-p android|ios|all] [-e PROFILE_NAME] [--local] [--output <value>] [--wait] [--clear-cache]
[-s | --auto-submit-with-profile PROFILE_NAME] [--what-to-test <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--refresh-ad-hoc-provisioning-profile] [--verbose-logs]
[--json] [--non-interactive]
$ eas build [-p android|ios|all] [-e PROFILE_NAME] [--force --link-project-id PROJECT_ID] [--local]
[--output <value>] [--wait] [--clear-cache] [-s | --auto-submit-with-profile PROFILE_NAME] [--what-to-test <value>]
[-m <value>] [--build-logger-level trace|debug|info|warn|error|fatal] [--freeze-credentials]
[--refresh-ad-hoc-provisioning-profile] [--verbose-logs] [--json] [--non-interactive]

FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if
Expand All @@ -551,9 +551,13 @@ FLAGS
--build-logger-level=<option> The level of logs to output during the build process. Defaults to "info".
<options: trace|debug|info|warn|error|fatal>
--clear-cache Clear cache before the build
--force Used with --link-project-id: overwrite an existing different project
link, "owner", or "slug" in your app config without prompting
--freeze-credentials Prevent the build from updating credentials in non-interactive mode
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies
--non-interactive.
--link-project-id=PROJECT_ID ID of an existing EAS project to link this directory to before building.
Writes "extra.eas.projectId" to your app config, like `eas init --id`.
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--output=<value> Output path for local build
Expand Down
148 changes: 148 additions & 0 deletions packages/eas-cli/src/commands/build/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
import { runBuildAndSubmitAsync } from '../../../build/runBuildAndSubmit';
import { linkExistingProjectByIdAsync } from '../../../project/projectInitialization';
import Build from '../index';

jest.mock('fs-extra', () => ({
pathExists: jest.fn().mockResolvedValue(false),
}));
jest.mock('../../../build/runBuildAndSubmit', () => ({
runBuildAndSubmitAsync: jest.fn(),
}));
jest.mock('../../../project/projectInitialization', () => ({
linkExistingProjectByIdAsync: jest.fn(),
}));
jest.mock('../../../utils/statuspageService', () => ({
maybeWarnAboutEasOutagesAsync: jest.fn(),
}));
jest.mock('../../../utils/json');
jest.mock('../../../log');

const VALID_UUID = '58b3e612-4d49-4de6-9dd4-0e5db1e0e6b4';

describe(Build, () => {
const mockConfig = getMockOclifConfig();
const graphqlClient = {};

beforeEach(() => {
jest.clearAllMocks();
jest.mocked(runBuildAndSubmitAsync).mockResolvedValue({ buildIds: [], buildProfiles: [] });
jest.mocked(linkExistingProjectByIdAsync).mockResolvedValue({
projectId: VALID_UUID,
status: 'linked',
owner: 'jester',
slug: 'testing-123',
});
});

function createCommand(argv: string[]): Build {
const command = new Build(argv, mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockResolvedValue({
loggedIn: {
actor: {},
graphqlClient,
},
getDynamicPrivateProjectConfigAsync: jest.fn(),
projectDir: '/project',
analytics: {},
vcsClient: {},
} as any);
return command;
}

it('links the project before starting the build when --link-project-id is passed', async () => {
await createCommand([
'--platform',
'ios',
'--non-interactive',
'--link-project-id',
VALID_UUID,
]).runAsync();

expect(linkExistingProjectByIdAsync).toHaveBeenCalledWith(graphqlClient, VALID_UUID, '/project', {
force: false,
nonInteractive: true,
});
const linkOrder = jest.mocked(linkExistingProjectByIdAsync).mock.invocationCallOrder[0];
const buildOrder = jest.mocked(runBuildAndSubmitAsync).mock.invocationCallOrder[0];
expect(linkOrder).toBeLessThan(buildOrder);
});

it('passes force: true when --force is set', async () => {
await createCommand([
'--platform',
'ios',
'--non-interactive',
'--link-project-id',
VALID_UUID,
'--force',
]).runAsync();

expect(linkExistingProjectByIdAsync).toHaveBeenCalledWith(graphqlClient, VALID_UUID, '/project', {
force: true,
nonInteractive: true,
});
});

it('does not link when the flag is not passed', async () => {
await createCommand(['--platform', 'ios', '--non-interactive']).runAsync();

expect(linkExistingProjectByIdAsync).not.toHaveBeenCalled();
expect(runBuildAndSubmitAsync).toHaveBeenCalled();
});

it('rejects --force without --link-project-id', async () => {
await expect(
createCommand(['--platform', 'ios', '--non-interactive', '--force']).runAsync()
).rejects.toThrow(/link-project-id/);

expect(runBuildAndSubmitAsync).not.toHaveBeenCalled();
});

it('rejects a --link-project-id value that is not a UUID', async () => {
await expect(
createCommand([
'--platform',
'ios',
'--non-interactive',
'--link-project-id',
'not-a-uuid',
]).runAsync()
).rejects.toThrow('must be a valid UUID');

expect(linkExistingProjectByIdAsync).not.toHaveBeenCalled();
expect(runBuildAndSubmitAsync).not.toHaveBeenCalled();
});

it('does not start a build when linking fails', async () => {
jest
.mocked(linkExistingProjectByIdAsync)
.mockRejectedValue(new Error('Failed to link project'));

await expect(
createCommand([
'--platform',
'ios',
'--non-interactive',
'--link-project-id',
VALID_UUID,
]).runAsync()
).rejects.toThrow('Failed to link project');

expect(runBuildAndSubmitAsync).not.toHaveBeenCalled();
});

it('works with --json', async () => {
await createCommand([
'--platform',
'ios',
'--non-interactive',
'--json',
'--link-project-id',
VALID_UUID,
]).runAsync();

expect(linkExistingProjectByIdAsync).toHaveBeenCalled();
expect(runBuildAndSubmitAsync).toHaveBeenCalled();
});
});
27 changes: 27 additions & 0 deletions packages/eas-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import * as uuid from 'uuid';

import { LocalBuildMode } from '../../build/local';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
Expand All @@ -17,6 +18,7 @@ import {
import { StatuspageServiceName } from '../../graphql/generated';
import Log, { link } from '../../log';
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
import { linkExistingProjectByIdAsync } from '../../project/projectInitialization';
import { selectAsync } from '../../prompts';
import uniq from '../../utils/expodash/uniq';
import { enableJsonOutput } from '../../utils/json';
Expand All @@ -28,6 +30,8 @@ interface RawBuildFlags {
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile?: string;
'link-project-id'?: string;
force?: boolean;
'non-interactive': boolean;
local: boolean;
output?: string;
Expand Down Expand Up @@ -67,6 +71,16 @@ export default class Build extends EasCommand {
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'link-project-id': Flags.string({
description:
'ID of an existing EAS project to link this directory to before building. Writes "extra.eas.projectId" to your app config, like `eas init --id`.',
helpValue: 'PROJECT_ID',
}),
force: Flags.boolean({
dependsOn: ['link-project-id'],
description:
'Used with --link-project-id: overwrite an existing different project link, "owner", or "slug" in your app config without prompting',
}),
local: Flags.boolean({
default: false,
description: 'Run build locally [experimental]',
Expand Down Expand Up @@ -161,6 +175,13 @@ export default class Build extends EasCommand {
withServerSideEnvironment: null,
});

if (rawFlags['link-project-id']) {
await linkExistingProjectByIdAsync(graphqlClient, rawFlags['link-project-id'], projectDir, {
force: rawFlags.force ?? false,
nonInteractive: flags.nonInteractive,
});
}

await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);

if (!flags.localBuildOptions.localBuildMode) {
Expand Down Expand Up @@ -214,6 +235,12 @@ export default class Build extends EasCommand {
if (!flags.platform && nonInteractive) {
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
if (flags['link-project-id'] && !uuid.validate(flags['link-project-id'])) {
Errors.error(
`--link-project-id must be a valid UUID. Received: ${flags['link-project-id']}`,
{ exit: 1 }
);
}
const autoSubmit = flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined;
if (flags['what-to-test'] && !autoSubmit) {
Errors.error(
Expand Down
Loading
Loading