Compare commits

..
3 Commits
Author SHA1 Message Date
Cedric van Putten 058183f202 v3.1.1 2022-12-19 16:57:33 +01:00
Cedric van Putten 7ee50de78e [eas-cli] Fix update --channel and --branch flag validation (#1579)
* [eas-cli] Add initial test cases for the update command

* [eas-cli] Add runtimeVersion option when mocking exports

* [eas-cli] Fix update flag validation for --branch and --channel

* [eas-cli] Add test case for --non-interactive, --channel, and --message

* update CHANGELOG.md

* [eas-cli] Fix linting issue

* [eas-cli] Reuse the input dir from the command

* [eas-cli] Explicitly mention the allowed flag combinations to avoid ambiguity

* [eas-cli] Move --auto to a distinct location in the error message

* [eas-cli] Fix linting issue
2022-12-19 16:36:18 +01:00
Expo CI ca11aa9e41 update CHANGELOG.md 2022-12-17 00:00:52 +00:00
8 changed files with 1471 additions and 8 deletions

No files matched your search

+10 -4
View File
@@ -8,14 +8,20 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Fix update `--channel` and `--branch` flag validation. ([#1579](https://github.com/expo/eas-cli/pull/1579) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
## [3.1.0](https://github.com/expo/eas-cli/releases/tag/v3.1.0) - 2022-12-17
### 🎉 New features
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
### 🧹 Chores
## [3.0.0](https://github.com/expo/eas-cli/releases/tag/v3.0.0) - 2022-12-07
### 🛠 Breaking changes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "3.1.0"
"version": "3.1.1"
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "3.1.0",
"version": "3.1.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -0,0 +1,248 @@
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import UpdatePublish from '..';
import { ensureBranchExistsAsync } from '../../../branch/queries';
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
...jest.requireActual('../../../project/publish'),
buildBundlesAsync: jest.fn(),
collectAssetsAsync: jest.fn(),
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
uploadAssetsAsync: jest.fn(),
}));
describe(UpdatePublish.name, () => {
afterEach(() => vol.reset());
// Deprecated and split to a new command: update:republish
it('errors with --republish', async () => {
await expect(new UpdatePublish(['--republish'], commandOptions).run()).rejects.toThrow(
'--group and --republish flags are deprecated'
);
});
// Deprecated and split to a new command: update:republish
it('errors with --group', async () => {
await expect(new UpdatePublish(['--group=abc123'], commandOptions).run()).rejects.toThrow(
'--group and --republish flags are deprecated'
);
});
it('errors with both --channel and --branch', async () => {
const flags = ['--channel=channel123', '--branch=branch123'];
mockTestProject();
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
});
it('creates a new update with --non-interactive, --branch, and --message', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
id: 'update123',
group: 'group123',
runtimeVersion,
platform,
manifestPermalink: 'https://example.com/update123/manifest',
}))
);
await new UpdatePublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a new update with --non-interactive, --channel, and --message', async () => {
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
const { projectId } = mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
id: 'update123',
group: 'group123',
runtimeVersion,
platform,
manifestPermalink: 'https://example.com/update123/manifest',
}))
);
await new UpdatePublish(flags, commandOptions).run();
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
{
appId: projectId,
branchName: 'branchFromChannel',
}
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
});
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
function mockTestProject({
configuredProjectId = '1234',
}: {
configuredProjectId?: string;
} = {}): { projectId: string } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => ({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
}));
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId };
}
/** Create a new in-memory export of the project */
function mockTestExport({
exportDir = 'dist',
platforms = ['android', 'ios'],
runtimeVersion = 'exposdk:47.0.0',
}: {
exportDir?: string;
platforms?: Platform[];
runtimeVersion?: string;
} = {}): {
inputDir: string;
platforms: Platform[];
runtimeVersion: string;
} {
/* eslint-disable node/no-sync */
vol.mkdirpSync(path.join(projectRoot, exportDir, 'bundles'));
for (const platform of platforms) {
vol.writeFileSync(
path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
`console.log("fake bundle for ${platform}");`
);
}
jest.mocked(collectAssetsAsync).mockResolvedValue(
Object.fromEntries(
platforms.map(platform => [
platform,
{
assets: [],
launchAsset: {
contentType: 'application/javascript',
path: path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
},
},
])
)
);
jest.mocked(uploadAssetsAsync).mockResolvedValue({
// platforms are mocked all containing only the launch asset
assetCount: platforms.length,
uniqueAssetCount: platforms.length,
uniqueUploadedAssetCount: platforms.length,
assetLimitPerUpdateGroup: 9001,
});
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
return { inputDir: exportDir, platforms, runtimeVersion };
}
@@ -48,6 +48,8 @@ jest.mock('../../../ora', () => ({
}));
describe(UpdateRepublish.name, () => {
afterEach(() => vol.reset());
it('errors without --branch or --group', async () => {
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
'--branch or --group must be specified'
@@ -509,9 +509,9 @@ export default class UpdatePublish extends EasCommand {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && channelName && updateMessage)) {
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--auto or both --channel or --branch and --message are required when updating in non-interactive mode',
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
{ exit: 1 }
);
}
+1
View File
@@ -31,6 +31,7 @@ export type RawAsset = {
contentType: string;
path: string;
};
type CollectedAssets = {
[platform in Platform]?: {
launchAsset: RawAsset;