Skip to content

Commit d65664d

Browse files
authored
Merge pull request #4138 from github/henrymercer/bundle-download-errors
Preserve HTTP errors from streaming bundle downloads
2 parents 1cf8f51 + 0224ca9 commit d65664d

3 files changed

Lines changed: 173 additions & 17 deletions

File tree

lib/entry-points.js

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools-download.test.ts

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import { once } from "events";
2+
import * as fs from "fs";
3+
import { ClientRequest, IncomingMessage } from "http";
24
import * as path from "path";
35

6+
import * as core from "@actions/core";
47
import * as toolcache from "@actions/tool-cache";
58
import test from "ava";
9+
import { https } from "follow-redirects";
610
import nock from "nock";
711
import * as sinon from "sinon";
812

913
import { getRunnerLogger } from "./logging";
1014
import * as tar from "./tar";
1115
import { setupTests } from "./testing-utils";
1216
import { downloadAndExtract } from "./tools-download";
13-
import { withTmpDir } from "./util";
17+
import * as util from "./util";
1418

1519
setupTests(test);
1620

1721
test.serial(
1822
"downloadAndExtract reports the durations when downloading before extracting",
1923
async (t) => {
20-
await withTmpDir(async (tmpDir) => {
24+
await util.withTmpDir(async (tmpDir) => {
2125
const archivePath = path.join(tmpDir, "codeql-bundle.tar.gz");
2226
const destination = path.join(tmpDir, "codeql");
2327
sinon.stub(toolcache, "downloadTool").resolves(archivePath);
@@ -43,13 +47,16 @@ test.serial(
4347
test.serial(
4448
"downloadAndExtract falls back to downloading before extracting if streaming fails",
4549
async (t) => {
46-
await withTmpDir(async (tmpDir) => {
50+
await util.withTmpDir(async (tmpDir) => {
4751
sinon.stub(process, "platform").value("linux");
4852
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
4953
const destination = path.join(tmpDir, "codeql");
5054
const downloadTool = sinon
5155
.stub(toolcache, "downloadTool")
52-
.resolves(archivePath);
56+
.callsFake(async () => {
57+
t.false(fs.existsSync(destination));
58+
return archivePath;
59+
});
5360
const extract = sinon.stub(tar, "extract").resolves(destination);
5461
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
5562
const request = nock("https://example.com")
@@ -78,10 +85,134 @@ test.serial(
7885
},
7986
);
8087

88+
test.serial(
89+
"downloadAndExtract rethrows a 404 rather than retrying the download",
90+
async (t) => {
91+
await util.withTmpDir(async (tmpDir) => {
92+
sinon.stub(process, "platform").value("linux");
93+
const destination = path.join(tmpDir, "codeql");
94+
const downloadTool = sinon.stub(toolcache, "downloadTool");
95+
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
96+
const request = nock("https://example.com")
97+
.get("/codeql-bundle.tar.zst")
98+
.reply(404, "Not found");
99+
100+
const error = await t.throwsAsync(
101+
downloadAndExtract(
102+
"https://example.com/codeql-bundle.tar.zst",
103+
"zstd",
104+
destination,
105+
undefined,
106+
{},
107+
{ type: "gnu", version: "1.34" },
108+
getRunnerLogger(true),
109+
),
110+
{
111+
instanceOf: util.HTTPError,
112+
message:
113+
"Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst. HTTP status code: 404.",
114+
},
115+
);
116+
117+
t.is(error?.status, 404);
118+
t.true(request.isDone());
119+
t.false(extractTarZst.called);
120+
t.false(downloadTool.called);
121+
t.false(fs.existsSync(destination));
122+
});
123+
},
124+
);
125+
126+
test.serial(
127+
"downloadAndExtract falls back to downloading before extracting on a server error",
128+
async (t) => {
129+
await util.withTmpDir(async (tmpDir) => {
130+
sinon.stub(process, "platform").value("linux");
131+
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
132+
const destination = path.join(tmpDir, "codeql");
133+
const downloadTool = sinon
134+
.stub(toolcache, "downloadTool")
135+
.callsFake(async () => {
136+
t.false(fs.existsSync(destination));
137+
return archivePath;
138+
});
139+
const extract = sinon.stub(tar, "extract").resolves(destination);
140+
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
141+
const request = nock("https://example.com")
142+
.get("/codeql-bundle.tar.zst")
143+
.reply(500);
144+
145+
const statusReport = await downloadAndExtract(
146+
"https://example.com/codeql-bundle.tar.zst",
147+
"zstd",
148+
destination,
149+
undefined,
150+
{},
151+
{ type: "gnu", version: "1.34" },
152+
getRunnerLogger(true),
153+
);
154+
155+
t.assert(Number.isInteger(statusReport.downloadDurationMs));
156+
t.true(request.isDone());
157+
t.false(extractTarZst.called);
158+
t.true(downloadTool.calledOnce);
159+
t.true(extract.calledOnce);
160+
});
161+
},
162+
);
163+
164+
test.serial(
165+
"downloadAndExtract handles an unknown status as a non-HTTP error",
166+
async (t) => {
167+
const asHTTPError = sinon.spy(util, "asHTTPError");
168+
await util.withTmpDir(async (tmpDir) => {
169+
sinon.stub(process, "platform").value("linux");
170+
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
171+
const destination = path.join(tmpDir, "codeql");
172+
const response = sinon.createStubInstance(IncomingMessage);
173+
response.statusCode = undefined;
174+
sinon
175+
.stub(https, "get")
176+
.callsArgWith(2, response)
177+
.returns(sinon.createStubInstance(ClientRequest));
178+
const warning = sinon.stub(core, "warning");
179+
const downloadTool = sinon
180+
.stub(toolcache, "downloadTool")
181+
.resolves(archivePath);
182+
const extract = sinon.stub(tar, "extract").resolves(destination);
183+
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
184+
185+
await downloadAndExtract(
186+
"https://example.com/codeql-bundle.tar.zst",
187+
"zstd",
188+
destination,
189+
undefined,
190+
{},
191+
{ type: "gnu", version: "1.34" },
192+
getRunnerLogger(true),
193+
);
194+
195+
t.is(
196+
warning.firstCall.args[0],
197+
"Failed to download and extract CodeQL bundle using streaming with error: Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst.",
198+
);
199+
t.true(response.resume.calledOnce);
200+
t.false(extractTarZst.called);
201+
t.true(downloadTool.calledOnce);
202+
t.true(extract.calledOnce);
203+
});
204+
205+
t.true(asHTTPError.calledOnce);
206+
t.true(asHTTPError.firstCall.args[0] instanceof Error);
207+
t.false(asHTTPError.firstCall.args[0] instanceof util.HTTPError);
208+
t.is(asHTTPError.firstCall.returnValue, undefined);
209+
},
210+
);
211+
81212
test.serial(
82213
"downloadAndExtract reports only the total duration when streaming extraction",
83214
async (t) => {
84-
await withTmpDir(async (tmpDir) => {
215+
await util.withTmpDir(async (tmpDir) => {
85216
sinon.stub(process, "platform").value("linux");
86217
const downloadTool = sinon.stub(toolcache, "downloadTool");
87218
const extractTarZst = sinon

src/tools-download.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import { ActionState } from "./action-common";
1414
import { ActionsEnvVars, getEnv, ReadOnlyEnv } from "./environment";
1515
import { formatDuration, Logger } from "./logging";
1616
import * as tar from "./tar";
17-
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
17+
import {
18+
asHTTPError,
19+
cleanUpPath,
20+
getErrorMessage,
21+
getRequiredEnvParam,
22+
HTTPError,
23+
} from "./util";
1824

1925
/**
2026
* High watermark to use when streaming the download and extraction of the CodeQL tools.
@@ -88,14 +94,20 @@ export async function downloadAndExtract(
8894
return { totalDurationMs };
8995
}
9096
} catch (e) {
97+
// If we failed during processing, we want to clean up the destination directory
98+
// before we either try again or give up.
99+
await cleanUpPath(dest, "CodeQL bundle", logger);
100+
101+
// Retrying a 404 is pointless: the asset does not exist, so downloading it a different way
102+
// will fail in the same way.
103+
if (asHTTPError(e)?.status === 404) {
104+
throw e;
105+
}
106+
91107
core.warning(
92108
`Failed to download and extract CodeQL bundle using streaming with error: ${getErrorMessage(e)}`,
93109
);
94110
core.warning(`Falling back to downloading the bundle before extracting.`);
95-
96-
// If we failed during processing, we want to clean up the destination directory
97-
// before we try again.
98-
await cleanUpPath(dest, "CodeQL bundle", logger);
99111
}
100112

101113
const toolsDownloadStart = performance.now();
@@ -191,9 +203,14 @@ async function downloadAndExtractZstdWithStreaming(
191203
if (response.statusCode !== 200) {
192204
// Discard the response body so that the connection can be released.
193205
response.resume();
194-
throw new Error(
195-
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
196-
);
206+
const baseMessage = `Failed to download CodeQL bundle from ${codeqlURL}.`;
207+
if (response.statusCode !== undefined) {
208+
throw new HTTPError(
209+
`${baseMessage} HTTP status code: ${response.statusCode}.`,
210+
response.statusCode,
211+
);
212+
}
213+
throw new Error(baseMessage);
197214
}
198215

199216
await tar.extractTarZst(response, dest, tarVersion, logger);

0 commit comments

Comments
 (0)