mirror of
https://github.com/github/codeql-action.git
synced 2026-05-25 00:24:42 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ba697a3ec | |||
| 74374a3893 | |||
| 3e85884434 | |||
| 5aed9f7d64 | |||
| f9feddd874 | |||
| 15f19e1870 | |||
| 51cc08af6f | |||
| 71b697dd8b | |||
| f5808271b0 | |||
| da26a016ee | |||
| 4536424fcf | |||
| d98bedfdea | |||
| 952a538c24 | |||
| 04d4fd51e9 | |||
| c05837d3e8 |
@@ -9,7 +9,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/update-supported-enterprise-server-versions.yml
|
||||
- .github/workflows/update-supported-enterprise-server-versions/update.py
|
||||
- pr-checks/update-ghes-versions.ts
|
||||
|
||||
jobs:
|
||||
update-supported-enterprise-server-versions:
|
||||
@@ -22,12 +22,18 @@ jobs:
|
||||
pull-requests: write # needed to create pull request
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Checkout CodeQL Action
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Checkout Enterprise Releases
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -35,18 +41,18 @@ jobs:
|
||||
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
|
||||
path: ${{ github.workspace }}/enterprise-releases/
|
||||
sparse-checkout: releases.json
|
||||
|
||||
- name: Update Supported Enterprise Server Versions
|
||||
working-directory: pr-checks
|
||||
run: |
|
||||
cd ./.github/workflows/update-supported-enterprise-server-versions/
|
||||
python3 -m pip install pipenv
|
||||
pipenv install
|
||||
pipenv run ./update.py
|
||||
npx tsx update-ghes-versions.ts
|
||||
rm --recursive "$ENTERPRISE_RELEASES_PATH"
|
||||
npm ci
|
||||
npm run build
|
||||
env:
|
||||
ENTERPRISE_RELEASES_PATH: ${{ github.workspace }}/enterprise-releases/
|
||||
|
||||
- name: Rebuild
|
||||
run: npm run build
|
||||
|
||||
- name: Update git config
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
semver = "*"
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e3ba923dcb4888e05de5448c18a732bf40197e80fabfa051a61c01b22c504879"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"semver": {
|
||||
"hashes": [
|
||||
"sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4",
|
||||
"sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.13.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import semver
|
||||
|
||||
_API_COMPATIBILITY_PATH = pathlib.Path(__file__).absolute().parents[3] / "src" / "api-compatibility.json"
|
||||
_ENTERPRISE_RELEASES_PATH = pathlib.Path(os.environ["ENTERPRISE_RELEASES_PATH"])
|
||||
_RELEASE_FILE_PATH = _ENTERPRISE_RELEASES_PATH / "releases.json"
|
||||
_FIRST_SUPPORTED_RELEASE = semver.VersionInfo.parse("2.22.0") # Versions older than this did not include Code Scanning.
|
||||
|
||||
def main():
|
||||
api_compatibility_data = json.loads(_API_COMPATIBILITY_PATH.read_text())
|
||||
|
||||
releases = json.loads(_RELEASE_FILE_PATH.read_text())
|
||||
|
||||
# Remove GHES version using a previous version numbering scheme.
|
||||
if "11.10" in releases:
|
||||
del releases["11.10"]
|
||||
|
||||
oldest_supported_release = None
|
||||
newest_supported_release = semver.VersionInfo.parse(api_compatibility_data["maximumVersion"] + ".0")
|
||||
|
||||
for release_version_string, release_data in releases.items():
|
||||
release_version = semver.VersionInfo.parse(release_version_string + ".0")
|
||||
if release_version < _FIRST_SUPPORTED_RELEASE:
|
||||
continue
|
||||
|
||||
if release_version > newest_supported_release:
|
||||
feature_freeze_date = datetime.date.fromisoformat(release_data["feature_freeze"])
|
||||
if feature_freeze_date < datetime.date.today() + datetime.timedelta(weeks=2):
|
||||
newest_supported_release = release_version
|
||||
|
||||
if oldest_supported_release is None or release_version < oldest_supported_release:
|
||||
end_of_life_date = datetime.date.fromisoformat(release_data["end"])
|
||||
# The GHES version is not actually end of life until the end of the day specified by
|
||||
# `end_of_life_date`. Wait an extra week to be safe.
|
||||
is_end_of_life = datetime.date.today() > end_of_life_date + datetime.timedelta(weeks=1)
|
||||
if not is_end_of_life:
|
||||
oldest_supported_release = release_version
|
||||
|
||||
api_compatibility_data = {
|
||||
"minimumVersion": f"{oldest_supported_release.major}.{oldest_supported_release.minor}",
|
||||
"maximumVersion": f"{newest_supported_release.major}.{newest_supported_release.minor}",
|
||||
}
|
||||
_API_COMPATIBILITY_PATH.write_text(json.dumps(api_compatibility_data, sort_keys=True) + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+43
@@ -26704,6 +26704,47 @@ var require_coerce = __commonJS({
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/semver/functions/truncate.js
|
||||
var require_truncate = __commonJS({
|
||||
"node_modules/semver/functions/truncate.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var parse2 = require_parse2();
|
||||
var constants = require_constants6();
|
||||
var SemVer = require_semver();
|
||||
var truncate = (version, truncation, options) => {
|
||||
if (!constants.RELEASE_TYPES.includes(truncation)) {
|
||||
return null;
|
||||
}
|
||||
const clonedVersion = cloneInputVersion(version, options);
|
||||
return clonedVersion && doTruncation(clonedVersion, truncation);
|
||||
};
|
||||
var cloneInputVersion = (version, options) => {
|
||||
const versionStringToParse = version instanceof SemVer ? version.version : version;
|
||||
return parse2(versionStringToParse, options);
|
||||
};
|
||||
var doTruncation = (version, truncation) => {
|
||||
if (isPrerelease(truncation)) {
|
||||
return version.version;
|
||||
}
|
||||
version.prerelease = [];
|
||||
switch (truncation) {
|
||||
case "major":
|
||||
version.minor = 0;
|
||||
version.patch = 0;
|
||||
break;
|
||||
case "minor":
|
||||
version.patch = 0;
|
||||
break;
|
||||
}
|
||||
return version.format();
|
||||
};
|
||||
var isPrerelease = (type2) => {
|
||||
return type2.startsWith("pre");
|
||||
};
|
||||
module2.exports = truncate;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/semver/internal/lrucache.js
|
||||
var require_lrucache = __commonJS({
|
||||
"node_modules/semver/internal/lrucache.js"(exports2, module2) {
|
||||
@@ -27738,6 +27779,7 @@ var require_semver2 = __commonJS({
|
||||
var lte = require_lte();
|
||||
var cmp = require_cmp();
|
||||
var coerce3 = require_coerce();
|
||||
var truncate = require_truncate();
|
||||
var Comparator = require_comparator();
|
||||
var Range2 = require_range();
|
||||
var satisfies2 = require_satisfies();
|
||||
@@ -27776,6 +27818,7 @@ var require_semver2 = __commonJS({
|
||||
lte,
|
||||
cmp,
|
||||
coerce: coerce3,
|
||||
truncate,
|
||||
Comparator,
|
||||
Range: Range2,
|
||||
satisfies: satisfies2,
|
||||
|
||||
Generated
+43
@@ -28009,6 +28009,47 @@ var require_coerce = __commonJS({
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/semver/functions/truncate.js
|
||||
var require_truncate = __commonJS({
|
||||
"node_modules/semver/functions/truncate.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var parse2 = require_parse2();
|
||||
var constants = require_constants6();
|
||||
var SemVer = require_semver();
|
||||
var truncate = (version, truncation, options) => {
|
||||
if (!constants.RELEASE_TYPES.includes(truncation)) {
|
||||
return null;
|
||||
}
|
||||
const clonedVersion = cloneInputVersion(version, options);
|
||||
return clonedVersion && doTruncation(clonedVersion, truncation);
|
||||
};
|
||||
var cloneInputVersion = (version, options) => {
|
||||
const versionStringToParse = version instanceof SemVer ? version.version : version;
|
||||
return parse2(versionStringToParse, options);
|
||||
};
|
||||
var doTruncation = (version, truncation) => {
|
||||
if (isPrerelease(truncation)) {
|
||||
return version.version;
|
||||
}
|
||||
version.prerelease = [];
|
||||
switch (truncation) {
|
||||
case "major":
|
||||
version.minor = 0;
|
||||
version.patch = 0;
|
||||
break;
|
||||
case "minor":
|
||||
version.patch = 0;
|
||||
break;
|
||||
}
|
||||
return version.format();
|
||||
};
|
||||
var isPrerelease = (type2) => {
|
||||
return type2.startsWith("pre");
|
||||
};
|
||||
module2.exports = truncate;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/semver/internal/lrucache.js
|
||||
var require_lrucache = __commonJS({
|
||||
"node_modules/semver/internal/lrucache.js"(exports2, module2) {
|
||||
@@ -29043,6 +29084,7 @@ var require_semver2 = __commonJS({
|
||||
var lte = require_lte();
|
||||
var cmp = require_cmp();
|
||||
var coerce3 = require_coerce();
|
||||
var truncate = require_truncate();
|
||||
var Comparator = require_comparator();
|
||||
var Range2 = require_range();
|
||||
var satisfies2 = require_satisfies();
|
||||
@@ -29081,6 +29123,7 @@ var require_semver2 = __commonJS({
|
||||
lte,
|
||||
cmp,
|
||||
coerce: coerce3,
|
||||
truncate,
|
||||
Comparator,
|
||||
Range: Range2,
|
||||
satisfies: satisfies2,
|
||||
|
||||
Generated
+4
-3
@@ -8320,9 +8320,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -10404,6 +10404,7 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,3 +21,9 @@ export const BUILTIN_LANGUAGES_FILE = path.join(
|
||||
"languages",
|
||||
"builtin.json",
|
||||
);
|
||||
|
||||
/** Path to the api-compatibility.json file. */
|
||||
export const API_COMPATIBILITY_FILE = path.join(
|
||||
SOURCE_ROOT,
|
||||
"api-compatibility.json",
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/*
|
||||
* Tests for the update-ghes-versions.ts script
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
addWeeks,
|
||||
determineSupportedRange,
|
||||
type EnterpriseReleases,
|
||||
parseEnterpriseVersion,
|
||||
printEnterpriseVersion,
|
||||
} from "./update-ghes-versions";
|
||||
|
||||
describe("parseEnterpriseVersion", async () => {
|
||||
await it("parses a two-component version string", () => {
|
||||
const ver = parseEnterpriseVersion("3.10");
|
||||
assert.notEqual(ver, null);
|
||||
assert.equal(ver!.major, 3);
|
||||
assert.equal(ver!.minor, 10);
|
||||
assert.equal(ver!.patch, 0);
|
||||
});
|
||||
|
||||
await it("parses a three-component version string", () => {
|
||||
const ver = parseEnterpriseVersion("3.10.2");
|
||||
assert.notEqual(ver, null);
|
||||
assert.equal(ver!.major, 3);
|
||||
assert.equal(ver!.minor, 10);
|
||||
assert.equal(ver!.patch, 2);
|
||||
});
|
||||
|
||||
await it("returns null for invalid input", () => {
|
||||
assert.equal(parseEnterpriseVersion("not-a-version"), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("printEnterpriseVersion", async () => {
|
||||
await it("prints only major.minor when patch is 0", () => {
|
||||
const ver = parseEnterpriseVersion("3.10")!;
|
||||
assert.equal(printEnterpriseVersion(ver), "3.10");
|
||||
});
|
||||
|
||||
await it("includes patch when non-zero", () => {
|
||||
const ver = parseEnterpriseVersion("3.10.2")!;
|
||||
assert.equal(printEnterpriseVersion(ver), "3.10.2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addWeeks", async () => {
|
||||
await it("adds weeks to a date", () => {
|
||||
const date = new Date("2025-01-01T00:00:00Z");
|
||||
const result = addWeeks(date, 2);
|
||||
assert.equal(result.toISOString(), "2025-01-15T00:00:00.000Z");
|
||||
});
|
||||
|
||||
await it("does not mutate the original date", () => {
|
||||
const date = new Date("2025-01-01T00:00:00Z");
|
||||
addWeeks(date, 2);
|
||||
assert.equal(date.toISOString(), "2025-01-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to build a release entry with a feature freeze and end-of-life date.
|
||||
* Dates are ISO date strings (e.g. "2025-06-01").
|
||||
*/
|
||||
function release(featureFreeze: string, end: string) {
|
||||
return { feature_freeze: featureFreeze, end };
|
||||
}
|
||||
|
||||
describe("determineSupportedRange", async () => {
|
||||
// A fixed "today" for deterministic tests.
|
||||
const today = new Date("2025-06-15");
|
||||
|
||||
const farPastEnd = "2020-01-01";
|
||||
const farFutureEnd = "2099-12-31";
|
||||
const farPastFreeze = "2020-01-01";
|
||||
const farFutureFreeze = "2099-12-31";
|
||||
|
||||
await it("returns the only supported release as both min and max", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
"3.10": release(farPastFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.10" },
|
||||
releases,
|
||||
);
|
||||
assert.equal(result.minimumVersion, "3.10");
|
||||
assert.equal(result.maximumVersion, "3.10");
|
||||
});
|
||||
|
||||
await it("determines the range from multiple supported releases", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
"3.10": release(farPastFreeze, farFutureEnd),
|
||||
"3.11": release(farPastFreeze, farFutureEnd),
|
||||
"3.12": release(farPastFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.12" },
|
||||
releases,
|
||||
);
|
||||
assert.equal(result.minimumVersion, "3.10");
|
||||
assert.equal(result.maximumVersion, "3.12");
|
||||
});
|
||||
|
||||
await it("drops an end-of-life release from the minimum", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
// 3.10 has been end of life for a long time.
|
||||
"3.10": release(farPastFreeze, farPastEnd),
|
||||
"3.11": release(farPastFreeze, farFutureEnd),
|
||||
"3.12": release(farPastFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.12" },
|
||||
releases,
|
||||
);
|
||||
assert.equal(result.minimumVersion, "3.11");
|
||||
assert.equal(result.maximumVersion, "3.12");
|
||||
});
|
||||
|
||||
await it("bumps the maximum when a newer release's feature freeze has passed", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
"3.10": release(farPastFreeze, farFutureEnd),
|
||||
"3.11": release(farPastFreeze, farFutureEnd),
|
||||
// 3.12 has a feature freeze far in the past, so it should be picked up.
|
||||
"3.12": release(farPastFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
// The stored maximum is 3.11, but 3.12 should be picked up.
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.11" },
|
||||
releases,
|
||||
);
|
||||
assert.equal(result.minimumVersion, "3.10");
|
||||
assert.equal(result.maximumVersion, "3.12");
|
||||
});
|
||||
|
||||
await it("does not bump the maximum when feature freeze is far in the future", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
"3.10": release(farPastFreeze, farFutureEnd),
|
||||
"3.11": release(farPastFreeze, farFutureEnd),
|
||||
// 3.12 has a feature freeze far in the future, so it should NOT be picked up.
|
||||
"3.12": release(farFutureFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.11" },
|
||||
releases,
|
||||
);
|
||||
assert.equal(result.minimumVersion, "3.10");
|
||||
assert.equal(result.maximumVersion, "3.11");
|
||||
});
|
||||
|
||||
await it("ignores releases older than the first supported release (2.22)", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
"2.21": release(farPastFreeze, farFutureEnd),
|
||||
"3.10": release(farPastFreeze, farFutureEnd),
|
||||
"3.11": release(farPastFreeze, farFutureEnd),
|
||||
};
|
||||
const result = determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.11" },
|
||||
releases,
|
||||
);
|
||||
// 2.21 is older than 2.22, so it should be ignored — 3.10 remains the minimum.
|
||||
assert.equal(result.minimumVersion, "3.10");
|
||||
assert.equal(result.maximumVersion, "3.11");
|
||||
});
|
||||
|
||||
await it("throws when no supported releases remain", () => {
|
||||
const releases: EnterpriseReleases = {
|
||||
// All releases are end of life.
|
||||
"3.10": release(farPastFreeze, farPastEnd),
|
||||
"3.11": release(farPastFreeze, farPastEnd),
|
||||
};
|
||||
assert.throws(
|
||||
() =>
|
||||
determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "3.11" },
|
||||
releases,
|
||||
),
|
||||
/Could not determine oldest supported release/,
|
||||
);
|
||||
});
|
||||
|
||||
await it("throws when maximumVersion is not a valid version", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
determineSupportedRange(
|
||||
today,
|
||||
{ minimumVersion: "3.10", maximumVersion: "invalid" },
|
||||
{},
|
||||
),
|
||||
/is not a valid semantic version/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Executable
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Updates src/api-compatibility.json with the current range of supported
|
||||
* GitHub Enterprise Server versions by reading the releases.json file from
|
||||
* an `enterprise-releases` checkout.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { type SemVer } from "semver";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as json from "../src/json";
|
||||
|
||||
import { API_COMPATIBILITY_FILE } from "./config";
|
||||
|
||||
/** The first GHES version that included Code Scanning. */
|
||||
const FIRST_SUPPORTED_RELEASE: SemVer = new semver.SemVer("2.22.0");
|
||||
|
||||
/** Environment variables specific to this script. */
|
||||
export enum EnvVar {
|
||||
ENTERPRISE_RELEASES_PATH = "ENTERPRISE_RELEASES_PATH",
|
||||
}
|
||||
|
||||
/**
|
||||
* The semver specification requires three numeric components, but GHES release families
|
||||
* only have two. This function uses `semver.coerce` to first coerce the version string
|
||||
* into an acceptable input for `semver.parse`. E.g. `3.10` becomes `3.10.0`.
|
||||
*/
|
||||
export function parseEnterpriseVersion(val: string): SemVer | null {
|
||||
return semver.parse(semver.coerce(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirroring `parseEnterpriseVersion`, this function returns only the major and minor
|
||||
* version components from `ver`.
|
||||
*/
|
||||
export function printEnterpriseVersion(ver: SemVer) {
|
||||
if (ver.patch === 0) {
|
||||
return `${ver.major}.${ver.minor}`;
|
||||
}
|
||||
return ver.toString();
|
||||
}
|
||||
|
||||
/** The JSON schema for `API_COMPATIBILITY_FILE`. */
|
||||
const apiCompatibilitySchema = {
|
||||
minimumVersion: json.string,
|
||||
maximumVersion: json.string,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** The type representing the expected contents of `API_COMPATIBILITY_FILE`. */
|
||||
type ApiCompatibility = json.FromSchema<typeof apiCompatibilitySchema>;
|
||||
|
||||
/** Reads the current contents of the `API_COMPATIBILITY_FILE` file. */
|
||||
export function readApiCompatibility(): ApiCompatibility {
|
||||
const apiCompatibilityData: unknown = JSON.parse(
|
||||
fs.readFileSync(API_COMPATIBILITY_FILE, "utf8"),
|
||||
);
|
||||
|
||||
if (!json.isObject(apiCompatibilityData)) {
|
||||
throw new Error(
|
||||
`Expected '${API_COMPATIBILITY_FILE}' to contain an object.`,
|
||||
);
|
||||
}
|
||||
if (!json.validateSchema(apiCompatibilitySchema, apiCompatibilityData)) {
|
||||
throw new Error(
|
||||
`The contents of '${API_COMPATIBILITY_FILE}' do not match the expected JSON schema.`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiCompatibilityData;
|
||||
}
|
||||
|
||||
/** The JSON schema for entries in the `releases.json` file. */
|
||||
const releaseDataSchema = {
|
||||
feature_freeze: json.string,
|
||||
end: json.string,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** The type representing entries in the `releases.json` file. */
|
||||
export type ReleaseData = json.FromSchema<typeof releaseDataSchema>;
|
||||
|
||||
/** A mapping from GHES releases to release information. */
|
||||
export type EnterpriseReleases = Record<string, ReleaseData>;
|
||||
|
||||
/** Reads information about GHES releases. */
|
||||
export function readEnterpriseReleases(
|
||||
enterpriseReleasesPath: string,
|
||||
): EnterpriseReleases {
|
||||
const releaseFilePath = path.join(enterpriseReleasesPath, "releases.json");
|
||||
const releases: unknown = JSON.parse(
|
||||
fs.readFileSync(releaseFilePath, "utf8"),
|
||||
);
|
||||
|
||||
if (!json.isObject(releases)) {
|
||||
throw new Error(`Expected '${releaseFilePath}' to contain an object.`);
|
||||
}
|
||||
|
||||
// Remove GHES version using a previous version numbering scheme.
|
||||
delete releases["11.10"];
|
||||
|
||||
// Validate that the object satisfies the schema.
|
||||
for (const [, releaseData] of Object.entries(releases)) {
|
||||
if (!json.isObject(releaseData)) {
|
||||
throw new Error(
|
||||
`Expected release data to be an object, but it is ${typeof releaseData}.`,
|
||||
);
|
||||
}
|
||||
if (!json.validateSchema(releaseDataSchema, releaseData)) {
|
||||
throw new Error("Expected release data to satisfy schema.");
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
/** Adds `weeks`-many weeks to the UTC date of `date`. */
|
||||
export function addWeeks(date: Date, weeks: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setUTCDate(date.getUTCDate() + weeks * 7);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Determines the current range of GHES versions we should support. */
|
||||
export function determineSupportedRange(
|
||||
today: Date,
|
||||
apiCompatibilityData: ApiCompatibility,
|
||||
releases: EnterpriseReleases,
|
||||
): ApiCompatibility {
|
||||
// We only care about the UTC date component.
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
// Our goal is to identify the oldest and newest GHES release we should support.
|
||||
// We begin with `oldestSupportRelease = undefined` so that we determine the
|
||||
// minimum from scratch and don't stick to `apiCompatibilityData.minimumVersion`
|
||||
// when it is no longer supported.
|
||||
// For `newestSupportedRelease`, we assume that `apiCompatibilityData.maximumVersion`
|
||||
// is guaranteed to not be outdated.
|
||||
let oldestSupportedRelease: SemVer | undefined;
|
||||
let newestSupportedRelease = parseEnterpriseVersion(
|
||||
apiCompatibilityData.maximumVersion,
|
||||
);
|
||||
|
||||
if (newestSupportedRelease === null) {
|
||||
throw new Error(
|
||||
`${apiCompatibilityData.maximumVersion} is not a valid semantic version.`,
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: We deliberately omit including any data from `releases` in the error messages below.
|
||||
|
||||
for (const [releaseVersionString, releaseData] of Object.entries(releases)) {
|
||||
const releaseVersion = parseEnterpriseVersion(releaseVersionString);
|
||||
|
||||
if (releaseVersion === null) {
|
||||
throw new Error("Invalid enterprise release version.");
|
||||
}
|
||||
|
||||
// Ignore GHES releases older than `FIRST_SUPPORTED_RELEASE`.
|
||||
if (semver.compare(releaseVersion, FIRST_SUPPORTED_RELEASE) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set `newestSupportedRelease` to a GHES release if it has a greater version
|
||||
// than the current `newestSupportedRelease` and the feature freeze has
|
||||
// already happened or will be in the next two weeks.
|
||||
if (semver.compare(releaseVersion, newestSupportedRelease) > 0) {
|
||||
const featureFreezeDate = new Date(releaseData.feature_freeze);
|
||||
if (featureFreezeDate < addWeeks(today, 2)) {
|
||||
newestSupportedRelease = releaseVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
oldestSupportedRelease === undefined ||
|
||||
semver.compare(releaseVersion, oldestSupportedRelease) < 0
|
||||
) {
|
||||
const endOfLifeDate = new Date(releaseData.end);
|
||||
// The GHES version is not actually end of life until the end of the day
|
||||
// specified by `endOfLifeDate`. Wait an extra week to be safe.
|
||||
const isEndOfLife = today > addWeeks(endOfLifeDate, 1);
|
||||
if (!isEndOfLife) {
|
||||
oldestSupportedRelease = releaseVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!oldestSupportedRelease) {
|
||||
throw new Error("Could not determine oldest supported release.");
|
||||
}
|
||||
|
||||
return {
|
||||
maximumVersion: printEnterpriseVersion(newestSupportedRelease),
|
||||
minimumVersion: printEnterpriseVersion(oldestSupportedRelease),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const enterpriseReleasesPath = process.env[EnvVar.ENTERPRISE_RELEASES_PATH];
|
||||
if (!enterpriseReleasesPath) {
|
||||
throw new Error(
|
||||
`${EnvVar.ENTERPRISE_RELEASES_PATH} environment variable must be set`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the version compatibility data stored in the repo.
|
||||
const apiCompatibilityData = readApiCompatibility();
|
||||
|
||||
// Get the GHES release information.
|
||||
const releases = readEnterpriseReleases(enterpriseReleasesPath);
|
||||
|
||||
// Determine the supported range.
|
||||
const newCompatibilityData: ApiCompatibility = determineSupportedRange(
|
||||
new Date(),
|
||||
apiCompatibilityData,
|
||||
releases,
|
||||
);
|
||||
|
||||
// If the version range has changed, write the updates to `API_COMPATIBILITY_FILE`.
|
||||
if (
|
||||
newCompatibilityData.minimumVersion !==
|
||||
apiCompatibilityData.minimumVersion ||
|
||||
newCompatibilityData.maximumVersion !== apiCompatibilityData.maximumVersion
|
||||
) {
|
||||
const data = JSON.stringify(newCompatibilityData);
|
||||
fs.writeFileSync(API_COMPATIBILITY_FILE, `${data}\n`);
|
||||
|
||||
console.log(
|
||||
`Updated '${path.basename(API_COMPATIBILITY_FILE)}': ${newCompatibilityData.minimumVersion} - ${newCompatibilityData.maximumVersion}`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`No changes, not writing to '${path.basename(API_COMPATIBILITY_FILE)}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user