Parse GitHub release URLs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Henry Mercer
2026-09-25 17:40:24 +01:00
parent 78a5dc251b
commit 4a4922e538
2 changed files with 152 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import {
BundleSelectionOptions,
getPublicRelease,
getRelease,
parseCodeQLReleaseUrl,
selectBundle,
} from "./codeql-release";
import { ActionsEnvVars } from "./environment";
@@ -17,6 +18,7 @@ import {
getTestEnv,
initAllState,
LoggedMessage,
SAMPLE_DOTCOM_API_DETAILS,
} from "./testing-utils";
import { ConfigurationError, GitHubVariant } from "./util";
@@ -198,3 +200,93 @@ test("getPublicRelease constructs download URLs without looking up the release",
);
t.deepEqual(fixture.requests, []);
});
test("parseCodeQLReleaseUrl accepts web and legacy links and decodes tags once", (t) => {
for (const [suffix, tagName] of [
[`tag/${TAG}`, TAG],
[TAG, TAG],
["codeql-bundle-20230120", "codeql-bundle-20230120"],
["codeql-bundle-v2.27.1-rc.1", "codeql-bundle-v2.27.1-rc.1"],
["tag/run-123", "run-123"],
["tag/build/123", "build/123"],
["tag/build%2F123%2Brc%231", "build/123+rc#1"],
["tag/build%252F123", "build%2F123"],
[`tag/${TAG}/?expanded=true#assets`, TAG],
]) {
t.deepEqual(
parseCodeQLReleaseUrl(
`https://github.com/octo/tools/releases/${suffix}`,
SAMPLE_DOTCOM_API_DETAILS,
),
{ ...REFERENCE, isCurrentInstance: true, tagName },
);
}
t.throws(
() =>
parseCodeQLReleaseUrl(
"https://github.com/octo/tools/releases/tag/%zz",
SAMPLE_DOTCOM_API_DETAILS,
),
{ instanceOf: ConfigurationError, message: /Invalid URL encoding/ },
);
});
test("parseCodeQLReleaseUrl matches the current instance and GitHub.com by origin", (t) => {
for (const [url, origin, serverURL] of [
["https://github.example.test", "https://github.com", "https://github.com"],
[
"https://github.example.test/",
"https://github.example.test",
"https://github.example.test",
],
[
"https://GitHub.Example.test",
"https://github.example.test",
"https://github.example.test",
],
[
"https://github.example.test:443",
"https://GITHUB.example.test:443",
"https://github.example.test",
],
]) {
t.deepEqual(
parseCodeQLReleaseUrl(`${origin}/octo/tools/releases/tag/${TAG}`, {
auth: "token",
url,
apiURL: undefined,
}),
{
...REFERENCE,
serverURL,
isCurrentInstance: serverURL !== "https://github.com",
},
`${url} ${origin}`,
);
}
});
test("parseCodeQLReleaseUrl excludes archives, REST references and untrusted URLs", (t) => {
for (const input of [
"/tmp/codeql-bundle.tar.zst",
"nightly",
`https://github.com/octo/tools/releases/download/${TAG}/${JAVA}`,
"https://api.github.com/repos/octo/tools/releases/assets/123",
"https://api.github.com/repos/octo/tools/releases/123",
`https://api.github.com/repos/octo/tools/releases/tags/${TAG}`,
"https://github.com/octo/tools/releases/latest",
"https://github.com/octo/tools/releases/tag/",
"https://github.com/octo/tools/releases/run-123",
`https://github.com.example.test/octo/tools/releases/tag/${TAG}`,
`https://github.com:8443/octo/tools/releases/tag/${TAG}`,
`https://github.com@example.test/octo/tools/releases/tag/${TAG}`,
`https://user@github.com/octo/tools/releases/tag/${TAG}`,
`http://github.com/octo/tools/releases/tag/${TAG}`,
]) {
t.is(
parseCodeQLReleaseUrl(input, SAMPLE_DOTCOM_API_DETAILS),
undefined,
input,
);
}
});

View File

@@ -1,6 +1,7 @@
import * as semver from "semver";
import { ActionState } from "./action-common";
import type { GitHubApiDetails } from "./api-client";
import { CodeQLBundle, getCodeQLBundleName } from "./codeql-bundle";
import { CODEQL_VERSION_ZSTD_BUNDLE } from "./feature-flags";
import {
@@ -9,7 +10,7 @@ import {
} from "./per-language-bundles";
import { BundlePlatform } from "./platform";
import type { CompressionMethod } from "./tar";
import { ConfigurationError, GitHubVariant } from "./util";
import { ConfigurationError, GITHUB_DOTCOM_URL, GitHubVariant } from "./util";
/** Identifies a release on a GitHub instance. */
export interface CodeQLReleaseReference {
@@ -27,6 +28,64 @@ export interface CodeQLRelease {
getAssetURL(name: string): string | undefined;
}
/** A release requested by URL, on this GitHub instance or on GitHub.com. */
export interface RequestedRelease extends CodeQLReleaseReference {
isCurrentInstance: boolean;
}
/**
* Recognizes release pages, including legacy bundle links, but never asset URLs.
*
* We only accept https URLs without credentials, on this GitHub instance, whose API we can use, or
* on GitHub.com, whose public releases we can download without credentials. The tag is
* percent-decoded once, and may contain `/`.
*/
export function parseCodeQLReleaseUrl(
input: string,
apiDetails: GitHubApiDetails,
): RequestedRelease | undefined {
let url: URL;
try {
url = new URL(input);
} catch {
return undefined;
}
if (url.protocol !== "https:" || url.username || url.password) {
return undefined;
}
// GitHub instances are served from the root of their origin.
const isCurrentInstance = url.origin === new URL(apiDetails.url).origin;
if (!isCurrentInstance && url.origin !== new URL(GITHUB_DOTCOM_URL).origin) {
return undefined;
}
// Older links to bundle releases omit "tag/", which GitHub still supports. We only accept this
// form for bundle tags, since other names can clash with routes like `releases/latest`.
const match = url.pathname
.replace(/\/$/, "")
.match(
/^\/([\w.-]+)\/([\w.-]+)\/releases\/(?:tag\/(.+)|(codeql-bundle-[^/]+))$/,
);
if (match === null) {
return undefined;
}
let tagName: string;
try {
tagName = decodeURIComponent(match[3] ?? match[4]);
} catch {
throw new ConfigurationError(
"Invalid URL encoding in the CodeQL release tag.",
);
}
return {
serverURL: url.origin,
isCurrentInstance,
owner: match[1],
repo: match[2],
tagName,
};
}
/**
* Encodes a tag for use in a URL path. Slashes stay as path separators, as in GitHub's release URLs
* for tags like `build/123`.