Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/codeql/codeql-config-javascript.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "CodeQL config"
queries:
queries:
- name: Run custom queries
uses: ./queries
# Run all extra query suites, both because we want to
Expand All @@ -13,3 +13,5 @@ queries:
paths-ignore:
- lib
- tests
- "**/*.test.ts"
- "**/testing-util.ts"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th

## [UNRELEASED]

- Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. [#3850](https://github.com/github/codeql-action/pull/3850)
- Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. [#3852](https://github.com/github/codeql-action/pull/3852)
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. [#3837](https://github.com/github/codeql-action/pull/3837)

Expand Down
702 changes: 391 additions & 311 deletions lib/start-proxy-action.js

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions src/json/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import test from "ava";

import { setupTests } from "../testing-utils";

import * as json from ".";

setupTests(test);

const testSchema = {
requiredKey: json.string,
};

const optionalSchema = {
optionalKey: json.optional(json.string),
};

test("validateSchema - required properties are required", async (t) => {
t.false(json.validateSchema(testSchema, {}));
t.false(json.validateSchema(testSchema, { requiredKey: undefined }));
t.false(json.validateSchema(testSchema, { requiredKey: null }));
t.false(json.validateSchema(testSchema, { requiredKey: 0 }));
t.false(json.validateSchema(testSchema, { requiredKey: 123 }));
t.false(json.validateSchema(testSchema, { requiredKey: false }));
t.false(json.validateSchema(testSchema, { requiredKey: true }));
t.false(json.validateSchema(testSchema, { requiredKey: [] }));
t.false(json.validateSchema(testSchema, { requiredKey: {} }));
t.true(json.validateSchema(testSchema, { requiredKey: "" }));
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
});

test("validateSchema - optional properties are optional", async (t) => {
// Optional fields may be absent
t.true(json.validateSchema(optionalSchema, {}));
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));

// But, if present, should have the expected type
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
t.false(json.validateSchema(optionalSchema, { optionalKey: true }));
t.false(json.validateSchema(optionalSchema, { optionalKey: [] }));
t.false(json.validateSchema(optionalSchema, { optionalKey: {} }));
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
});
79 changes: 79 additions & 0 deletions src/json/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,82 @@ export function isStringOrUndefined(
): value is string | undefined {
return value === undefined || isString(value);
}

/**
* Represents a field of type `T` in a schema.
* Carries a validation function and flag indicating whether the field is required or not.
*/
export type Validator<T> = {
validate: (val: unknown) => val is T;
required: boolean;
};

/** Extracts `T` from `Validator<T>`. */
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;

/** A validator for string fields in schemas. */
export const string = {
validate: isString,
required: true,
} as const satisfies Validator<string>;

/** Transforms a validator to be optional. */
export function optional<T>(validator: Validator<T>) {
return {
validate: (val: unknown) => {
return val === undefined || val === null || validator.validate(val);
},
required: false,
} as const satisfies Validator<T | undefined | null>;
}

/** Represents an arbitrary object schema. */
export type Schema = Record<string, Validator<any>>;

/** Extracts the required keys from `S`. */
export type RequiredKeys<S extends Schema> = {
[K in keyof S]: S[K]["required"] extends true ? K : never;
}[keyof S];

/** Extracts optional keys from `S`. */
export type OptionalKeys<S extends Schema> = {
[K in keyof S]: S[K]["required"] extends true ? never : K;
}[keyof S];

/** Constructs an object type corresponding to a schema. */
export type FromSchema<S extends Schema> = {
[K in RequiredKeys<S>]: UnwrapValidator<S[K]>;
} & { [K in OptionalKeys<S>]?: UnwrapValidator<S[K]> };

/**
* Validates that `obj` satisfies at least `schema`. Additional keys are accepted.
*
* @param schema The schema to validate against.
* @param obj The object to validate.
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
*/
export function validateSchema<S extends Schema>(
schema: S,
obj: UnvalidatedObject<any>,
): obj is FromSchema<S> {
for (const [key, validator] of Object.entries(schema)) {
const hasKey = key in obj;

// If the property is required, but absent, fail.
if (validator.required && !hasKey) {
return false;
}

// If the property is required, but undefined or null, fail.
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
return false;
}

// If the property is present, validate it.
if (hasKey && !validator.validate(obj[key])) {
return false;
}
}

return true;
}
106 changes: 106 additions & 0 deletions src/json/testing-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ExecutionContext } from "ava";

import * as json from ".";

/**
* Constructs an object based on `schema` for unit tests.
* Assumes that all keys in `schema` have string values.
*
* @param includeOptional Whether to include optional properties.
* @param schema The schema to base the object on.
* @returns An object that satisfies `schema`.
*/
export function makeFromSchema<S extends json.Schema>(
includeOptional: boolean,
schema: S,
): json.FromSchema<S> {
const result = {};
for (const [key, validator] of Object.entries(schema)) {
if (!validator.required && !includeOptional) {
continue;
}
result[key] = `value-for-${key}`;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}
return result as json.FromSchema<S>;
}

/** Options for `withSchemaMatrix`. */
export interface SchemaMatrixOptions {
/** Whether cases where the properties are entirely absent should be excluded. */
excludeAbsent?: boolean;
}

/**
* Constructs a test matrix of possible objects for `schema`: all required properties
* plus all permutations of possible states for the optional properties.
*
* @param schema The schema to construct a test matrix for.
* @param body The test body to call with each value from the test matrix.
*/
export function withSchemaMatrix<S extends json.Schema>(
t: ExecutionContext<any>,
schema: S,
opts: SchemaMatrixOptions,
body: (value: json.FromSchema<S>) => void,
): void {
// Construct a base object that includes all required properties.
const required = makeFromSchema(false, schema);

// Identify optional properties.
const optionalKeys: Array<keyof S> = [];

for (const [key, validator] of Object.entries(schema)) {
if (!validator.required) {
optionalKeys.push(key);
}
}

const optionalValues = (key: keyof S) => [
null,
undefined,
`value-for-${String(key)}`,
];

// Constructs an array of test objects, starting with `required` and combining it with all
// possible states of each optional property. For example, with default settings:
//
// For { requiredKey: string }, we get: `[{ requiredKey: "some-string-value" }]`
//
// For { requiredKey: string, optionalKey?: string }, we get:
// [ { requiredKey: "some-string-value" },
// { requiredKey: "some-string-value", optionalKey: undefined },
// { requiredKey: "some-string-value", optionalKey: null },
// { requiredKey: "some-string-value", optionalKey: "some-value" },
// ]
const permutations = (keys: Array<keyof S>) => {
if (keys.length === 0) return [required];

const bases = permutations(keys.slice(1));
const result: Array<json.FromSchema<S>> = [];

const optionalKey = keys[0];
for (const base of bases) {
if (!opts.excludeAbsent) {
// Optional keys can be absent entirely.
result.push(base);
}

// Or be present and have one of the `optionalValues`.
for (const optionalValue of optionalValues(optionalKey)) {
result.push({ ...base, [optionalKey]: optionalValue });
}
}
return result;
};

// Call `body` for all test cases.
const testCases = permutations(optionalKeys);
for (const testCase of testCases) {
try {
body(testCase);
} catch (err) {
t.log(testCase);
throw err;
}
}
}
1 change: 1 addition & 0 deletions src/start-proxy-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async function startProxy(
.map((credential) => ({
type: credential.type,
url: credential.url,
"replaces-base": credential["replaces-base"],
}));
core.setOutput("proxy_urls", JSON.stringify(registry_urls));

Expand Down
Loading
Loading