Skip to content

Commit 6b92160

Browse files
committed
ci: iOS job for release
1 parent e986068 commit 6b92160

File tree

3 files changed

+62
-170
lines changed

3 files changed

+62
-170
lines changed

ci/Jenkinsfile.combined

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env groovy
22

3-
library 'status-jenkins-lib@v1.9.31'
3+
library 'status-jenkins-lib@fix-ios-signing-with-fastlane'
44

55
/* Object to store public URLs for description. */
66
urls = [:]
@@ -113,6 +113,11 @@ pipeline {
113113
'MacOS/aarch64', jenkins.Build('status-app/systems/macos/aarch64/package')
114114
)
115115
} } }
116+
stage('iOS/aarch64') { steps { script {
117+
ios_aarch64 = getArtifacts(
118+
'iOS/aarch64', jenkins.Build('status-app/systems/ios/aarch64/package')
119+
)
120+
} } }
116121
}
117122
}
118123
stage('Publish') {

mobile/fastlane/Fastfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ platform :ios do
113113
type: options[:type],
114114
app_identifier: options[:app_identifier],
115115
readonly: false,
116-
# Auto-regenerate development profiles when new devices are registered
117-
force_for_new_devices: options[:type] == "development"
116+
# Auto-regenerate profiles when new devices are registered (for dev and adhoc)
117+
force_for_new_devices: options[:type] == "development" || options[:type] == "adhoc"
118118
}
119119

120120
# Only specify keychain params in CI where we create a custom keychain

scripts/diawi-upload.mjs

Lines changed: 54 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +1,122 @@
11
#!/usr/bin/env node
22

3-
/**
4-
* Diawi Upload Script
5-
* Uploads iOS IPA files to Diawi for distribution.
6-
*
7-
* Usage: node diawi-upload.mjs <ipa_path> [comment]
8-
*
9-
* Environment variables:
10-
* DIAWI_TOKEN - Required: API token for Diawi authentication
11-
* VERBOSE - Optional: Set to enable verbose logging
12-
* POLL_MAX_COUNT - Optional: Max polling attempts (default: 120)
13-
* POLL_INTERVAL_MS - Optional: Polling interval in ms (default: 500)
14-
*/
15-
163
import https from 'node:https'
174
import { basename } from 'node:path'
18-
import { createReadStream, statSync } from 'node:fs'
19-
import { randomBytes } from 'node:crypto'
5+
import { promisify } from 'node:util'
6+
import { createReadStream } from 'node:fs'
7+
8+
import log from 'npmlog'
9+
import FormData from 'form-data'
2010

2111
const UPLOAD_URL = 'https://upload.diawi.com/'
2212
const STATUS_URL = 'https://upload.diawi.com/status'
2313
const DIAWI_TOKEN = process.env.DIAWI_TOKEN
24-
const VERBOSE = process.env.VERBOSE === 'true' || process.env.VERBOSE === '1'
25-
const POLL_MAX_COUNT = parseInt(process.env.POLL_MAX_COUNT || '120', 10)
26-
const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS || '500', 10)
27-
28-
const log = {
29-
info: (prefix, ...args) => console.log(`[INFO] ${prefix}:`, ...args),
30-
verbose: (prefix, ...args) => VERBOSE && console.log(`[VERBOSE] ${prefix}:`, ...args),
31-
warn: (prefix, ...args) => console.warn(`[WARN] ${prefix}:`, ...args),
32-
error: (prefix, ...args) => console.error(`[ERROR] ${prefix}:`, ...args),
33-
}
14+
const LOG_LEVEL = process.env.VERBOSE ? 'verbose' : 'info'
15+
const POLL_MAX_COUNT = process.env.POLL_MAX_COUNT ? process.env.POLL_MAX_COUNT : 120
16+
const POLL_INTERVAL_MS = process.env.POLL_INTERVAL_MS ? process.env.POLL_INTERVAL_MS : 500
3417

35-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
18+
const sleep = (ms) => {
19+
return new Promise((resolve, reject) => {
20+
setTimeout(resolve, (ms))
21+
})
22+
}
3623

