Skip to content

Commit 2333995

Browse files
committed
Add expliciit prefix for remote file addresses
1 parent 57ca205 commit 2333995

4 files changed

Lines changed: 142 additions & 12 deletions

File tree

lib/entry-points.js

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config-utils.test.ts

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
initAllState,
4242
callee,
4343
SAMPLE_DOTCOM_API_DETAILS,
44+
AssertableTarget,
4445
} from "./testing-utils";
4546
import {
4647
GitHubVariant,
@@ -2605,18 +2606,104 @@ test.serial(
26052606
await withTmpDir(async (tmpDir) => {
26062607
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
26072608

2608-
const remoteAddress = "repo:file";
2609-
await callee(configUtils.loadUserConfig)
2610-
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
2611-
.passes(t.deepEqual, {});
2609+
// Construct the basic test target.
2610+
const target = callee(configUtils.loadUserConfig)
2611+
.withDefaultActionsEnv()
2612+
.withFeatures([Feature.NewRemoteFileAddresses]);
2613+
2614+
// Utility function to assert that `targetWithArgs` has identified
2615+
// the input as a remote file address.
2616+
const checkIsRemote =
2617+
(address: string) =>
2618+
async <R>(targetWithArgs: AssertableTarget<R>) => {
2619+
// We have stubbed `getRemoteConfig` to resolve to `{}`, so we
2620+
// expect that result.
2621+
await targetWithArgs.passes(t.deepEqual, {});
2622+
2623+
// And `getRemoteConfig` should have been called exactly once.
2624+
t.is(getRemoteConfig.callCount, 1);
2625+
2626+
// Get the arguments for the call and check that there were three.
2627+
// We don't care about the first, but check that the other two
2628+
// match our expectations. We break it down like this to get
2629+
// more useful test output.
2630+
const args = getRemoteConfig.getCalls()[0].args;
2631+
t.is(args.length, 3);
2632+
t.deepEqual(args[1], address);
2633+
t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS);
2634+
};
26122635

2613-
t.true(
2614-
getRemoteConfig.calledOnceWithExactly(
2615-
sinon.match.any,
2616-
remoteAddress,
2636+
// Utility function to assert that `targetWithArgs` has not identified
2637+
// the input as a remote file address.
2638+
const checkIsNotRemote = async <R>(
2639+
targetWithArgs: AssertableTarget<R>,
2640+
) => {
2641+
// We expect `loadUserConfig` to have thrown if it thinks the path is local,
2642+
// since the inputs we provide aren't for files that exist.
2643+
await targetWithArgs.throws(t);
2644+
2645+
// Additionally, we expect that `getRemoteConfig` wasn't called.
2646+
t.is(getRemoteConfig.callCount, 0);
2647+
};
2648+
2649+
// Utility function to add the explicit `REMOTE_PATH_PREFIX` to the input.
2650+
const withExplicitPrefix = (str: string) =>
2651+
`${file.REMOTE_PATH_PREFIX}${str}`;
2652+
2653+
// Utility to set up a call to `loadUserConfig` with the provided `address`
2654+
// and pass it to `assertion`.
2655+
const testTargetWith = async (
2656+
address: string,
2657+
assertion: (
2658+
targetWithArgs: AssertableTarget<Promise<UserConfig>>,
2659+
) => Promise<any>,
2660+
) => {
2661+
// Reset the stub's history since we re-use it.
2662+
getRemoteConfig.resetHistory();
2663+
2664+
// Log the input we are testing so that, in the event of a failure,
2665+
// it is easier to see which input was responsible.
2666+
t.log(`testTargetWith("${address}")`);
2667+
2668+
// Prepare the test call to `loadUserConfig`.
2669+
const targetWithArgs = target.withArgs(
2670+
address,
2671+
tmpDir,
26172672
SAMPLE_DOTCOM_API_DETAILS,
2618-
),
2673+
tmpDir,
2674+
);
2675+
2676+
// Pass it to the provided assertion function.
2677+
await assertion(targetWithArgs);
2678+
};
2679+
2680+
// Since this input contains an '@' character, it is treated as a remote path
2681+
// by the old logic even without the explicit prefix.
2682+
const remoteWithoutPrefix = "repo@main";
2683+
await testTargetWith(
2684+
remoteWithoutPrefix,
2685+
checkIsRemote(remoteWithoutPrefix),
2686+
);
2687+
await testTargetWith(
2688+
withExplicitPrefix(remoteWithoutPrefix),
2689+
checkIsRemote(remoteWithoutPrefix),
26192690
);
2691+
// It is only treated as a local path with the corresponding prefix.
2692+
await testTargetWith(`./${remoteWithoutPrefix}`, checkIsNotRemote);
2693+
2694+
// The following test inputs are examples of ambiguous paths. They could refer to
2695+
// valid local or remote paths. For each, we check that they are treated as remote
2696+
// paths if the explicit remote file prefix is used and as local paths otherwise.
2697+
const testInputs = ["repo:file", "input", "../input"];
2698+
2699+
for (const testInput of testInputs) {
2700+
for (const addPrefix of [true, false]) {
2701+
await testTargetWith(
2702+
addPrefix ? withExplicitPrefix(testInput) : testInput,
2703+
addPrefix ? checkIsRemote(testInput) : checkIsNotRemote,
2704+
);
2705+
}
2706+
}
26202707
});
26212708
},
26222709
);

