forked from parse-community/parse-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
CiVersionCheck.js
290 lines (261 loc) · 10.1 KB
/
CiVersionCheck.js
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
const core = require('@actions/core');
const semver = require('semver');
const yaml = require('yaml');
const fs = require('fs').promises;
/**
* This checks the CI version of an environment variable in a YAML file
* against a list of released versions of a package.
*/
class CiVersionCheck {
/**
* The constructor.
* @param {Object} config The config.
* @param {String} config.packageName The package name to check.
* @param {String} config.packageSupportUrl The URL to the package website
* that shows the End-of-Life support dates.
* @param {String} config.yamlFilePath The path to the GitHub workflow YAML
* file that contains the tests.
* @param {String} config.ciEnvironmentsKeyPath The key path in the CI YAML
* file to the environment specifications.
* @param {String} config.ciVersionKey The key in the CI YAML file to
* determine the package version.
* @param {Array<String>} config.releasedVersions The released versions of
* the package to check against.
* @param {Array<String>} config.ignoreReleasedVersions The versions to
* ignore when checking whether the CI tests against the latest versions.
* This can be used in case there is a package release for which Parse
* Server compatibility is not required.
* @param {String} [config.latestComponent='patch'] The version component
* (`major`, `minor`, `patch`) that must be the latest released version.
* Default is `patch`.
*
* For example:
* - Released versions: 1.0.0, 1.2.0, 1.2.1, 1.3.0, 1.3.1, 2.0.0
* - Tested version: 1.2.0
*
* If the latest version component is `patch`, then the check would
* fail and recommend an upgrade to version 1.2.1 and to add additional
* tests against 1.3.1 and 2.0.0.
* If the latest version component is `minor` then the check would
* fail and recommend an upgrade to version 1.3.0 and to add an additional
* test against 2.0.0.
* If the latest version component is `major` then the check would
* fail and recommend an upgrade to version 2.0.0.
*/
constructor(config) {
const {
packageName,
packageSupportUrl,
yamlFilePath,
ciEnvironmentsKeyPath,
ciVersionKey,
releasedVersions,
ignoreReleasedVersions = [],
latestComponent = CiVersionCheck.versionComponents.patch,
} = config;
// Ensure required params are set
if ([
packageName,
packageSupportUrl,
yamlFilePath,
ciEnvironmentsKeyPath,
ciVersionKey,
releasedVersions,
].includes(undefined)) {
throw 'invalid configuration';
}
if (!Object.keys(CiVersionCheck.versionComponents).includes(latestComponent)) {
throw 'invalid configuration for latestComponent';
}
this.packageName = packageName;
this.packageSupportUrl = packageSupportUrl;
this.yamlFilePath = yamlFilePath;
this.ciEnvironmentsKeyPath = ciEnvironmentsKeyPath;
this.ciVersionKey = ciVersionKey;
this.releasedVersions = releasedVersions;
this.ignoreReleasedVersions = ignoreReleasedVersions;
this.latestComponent = latestComponent;
}
/**
* The definition of version components.
*/
static get versionComponents() {
return Object.freeze({
major: 'major',
minor: 'minor',
patch: 'patch',
});
}
/**
* Returns the test environments as specified in the YAML file.
*/
async getTests() {
try {
// Get CI workflow
const ciYaml = await fs.readFile(this.yamlFilePath, 'utf-8');
const ci = yaml.parse(ciYaml);
// Extract package versions
let versions = this.ciEnvironmentsKeyPath.split('.').reduce((o,k) => o !== undefined ? o[k] : undefined, ci);
versions = Object.entries(versions)
.map(entry => entry[1])
.filter(entry => entry[this.ciVersionKey]);
return versions;
} catch (e) {
throw `Failed to determine ${this.packageName} versions from CI YAML file with error: ${e}`;
}
}
/**
* Returns the package versions which are missing in the CI environment.
* @param {Array<String>} releasedVersions The released versions; need to
* be sorted descending.
* @param {Array<String>} testedVersions The tested versions.
* @param {String} versionComponent The latest version component.
* @returns {Array<String>} The untested versions.
*/
getUntestedVersions(releasedVersions, testedVersions, versionComponent) {
// Use these example values for debugging the version range logic below
// versionComponent = CiVersionCheck.versionComponents.patch;
// this.ignoreReleasedVersions = ['<4.4.0', '~4.7.0'];
// testedVersions = ['4.4.3'];
// releasedVersions = [
// '5.0.0-rc0',
// '5.0.0',
// '4.9.1',
// '4.9.0',
// '4.8.1',
// '4.8.0',
// '4.7.1',
// '4.7.0',
// '4.4.3',
// '4.4.2',
// '4.4.0',
// '4.1.0',
// '3.5.0',
// ];
// Determine operator for range comparison
const operator = versionComponent == CiVersionCheck.versionComponents.major
? '>='
: versionComponent == CiVersionCheck.versionComponents.minor
? '^'
: '~'
// Get all untested versions
const untestedVersions = releasedVersions.reduce((m, v) => {
// If the version should be ignored, skip it
if (this.ignoreReleasedVersions.length > 0 && semver.satisfies(v, this.ignoreReleasedVersions.join(' || '))) {
return m;
}
// If the version is a pre-release, skip it
if ((semver.prerelease(v) || []).length > 0) {
return m;
}
// If a satisfying version has already been added to untested, skip it
if (semver.maxSatisfying(m, `${operator}${v}`)) {
return m;
}
// If a satisfying version is already tested, skip it
if (semver.maxSatisfying(testedVersions, `${operator}${v}`)) {
return m;
}
// Add version
m.push(v);
return m;
}, []);
return untestedVersions;
}
/**
* Returns the latest version for a given version and component.
* @param {Array<String>} versions The versions in which to search.
* @param {String} version The version for which a newer version
* should be searched.
* @param {String} versionComponent The version component up to
* which the latest version should be checked.
* @returns {String|undefined} The newer version.
*/
getNewerVersion(versions, version, versionComponent) {
// Determine operator for range comparison
const operator = versionComponent == CiVersionCheck.versionComponents.major
? '>='
: versionComponent == CiVersionCheck.versionComponents.minor
? '^'
: '~'
const latest = semver.maxSatisfying(versions, `${operator}${version}`);
// If the version should be ignored, skip it
if (this.ignoreReleasedVersions.length > 0 && semver.satisfies(latest, this.ignoreReleasedVersions.join(' || '))) {
return undefined;
}
// Return the latest version if it is newer than any currently used version
return semver.gt(latest, version) ? latest : undefined;
}
/**
* This validates that the given versions strictly follow semver
* syntax.
* @param {Array<String>} versions The versions to check.
*/
_validateVersionSyntax(versions) {
for (const version of versions) {
if (!semver.valid(version)) {
throw version;
}
}
}
/**
* Runs the check.
*/
async check() {
try {
console.log(`\nChecking ${this.packageName} versions in CI environments...`);
// Validate released versions syntax
try {
this._validateVersionSyntax(this.releasedVersions);
} catch (e) {
core.setFailed(`Failed to check ${this.packageName} versions because released version '${e}' does not follow semver syntax (x.y.z).`);
return;
}
// Sort versions descending
semver.sort(this.releasedVersions).reverse()
// Get tested package versions from CI
const tests = await this.getTests();
// Is true if any of the checks failed
let failed = false;
// Check whether each tested version is the latest patch
for (const test of tests) {
const version = test[this.ciVersionKey];
// Validate version syntax
try {
this._validateVersionSyntax([version]);
} catch (e) {
core.setFailed(`Failed to check ${this.packageName} versions because environment version '${e}' does not follow semver syntax (x.y.z).`);
return;
}
const newer = this.getNewerVersion(this.releasedVersions, version, this.latestComponent);
if (newer) {
console.log(`❌ CI environment '${test.name}' uses an old ${this.packageName} ${this.latestComponent} version ${version} instead of ${newer}.`);
failed = true;
} else {
console.log(`✅ CI environment '${test.name}' uses the latest ${this.packageName} ${this.latestComponent} version ${version}.`);
}
}
// Check whether there is a newer component version available that is not tested
const testedVersions = tests.map(test => test[this.ciVersionKey]);
const untested = this.getUntestedVersions(this.releasedVersions, testedVersions, this.latestComponent);
if (untested.length > 0) {
console.log(`❌ CI does not have environments using the following versions of ${this.packageName}: ${untested.join(', ')}.`);
failed = true;
} else {
console.log(`✅ CI has environments using all recent versions of ${this.packageName}.`);
}
if (failed) {
core.setFailed(
`CI environments are not up-to-date with the latest ${this.packageName} versions.` +
`\n\nCheck the error messages above and update the ${this.packageName} versions in the CI YAML ` +
`file.\n\nℹ️ Additionally, there may be versions of ${this.packageName} that have reached their official end-of-life ` +
`support date and should be removed from the CI, see ${this.packageSupportUrl}.`
);
}
} catch (e) {
const msg = `Failed to check ${this.packageName} versions with error: ${e}`;
core.setFailed(msg);
}
}
}
module.exports = CiVersionCheck;