Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lib/entry-points.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 82 additions & 2 deletions src/tools-download.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { once } from "events";
import * as fs from "fs";
import * as path from "path";

import * as toolcache from "@actions/tool-cache";
Expand All @@ -10,7 +11,7 @@ import { getRunnerLogger } from "./logging";
import * as tar from "./tar";
import { setupTests } from "./testing-utils";
import { downloadAndExtract } from "./tools-download";
import { withTmpDir } from "./util";
import { HTTPError, withTmpDir } from "./util";

setupTests(test);

Expand Down Expand Up @@ -49,7 +50,10 @@ test.serial(
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.resolves(archivePath);
.callsFake(async () => {
t.false(fs.existsSync(destination));
return archivePath;
});
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
Expand Down Expand Up @@ -78,6 +82,82 @@ test.serial(
},
);

test.serial(
"downloadAndExtract rethrows a 404 rather than retrying the download",
async (t) => {
await withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon.stub(toolcache, "downloadTool");
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
.get("/codeql-bundle.tar.zst")
.reply(404, "Not found");

const error = await t.throwsAsync(
downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
),
{
instanceOf: HTTPError,
message:
"Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst. HTTP status code: 404.",
},
);

t.is(error?.status, 404);
t.true(request.isDone());
t.false(extractTarZst.called);
t.false(downloadTool.called);
t.false(fs.existsSync(destination));
});
},
);

test.serial(
"downloadAndExtract falls back to downloading before extracting on a server error",
async (t) => {
await withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.callsFake(async () => {
t.false(fs.existsSync(destination));
return archivePath;
});
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
.get("/codeql-bundle.tar.zst")
.reply(500);

const statusReport = await downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
);

t.assert(Number.isInteger(statusReport.downloadDurationMs));
t.true(request.isDone());
t.false(extractTarZst.called);
t.true(downloadTool.calledOnce);
t.true(extract.calledOnce);
});
},
);

test.serial(
"downloadAndExtract reports only the total duration when streaming extraction",
async (t) => {
Expand Down
25 changes: 19 additions & 6 deletions src/tools-download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { ActionState } from "./action-common";
import { ActionsEnvVars, getEnv, ReadOnlyEnv } from "./environment";
import { formatDuration, Logger } from "./logging";
import * as tar from "./tar";
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
import {
asHTTPError,
cleanUpPath,
getErrorMessage,
getRequiredEnvParam,
HTTPError,
} from "./util";

/**
* High watermark to use when streaming the download and extraction of the CodeQL tools.
Expand Down Expand Up @@ -88,14 +94,20 @@ export async function downloadAndExtract(
return { totalDurationMs };
}
} catch (e) {
// If we failed during processing, we want to clean up the destination directory
// before we either try again or give up.
await cleanUpPath(dest, "CodeQL bundle", logger);

// Retrying a 404 is pointless: the asset does not exist, so downloading it a different way
// will fail in the same way.
if (asHTTPError(e)?.status === 404) {
throw e;
}

core.warning(
`Failed to download and extract CodeQL bundle using streaming with error: ${getErrorMessage(e)}`,
);
core.warning(`Falling back to downloading the bundle before extracting.`);

// If we failed during processing, we want to clean up the destination directory
// before we try again.
await cleanUpPath(dest, "CodeQL bundle", logger);
}

const toolsDownloadStart = performance.now();
Expand Down Expand Up @@ -191,8 +203,9 @@ async function downloadAndExtractZstdWithStreaming(
if (response.statusCode !== 200) {
// Discard the response body so that the connection can be released.
response.resume();
throw new Error(
throw new HTTPError(
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
response.statusCode ?? 0,
);
Comment on lines 203 to 209
}

Expand Down
Loading