1
0
mirror of synced 2025-12-23 21:07:12 -05:00

Port ghes-release-notes.js to TypeScript (#51196)

This commit is contained in:
Peter Bengtsson
2024-06-14 13:20:53 -04:00
committed by GitHub
parent 6bbed1d48e
commit 87b5ae62ab
8 changed files with 106 additions and 38 deletions

View File

@@ -37,7 +37,7 @@ import earlyAccessLinks from '@/early-access/middleware/early-access-links'
import categoriesForSupport from './categories-for-support'
import triggerError from '@/observability/middleware/trigger-error'
import secretScanning from '@/secret-scanning/middleware/secret-scanning'
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes.js'
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes'
import whatsNewChangelog from './context/whats-new-changelog.js'
import layout from './context/layout.js'
import currentProductTree from './context/current-product-tree.js'

View File

@@ -1,13 +1,14 @@
import semver from 'semver'
import { supported, latestStable, latest } from '#src/versions/lib/enterprise-server-releases.js'
import { renderContent } from '#src/content-render/index.js'
import { supported, latestStable, latest } from '@/versions/lib/enterprise-server-releases.js'
import { renderContent } from '@/content-render/index.js'
import type { Context, GHESReleasePatch, ReleaseNotes } from '@/types'
/**
* Create an array of release note objects and sort them by number.
* Turn { [key]: { notes, intro, date, sections... } }
* Into [{ version, patches: [ {notes, intro, date, sections... }] }]
*/
export function formatReleases(releaseNotes) {
export function formatReleases(releaseNotes: ReleaseNotes) {
// Get release note numbers in dot notation and sort from highest to lowest.
const sortedReleaseNumbers = Object.keys(releaseNotes)
.map((r) => r.replace(/-/g, '.'))
@@ -21,11 +22,11 @@ export function formatReleases(releaseNotes) {
// Change version-rc1 to version-rc.1 to make these proper semver RC versions.
const patchNumberSemver = patchNumber.replace(/rc/, 'rc.')
return {
...notesPerVersion[patchNumber],
version: `${releaseNumber}.${patchNumberSemver}`,
patchVersion: patchNumberSemver,
downloadVersion: `${releaseNumber}.${patchNumber.replace(/-rc\d*$/, '')}`, // Remove RC
release: releaseNumber,
...notesPerVersion[patchNumber],
}
})
.sort((a, b) => semver.compare(b.version, a.version))
@@ -50,11 +51,15 @@ export function formatReleases(releaseNotes) {
* case of a sub-section.
* Returns [{version, patchVersion, intro, date, sections: { features: [], bugs: []...}}]
*/
export async function renderPatchNotes(patches, ctx) {
export async function renderPatchNotes(
patches: GHESReleasePatch[],
ctx: Context,
): Promise<GHESReleasePatch[]> {
return await Promise.all(
patches.map(async (patch) => {
// Clone the patch object but drop 'sections' so we can render them below without mutations
const { sections, ...renderedPatch } = patch
// const { sections } = patch
const renderedPatch: GHESReleasePatch = { ...patch, sections: {} }
renderedPatch.intro = await renderContent(patch.intro, ctx)
// Now render the sections...
@@ -69,17 +74,19 @@ export async function renderPatchNotes(patches, ctx) {
// where `note` may be a string or an object like { heading, notes: []}
if (typeof note === 'string') {
return renderContent(note, ctx)
} else if (typeof note === 'object' && 'heading' in note && 'notes' in note) {
return {
heading: note.heading,
notes: await Promise.all(
note.notes.map(async (noteStr) => renderContent(noteStr, ctx)),
),
}
} else {
const renderedNoteObj = {}
renderedNoteObj.heading = note.heading
renderedNoteObj.notes = await Promise.all(
note.notes.map(async (noteStr) => renderContent(noteStr, ctx)),
)
return renderedNoteObj
throw new Error('Unrecognized note type')
}
}),
)
return [sectionType, renderedSectionArray]
}),
),

View File

@@ -1,11 +1,12 @@
import { getDataByLanguage, getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
import { getDataByLanguage, getDeepDataByLanguage } from '@/data-directory/lib/get-data.js'
import type { ReleaseNotes } from '@/types'
// If we one day support release-notes for other products, add it here.
// Checking against this is only really to make sure there's no typos
// since we don't have TypeScript to make sure the argument is valid.
const VALID_PREFIXES = new Set(['enterprise-server', 'github-ae'])
export function getReleaseNotes(prefix, langCode) {
export function getReleaseNotes(prefix: string, langCode: string) {
if (!VALID_PREFIXES.has(prefix)) {
throw new Error(
`'${prefix}' is not a valid prefix for this function. Must be one of ${Array.from(
@@ -16,7 +17,7 @@ export function getReleaseNotes(prefix, langCode) {
// Use English as the foundation, then we'll try to load each individual
// data/release-notes/**/*.yml file from the translation.
// If the language is 'en', don't even bother merging.
const releaseNotes = getDeepDataByLanguage(`release-notes.${prefix}`, 'en')
const releaseNotes = getDeepDataByLanguage(`release-notes.${prefix}`, 'en') as ReleaseNotes
if (langCode === 'en') {
// Exit early because nothing special needs to be done.
return releaseNotes
@@ -34,7 +35,7 @@ export function getReleaseNotes(prefix, langCode) {
// use the English ones.
// The output of `getDeepDataByLanguage()` is a mutable object
// from a memoize cache, so don't mutate it to avoid confusing bugs.
const translatedReleaseNotes = {}
const translatedReleaseNotes: ReleaseNotes = {}
// Now, let's iterated over all nested keys and for each one load in the
// translated releases.

View File

@@ -1,9 +1,18 @@
import { formatReleases, renderPatchNotes } from '#src/release-notes/lib/release-notes-utils.js'
import { all } from '#src/versions/lib/enterprise-server-releases.js'
import { executeWithFallback } from '#src/languages/lib/render-with-fallback.js'
import { getReleaseNotes } from './get-release-notes.js'
import type { NextFunction, Response } from 'express'
export default async function ghesReleaseNotesContext(req, res, next) {
import { formatReleases, renderPatchNotes } from '@/release-notes/lib/release-notes-utils'
import { all } from '@/versions/lib/enterprise-server-releases.js'
import { executeWithFallback } from '@/languages/lib/render-with-fallback.js'
import { getReleaseNotes } from './get-release-notes'
import type { Context, ExtendedRequest } from '@/types'
export default async function ghesReleaseNotesContext(
req: ExtendedRequest,
res: Response,
next: NextFunction,
) {
if (!req.pagePath || !req.context || !req.context.currentVersion)
throw new Error('request not contextualized')
if (!(req.pagePath.endsWith('/release-notes') || req.pagePath.endsWith('/admin'))) return next()
const [requestedPlan, requestedRelease] = req.context.currentVersion.split('@')
if (requestedPlan !== 'enterprise-server') return next()
@@ -38,9 +47,9 @@ export default async function ghesReleaseNotesContext(req, res, next) {
req.context.ghesReleases = formatReleases(ghesReleaseNotes)
// Find the notes for the current release only
const currentReleaseNotes = req.context.ghesReleases.find(
(r) => r.version === requestedRelease,
).patches
const matchedReleaseNotes = req.context.ghesReleases.find((r) => r.version === requestedRelease)
if (!matchedReleaseNotes) throw new Error('Release notes not found')
const currentReleaseNotes = matchedReleaseNotes.patches
// This means the AUTOTITLE links are in the current language, but
// 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) {
// Returns the current release's patches array: [{version, patchVersion, intro, date, sections}]
req.context.ghesReleaseNotes = await executeWithFallback(
req.context,
() => renderPatchNotes(currentReleaseNotes, req.context),
(enContext) => {
() => renderPatchNotes(currentReleaseNotes, req.context!),
(enContext: Context) => {
// Something in the release notes ultimately caused a Liquid
// rendering error. Let's start over and gather the English release
// notes instead.
enContext.ghesReleases = formatReleases(ghesReleaseNotes)
const currentReleaseNotes = enContext.ghesReleases.find(
const matchedReleaseNotes = enContext.ghesReleases!.find(
(r) => r.version === requestedRelease,
).patches
)
if (!matchedReleaseNotes) throw new Error('Release notes not found')
const currentReleaseNotes = matchedReleaseNotes.patches
return renderPatchNotes(currentReleaseNotes, enContext)
},
)
@@ -74,7 +86,7 @@ export default async function ghesReleaseNotesContext(req, res, next) {
// GHES release notes on docs started with 2.20 but older release notes exist on enterprise.github.com.
// So we want to use _all_ GHES versions when calculating next and previous releases.
req.context.latestPatch = req.context.ghesReleaseNotes[0].version
req.context.latestPatch = req.context.ghesReleaseNotes![0].version
req.context.latestRelease = all[0]
// Add convenience props for "Supported releases" section on GHES Admin landing page (NOT release notes).

View File

@@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
import nock from 'nock'
import { get, getDOM } from '#src/tests/helpers/e2etest.js'
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
import { get, getDOM } from '@/tests/helpers/e2etest.js'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
describe('release notes', () => {
vi.setConfig({ testTimeout: 60 * 1000 })

View File

@@ -1,8 +1,8 @@
import { describe, expect, test, vi } from 'vitest'
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
import { get } from '#src/tests/helpers/e2etest.js'
import Page from '#src/frame/lib/page.js'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
import { get } from '@/tests/helpers/e2etest.js'
import Page from '@/frame/lib/page.js'
// The English content page's `versions:` frontmatter is the source
// of (convenient) truth about which versions of this page is available.
@@ -11,6 +11,7 @@ const page = await Page.init({
relativePath: 'admin/release-notes.md',
languageCode: 'en',
})
if (!page) throw new Error('Page not found')
describe('server', () => {
vi.setConfig({ testTimeout: 60 * 1000 })
@@ -28,7 +29,7 @@ describe('server', () => {
expect(res.statusCode).toBe(200)
})
const { applicableVersions } = page
const applicableVersions = page.applicableVersions
test.each(applicableVersions)('version %s that has release-notes', async (version) => {
const url = `/en/${version}/admin/release-notes`

View File

@@ -75,6 +75,52 @@ export type Context = {
redirectNotFound?: string
earlyAccessPageLinks?: string
secretScanningData?: SecretScanningData[]
ghesReleases?: GHESRelease[]
ghesReleaseNotes?: GHESReleasePatch[]
autotitleLanguage?: string
latestPatch?: string
latestRelease?: string
}
export type GHESRelease = {
version: string
patches: GHESReleasePatch[]
isReleaseCandidate: boolean
firstPreviousRelease?: string
secondPreviousRelease?: string
}
type ReleasePatchSectionNote = {
heading: string
notes: string[]
}
type ReleasePatchSection = {
security_fixes?: string[] | ReleasePatchSectionNote[]
known_issues?: string[] | ReleasePatchSectionNote[]
features?: string[] | ReleasePatchSectionNote[]
deprecations?: string[] | ReleasePatchSectionNote[]
bugs?: string[] | ReleasePatchSectionNote[]
errata?: string[] | ReleasePatchSectionNote[]
backups?: string[] | ReleasePatchSectionNote[]
}
export type GHESReleasePatch = {
version: string
patchVersion: string
downloadVersion: string
release: string
date: string
release_candidate?: boolean
deprecated?: boolean
intro?: string
sections: ReleasePatchSection
}
export type ReleaseNotes = {
[majorVersion: string]: {
[minorVersion: string]: GHESReleasePatch
}
}
export type SecretScanningData = {
@@ -129,6 +175,7 @@ export type Page = {
renderProp: (prop: string, context: any, opts: any) => Promise<string>
markdown: string
versions: FrontmatterVersions
applicableVersions: string[]
}
export type Tree = {