Support old and new formats

This commit is contained in:
Michael B. Gale
2026-06-26 18:09:25 +01:00
parent 812b882dee
commit f77cf557cd
4 changed files with 189 additions and 13 deletions

23
lib/entry-points.js generated
View File

@@ -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 [<owner>/]<repository>[/<file-path>][@<ref>]";
error3 += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
return error3;
}
function getConfigFileFormatInvalidMessage(configFile) {
@@ -151447,9 +151447,28 @@ function getDefaultOwner(env) {
}
return nwoParts[0].trim();
}
var OLD_REMOTE_ADDRESS_FORMAT = new RegExp(
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)"
);
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(
"^((?<owner>[^/]+)/)?(?<repo>[^/@]+)(/(?<path>[^@]+))?(@(?<ref>.*))?$"
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$"
);
const pieces = format.exec(configFile.trim());
const repo = pieces?.groups?.repo?.trim();

View File

@@ -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,
});
});

View File

@@ -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(
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
);
/**
* 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<RemoteFileAddress, undefined> {
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(
"^((?<owner>[^/]+)/)?(?<repo>[^/@]+)(/(?<path>[^@]+))?(@(?<ref>.*))?$",
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
);
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),
);

View File

@@ -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 [<owner>/]<repository>[/<file-path>][@<ref>]";
error += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
return error;
}