Gate new remote file format behind FF

This commit is contained in:
Michael B. Gale
2026-06-30 13:19:30 +01:00
parent 7b2af8940d
commit b6be81d5b9
5 changed files with 262 additions and 240 deletions

19
lib/entry-points.js generated
View File

@@ -151056,6 +151056,11 @@ function getInvalidConfigFileMessage(configFile, messages) {
const andMore = messages.length > 10 ? `, and ${messages.length - 10} more.` : "."; const andMore = messages.length > 10 ? `, and ${messages.length - 10} more.` : ".";
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`; return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
} }
function getConfigFileRepoOldFormatInvalidMessage(configFile) {
let error3 = `The configuration file "${configFile}" is not a supported remote file reference.`;
error3 += " Expected format <owner>/<repository>/<file-path>@<ref>";
return error3;
}
function getConfigFileRepoFormatInvalidMessage(configFile) { function getConfigFileRepoFormatInvalidMessage(configFile) {
let error3 = `The configuration file "${configFile}" is not a supported remote file reference.`; let error3 = `The configuration file "${configFile}" is not a supported remote file reference.`;
error3 += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]"; error3 += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
@@ -151467,11 +151472,19 @@ function parseOldRemoteFileAddress(input) {
ref: pieces.groups.ref.trim() ref: pieces.groups.ref.trim()
}); });
} }
function parseRemoteFileAddress(env, configFile) { async function parseRemoteFileAddress(actionState, configFile) {
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile); const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
if (oldFormatAddressResult.isSuccess()) { if (oldFormatAddressResult.isSuccess()) {
return oldFormatAddressResult.value; return oldFormatAddressResult.value;
} }
const allowNewFormat = await actionState.features.getValue(
"new_remote_file_addresses" /* NewRemoteFileAddresses */
);
if (!allowNewFormat) {
throw new ConfigurationError(
getConfigFileRepoOldFormatInvalidMessage(configFile)
);
}
const format = new RegExp( const format = new RegExp(
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$" "^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$"
); );
@@ -151491,7 +151504,7 @@ function parseRemoteFileAddress(env, configFile) {
); );
} }
return { return {
owner: owner || getDefaultOwner(env), owner: owner || getDefaultOwner(actionState.env),
repo, repo,
path: path29 || DEFAULT_CONFIG_FILE_NAME, path: path29 || DEFAULT_CONFIG_FILE_NAME,
ref: ref || DEFAULT_CONFIG_FILE_REF ref: ref || DEFAULT_CONFIG_FILE_REF
@@ -151515,7 +151528,7 @@ function getConfigFileInput(logger, actions, repositoryProperties) {
return void 0; return void 0;
} }
async function getRemoteConfig(actionState, configFile, apiDetails) { async function getRemoteConfig(actionState, configFile, apiDetails) {
const address = parseRemoteFileAddress(getEnv(), configFile); const address = await parseRemoteFileAddress(actionState, configFile);
const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({ const response = await getApiClientWithExternalAuth(apiDetails).rest.repos.getContent({
owner: address.owner, owner: address.owner,
repo: address.repo, repo: address.repo,

View File

@@ -8,7 +8,7 @@ import {
RepositoryPropertyName, RepositoryPropertyName,
} from "../feature-flags/properties"; } from "../feature-flags/properties";
import { Logger } from "../logging"; import { Logger } from "../logging";
import { ConfigurationError, getEnv } from "../util"; import { ConfigurationError } from "../util";
import { parseUserConfig, UserConfig } from "./db-config"; import { parseUserConfig, UserConfig } from "./db-config";
import { parseRemoteFileAddress } from "./remote-file"; import { parseRemoteFileAddress } from "./remote-file";
@@ -55,7 +55,7 @@ export async function getRemoteConfig(
configFile: string, configFile: string,
apiDetails: api.GitHubApiCombinedDetails, apiDetails: api.GitHubApiCombinedDetails,
): Promise<UserConfig> { ): Promise<UserConfig> {
const address = parseRemoteFileAddress(getEnv(), configFile); const address = await parseRemoteFileAddress(actionState, configFile);
const response = await api const response = await api
.getApiClientWithExternalAuth(apiDetails) .getApiClientWithExternalAuth(apiDetails)

View File

@@ -2,7 +2,9 @@ import test from "ava";
import sinon from "sinon"; import sinon from "sinon";
import { ActionsEnvVars } from "../actions-util"; import { ActionsEnvVars } from "../actions-util";
import { getTestEnv } from "../testing-utils"; import * as errors from "../error-messages";
import { Feature } from "../feature-flags";
import { callee, getTestEnv } from "../testing-utils";
import { ConfigurationError } from "../util"; import { ConfigurationError } from "../util";
import { import {
@@ -12,230 +14,220 @@ import {
RemoteFileAddress, RemoteFileAddress,
} from "./remote-file"; } from "./remote-file";
type ParseRemoteFileAddressTest = {
input: string;
expected: RemoteFileAddress;
};
test("parseRemoteFileAddress accepts full remote addresses", async (t) => { test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
const env = getTestEnv(); const target = callee(parseRemoteFileAddress);
// Old format. const expected: RemoteFileAddress = {
t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ref"), {
owner: "owner", owner: "owner",
repo: "repo", repo: "repo",
path: "path", path: "path",
ref: "ref", ref: "ref",
} satisfies RemoteFileAddress); };
t.deepEqual(parseRemoteFileAddress(env, "owner /repo/path@ref"), { const oldFormatInputs: ParseRemoteFileAddressTest[] = [
owner: "owner", { input: "owner/repo/path@ref", expected },
repo: "repo", { input: "owner /repo/path@ref", expected },
path: "path", { input: "owner/ repo/path@ref", expected },
ref: "ref", { input: "owner/repo /path@ref", expected },
} satisfies RemoteFileAddress); { input: "owner/repo/ path@ref", expected },
{ input: "owner/repo/path @ref", expected },
t.deepEqual(parseRemoteFileAddress(env, "owner/ repo/path@ref"), { { input: "owner/repo/path@ ref", expected },
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "owner/repo /path@ref"), {
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "owner/repo/ path@ref"), {
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path @ref"), {
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "owner/repo/path@ ref"), {
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(
parseRemoteFileAddress(env, "owner/repo/path/to/codeql.yml@ref/feature"),
{ {
owner: "owner", input: "owner/repo/path/to/codeql.yml@ref/feature",
repo: "repo", expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
path: "path/to/codeql.yml", },
ref: "ref/feature",
} satisfies RemoteFileAddress,
);
t.deepEqual(
parseRemoteFileAddress(
env,
" owner/repo/path/to/codeql.yml@ref/feature ",
),
{ {
owner: "owner", input: " owner/repo/path/to/codeql.yml@ref/feature ",
repo: "repo", expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
path: "path/to/codeql.yml", },
ref: "ref/feature", ];
} satisfies RemoteFileAddress,
); for (const oldFormatInput of oldFormatInputs) {
await target
.withArgs(oldFormatInput.input)
.passes(async (fn) => t.deepEqual(await fn(), oldFormatInput.expected));
}
// New format. // New format.
t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref:path"), { const newFormatInputs: ParseRemoteFileAddressTest[] = [
owner: "owner", { input: "owner/repo@ref:path", expected },
repo: "repo", { input: "owner /repo@ref:path", expected },
path: "path", { input: "owner/ repo@ref:path", expected },
ref: "ref", { input: "owner/repo @ref:path", expected },
} satisfies RemoteFileAddress); { input: "owner/repo@ ref:path", expected },
{ input: "owner/repo@ref :path", expected },
t.deepEqual(parseRemoteFileAddress(env, "owner /repo@ref:path"), { { input: "owner/repo@ref: path", expected },
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", input: "owner/repo@ref/feature:path/to/codeql.yml",
repo: "repo", expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
path: "path/to/codeql.yml", },
ref: "ref/feature",
} satisfies RemoteFileAddress,
);
t.deepEqual(
parseRemoteFileAddress(
env,
" owner/repo@ref/feature:path/to/codeql.yml ",
),
{ {
owner: "owner", input: " owner/repo@ref/feature:path/to/codeql.yml ",
repo: "repo", expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
path: "path/to/codeql.yml", },
ref: "ref/feature", ];
} satisfies RemoteFileAddress,
); for (const newFormatInput of newFormatInputs) {
const targetWithArgs = target.withArgs(newFormatInput.input);
// Should fail when the FF is not enabled.
await targetWithArgs
.withFeatures([])
.passes(async (fn) =>
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
);
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(async (fn) => t.deepEqual(await fn(), newFormatInput.expected));
}
}); });
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => { test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
const env = getTestEnv(); const target = callee(parseRemoteFileAddress);
const env = target.getState().env;
const owner = "test-owner"; const owner = "test-owner";
const getRequired = sinon.stub(env, "getRequired"); const getRequired = sinon.stub(env, "getRequired");
getRequired getRequired
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY) .withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
.returns(`${owner}/current-repo`); .returns(`${owner}/current-repo`);
t.deepEqual(parseRemoteFileAddress(env, "repo@ref:path.yml"), { const targetWithEnv = target.withEnv(env);
owner,
repo: "repo",
path: "path.yml",
ref: "ref",
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "repo@ref"), { const testCases: ParseRemoteFileAddressTest[] = [
owner, {
repo: "repo", input: "repo@ref:path.yml",
path: DEFAULT_CONFIG_FILE_NAME, expected: {
ref: "ref", owner,
} satisfies RemoteFileAddress); repo: "repo",
path: "path.yml",
ref: "ref",
},
},
{
input: "repo@ref",
expected: {
owner,
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: "ref",
},
},
{
input: "repo:path.yml",
expected: {
owner,
repo: "repo",
path: "path.yml",
ref: DEFAULT_CONFIG_FILE_REF,
},
},
{
input: "repo",
expected: {
owner,
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: DEFAULT_CONFIG_FILE_REF,
},
},
];
t.deepEqual(parseRemoteFileAddress(env, "repo:path.yml"), { for (const testCase of testCases) {
owner, const targetWithArgs = targetWithEnv.withArgs(testCase.input);
repo: "repo",
path: "path.yml",
ref: DEFAULT_CONFIG_FILE_REF,
} satisfies RemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "repo"), { // Should fail when the FF is not enabled.
owner, await targetWithArgs
repo: "repo", .withFeatures([])
path: DEFAULT_CONFIG_FILE_NAME, .passes(async (fn) =>
ref: DEFAULT_CONFIG_FILE_REF, t.throwsAsync(fn, { instanceOf: ConfigurationError }),
} satisfies RemoteFileAddress); );
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
}
}); });
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => { test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
const env = getTestEnv(); const target = callee(parseRemoteFileAddress).withArgs("repo@ref");
const env = target.getState().env;
const getRequired = sinon.stub(env, "getRequired"); const getRequired = sinon.stub(env, "getRequired");
getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`); getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`);
t.throws(() => parseRemoteFileAddress(env, "repo@ref"), { await target
instanceOf: Error, .withEnv(env)
}); .withFeatures([Feature.NewRemoteFileAddresses])
.passes(async (fn) => t.throwsAsync(fn, { instanceOf: Error }));
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
}); });
test("parseRemoteFileAddress accepts remote address without a path", async (t) => { test("parseRemoteFileAddress accepts remote address without a path", async (t) => {
const env = getTestEnv(); const target = callee(parseRemoteFileAddress);
t.deepEqual(parseRemoteFileAddress(env, "owner/repo@ref"), { const testCases: ParseRemoteFileAddressTest[] = [
owner: "owner", {
repo: "repo", input: "owner/repo@ref",
path: DEFAULT_CONFIG_FILE_NAME, expected: {
ref: "ref", owner: "owner",
} satisfies RemoteFileAddress); repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: "ref",
},
},
{
input: "owner/repo",
expected: {
owner: "owner",
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: DEFAULT_CONFIG_FILE_REF,
},
},
];
t.deepEqual(parseRemoteFileAddress(env, "owner/repo"), { for (const testCase of testCases) {
owner: "owner", const targetWithArgs = target.withArgs(testCase.input);
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME, // Should fail when the FF is not enabled.
ref: DEFAULT_CONFIG_FILE_REF, await targetWithArgs
} satisfies RemoteFileAddress); .withFeatures([])
.passes(async (fn) =>
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
);
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
}
}); });
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => { test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
const env = getTestEnv(); const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
t.deepEqual(parseRemoteFileAddress(env, "owner/repo:path"), { // Should only accept the input if the FF is enabled.
owner: "owner", await target.withFeatures([]).passes(t.throwsAsync);
repo: "repo", await target
path: "path", .withFeatures([Feature.NewRemoteFileAddresses])
ref: DEFAULT_CONFIG_FILE_REF, .passes(async (fn) =>
} satisfies RemoteFileAddress); t.deepEqual(await fn(), {
owner: "owner",
repo: "repo",
path: "path",
ref: DEFAULT_CONFIG_FILE_REF,
} satisfies RemoteFileAddress),
);
}); });
test("parseRemoteFileAddress rejects invalid values", async (t) => { test("parseRemoteFileAddress rejects invalid values", async (t) => {
@@ -246,49 +238,45 @@ test("parseRemoteFileAddress rejects invalid values", async (t) => {
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY) .withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
.returns(`${owner}/current-repo`); .returns(`${owner}/current-repo`);
t.throws(() => parseRemoteFileAddress(env, " "), { const target = callee(parseRemoteFileAddress).withEnv(env);
instanceOf: ConfigurationError,
}); const testInputs = [
t.throws(() => parseRemoteFileAddress(env, "repo//absolute"), { " ",
instanceOf: ConfigurationError, "repo//absolute",
}); "repo:/absolute",
t.throws(() => parseRemoteFileAddress(env, "repo:/absolute"), { "/repo@ref",
instanceOf: ConfigurationError, " /repo@ref",
}); "repo@",
t.throws(() => parseRemoteFileAddress(env, "/repo@ref"), { "repo:",
instanceOf: ConfigurationError, "repo/",
}); "/repo",
t.throws(() => parseRemoteFileAddress(env, " /repo@ref"), { ":path",
instanceOf: ConfigurationError, "@ref",
}); "@ref:path",
t.throws(() => parseRemoteFileAddress(env, "repo@"), { "owner/@ref:path",
instanceOf: ConfigurationError, "owner/@ref",
}); "owner/:path",
t.throws(() => parseRemoteFileAddress(env, "repo:"), { ];
instanceOf: ConfigurationError,
}); for (const testInput of testInputs) {
t.throws(() => parseRemoteFileAddress(env, "repo/"), { const targetWithArgs = target.withArgs(testInput);
instanceOf: ConfigurationError,
}); // Should throw both when the new format is and isn't accepted.
t.throws(() => parseRemoteFileAddress(env, "/repo"), { await targetWithArgs.withFeatures([]).passes(async (fn) =>
instanceOf: ConfigurationError, t.throwsAsync(fn, {
}); instanceOf: ConfigurationError,
t.throws(() => parseRemoteFileAddress(env, ":path"), { message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
instanceOf: ConfigurationError, }),
}); );
t.throws(() => parseRemoteFileAddress(env, "@ref"), { await targetWithArgs
instanceOf: ConfigurationError, .withFeatures([Feature.NewRemoteFileAddresses])
}); .passes(async (fn) =>
t.throws(() => parseRemoteFileAddress(env, "@ref:path"), { t.throwsAsync(fn, {
instanceOf: ConfigurationError, // When the new format is accepted, there are some more specific
}); // errors in some cases. It is sufficient for us to check that
t.throws(() => parseRemoteFileAddress(env, "owner/@ref:path"), { // an exception is thrown.
instanceOf: ConfigurationError, instanceOf: ConfigurationError,
}); }),
t.throws(() => parseRemoteFileAddress(env, "owner/@ref"), { );
instanceOf: ConfigurationError, }
});
t.throws(() => parseRemoteFileAddress(env, "owner/:path"), {
instanceOf: ConfigurationError,
});
}); });

View File

@@ -1,6 +1,8 @@
import { ActionState } from "../action-common";
import { ActionsEnvVars } from "../actions-util"; import { ActionsEnvVars } from "../actions-util";
import { Env } from "../environment"; import { Env } from "../environment";
import * as errorMessages from "../error-messages"; import * as errorMessages from "../error-messages";
import { Feature } from "../feature-flags";
import { ConfigurationError, Failure, Result, Success } from "../util"; import { ConfigurationError, Failure, Result, Success } from "../util";
/** Represents remote file addresses. */ /** Represents remote file addresses. */
@@ -72,15 +74,15 @@ function parseOldRemoteFileAddress(
/** /**
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components. * Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
* *
* @param env The current environment variables. * @param actionState The current Action state.
* @param configFile The string to try and parse. * @param configFile The string to try and parse.
* @returns The successful result of executing the regex. * @returns The successful result of executing the regex.
* @throws `ConfigurationError` if the format of `configFile` is not valid. * @throws `ConfigurationError` if the format of `configFile` is not valid.
*/ */
export function parseRemoteFileAddress( export async function parseRemoteFileAddress(
env: Env, actionState: ActionState,
configFile: string, configFile: string,
): RemoteFileAddress { ): Promise<RemoteFileAddress> {
// Try to parse the input using the old format. If successful, return the // Try to parse the input using the old format. If successful, return the
// resulting `RemoteFileAddress`. Otherwise, continue using the new format. // resulting `RemoteFileAddress`. Otherwise, continue using the new format.
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile); const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
@@ -89,6 +91,16 @@ export function parseRemoteFileAddress(
return oldFormatAddressResult.value; return oldFormatAddressResult.value;
} }
// If the FF for the new format is not enabled, throw the old format error.
const allowNewFormat = await actionState.features.getValue(
Feature.NewRemoteFileAddresses,
);
if (!allowNewFormat) {
throw new ConfigurationError(
errorMessages.getConfigFileRepoOldFormatInvalidMessage(configFile),
);
}
// retrieve the various parts of the config location, and ensure they're present // retrieve the various parts of the config location, and ensure they're present
const format = new RegExp( const format = new RegExp(
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$", "^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
@@ -119,7 +131,7 @@ export function parseRemoteFileAddress(
} }
return { return {
owner: owner || getDefaultOwner(env), owner: owner || getDefaultOwner(actionState.env),
repo, repo,
path: path || DEFAULT_CONFIG_FILE_NAME, path: path || DEFAULT_CONFIG_FILE_NAME,
ref: ref || DEFAULT_CONFIG_FILE_REF, ref: ref || DEFAULT_CONFIG_FILE_REF,

View File

@@ -30,6 +30,15 @@ export function getInvalidConfigFileMessage(
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`; return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
} }
export function getConfigFileRepoOldFormatInvalidMessage(
configFile: string,
): string {
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
error += " Expected format <owner>/<repository>/<file-path>@<ref>";
return error;
}
export function getConfigFileRepoFormatInvalidMessage( export function getConfigFileRepoFormatInvalidMessage(
configFile: string, configFile: string,
): string { ): string {