37-
/**
38-
* Perform a GET request
39-
*/
4024
const getRequest = async (url) => {
4125
return new Promise((resolve, reject) => {
42-
const data = []
43-
https.get(url, (res) => {
44-
res.on('error', (err) => reject(err))
45-
res.on('data', (chunk) => data.push(chunk))
26+
let data = []
27+
https.get(url, res => {
28+
res.on('error', err => reject(err))
29+
res.on('data', chunk => { data.push(chunk) })
4630
res.on('end', () => {
47-
const payload = Buffer.concat(data).toString()
31+
let payload = Buffer.concat(data).toString()
4832
resolve({
4933
code: res.statusCode,
5034
message: res.statusMessage,
5135
payload: payload,
5236
})
5337
})
54-
}).on('error', reject)
38+
})
5539
})
5640
}
5741

58-
/**
59-
* Create multipart form data and upload to Diawi
60-
* Using native Node.js without external dependencies
61-
*/
6242
const uploadIpa = async (ipaPath, comment, token) => {
63-
const boundary = `----FormBoundary${randomBytes(16).toString('hex')}`
64-
const fileName = basename(ipaPath)
65-
const fileSize = statSync(ipaPath).size
43+
let form = new FormData()
44+
form.append('token', token)
45+
form.append('file', createReadStream(ipaPath))
46+
form.append('comment', comment || basename(ipaPath))
6647

67-
log.info('upload', `File: ${fileName}, Size: ${(fileSize / 1024 / 1024).toFixed(2)} MB`)
48+
const formSubmitPromise = promisify(form.submit.bind(form))
6849

69-
return new Promise((resolve, reject) => {
70-
// Build the multipart form data parts
71-
const tokenPart = [
72-
`--${boundary}`,
73-
'Content-Disposition: form-data; name="token"',
74-
'',
75-
token,
76-
].join('\r\n')
77-
78-
const commentPart = [
79-
`--${boundary}`,
80-
'Content-Disposition: form-data; name="comment"',
81-
'',
82-
comment || fileName,
83-
].join('\r\n')
84-
85-
const fileHeader = [
86-
`--${boundary}`,
87-
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
88-
'Content-Type: application/octet-stream',
89-
'',
90-
].join('\r\n')
91-
92-
const footer = `\r\n--${boundary}--\r\n`
93-
94-
// Calculate total content length
95-
const headerBuffer = Buffer.from(`${tokenPart}\r\n${commentPart}\r\n${fileHeader}\r\n`)
96-
const footerBuffer = Buffer.from(footer)
97-
const contentLength = headerBuffer.length + fileSize + footerBuffer.length
98-
99-
const url = new URL(UPLOAD_URL)
100-
const options = {
101-
hostname: url.hostname,
102-
port: 443,
103-
path: url.pathname,
104-
method: 'POST',
105-
headers: {
106-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
107-
'Content-Length': contentLength,
108-
},
109-
}
110-
111-
const req = https.request(options, (res) => {
112-
let data = ''
113-
res.on('data', (chunk) => { data += chunk })
114-
res.on('end', () => {
115-
if (res.statusCode !== 200) {
116-
log.error('upload', `Upload failed: ${res.statusCode} ${res.statusMessage}`)
117-
log.error('upload', `Response: ${data}`)
118-
reject(new Error(`Upload failed: ${res.statusCode} ${res.statusMessage}`))
119-
return
120-
}
121-
try {
122-
const json = JSON.parse(data)
123-
resolve(json.job)
124-
} catch (e) {
125-
reject(new Error(`Failed to parse response: ${data}`))
126-
}
127-
})
128-
})
129-
130-
req.on('error', (err) => {
131-
log.error('upload', `Request error: ${err.message}`)
132-
reject(err)
133-
})
134-
135-
// Write header parts
136-
req.write(headerBuffer)
50+
const res = await formSubmitPromise(UPLOAD_URL)
51+
if (res.statusCode != 200) {
52+
log.error('uploadIpa', 'Upload failed: %d %s', res.statusCode, res.statusMessage)
53+
process.exit(1)
54+
}
13755

138-
// Stream the file
139-
const fileStream = createReadStream(ipaPath)
140-
fileStream.on('data', (chunk) => req.write(chunk))
141-
fileStream.on('end', () => {
142-
req.write(footerBuffer)
143-
req.end()
144-
})
145-
fileStream.on('error', (err) => {
146-
log.error('upload', `File read error: ${err.message}`)
147-
reject(err)
56+
return new Promise((resolve) => {
57+
const jobId = res.on('data', async (data) => {
58+
resolve(JSON.parse(data)['job'])
14859
})
14960
})
15061
}
15162

152-
/**
153-
* Check the status of an upload job
154-
*/
15563
const checkStatus = async (jobId, token) => {
156-
const params = new URLSearchParams({ token, job: jobId })
157-
const rval = await getRequest(`${STATUS_URL}?${params.toString()}`)
158-
159-
if (rval.code !== 200) {
160-
log.error('checkStatus', `Check query failed: ${rval.code} ${rval.message}`)
161-
throw new Error(`Status check failed: ${rval.code} ${rval.message}`)
64+
let params = new URLSearchParams({
65+
token: token, job: jobId,
66+
})
67+
let rval = await getRequest(`${STATUS_URL}?${params.toString()}`)
68+
if (rval.code != 200) {
69+
log.error('checkStatus', 'Check query failed: %d %s', rval.code, rval.message)
70+
process.exit(1)
16271
}
163-
16472
return JSON.parse(rval.payload)
16573
}
16674

167-
/**
168-
* Poll for upload completion
169-
*/
17075
const pollStatus = async (jobId, token) => {
17176
let interval = POLL_INTERVAL_MS
172-
17377
for (let i = 0; i <= POLL_MAX_COUNT; i++) {
174-
const json = await checkStatus(jobId, token)
175-
78+
let json = await checkStatus(jobId, token)
17679
switch (json.status) {
17780
case 2000:
178-
// Success
17981
return json
18082
case 2001:
181-
// Still processing
182-
log.verbose('pollStatus', `Waiting: ${json.message}`)
183-
break
83+
log.verbose('pollStatus', 'Waiting: %s', json.message)
84+
break /* Nothing, just poll again. */
18485
case 4000000:
185-
// Rate limited, back off
186-
log.warn('pollStatus', `Doubling polling interval: ${json.message}`)
86+
log.warning('pollStatus', 'Doubling polling interval: %s', json.message)
18787
interval *= 2
18888
break
18989
default:
19090
log.error('pollStatus', `Error in status response: ${json.message}`)
191-
throw new Error(`Diawi error: ${json.message}`)
91+
process.exit(1)
19292
}
193-
19493
await sleep(interval)
19594
}
196-
197-
throw new Error(`Failed to poll status after ${POLL_MAX_COUNT} retries`)
95+
log.error('pollStatus', 'Failed to poll status after %d retries.', POLL_MAX_COUNT)
96+
process.exit(1)
19897
}
19998

20099
const main = async () => {
201100
const targetFile = process.argv[2]
202101
const comment = process.argv[3]
102+
log.level = LOG_LEVEL
203103

204-
if (!DIAWI_TOKEN) {
104+
if (DIAWI_TOKEN === undefined) {
205105
log.error('main', 'No DIAWI_TOKEN env var provided!')
206106
process.exit(1)
207107
}
208-
209-
if (!targetFile) {
108+
if (targetFile === undefined) {
210109
log.error('main', 'No file path provided!')
211-
log.error('main', 'Usage: node diawi-upload.mjs <ipa_path> [comment]')
212110
process.exit(1)
213111
}
214112

215-
try {
216-
log.info('main', `Uploading: ${targetFile}`)
217-
const jobId = await uploadIpa(targetFile, comment, DIAWI_TOKEN)
113+
log.info('main', 'Uploading: %s', targetFile)
114+
let jobId = await uploadIpa(targetFile, comment, DIAWI_TOKEN)
218115

219-
log.info('main', `Polling upload job status: ${jobId}`)
220-
const uploadMeta = await pollStatus(jobId, DIAWI_TOKEN)
116+
log.info('main', 'Polling upload job status: %s', jobId)
117+
let uploadMeta = await pollStatus(jobId, DIAWI_TOKEN)
221118

222-
// Output the result as JSON (for parsing by CI)
223-
console.log(JSON.stringify(uploadMeta, null, 2))
224-
225-
// Also output the direct link for convenience
226-
if (uploadMeta.link) {
227-
log.info('main', `Diawi URL: ${uploadMeta.link}`)
228-
}
229-
} catch (error) {
230-
log.error('main', error.message)
231-
process.exit(1)
232-
}
119+
console.log(uploadMeta)
233120
}
234121

235122
main()

0 commit comments

Comments
 (0)