diff --git a/lib/entry-points.js b/lib/entry-points.js index c91850e7a..ab20d259a 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151053,7 +151053,7 @@ function getInvalidConfigFileMessage(configFile, messages) { } function getConfigFileRepoFormatInvalidMessage(configFile) { let error3 = `The configuration file "${configFile}" is not a supported remote file reference.`; - error3 += " Expected format [/][/][@]"; + error3 += " Expected format [/][@][:]"; return error3; } function getConfigFileFormatInvalidMessage(configFile) { @@ -151447,9 +151447,28 @@ function getDefaultOwner(env) { } return nwoParts[0].trim(); } +var OLD_REMOTE_ADDRESS_FORMAT = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)" +); +function parseOldRemoteFileAddress(input) { + const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input); + if (pieces?.groups === void 0 || pieces.length < 5) { + return new Failure(void 0); + } + return new Success({ + owner: pieces.groups.owner.trim(), + repo: pieces.groups.repo.trim(), + path: pieces.groups.path.trim(), + ref: pieces.groups.ref.trim() + }); +} function parseRemoteFileAddress(env, configFile) { + const oldFormatAddressResult = parseOldRemoteFileAddress(configFile); + if (oldFormatAddressResult.isSuccess()) { + return oldFormatAddressResult.value; + } const format = new RegExp( - "^((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?$" + "^((?[^:@/]+)/)?(?[^:@/]+)(@(?[^:]+))?(:(?.+))?$" ); const pieces = format.exec(configFile.trim()); const repo = pieces?.groups?.repo?.trim(); diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index 36b68fd55..f0ef1c13c 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -15,6 +15,7 @@ import { test("parseRemoteFileAddress accepts full remote addresses", async (t) => { const env = getTestEnv(); + // Old format. t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ref"), { owner: "owner", repo: "repo", @@ -86,6 +87,79 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { ref: "ref/feature", } satisfies RemoteFileAddress, ); + + // New format. + t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref:path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner /repo@ref:path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/ repo@ref:path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo @ref:path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ ref:path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref :path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref: path"), { + owner: "owner", + repo: "repo", + path: "path", + ref: "ref", + } satisfies RemoteFileAddress); + + t.deepEqual( + parseRemoteFileAddress(env, "owner/repo@ref/feature:path/to/codeql.yml"), + { + owner: "owner", + repo: "repo", + path: "path/to/codeql.yml", + ref: "ref/feature", + } satisfies RemoteFileAddress, + ); + + t.deepEqual( + parseRemoteFileAddress( + env, + " owner/repo@ref/feature:path/to/codeql.yml ", + ), + { + owner: "owner", + repo: "repo", + path: "path/to/codeql.yml", + ref: "ref/feature", + } satisfies RemoteFileAddress, + ); }); test("parseRemoteFileAddress accepts remote address without an owner", async (t) => { @@ -96,6 +170,13 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t) .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) .returns(`${owner}/current-repo`); + t.deepEqual(parseRemoteFileAddress(env, "repo@ref:path.yml"), { + owner, + repo: "repo", + path: "path.yml", + ref: "ref", + } satisfies RemoteFileAddress); + t.deepEqual(parseRemoteFileAddress(env, "repo@ref"), { owner, repo: "repo", @@ -103,6 +184,13 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t) ref: "ref", } satisfies RemoteFileAddress); + t.deepEqual(parseRemoteFileAddress(env, "repo:path.yml"), { + owner, + repo: "repo", + path: "path.yml", + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); + t.deepEqual(parseRemoteFileAddress(env, "repo"), { owner, repo: "repo", @@ -142,14 +230,7 @@ test("parseRemoteFileAddress accepts remote address without a path", async (t) = test("parseRemoteFileAddress accepts remote address without a ref", async (t) => { const env = getTestEnv(); - t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path"), { - owner: "owner", - repo: "repo", - path: "path", - ref: DEFAULT_CONFIG_FILE_REF, - } satisfies RemoteFileAddress); - - t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@"), { + t.deepEqual(parseRemoteFileAddress(env, "owner/repo:path"), { owner: "owner", repo: "repo", path: "path", @@ -171,10 +252,43 @@ test("parseRemoteFileAddress rejects invalid values", async (t) => { t.throws(() => parseRemoteFileAddress(env, "repo//absolute"), { instanceOf: ConfigurationError, }); + t.throws(() => parseRemoteFileAddress(env, "repo:/absolute"), { + instanceOf: ConfigurationError, + }); t.throws(() => parseRemoteFileAddress(env, "/repo@ref"), { instanceOf: ConfigurationError, }); t.throws(() => parseRemoteFileAddress(env, " /repo@ref"), { instanceOf: ConfigurationError, }); + t.throws(() => parseRemoteFileAddress(env, "repo@"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "repo:"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "repo/"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "/repo"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, ":path"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "@ref"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "@ref:path"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "owner/@ref:path"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "owner/@ref"), { + instanceOf: ConfigurationError, + }); + t.throws(() => parseRemoteFileAddress(env, "owner/:path"), { + instanceOf: ConfigurationError, + }); }); diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index a4f8ea890..af68380e1 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -1,7 +1,7 @@ import { ActionsEnvVars } from "../actions-util"; import { Env } from "../environment"; import * as errorMessages from "../error-messages"; -import { ConfigurationError } from "../util"; +import { ConfigurationError, Failure, Result, Success } from "../util"; /** Represents remote file addresses. */ export interface RemoteFileAddress { @@ -37,6 +37,38 @@ function getDefaultOwner(env: Env): string { return nwoParts[0].trim(); } +/** + * The old remote address format that's always been supported for the `config-file` input. + * All the components are required. Unchanged from the previous implementation. + */ +const OLD_REMOTE_ADDRESS_FORMAT = new RegExp( + "(?[^/]+)/(?[^/]+)/(?[^@]+)@(?.*)", +); + +/** + * Attempts to parse `input` as a `RemoteFileAddress` using the old format. + * + * @param input The input to try and parse. + * @returns A `RemoteFileAddress` value if successful or `undefined` otherwise. + */ +function parseOldRemoteFileAddress( + input: string, +): Result { + const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input); + + // 5 = 4 groups + the whole expression + if (pieces?.groups === undefined || pieces.length < 5) { + return new Failure(undefined); + } + + return new Success({ + owner: pieces.groups.owner.trim(), + repo: pieces.groups.repo.trim(), + path: pieces.groups.path.trim(), + ref: pieces.groups.ref.trim(), + }); +} + /** * Attempts to parse `configFile` into an array of `RemoteFileAddress` components. * @@ -49,9 +81,17 @@ export function parseRemoteFileAddress( env: Env, configFile: string, ): RemoteFileAddress { + // Try to parse the input using the old format. If successful, return the + // resulting `RemoteFileAddress`. Otherwise, continue using the new format. + const oldFormatAddressResult = parseOldRemoteFileAddress(configFile); + + if (oldFormatAddressResult.isSuccess()) { + return oldFormatAddressResult.value; + } + // retrieve the various parts of the config location, and ensure they're present const format = new RegExp( - "^((?[^/]+)/)?(?[^/@]+)(/(?[^@]+))?(@(?.*))?$", + "^((?[^:@/]+)/)?(?[^:@/]+)(@(?[^:]+))?(:(?.+))?$", ); const pieces = format.exec(configFile.trim()); @@ -59,6 +99,9 @@ export function parseRemoteFileAddress( // Check that the regular expression matched and that we have at least the repo name. if (!pieces?.groups || !repo || repo.length === 0) { + // Neither the old format nor the new format worked. Throw an error that + // explains the format we accept. We only mention the new format, since that's + // what we want to be used going forward. throw new ConfigurationError( errorMessages.getConfigFileRepoFormatInvalidMessage(configFile), ); diff --git a/src/error-messages.ts b/src/error-messages.ts index b45d9562b..377310933 100644 --- a/src/error-messages.ts +++ b/src/error-messages.ts @@ -34,7 +34,7 @@ export function getConfigFileRepoFormatInvalidMessage( configFile: string, ): string { let error = `The configuration file "${configFile}" is not a supported remote file reference.`; - error += " Expected format [/][/][@]"; + error += " Expected format [/][@][:]"; return error; }