Skip to content

Commit 87b5ae6

Browse files
author
Peter Bengtsson
authored
Port ghes-release-notes.js to TypeScript (#51196)
1 parent 6bbed1d commit 87b5ae6

8 files changed

Lines changed: 106 additions & 38 deletions

File tree

data/release-notes/enterprise-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Note that patch files can be deprecated individually (i.e., hidden on the docs s
3131

3232
### Middleware processing
3333

34-
The YAML data is processed and sorted by `src/release-notes/middleware/context/ghes-release-notes.js` and added to the `context` object.
34+
The YAML data is processed and sorted by `src/release-notes/middleware/context/ghes-release-notes.ts` and added to the `context` object.
3535

3636
### Layouts
3737

src/frame/middleware/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import earlyAccessLinks from '@/early-access/middleware/early-access-links'
3737
import categoriesForSupport from './categories-for-support'
3838
import triggerError from '@/observability/middleware/trigger-error'
3939
import secretScanning from '@/secret-scanning/middleware/secret-scanning'
40-
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes.js'
40+
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes'
4141
import whatsNewChangelog from './context/whats-new-changelog.js'
4242
import layout from './context/layout.js'
4343
import currentProductTree from './context/current-product-tree.js'

src/release-notes/lib/release-notes-utils.js renamed to src/release-notes/lib/release-notes-utils.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import semver from 'semver'
2-
import { supported, latestStable, latest } from '#src/versions/lib/enterprise-server-releases.js'
3-
import { renderContent } from '#src/content-render/index.js'
2+
import { supported, latestStable, latest } from '@/versions/lib/enterprise-server-releases.js'
3+
import { renderContent } from '@/content-render/index.js'
4+
import type { Context, GHESReleasePatch, ReleaseNotes } from '@/types'
45

56
/**
67
* Create an array of release note objects and sort them by number.
78
* Turn { [key]: { notes, intro, date, sections... } }
89
* Into [{ version, patches: [ {notes, intro, date, sections... }] }]
910
*/
10-
export function formatReleases(releaseNotes) {
11+
export function formatReleases(releaseNotes: ReleaseNotes) {
1112
// Get release note numbers in dot notation and sort from highest to lowest.
1213
const sortedReleaseNumbers = Object.keys(releaseNotes)
1314
.map((r) => r.replace(/-/g, '.'))
@@ -21,11 +22,11 @@ export function formatReleases(releaseNotes) {
2122
// Change version-rc1 to version-rc.1 to make these proper semver RC versions.
2223
const patchNumberSemver = patchNumber.replace(/rc/, 'rc.')
2324
return {
25+
...notesPerVersion[patchNumber],
2426
version: `${releaseNumber}.${patchNumberSemver}`,
2527
patchVersion: patchNumberSemver,
2628
downloadVersion: `${releaseNumber}.${patchNumber.replace(/-rc\d*$/, '')}`, // Remove RC
2729
release: releaseNumber,
28-
...notesPerVersion[patchNumber],
2930
}
3031
})
3132
.sort((a, b) => semver.compare(b.version, a.version))
@@ -50,11 +51,15 @@ export function formatReleases(releaseNotes) {
5051
* case of a sub-section.
5152
* Returns [{version, patchVersion, intro, date, sections: { features: [], bugs: []...}}]
5253
*/
53-
export async function renderPatchNotes(patches, ctx) {
54+
export async function renderPatchNotes(
55+
patches: GHESReleasePatch[],
56+
ctx: Context,
57+
): Promise<GHESReleasePatch[]> {
5458
return await Promise.all(
5559
patches.map(async (patch) => {
5660
// Clone the patch object but drop 'sections' so we can render them below without mutations
57-
const { sections, ...renderedPatch } = patch
61+
// const { sections } = patch
62+
const renderedPatch: GHESReleasePatch = { ...patch, sections: {} }
5863
renderedPatch.intro = await renderContent(patch.intro, ctx)
5964

6065
// Now render the sections...
@@ -69,17 +74,19 @@ export async function renderPatchNotes(patches, ctx) {
6974
// where `note` may be a string or an object like { heading, notes: []}
7075
if (typeof note === 'string') {
7176
return renderContent(note, ctx)
77+
} else if (typeof note === 'object' && 'heading' in note && 'notes' in note) {
78+
return {
79+
heading: note.heading,
80+
notes: await Promise.all(
81+
note.notes.map(async (noteStr) => renderContent(noteStr, ctx)),
82+
),
83+
}
7284
} else {
73-
const renderedNoteObj = {}
74-
renderedNoteObj.heading = note.heading
75-
renderedNoteObj.notes = await Promise.all(
76-
note.notes.map(async (noteStr) => renderContent(noteStr, ctx)),
77-
)
78-
79-
return renderedNoteObj
85+
throw new Error('Unrecognized note type')
8086
}
8187
}),
8288
)
89+
8390
return [sectionType, renderedSectionArray]
8491
}),
8592
),

src/release-notes/middleware/get-release-notes.js renamed to src/release-notes/middleware/get-release-notes.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { getDataByLanguage, getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
1+
import { getDataByLanguage, getDeepDataByLanguage } from '@/data-directory/lib/get-data.js'
2+
import type { ReleaseNotes } from '@/types'
23

34
// If we one day support release-notes for other products, add it here.
45
// Checking against this is only really to make sure there's no typos
56
// since we don't have TypeScript to make sure the argument is valid.
67
const VALID_PREFIXES = new Set(['enterprise-server', 'github-ae'])
78

8-
export function getReleaseNotes(prefix, langCode) {
9+
export function getReleaseNotes(prefix: string, langCode: string) {
910
if (!VALID_PREFIXES.has(prefix)) {
1011
throw new Error(
1112
`'${prefix}' is not a valid prefix for this function. Must be one of ${Array.from(
@@ -16,7 +17,7 @@ export function getReleaseNotes(prefix, langCode) {
1617
// Use English as the foundation, then we'll try to load each individual
1718
// data/release-notes/**/*.yml file from the translation.
1819
// If the language is 'en', don't even bother merging.
19-
const releaseNotes = getDeepDataByLanguage(`release-notes.${prefix}`, 'en')
20+
const releaseNotes = getDeepDataByLanguage(`release-notes.${prefix}`, 'en') as ReleaseNotes
2021
if (langCode === 'en') {
2122
// Exit early because nothing special needs to be done.
2223
return releaseNotes
@@ -34,7 +35,7 @@ export function getReleaseNotes(prefix, langCode) {
3435
// use the English ones.
3536
// The output of `getDeepDataByLanguage()` is a mutable object
3637
// from a memoize cache, so don't mutate it to avoid confusing bugs.
37-
const translatedReleaseNotes = {}
38+
const translatedReleaseNotes: ReleaseNotes = {}
3839

3940
// Now, let's iterated over all nested keys and for each one load in the
4041
// translated releases.

src/release-notes/middleware/ghes-release-notes.js renamed to src/release-notes/middleware/ghes-release-notes.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import { formatReleases, renderPatchNotes } from '#src/release-notes/lib/release-notes-utils.js'
2-
import { all } from '#src/versions/lib/enterprise-server-releases.js'
3-
import { executeWithFallback } from '#src/languages/lib/render-with-fallback.js'
4-
import { getReleaseNotes } from './get-release-notes.js'
1+
import type { NextFunction, Response } from 'express'
52

6-
export default async function ghesReleaseNotesContext(req, res, next) {
3+
import { formatReleases, renderPatchNotes } from '@/release-notes/lib/release-notes-utils'
4+
import { all } from '@/versions/lib/enterprise-server-releases.js'
5+
import { executeWithFallback } from '@/languages/lib/render-with-fallback.js'
6+
import { getReleaseNotes } from './get-release-notes'
7+
import type { Context, ExtendedRequest } from '@/types'
8+
9+
export default async function ghesReleaseNotesContext(
10+
req: ExtendedRequest,
11+
res: Response,
12+
next: NextFunction,
13+
) {
14+
if (!req.pagePath || !req.context || !req.context.currentVersion)
15+
throw new Error('request not contextualized')
716
if (!(req.pagePath.endsWith('/release-notes') || req.pagePath.endsWith('/admin'))) return next()
817
const [requestedPlan, requestedRelease] = req.context.currentVersion.split('@')
918
if (requestedPlan !== 'enterprise-server') return next()
@@ -38,9 +47,9 @@ export default async function ghesReleaseNotesContext(req, res, next) {
3847
req.context.ghesReleases = formatReleases(ghesReleaseNotes)
3948

4049
// Find the notes for the current release only
41-
const currentReleaseNotes = req.context.ghesReleases.find(
42-
(r) => r.version === requestedRelease,
43-
).patches
50+
const matchedReleaseNotes = req.context.ghesReleases.find((r) => r.version === requestedRelease)
51+
if (!matchedReleaseNotes) throw new Error('Release notes not found')
52+
const currentReleaseNotes = matchedReleaseNotes.patches
4453

4554
// This means the AUTOTITLE links are in the current language, but
4655
// since we're already force the source of the release notes from English
@@ -55,15 +64,18 @@ export default async function ghesReleaseNotesContext(req, res, next) {
5564
// Returns the current release's patches array: [{version, patchVersion, intro, date, sections}]
5665
req.context.ghesReleaseNotes = await executeWithFallback(
5766
req.context,
58-
() => renderPatchNotes(currentReleaseNotes, req.context),
59-
(enContext) => {
67+
() => renderPatchNotes(currentReleaseNotes, req.context!),
68+
(enContext: Context) => {
6069
// Something in the release notes ultimately caused a Liquid
6170
// rendering error. Let's start over and gather the English release
6271
// notes instead.
6372
enContext.ghesReleases = formatReleases(ghesReleaseNotes)
64-
const currentReleaseNotes = enContext.ghesReleases.find(
73+
74+
const matchedReleaseNotes = enContext.ghesReleases!.find(
6575
(r) => r.version === requestedRelease,
66-
).patches
76+
)
77+
if (!matchedReleaseNotes) throw new Error('Release notes not found')
78+
const currentReleaseNotes = matchedReleaseNotes.patches
6779
return renderPatchNotes(currentReleaseNotes, enContext)
6880
},
6981
)
@@ -74,7 +86,7 @@ export default async function ghesReleaseNotesContext(req, res, next) {
7486

7587
// GHES release notes on docs started with 2.20 but older release notes exist on enterprise.github.com.
7688
// So we want to use _all_ GHES versions when calculating next and previous releases.
77-
req.context.latestPatch = req.context.ghesReleaseNotes[0].version
89+
req.context.latestPatch = req.context.ghesReleaseNotes![0].version
7890
req.context.latestRelease = all[0]
7991

8092
// Add convenience props for "Supported releases" section on GHES Admin landing page (NOT release notes).

src/release-notes/tests/release-notes-1.js renamed to src/release-notes/tests/release-notes-1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
22
import nock from 'nock'
33

4-
import { get, getDOM } from '#src/tests/helpers/e2etest.js'
5-
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
4+
import { get, getDOM } from '@/tests/helpers/e2etest.js'
5+
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
66

77
describe('release notes', () => {
88
vi.setConfig({ testTimeout: 60 * 1000 })
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expect, test, vi } from 'vitest'
22

3-
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
4-
import { get } from '#src/tests/helpers/e2etest.js'
5-
import Page from '#src/frame/lib/page.js'
3+
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
4+
import { get } from '@/tests/helpers/e2etest.js'
5+
import Page from '@/frame/lib/page.js'
66

77
// The English content page's `versions:` frontmatter is the source
88
// of (convenient) truth about which versions of this page is available.
@@ -11,6 +11,7 @@ const page = await Page.init({
1111
relativePath: 'admin/release-notes.md',
1212
languageCode: 'en',
1313
})
14+
if (!page) throw new Error('Page not found')
1415

1516
describe('server', () => {
1617
vi.setConfig({ testTimeout: 60 * 1000 })
@@ -28,7 +29,7 @@ describe('server', () => {
2829
expect(res.statusCode).toBe(200)
2930
})
3031

31-
const { applicableVersions } = page
32+
const applicableVersions = page.applicableVersions
3233

3334
test.each(applicableVersions)('version %s that has release-notes', async (version) => {
3435
const url = `/en/${version}/admin/release-notes`

src/types.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,52 @@ export type Context = {
7575
redirectNotFound?: string
7676
earlyAccessPageLinks?: string
7777
secretScanningData?: SecretScanningData[]
78+
ghesReleases?: GHESRelease[]
79+
ghesReleaseNotes?: GHESReleasePatch[]
80+
autotitleLanguage?: string
81+
latestPatch?: string
82+
latestRelease?: string
83+
}
84+
85+
export type GHESRelease = {
86+
version: string
87+
patches: GHESReleasePatch[]
88+
isReleaseCandidate: boolean
89+
firstPreviousRelease?: string
90+
secondPreviousRelease?: string
91+
}
92+
93+
type ReleasePatchSectionNote = {
94+
heading: string
95+
notes: string[]
96+
}
97+
98+
type ReleasePatchSection = {
99+
security_fixes?: string[] | ReleasePatchSectionNote[]
100+
known_issues?: string[] | ReleasePatchSectionNote[]
101+
features?: string[] | ReleasePatchSectionNote[]
102+
deprecations?: string[] | ReleasePatchSectionNote[]
103+
bugs?: string[] | ReleasePatchSectionNote[]
104+
errata?: string[] | ReleasePatchSectionNote[]
105+
backups?: string[] | ReleasePatchSectionNote[]
106+
}
107+
108+
export type GHESReleasePatch = {
109+
version: string
110+
patchVersion: string
111+
downloadVersion: string
112+
release: string
113+
date: string
114+
release_candidate?: boolean
115+
deprecated?: boolean
116+
intro?: string
117+
sections: ReleasePatchSection
118+
}
119+
120+
export type ReleaseNotes = {
121+
[majorVersion: string]: {
122+
[minorVersion: string]: GHESReleasePatch
123+
}
78124
}
79125

80126
export type SecretScanningData = {
@@ -129,6 +175,7 @@ export type Page = {
129175
renderProp: (prop: string, context: any, opts: any) => Promise<string>
130176
markdown: string
131177
versions: FrontmatterVersions
178+
applicableVersions: string[]
132179
}
133180

134181
export type Tree = {

0 commit comments

Comments
 (0)