Skip to content

Commit 52d6c14

Browse files
authored
Convert ghes-releases scripts from JavaScript to TypeScript (#55590)
1 parent e3b78a6 commit 52d6c14

File tree

4 files changed

+147
-64
lines changed

4 files changed

+147
-64
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"content-changes-table-comment": "tsx src/workflows/content-changes-table-comment.ts",
2828
"copy-fixture-data": "tsx src/tests/scripts/copy-fixture-data.js",
2929
"count-translation-corruptions": "tsx src/languages/scripts/count-translation-corruptions.ts",
30-
"create-enterprise-issue": "tsx src/ghes-releases/scripts/create-enterprise-issue.js",
30+
"create-enterprise-issue": "tsx src/ghes-releases/scripts/create-enterprise-issue.ts",
3131
"debug": "cross-env NODE_ENV=development ENABLED_LANGUAGES=en nodemon --inspect src/frame/server.ts",
3232
"delete-orphan-translation-files": "tsx src/workflows/delete-orphan-translation-files.ts",
3333
"deleted-assets-pr-comment": "tsx src/assets/scripts/deleted-assets-pr-comment.ts",
@@ -76,7 +76,7 @@
7676
"purge-fastly-edge-cache-per-language": "tsx src/languages/scripts/purge-fastly-edge-cache-per-language.js",
7777
"purge-old-workflow-runs": "tsx src/workflows/purge-old-workflow-runs.js",
7878
"ready-for-docs-review": "tsx src/workflows/ready-for-docs-review.ts",
79-
"release-banner": "tsx src/ghes-releases/scripts/release-banner.js",
79+
"release-banner": "tsx src/ghes-releases/scripts/release-banner.ts",
8080
"reusables": "tsx src/content-render/scripts/reusables-cli.ts",
8181
"rendered-content-link-checker": "tsx src/links/scripts/rendered-content-link-checker.ts",
8282
"rendered-content-link-checker-cli": "tsx src/links/scripts/rendered-content-link-checker-cli.ts",
@@ -98,7 +98,7 @@
9898
"tsc": "tsc --noEmit",
9999
"unallowed-contributions": "tsx src/workflows/unallowed-contributions.ts",
100100
"update-data-and-image-paths": "tsx src/early-access/scripts/update-data-and-image-paths.js",
101-
"update-enterprise-dates": "tsx src/ghes-releases/scripts/update-enterprise-dates.js",
101+
"update-enterprise-dates": "tsx src/ghes-releases/scripts/update-enterprise-dates.ts",
102102
"update-internal-links": "tsx src/links/scripts/update-internal-links.ts",
103103
"validate-asset-images": "tsx src/assets/scripts/validate-asset-images.ts",
104104
"validate-github-github-docs-urls": "tsx src/links/scripts/validate-github-github-docs-urls/index.ts",

src/ghes-releases/scripts/create-enterprise-issue.js renamed to src/ghes-releases/scripts/create-enterprise-issue.ts

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ import { latest, oldestSupported } from '#src/versions/lib/enterprise-server-rel
88
import { getContents } from '#src/workflows/git-utils.ts'
99
import github from '#src/workflows/github.ts'
1010

11+
interface ReleaseDates {
12+
[releaseNumber: string]: {
13+
start: string
14+
end: string
15+
prp?: string
16+
feature_freeze?: string
17+
code_freeze?: string
18+
release_candidate?: string
19+
}
20+
}
21+
22+
interface ReleaseTemplate {
23+
content: string
24+
issue?: {
25+
number: number
26+
html_url: string
27+
}
28+
}
29+
30+
interface ReleaseTemplates {
31+
[templateName: string]: ReleaseTemplate
32+
}
33+
34+
interface ReleaseTemplateContext {
35+
[key: string]: string
36+
}
37+
38+
interface IssueSearchOpts {
39+
labels?: string[]
40+
searchQuery?: string
41+
titleMatch?: string
42+
}
43+
1144
// Required by github() to authenticate
1245
if (!process.env.GITHUB_TOKEN) {
1346
throw new Error('Error! You must have a GITHUB_TOKEN set in an .env file to run this script.')
@@ -99,7 +132,7 @@ async function createReleaseIssue() {
99132

100133
// Only open an issue if today is within 30 days before
101134
// the release candidate date
102-
if (getNumberDaysUntilMilestone(rcDate) > 30) {
135+
if (getNumberDaysUntilMilestone(rcDate || '') > 30) {
103136
console.log(
104137
`The ${releaseNumber} release candidate is not until ${rcDate}! An issue will be opened 30 days prior to the release candidate date.`,
105138
)
@@ -134,11 +167,18 @@ async function createReleaseIssue() {
134167
releaseTemplateContext,
135168
)
136169
await addRepoLabels(repo, labels)
137-
await updateIssue(repo, template.issue.number, title, body, labels)
170+
await updateIssue(repo, template.issue!.number, title, body, labels)
138171
}
139172
}
140173

141-
async function createIssue(fullRepo, title, body, labels, releaseNumber, releaseType) {
174+
async function createIssue(
175+
fullRepo: string,
176+
title: string,
177+
body: string,
178+
labels: string[],
179+
releaseNumber: string,
180+
releaseType: string,
181+
) {
142182
const [owner, repo] = fullRepo.split('/')
143183
if (!owner || !repo) throw new Error('Please provide a valid repo name in the format owner/repo')
144184
let issue
@@ -150,7 +190,7 @@ async function createIssue(fullRepo, title, body, labels, releaseNumber, release
150190
body,
151191
labels,
152192
})
153-
} catch (error) {
193+
} catch (error: any) {
154194
console.log(`#ERROR# ${error}\n🛑 There was an error creating the issue.`)
155195
throw error
156196
}
@@ -163,7 +203,13 @@ async function createIssue(fullRepo, title, body, labels, releaseNumber, release
163203
return issue
164204
}
165205

166-
async function updateIssue(fullRepo, issueNumber, title, body, labels) {
206+
async function updateIssue(
207+
fullRepo: string,
208+
issueNumber: number,
209+
title: string,
210+
body: string,
211+
labels: string[],
212+
) {
167213
const [owner, repo] = fullRepo.split('/')
168214
if (!owner || !repo) throw new Error('Please provide a valid repo name in the format owner/repo')
169215

@@ -177,7 +223,7 @@ async function updateIssue(fullRepo, issueNumber, title, body, labels) {
177223
body,
178224
labels,
179225
})
180-
} catch (error) {
226+
} catch (error: any) {
181227
console.log(
182228
`#ERROR# ${error}\n🛑 There was an error updating issue ${issueNumber} in ${fullRepo}.`,
183229
)
@@ -188,17 +234,17 @@ async function updateIssue(fullRepo, issueNumber, title, body, labels) {
188234
}
189235
}
190236

191-
async function addRepoLabels(fullRepo, labels) {
237+
async function addRepoLabels(fullRepo: string, labels: string[]) {
192238
const [owner, repo] = fullRepo.split('/')
193-
const labelsToAdd = []
239+
const labelsToAdd: string[] = []
194240
for (const name of labels) {
195241
try {
196242
await octokit.request('GET /repos/{owner}/{repo}/labels/{name}', {
197243
owner,
198244
repo,
199245
name,
200246
})
201-
} catch (error) {
247+
} catch (error: any) {
202248
if (error.status === 404) {
203249
labelsToAdd.push(name)
204250
} else {
@@ -214,65 +260,78 @@ async function addRepoLabels(fullRepo, labels) {
214260
repo,
215261
name,
216262
})
217-
} catch (error) {
263+
} catch (error: any) {
218264
console.log(`#ERROR# ${error}\n🛑 There was an error adding the label ${name}.`)
219265
throw error
220266
}
221267
}
222268
}
223269

224-
function getReleaseTemplates() {
270+
function getReleaseTemplates(): ReleaseTemplates {
225271
const templateFiles = walk('src/ghes-releases/lib/release-templates', {
226272
includeBasePath: true,
227273
directories: false,
228274
globs: ['**/*.md'],
229275
ignore: ['**/README.md'],
230276
})
231-
const releaseTemplates = {}
277+
const releaseTemplates: ReleaseTemplates = {}
232278
for (const file of templateFiles) {
233279
releaseTemplates[basename(file, '.md')] = { content: readFileSync(file, 'utf8') }
234280
}
235281
return releaseTemplates
236282
}
237283

238-
function getReleaseTemplateContext(releaseNumber, releaseInfo, releaseTemplates) {
239-
const context = {
284+
function getReleaseTemplateContext(
285+
releaseNumber: string,
286+
releaseInfo: ReleaseDates[string],
287+
releaseTemplates: ReleaseTemplates,
288+
): ReleaseTemplateContext {
289+
const context: ReleaseTemplateContext = {
240290
'release-number': releaseNumber,
241291
'release-target-date': releaseInfo.start,
242-
'release-prp': releaseInfo.prp,
243-
'release-feature-freeze-date': releaseInfo.feature_freeze,
244-
'release-code-freeze-date': releaseInfo.code_freeze,
245-
'release-rc-target-date': releaseInfo.release_candidate,
292+
'release-prp': releaseInfo.prp || '',
293+
'release-feature-freeze-date': releaseInfo.feature_freeze || '',
294+
'release-code-freeze-date': releaseInfo.code_freeze || '',
295+
'release-rc-target-date': releaseInfo.release_candidate || '',
246296
}
247297
// Add a context variable for each issue url
248298
for (const [templateName, template] of Object.entries(releaseTemplates)) {
249-
context[`${templateName}-url`] = template.issue.html_url
299+
if (template.issue) {
300+
context[`${templateName}-url`] = template.issue.html_url
301+
}
250302
}
251303

252304
// Create a context variable for each of the
253305
// 7 days before release-rc-target-date
254-
const rcTargetDate = new Date(releaseInfo.release_candidate).getTime()
255-
for (let i = 1; i <= 7; i++) {
256-
const day = i
257-
const milliSecondsBefore = day * 24 * 60 * 60 * 1000
258-
const rcDateBefore = new Date(rcTargetDate - milliSecondsBefore).toISOString().slice(0, 10)
259-
context[`release-rc-target-date-minus-${day}`] = rcDateBefore
306+
if (releaseInfo.release_candidate) {
307+
const rcTargetDate = new Date(releaseInfo.release_candidate).getTime()
308+
for (let i = 1; i <= 7; i++) {
309+
const day = i
310+
const milliSecondsBefore = day * 24 * 60 * 60 * 1000
311+
const rcDateBefore = new Date(rcTargetDate - milliSecondsBefore).toISOString().slice(0, 10)
312+
context[`release-rc-target-date-minus-${day}`] = rcDateBefore
313+
}
260314
}
261315
return context
262316
}
263317

264-
async function getRenderedTemplate(templateContent, releaseTemplateContext) {
318+
async function getRenderedTemplate(
319+
templateContent: string,
320+
releaseTemplateContext: ReleaseTemplateContext,
321+
) {
265322
const { content, data } = matter(templateContent)
266323
const title = await liquid.parseAndRender(data.title, releaseTemplateContext)
267324
const body = await liquid.parseAndRender(content, releaseTemplateContext)
268325
const labels = await Promise.all(
269-
data.labels.map(async (label) => await liquid.parseAndRender(label, releaseTemplateContext)),
326+
data.labels.map(
327+
async (label: string) => await liquid.parseAndRender(label, releaseTemplateContext),
328+
),
270329
)
271330

272331
return { title, body, labels }
273332
}
274333

275-
function getNumberDaysUntilMilestone(milestoneDate) {
334+
function getNumberDaysUntilMilestone(milestoneDate: string): number {
276335
const today = new Date().toISOString().slice(0, 10)
277336
const nextMilestoneDateTime = new Date(milestoneDate).getTime()
278337
const todayTime = new Date(today).getTime()
@@ -281,7 +340,7 @@ function getNumberDaysUntilMilestone(milestoneDate) {
281340
return Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24))
282341
}
283342

284-
function getNextReleaseNumber(releaseDates) {
343+
function getNextReleaseNumber(releaseDates: ReleaseDates): string {
285344
const indexOfLatest = Object.keys(releaseDates).indexOf(latest)
286345
const indexOfNext = indexOfLatest + 1
287346
return Object.keys(releaseDates)[indexOfNext]
@@ -292,9 +351,9 @@ function getNextReleaseNumber(releaseDates) {
292351
// labels: ['enterprise deprecation', 'ghes 3.0']
293352
// titleMatch: 'GHES 3.0'
294353
async function isExistingIssue(
295-
repo,
296-
opts = { labels: undefined, searchQuery: undefined, titleMatch: undefined },
297-
) {
354+
repo: string,
355+
opts: IssueSearchOpts = { labels: undefined, searchQuery: undefined, titleMatch: undefined },
356+
): Promise<boolean> {
298357
const { labels, searchQuery, titleMatch } = opts
299358
const labelQuery = labels && labels.map((label) => `label:"${encodeURI(label)}"`).join('+')
300359
let query = encodeURIComponent('is:issue ' + `repo:${repo} `)
@@ -314,21 +373,21 @@ async function isExistingIssue(
314373
console.log(`Issue ${issue.html_url} already exists for this release.`)
315374
return true
316375
}
317-
return
376+
return false
318377
}
319378
}
320379

321380
const issueExists = !!issues.data.items.length
322381
if (issueExists) {
323382
console.log(
324-
`Issue ${issues.data.items.map((item) => item.html_url)} already exists for this release.`,
383+
`Issue ${issues.data.items.map((item: { html_url: string }) => item.html_url)} already exists for this release.`,
325384
)
326385
}
327386
return issueExists
328387
}
329388

330-
async function getReleaseDates() {
331-
let rawDates = []
389+
async function getReleaseDates(): Promise<ReleaseDates> {
390+
let rawDates: ReleaseDates = {}
332391
try {
333392
rawDates = JSON.parse(
334393
await getContents('github', 'enterprise-releases', 'master', 'releases.json'),

src/ghes-releases/scripts/release-banner.js renamed to src/ghes-releases/scripts/release-banner.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { program } from 'commander'
1010
import { allVersions } from '#src/versions/lib/all-versions.js'
1111

1212
const releaseCandidateJSFile = 'src/versions/lib/enterprise-server-releases.js'
13-
const allowedActions = ['create', 'remove']
13+
const allowedActions = ['create', 'remove'] as const
14+
15+
type AllowedAction = (typeof allowedActions)[number]
1416

1517
program
1618
.description('Create or remove a release candidate banner for the provided docs version.')
@@ -23,7 +25,7 @@ program
2325

2426
const options = program.opts()
2527

26-
if (!allowedActions.includes(options.action)) {
28+
if (!allowedActions.includes(options.action as AllowedAction)) {
2729
console.log(`Error! You must specify --action <${allowedActions.join(' or ')}>.`)
2830
process.exit(1)
2931
}
@@ -36,30 +38,37 @@ if (!Object.keys(allVersions).includes(options.version)) {
3638
}
3739

3840
// Load the release candidate variable
39-
let jsCode = await fs.readFile(releaseCandidateJSFile, 'utf8')
40-
const lineRegex = /export const releaseCandidate = .*/
41-
if (!lineRegex.test(jsCode)) {
42-
throw new Error(
43-
`The file ${releaseCandidateJSFile} does not contain a release candidate variable. ('export const releaseCandidate = ...')`,
44-
)
45-
}
41+
async function main(): Promise<void> {
42+
let jsCode = await fs.readFile(releaseCandidateJSFile, 'utf8')
43+
const lineRegex = /export const releaseCandidate = .*/
44+
if (!lineRegex.test(jsCode)) {
45+
throw new Error(
46+
`The file ${releaseCandidateJSFile} does not contain a release candidate variable. ('export const releaseCandidate = ...')`,
47+
)
48+
}
4649

47-
// Create or remove the variable
48-
if (options.action === 'create') {
49-
jsCode = jsCode.replace(
50-
lineRegex,
51-
`export const releaseCandidate = '${options.version.split('@')[1]}'`,
52-
)
53-
} else if (options.action === 'remove') {
54-
jsCode = jsCode.replace(lineRegex, `export const releaseCandidate = null`)
55-
}
50+
// Create or remove the variable
51+
if (options.action === 'create') {
52+
jsCode = jsCode.replace(
53+
lineRegex,
54+
`export const releaseCandidate = '${options.version.split('@')[1]}'`,
55+
)
56+
} else if (options.action === 'remove') {
57+
jsCode = jsCode.replace(lineRegex, `export const releaseCandidate = null`)
58+
}
5659

57-
// Update the file
58-
await fs.writeFile(releaseCandidateJSFile, jsCode)
60+
// Update the file
61+
await fs.writeFile(releaseCandidateJSFile, jsCode)
5962

60-
// Display next steps
61-
console.log(`\nDone! Commit the update to ${releaseCandidateJSFile}. This ${options.action}s the banner for ${options.version}.
63+
// Display next steps
64+
console.log(`\nDone! Commit the update to ${releaseCandidateJSFile}. This ${options.action}s the banner for ${options.version}.
6265
6366
- To change the banner text, you can edit header.notices.release_candidate in data/ui.yml.
6467
- To change the banner styling, you can edit includes/header-notification.html.
6568
`)
69+
}
70+
71+
main().catch((error) => {
72+
console.error('Error:', error)
73+
process.exit(1)
74+
})

0 commit comments

Comments
 (0)