Skip to content

Commit ee70cd5

Browse files
authored
Merge pull request #3916 from github/mbg/ts/update-ghes-versions
Convert `update-supported-enterprise-server-versions` script to TypeScript
2 parents 516447a + 839edd2 commit ee70cd5

9 files changed

Lines changed: 473 additions & 94 deletions

File tree

.github/workflows/update-supported-enterprise-server-versions.yml

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
- main
1010
paths:
1111
- .github/workflows/update-supported-enterprise-server-versions.yml
12-
- .github/workflows/update-supported-enterprise-server-versions/update.py
12+
- pr-checks/update-ghes-versions.ts
1313

1414
jobs:
1515
update-supported-enterprise-server-versions:
@@ -26,27 +26,38 @@ jobs:
2626
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
2727
with:
2828
python-version: "3.13"
29+
2930
- name: Checkout CodeQL Action
3031
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
32+
33+
- name: Set up Node.js
34+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
35+
with:
36+
node-version: 24
37+
cache: 'npm'
38+
39+
- name: Install dependencies
40+
run: npm ci
41+
3142
- name: Checkout Enterprise Releases
3243
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3344
with:
3445
repository: github/enterprise-releases
3546
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
3647
path: ${{ github.workspace }}/enterprise-releases/
3748
sparse-checkout: releases.json
49+
3850
- name: Update Supported Enterprise Server Versions
51+
working-directory: pr-checks
3952
run: |
40-
cd ./.github/workflows/update-supported-enterprise-server-versions/
41-
python3 -m pip install pipenv
42-
pipenv install
43-
pipenv run ./update.py
53+
npx tsx update-ghes-versions.ts
4454
rm --recursive "$ENTERPRISE_RELEASES_PATH"
45-
npm ci
46-
npm run build
4755
env:
4856
ENTERPRISE_RELEASES_PATH: ${{ github.workspace }}/enterprise-releases/
4957

58+
- name: Rebuild
59+
run: npm run build
60+
5061
- name: Update git config
5162
run: |
5263
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

.github/workflows/update-supported-enterprise-server-versions/Pipfile

Lines changed: 0 additions & 9 deletions
This file was deleted.

.github/workflows/update-supported-enterprise-server-versions/Pipfile.lock

Lines changed: 0 additions & 27 deletions
This file was deleted.

.github/workflows/update-supported-enterprise-server-versions/update.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pr-checks/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ export const BUILTIN_LANGUAGES_FILE = path.join(
2424
"languages",
2525
"builtin.json",
2626
);
27+
28+
/** Path to the api-compatibility.json file. */
29+
export const API_COMPATIBILITY_FILE = path.join(
30+
SOURCE_ROOT,
31+
"api-compatibility.json",
32+
);

