Skip to content

Commit dcc64b6

Browse files
committed
🐛 Fix(ci): buffer AtomGit uploads for large assets
1 parent bad47fb commit dcc64b6

2 files changed

Lines changed: 40 additions & 38 deletions

File tree

.github/workflows/mirror-release-to-atomgit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
ATOM_OWNER: ${{ vars.ATOM_OWNER || 'Molunerfinn' }}
3131
ATOM_REPO: ${{ vars.ATOM_REPO || 'PicGo' }}
3232
ATOM_TARGET_COMMITISH: ${{ vars.ATOM_TARGET_COMMITISH }}
33-
ATOM_UPLOAD_CONCURRENCY: ${{ vars.ATOM_UPLOAD_CONCURRENCY || '3' }}
33+
ATOM_UPLOAD_TIMEOUT_MINUTES: ${{ vars.ATOM_UPLOAD_TIMEOUT_MINUTES || '10' }}
3434
steps:
3535
- name: Check out git repository
3636
uses: actions/checkout@v4

scripts/publish-atomgit-release.js

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const DIST_DIR = path.resolve(process.env.DIST_DIR || 'dist')
1111
const RELEASE_METADATA_PATH = path.resolve(process.env.GITHUB_RELEASE_JSON || 'github-release.json')
1212
const MAX_ATTEMPTS = 3
1313
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
14-
const UPLOAD_CONCURRENCY = Number.parseInt(process.env.ATOM_UPLOAD_CONCURRENCY || '3', 10)
14+
const UPLOAD_TIMEOUT_MINUTES = Number.parseInt(process.env.ATOM_UPLOAD_TIMEOUT_MINUTES || '10', 10)
1515

1616
function requireEnvironmentVariable(name, value) {
1717
if (!value) {
@@ -192,41 +192,49 @@ async function getUploadRequest(fileName) {
192192

193193
async function uploadAsset(filePath) {
194194
const fileName = path.basename(filePath)
195-
const uploadRequest = await getUploadRequest(fileName)
196-
const fileSize = fs.statSync(filePath).size
197-
const uploadHeaders = new Headers(uploadRequest.headers)
198-
uploadHeaders.set('Content-Length', String(fileSize))
195+
const fileBuffer = await fs.promises.readFile(filePath)
196+
const fileSize = fileBuffer.byteLength
199197
const startedAt = Date.now()
200198

201199
console.log(`[AtomGit] Uploading ${fileName} (${formatFileSize(fileSize)})`)
202-
const response = await requestWithRetry(
203-
uploadRequest.url,
204-
() => ({
205-
method: 'PUT',
206-
headers: uploadHeaders,
207-
body: fs.createReadStream(filePath),
208-
duplex: 'half'
209-
}),
210-
`upload ${fileName}`
211-
)
212200

213-
await assertSuccessfulResponse(response, `Upload ${fileName}`)
214-
console.log(`[AtomGit] Uploaded ${fileName} in ${formatDuration(Date.now() - startedAt)}`)
215-
}
201+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
202+
const uploadRequest = await getUploadRequest(fileName)
203+
const uploadHeaders = new Headers(uploadRequest.headers)
204+
uploadHeaders.set('Content-Length', String(fileSize))
205+
let response
206+
207+
try {
208+
response = await fetch(uploadRequest.url, {
209+
method: 'PUT',
210+
headers: uploadHeaders,
211+
body: fileBuffer,
212+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MINUTES * 60 * 1000)
213+
})
214+
} catch (error) {
215+
if (attempt === MAX_ATTEMPTS) {
216+
throw error
217+
}
216218

217-
async function uploadAssets(assetPaths) {
218-
let nextAssetIndex = 0
219-
const workerCount = Math.min(UPLOAD_CONCURRENCY, assetPaths.length)
219+
console.warn(`[AtomGit] Upload ${fileName} failed; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
220+
await wait(1000 * attempt)
221+
continue
222+
}
220223

221-
const runWorker = async () => {
222-
while (nextAssetIndex < assetPaths.length) {
223-
const assetIndex = nextAssetIndex
224-
nextAssetIndex += 1
225-
await uploadAsset(assetPaths[assetIndex])
224+
if (response.ok) {
225+
await response.text()
226+
console.log(`[AtomGit] Uploaded ${fileName} in ${formatDuration(Date.now() - startedAt)}`)
227+
return
226228
}
227-
}
228229

229-
await Promise.all(Array.from({ length: workerCount }, runWorker))
230+
const responseBody = await readResponseBody(response)
231+
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
232+
throw new Error(`Upload ${fileName} failed with HTTP ${response.status}: ${formatResponseBody(responseBody)}`)
233+
}
234+
235+
console.warn(`[AtomGit] Upload ${fileName} returned ${response.status}; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
236+
await wait(1000 * attempt)
237+
}
230238
}
231239

232240
function readReleaseMetadata() {
@@ -264,8 +272,8 @@ async function publishRelease() {
264272
requireEnvironmentVariable('ATOM_OWNER', ATOM_OWNER)
265273
requireEnvironmentVariable('ATOM_REPO', ATOM_REPO)
266274
requireEnvironmentVariable('RELEASE_TAG', RELEASE_TAG)
267-
if (!Number.isInteger(UPLOAD_CONCURRENCY) || UPLOAD_CONCURRENCY < 1 || UPLOAD_CONCURRENCY > 10) {
268-
throw new Error('ATOM_UPLOAD_CONCURRENCY must be an integer between 1 and 10')
275+
if (!Number.isInteger(UPLOAD_TIMEOUT_MINUTES) || UPLOAD_TIMEOUT_MINUTES < 1 || UPLOAD_TIMEOUT_MINUTES > 120) {
276+
throw new Error('ATOM_UPLOAD_TIMEOUT_MINUTES must be an integer between 1 and 120')
269277
}
270278

271279
const metadata = readReleaseMetadata()
@@ -278,7 +286,6 @@ async function publishRelease() {
278286
await verifyCredentials()
279287
const release = await getOrCreateRelease(metadata)
280288
const existingAssetNames = getExistingAssetNames(release)
281-
const pendingAssetPaths = []
282289

283290
for (const assetPath of assetPaths) {
284291
const fileName = path.basename(assetPath)
@@ -287,12 +294,7 @@ async function publishRelease() {
287294
continue
288295
}
289296

290-
pendingAssetPaths.push(assetPath)
291-
}
292-
293-
if (pendingAssetPaths.length > 0) {
294-
console.log(`[AtomGit] Uploading ${pendingAssetPaths.length} assets with concurrency ${UPLOAD_CONCURRENCY}`)
295-
await uploadAssets(pendingAssetPaths)
297+
await uploadAsset(assetPath)
296298
}
297299

298300
console.log(`[AtomGit] Release ${RELEASE_TAG} mirror completed`)

0 commit comments

Comments
 (0)