| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th | |||
| 4 | 4 | ||
| 5 | 5 | ## [UNRELEASED] | |
| 6 | 6 | ||
| 7 | - No user facing changes. | ||
| 7 | + - Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the `init` Action instead of falling back to downloading the bundle before extracting it. [#4061](https://github.com/github/codeql-action/pull/4061) | ||
| 8 | 8 | ||
| 9 | 9 | ## 4.37.4 - 29 Jul 2026 | |
| 10 | 10 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,33 @@ | |||
| 1 | + import * as path from "path"; | ||
| 2 | + import * as stream from "stream"; | ||
| 3 | + | ||
| 4 | + import test from "ava"; | ||
| 5 | + | ||
| 6 | + import { getRunnerLogger } from "./logging"; | ||
| 7 | + import { extractTarZst } from "./tar"; | ||
| 8 | + import { setupTests } from "./testing-utils"; | ||
| 9 | + import { withTmpDir } from "./util"; | ||
| 10 | + | ||
| 11 | + setupTests(test); | ||
| 12 | + | ||
| 13 | + test("extractTarZst rejects if the input stream errors", async (t) => { | ||
| 14 | + await withTmpDir(async (tmpDir) => { | ||
| 15 | + const archive = new stream.PassThrough(); | ||
| 16 | + const promise = extractTarZst( | ||
| 17 | + archive, | ||
| 18 | + path.join(tmpDir, "dest"), | ||
| 19 | + { type: "gnu", version: "1.34" }, | ||
| 20 | + getRunnerLogger(true), | ||
| 21 | + ); | ||
| 22 | + | ||
| 23 | + archive.destroy( | ||
| 24 | + Object.assign(new Error("socket hang up"), { | ||
| 25 | + code: "ECONNRESET", | ||
| 26 | + }), | ||
| 27 | + ); | ||
| 28 | + | ||
| 29 | + await t.throwsAsync(promise, { | ||
| 30 | + message: /Error while downloading and extracting tar/, | ||
| 31 | + }); | ||
| 32 | + }); | ||
| 33 | + }); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -194,10 +194,15 @@ export async function extractTarZst( | |||
| 194 | 194 | }); | |
| 195 | 195 | ||
| 196 | 196 | if (tar instanceof stream.Readable) { | |
| 197 | - tar.pipe(tarProcess.stdin).on("error", (err) => { | ||
| 198 | - reject( | ||
| 199 | - new Error(`Error while downloading and extracting tar: ${err}`), | ||
| 200 | - ); | ||
| 197 | + // Use `pipeline` rather than `pipe` so that an error on either stream is reported here | ||
| 198 | + // rather than being emitted as an unhandled `error` event, and so that `tar`'s standard | ||
| 199 | + // input is closed if the download fails partway through. | ||
| 200 | + stream.pipeline(tar, tarProcess.stdin, (err) => { | ||
| 201 | + if (err) { | ||
| 202 | + reject( | ||
| 203 | + new Error(`Error while downloading and extracting tar: ${err}`), | ||
| 204 | + ); | ||
| 205 | + } | ||
| 201 | 206 | }); | |
| 202 | 207 | } | |
| 203 | 208 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,43 @@ test.serial( | |||
| 38 | 38 | }, | |
| 39 | 39 | ); | |
| 40 | 40 | ||
| 41 | + test.serial( | ||
| 42 | + "downloadAndExtract falls back to downloading before extracting if streaming fails", | ||
| 43 | + async (t) => { | ||
| 44 | + await withTmpDir(async (tmpDir) => { | ||
| 45 | + sinon.stub(process, "platform").value("linux"); | ||
| 46 | + const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst"); | ||
| 47 | + const destination = path.join(tmpDir, "codeql"); | ||
| 48 | + const downloadTool = sinon | ||
| 49 | + .stub(toolcache, "downloadTool") | ||
| 50 | + .resolves(archivePath); | ||
| 51 | + const extract = sinon.stub(tar, "extract").resolves(destination); | ||
| 52 | + const extractTarZst = sinon.stub(tar, "extractTarZst").resolves(); | ||
| 53 | + const request = nock("https://example.com") | ||
| 54 | + .get("/codeql-bundle.tar.zst") | ||
| 55 | + .replyWithError( | ||
| 56 | + Object.assign(new Error("socket hang up"), { code: "ECONNRESET" }), | ||
| 57 | + ); | ||
| 58 | + | ||
| 59 | + const statusReport = await downloadAndExtract( | ||
| 60 | + "https://example.com/codeql-bundle.tar.zst", | ||
| 61 | + "zstd", | ||
| 62 | + destination, | ||
| 63 | + undefined, | ||
| 64 | + {}, | ||
| 65 | + { type: "gnu", version: "1.34" }, | ||
| 66 | + getRunnerLogger(true), | ||
| 67 | + ); | ||
| 68 | + | ||
| 69 | + t.assert(Number.isInteger(statusReport.downloadDurationMs)); | ||
| 70 | + t.true(request.isDone()); | ||
| 71 | + t.false(extractTarZst.called); | ||
| 72 | + t.true(downloadTool.calledOnce); | ||
| 73 | + t.true(extract.calledOnce); | ||
| 74 | + }); | ||
| 75 | + }, | ||
| 76 | + ); | ||
| 77 | + | ||
| 41 | 78 | test.serial( | |
| 42 | 79 | "downloadAndExtract omits the download duration when streaming extraction", | |
| 43 | 80 | async (t) => { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,6 +19,12 @@ import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util"; | |||
| 19 | 19 | */ | |
| 20 | 20 | const STREAMING_HIGH_WATERMARK_BYTES = 4 * 1024 * 1024; // 4 MiB | |
| 21 | 21 | ||
| 22 | + /** | ||
| 23 | + * How long the streaming download of the CodeQL tools may stall for before we abort it. This | ||
| 24 | + * applies both to establishing the connection and to gaps between chunks of the response body. | ||
| 25 | + */ | ||
| 26 | + const STREAMING_STALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes | ||
| 27 | + | ||
| 22 | 28 | /** | |
| 23 | 29 | * The name of the tool cache directory for the CodeQL tools. | |
| 24 | 30 | */ | |
@@ -137,8 +143,8 @@ async function downloadAndExtractZstdWithStreaming( | |||
| 137 | 143 | authorization ? { authorization } : {}, | |
| 138 | 144 | headers, | |
| 139 | 145 | ); | |
| 140 | - const response = await new Promise<IncomingMessage>((resolve) => | ||
| 141 | - https.get( | ||
| 146 | + const response = await new Promise<IncomingMessage>((resolve, reject) => { | ||
| 147 | + const request = https.get( | ||
| 142 | 148 | codeqlURL, | |
| 143 | 149 | { | |
| 144 | 150 | headers, | |
@@ -148,10 +154,24 @@ async function downloadAndExtractZstdWithStreaming( | |||
| 148 | 154 | agent, | |
| 149 | 155 | } as unknown as RequestOptions, | |
| 150 | 156 | (r) => resolve(r), | |
| 151 | - ), | ||
| 152 | - ); | ||
| 157 | + ); | ||
| 158 | + // Without this listener, connection failures such as `ECONNRESET` are emitted as unhandled | ||
| 159 | + // `error` events, which terminate the process instead of letting us fall back to downloading | ||
| 160 | + // the bundle before extracting it. This listener stays attached after the response arrives, so | ||
| 161 | + // it also handles errors that occur while the response is being streamed. | ||
| 162 | + request.on("error", reject); | ||
| 163 | + request.setTimeout(STREAMING_STALL_TIMEOUT_MS, () => { | ||
| 164 | + request.destroy( | ||
| 165 | + new Error( | ||
| 166 | + `No data received for ${formatDuration(STREAMING_STALL_TIMEOUT_MS)}.`, | ||
| 167 | + ), | ||
| 168 | + ); | ||
| 169 | + }); | ||
| 170 | + }); | ||
| 153 | 171 | ||
| 154 | 172 | if (response.statusCode !== 200) { | |
| 173 | + // Discard the response body so that the connection can be released. | ||
| 174 | + response.resume(); | ||
| 155 | 175 | throw new Error( | |
| 156 | 176 | `Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`, | |
| 157 | 177 | ); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments