Skip to content

Commit 7851e55

Browse files
authored
Merge pull request #3850 from github/mbg/private-registry/cloudsmith-gcp
Private registries: Add support for Cloudsmith and GCP OIDC configurations
2 parents a6109b1 + 262a15f commit 7851e55

13 files changed

Lines changed: 1124 additions & 612 deletions

.github/codeql/codeql-config-javascript.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: "CodeQL config"
2-
queries:
2+
queries:
33
- name: Run custom queries
44
uses: ./queries
55
# Run all extra query suites, both because we want to
@@ -13,3 +13,5 @@ queries:
1313
paths-ignore:
1414
- lib
1515
- tests
16+
- "**/*.test.ts"
17+
- "**/testing-util.ts"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
44

55
## [UNRELEASED]
66

7+
- Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. [#3850](https://github.com/github/codeql-action/pull/3850)
78
- 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)
89
- _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)
910

lib/start-proxy-action.js

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

src/json/index.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import test from "ava";
2+
3+
import { setupTests } from "../testing-utils";
4+
5+
import * as json from ".";
6+
7+
setupTests(test);
8+
9+
const testSchema = {
10+
requiredKey: json.string,
11+
};
12+
13+
const optionalSchema = {
14+
optionalKey: json.optional(json.string),
15+
};
16+
17+
test("validateSchema - required properties are required", async (t) => {
18+
t.false(json.validateSchema(testSchema, {}));
19+
t.false(json.validateSchema(testSchema, { requiredKey: undefined }));
20+
t.false(json.validateSchema(testSchema, { requiredKey: null }));
21+
t.false(json.validateSchema(testSchema, { requiredKey: 0 }));
22+
t.false(json.validateSchema(testSchema, { requiredKey: 123 }));
23+
t.false(json.validateSchema(testSchema, { requiredKey: false }));
24+
t.false(json.validateSchema(testSchema, { requiredKey: true }));
25+
t.false(json.validateSchema(testSchema, { requiredKey: [] }));
26+
t.false(json.validateSchema(testSchema, { requiredKey: {} }));
27+
t.true(json.validateSchema(testSchema, { requiredKey: "" }));
28+
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
29+
});
30+
31+
test("validateSchema - optional properties are optional", async (t) => {
32+
// Optional fields may be absent
33+
t.true(json.validateSchema(optionalSchema, {}));
34+
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
35+
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
36+
37+
// But, if present, should have the expected type
38+
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
39+
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
40+
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
41+
t.false(json.validateSchema(optionalSchema, { optionalKey: true }));
42+
t.false(json.validateSchema(optionalSchema, { optionalKey: [] }));
43+
t.false(json.validateSchema(optionalSchema, { optionalKey: {} }));
44+
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
45+
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
46+
});

src/json/index.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,82 @@ export function isStringOrUndefined(
3636
): value is string | undefined {
3737
return value === undefined || isString(value);
3838
}
39+
40+
/**
41+
* Represents a field of type `T` in a schema.
42+
* Carries a validation function and flag indicating whether the field is required or not.
43+
*/
44+
export type Validator<T> = {
45+
validate: (val: unknown) => val is T;
46+
required: boolean;
47+
};
48+
49+
/** Extracts `T` from `Validator<T>`. */
50+
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;
51+
52+
/** A validator for string fields in schemas. */
53+
export const string = {
54+
validate: isString,
55+
required: true,
56+
} as const satisfies Validator<string>;
57+
58+
/** Transforms a validator to be optional. */
59+
export function optional<T>(validator: Validator<T>) {
60+
return {
61+
validate: (val: unknown) => {
62+
return val === undefined || val === null || validator.validate(val);
63+
},
64+
required: false,
65+
} as const satisfies Validator<T | undefined | null>;
66+
}
67+
68+
/** Represents an arbitrary object schema. */
69+
export type Schema = Record<string, Validator<any>>;
70+
71+
/** Extracts the required keys from `S`. */
72+
export type RequiredKeys<S extends Schema> = {
73+
[K in keyof S]: S[K]["required"] extends true ? K : never;
74+
}[keyof S];
75+
76+
/** Extracts optional keys from `S`. */
77+
export type OptionalKeys<S extends Schema> = {
78+
[K in keyof S]: S[K]["required"] extends true ? never : K;
79+
}[keyof S];
80+
81+
/** Constructs an object type corresponding to a schema. */
82+
export type FromSchema<S extends Schema> = {
83+
[K in RequiredKeys<S>]: UnwrapValidator<S[K]>;
84+
} & { [K in OptionalKeys<S>]?: UnwrapValidator<S[K]> };
85+
86+
/**
87+
* Validates that `obj` satisfies at least `schema`. Additional keys are accepted.
88+
*
89+
* @param schema The schema to validate against.
90+
* @param obj The object to validate.
91+
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
92+
*/
93+
export function validateSchema<S extends Schema>(
94+
schema: S,
95+
obj: UnvalidatedObject<any>,
96+
): obj is FromSchema<S> {
97+
for (const [key, validator] of Object.entries(schema)) {
98+
const hasKey = key in obj;
99+
100+
// If the property is required, but absent, fail.
101+
if (validator.required && !hasKey) {
102+
return false;
103+
}
104+
105+
// If the property is required, but undefined or null, fail.
106+
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
107+
return false;
108+
}
109+
110+
// If the property is present, validate it.
111+
if (hasKey && !validator.validate(obj[key])) {
112+
return false;
113+
}
114+
}
115+
116+
return true;
117+
}

src/json/testing-util.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ExecutionContext } from "ava";
2+
3+
import * as json from ".";
4+
5+
/**
6+
* Constructs an object based on `schema` for unit tests.
7+
* Assumes that all keys in `schema` have string values.
8+
*
9+
* @param includeOptional Whether to include optional properties.
10+
* @param schema The schema to base the object on.
11+
* @returns An object that satisfies `schema`.
12+
*/
13+
export function makeFromSchema<S extends json.Schema>(
14+
includeOptional: boolean,
15+
schema: S,
16+
): json.FromSchema<S> {
17+
const result = {};
18+
for (const [key, validator] of Object.entries(schema)) {
19+
if (!validator.required && !includeOptional) {
20+
continue;
21+
}
22+
result[key] = `value-for-${key}`;
23+
}
24+
return result as json.FromSchema<S>;
25+
}
26+
27+
/** Options for `withSchemaMatrix`. */
28+
export interface SchemaMatrixOptions {
29+
/** Whether cases where the properties are entirely absent should be excluded. */
30+
excludeAbsent?: boolean;
31+
}
32+
33+
/**
34+
* Constructs a test matrix of possible objects for `schema`: all required properties
35+
* plus all permutations of possible states for the optional properties.
36+
*
37+
* @param schema The schema to construct a test matrix for.
38+
* @param body The test body to call with each value from the test matrix.
39+
*/
40+
export function withSchemaMatrix<S extends json.Schema>(
41+
t: ExecutionContext<any>,
42+
schema: S,
43+
opts: SchemaMatrixOptions,
44+
body: (value: json.FromSchema<S>) => void,
45+
): void {
46+
// Construct a base object that includes all required properties.
47+
const required = makeFromSchema(false, schema);
48+
49+
// Identify optional properties.
50+
const optionalKeys: Array<keyof S> = [];
51+
52+
for (const [key, validator] of Object.entries(schema)) {
53+
if (!validator.required) {
54+
optionalKeys.push(key);
55+
}
56+
}
57+
58+
const optionalValues = (key: keyof S) => [
59+
null,
60+
undefined,
61+
`value-for-${String(key)}`,
62+
];
63+
64+
// Constructs an array of test objects, starting with `required` and combining it with all
65+
// possible states of each optional property. For example, with default settings:
66+
//
67+
// For { requiredKey: string }, we get: `[{ requiredKey: "some-string-value" }]`
68+
//
69+
// For { requiredKey: string, optionalKey?: string }, we get:
70+
// [ { requiredKey: "some-string-value" },
71+
// { requiredKey: "some-string-value", optionalKey: undefined },
72+
// { requiredKey: "some-string-value", optionalKey: null },
73+
// { requiredKey: "some-string-value", optionalKey: "some-value" },
74+
// ]
75+
const permutations = (keys: Array<keyof S>) => {
76+
if (keys.length === 0) return [required];
77+
78+
const bases = permutations(keys.slice(1));
79+
const result: Array<json.FromSchema<S>> = [];
80+
81+
const optionalKey = keys[0];
82+
for (const base of bases) {
83+
if (!opts.excludeAbsent) {
84+
// Optional keys can be absent entirely.
85+
result.push(base);
86+
}
87+
88+
// Or be present and have one of the `optionalValues`.
89+
for (const optionalValue of optionalValues(optionalKey)) {
90+
result.push({ ...base, [optionalKey]: optionalValue });
91+
}
92+
}
93+
return result;
94+
};
95+
96+
// Call `body` for all test cases.
97+
const testCases = permutations(optionalKeys);
98+
for (const testCase of testCases) {
99+
try {
100+
body(testCase);
101+
} catch (err) {
102+
t.log(testCase);
103+
throw err;
104+
}
105+
}
106+
}

src/start-proxy-action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ async function startProxy(
198198
.map((credential) => ({
199199
type: credential.type,
200200
url: credential.url,
201+
"replaces-base": credential["replaces-base"],
201202
}));
202203
core.setOutput("proxy_urls", JSON.stringify(registry_urls));
203204

0 commit comments

Comments
 (0)