src/config-utils.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import {
3131
parseUserConfig,
3232
UserConfig,
3333
} from "./config/db-config";
34-
import { getRemoteConfig, LOCAL_PATH_PREFIX } from "./config/file";
34+
import {
35+
getRemoteConfig,
36+
LOCAL_PATH_PREFIX,
37+
REMOTE_PATH_PREFIX,
38+
} from "./config/file";
3539
import {
3640
parseRegistries,
3741
type RegistryConfigNoCredentials,
@@ -501,6 +505,12 @@ export async function loadUserConfig(
501505
);
502506
return getLocalConfig(actionState.logger, configFile, validateConfig);
503507
} else {
508+
// Drop the explicit prefix if it is present. Since `REMOTE_PATH_PREFIX` is chosen
509+
// to not conflict with permissable characters in "owner" or "repo" components,
510+
// this does not risk removing valid parts of either component by accident.
511+
if (isRemotePath(configFile)) {
512+
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
513+
}
504514
return await getRemoteConfig(actionState, configFile, apiDetails);
505515
}
506516
}
@@ -1287,6 +1297,16 @@ function isRelativePath(configPath: string): boolean {
12871297
return configPath.startsWith(LOCAL_PATH_PREFIX);
12881298
}
12891299

1300+
/**
1301+
* Determines if `configPath` starts with the prefix used to explicitly mark a path
1302+
* as a remote path (`REMOTE_PATH_PREFIX`).
1303+
*
1304+
* @param configPath The path to test.
1305+
*/
1306+
function isRemotePath(configPath: string): boolean {
1307+
return configPath.startsWith(REMOTE_PATH_PREFIX);
1308+
}
1309+
12901310
/**
12911311
* Determines if `configPath` contains a '@' character.
12921312
*
@@ -1298,8 +1318,6 @@ function containsAtRef(configPath: string): boolean {
12981318

12991319
/**
13001320
* Determines if `configPath` refers to a local configuration file.
1301-
* This assumes the `OLD_REMOTE_ADDRESS_FORMAT` which must contain a '@'
1302-
* character for remote addresses.
13031321
*
13041322
* @param configPath The path to test.
13051323
* @returns True if it is local, or false otherwise.
@@ -1311,8 +1329,15 @@ function isLocal(configPath: string): boolean {
13111329
if (isRelativePath(configPath)) {
13121330
return true;
13131331
}
1332+
// If the path starts with `REMOTE_PATH_PREFIX`, it is explicitly remote.
1333+
// This allows users to resolve ambiguity by specifying `REMOTE_PATH_PREFIX`.
1334+
if (isRemotePath(configPath)) {
1335+
return false;
1336+
}
13141337

13151338
// Otherwise, the path is also local if it does not contain '@'.
1339+
// This assumes the `OLD_REMOTE_ADDRESS_FORMAT` which must contain a '@'
1340+
// character for remote addresses.
13161341
return !containsAtRef(configPath);
13171342
}
13181343

src/config/file.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import { parseRemoteFileAddress } from "./remote-file";
1616
*/
1717
export const LOCAL_PATH_PREFIX = "./";
1818

19+
/**
20+
* The prefix that can be specified to indicate that a path should be treated as a remote file address.
21+
* The new remote file address format must start with either an owner or repository name. Both
22+
* are restricted to ASCII characters, '.', and '-'. The prefix chosen here does not interfere with
23+
* those and is _unlikely_ (but not impossible) to appear in a local file path.
24+
*/
25+
export const REMOTE_PATH_PREFIX = "::";
26+
1927
/**
2028
* Gets the value that is configured for the configuration file, if any.
2129
*/

0 commit comments

Comments
 (0)