Skip to content

Commit 9e7c070

Browse files
authored
Merge pull request #4014 from github/mbg/explicit-remote-prefix
Add support for an explicit remote path prefix
2 parents 2d682ac + 3492b7e commit 9e7c070

6 files changed

Lines changed: 375 additions & 49 deletions

File tree

lib/entry-points.js

Lines changed: 50 additions & 19 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: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import test, { ExecutionContext } from "ava";
66
import * as yaml from "js-yaml";
77
import * as sinon from "sinon";
88

9+
import { ActionState } from "./action-common";
910
import * as actionsUtil from "./actions-util";
1011
import { AnalysisKind, supportedAnalysisKinds } from "./analyses";
1112
import * as api from "./api-client";
1213
import { CachingKind } from "./caching-utils";
1314
import { createStubCodeQL } from "./codeql";
1415
import { UserConfig } from "./config/db-config";
16+
import * as file from "./config/file";
1517
import * as configUtils from "./config-utils";
1618
import * as errorMessages from "./error-messages";
1719
import { Feature } from "./feature-flags";
@@ -38,6 +40,8 @@ import {
3840
makeMacro,
3941
initAllState,
4042
callee,
43+
SAMPLE_DOTCOM_API_DETAILS,
44+
AssertableTarget,
4145
} from "./testing-utils";
4246
import {
4347
GitHubVariant,
@@ -2529,3 +2533,177 @@ test("determineUserConfig - ignores config file input outside Default Setup if F
25292533
});
25302534
});
25312535
});
2536+
2537+
test("loadUserConfig - loads local configuration files", async (t) => {
2538+
await withTmpDir(async (workspaceDir) => {
2539+
await withTmpDir(async (tmpDir) => {
2540+
// Construct the test target.
2541+
const loadUserConfig = (
2542+
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
2543+
filePath: string,
2544+
) =>
2545+
configUtils.loadUserConfig(
2546+
actionState,
2547+
filePath,
2548+
workspaceDir,
2549+
SAMPLE_DOTCOM_API_DETAILS,
2550+
tmpDir,
2551+
);
2552+
const target = callee(loadUserConfig);
2553+
2554+
// `loadUserConfig` should load local configuration files if they are inside the workspace:
2555+
const insideOfWorkspace = path.join(workspaceDir, "some-file.yml");
2556+
fs.writeFileSync(insideOfWorkspace, "test-key: present", "utf8");
2557+
2558+
await target
2559+
.withArgs(insideOfWorkspace)
2560+
.passes(t.deepEqual, { "test-key": "present" });
2561+
2562+
// `loadUserConfig` should normally throw if the path is outside of the workspace:
2563+
const outsideOfWorkspace = path.join(
2564+
tmpDir,
2565+
"not-the-generated-file.yml",
2566+
);
2567+
fs.writeFileSync(outsideOfWorkspace, "test-key: present", "utf8");
2568+
2569+
await target
2570+
.withArgs(outsideOfWorkspace)
2571+
.throws(t, { instanceOf: ConfigurationError });
2572+
2573+
// `loadUserConfig` does not throw if the path is the result of `userConfigFromActionPath`:
2574+
const generatedPath = configUtils.userConfigFromActionPath(tmpDir);
2575+
fs.writeFileSync(generatedPath, "test-key: present", "utf8");
2576+
2577+
await target
2578+
.withArgs(generatedPath)
2579+
.passes(t.deepEqual, { "test-key": "present" });
2580+
});
2581+
});
2582+
});
2583+
2584+
test.serial("loadUserConfig - loads remote configuration files", async (t) => {
2585+
await withTmpDir(async (tmpDir) => {
2586+
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
2587+
2588+
const remoteAddress = "owner/repo/file@ref";
2589+
await callee(configUtils.loadUserConfig)
2590+
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
2591+
.passes(t.deepEqual, {});
2592+
2593+
t.true(
2594+
getRemoteConfig.calledOnceWithExactly(
2595+
sinon.match.any,
2596+
remoteAddress,
2597+
SAMPLE_DOTCOM_API_DETAILS,
2598+
),
2599+
);
2600+
});
2601+
});
2602+
2603+
test.serial(
2604+
"loadUserConfig - loads remote configuration files (new format, partial)",
2605+
async (t) => {
2606+
await withTmpDir(async (tmpDir) => {
2607+
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
2608+
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+
};
2635+
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,
2672+
SAMPLE_DOTCOM_API_DETAILS,
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),
2690+
);
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+
}
2707+
});
2708+
},
2709+
);

0 commit comments

Comments
 (0)