@capgo/cli 4.11.5 → 4.12.0
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/CHANGELOG.md +11 -0
- package/dist/index.js +976 -31421
- package/package.json +1 -1
- package/src/bundle/upload.ts +287 -280
- package/src/index.ts +1 -0
- package/src/key.ts +5 -0
- package/src/utils.ts +36 -0
package/src/bundle/upload.ts
CHANGED
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import process from 'node:process'
|
|
4
3
|
import type { Buffer } from 'node:buffer'
|
|
5
|
-
import
|
|
6
|
-
import { program } from 'commander'
|
|
4
|
+
import process from 'node:process'
|
|
7
5
|
import * as p from '@clack/prompts'
|
|
6
|
+
import { program } from 'commander'
|
|
8
7
|
import { checksum as getChecksum } from '@tomasklaen/checksum'
|
|
9
8
|
import ciDetect from 'ci-info'
|
|
9
|
+
import type LogSnag from 'logsnag'
|
|
10
10
|
import ky, { HTTPError } from 'ky'
|
|
11
|
-
import {
|
|
12
|
-
PutObjectCommand,
|
|
13
|
-
S3Client,
|
|
14
|
-
} from '@aws-sdk/client-s3'
|
|
15
|
-
import { checkLatest } from '../api/update'
|
|
16
|
-
import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
|
|
17
11
|
import { encryptSource } from '../api/crypto'
|
|
18
|
-
import type
|
|
19
|
-
|
|
20
|
-
} from '../
|
|
21
|
-
import {
|
|
22
|
-
OrganizationPerm,
|
|
23
|
-
baseKeyPub,
|
|
24
|
-
checkCompatibility,
|
|
25
|
-
checkPlanValid,
|
|
26
|
-
convertAppName,
|
|
27
|
-
createSupabaseClient,
|
|
28
|
-
deletedFailedVersion,
|
|
29
|
-
findSavedKey,
|
|
30
|
-
formatError,
|
|
31
|
-
getConfig,
|
|
32
|
-
getLocalConfig,
|
|
33
|
-
getLocalDepenencies,
|
|
34
|
-
getOrganizationId,
|
|
35
|
-
getPMAndCommand,
|
|
36
|
-
hasOrganizationPerm,
|
|
37
|
-
regexSemver,
|
|
38
|
-
requireUpdateMetadata,
|
|
39
|
-
updateOrCreateChannel,
|
|
40
|
-
updateOrCreateVersion,
|
|
41
|
-
uploadMultipart,
|
|
42
|
-
uploadUrl,
|
|
43
|
-
useLogSnag,
|
|
44
|
-
verifyUser,
|
|
45
|
-
zipFile,
|
|
46
|
-
} from '../utils'
|
|
12
|
+
import { type OptionsBase, OrganizationPerm, baseKeyPub, checkChecksum, checkCompatibility, checkPlanValid, convertAppName, createSupabaseClient, deletedFailedVersion, findSavedKey, formatError, getConfig, getLocalConfig, getLocalDepenencies, getOrganizationId, getPMAndCommand, hasOrganizationPerm, regexSemver, updateOrCreateChannel, updateOrCreateVersion, uploadMultipart, uploadUrl, useLogSnag, verifyUser, zipFile } from '../utils'
|
|
13
|
+
import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
|
|
14
|
+
import { checkLatest } from '../api/update'
|
|
47
15
|
import { checkIndexPosition, searchInDirectory } from './check'
|
|
48
16
|
|
|
49
|
-
const alertMb = 20
|
|
50
|
-
|
|
51
17
|
interface Options extends OptionsBase {
|
|
52
18
|
bundle?: string
|
|
53
19
|
path?: string
|
|
@@ -66,73 +32,64 @@ interface Options extends OptionsBase {
|
|
|
66
32
|
minUpdateVersion?: string
|
|
67
33
|
autoMinUpdateVersion?: boolean
|
|
68
34
|
ignoreMetadataCheck?: boolean
|
|
35
|
+
ignoreChecksumCheck?: boolean
|
|
69
36
|
timeout?: number
|
|
70
37
|
multipart?: boolean
|
|
71
38
|
}
|
|
72
39
|
|
|
40
|
+
const alertMb = 20
|
|
73
41
|
const UPLOAD_TIMEOUT = 120000
|
|
74
42
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let { bundle, path, channel } = options
|
|
80
|
-
const { external, key, displayIvSession, autoMinUpdateVersion, ignoreMetadataCheck } = options
|
|
81
|
-
let { minUpdateVersion } = options
|
|
82
|
-
const { s3Region, s3Apikey, s3Apisecret, s3BucketName } = options
|
|
83
|
-
let useS3 = false
|
|
84
|
-
let s3Client
|
|
85
|
-
if (s3Region && s3Apikey && s3Apisecret && s3BucketName) {
|
|
86
|
-
p.log.info('Uploading to S3')
|
|
87
|
-
useS3 = true
|
|
88
|
-
s3Client = new S3Client({
|
|
89
|
-
region: s3Region,
|
|
90
|
-
credentials: {
|
|
91
|
-
accessKeyId: s3Apikey,
|
|
92
|
-
secretAccessKey: s3Apisecret,
|
|
93
|
-
},
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
options.apikey = options.apikey || findSavedKey()
|
|
98
|
-
const snag = useLogSnag()
|
|
99
|
-
|
|
100
|
-
channel = channel || 'dev'
|
|
101
|
-
|
|
102
|
-
const config = await getConfig()
|
|
43
|
+
type ConfigType = Awaited<ReturnType<typeof getConfig>>
|
|
44
|
+
type SupabaseType = Awaited<ReturnType<typeof createSupabaseClient>>
|
|
45
|
+
type pmType = ReturnType<typeof getPMAndCommand>
|
|
46
|
+
type localConfigType = Awaited<ReturnType<typeof getLocalConfig>>
|
|
103
47
|
|
|
104
|
-
|
|
105
|
-
appid = appid || config?.app?.appId
|
|
48
|
+
function getBundle(config: ConfigType, options: Options) {
|
|
106
49
|
// create bundle name format : 1.0.0-beta.x where x is a uuid
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
50
|
+
const bundle = options.bundle
|
|
51
|
+
|| config?.app?.extConfig?.plugins?.CapacitorUpdater?.version
|
|
52
|
+
|| config?.app?.package?.version
|
|
53
|
+
|| `0.0.1-beta.${randomUUID().split('-')[0]}`
|
|
54
|
+
|
|
110
55
|
if (!regexSemver.test(bundle)) {
|
|
111
56
|
p.log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
|
|
112
57
|
program.error('')
|
|
113
58
|
}
|
|
114
|
-
|
|
115
|
-
|
|
59
|
+
|
|
60
|
+
return bundle
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getApikey(options: Options) {
|
|
64
|
+
const apikey = options.apikey || findSavedKey()
|
|
65
|
+
if (!apikey) {
|
|
116
66
|
p.log.error(`Missing API key, you need to provide a API key to upload your bundle`)
|
|
117
67
|
program.error('')
|
|
118
68
|
}
|
|
119
|
-
|
|
69
|
+
|
|
70
|
+
return apikey
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getAppIdAndPath(appId: string | undefined, options: Options, config: ConfigType) {
|
|
74
|
+
const finalAppId = appId || config?.app?.appId
|
|
75
|
+
const path = options.path || config?.app?.webDir
|
|
76
|
+
|
|
77
|
+
if (!finalAppId || !path) {
|
|
120
78
|
p.log.error('Missing argument, you need to provide a appid and a path (--path), or be in a capacitor project')
|
|
121
79
|
program.error('')
|
|
122
80
|
}
|
|
123
|
-
|
|
124
|
-
if (s3BucketName || s3Region || s3Apikey || s3Apisecret) {
|
|
125
|
-
if (!s3BucketName || !s3Region || !s3Apikey || !s3Apisecret) {
|
|
126
|
-
p.log.error('Missing argument, for S3 upload you need to provide a bucket name, region, API key, and API secret')
|
|
127
|
-
program.error('')
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// check if path exist
|
|
81
|
+
|
|
131
82
|
if (!existsSync(path)) {
|
|
132
83
|
p.log.error(`Path ${path} does not exist, build your app first, or provide a valid path`)
|
|
133
84
|
program.error('')
|
|
134
85
|
}
|
|
135
86
|
|
|
87
|
+
return { appid: finalAppId, path }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkNotifyAppReady(options: Options, path: string) {
|
|
91
|
+
const checkNotifyAppReady = options.codeCheck
|
|
92
|
+
|
|
136
93
|
if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
|
|
137
94
|
const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
|
|
138
95
|
if (!isPluginConfigured) {
|
|
@@ -145,29 +102,23 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
|
|
|
145
102
|
program.error('')
|
|
146
103
|
}
|
|
147
104
|
}
|
|
105
|
+
}
|
|
148
106
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const localConfig = await getLocalConfig()
|
|
152
|
-
const supabase = await createSupabaseClient(options.apikey)
|
|
153
|
-
const userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'upload'])
|
|
154
|
-
// Check we have app access to this appId
|
|
155
|
-
const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appid, OrganizationPerm.upload)
|
|
156
|
-
|
|
157
|
-
// Now if it does exist we will fetch the org id
|
|
158
|
-
const orgId = await getOrganizationId(supabase, appid)
|
|
159
|
-
await checkPlanValid(supabase, orgId, options.apikey, appid, true)
|
|
160
|
-
|
|
161
|
-
const updateMetadataRequired = await requireUpdateMetadata(supabase, channel, appid)
|
|
162
|
-
|
|
107
|
+
async function verifyCompatibility(supabase: SupabaseType, pm: pmType, options: Options, channel: string, appid: string, bundle: string) {
|
|
163
108
|
// Check compatibility here
|
|
109
|
+
const ignoreMetadataCheck = options.ignoreMetadataCheck
|
|
110
|
+
const autoMinUpdateVersion = options.autoMinUpdateVersion
|
|
111
|
+
let minUpdateVersion = options.minUpdateVersion
|
|
112
|
+
|
|
164
113
|
const { data: channelData, error: channelError } = await supabase
|
|
165
114
|
.from('channels')
|
|
166
|
-
.select('version ( minUpdateVersion, native_packages )')
|
|
115
|
+
.select('disableAutoUpdate, version ( minUpdateVersion, native_packages )')
|
|
167
116
|
.eq('name', channel)
|
|
168
117
|
.eq('app_id', appid)
|
|
169
118
|
.single()
|
|
170
119
|
|
|
120
|
+
const updateMetadataRequired = !!channelData && channelData.disableAutoUpdate === 'version_number'
|
|
121
|
+
|
|
171
122
|
// eslint-disable-next-line no-undef-init
|
|
172
123
|
let localDependencies: Awaited<ReturnType<typeof getLocalDepenencies>> | undefined = undefined
|
|
173
124
|
let finalCompatibility: Awaited<ReturnType<typeof checkCompatibility>>['finalCompatibility']
|
|
@@ -185,7 +136,7 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
|
|
|
185
136
|
localDependencies = localDependenciesWithChannel
|
|
186
137
|
|
|
187
138
|
if (finalCompatibility.find(x => x.localVersion !== x.remoteVersion)) {
|
|
188
|
-
|
|
139
|
+
spinner.stop(`Bundle NOT compatible with ${channel} channel`)
|
|
189
140
|
p.log.warn(`You can check compatibility with "${pm.runner} @capgo/cli bundle compatibility"`)
|
|
190
141
|
|
|
191
142
|
if (autoMinUpdateVersion) {
|
|
@@ -202,14 +153,16 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
|
|
|
202
153
|
}
|
|
203
154
|
|
|
204
155
|
minUpdateVersion = lastMinUpdateVersion
|
|
205
|
-
|
|
156
|
+
spinner.stop(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
206
157
|
}
|
|
207
158
|
catch (error) {
|
|
208
159
|
p.log.error(`Cannot auto set compatibility, invalid data ${channelData}`)
|
|
209
160
|
program.error('')
|
|
210
161
|
}
|
|
211
162
|
}
|
|
212
|
-
|
|
163
|
+
else {
|
|
164
|
+
spinner.stop(`Bundle compatible with ${channel} channel`)
|
|
165
|
+
}
|
|
213
166
|
}
|
|
214
167
|
else if (!ignoreMetadataCheck) {
|
|
215
168
|
p.log.warn(`Channel ${channel} is new or it's your first upload with compatibility check, it will be ignored this time`)
|
|
@@ -233,223 +186,172 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
|
|
|
233
186
|
}
|
|
234
187
|
}
|
|
235
188
|
|
|
189
|
+
const hashedLocalDependencies = localDependencies
|
|
190
|
+
? new Map(localDependencies
|
|
191
|
+
.filter(a => !!a.native && a.native !== undefined)
|
|
192
|
+
.map(a => [a.name, a]))
|
|
193
|
+
: new Map()
|
|
194
|
+
|
|
195
|
+
const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
|
|
196
|
+
|
|
197
|
+
return { nativePackages, minUpdateVersion }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function checkTrial(supabase: SupabaseType, orgId: string, localConfig: localConfigType) {
|
|
236
201
|
const { data: isTrial, error: isTrialsError } = await supabase
|
|
237
202
|
.rpc('is_trial_org', { orgid: orgId })
|
|
238
203
|
.single()
|
|
239
204
|
if ((isTrial && isTrial > 0) || isTrialsError) {
|
|
240
|
-
|
|
205
|
+
// TODO: Come back to this to fix for orgs v3
|
|
241
206
|
p.log.warn(`WARNING !!\nTrial expires in ${isTrial} days`)
|
|
242
207
|
p.log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans?oid=${orgId}`)
|
|
243
208
|
}
|
|
209
|
+
}
|
|
244
210
|
|
|
211
|
+
async function checkVersionExists(supabase: SupabaseType, appid: string, bundle: string) {
|
|
245
212
|
// check if app already exist
|
|
213
|
+
// apikey is sooo legacy code, current prod does not use it
|
|
246
214
|
const { data: appVersion, error: appVersionError } = await supabase
|
|
247
|
-
.rpc('exist_app_versions', { appid, apikey:
|
|
215
|
+
.rpc('exist_app_versions', { appid, apikey: '', name_version: bundle })
|
|
248
216
|
.single()
|
|
249
217
|
|
|
250
218
|
if (appVersion || appVersionError) {
|
|
251
219
|
p.log.error(`Version already exists ${formatError(appVersionError)}`)
|
|
252
220
|
program.error('')
|
|
253
221
|
}
|
|
222
|
+
}
|
|
254
223
|
|
|
224
|
+
async function prepareBundleFile(path: string, options: Options, localConfig: localConfigType, snag: LogSnag, orgId: string, appid: string) {
|
|
255
225
|
let sessionKey
|
|
256
226
|
let checksum = ''
|
|
257
227
|
let zipped: Buffer | null = null
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (!res) {
|
|
279
|
-
p.log.error(`Error: Missing public key`)
|
|
280
|
-
program.error('')
|
|
281
|
-
}
|
|
282
|
-
keyData = localConfig.signKey || ''
|
|
228
|
+
const key = options.key
|
|
229
|
+
|
|
230
|
+
zipped = await zipFile(path)
|
|
231
|
+
const s = p.spinner()
|
|
232
|
+
s.start(`Calculating checksum`)
|
|
233
|
+
checksum = await getChecksum(zipped, 'crc32')
|
|
234
|
+
s.stop(`Checksum: ${checksum}`)
|
|
235
|
+
// key should be undefined or a string if false it should ingore encryption
|
|
236
|
+
if (!key) {
|
|
237
|
+
p.log.info(`Encryption ignored`)
|
|
238
|
+
}
|
|
239
|
+
else if (key || existsSync(baseKeyPub)) {
|
|
240
|
+
const publicKey = typeof key === 'string' ? key : baseKeyPub
|
|
241
|
+
let keyData = options.keyData || ''
|
|
242
|
+
// check if publicKey exist
|
|
243
|
+
if (!keyData && !existsSync(publicKey)) {
|
|
244
|
+
p.log.error(`Cannot find public key ${publicKey}`)
|
|
245
|
+
if (ciDetect.isCI) {
|
|
246
|
+
p.log.error('Cannot ask if user wants to use capgo public key on the cli')
|
|
247
|
+
program.error('')
|
|
283
248
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
tags: {
|
|
290
|
-
'app-id': appid,
|
|
291
|
-
},
|
|
292
|
-
notify: false,
|
|
293
|
-
}).catch()
|
|
294
|
-
// open with fs publicKey path
|
|
295
|
-
if (!keyData) {
|
|
296
|
-
const keyFile = readFileSync(publicKey)
|
|
297
|
-
keyData = keyFile.toString()
|
|
249
|
+
|
|
250
|
+
const res = await p.confirm({ message: 'Do you want to use our public key ?' })
|
|
251
|
+
if (!res) {
|
|
252
|
+
p.log.error(`Error: Missing public key`)
|
|
253
|
+
program.error('')
|
|
298
254
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
255
|
+
keyData = localConfig.signKey || ''
|
|
256
|
+
}
|
|
257
|
+
await snag.track({
|
|
258
|
+
channel: 'app',
|
|
259
|
+
event: 'App encryption',
|
|
260
|
+
icon: '🔑',
|
|
261
|
+
user_id: orgId,
|
|
262
|
+
tags: {
|
|
263
|
+
'app-id': appid,
|
|
264
|
+
},
|
|
265
|
+
notify: false,
|
|
266
|
+
}).catch()
|
|
267
|
+
// open with fs publicKey path
|
|
268
|
+
if (!keyData) {
|
|
269
|
+
const keyFile = readFileSync(publicKey)
|
|
270
|
+
keyData = keyFile.toString()
|
|
271
|
+
}
|
|
272
|
+
// encrypt
|
|
273
|
+
p.log.info(`Encrypting your bundle`)
|
|
274
|
+
const res = encryptSource(zipped, keyData)
|
|
275
|
+
sessionKey = res.ivSessionKey
|
|
276
|
+
if (options.displayIvSession) {
|
|
277
|
+
p.log.info(`Your Iv Session key is ${sessionKey},
|
|
305
278
|
keep it safe, you will need it to decrypt your bundle.
|
|
306
279
|
It will be also visible in your dashboard\n`)
|
|
307
|
-
}
|
|
308
|
-
zipped = res.encryptedData
|
|
309
|
-
}
|
|
310
|
-
const mbSize = Math.floor(zipped.byteLength / 1024 / 1024)
|
|
311
|
-
if (mbSize > alertMb) {
|
|
312
|
-
p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
313
|
-
p.log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
314
|
-
await snag.track({
|
|
315
|
-
channel: 'app-error',
|
|
316
|
-
event: 'App Too Large',
|
|
317
|
-
icon: '🚛',
|
|
318
|
-
user_id: orgId,
|
|
319
|
-
tags: {
|
|
320
|
-
'app-id': appid,
|
|
321
|
-
},
|
|
322
|
-
notify: false,
|
|
323
|
-
}).catch()
|
|
324
280
|
}
|
|
281
|
+
zipped = res.encryptedData
|
|
325
282
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
else {
|
|
331
|
-
if (useS3) {
|
|
332
|
-
zipped = await zipFile(path)
|
|
333
|
-
const s = p.spinner()
|
|
334
|
-
s.start(`Calculating checksum`)
|
|
335
|
-
checksum = await getChecksum(zipped, 'crc32')
|
|
336
|
-
s.stop(`Checksum: ${checksum}`)
|
|
337
|
-
}
|
|
283
|
+
const mbSize = Math.floor((zipped?.byteLength ?? 0) / 1024 / 1024)
|
|
284
|
+
if (mbSize > alertMb) {
|
|
285
|
+
p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
286
|
+
p.log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
338
287
|
await snag.track({
|
|
339
|
-
channel: 'app',
|
|
340
|
-
event: 'App
|
|
341
|
-
icon: '
|
|
288
|
+
channel: 'app-error',
|
|
289
|
+
event: 'App Too Large',
|
|
290
|
+
icon: '🚛',
|
|
342
291
|
user_id: orgId,
|
|
343
292
|
tags: {
|
|
344
293
|
'app-id': appid,
|
|
345
294
|
},
|
|
346
295
|
notify: false,
|
|
347
296
|
}).catch()
|
|
348
|
-
sessionKey = options.ivSessionKey
|
|
349
297
|
}
|
|
350
298
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.filter(a => !!a.native && a.native !== undefined)
|
|
354
|
-
.map(a => [a.name, a]))
|
|
355
|
-
: new Map()
|
|
356
|
-
|
|
357
|
-
const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
|
|
299
|
+
return { zipped, sessionKey, checksum }
|
|
300
|
+
}
|
|
358
301
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
session_key: sessionKey,
|
|
364
|
-
external_url: external,
|
|
365
|
-
storage_provider: external ? 'external' : 'r2-direct',
|
|
366
|
-
minUpdateVersion,
|
|
367
|
-
native_packages: nativePackages,
|
|
368
|
-
owner_org: orgId,
|
|
369
|
-
user_id: userId,
|
|
370
|
-
checksum,
|
|
371
|
-
}
|
|
372
|
-
const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
|
|
373
|
-
if (dbError) {
|
|
374
|
-
p.log.error(`Cannot add bundle ${formatError(dbError)}`)
|
|
375
|
-
program.error('')
|
|
376
|
-
}
|
|
377
|
-
if (!external && zipped) {
|
|
378
|
-
const spinner = p.spinner()
|
|
379
|
-
spinner.start(`Uploading Bundle`)
|
|
380
|
-
const startTime = performance.now()
|
|
302
|
+
async function uploadBundleToCapgoCloud(supabase: SupabaseType, appid: string, bundle: string, orgId: string, zipped: Buffer, options: Options) {
|
|
303
|
+
const spinner = p.spinner()
|
|
304
|
+
spinner.start(`Uploading Bundle`)
|
|
305
|
+
const startTime = performance.now()
|
|
381
306
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
const url = await uploadUrl(supabase, appid, bundle)
|
|
389
|
-
if (!url) {
|
|
390
|
-
p.log.error(`Cannot get upload url`)
|
|
391
|
-
program.error('')
|
|
392
|
-
}
|
|
393
|
-
await ky.put(url, {
|
|
394
|
-
timeout: options.timeout || UPLOAD_TIMEOUT,
|
|
395
|
-
retry: 5,
|
|
396
|
-
body: zipped,
|
|
397
|
-
})
|
|
398
|
-
}
|
|
307
|
+
try {
|
|
308
|
+
if (options.multipart !== undefined && options.multipart) {
|
|
309
|
+
p.log.info(`Uploading bundle as multipart`)
|
|
310
|
+
await uploadMultipart(supabase, appid, bundle, zipped, orgId)
|
|
399
311
|
}
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (errorUpload instanceof HTTPError) {
|
|
406
|
-
const body = await errorUpload.response.text()
|
|
407
|
-
p.log.error(`Response: ${formatError(body)}`)
|
|
312
|
+
else {
|
|
313
|
+
const url = await uploadUrl(supabase, appid, bundle)
|
|
314
|
+
if (!url) {
|
|
315
|
+
p.log.error(`Cannot get upload url`)
|
|
316
|
+
program.error('')
|
|
408
317
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
versionData.storage_provider = 'r2'
|
|
415
|
-
const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
|
|
416
|
-
if (dbError2) {
|
|
417
|
-
p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
|
|
418
|
-
program.error('')
|
|
318
|
+
await ky.put(url, {
|
|
319
|
+
timeout: options.timeout || UPLOAD_TIMEOUT,
|
|
320
|
+
retry: 5,
|
|
321
|
+
body: zipped,
|
|
322
|
+
})
|
|
419
323
|
}
|
|
420
|
-
const endTime = performance.now()
|
|
421
|
-
const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
422
|
-
spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
|
|
423
324
|
}
|
|
424
|
-
|
|
425
|
-
const spinner = p.spinner()
|
|
426
|
-
spinner.start(`Uploading Bundle`)
|
|
427
|
-
|
|
428
|
-
const fileName = `${appid}-${bundle}`
|
|
429
|
-
const encodeFileName = encodeURIComponent(fileName)
|
|
430
|
-
const command: PutObjectCommand = new PutObjectCommand({
|
|
431
|
-
Bucket: s3BucketName,
|
|
432
|
-
Key: fileName,
|
|
433
|
-
Body: zipped,
|
|
434
|
-
})
|
|
435
|
-
const startTime = performance.now()
|
|
436
|
-
const response = await s3Client.send(command)
|
|
437
|
-
if (response.$metadata.httpStatusCode !== 200) {
|
|
438
|
-
p.log.error(`Cannot upload to S3`)
|
|
439
|
-
program.error('')
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
versionData.storage_provider = 'external'
|
|
443
|
-
versionData.external_url = `https://${s3BucketName}.s3.amazonaws.com/${encodeFileName}`
|
|
444
|
-
const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
|
|
445
|
-
if (dbError2) {
|
|
446
|
-
p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
|
|
447
|
-
program.error('')
|
|
448
|
-
}
|
|
325
|
+
catch (errorUpload) {
|
|
449
326
|
const endTime = performance.now()
|
|
450
327
|
const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
451
|
-
spinner.stop(`
|
|
328
|
+
spinner.stop(`Failed to upload bundle ( after ${uploadTime} seconds)`)
|
|
329
|
+
p.log.error(`Cannot upload bundle ( try again with --multipart option) ${formatError(errorUpload)}`)
|
|
330
|
+
if (errorUpload instanceof HTTPError) {
|
|
331
|
+
const body = await errorUpload.response.text()
|
|
332
|
+
p.log.error(`Response: ${formatError(body)}`)
|
|
333
|
+
}
|
|
334
|
+
// call delete version on path /delete_failed_version to delete the version
|
|
335
|
+
await deletedFailedVersion(supabase, appid, bundle)
|
|
336
|
+
program.error('')
|
|
452
337
|
}
|
|
338
|
+
|
|
339
|
+
const endTime = performance.now()
|
|
340
|
+
const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
341
|
+
spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function setVersionInChannel(
|
|
345
|
+
supabase: SupabaseType,
|
|
346
|
+
options: Options,
|
|
347
|
+
bundle: string,
|
|
348
|
+
channel: string,
|
|
349
|
+
userId: string,
|
|
350
|
+
orgId: string,
|
|
351
|
+
appid: string,
|
|
352
|
+
localConfig: localConfigType,
|
|
353
|
+
permissions: OrganizationPerm,
|
|
354
|
+
) {
|
|
453
355
|
const { data: versionId } = await supabase
|
|
454
356
|
.rpc('get_app_versions', { apikey: options.apikey, name_version: bundle, appid })
|
|
455
357
|
.single()
|
|
@@ -473,16 +375,121 @@ It will be also visible in your dashboard\n`)
|
|
|
473
375
|
else if (data?.id)
|
|
474
376
|
p.log.info(`Link device to this bundle to try it: ${bundleUrl}`)
|
|
475
377
|
|
|
476
|
-
if (options.bundleUrl)
|
|
378
|
+
if (options.bundleUrl) {
|
|
477
379
|
p.log.info(`Bundle url: ${bundleUrl}`)
|
|
380
|
+
}
|
|
381
|
+
else if (!versionId) {
|
|
382
|
+
p.log.warn('Cannot set bundle with upload key, use key with more rights for that')
|
|
383
|
+
program.error('')
|
|
384
|
+
}
|
|
385
|
+
else if (!hasOrganizationPerm(permissions, OrganizationPerm.write)) {
|
|
386
|
+
p.log.warn('Cannot set channel as a upload organization member')
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function uploadBundle(preAppid: string, options: Options, shouldExit = true) {
|
|
392
|
+
p.intro(`Uploading`)
|
|
393
|
+
const pm = getPMAndCommand()
|
|
394
|
+
await checkLatest()
|
|
395
|
+
|
|
396
|
+
const { s3Region, s3Apikey, s3Apisecret, s3BucketName } = options
|
|
397
|
+
|
|
398
|
+
if (s3BucketName || s3Region || s3Apikey || s3Apisecret) {
|
|
399
|
+
if (!s3BucketName || !s3Region || !s3Apikey || !s3Apisecret) {
|
|
400
|
+
p.log.error('Missing argument, for S3 upload you need to provide a bucket name, region, API key, and API secret')
|
|
401
|
+
program.error('')
|
|
402
|
+
}
|
|
478
403
|
}
|
|
479
|
-
|
|
480
|
-
|
|
404
|
+
|
|
405
|
+
if (s3Region && s3Apikey && s3Apisecret && s3BucketName) {
|
|
406
|
+
p.log.info('Uploading to S3')
|
|
407
|
+
// const s3Client = new S3Client({
|
|
408
|
+
// region: s3Region,
|
|
409
|
+
// credentials: {
|
|
410
|
+
// accessKeyId: s3Apikey,
|
|
411
|
+
// secretAccessKey: s3Apisecret,
|
|
412
|
+
// },
|
|
413
|
+
// })
|
|
414
|
+
p.log.error('S3 upload is not available we have currenly an issue with it')
|
|
481
415
|
program.error('')
|
|
416
|
+
// todo: figure out s3 upload
|
|
417
|
+
return
|
|
482
418
|
}
|
|
483
|
-
|
|
484
|
-
|
|
419
|
+
|
|
420
|
+
const apikey = getApikey(options)
|
|
421
|
+
const config = await getConfig()
|
|
422
|
+
const { appid, path } = getAppIdAndPath(preAppid, options, config)
|
|
423
|
+
const bundle = getBundle(config, options)
|
|
424
|
+
const channel = options.channel || 'dev'
|
|
425
|
+
const snag = useLogSnag()
|
|
426
|
+
|
|
427
|
+
checkNotifyAppReady(options, path)
|
|
428
|
+
|
|
429
|
+
p.log.info(`Upload ${appid}@${bundle} started from path "${path}" to Capgo cloud`)
|
|
430
|
+
|
|
431
|
+
const localConfig = await getLocalConfig()
|
|
432
|
+
const supabase = await createSupabaseClient(apikey)
|
|
433
|
+
const userId = await verifyUser(supabase, apikey, ['write', 'all', 'upload'])
|
|
434
|
+
// Check we have app access to this appId
|
|
435
|
+
const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, apikey, appid, OrganizationPerm.upload)
|
|
436
|
+
|
|
437
|
+
// Now if it does exist we will fetch the org id
|
|
438
|
+
const orgId = await getOrganizationId(supabase, appid)
|
|
439
|
+
await checkPlanValid(supabase, orgId, options.apikey, appid, true)
|
|
440
|
+
await checkTrial(supabase, orgId, localConfig)
|
|
441
|
+
const { nativePackages, minUpdateVersion } = await verifyCompatibility(supabase, pm, options, channel, appid, bundle)
|
|
442
|
+
await checkVersionExists(supabase, appid, bundle)
|
|
443
|
+
|
|
444
|
+
if (options.external && !options.external.startsWith('https://')) {
|
|
445
|
+
p.log.error(`External link should should start with "https://" current is "${external}"`)
|
|
446
|
+
program.error('')
|
|
485
447
|
}
|
|
448
|
+
|
|
449
|
+
const versionData = {
|
|
450
|
+
// bucket_id: external ? undefined : fileName,
|
|
451
|
+
name: bundle,
|
|
452
|
+
app_id: appid,
|
|
453
|
+
session_key: undefined as undefined | string,
|
|
454
|
+
external_url: options.external,
|
|
455
|
+
storage_provider: options.external ? 'external' : 'r2-direct',
|
|
456
|
+
minUpdateVersion,
|
|
457
|
+
native_packages: nativePackages,
|
|
458
|
+
owner_org: orgId,
|
|
459
|
+
user_id: userId,
|
|
460
|
+
checksum: undefined as undefined | string,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let zipped: Buffer | null = null
|
|
464
|
+
if (!options.external) {
|
|
465
|
+
const { zipped: _zipped, sessionKey, checksum } = await prepareBundleFile(path, options, localConfig, snag, orgId, appid)
|
|
466
|
+
versionData.session_key = sessionKey
|
|
467
|
+
versionData.checksum = checksum
|
|
468
|
+
zipped = _zipped
|
|
469
|
+
if (!options.ignoreChecksumCheck) {
|
|
470
|
+
await checkChecksum(supabase, appid, channel, checksum)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
|
|
475
|
+
if (dbError) {
|
|
476
|
+
p.log.error(`Cannot add bundle ${formatError(dbError)}`)
|
|
477
|
+
program.error('')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (zipped) {
|
|
481
|
+
await uploadBundleToCapgoCloud(supabase, appid, bundle, orgId, zipped, options)
|
|
482
|
+
|
|
483
|
+
versionData.storage_provider = 'r2'
|
|
484
|
+
const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
|
|
485
|
+
if (dbError2) {
|
|
486
|
+
p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
|
|
487
|
+
program.error('')
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await setVersionInChannel(supabase, options, bundle, channel, userId, orgId, appid, localConfig, permissions)
|
|
492
|
+
|
|
486
493
|
await snag.track({
|
|
487
494
|
channel: 'app',
|
|
488
495
|
event: 'App Uploaded',
|