import * as github from "@actions/github"; import { isDynamicWorkflow } from "../actions-util"; import { getRepositoryProperties } from "../api-client"; import { Logger } from "../logging"; import { RepositoryNwo } from "../repository"; import { getErrorMessage, Result, Success, Failure } from "../util"; /** 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", TOOLS = "github-codeql-tools", } /** Parsed types of the known repository properties. */ export type AllRepositoryProperties = { [RepositoryPropertyName.DISABLE_OVERLAY]: boolean; [RepositoryPropertyName.EXTRA_QUERIES]: string; [RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean; [RepositoryPropertyName.TOOLS]: string; }; /** Parsed repository properties. */ export type RepositoryProperties = Partial; /** 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; [RepositoryPropertyName.TOOLS]: 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 = ( 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 = { /** 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; }; /** 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; } = { [RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty, [RepositoryPropertyName.EXTRA_QUERIES]: stringProperty, [RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty, [RepositoryPropertyName.TOOLS]: stringProperty, }; /** * 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 { 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}`, ); } } /** * Loads [repository properties](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) if applicable. */ export async function loadRepositoryProperties( repositoryNwo: RepositoryNwo, logger: Logger, ): Promise> { // See if we can skip loading repository properties early. In particular, // repositories owned by users cannot have repository properties, so we can // skip the API call entirely in that case. const repositoryOwnerType = github.context.payload.repository?.owner.type; logger.debug( `Repository owner type is '${repositoryOwnerType ?? "unknown"}'.`, ); if (repositoryOwnerType === "User") { logger.debug( "Skipping loading repository properties because the repository is owned by a user and " + "therefore cannot have repository properties.", ); return new Success({}); } try { return new Success(await loadPropertiesFromApi(logger, repositoryNwo)); } catch (error) { logger.info( `Failed to load repository properties: ${getErrorMessage(error)}`, ); return new Failure(error); } } /** * 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( 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( 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); }