diff --git a/.github/workflows/update-supported-enterprise-server-versions.yml b/.github/workflows/update-supported-enterprise-server-versions.yml index 4cead58f4f..1727ea8f5d 100644 --- a/.github/workflows/update-supported-enterprise-server-versions.yml +++ b/.github/workflows/update-supported-enterprise-server-versions.yml @@ -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" diff --git a/.github/workflows/update-supported-enterprise-server-versions/Pipfile b/.github/workflows/update-supported-enterprise-server-versions/Pipfile deleted file mode 100644 index e892e49219..0000000000 --- a/.github/workflows/update-supported-enterprise-server-versions/Pipfile +++ /dev/null @@ -1,9 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] -semver = "*" diff --git a/.github/workflows/update-supported-enterprise-server-versions/Pipfile.lock b/.github/workflows/update-supported-enterprise-server-versions/Pipfile.lock deleted file mode 100644 index 3357cf2864..0000000000 --- a/.github/workflows/update-supported-enterprise-server-versions/Pipfile.lock +++ /dev/null @@ -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": {} -} diff --git a/.github/workflows/update-supported-enterprise-server-versions/update.py b/.github/workflows/update-supported-enterprise-server-versions/update.py deleted file mode 100755 index bdda902cd0..0000000000 --- a/.github/workflows/update-supported-enterprise-server-versions/update.py +++ /dev/null @@ -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() diff --git a/lib/entry-points.js b/lib/entry-points.js index 079b8eee5a..3f0bc15480 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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, diff --git a/lib/upload-lib.js b/lib/upload-lib.js index d355fedf43..66dfb4a73a 100644 --- a/lib/upload-lib.js +++ b/lib/upload-lib.js @@ -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, diff --git a/package-lock.json b/package-lock.json index 6608c05602..0604abf7d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/pr-checks/config.ts b/pr-checks/config.ts index 92c8beef0a..97858c776c 100644 --- a/pr-checks/config.ts +++ b/pr-checks/config.ts @@ -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", +); diff --git a/pr-checks/package.json b/pr-checks/package.json index 2741560f68..f21f86fd70 100644 --- a/pr-checks/package.json +++ b/pr-checks/package.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": { diff --git a/pr-checks/update-ghes-versions.test.ts b/pr-checks/update-ghes-versions.test.ts new file mode 100644 index 0000000000..187c067c37 --- /dev/null +++ b/pr-checks/update-ghes-versions.test.ts @@ -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/, + ); + }); +}); diff --git a/pr-checks/update-ghes-versions.ts b/pr-checks/update-ghes-versions.ts new file mode 100755 index 0000000000..055424dff1 --- /dev/null +++ b/pr-checks/update-ghes-versions.ts @@ -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; + +/** 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; + +/** A mapping from GHES releases to release information. */ +export type EnterpriseReleases = Record; + +/** 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(); +}