Skip to content

Commit

Permalink
Merge pull request #33517 from github/repo-sync
Browse files Browse the repository at this point in the history
Repo sync
  • Loading branch information
docs-bot authored Jun 14, 2024
2 parents 068cc7f + 6ca0794 commit 204af2e
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 58 deletions.
2 changes: 1 addition & 1 deletion data/release-notes/enterprise-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Note that patch files can be deprecated individually (i.e., hidden on the docs s

### Middleware processing

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

### Layouts

Expand Down
35 changes: 23 additions & 12 deletions src/changelogs/lib/changelog.js → src/changelogs/lib/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@ import path from 'path'

import Parser from 'rss-parser'

import type { ChangelogItem } from '@/types'

const CHANGELOG_CACHE_FILE_PATH = process.env.CHANGELOG_CACHE_FILE_PATH
// This is useful to set when doing things like sync search.
const CHANGELOG_DISABLED = Boolean(JSON.parse(process.env.CHANGELOG_DISABLED || 'false'))

async function getRssFeed(url) {
async function getRssFeed(url: string) {
const parser = new Parser({ timeout: 5000 })
const feedUrl = `${url}/feed`
let feed

try {
feed = await parser.parseURL(feedUrl)
} catch (err) {
console.error(`cannot get ${feedUrl}: ${err.message}`)
console.error(`cannot get ${feedUrl}: ${err instanceof Error ? err.message : err}`)
return
}

return feed
}

export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {
export async function getChangelogItems(
prefix: string | undefined,
feedUrl: string,
ignoreCache = false,
): Promise<ChangelogItem[] | undefined> {
if (CHANGELOG_DISABLED) {
if (process.env.NODE_ENV === 'development') {
console.warn(`Downloading changelog (${feedUrl}) items is disabled.`)
Expand All @@ -44,14 +50,15 @@ export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {
}

// only show the first 3 posts
const changelog = feed.items.slice(0, 3).map((item) => {
const changelog: ChangelogItem[] = feed.items.slice(0, 3).map((item) => {
const rawTitle = item.title as string
// remove the prefix if it exists (Ex: 'GitHub Actions: '), where the colon and expected whitespace should be hardcoded.
const title = prefix ? item.title.replace(new RegExp(`^${prefix}`), '') : item.title
const title = prefix ? rawTitle.replace(new RegExp(`^${prefix}`), '') : rawTitle
return {
// capitalize the first letter of the title
title: title.trim().charAt(0).toUpperCase() + title.slice(1),
date: item.isoDate,
href: item.link,
date: item.isoDate as string,
href: item.link as string,
}
})

Expand All @@ -65,13 +72,13 @@ export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {

const globalCache = new Map()

function getChangelogCacheKey(prefix, feedUrl) {
function getChangelogCacheKey(prefix: string | undefined, feedUrl: string) {
// Return a string that is only letters so it's safe to use this
// for the filename when caching to disk.
return `${prefix || ''}${feedUrl}`.replace(/[^a-z]+/gi, '')
}

function getDiskCachePath(prefix, feedUrl) {
function getDiskCachePath(prefix: string | undefined, feedUrl: string) {
// When in local development or in tests, use disk caching
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
if (CHANGELOG_CACHE_FILE_PATH) {
Expand All @@ -84,7 +91,7 @@ function getDiskCachePath(prefix, feedUrl) {
}
}

function getChangelogItemsFromCache(prefix, feedUrl) {
function getChangelogItemsFromCache(prefix: string | undefined, feedUrl: string) {
const cacheKey = getChangelogCacheKey(prefix, feedUrl)

if (globalCache.get(cacheKey)) {
Expand All @@ -103,7 +110,7 @@ function getChangelogItemsFromCache(prefix, feedUrl) {
return payload
} catch (err) {
// If it wasn't on disk, that's fine.
if (err.code === 'ENOENT') return
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return
// The JSON.parse() most likely failed. Ignore the error
// but delete the file so it won't be attempted again.
if (err instanceof SyntaxError) {
Expand All @@ -115,7 +122,11 @@ function getChangelogItemsFromCache(prefix, feedUrl) {
}
}

function setChangelogItemsCache(prefix, feedUrl, payload) {
function setChangelogItemsCache(
prefix: string | undefined,
feedUrl: string,
payload: ChangelogItem[],
) {
const cacheKey = getChangelogCacheKey(prefix, feedUrl)
globalCache.set(cacheKey, payload)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import path from 'path'
import nock from 'nock'
import { afterAll, beforeAll, describe, expect, test } from 'vitest'

import { getChangelogItems } from '#src/changelogs/lib/changelog.js'
import { getChangelogItems } from '@/changelogs/lib/changelog'
import type { ChangelogItem } from '@/types'

describe('getChangelogItems module', () => {
let changelog
let changelog: ChangelogItem[] | undefined

beforeAll(async () => {
const rssFeedContent = await fs.readFile(
Expand Down Expand Up @@ -35,7 +36,7 @@ describe('getChangelogItems module', () => {
afterAll(() => nock.cleanAll())

test('changelog contains 3 items', async () => {
expect(changelog.length).toEqual(3)
expect(changelog && changelog.length).toEqual(3)
})

test('each changelog item has expected title, date, and href', async () => {
Expand All @@ -57,6 +58,7 @@ describe('getChangelogItems module', () => {
},
]

if (!changelog) throw new Error('changelog is undefined')
for (let i = 0; i < 3; i++) {
const changeLogEntry = changelog[i]
const expectedEntry = expectedChangelogValues[i]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { getChangelogItems } from '#src/changelogs/lib/changelog.js'
import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js'
import type { Response, NextFunction } from 'express'

export default async function whatsNewChangelog(req, res, next) {
import { getChangelogItems } from '@/changelogs/lib/changelog.js'
import getApplicableVersions from '@/versions/lib/get-applicable-versions.js'
import type { ExtendedRequest } from '@/types'

export default async function whatsNewChangelog(
req: ExtendedRequest,
res: Response,
next: NextFunction,
) {
if (!req.context) throw new Error('request not contextualized')
if (!req.context.page) return next()
if (!req.context.page.changelog) return next()
const label = req.context.page.changelog.label.split(/\s+/g).join('')
Expand All @@ -16,7 +24,7 @@ export default async function whatsNewChangelog(req, res, next) {
}
}

const labelUrls = {
const labelUrls: Record<string, string> = {
education: 'https://github.blog/category/community/education',
enterprise: 'https://github.blog/category/enterprise/',
}
Expand Down
4 changes: 2 additions & 2 deletions src/frame/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ 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 whatsNewChangelog from './context/whats-new-changelog.js'
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes'
import whatsNewChangelog from './context/whats-new-changelog'
import layout from './context/layout.js'
import currentProductTree from './context/current-product-tree.js'
import genericToc from './context/generic-toc.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -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, '.'))
Expand All @@ -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))
Expand All @@ -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...
Expand All @@ -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]
}),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)
},
)
Expand All @@ -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).
Expand Down
Original file line number Diff line number Diff line change
@@ -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 })
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 })
Expand All @@ -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`
Expand Down
Loading

0 comments on commit 204af2e

Please sign in to comment.