-
Notifications
You must be signed in to change notification settings - Fork 453
Expand file tree
/
Copy pathproperties.ts
More file actions
228 lines (198 loc) · 7.85 KB
/
properties.ts
File metadata and controls
228 lines (198 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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);
}