Files
codeql-action/src/feature-flags/properties.ts
T
2026-03-13 10:56:36 +00:00

229 lines
7.9 KiB
TypeScript

import { isDynamicWorkflow } from "../actions-util";
import { getRepositoryProperties } from "../api-client";
import { Logger } from "../logging";
import { RepositoryNwo } from "../repository";
/** The common prefix that we expect all of our repository properties to have. */
export const GITHUB_CODEQL_PROPERTY_PREFIX = "github-codeql-";
/**
* Enumerates repository property names that have some meaning to us.
*/
export enum RepositoryPropertyName {
DISABLE_OVERLAY = "github-codeql-disable-overlay",
EXTRA_QUERIES = "github-codeql-extra-queries",
FILE_COVERAGE_ON_PRS = "github-codeql-file-coverage-on-prs",
}
/** Parsed types of the known repository properties. */
export type AllRepositoryProperties = {
[RepositoryPropertyName.DISABLE_OVERLAY]: boolean;
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
};
/** Parsed repository properties. */
export type RepositoryProperties = Partial<AllRepositoryProperties>;
/** Maps known repository properties to the type we expect to get from the API. */
export type RepositoryPropertyApiType = {
[RepositoryPropertyName.DISABLE_OVERLAY]: string;
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: string;
};
/** The type of functions which take the `value` from the API and try to convert it to the type we want. */
export type PropertyParser<K extends RepositoryPropertyName> = (
name: K,
value: RepositoryPropertyApiType[K],
logger: Logger,
) => AllRepositoryProperties[K];
/** Possible types of `value`s we get from the API. */
export type RepositoryPropertyValue = string | string[];
/** The type of repository property configurations. */
export type PropertyInfo<K extends RepositoryPropertyName> = {
/** A validator which checks that the value received from the API is what we expect. */
validate: (
value: RepositoryPropertyValue,
) => value is RepositoryPropertyApiType[K];
/** A `PropertyParser` for the property. */
parse: PropertyParser<K>;
};
/** Determines whether a value from the API is a string or not. */
function isString(value: RepositoryPropertyValue): value is string {
return typeof value === "string";
}
/** A repository property that we expect to contain a string value. */
const stringProperty = {
validate: isString,
parse: parseStringRepositoryProperty,
};
/** A repository property that we expect to contain a boolean value. */
const booleanProperty = {
// The value from the API should come as a string, which we then parse into a boolean.
validate: isString,
parse: parseBooleanRepositoryProperty,
};
/** Parsers that transform repository properties from the API response into typed values. */
const repositoryPropertyParsers: {
[K in RepositoryPropertyName]: PropertyInfo<K>;
} = {
[RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty,
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
};
/**
* A repository property has a name and a value.
*/
export interface GitHubRepositoryProperty {
property_name: string;
value: RepositoryPropertyValue;
}
/**
* The API returns a list of `GitHubRepositoryProperty` objects.
*/
export type GitHubPropertiesResponse = GitHubRepositoryProperty[];
/**
* Retrieves all known repository properties from the API.
*
* @param logger The logger to use.
* @param repositoryNwo Information about the repository for which to load properties.
* @returns Returns a partial mapping from `RepositoryPropertyName` to values.
*/
export async function loadPropertiesFromApi(
logger: Logger,
repositoryNwo: RepositoryNwo,
): Promise<RepositoryProperties> {
try {
const response = await getRepositoryProperties(repositoryNwo);
const remoteProperties = response.data as GitHubPropertiesResponse;
if (!Array.isArray(remoteProperties)) {
throw new Error(
`Expected repository properties API to return an array, but got: ${JSON.stringify(response.data)}`,
);
}
logger.debug(
`Retrieved ${remoteProperties.length} repository properties: ${remoteProperties.map((p) => p.property_name).join(", ")}`,
);
const properties: RepositoryProperties = {};
const unrecognisedProperties: string[] = [];
for (const property of remoteProperties) {
if (property.property_name === undefined) {
throw new Error(
`Expected repository property object to have a 'property_name', but got: ${JSON.stringify(property)}`,
);
}
if (isKnownPropertyName(property.property_name)) {
setProperty(properties, property.property_name, property.value, logger);
} else if (
property.property_name.startsWith(GITHUB_CODEQL_PROPERTY_PREFIX) &&
!isDynamicWorkflow()
) {
unrecognisedProperties.push(property.property_name);
}
}
if (Object.keys(properties).length === 0) {
logger.debug("No known repository properties were found.");
} else {
logger.debug(
"Loaded the following values for the repository properties:",
);
for (const [property, value] of Object.entries(properties).sort(
([nameA], [nameB]) => nameA.localeCompare(nameB),
)) {
logger.debug(` ${property}: ${value}`);
}
}
// Emit a warning if we encountered unrecognised properties that have our prefix.
if (unrecognisedProperties.length > 0) {
const unrecognisedPropertyList = unrecognisedProperties
.map((name) => `'${name}'`)
.join(", ");
logger.warning(
`Found repository properties (${unrecognisedPropertyList}), ` +
"which look like CodeQL Action repository properties, " +
"but which are not understood by this version of the CodeQL Action. " +
"Do you need to update to a newer version?",
);
}
return properties;
} catch (e) {
throw new Error(
`Encountered an error while trying to determine repository properties: ${e}`,
);
}
}
/**
* Validate that `value` has the correct type for `K` and, if so, update the partial set of repository
* properties with the parsed value of the specified property.
*/
function setProperty<K extends RepositoryPropertyName>(
properties: RepositoryProperties,
name: K,
value: RepositoryPropertyValue,
logger: Logger,
): void {
const propertyOptions = repositoryPropertyParsers[name];
// We perform the validation here for two reasons:
// 1. This function is only called if `name` is a property we care about, to avoid throwing
// on unrelated properties that may use representations we do not support.
// 2. The `propertyOptions.validate` function checks that the type of `value` we received from
// the API is what expect and narrows the type accordingly, allowing us to call `parse`.
if (propertyOptions.validate(value)) {
properties[name] = propertyOptions.parse(name, value, logger);
} else {
throw new Error(
`Unexpected value for repository property '${name}' (${typeof value}), got: ${JSON.stringify(value)}`,
);
}
}
/** Parse a boolean repository property. */
function parseBooleanRepositoryProperty(
name: string,
value: string,
logger: Logger,
): boolean {
if (value !== "true" && value !== "false") {
logger.warning(
`Repository property '${name}' has unexpected value '${value}'. Expected 'true' or 'false'. Defaulting to false.`,
);
}
return value === "true";
}
/** Parse a string repository property. */
function parseStringRepositoryProperty(_name: string, value: string): string {
return value;
}
/** Set of known repository property names, for fast lookups. */
const KNOWN_REPOSITORY_PROPERTY_NAMES = new Set<string>(
Object.values(RepositoryPropertyName),
);
/** Returns whether the given value is a known repository property name. */
function isKnownPropertyName(name: string): name is RepositoryPropertyName {
return KNOWN_REPOSITORY_PROPERTY_NAMES.has(name);
}