Skip to content

Commit 6912e45

Browse files
authored
Improvements to workflow:create (#3194)
# Why The existing `workflow:create` command simply creates a very bare-bones workflow, that is not very useful to developers who need to know how to fully use workflows for managing builds, updates, and other EAS operations. # How - Define four different workflow templates, initialized as JSON objects. - After the user selects which type of workflow (build, update, deploy, custom), sanity checks are made, EAS build and EAS update are configured if needed, and any missing build profiles are created in `eas.json`. - Once the user has made their selections, the final workflow YAML is dynamically generated and written to disk with an explanatory header. - After writing the workflow, next steps are provided for any further configuration that may be needed, especially if build credentials or app store credentials are needed. In future, we can easily add more templates that use other workflow job types (fingerprint, repack, etc.). # Test Plan - Tested against different projects - CI should pass
1 parent 839cbac commit 6912e45

File tree

8 files changed

+1058
-82
lines changed

8 files changed

+1058
-82
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.
88

99
### 🎉 New features
1010

11+
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
12+
1113
### 🐛 Bug fixes
1214

1315
### 🧹 Chores

packages/eas-cli/graphql.schema.json

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { ExpoConfig } from '@expo/config';
2+
import { BuildProfile, EasJsonAccessor, EasJsonUtils, Platform } from '@expo/eas-json';
3+
import { AndroidBuildProfile, IosBuildProfile } from '@expo/eas-json/build/build/types';
4+
5+
import BuildConfigure from '../../commands/build/configure';
6+
import UpdateConfigure from '../../commands/update/configure';
7+
import Log from '../../log';
8+
import { promptAsync } from '../../prompts';
9+
10+
export async function buildProfileNamesFromProjectAsync(projectDir: string): Promise<Set<string>> {
11+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
12+
13+
const buildProfileNames = new Set(
14+
easJsonAccessor && (await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor))
15+
);
16+
return buildProfileNames;
17+
}
18+
export async function getBuildProfileAsync(
19+
projectDir: string,
20+
platform: Platform,
21+
profileName: string
22+
): Promise<BuildProfile<Platform>> {
23+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
24+
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
25+
easJsonAccessor,
26+
platform,
27+
profileName
28+
);
29+
return buildProfile;
30+
}
31+
export async function buildProfilesFromProjectAsync(
32+
projectDir: string
33+
): Promise<Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }>> {
34+
const buildProfileNames = await buildProfileNamesFromProjectAsync(projectDir);
35+
const buildProfiles: Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }> =
36+
new Map();
37+
for (const profileName of buildProfileNames) {
38+
const iosBuildProfile = (await getBuildProfileAsync(
39+
projectDir,
40+
Platform.IOS,
41+
profileName
42+
)) as IosBuildProfile;
43+
const androidBuildProfile = (await getBuildProfileAsync(
44+
projectDir,
45+
Platform.ANDROID,
46+
profileName
47+
)) as AndroidBuildProfile;
48+
buildProfiles.set(profileName, { android: androidBuildProfile, ios: iosBuildProfile });
49+
}
50+
return buildProfiles;
51+
}
52+
export function isBuildProfileForDevelopment(
53+
buildProfile: BuildProfile<Platform>,
54+
platform: Platform
55+
): boolean {
56+
if (buildProfile.developmentClient) {
57+
return true;
58+
}
59+
if (platform === Platform.ANDROID) {
60+
return (buildProfile as BuildProfile<Platform.ANDROID>).gradleCommand === ':app:assembleDebug';
61+
}
62+
if (platform === Platform.IOS) {
63+
return (buildProfile as BuildProfile<Platform.IOS>).buildConfiguration === 'Debug';
64+
}
65+
return false;
66+
}
67+
export function isIosBuildProfileForSimulator(buildProfile: BuildProfile<Platform.IOS>): boolean {
68+
return buildProfile.simulator ?? false;
69+
}
70+
export async function addAndroidDevelopmentBuildProfileToEasJsonAsync(
71+
projectDir: string,
72+
buildProfileName: string
73+
): Promise<void> {
74+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
75+
await easJsonAccessor.readRawJsonAsync();
76+
easJsonAccessor.patch(easJsonRawObject => {
77+
easJsonRawObject.build = {
78+
...easJsonRawObject.build,
79+
[buildProfileName]: {
80+
developmentClient: true,
81+
distribution: 'internal',
82+
android: {
83+
gradleCommand: ':app:assembleDebug',
84+
},
85+
},
86+
};
87+
return easJsonRawObject;
88+
});
89+
await easJsonAccessor.writeAsync();
90+
}
91+
export async function addIosDevelopmentBuildProfileToEasJsonAsync(
92+
projectDir: string,
93+
buildProfileName: string,
94+
simulator: boolean
95+
): Promise<void> {
96+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
97+
await easJsonAccessor.readRawJsonAsync();
98+
easJsonAccessor.patch(easJsonRawObject => {
99+
easJsonRawObject.build = {
100+
...easJsonRawObject.build,
101+
[buildProfileName]: {
102+
developmentClient: true,
103+
distribution: 'internal',
104+
ios: {
105+
buildConfiguration: 'Debug',
106+
simulator,
107+
},
108+
},
109+
};
110+
return easJsonRawObject;
111+
});
112+
await easJsonAccessor.writeAsync();
113+
}
114+
115+
export async function addProductionBuildProfileToEasJsonIfNeededAsync(
116+
projectDir: string
117+
): Promise<boolean> {
118+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
119+
await easJsonAccessor.readRawJsonAsync();
120+
let profileAdded = false;
121+
easJsonAccessor.patch(easJsonRawObject => {
122+
if (!easJsonRawObject.build?.production) {
123+
profileAdded = true;
124+
easJsonRawObject.build = {
125+
...(easJsonRawObject.build ?? {}),
126+
production: {},
127+
};
128+
// Also add the profile to submit
129+
easJsonRawObject.submit = {
130+
...(easJsonRawObject.submit ?? {}),
131+
production: {},
132+
};
133+
}
134+
return easJsonRawObject;
135+
});
136+
if (profileAdded) {
137+
Log.log('Added missing production build profile to eas.json');
138+
}
139+
await easJsonAccessor.writeAsync();
140+
return profileAdded;
141+
}
142+
143+
export async function hasBuildConfigureBeenRunAsync({
144+
projectDir,
145+
expoConfig,
146+
}: {
147+
projectDir: string;
148+
expoConfig: ExpoConfig;
149+
}): Promise<boolean> {
150+
// Is there a project ID in the Expo config?
151+
if (!expoConfig.extra?.eas?.projectId) {
152+
return false;
153+
}
154+
// Is there an eas.json?
155+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
156+
try {
157+
await easJsonAccessor.readAsync();
158+
} catch {
159+
return false;
160+
}
161+
return true;
162+
}
163+
164+
export async function hasUpdateConfigureBeenRunAsync({
165+
projectDir,
166+
expoConfig,
167+
}: {
168+
projectDir: string;
169+
expoConfig: ExpoConfig;
170+
}): Promise<boolean> {
171+
// Does the Expo config have an updates URL?
172+
if (!expoConfig.updates?.url) {
173+
return false;
174+
}
175+
// Does at least one build profile have a channel?
176+
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
177+
try {
178+
const easJson = await easJsonAccessor.readAsync();
179+
return Object.values(easJson.build ?? {}).some(buildProfile => !!buildProfile.channel);
180+
} catch {
181+
return false;
182+
}
183+
}
184+
185+
/**
186+
* Runs update:configure if needed. Returns a boolean (proceed with workflow creation, or not)
187+
*/
188+
189+
export async function runUpdateConfigureIfNeededAsync({
190+
projectDir,
191+
expoConfig,
192+
}: {
193+
projectDir: string;
194+
expoConfig: ExpoConfig;
195+
}): Promise<boolean> {
196+
if (
197+
await hasUpdateConfigureBeenRunAsync({
198+
projectDir,
199+
expoConfig,
200+
})
201+
) {
202+
return true;
203+
}
204+
const nextStep = (
205+
await promptAsync({
206+
type: 'select',
207+
name: 'nextStep',
208+
message:
209+
'You have chosen to create a workflow that requires EAS Update configuration. What would you like to do?',
210+
choices: [
211+
{ title: 'Configure EAS Update and then proceed', value: 'configure' },
212+
{ title: 'EAS Update is already configured, proceed', value: 'proceed' },
213+
{ title: 'Choose a different workflow template', value: 'repeat' },
214+
],
215+
})
216+
).nextStep;
217+
switch (nextStep) {
218+
case 'configure':
219+
Log.newLine();
220+
await UpdateConfigure.run([]);
221+
return true;
222+
case 'proceed':
223+
return true;
224+
default:
225+
return false;
226+
}
227+
}
228+
/**
229+
* Runs build:configure if needed. Returns a boolean (proceed with workflow creation, or not)
230+
*/
231+
export async function runBuildConfigureIfNeededAsync({
232+
projectDir,
233+
expoConfig,
234+
}: {
235+
projectDir: string;
236+
expoConfig: ExpoConfig;
237+
}): Promise<boolean> {
238+
if (
239+
await hasBuildConfigureBeenRunAsync({
240+
projectDir,
241+
expoConfig,
242+
})
243+
) {
244+
return true;
245+
}
246+
const nextStep = (
247+
await promptAsync({
248+
type: 'select',
249+
name: 'nextStep',
250+
message:
251+
'You have chosen to create a workflow that requires EAS Build configuration. What would you like to do?',
252+
choices: [
253+
{ title: 'Configure EAS Build and then proceed', value: 'configure' },
254+
{ title: 'EAS Build is already configured, proceed', value: 'proceed' },
255+
{ title: 'Choose a different workflow template', value: 'repeat' },
256+
],
257+
})
258+
).nextStep;
259+
switch (nextStep) {
260+
case 'configure':
261+
Log.newLine();
262+
await BuildConfigure.run(['-p', 'all']);
263+
return true;
264+
case 'proceed':
265+
return true;
266+
default:
267+
return false;
268+
}
269+
}

0 commit comments

Comments
 (0)