Merge pull request #4050 from github/mbg/status/registries

Include `registry_types` in more status reports
This commit is contained in:
Michael B. Gale
2026-07-31 15:32:50 +00:00
committed by GitHub
6 changed files with 347 additions and 166 deletions

337
lib/entry-points.js generated
View File

@@ -92812,10 +92812,10 @@ var require_util12 = __commonJS({
return objectToString(arg) === "[object Array]";
}
exports2.isArray = isArray2;
function isBoolean(arg) {
function isBoolean2(arg) {
return typeof arg === "boolean";
}
exports2.isBoolean = isBoolean;
exports2.isBoolean = isBoolean2;
function isNull(arg) {
return arg === null;
}
@@ -96885,7 +96885,7 @@ var require_validators = __commonJS({
throw new ERR_INVALID_ARG_TYPE(name, "a dictionary", value);
}
});
var validateArray = hideStackFrames((value, name, minLength = 0) => {
var validateArray2 = hideStackFrames((value, name, minLength = 0) => {
if (!ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, "Array", value);
}
@@ -96895,19 +96895,19 @@ var require_validators = __commonJS({
}
});
function validateStringArray(value, name) {
validateArray(value, name);
validateArray2(value, name);
for (let i = 0; i < value.length; i++) {
validateString(value[i], `${name}[${i}]`);
}
}
function validateBooleanArray(value, name) {
validateArray(value, name);
validateArray2(value, name);
for (let i = 0; i < value.length; i++) {
validateBoolean(value[i], `${name}[${i}]`);
}
}
function validateAbortSignalArray(value, name) {
validateArray(value, name);
validateArray2(value, name);
for (let i = 0; i < value.length; i++) {
const signal = value[i];
const indexedName = `${name}[${i}]`;
@@ -97003,7 +97003,7 @@ var require_validators = __commonJS({
isInt32,
isUint32,
parseFileMode,
validateArray,
validateArray: validateArray2,
validateStringArray,
validateBooleanArray,
validateAbortSignalArray,
@@ -144596,21 +144596,25 @@ function isString(value) {
function isNumber(value) {
return typeof value === "number";
}
function isBoolean(value) {
return typeof value === "boolean";
}
function isStringOrUndefined(value) {
return value === void 0 || isString(value);
}
function defaultCheck(validate2) {
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate2(arg) });
}
function makeValidator(validate2, required = true) {
function makeValidator(validate2) {
return {
validate: validate2,
check: defaultCheck(validate2),
required
required: true
};
}
var string = makeValidator(isString);
var number = makeValidator(isNumber);
var boolean = makeValidator(isBoolean);
function array(validator) {
const validate2 = (val) => {
return isArray(val) && val.every((e) => validator.validate(e));
@@ -144692,6 +144696,10 @@ function validateSchema(schema, obj) {
const result = checkSchema(schema, obj, { failFast: true });
return result.valid;
}
function validateArray(elementSchema, arr) {
const elementValidator = object(elementSchema);
return array(elementValidator).validate(arr);
}
function successfulCheckSchema() {
return {
valid: true,
@@ -146400,6 +146408,148 @@ async function getGeneratedFiles(workingDirectory) {
return generatedFiles;
}
// src/start-proxy/types.ts
var usernameSchema = {
/** The username needed to authenticate to the package registry, if any. */
username: optionalOrNull(string)
};
function hasUsername(config) {
return "username" in config;
}
var usernamePasswordSchema = {
/** The password needed to authenticate to the package registry, if any. */
password: optionalOrNull(string),
...usernameSchema
};
function hasUsernameAndPassword(config) {
return hasUsername(config) && "password" in config;
}
var tokenSchema = {
/** The token needed to authenticate to the package registry, if any. */
token: optionalOrNull(string),
...usernameSchema
};
function hasToken(config) {
return "token" in config;
}
function isToken(config) {
return "token" in config && validateSchema(tokenSchema, config);
}
var azureConfigSchema = {
"tenant-id": string,
"client-id": string
};
function isAzureConfig(config) {
return validateSchema(azureConfigSchema, config);
}
var awsConfigSchema = {
"aws-region": string,
"account-id": string,
"role-name": string,
domain: string,
"domain-owner": string,
audience: optionalOrNull(string)
};
function isAWSConfig(config) {
return validateSchema(awsConfigSchema, config);
}
var jfrogConfigSchema = {
"jfrog-oidc-provider-name": string,
audience: optionalOrNull(string),
"identity-mapping-name": optionalOrNull(string)
};
function isJFrogConfig(config) {
return validateSchema(jfrogConfigSchema, config);
}
var cloudsmithConfigSchema = {
namespace: string,
"service-slug": string,
"api-host": string
};
function isCloudsmithConfig(config) {
return validateSchema(cloudsmithConfigSchema, config);
}
var gcpConfigSchema = {
"workload-identity-provider": string,
"service-account": optionalOrNull(string),
audience: optionalOrNull(string)
};
function isGCPConfig(config) {
return validateSchema(gcpConfigSchema, config);
}
var oidcSchemas = [
{ schema: azureConfigSchema, name: "Azure" },
{ schema: awsConfigSchema, name: "AWS" },
{ schema: jfrogConfigSchema, name: "JFrog" },
{ schema: cloudsmithConfigSchema, name: "Cloudsmith" },
{ schema: gcpConfigSchema, name: "GCP" }
];
function credentialToStr(credential) {
let result = `Type: ${credential.type};`;
const appendIfDefined = (name, val) => {
if (isDefined2(val)) {
result += ` ${name}: ${val};`;
}
};
appendIfDefined("Url", credential.url);
appendIfDefined("Host", credential.host);
if (hasUsername(credential)) {
appendIfDefined("Username", credential.username);
}
if ("password" in credential) {
appendIfDefined(
"Password",
isDefined2(credential.password) ? "***" : void 0
);
}
if (hasToken(credential)) {
appendIfDefined("Token", isDefined2(credential.token) ? "***" : void 0);
}
if (isAzureConfig(credential)) {
appendIfDefined("Tenant", credential["tenant-id"]);
appendIfDefined("Client", credential["client-id"]);
} else if (isAWSConfig(credential)) {
appendIfDefined("AWS Region", credential["aws-region"]);
appendIfDefined("AWS Account", credential["account-id"]);
appendIfDefined("AWS Role", credential["role-name"]);
appendIfDefined("AWS Domain", credential.domain);
appendIfDefined("AWS Domain Owner", credential["domain-owner"]);
appendIfDefined("AWS Audience", credential.audience);
} else if (isJFrogConfig(credential)) {
appendIfDefined("JFrog Provider", credential["jfrog-oidc-provider-name"]);
appendIfDefined(
"JFrog Identity Mapping",
credential["identity-mapping-name"]
);
appendIfDefined("JFrog Audience", credential.audience);
} else if (isCloudsmithConfig(credential)) {
appendIfDefined("Cloudsmith Namespace", credential.namespace);
appendIfDefined("Cloudsmith Service Slug", credential["service-slug"]);
appendIfDefined("Cloudsmith API Host", credential["api-host"]);
} else if (isGCPConfig(credential)) {
appendIfDefined(
"GCP Workload Identity Provider",
credential["workload-identity-provider"]
);
appendIfDefined("GCP Service Account", credential["service-account"]);
appendIfDefined("GCP Audience", credential.audience);
}
return result;
}
var registryBaseSchema = {
/** The type of the package registry. */
type: string,
/** Whether the registry replaces the base registry for the ecosystem. */
"replaces-base": optional(boolean)
};
function getAddressString(address) {
if (address.url === void 0) {
return address.host;
} else {
return address.url;
}
}
// src/status-report.ts
function getDisplayActionName(actionName) {
if (actionName === "finish" /* Analyze */) {
@@ -146468,6 +146618,34 @@ function setJobStatusIfUnsuccessful(actionStatus) {
);
}
}
function getRegistryTypesFromEnv(logger, env = getEnv()) {
const value = env.getOptional("CODEQL_PROXY_URLS" /* PROXY_URLS */);
if (value === void 0) {
return void 0;
}
try {
const data = JSON.parse(value);
if (!isArray(data)) {
logger.debug(
`Expected '${"CODEQL_PROXY_URLS" /* PROXY_URLS */}' to contain a JSON array, but got '${typeof data}'.`
);
return void 0;
}
if (!validateArray(registryBaseSchema, data)) {
logger.debug(
`Expected '${"CODEQL_PROXY_URLS" /* PROXY_URLS */}' to contain a JSON array of registry objects, but got something else.`
);
return void 0;
}
const types2 = new Set(data.map((r) => r.type));
return Array.from(types2).sort().join(",");
} catch (err) {
logger.debug(
`Failed to parse '${"CODEQL_PROXY_URLS" /* PROXY_URLS */}': ${getErrorMessage(err)}.`
);
return void 0;
}
}
async function createStatusReportBase(actionName, status, actionStartedAt, config, diskInfo, logger, cause, exception) {
try {
const commitOid = getOptionalInput("sha") || process.env["GITHUB_SHA"] || "";
@@ -146507,6 +146685,7 @@ async function createStatusReportBase(actionName, status, actionStartedAt, confi
job_name: jobName,
job_run_uuid: jobRunUUID,
ref,
registry_types: getRegistryTypesFromEnv(logger),
runner_os: runnerOs,
started_at: workflowStartedAt,
status,
@@ -157415,7 +157594,7 @@ var import_async = __toESM(require_async(), 1);
var import_path6 = require("path");
// node_modules/archiver/lib/error.js
var import_util33 = __toESM(require("util"), 1);
var import_util34 = __toESM(require("util"), 1);
var ERROR_CODES = {
ABORTED: "archive was aborted",
DIRECTORYDIRPATHREQUIRED: "diretory dirpath argument must be a non-empty string value",
@@ -157440,7 +157619,7 @@ function ArchiverError(code, data) {
this.code = code;
this.data = data;
}
import_util33.default.inherits(ArchiverError, Error);
import_util34.default.inherits(ArchiverError, Error);
// node_modules/archiver/lib/core.js
var import_readable_stream2 = __toESM(require_ours(), 1);
@@ -161870,142 +162049,6 @@ var path26 = __toESM(require("path"));
var core26 = __toESM(require_core());
var toolcache4 = __toESM(require_tool_cache());
// src/start-proxy/types.ts
var usernameSchema = {
/** The username needed to authenticate to the package registry, if any. */
username: optionalOrNull(string)
};
function hasUsername(config) {
return "username" in config;
}
var usernamePasswordSchema = {
/** The password needed to authenticate to the package registry, if any. */
password: optionalOrNull(string),
...usernameSchema
};
function hasUsernameAndPassword(config) {
return hasUsername(config) && "password" in config;
}
var tokenSchema = {
/** The token needed to authenticate to the package registry, if any. */
token: optionalOrNull(string),
...usernameSchema
};
function hasToken(config) {
return "token" in config;
}
function isToken(config) {
return "token" in config && validateSchema(tokenSchema, config);
}
var azureConfigSchema = {
"tenant-id": string,
"client-id": string
};
function isAzureConfig(config) {
return validateSchema(azureConfigSchema, config);
}
var awsConfigSchema = {
"aws-region": string,
"account-id": string,
"role-name": string,
domain: string,
"domain-owner": string,
audience: optionalOrNull(string)
};
function isAWSConfig(config) {
return validateSchema(awsConfigSchema, config);
}
var jfrogConfigSchema = {
"jfrog-oidc-provider-name": string,
audience: optionalOrNull(string),
"identity-mapping-name": optionalOrNull(string)
};
function isJFrogConfig(config) {
return validateSchema(jfrogConfigSchema, config);
}
var cloudsmithConfigSchema = {
namespace: string,
"service-slug": string,
"api-host": string
};
function isCloudsmithConfig(config) {
return validateSchema(cloudsmithConfigSchema, config);
}
var gcpConfigSchema = {
"workload-identity-provider": string,
"service-account": optionalOrNull(string),
audience: optionalOrNull(string)
};
function isGCPConfig(config) {
return validateSchema(gcpConfigSchema, config);
}
var oidcSchemas = [
{ schema: azureConfigSchema, name: "Azure" },
{ schema: awsConfigSchema, name: "AWS" },
{ schema: jfrogConfigSchema, name: "JFrog" },
{ schema: cloudsmithConfigSchema, name: "Cloudsmith" },
{ schema: gcpConfigSchema, name: "GCP" }
];
function credentialToStr(credential) {
let result = `Type: ${credential.type};`;
const appendIfDefined = (name, val) => {
if (isDefined2(val)) {
result += ` ${name}: ${val};`;
}
};
appendIfDefined("Url", credential.url);
appendIfDefined("Host", credential.host);
if (hasUsername(credential)) {
appendIfDefined("Username", credential.username);
}
if ("password" in credential) {
appendIfDefined(
"Password",
isDefined2(credential.password) ? "***" : void 0
);
}
if (hasToken(credential)) {
appendIfDefined("Token", isDefined2(credential.token) ? "***" : void 0);
}
if (isAzureConfig(credential)) {
appendIfDefined("Tenant", credential["tenant-id"]);
appendIfDefined("Client", credential["client-id"]);
} else if (isAWSConfig(credential)) {
appendIfDefined("AWS Region", credential["aws-region"]);
appendIfDefined("AWS Account", credential["account-id"]);
appendIfDefined("AWS Role", credential["role-name"]);
appendIfDefined("AWS Domain", credential.domain);
appendIfDefined("AWS Domain Owner", credential["domain-owner"]);
appendIfDefined("AWS Audience", credential.audience);
} else if (isJFrogConfig(credential)) {
appendIfDefined("JFrog Provider", credential["jfrog-oidc-provider-name"]);
appendIfDefined(
"JFrog Identity Mapping",
credential["identity-mapping-name"]
);
appendIfDefined("JFrog Audience", credential.audience);
} else if (isCloudsmithConfig(credential)) {
appendIfDefined("Cloudsmith Namespace", credential.namespace);
appendIfDefined("Cloudsmith Service Slug", credential["service-slug"]);
appendIfDefined("Cloudsmith API Host", credential["api-host"]);
} else if (isGCPConfig(credential)) {
appendIfDefined(
"GCP Workload Identity Provider",
credential["workload-identity-provider"]
);
appendIfDefined("GCP Service Account", credential["service-account"]);
appendIfDefined("GCP Audience", credential.audience);
}
return result;
}
function getAddressString(address) {
if (address.url === void 0) {
return address.host;
} else {
return address.url;
}
}
// src/start-proxy/validation.ts
var core25 = __toESM(require_core());
function cloneCredential(schema, obj) {

View File

@@ -35,6 +35,11 @@ export function isNumber(value: unknown): value is number {
return typeof value === "number";
}
/** Asserts that `value` is a boolean. */
export function isBoolean(value: unknown): value is boolean {
return typeof value === "boolean";
}
/** Asserts that `value` is either a string or undefined. */
export function isStringOrUndefined(
value: unknown,
@@ -62,14 +67,11 @@ function defaultCheck(
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
}
function makeValidator<T>(
validate: (arg: unknown) => arg is T,
required: boolean = true,
) {
function makeValidator<T>(validate: (arg: unknown) => arg is T) {
return {
validate,
check: defaultCheck(validate),
required,
required: true,
} as const satisfies Validator<T>;
}
@@ -82,6 +84,9 @@ export const string = makeValidator(isString);
/** A validator for number fields in schemas. */
export const number = makeValidator(isNumber);
/** A validator for boolean fields in schemas. */
export const boolean = makeValidator(isBoolean);
/** A validator for arrays. */
export function array<T>(validator: Validator<T>) {
const validate = (val: unknown) => {
@@ -221,6 +226,23 @@ export function validateSchema<
return result.valid;
}
/**
* Validates that `arr` is an array whose elements satisfy at least `elementSchema`.
* Additional keys are accepted in each element.
*
* @param elementSchema The schema to validate the elements against.
* @param arr The array to validate.
* @returns Asserts that `arr` has elements of `schema`'s type if validation is successful.
*/
export function validateArray<
S extends Schema,
T extends UnvalidatedArray = Array<FromSchema<S>>,
>(elementSchema: S, arr: UnvalidatedArray): arr is T {
const elementValidator = object(elementSchema);
return array(elementValidator).validate(arr);
}
export interface CheckSchemaOptions {
/** Whether to stop validation after the first error. */
failFast?: boolean;

View File

@@ -83,12 +83,6 @@ export class StartProxyError extends Error {
}
}
interface StartProxyStatus extends StatusReportBase {
// A comma-separated list of registry types which are configured for CodeQL.
// This only includes registry types we support, not all that are configured.
registry_types: string;
}
/**
* Sends a status report for the `start-proxy` action indicating a successful outcome.
*
@@ -112,7 +106,7 @@ export async function sendSuccessStatusReport(
logger,
);
if (statusReportBase !== undefined) {
const statusReport: StartProxyStatus = {
const statusReport: StatusReportBase = {
...statusReportBase,
registry_types: registry_types.join(","),
};

View File

@@ -254,13 +254,19 @@ export function credentialToStr(credential: Credential): string {
return result;
}
/** A package registry is identified by its type and address. */
export type Registry = {
/** The schema for `RegistryBase` objects. */
export const registryBaseSchema = {
/** The type of the package registry. */
type: string;
type: json.string,
/** Whether the registry replaces the base registry for the ecosystem. */
"replaces-base"?: boolean;
} & Address;
"replaces-base": json.optional(json.boolean),
} as const satisfies json.Schema;
/** Information about a registry, other than its address. */
export type RegistryBase = json.FromSchema<typeof registryBaseSchema>;
/** A package registry is identified by its type and address. */
export type Registry = RegistryBase & Address;
// If a registry has an `url`, then that takes precedence over the `host` which may or may
// not be defined.

View File

@@ -4,15 +4,17 @@ import * as uuid from "uuid";
import * as actionsUtil from "./actions-util";
import { Config } from "./config-utils";
import { EnvVar } from "./environment";
import { EnvVar, RegistryProxyVars } from "./environment";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import { ToolsSource } from "./setup-codeql";
import type { Registry } from "./start-proxy";
import {
ActionName,
createInitWithConfigStatusReport,
createStatusReportBase,
getActionsStatus,
getRegistryTypesFromEnv,
getJobUUID,
InitStatusReport,
InitWithConfigStatusReport,
@@ -22,12 +24,69 @@ import {
setupActionsVars,
createTestConfig,
makeMacro,
getTestEnv,
RecordingLogger,
callee,
} from "./testing-utils";
import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util";
setupTests(test);
test("getRegistryTypesFromEnv - gets unique registry types from environment", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([
{ type: "git_source", url: "https://example.com" },
{ type: "git_source", url: "https://github.com" },
{ type: "docker_registry", url: "https://registry.example.com" },
] satisfies Array<Partial<Registry>>),
});
const result = getRegistryTypesFromEnv(logger, env);
t.deepEqual(result, ["git_source", "docker_registry"].sort().join(","));
});
test("getRegistryTypesFromEnv - returns undefined if the env var is not set", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({});
const result = getRegistryTypesFromEnv(logger, env);
t.is(result, undefined);
});
test("getRegistryTypesFromEnv - returns undefined if the env var is not valid JSON", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({ [RegistryProxyVars.PROXY_URLS]: "[" });
const result = getRegistryTypesFromEnv(logger, env);
t.is(result, undefined);
});
test("getRegistryTypesFromEnv - returns undefined if the env var is unexpected JSON", async (t) => {
const logger = new RecordingLogger(true);
t.is(
getRegistryTypesFromEnv(
logger,
getTestEnv({
// Top-level object rather than an array of objects.
[RegistryProxyVars.PROXY_URLS]: JSON.stringify({ type: "git_source" }),
}),
),
undefined,
);
t.is(
getRegistryTypesFromEnv(
logger,
getTestEnv({
// Object has no "type" key.
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([{}]),
}),
),
undefined,
);
});
test("getJobUUID - generates valid UUIDs", async (t) => {
await callee(getJobUUID)
.withArgs()
@@ -74,6 +133,9 @@ function setupEnvironmentAndStub(tmpDir: string) {
process.env[EnvVar.ANALYSIS_KEY] = "analysis-key";
process.env["ImageVersion"] = "2023.05.19.1";
process.env[RegistryProxyVars.PROXY_URLS] = JSON.stringify([
{ type: "maven_repository" },
] satisfies Array<Partial<Registry>>);
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
getRequiredInput.withArgs("matrix").resolves("input/matrix");
@@ -117,6 +179,7 @@ test.serial("createStatusReportBase", async (t) => {
t.is(typeof statusReport.job_run_uuid, "string");
t.is(statusReport.languages, "java,swift");
t.is(statusReport.ref, process.env["GITHUB_REF"]!);
t.is(statusReport.registry_types, "maven_repository");
t.is(statusReport.runner_available_disk_space_bytes, 100);
t.is(statusReport.runner_image_version, process.env["ImageVersion"]);
t.is(statusReport.runner_os, process.env["RUNNER_OS"]!);

View File

@@ -19,12 +19,14 @@ import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
import { DocUrl } from "./doc-url";
import { EnvVar } from "./environment";
import { EnvVar, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
import { getRef } from "./git-utils";
import * as json from "./json";
import type { Logger } from "./logging";
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
import { getRepositoryNwo } from "./repository";
import type { ToolsSource } from "./setup-codeql";
import { registryBaseSchema } from "./start-proxy/types";
import {
ConfigurationError,
getRequiredEnvParam,
@@ -185,6 +187,12 @@ export interface StatusReportBase {
ml_powered_javascript_queries?: string;
/** Ref that the workflow was triggered on. */
ref: string;
/**
* A comma-separated list of private registry types which are configured for CodeQL.
* This only includes registry types we support (as determined by the `start-proxy` action),
* not all that are configured.
*/
registry_types?: string;
/** Action runner hardware architecture (context runner.arch). */
runner_arch?: string;
/** Available disk space on the runner, in bytes. */
@@ -288,6 +296,50 @@ export interface EventReport {
started_at: string;
}
/**
* Attempts to retrieve a list of private registry types from the `CODEQL_PROXY_URLS` environment
* variable and returns it as a comma-separated string if successful. Returns `undefined` otherwise.
*/
export function getRegistryTypesFromEnv(
logger: Logger,
env: ReadOnlyEnv = getEnv(),
): string | undefined {
// Try to get the value of the environment variable.
const value = env.getOptional(RegistryProxyVars.PROXY_URLS);
if (value === undefined) {
return undefined;
}
// Try to parse the JSON we expect to find in it and return the comma-separated list of
// (unique) registry types.
try {
const data = JSON.parse(value) as unknown;
// Check that the parsed JSON meets our expectations.
if (!json.isArray(data)) {
logger.debug(
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array, but got '${typeof data}'.`,
);
return undefined;
}
if (!json.validateArray(registryBaseSchema, data)) {
logger.debug(
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array of registry objects, but got something else.`,
);
return undefined;
}
const types = new Set(data.map((r) => r.type));
return Array.from(types).sort().join(",");
} catch (err) {
logger.debug(
`Failed to parse '${RegistryProxyVars.PROXY_URLS}': ${getErrorMessage(err)}.`,
);
return undefined;
}
}
/**
* Compose a StatusReport.
*
@@ -350,6 +402,7 @@ export async function createStatusReportBase(
job_name: jobName,
job_run_uuid: jobRunUUID,
ref,
registry_types: getRegistryTypesFromEnv(logger),
runner_os: runnerOs,
started_at: workflowStartedAt,
status,