pr-checks/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@octokit/core": "^7.0.6",
88
"@octokit/plugin-paginate-rest": ">=9.2.2",
99
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
10+
"semver": "^7.8.0",
1011
"yaml": "^2.9.0"
1112
},
1213
"devDependencies": {
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env npx tsx
2+
3+
/*
4+
* Tests for the update-ghes-versions.ts script
5+
*/
6+
7+
import * as assert from "node:assert/strict";
8+
import { describe, it } from "node:test";
9+
10+
import {
11+
addWeeks,
12+
determineSupportedRange,
13+
type EnterpriseReleases,
14+
parseEnterpriseVersion,
15+
printEnterpriseVersion,
16+
} from "./update-ghes-versions";
17+
18+
describe("parseEnterpriseVersion", async () => {
19+
await it("parses a two-component version string", () => {
20+
const ver = parseEnterpriseVersion("3.10");
21+
assert.notEqual(ver, null);
22+
assert.equal(ver!.major, 3);
23+
assert.equal(ver!.minor, 10);
24+
assert.equal(ver!.patch, 0);
25+
});
26+
27+
await it("parses a three-component version string", () => {
28+
const ver = parseEnterpriseVersion("3.10.2");
29+
assert.notEqual(ver, null);
30+
assert.equal(ver!.major, 3);
31+
assert.equal(ver!.minor, 10);
32+
assert.equal(ver!.patch, 2);
33+
});
34+
35+
await it("returns null for invalid input", () => {
36+
assert.equal(parseEnterpriseVersion("not-a-version"), null);
37+
});
38+
});
39+
40+
describe("printEnterpriseVersion", async () => {
41+
await it("prints only major.minor when patch is 0", () => {
42+
const ver = parseEnterpriseVersion("3.10")!;
43+
assert.equal(printEnterpriseVersion(ver), "3.10");
44+
});
45+
46+
await it("includes patch when non-zero", () => {
47+
const ver = parseEnterpriseVersion("3.10.2")!;
48+
assert.equal(printEnterpriseVersion(ver), "3.10.2");
49+
});
50+
});
51+
52+
describe("addWeeks", async () => {
53+
await it("adds weeks to a date", () => {
54+
const date = new Date("2025-01-01T00:00:00Z");
55+
const result = addWeeks(date, 2);
56+
assert.equal(result.toISOString(), "2025-01-15T00:00:00.000Z");
57+
});
58+
59+
await it("does not mutate the original date", () => {
60+
const date = new Date("2025-01-01T00:00:00Z");
61+
addWeeks(date, 2);
62+
assert.equal(date.toISOString(), "2025-01-01T00:00:00.000Z");
63+
});
64+
});
65+
66+
/**
67+
* Helper to build a release entry with a feature freeze and end-of-life date.
68+
* Dates are ISO date strings (e.g. "2025-06-01").
69+
*/
70+
function release(featureFreeze: string, end: string) {
71+
return { feature_freeze: featureFreeze, end };
72+
}
73+
74+
describe("determineSupportedRange", async () => {
75+
// A fixed "today" for deterministic tests.
76+
const today = new Date("2025-06-15");
77+
78+
const farPastEnd = "2020-01-01";
79+
const farFutureEnd = "2099-12-31";
80+
const farPastFreeze = "2020-01-01";
81+
const farFutureFreeze = "2099-12-31";
82+
83+
await it("returns the only supported release as both min and max", () => {
84+
const releases: EnterpriseReleases = {
85+
"3.10": release(farPastFreeze, farFutureEnd),
86+
};
87+
const result = determineSupportedRange(
88+
today,
89+
{ minimumVersion: "3.10", maximumVersion: "3.10" },
90+
releases,
91+
);
92+
assert.equal(result.minimumVersion, "3.10");
93+
assert.equal(result.maximumVersion, "3.10");
94+
});
95+
96+
await it("determines the range from multiple supported releases", () => {
97+
const releases: EnterpriseReleases = {
98+
"3.10": release(farPastFreeze, farFutureEnd),
99+
"3.11": release(farPastFreeze, farFutureEnd),
100+
"3.12": release(farPastFreeze, farFutureEnd),
101+
};
102+
const result = determineSupportedRange(
103+
today,
104+
{ minimumVersion: "3.10", maximumVersion: "3.12" },
105+
releases,
106+
);
107+
assert.equal(result.minimumVersion, "3.10");
108+
assert.equal(result.maximumVersion, "3.12");
109+
});
110+
111+
await it("drops an end-of-life release from the minimum", () => {
112+
const releases: EnterpriseReleases = {
113+
// 3.10 has been end of life for a long time.
114+
"3.10": release(farPastFreeze, farPastEnd),
115+
"3.11": release(farPastFreeze, farFutureEnd),
116+
"3.12": release(farPastFreeze, farFutureEnd),
117+
};
118+
const result = determineSupportedRange(
119+
today,
120+
{ minimumVersion: "3.10", maximumVersion: "3.12" },
121+
releases,
122+
);
123+
assert.equal(result.minimumVersion, "3.11");
124+
assert.equal(result.maximumVersion, "3.12");
125+
});
126+
127+
await it("bumps the maximum when a newer release's feature freeze has passed", () => {
128+
const releases: EnterpriseReleases = {
129+
"3.10": release(farPastFreeze, farFutureEnd),
130+
"3.11": release(farPastFreeze, farFutureEnd),
131+
// 3.12 has a feature freeze far in the past, so it should be picked up.
132+
"3.12": release(farPastFreeze, farFutureEnd),
133+
};
134+
const result = determineSupportedRange(
135+
today,
136+
// The stored maximum is 3.11, but 3.12 should be picked up.
137+
{ minimumVersion: "3.10", maximumVersion: "3.11" },
138+
releases,
139+
);
140+
assert.equal(result.minimumVersion, "3.10");
141+
assert.equal(result.maximumVersion, "3.12");
142+
});
143+
144+
await it("does not bump the maximum when feature freeze is far in the future", () => {
145+
const releases: EnterpriseReleases = {
146+
"3.10": release(farPastFreeze, farFutureEnd),
147+
"3.11": release(farPastFreeze, farFutureEnd),
148+
// 3.12 has a feature freeze far in the future, so it should NOT be picked up.
149+
"3.12": release(farFutureFreeze, farFutureEnd),
150+
};
151+
const result = determineSupportedRange(
152+
today,
153+
{ minimumVersion: "3.10", maximumVersion: "3.11" },
154+
releases,
155+
);
156+
assert.equal(result.minimumVersion, "3.10");
157+
assert.equal(result.maximumVersion, "3.11");
158+
});
159+
160+
await it("ignores releases older than the first supported release (2.22)", () => {
161+
const releases: EnterpriseReleases = {
162+
"2.21": release(farPastFreeze, farFutureEnd),
163+
"3.10": release(farPastFreeze, farFutureEnd),
164+
"3.11": release(farPastFreeze, farFutureEnd),
165+
};
166+
const result = determineSupportedRange(
167+
today,
168+
{ minimumVersion: "3.10", maximumVersion: "3.11" },
169+
releases,
170+
);
171+
// 2.21 is older than 2.22, so it should be ignored — 3.10 remains the minimum.
172+
assert.equal(result.minimumVersion, "3.10");
173+
assert.equal(result.maximumVersion, "3.11");
174+
});
175+
176+
await it("throws when no supported releases remain", () => {
177+
const releases: EnterpriseReleases = {
178+
// All releases are end of life.
179+
"3.10": release(farPastFreeze, farPastEnd),
180+
"3.11": release(farPastFreeze, farPastEnd),
181+
};
182+
assert.throws(
183+
() =>
184+
determineSupportedRange(
185+
today,
186+
{ minimumVersion: "3.10", maximumVersion: "3.11" },
187+
releases,
188+
),
189+
/Could not determine oldest supported release/,
190+
);
191+
});
192+
193+
await it("throws when maximumVersion is not a valid version", () => {
194+
assert.throws(
195+
() =>
196+
determineSupportedRange(
197+
today,
198+
{ minimumVersion: "3.10", maximumVersion: "invalid" },
199+
{},
200+
),
201+
/is not a valid semantic version/,
202+
);
203+
});
204+
});

0 commit comments

Comments
 (0)