eoas 1.0.38 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/commands/init.js +11 -0
- package/dist/commands/publish.d.ts +5 -5
- package/dist/commands/publish.js +167 -71
- package/dist/commands/republish.d.ts +12 -0
- package/dist/commands/republish.js +152 -0
- package/dist/commands/rollback.d.ts +13 -0
- package/dist/commands/rollback.js +166 -0
- package/dist/lib/assets.d.ts +15 -4
- package/dist/lib/assets.js +16 -7
- package/dist/lib/expoConfig.d.ts +3 -0
- package/dist/lib/expoConfig.js +33 -3
- package/dist/lib/fetch.d.ts +2 -0
- package/dist/lib/fetch.js +27 -0
- package/dist/lib/packageRunner.d.ts +12 -0
- package/dist/lib/packageRunner.js +72 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# EOAS (Expo
|
|
1
|
+
# EOAS (Expo Open Application Services)
|
|
2
2
|
|
|
3
|
-
EOAS (Expo
|
|
3
|
+
EOAS ((Expo Open Application Services) is a powerful helper package designed to simplify the setup and update publication process for the [expo-open-ota](https://github.com/axelmarciano/expo-open-ota) project.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
package/dist/commands/init.js
CHANGED
|
@@ -27,6 +27,16 @@ class Init extends core_1.Command {
|
|
|
27
27
|
log_1.default.error('Could not find Expo config in this project. Please make sure you have an Expo config.');
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
+
const detectedAppId = config.extra?.eas
|
|
31
|
+
?.projectId;
|
|
32
|
+
const { appId } = await (0, prompts_1.promptAsync)({
|
|
33
|
+
message: 'Enter the Expo project id for this project (sent as the expo-app-id header).\n' +
|
|
34
|
+
' See https://axelmarciano.github.io/expo-open-ota/docs/getting-started/prerequisites for details.',
|
|
35
|
+
name: 'appId',
|
|
36
|
+
type: 'text',
|
|
37
|
+
initial: detectedAppId,
|
|
38
|
+
validate: v => !!v,
|
|
39
|
+
});
|
|
30
40
|
const { updateUrl: promptedUrl } = await (0, prompts_1.promptAsync)({
|
|
31
41
|
message: 'Enter the URL of your update server (ex: https://customota.com)',
|
|
32
42
|
name: 'updateUrl',
|
|
@@ -95,6 +105,7 @@ class Init extends core_1.Command {
|
|
|
95
105
|
enabled: true,
|
|
96
106
|
requestHeaders: {
|
|
97
107
|
'expo-channel-name': 'process.env.RELEASE_CHANNEL',
|
|
108
|
+
'expo-app-id': appId,
|
|
98
109
|
},
|
|
99
110
|
};
|
|
100
111
|
const updateConfigSpinner = (0, ora_1.ora)('Updating Expo config').start();
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { Config } from '@oclif/core/lib/config';
|
|
3
|
-
import { Client } from '../lib/vcs/vcs';
|
|
4
2
|
export default class Publish extends Command {
|
|
5
|
-
vcsClient: Client;
|
|
6
|
-
constructor(argv: string[], config: Config);
|
|
7
3
|
static args: {};
|
|
8
4
|
static description: string;
|
|
9
5
|
static examples: string[];
|
|
10
6
|
static flags: {
|
|
11
7
|
platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
-
channel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
channel: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
disableRepositoryCheck: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
10
|
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
11
|
nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
outputDir: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
packageRunner: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
message: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
15
|
};
|
|
16
16
|
private sanitizeFlags;
|
|
17
17
|
run(): Promise<void>;
|
package/dist/commands/publish.js
CHANGED
|
@@ -7,25 +7,21 @@ const core_1 = require("@oclif/core");
|
|
|
7
7
|
const form_data_1 = tslib_1.__importDefault(require("form-data"));
|
|
8
8
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
9
9
|
const mime_1 = tslib_1.__importDefault(require("mime"));
|
|
10
|
-
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
11
10
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
12
11
|
const assets_1 = require("../lib/assets");
|
|
13
12
|
const auth_1 = require("../lib/auth");
|
|
14
13
|
const expoConfig_1 = require("../lib/expoConfig");
|
|
14
|
+
const fetch_1 = require("../lib/fetch");
|
|
15
15
|
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
16
16
|
const ora_1 = require("../lib/ora");
|
|
17
17
|
const package_1 = require("../lib/package");
|
|
18
|
+
const packageRunner_1 = require("../lib/packageRunner");
|
|
18
19
|
const prompts_1 = require("../lib/prompts");
|
|
19
20
|
const repo_1 = require("../lib/repo");
|
|
20
21
|
const runtimeVersion_1 = require("../lib/runtimeVersion");
|
|
21
22
|
const vcs_1 = require("../lib/vcs");
|
|
22
23
|
const workflow_1 = require("../lib/workflow");
|
|
23
24
|
class Publish extends core_1.Command {
|
|
24
|
-
vcsClient;
|
|
25
|
-
constructor(argv, config) {
|
|
26
|
-
super(argv, config);
|
|
27
|
-
this.vcsClient = (0, vcs_1.resolveVcsClient)(false);
|
|
28
|
-
}
|
|
29
25
|
static args = {};
|
|
30
26
|
static description = 'Publish a new update to the self-hosted update server';
|
|
31
27
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
@@ -38,7 +34,15 @@ class Publish extends core_1.Command {
|
|
|
38
34
|
}),
|
|
39
35
|
channel: core_1.Flags.string({
|
|
40
36
|
description: 'Name of the channel to publish the update to',
|
|
41
|
-
required:
|
|
37
|
+
required: false,
|
|
38
|
+
deprecated: {
|
|
39
|
+
message: 'Channel was initially used to provide RELEASE_CHANNEL in the environment when resolving the runtime version. It is no longer needed, you can use RELEASE_CHANNEL={channel} eoas publish --branch={branch} instead',
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
disableRepositoryCheck: core_1.Flags.boolean({
|
|
43
|
+
description: 'Disable repository check (Useful for CI/CD)',
|
|
44
|
+
default: false,
|
|
45
|
+
hidden: true,
|
|
42
46
|
}),
|
|
43
47
|
branch: core_1.Flags.string({
|
|
44
48
|
description: 'Name of the branch to point to',
|
|
@@ -48,13 +52,30 @@ class Publish extends core_1.Command {
|
|
|
48
52
|
description: 'Run command in non-interactive mode',
|
|
49
53
|
default: false,
|
|
50
54
|
}),
|
|
55
|
+
outputDir: core_1.Flags.string({
|
|
56
|
+
description: "Where to write build output. You can override the default dist output directory if it's being used by something else",
|
|
57
|
+
default: 'dist',
|
|
58
|
+
}),
|
|
59
|
+
packageRunner: core_1.Flags.string({
|
|
60
|
+
description: 'Package runner to use for spawning Expo CLI commands (e.g. npx, bunx, pnpx). Can also be set via EOAS_PACKAGE_RUNNER env var. Defaults to npx.',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
63
|
+
message: core_1.Flags.string({
|
|
64
|
+
char: 'm',
|
|
65
|
+
description: 'A short message describing the update. Defaults to the latest git commit message.',
|
|
66
|
+
required: false,
|
|
67
|
+
}),
|
|
51
68
|
};
|
|
52
69
|
sanitizeFlags(flags) {
|
|
53
70
|
return {
|
|
71
|
+
disableRepositoryCheck: flags.disableRepositoryCheck,
|
|
54
72
|
platform: flags.platform,
|
|
55
73
|
branch: flags.branch,
|
|
56
74
|
nonInteractive: flags.nonInteractive,
|
|
57
|
-
|
|
75
|
+
outputDir: flags.outputDir,
|
|
76
|
+
packageRunner: (0, packageRunner_1.resolvePackageRunner)(flags.packageRunner, process.cwd()),
|
|
77
|
+
providedDeprecatedChannel: flags.channel,
|
|
78
|
+
message: flags.message,
|
|
58
79
|
};
|
|
59
80
|
}
|
|
60
81
|
async run() {
|
|
@@ -64,45 +85,36 @@ class Publish extends core_1.Command {
|
|
|
64
85
|
process.exit(1);
|
|
65
86
|
}
|
|
66
87
|
const { flags } = await this.parse(Publish);
|
|
67
|
-
const { platform, nonInteractive, branch,
|
|
88
|
+
const { platform, nonInteractive, branch, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, } = this.sanitizeFlags(flags);
|
|
68
89
|
if (!branch) {
|
|
69
90
|
log_1.default.error('Branch name is required');
|
|
70
91
|
process.exit(1);
|
|
71
92
|
}
|
|
72
|
-
if (!channel) {
|
|
73
|
-
log_1.default.error('Channel name is required');
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
await this.vcsClient.ensureRepoExistsAsync();
|
|
77
|
-
await (0, repo_1.ensureRepoIsCleanAsync)(this.vcsClient, nonInteractive);
|
|
78
93
|
const projectDir = process.cwd();
|
|
79
94
|
const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
|
|
80
95
|
if (!hasExpo) {
|
|
81
96
|
log_1.default.error('Expo is not installed in this project. Please install Expo first.');
|
|
82
97
|
process.exit(1);
|
|
83
98
|
}
|
|
84
|
-
const
|
|
99
|
+
const vcsClient = (0, vcs_1.resolveVcsClient)(true);
|
|
100
|
+
if (!disableRepositoryCheck) {
|
|
101
|
+
await (0, repo_1.ensureRepoIsCleanAsync)(vcsClient, nonInteractive);
|
|
102
|
+
}
|
|
103
|
+
const config = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
|
|
85
104
|
env: {
|
|
86
|
-
|
|
105
|
+
...process.env,
|
|
106
|
+
...(providedDeprecatedChannel ? { RELEASE_CHANNEL: providedDeprecatedChannel } : {}),
|
|
87
107
|
},
|
|
108
|
+
packageRunner,
|
|
88
109
|
});
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
let baseUrl;
|
|
95
|
-
try {
|
|
96
|
-
const parsedUrl = new URL(updateUrl);
|
|
97
|
-
baseUrl = parsedUrl.origin;
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
log_1.default.error('Invalid URL', e);
|
|
110
|
+
const serverUrl = await (0, expoConfig_1.resolveServerUrl)(config).catch(e => {
|
|
111
|
+
log_1.default.error(e.message);
|
|
101
112
|
process.exit(1);
|
|
102
|
-
}
|
|
113
|
+
});
|
|
114
|
+
const appId = (0, expoConfig_1.requireExpoAppId)(config);
|
|
103
115
|
if (!nonInteractive) {
|
|
104
116
|
const confirmed = await (0, prompts_1.confirmAsync)({
|
|
105
|
-
message: `Is this the correct URL of your self-hosted update server? ${
|
|
117
|
+
message: `Is this the correct URL of your self-hosted update server? ${serverUrl}`,
|
|
106
118
|
name: 'export',
|
|
107
119
|
type: 'confirm',
|
|
108
120
|
});
|
|
@@ -111,45 +123,72 @@ class Publish extends core_1.Command {
|
|
|
111
123
|
process.exit(1);
|
|
112
124
|
}
|
|
113
125
|
}
|
|
126
|
+
const commitHash = await vcsClient.getCommitHashAsync();
|
|
127
|
+
let resolvedMessage = message;
|
|
128
|
+
if (!resolvedMessage && vcsClient.canGetLastCommitMessage()) {
|
|
129
|
+
resolvedMessage = (await vcsClient.getLastCommitMessageAsync()) ?? undefined;
|
|
130
|
+
}
|
|
114
131
|
const runtimeSpinner = (0, ora_1.ora)('🔄 Resolving runtime version...').start();
|
|
115
132
|
const runtimeVersions = [
|
|
116
133
|
...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Ios
|
|
117
134
|
? [
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
{
|
|
136
|
+
runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
|
|
137
|
+
exp: config,
|
|
138
|
+
platform: 'ios',
|
|
139
|
+
workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.IOS, vcsClient),
|
|
140
|
+
projectDir,
|
|
141
|
+
env: {
|
|
142
|
+
...process.env,
|
|
143
|
+
...(providedDeprecatedChannel
|
|
144
|
+
? { RELEASE_CHANNEL: providedDeprecatedChannel }
|
|
145
|
+
: {}),
|
|
146
|
+
},
|
|
147
|
+
}))?.runtimeVersion,
|
|
120
148
|
platform: 'ios',
|
|
121
|
-
|
|
122
|
-
projectDir,
|
|
123
|
-
env: {
|
|
124
|
-
RELEASE_CHANNEL: channel,
|
|
125
|
-
},
|
|
126
|
-
}))?.runtimeVersion,
|
|
149
|
+
},
|
|
127
150
|
]
|
|
128
151
|
: []),
|
|
129
152
|
...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Android
|
|
130
153
|
? [
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
{
|
|
155
|
+
runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
|
|
156
|
+
exp: config,
|
|
157
|
+
platform: 'android',
|
|
158
|
+
workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.ANDROID, vcsClient),
|
|
159
|
+
projectDir,
|
|
160
|
+
env: {
|
|
161
|
+
...process.env,
|
|
162
|
+
...(providedDeprecatedChannel
|
|
163
|
+
? { RELEASE_CHANNEL: providedDeprecatedChannel }
|
|
164
|
+
: {}),
|
|
165
|
+
},
|
|
166
|
+
}))?.runtimeVersion,
|
|
133
167
|
platform: 'android',
|
|
134
|
-
|
|
135
|
-
projectDir,
|
|
136
|
-
env: {
|
|
137
|
-
RELEASE_CHANNEL: channel,
|
|
138
|
-
},
|
|
139
|
-
}))?.runtimeVersion,
|
|
168
|
+
},
|
|
140
169
|
]
|
|
141
170
|
: []),
|
|
142
|
-
].filter(
|
|
171
|
+
].filter(({ runtimeVersion }) => !!runtimeVersion);
|
|
143
172
|
if (!runtimeVersions.length) {
|
|
144
173
|
runtimeSpinner.fail('Could not resolve runtime versions for the requested platforms');
|
|
145
174
|
log_1.default.error('Could not resolve runtime versions for the requested platforms');
|
|
146
175
|
process.exit(1);
|
|
147
176
|
}
|
|
148
177
|
runtimeSpinner.succeed('✅ Runtime versions resolved');
|
|
178
|
+
const cleaningSpinner = (0, ora_1.ora)(`🗑️ Cleaning up ${outputDir} directory...`).start();
|
|
179
|
+
try {
|
|
180
|
+
await fs_extra_1.default.remove(path_1.default.join(projectDir, outputDir));
|
|
181
|
+
cleaningSpinner.succeed('✅ Cleanup completed');
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
cleaningSpinner.fail('❌ Failed to clean up the output directory');
|
|
185
|
+
log_1.default.error(e);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
149
188
|
const exportSpinner = (0, ora_1.ora)('📦 Exporting project files...').start();
|
|
150
189
|
try {
|
|
151
|
-
|
|
152
|
-
const { stdout } = await (0, spawn_async_1.default)(
|
|
190
|
+
const specifiedPlatform = platform === expoConfig_1.RequestedPlatform.All ? [] : ['--platform', platform];
|
|
191
|
+
const { stdout } = await (0, spawn_async_1.default)(packageRunner, ['expo', 'export', '--output-dir', outputDir, ...specifiedPlatform], {
|
|
153
192
|
cwd: projectDir,
|
|
154
193
|
env: {
|
|
155
194
|
...process.env,
|
|
@@ -159,51 +198,65 @@ class Publish extends core_1.Command {
|
|
|
159
198
|
exportSpinner.succeed('🚀 Project exported successfully');
|
|
160
199
|
log_1.default.withInfo(stdout);
|
|
161
200
|
}
|
|
162
|
-
catch {
|
|
163
|
-
exportSpinner.fail(
|
|
201
|
+
catch (e) {
|
|
202
|
+
exportSpinner.fail(`❌ Failed to export the project, ${e}`);
|
|
164
203
|
process.exit(1);
|
|
165
204
|
}
|
|
166
205
|
const publicConfig = await (0, expoConfig_1.getPublicExpoConfigAsync)(projectDir, {
|
|
167
206
|
skipSDKVersionRequirement: true,
|
|
207
|
+
packageRunner,
|
|
168
208
|
});
|
|
169
209
|
if (!publicConfig) {
|
|
170
210
|
log_1.default.error('Could not find Expo config in this project. Please make sure you have an Expo config.');
|
|
171
211
|
process.exit(1);
|
|
172
212
|
}
|
|
173
213
|
// eslint-disable-next-line
|
|
174
|
-
fs_extra_1.default.writeJsonSync(path_1.default.join(projectDir,
|
|
214
|
+
fs_extra_1.default.writeJsonSync(path_1.default.join(projectDir, outputDir, 'expoConfig.json'), publicConfig, {
|
|
175
215
|
spaces: 2,
|
|
176
216
|
});
|
|
177
|
-
log_1.default.withInfo(
|
|
217
|
+
log_1.default.withInfo(`expoConfig.json file created in ${outputDir} directory`);
|
|
178
218
|
const uploadFilesSpinner = (0, ora_1.ora)('📤 Uploading files...').start();
|
|
179
|
-
const files = (0, assets_1.computeFilesRequests)(projectDir, platform || expoConfig_1.RequestedPlatform.All);
|
|
219
|
+
const files = (0, assets_1.computeFilesRequests)(projectDir, outputDir, platform || expoConfig_1.RequestedPlatform.All);
|
|
180
220
|
if (!files.length) {
|
|
181
221
|
uploadFilesSpinner.fail('No files to upload');
|
|
182
222
|
process.exit(1);
|
|
183
223
|
}
|
|
224
|
+
let uploadUrls = [];
|
|
184
225
|
try {
|
|
185
|
-
|
|
226
|
+
uploadUrls = await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
|
|
186
227
|
if (!runtimeVersion) {
|
|
187
228
|
throw new Error('Runtime version is not resolved');
|
|
188
229
|
}
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
return {
|
|
231
|
+
...(await (0, assets_1.requestUploadUrls)({
|
|
232
|
+
body: {
|
|
233
|
+
fileNames: files.map(file => file.path),
|
|
234
|
+
},
|
|
235
|
+
requestUploadUrl: `${serverUrl}/${appId}/requestUploadUrl/${branch}`,
|
|
236
|
+
auth: credentials,
|
|
237
|
+
runtimeVersion,
|
|
238
|
+
platform,
|
|
239
|
+
commitHash,
|
|
240
|
+
message: resolvedMessage,
|
|
241
|
+
})),
|
|
242
|
+
runtimeVersion,
|
|
243
|
+
platform,
|
|
244
|
+
};
|
|
192
245
|
}));
|
|
193
|
-
const allItems = uploadUrls.
|
|
246
|
+
const allItems = uploadUrls.flatMap(({ uploadRequests }) => uploadRequests);
|
|
194
247
|
await Promise.all(allItems.map(async (itm) => {
|
|
195
|
-
const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${
|
|
248
|
+
const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/${appId}/uploadLocalFile`);
|
|
196
249
|
const formData = new form_data_1.default();
|
|
197
250
|
let file;
|
|
198
251
|
try {
|
|
199
|
-
file = fs_extra_1.default.createReadStream(path_1.default.join(projectDir,
|
|
252
|
+
file = fs_extra_1.default.createReadStream(path_1.default.join(projectDir, outputDir, itm.filePath));
|
|
200
253
|
}
|
|
201
254
|
catch {
|
|
202
255
|
throw new Error(`Failed to read file ${itm.filePath}`);
|
|
203
256
|
}
|
|
204
257
|
formData.append(itm.fileName, file);
|
|
205
258
|
if (isLocalBucketFileUpload) {
|
|
206
|
-
const response = await (0,
|
|
259
|
+
const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
|
|
207
260
|
method: 'PUT',
|
|
208
261
|
headers: {
|
|
209
262
|
...formData.getHeaders(),
|
|
@@ -227,8 +280,8 @@ class Publish extends core_1.Command {
|
|
|
227
280
|
if (!contentType) {
|
|
228
281
|
contentType = 'application/octet-stream';
|
|
229
282
|
}
|
|
230
|
-
const buffer = await fs_extra_1.default.readFile(path_1.default.join(projectDir,
|
|
231
|
-
const response = await (0,
|
|
283
|
+
const buffer = await fs_extra_1.default.readFile(path_1.default.join(projectDir, outputDir, itm.filePath));
|
|
284
|
+
const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
|
|
232
285
|
method: 'PUT',
|
|
233
286
|
headers: {
|
|
234
287
|
'Content-Type': contentType,
|
|
@@ -244,15 +297,58 @@ class Publish extends core_1.Command {
|
|
|
244
297
|
}));
|
|
245
298
|
uploadFilesSpinner.succeed('✅ Files uploaded successfully');
|
|
246
299
|
}
|
|
247
|
-
catch {
|
|
300
|
+
catch (e) {
|
|
248
301
|
uploadFilesSpinner.fail('❌ Failed to upload static files');
|
|
302
|
+
log_1.default.error(e);
|
|
249
303
|
process.exit(1);
|
|
250
304
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
305
|
+
const markAsFinishedSpinner = (0, ora_1.ora)('🔗 Marking the updates as finished...').start();
|
|
306
|
+
const results = await Promise.all(uploadUrls.map(async ({ updateId, platform, runtimeVersion }) => {
|
|
307
|
+
const markAsUploadedUrl = new URL(`${serverUrl}/${appId}/markUpdateAsUploaded/${branch}`);
|
|
308
|
+
markAsUploadedUrl.searchParams.set('platform', platform);
|
|
309
|
+
markAsUploadedUrl.searchParams.set('updateId', updateId);
|
|
310
|
+
markAsUploadedUrl.searchParams.set('runtimeVersion', runtimeVersion);
|
|
311
|
+
const response = await (0, fetch_1.fetchWithRetries)(markAsUploadedUrl.toString(), {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: {
|
|
314
|
+
...(0, auth_1.getAuthExpoHeaders)(credentials),
|
|
315
|
+
'Content-Type': 'application/json',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
// If success and status code = 200
|
|
319
|
+
if (response.ok) {
|
|
320
|
+
log_1.default.withInfo(`✅ Update ready for ${platform}`);
|
|
321
|
+
return 'deployed';
|
|
322
|
+
}
|
|
323
|
+
// If response.status === 406 duplicate update
|
|
324
|
+
if (response.status === 406) {
|
|
325
|
+
log_1.default.withInfo(`⚠️ There is no change in the update for ${platform}, ignored...`);
|
|
326
|
+
return 'identical';
|
|
327
|
+
}
|
|
328
|
+
log_1.default.error('❌ Failed to mark the update as finished for platform', platform);
|
|
329
|
+
log_1.default.newLine();
|
|
330
|
+
log_1.default.error(await response.text());
|
|
331
|
+
return 'error';
|
|
332
|
+
}));
|
|
333
|
+
const erroredUpdates = results.filter(result => result === 'error');
|
|
334
|
+
const hasSuccess = results.some(result => result === 'deployed');
|
|
335
|
+
const allIdentical = results.every(result => result === 'identical');
|
|
336
|
+
if (allIdentical) {
|
|
337
|
+
markAsFinishedSpinner.warn('⚠️ No changes found in the update, nothing to deploy');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (erroredUpdates.length) {
|
|
341
|
+
markAsFinishedSpinner.fail('❌ Some errors occurred while marking updates as finished');
|
|
342
|
+
throw new Error();
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
markAsFinishedSpinner.succeed(`\n✅ Your update has been successfully pushed to ${serverUrl}`);
|
|
346
|
+
}
|
|
347
|
+
if (hasSuccess) {
|
|
348
|
+
log_1.default.withInfo(`🌿 Branch: \`${branch}\``);
|
|
349
|
+
log_1.default.withInfo(`⏳ Deployed at: \`${new Date().toUTCString()}\`\n`);
|
|
350
|
+
log_1.default.withInfo('🔥 Your users will receive the latest update automatically!');
|
|
351
|
+
}
|
|
256
352
|
}
|
|
257
353
|
}
|
|
258
354
|
exports.default = Publish;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Publish extends Command {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
private sanitizeFlags;
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const ora_1 = tslib_1.__importDefault(require("ora"));
|
|
6
|
+
const auth_1 = require("../lib/auth");
|
|
7
|
+
const expoConfig_1 = require("../lib/expoConfig");
|
|
8
|
+
const fetch_1 = require("../lib/fetch");
|
|
9
|
+
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
10
|
+
const package_1 = require("../lib/package");
|
|
11
|
+
const prompts_1 = require("../lib/prompts");
|
|
12
|
+
const vcs_1 = require("../lib/vcs");
|
|
13
|
+
class Publish extends core_1.Command {
|
|
14
|
+
static args = {};
|
|
15
|
+
static description = 'Republish a previous update to a branch';
|
|
16
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
17
|
+
static flags = {
|
|
18
|
+
branch: core_1.Flags.string({
|
|
19
|
+
description: 'Name of the branch to point to',
|
|
20
|
+
required: true,
|
|
21
|
+
}),
|
|
22
|
+
platform: core_1.Flags.string({
|
|
23
|
+
type: 'option',
|
|
24
|
+
options: ['ios', 'android'],
|
|
25
|
+
default: 'all',
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
sanitizeFlags(flags) {
|
|
30
|
+
return {
|
|
31
|
+
branch: flags.branch,
|
|
32
|
+
platform: flags.platform,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async run() {
|
|
36
|
+
const credentials = (0, auth_1.retrieveExpoCredentials)();
|
|
37
|
+
if (!credentials.token && !credentials.sessionSecret) {
|
|
38
|
+
log_1.default.error('You are not logged to eas, please run `eas login`');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const { flags } = await this.parse(Publish);
|
|
42
|
+
const { branch, platform } = this.sanitizeFlags(flags);
|
|
43
|
+
if (!branch) {
|
|
44
|
+
log_1.default.error('Branch name is required');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (!platform) {
|
|
48
|
+
log_1.default.error('Platform is required');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const vcsClient = (0, vcs_1.resolveVcsClient)(true);
|
|
52
|
+
await vcsClient.ensureRepoExistsAsync();
|
|
53
|
+
// const commitHash = await vcsClient.getCommitHashAsync();
|
|
54
|
+
const projectDir = process.cwd();
|
|
55
|
+
const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
|
|
56
|
+
if (!hasExpo) {
|
|
57
|
+
log_1.default.error('Expo is not installed in this project. Please install Expo first.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
|
|
61
|
+
env: process.env,
|
|
62
|
+
});
|
|
63
|
+
const updateUrl = (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
|
|
64
|
+
if (!updateUrl) {
|
|
65
|
+
log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
|
|
69
|
+
let baseUrl;
|
|
70
|
+
try {
|
|
71
|
+
const parsedUrl = new URL(updateUrl);
|
|
72
|
+
baseUrl = parsedUrl.origin;
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
log_1.default.error('Invalid URL', e);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const runtimeVersionsEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersions`;
|
|
79
|
+
const response = await (0, fetch_1.fetchWithRetries)(runtimeVersionsEndpoint, {
|
|
80
|
+
headers: {
|
|
81
|
+
...(0, auth_1.getAuthExpoHeaders)(credentials),
|
|
82
|
+
'use-expo-auth': 'true',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
log_1.default.error(`Failed to fetch runtime versions: ${await response.text()}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const runtimeVersions = (await response.json());
|
|
90
|
+
const filteredRuntimeVersions = runtimeVersions.filter(runtimeVersion => runtimeVersion.numberOfUpdates > 1);
|
|
91
|
+
if (filteredRuntimeVersions.length === 0) {
|
|
92
|
+
log_1.default.error('No runtime versions found');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// Ask the user to select a runtime version
|
|
96
|
+
const selectedRuntimeVersion = await (0, prompts_1.promptAsync)({
|
|
97
|
+
type: 'select',
|
|
98
|
+
name: 'runtimeVersion',
|
|
99
|
+
message: 'Select a runtime version',
|
|
100
|
+
choices: filteredRuntimeVersions.map(runtimeVersion => ({
|
|
101
|
+
title: runtimeVersion.runtimeVersion,
|
|
102
|
+
value: runtimeVersion.runtimeVersion,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
log_1.default.log(`Selected runtime version: ${selectedRuntimeVersion.runtimeVersion}`);
|
|
106
|
+
const updatesEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersion/${selectedRuntimeVersion.runtimeVersion}/updates`;
|
|
107
|
+
const updatesResponse = await (0, fetch_1.fetchWithRetries)(updatesEndpoint, {
|
|
108
|
+
headers: {
|
|
109
|
+
...(0, auth_1.getAuthExpoHeaders)(credentials),
|
|
110
|
+
'use-expo-auth': 'true',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (!updatesResponse.ok) {
|
|
114
|
+
log_1.default.error(`Failed to fetch updates: ${await updatesResponse.text()}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const updates = (await updatesResponse.json()).filter(u => {
|
|
118
|
+
return u.updateUUID !== 'Rollback to embedded' && u.platform === platform;
|
|
119
|
+
});
|
|
120
|
+
const selectedUpdated = await (0, prompts_1.promptAsync)({
|
|
121
|
+
type: 'select',
|
|
122
|
+
name: 'update',
|
|
123
|
+
message: 'Select an update to republish',
|
|
124
|
+
choices: updates.map(update => ({
|
|
125
|
+
title: update.updateUUID,
|
|
126
|
+
value: update,
|
|
127
|
+
description: `Created at: ${update.createdAt}, Platform: ${update.platform}, Commit hash: ${update.commitHash}`,
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
log_1.default.log(`Re-publishing update: ${selectedUpdated.update.updateUUID}`);
|
|
131
|
+
const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
|
|
132
|
+
republishUrl.searchParams.set('platform', platform);
|
|
133
|
+
republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
|
|
134
|
+
republishUrl.searchParams.set('updateId', selectedUpdated.update.updateId);
|
|
135
|
+
republishUrl.searchParams.set('commitHash', selectedUpdated.update.commitHash);
|
|
136
|
+
const republishSpinner = (0, ora_1.default)('🔄 Republishing update...').start();
|
|
137
|
+
const republishResponse = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
...(0, auth_1.getAuthExpoHeaders)(credentials),
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (!republishResponse.ok) {
|
|
145
|
+
republishSpinner.fail('❌ Republish failed');
|
|
146
|
+
log_1.default.error(`Failed to republish update: ${await republishResponse.text()}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
republishSpinner.succeed('✅ Republish successful');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.default = Publish;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Publish extends Command {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
private sanitizeFlags;
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
|
+
const core_1 = require("@oclif/core");
|
|
6
|
+
const auth_1 = require("../lib/auth");
|
|
7
|
+
const expoConfig_1 = require("../lib/expoConfig");
|
|
8
|
+
const fetch_1 = require("../lib/fetch");
|
|
9
|
+
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
10
|
+
const ora_1 = require("../lib/ora");
|
|
11
|
+
const package_1 = require("../lib/package");
|
|
12
|
+
const prompts_1 = require("../lib/prompts");
|
|
13
|
+
const runtimeVersion_1 = require("../lib/runtimeVersion");
|
|
14
|
+
const vcs_1 = require("../lib/vcs");
|
|
15
|
+
const workflow_1 = require("../lib/workflow");
|
|
16
|
+
class Publish extends core_1.Command {
|
|
17
|
+
static args = {};
|
|
18
|
+
static description = 'Publish a new rollback to the self-hosted update server';
|
|
19
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
20
|
+
static flags = {
|
|
21
|
+
platform: core_1.Flags.string({
|
|
22
|
+
type: 'option',
|
|
23
|
+
options: Object.values(expoConfig_1.RequestedPlatform),
|
|
24
|
+
default: expoConfig_1.RequestedPlatform.All,
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
branch: core_1.Flags.string({
|
|
28
|
+
description: 'Name of the branch to point to',
|
|
29
|
+
required: true,
|
|
30
|
+
}),
|
|
31
|
+
nonInteractive: core_1.Flags.boolean({
|
|
32
|
+
description: 'Run command in non-interactive mode',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
sanitizeFlags(flags) {
|
|
37
|
+
return {
|
|
38
|
+
platform: flags.platform,
|
|
39
|
+
branch: flags.branch,
|
|
40
|
+
nonInteractive: flags.nonInteractive,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async run() {
|
|
44
|
+
const credentials = (0, auth_1.retrieveExpoCredentials)();
|
|
45
|
+
if (!credentials.token && !credentials.sessionSecret) {
|
|
46
|
+
log_1.default.error('You are not logged to eas, please run `eas login`');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const { flags } = await this.parse(Publish);
|
|
50
|
+
const { platform, branch, nonInteractive } = this.sanitizeFlags(flags);
|
|
51
|
+
if (!branch) {
|
|
52
|
+
log_1.default.error('Branch name is required');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const vcsClient = (0, vcs_1.resolveVcsClient)(true);
|
|
56
|
+
await vcsClient.ensureRepoExistsAsync();
|
|
57
|
+
const commitHash = await vcsClient.getCommitHashAsync();
|
|
58
|
+
const projectDir = process.cwd();
|
|
59
|
+
const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
|
|
60
|
+
if (!hasExpo) {
|
|
61
|
+
log_1.default.error('Expo is not installed in this project. Please install Expo first.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!nonInteractive) {
|
|
65
|
+
const confirmed = await (0, prompts_1.confirmAsync)({
|
|
66
|
+
message: `Are you sure you want to publish a rollback to the branch ${branch} ?`,
|
|
67
|
+
name: 'export',
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
});
|
|
70
|
+
if (!confirmed) {
|
|
71
|
+
log_1.default.error('Operation cancelled');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
|
|
76
|
+
env: process.env,
|
|
77
|
+
});
|
|
78
|
+
if (privateConfig?.updates?.disableAntiBrickingMeasures) {
|
|
79
|
+
log_1.default.error('When using disableAntiBrickingMeasures, expo-updates is ignoring the embeded update of the app, please use republish command instead');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const updateUrl = (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
|
|
83
|
+
if (!updateUrl) {
|
|
84
|
+
log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
|
|
88
|
+
let baseUrl;
|
|
89
|
+
try {
|
|
90
|
+
const parsedUrl = new URL(updateUrl);
|
|
91
|
+
baseUrl = parsedUrl.origin;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
log_1.default.error('Invalid URL', e);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const runtimeSpinner = (0, ora_1.ora)('🔄 Resolving runtime version...').start();
|
|
98
|
+
const runtimeVersions = [
|
|
99
|
+
...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Ios
|
|
100
|
+
? [
|
|
101
|
+
{
|
|
102
|
+
runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
|
|
103
|
+
exp: privateConfig,
|
|
104
|
+
platform: 'ios',
|
|
105
|
+
workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.IOS, vcsClient),
|
|
106
|
+
projectDir,
|
|
107
|
+
env: process.env,
|
|
108
|
+
}))?.runtimeVersion,
|
|
109
|
+
platform: 'ios',
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
: []),
|
|
113
|
+
...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Android
|
|
114
|
+
? [
|
|
115
|
+
{
|
|
116
|
+
runtimeVersion: (await (0, runtimeVersion_1.resolveRuntimeVersionAsync)({
|
|
117
|
+
exp: privateConfig,
|
|
118
|
+
platform: 'android',
|
|
119
|
+
workflow: await (0, workflow_1.resolveWorkflowAsync)(projectDir, eas_build_job_1.Platform.ANDROID, vcsClient),
|
|
120
|
+
projectDir,
|
|
121
|
+
env: process.env,
|
|
122
|
+
}))?.runtimeVersion,
|
|
123
|
+
platform: 'android',
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
: []),
|
|
127
|
+
].filter(({ runtimeVersion }) => !!runtimeVersion);
|
|
128
|
+
if (!runtimeVersions.length) {
|
|
129
|
+
runtimeSpinner.fail('Could not resolve runtime versions for the requested platforms');
|
|
130
|
+
log_1.default.error('Could not resolve runtime versions for the requested platforms');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
runtimeSpinner.succeed('✅ Runtime versions resolved');
|
|
134
|
+
const rollbackSpinner = (0, ora_1.ora)('📦 Uploading rollback...').start();
|
|
135
|
+
const erroredPlatforms = [];
|
|
136
|
+
await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
|
|
137
|
+
const rollbackUrl = new URL(`${baseUrl}/${appId}/rollback/${branch}`);
|
|
138
|
+
rollbackUrl.searchParams.set('commitHash', commitHash ?? '');
|
|
139
|
+
rollbackUrl.searchParams.set('platform', platform);
|
|
140
|
+
rollbackUrl.searchParams.set('runtimeVersion', runtimeVersion ?? '');
|
|
141
|
+
const response = await (0, fetch_1.fetchWithRetries)(rollbackUrl.toString(), {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
...(0, auth_1.getAuthExpoHeaders)(credentials),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
erroredPlatforms.push({
|
|
149
|
+
platform,
|
|
150
|
+
reason: await response.text(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
if (erroredPlatforms.length) {
|
|
155
|
+
rollbackSpinner.fail('❌ Rollback failed');
|
|
156
|
+
erroredPlatforms.forEach(({ platform, reason }) => {
|
|
157
|
+
log_1.default.error(`Failed to publish rollback for ${platform}: ${reason}`);
|
|
158
|
+
});
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
rollbackSpinner.succeed('✅ Rollback published successfully');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.default = Publish;
|
package/dist/lib/assets.d.ts
CHANGED
|
@@ -7,13 +7,24 @@ interface AssetToUpload {
|
|
|
7
7
|
name: string;
|
|
8
8
|
ext: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function computeFilesRequests(projectDir: string, requestedPlatform: RequestedPlatform): AssetToUpload[];
|
|
10
|
+
export declare function computeFilesRequests(projectDir: string, outputDir: string, requestedPlatform: RequestedPlatform): AssetToUpload[];
|
|
11
11
|
export interface RequestUploadUrlItem {
|
|
12
12
|
requestUploadUrl: string;
|
|
13
13
|
fileName: string;
|
|
14
14
|
filePath: string;
|
|
15
15
|
}
|
|
16
|
-
export declare function requestUploadUrls(body: {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, }: {
|
|
17
|
+
body: {
|
|
18
|
+
fileNames: string[];
|
|
19
|
+
};
|
|
20
|
+
requestUploadUrl: string;
|
|
21
|
+
auth: ExpoCredentials;
|
|
22
|
+
runtimeVersion: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
commitHash?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
uploadRequests: RequestUploadUrlItem[];
|
|
28
|
+
updateId: string;
|
|
29
|
+
}>;
|
|
19
30
|
export {};
|
package/dist/lib/assets.js
CHANGED
|
@@ -4,10 +4,10 @@ exports.requestUploadUrls = exports.computeFilesRequests = exports.MetadataJoi =
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
6
6
|
const joi_1 = tslib_1.__importDefault(require("joi"));
|
|
7
|
-
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
8
7
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
8
|
const auth_1 = require("./auth");
|
|
10
9
|
const expoConfig_1 = require("./expoConfig");
|
|
10
|
+
const fetch_1 = require("./fetch");
|
|
11
11
|
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
12
12
|
const fileMetadataJoi = joi_1.default.object({
|
|
13
13
|
assets: joi_1.default.array()
|
|
@@ -54,8 +54,8 @@ function loadMetadata(distRoot) {
|
|
|
54
54
|
log_1.default.debug(`Loaded ${platforms.length} platform(s): ${platforms.join(', ')}`);
|
|
55
55
|
return metadata;
|
|
56
56
|
}
|
|
57
|
-
function computeFilesRequests(projectDir, requestedPlatform) {
|
|
58
|
-
const metadata = loadMetadata(path_1.default.join(projectDir,
|
|
57
|
+
function computeFilesRequests(projectDir, outputDir, requestedPlatform) {
|
|
58
|
+
const metadata = loadMetadata(path_1.default.join(projectDir, outputDir));
|
|
59
59
|
const assets = [
|
|
60
60
|
{ path: 'metadata.json', name: 'metadata.json', ext: 'json' },
|
|
61
61
|
{ path: 'expoConfig.json', name: 'expoConfig.json', ext: 'json' },
|
|
@@ -73,17 +73,26 @@ function computeFilesRequests(projectDir, requestedPlatform) {
|
|
|
73
73
|
return assets;
|
|
74
74
|
}
|
|
75
75
|
exports.computeFilesRequests = computeFilesRequests;
|
|
76
|
-
async function requestUploadUrls(body, requestUploadUrl, auth, runtimeVersion) {
|
|
77
|
-
const
|
|
76
|
+
async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, }) {
|
|
77
|
+
const uploadUrl = new URL(requestUploadUrl);
|
|
78
|
+
uploadUrl.searchParams.set('runtimeVersion', runtimeVersion);
|
|
79
|
+
uploadUrl.searchParams.set('platform', platform);
|
|
80
|
+
uploadUrl.searchParams.set('commitHash', commitHash ?? '');
|
|
81
|
+
const requestBody = { ...body };
|
|
82
|
+
if (message) {
|
|
83
|
+
requestBody.message = message;
|
|
84
|
+
}
|
|
85
|
+
const response = await (0, fetch_1.fetchWithRetries)(uploadUrl.toString(), {
|
|
78
86
|
method: 'POST',
|
|
79
87
|
headers: {
|
|
80
88
|
...(0, auth_1.getAuthExpoHeaders)(auth),
|
|
81
89
|
'Content-Type': 'application/json',
|
|
82
90
|
},
|
|
83
|
-
body: JSON.stringify(
|
|
91
|
+
body: JSON.stringify(requestBody),
|
|
84
92
|
});
|
|
85
93
|
if (!response.ok) {
|
|
86
|
-
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
throw new Error(`Failed to request upload URL: ${text}`);
|
|
87
96
|
}
|
|
88
97
|
return await response.json();
|
|
89
98
|
}
|
package/dist/lib/expoConfig.d.ts
CHANGED
|
@@ -14,10 +14,13 @@ export interface ExpoConfigOptions {
|
|
|
14
14
|
env?: Env;
|
|
15
15
|
skipSDKVersionRequirement?: boolean;
|
|
16
16
|
skipPlugins?: boolean;
|
|
17
|
+
packageRunner?: string;
|
|
17
18
|
}
|
|
18
19
|
export declare function getPrivateExpoConfigAsync(projectDir: string, opts?: ExpoConfigOptions): Promise<ExpoConfig>;
|
|
19
20
|
export declare function ensureExpoConfigExists(projectDir: string): void;
|
|
20
21
|
export declare function isUsingStaticExpoConfig(projectDir: string): boolean;
|
|
21
22
|
export declare function getPublicExpoConfigAsync(projectDir: string, opts?: ExpoConfigOptions): Promise<PublicExpoConfig>;
|
|
22
23
|
export declare function getExpoConfigUpdateUrl(config: ExpoConfig): string | undefined;
|
|
24
|
+
export declare function requireExpoAppId(config: ExpoConfig): string;
|
|
23
25
|
export declare function createOrModifyExpoConfigAsync(projectDir: string, exp: Partial<ExpoConfig>): Promise<void>;
|
|
26
|
+
export declare function resolveServerUrl(config: ExpoConfig): Promise<string>;
|
package/dist/lib/expoConfig.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createOrModifyExpoConfigAsync = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
|
|
3
|
+
exports.resolveServerUrl = exports.createOrModifyExpoConfigAsync = exports.requireExpoAppId = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
// This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
|
|
6
6
|
const config_1 = require("@expo/config");
|
|
@@ -11,6 +11,7 @@ const jscodeshift_1 = tslib_1.__importDefault(require("jscodeshift"));
|
|
|
11
11
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
12
12
|
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
13
13
|
const package_1 = require("./package");
|
|
14
|
+
const packageRunner_1 = require("./packageRunner");
|
|
14
15
|
var RequestedPlatform;
|
|
15
16
|
(function (RequestedPlatform) {
|
|
16
17
|
RequestedPlatform["Android"] = "android";
|
|
@@ -27,8 +28,9 @@ async function getExpoConfigInternalAsync(projectDir, opts = {}) {
|
|
|
27
28
|
};
|
|
28
29
|
let exp;
|
|
29
30
|
if ((0, package_1.isExpoInstalled)(projectDir)) {
|
|
31
|
+
const runner = (0, packageRunner_1.resolvePackageRunner)(opts.packageRunner, projectDir);
|
|
30
32
|
try {
|
|
31
|
-
const { stdout } = await (0, spawn_async_1.default)(
|
|
33
|
+
const { stdout } = await (0, spawn_async_1.default)(runner, ['expo', 'config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])], {
|
|
32
34
|
cwd: projectDir,
|
|
33
35
|
env: {
|
|
34
36
|
...process.env,
|
|
@@ -40,7 +42,7 @@ async function getExpoConfigInternalAsync(projectDir, opts = {}) {
|
|
|
40
42
|
}
|
|
41
43
|
catch (err) {
|
|
42
44
|
if (!wasExpoConfigWarnPrinted) {
|
|
43
|
-
log_1.default.warn(`Failed to read the app config from the project using "
|
|
45
|
+
log_1.default.warn(`Failed to read the app config from the project using "${runner} expo config" command: ${err.message}.`);
|
|
44
46
|
log_1.default.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.');
|
|
45
47
|
wasExpoConfigWarnPrinted = true;
|
|
46
48
|
}
|
|
@@ -109,6 +111,18 @@ function getExpoConfigUpdateUrl(config) {
|
|
|
109
111
|
return config.updates?.url;
|
|
110
112
|
}
|
|
111
113
|
exports.getExpoConfigUpdateUrl = getExpoConfigUpdateUrl;
|
|
114
|
+
function requireExpoAppId(config) {
|
|
115
|
+
const appId = config.updates
|
|
116
|
+
?.requestHeaders?.['expo-app-id'];
|
|
117
|
+
if (!appId) {
|
|
118
|
+
log_1.default.error("Your Expo config is missing the 'expo-app-id' entry in updates.requestHeaders.");
|
|
119
|
+
log_1.default.error("This usually means you're running eoas v2+ against a v1-style single-app config or your config is missing the 'expo-app-id' entry.");
|
|
120
|
+
log_1.default.error("Fix: run 'npx eoas init' to migrate, or pin to the previous CLI via 'npx eoas@1 ...'.");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
return appId;
|
|
124
|
+
}
|
|
125
|
+
exports.requireExpoAppId = requireExpoAppId;
|
|
112
126
|
async function createOrModifyExpoConfigAsync(projectDir, exp) {
|
|
113
127
|
try {
|
|
114
128
|
ensureExpoConfigExists(projectDir);
|
|
@@ -196,3 +210,19 @@ function createValueNode(j, value) {
|
|
|
196
210
|
function stringifyWithEnv(obj) {
|
|
197
211
|
return JSON.stringify(obj, null, 2).replace(/"process\.env\.(\w+)"/g, 'process.env.$1');
|
|
198
212
|
}
|
|
213
|
+
async function resolveServerUrl(config) {
|
|
214
|
+
const updateUrl = config.updates?.url;
|
|
215
|
+
if (!updateUrl) {
|
|
216
|
+
throw new Error('No update URL found in the Expo config.');
|
|
217
|
+
}
|
|
218
|
+
let baseUrl;
|
|
219
|
+
try {
|
|
220
|
+
const parsedUrl = new URL(updateUrl);
|
|
221
|
+
baseUrl = parsedUrl.origin;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error('Invalid update URL.');
|
|
225
|
+
}
|
|
226
|
+
return baseUrl;
|
|
227
|
+
}
|
|
228
|
+
exports.resolveServerUrl = resolveServerUrl;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchWithRetries = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fetch_retry_1 = tslib_1.__importDefault(require("fetch-retry"));
|
|
6
|
+
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
7
|
+
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
8
|
+
const fetch = (0, fetch_retry_1.default)(node_fetch_1.default);
|
|
9
|
+
async function fetchWithRetries(url, options) {
|
|
10
|
+
return await fetch(url, {
|
|
11
|
+
...options,
|
|
12
|
+
retryDelay(attempt) {
|
|
13
|
+
return Math.pow(2, attempt) * 500;
|
|
14
|
+
},
|
|
15
|
+
retryOn: (attempt, error) => {
|
|
16
|
+
if (attempt > 3) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (error) {
|
|
20
|
+
log_1.default.warn(`Retry ${attempt} after network error:`, error.message);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.fetchWithRetries = fetchWithRetries;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the package runner command to use for spawning Expo CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. Explicit value passed as argument (e.g. from --packageRunner CLI flag)
|
|
6
|
+
* 2. EOAS_PACKAGE_RUNNER environment variable
|
|
7
|
+
* 3. Inferred from packageManager field in package.json
|
|
8
|
+
* 4. Falls back to 'npx'
|
|
9
|
+
*
|
|
10
|
+
* Supported values: npx, bunx, pnpx, or any other package runner binary.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolvePackageRunner(explicit?: string, projectDir?: string): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePackageRunner = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
6
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
7
|
+
const DEFAULT_PACKAGE_RUNNER = 'npx';
|
|
8
|
+
const VALID_RUNNER_RE = /^[a-zA-Z0-9._-]+$/;
|
|
9
|
+
function assertValidRunner(value, source) {
|
|
10
|
+
if (!VALID_RUNNER_RE.test(value)) {
|
|
11
|
+
throw new Error(`Invalid package runner "${value}" (from ${source}). Expected a simple binary name like npx, bunx or pnpx.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const PACKAGE_MANAGER_RUNNERS = {
|
|
15
|
+
bun: 'bunx',
|
|
16
|
+
pnpm: 'pnpx',
|
|
17
|
+
yarn: 'npx',
|
|
18
|
+
npm: 'npx',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the package runner command to use for spawning Expo CLI commands.
|
|
22
|
+
*
|
|
23
|
+
* Priority:
|
|
24
|
+
* 1. Explicit value passed as argument (e.g. from --packageRunner CLI flag)
|
|
25
|
+
* 2. EOAS_PACKAGE_RUNNER environment variable
|
|
26
|
+
* 3. Inferred from packageManager field in package.json
|
|
27
|
+
* 4. Falls back to 'npx'
|
|
28
|
+
*
|
|
29
|
+
* Supported values: npx, bunx, pnpx, or any other package runner binary.
|
|
30
|
+
*/
|
|
31
|
+
function resolvePackageRunner(explicit, projectDir) {
|
|
32
|
+
if (explicit) {
|
|
33
|
+
assertValidRunner(explicit, '--packageRunner flag');
|
|
34
|
+
return explicit;
|
|
35
|
+
}
|
|
36
|
+
if (process.env.EOAS_PACKAGE_RUNNER) {
|
|
37
|
+
assertValidRunner(process.env.EOAS_PACKAGE_RUNNER, 'EOAS_PACKAGE_RUNNER environment variable');
|
|
38
|
+
return process.env.EOAS_PACKAGE_RUNNER;
|
|
39
|
+
}
|
|
40
|
+
if (projectDir) {
|
|
41
|
+
const detected = detectRunnerFromPackageJson(projectDir);
|
|
42
|
+
if (detected)
|
|
43
|
+
return detected;
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_PACKAGE_RUNNER;
|
|
46
|
+
}
|
|
47
|
+
exports.resolvePackageRunner = resolvePackageRunner;
|
|
48
|
+
/**
|
|
49
|
+
* Walks up from projectDir to find a package.json with a packageManager field
|
|
50
|
+
* and maps it to the corresponding package runner binary.
|
|
51
|
+
*/
|
|
52
|
+
function detectRunnerFromPackageJson(startDir) {
|
|
53
|
+
let dir = path_1.default.resolve(startDir);
|
|
54
|
+
const root = path_1.default.parse(dir).root;
|
|
55
|
+
while (dir !== root) {
|
|
56
|
+
const pkgPath = path_1.default.join(dir, 'package.json');
|
|
57
|
+
try {
|
|
58
|
+
if (fs_extra_1.default.existsSync(pkgPath)) {
|
|
59
|
+
const pkg = fs_extra_1.default.readJsonSync(pkgPath);
|
|
60
|
+
if (pkg.packageManager) {
|
|
61
|
+
const name = pkg.packageManager.split('@')[0];
|
|
62
|
+
return PACKAGE_MANAGER_RUNNERS[name];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore read errors, keep walking up
|
|
68
|
+
}
|
|
69
|
+
dir = path_1.default.dirname(dir);
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eoas",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.1",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc --project tsconfig.json",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"repository": "axelmarciano/expo-open-ota",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@expo/code-signing-certificates": "^0.0.5",
|
|
26
|
-
"@expo/config": "10.0.
|
|
26
|
+
"@expo/config": "10.0.11",
|
|
27
27
|
"@expo/config-plugins": "9.0.12",
|
|
28
28
|
"@expo/eas-build-job": "1.0.165",
|
|
29
29
|
"@expo/fingerprint": "^0.11.7",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"@urql/exchange-retry": "1.2.0",
|
|
36
36
|
"better-opn": "3.0.2",
|
|
37
37
|
"chalk": "4.1.2",
|
|
38
|
+
"eslint": "^8.57.1",
|
|
38
39
|
"fast-glob": "3.3.2",
|
|
40
|
+
"fetch-retry": "^6.0.0",
|
|
39
41
|
"figures": "3.2.0",
|
|
40
42
|
"file-type": "^20.0.0",
|
|
41
43
|
"form-data": "^4.0.1",
|
|
@@ -47,19 +49,18 @@
|
|
|
47
49
|
"ignore": "5.3.0",
|
|
48
50
|
"joi": "17.11.0",
|
|
49
51
|
"jscodeshift": "^17.1.2",
|
|
52
|
+
"log-symbols": "^4.0.0",
|
|
50
53
|
"mime": "3.0.0",
|
|
51
54
|
"node-fetch": "^2.6.7",
|
|
52
55
|
"ora": "^5.1.0",
|
|
56
|
+
"prettier": "3.1.1",
|
|
53
57
|
"prompts": "^2.4.2",
|
|
54
58
|
"recast": "^0.23.9",
|
|
55
59
|
"resolve-from": "5.0.0",
|
|
56
60
|
"semver": "7.5.4",
|
|
57
61
|
"tar": "6.2.1",
|
|
58
62
|
"terminal-link": "2.1.1",
|
|
59
|
-
"uuid": "9.0.1"
|
|
60
|
-
"eslint": "^8.57.1",
|
|
61
|
-
"prettier": "3.1.1",
|
|
62
|
-
"log-symbols": "^4.0.0"
|
|
63
|
+
"uuid": "9.0.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@babel/parser": "^7.26.7",
|