diff --git a/packages/cli/src/util/deploy/process-deployment.ts b/packages/cli/src/util/deploy/process-deployment.ts index 3c001d228..56e01b0fe 100644 --- a/packages/cli/src/util/deploy/process-deployment.ts +++ b/packages/cli/src/util/deploy/process-deployment.ts @@ -115,29 +115,39 @@ export default async function processDeployment({ .reduce((a: number, b: number) => a + b, 0); const totalSizeHuman = bytes.format(missingSize, { decimalPlaces: 1 }); - uploads.forEach((e: any) => - e.on('progress', () => { - const uploadedBytes = uploads.reduce((acc: number, e: any) => { - return acc + e.bytesUploaded; - }, 0); + // When stderr is not a TTY then we only want to + // print upload progress in 25% increments + let nextStep = 0; + const stepSize = now._client.stderr.isTTY ? 0 : 0.25; - const bar = progress(uploadedBytes, missingSize); - if (!bar || uploadedBytes === missingSize) { - output.spinner(deployingSpinnerVal, 0); - } else { - const uploadedHuman = bytes.format(uploadedBytes, { - decimalPlaces: 1, - fixedDecimals: true, - }); + const updateProgress = () => { + const uploadedBytes = uploads.reduce((acc: number, e: any) => { + return acc + e.bytesUploaded; + }, 0); + + const bar = progress(uploadedBytes, missingSize); + if (!bar) { + output.spinner(deployingSpinnerVal, 0); + } else { + const uploadedHuman = bytes.format(uploadedBytes, { + decimalPlaces: 1, + fixedDecimals: true, + }); + const percent = uploadedBytes / missingSize; + if (percent >= nextStep) { output.spinner( `Uploading ${chalk.reset( `[${bar}] (${uploadedHuman}/${totalSizeHuman})` )}`, 0 ); + nextStep += stepSize; } - }) - ); + } + }; + + uploads.forEach((e: any) => e.on('progress', updateProgress)); + updateProgress(); } if (event.type === 'file-uploaded') { diff --git a/packages/cli/test/unit/commands/deploy.test.ts b/packages/cli/test/unit/commands/deploy.test.ts index 3266288f1..5bbaeeb81 100644 --- a/packages/cli/test/unit/commands/deploy.test.ts +++ b/packages/cli/test/unit/commands/deploy.test.ts @@ -1,4 +1,7 @@ +import bytes from 'bytes'; +import fs from 'fs-extra'; import { join } from 'path'; +import { randomBytes } from 'crypto'; import { fileNameSymbol } from '@vercel/client'; import { client } from '../../mocks/client'; import deploy from '../../../src/commands/deploy'; @@ -199,4 +202,119 @@ describe('deploy', () => { process.chdir(originalCwd); } }); + + it('should upload missing files', async () => { + const cwd = setupFixture('commands/deploy/archive'); + const originalCwd = process.cwd(); + + // Add random 1mb file + await fs.writeFile(join(cwd, 'data'), randomBytes(bytes('1mb'))); + + try { + process.chdir(cwd); + + const user = useUser(); + useTeams('team_dummy'); + useProject({ + ...defaultProject, + name: 'archive', + id: 'archive', + }); + + let body: any; + let fileUploaded = false; + client.scenario.post(`/v13/deployments`, (req, res) => { + if (fileUploaded) { + body = req.body; + res.json({ + creator: { + uid: user.id, + username: user.username, + }, + id: 'dpl_archive_test', + }); + } else { + const sha = req.body.files[0].sha; + res.status(400).json({ + error: { + code: 'missing_files', + message: 'Missing files', + missing: [sha], + }, + }); + } + }); + client.scenario.post('/v2/files', (req, res) => { + // Wait for file to be finished uploading + req.on('data', () => { + // Noop + }); + req.on('end', () => { + fileUploaded = true; + res.end(); + }); + }); + client.scenario.get(`/v13/deployments/dpl_archive_test`, (req, res) => { + res.json({ + creator: { + uid: user.id, + username: user.username, + }, + id: 'dpl_archive_test', + readyState: 'READY', + aliasAssigned: true, + alias: [], + }); + }); + client.scenario.get( + `/v10/now/deployments/dpl_archive_test`, + (req, res) => { + res.json({ + creator: { + uid: user.id, + username: user.username, + }, + id: 'dpl_archive_test', + readyState: 'READY', + aliasAssigned: true, + alias: [], + }); + } + ); + + // When stderr is not a TTY we expect 5 progress lines to be printed + client.stderr.isTTY = false; + + client.setArgv('deploy', '--archive=tgz'); + const uploadingLines: string[] = []; + client.stderr.on('data', data => { + if (data.startsWith('Uploading [')) { + uploadingLines.push(data); + } + }); + client.stderr.resume(); + const exitCode = await deploy(client); + expect(exitCode).toEqual(0); + expect(body?.files?.length).toEqual(1); + expect(body?.files?.[0].file).toEqual('.vercel/source.tgz'); + expect(uploadingLines.length).toEqual(5); + expect( + uploadingLines[0].startsWith('Uploading [--------------------]') + ).toEqual(true); + expect( + uploadingLines[1].startsWith('Uploading [=====---------------]') + ).toEqual(true); + expect( + uploadingLines[2].startsWith('Uploading [==========----------]') + ).toEqual(true); + expect( + uploadingLines[3].startsWith('Uploading [===============-----]') + ).toEqual(true); + expect( + uploadingLines[4].startsWith('Uploading [====================]') + ).toEqual(true); + } finally { + process.chdir(originalCwd); + } + }); }); diff --git a/packages/client/src/upload.ts b/packages/client/src/upload.ts index a12441aaa..25c8a0d15 100644 --- a/packages/client/src/upload.ts +++ b/packages/client/src/upload.ts @@ -1,4 +1,5 @@ -import { Agent } from 'https'; +import http from 'http'; +import https from 'https'; import { Readable } from 'stream'; import { EventEmitter } from 'events'; import retry from 'async-retry'; @@ -78,7 +79,9 @@ export async function* upload( debug('Building an upload list...'); const semaphore = new Sema(50, { capacity: 50 }); - const agent = new Agent({ keepAlive: true }); + const agent = apiUrl?.startsWith('https://') + ? new https.Agent({ keepAlive: true }) + : new http.Agent({ keepAlive: true }); shas.forEach((sha, index) => { const uploadProgress = uploads[index];