diff --git a/components/landing/ProductReleases.tsx b/components/landing/ProductReleases.tsx
new file mode 100644
index 0000000000..fb0d688d0b
--- /dev/null
+++ b/components/landing/ProductReleases.tsx
@@ -0,0 +1,68 @@
+import { ArrowRightIcon, ArrowUpIcon, FileIcon, ListUnorderedIcon } from '@primer/octicons-react'
+import { useMainContext } from 'components/context/MainContext'
+import { useProductLandingContext } from 'components/context/ProductLandingContext'
+import { useTranslation } from 'components/hooks/useTranslation'
+import { Link } from 'components/Link'
+import { useRouter } from 'next/router'
+
+export function ProductReleases() {
+ const { t } = useTranslation('product_landing')
+ const router = useRouter()
+ const { enterpriseServerReleases, allVersions } = useMainContext()
+ const { releases } = useProductLandingContext()
+ const currentPath = router.asPath.split('?')[0]
+ return (
+
+
+ {releases.map((release) => {
+ const releaseNumber = release.version
+ if (!enterpriseServerReleases.supported.includes(releaseNumber)) {
+ return null
+ }
+ const releaseVersion = `enterprise-server@${releaseNumber}`
+ const latestPatch = release.patches[0]
+ const firstPreviousVersion = `enterprise-server@${release.firstPreviousRelease}`
+ const secondPreviousVersion = `enterprise-server@${release.secondPreviousRelease}`
+ return (
+
+
+
{allVersions[releaseVersion].versionTitle}
+
+ {' '}
+
+ {t('release_notes_for')} {latestPatch.version}
+ {' '}
+ ({latestPatch.date})
+
+
+ {t('upgrade_from')}{' '}
+
+ {release.firstPreviousRelease}
+ {' '}
+ or{' '}
+
+ {release.secondPreviousRelease}
+
+
+
+ {' '}
+ {t('browse_all_docs')}
+
+
+
+ )
+ })}
+
+
+
+ {t('explore_release_notes')}
+
+
+ )
+}
diff --git a/components/release-notes/GHAEReleaseNotePatch.tsx b/components/release-notes/GHAEReleaseNotePatch.tsx
new file mode 100644
index 0000000000..530c910f4a
--- /dev/null
+++ b/components/release-notes/GHAEReleaseNotePatch.tsx
@@ -0,0 +1,59 @@
+import { useRef, useEffect } from 'react'
+
+import { useTranslation } from 'components/hooks/useTranslation'
+import { useOnScreen } from 'components/hooks/useOnScreen'
+import { PatchNotes } from './PatchNotes'
+import { ReleaseNotePatch } from './types'
+
+type Props = { patch: ReleaseNotePatch; didEnterView: () => void }
+export function GHAEReleaseNotePatch({ patch, didEnterView }: Props) {
+ const { t } = useTranslation('release_notes')
+ const containerRef = useRef
(null)
+ const onScreen = useOnScreen(containerRef, '-40% 0px -50%')
+ useEffect(() => {
+ if (onScreen) {
+ didEnterView()
+ }
+ }, [onScreen])
+
+ const bannerText = patch.currentWeek
+ ? t('banner_text_current')
+ : `${t('banner_text_past')} ${patch.friendlyDate}.`
+
+ return (
+
+
+
+
{patch.title}
+
+ {patch.release_candidate && (
+
+ Release Candidate
+
+ )}
+
+ Print
+
+
+ {patch.friendlyDate} - {bannerText}
+
+
+
+
+
+ )
+}
diff --git a/components/release-notes/GHAEReleaseNotes.tsx b/components/release-notes/GHAEReleaseNotes.tsx
new file mode 100644
index 0000000000..96970987cf
--- /dev/null
+++ b/components/release-notes/GHAEReleaseNotes.tsx
@@ -0,0 +1,89 @@
+import { useState } from 'react'
+import cx from 'classnames'
+import { ChevronDownIcon } from '@primer/octicons-react'
+import { GHAEReleaseNotePatch } from './GHAEReleaseNotePatch'
+import { GHAEReleaseNotesContextT } from './types'
+
+type GitHubAEProps = {
+ context: GHAEReleaseNotesContextT
+}
+export function GHAEReleaseNotes({ context }: GitHubAEProps) {
+ const { releaseNotes, releases, currentVersion } = context
+ const [focusedPatch, setFocusedPatch] = useState('')
+
+ return (
+
+
+
+
+
{currentVersion.planTitle} release notes
+
+
+
+
+ {releaseNotes.map((patch) => {
+ return (
+ setFocusedPatch(patch.version)}
+ />
+ )
+ })}
+
+
+
+
+
+
+ {releases.map((release) => {
+ return (
+
+
+
+ {release.version}
+
+
+ {release.patches.length} releases
+
+
+
+
+
+ {release.patches.map((patch) => {
+ const isActive = patch.version === focusedPatch
+ return (
+
+
+ {patch.friendlyDate}
+
+
+ )
+ })}
+
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/components/release-notes/GHESReleaseNotePatch.tsx b/components/release-notes/GHESReleaseNotePatch.tsx
new file mode 100644
index 0000000000..d7d3c66730
--- /dev/null
+++ b/components/release-notes/GHESReleaseNotePatch.tsx
@@ -0,0 +1,110 @@
+import { useEffect, useRef } from 'react'
+import dayjs from 'dayjs'
+
+import { useTranslation } from 'components/hooks/useTranslation'
+import { PatchNotes } from './PatchNotes'
+import { Link } from 'components/Link'
+import { CurrentVersion, ReleaseNotePatch, GHESMessage } from './types'
+import { useOnScreen } from 'components/hooks/useOnScreen'
+
+type Props = {
+ patch: ReleaseNotePatch
+ currentVersion: CurrentVersion
+ latestPatch: string
+ latestRelease: string
+ message: GHESMessage
+ didEnterView: () => void
+}
+export function GHESReleaseNotePatch({
+ patch,
+ currentVersion,
+ latestPatch,
+ latestRelease,
+ message,
+ didEnterView,
+}: Props) {
+ const { t } = useTranslation('header')
+ const containerRef = useRef(null)
+ const onScreen = useOnScreen(containerRef, '-40% 0px -50%')
+ useEffect(() => {
+ if (onScreen) {
+ didEnterView()
+ }
+ }, [onScreen])
+
+ return (
+
+
+
+
+ {currentVersion.versionTitle}.{patch.patchVersion}
+
+
+ {patch.release_candidate && (
+
+ Release Candidate
+
+ )}
+
+ {currentVersion.plan == 'enterprise-server' && (
+
+ Download
+
+ )}
+
+ Print
+
+
+ {dayjs(patch.date).format('MMMM, DD, YYYY')}
+
+ {patch.version !== latestPatch && currentVersion.currentRelease === latestRelease && (
+
+ {' '}
+ {t('notices.release_notes_use_latest')}
+
+ )}
+
+ {patch.version === latestPatch && currentVersion.currentRelease !== latestRelease && (
+
+ {' '}
+ {t('notices.release_notes_use_latest')}
+
+ )}
+
+ {patch.version !== latestPatch && currentVersion.currentRelease !== latestRelease && (
+
+ {' '}
+ {t('notices.release_notes_use_latest')}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/components/release-notes/GHESReleaseNotes.tsx b/components/release-notes/GHESReleaseNotes.tsx
new file mode 100644
index 0000000000..bf79b99c06
--- /dev/null
+++ b/components/release-notes/GHESReleaseNotes.tsx
@@ -0,0 +1,170 @@
+import { useState } from 'react'
+import cx from 'classnames'
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ LinkExternalIcon,
+} from '@primer/octicons-react'
+import { useMainContext } from 'components/context/MainContext'
+import dayjs from 'dayjs'
+
+import { Link } from 'components/Link'
+import { GHESReleaseNotesContextT } from './types'
+import { GHESReleaseNotePatch } from './GHESReleaseNotePatch'
+
+type Props = {
+ context: GHESReleaseNotesContextT
+}
+export function GHESReleaseNotes({ context }: Props) {
+ const { currentLanguage, currentProduct } = useMainContext()
+ const [focusedPatch, setFocusedPatch] = useState('')
+ const {
+ prevRelease,
+ nextRelease,
+ latestPatch,
+ latestRelease,
+ currentVersion,
+ releaseNotes,
+ releases,
+ message,
+ } = context
+ return (
+
+
+
+ {prevRelease ? (
+
+
{prevRelease}
+
+ ) : (
+
+ )}
+
+
+ {currentVersion.planTitle} {currentVersion.currentRelease} release notes
+
+
+ {nextRelease ? (
+
+ {nextRelease}
+
+ ) : (
+
+ )}
+
+
+
+ {releaseNotes.map((patch) => {
+ return (
+ {
+ setFocusedPatch(patch.version)
+ }}
+ />
+ )
+ })}
+
+
+
+
+
+
+ {releases.map((release) => {
+ const releaseLink = `/${currentLanguage}/${currentVersion.plan}@${release.version}/${currentProduct?.id}/release-notes`
+
+ if (!release.patches || release.patches.length === 0) {
+ return (
+
+
+ {release.version}
+
+
+
+ )
+ }
+
+ if (release.version === currentVersion.currentRelease) {
+ return (
+
+
+
+ {release.version}
+
+
+ {release.patches.length} releases
+
+
+
+
+
+ {release.patches.map((patch) => {
+ const isActive = patch.version === focusedPatch
+ return (
+
+
+ {patch.version}
+
+ {dayjs(patch.date).format('MMMM DD, YYYY')}
+
+
+
+ )
+ })}
+
+
+
+ )
+ }
+
+ return (
+
+
+ {release.version}
+
+ {release.patches.length} releases
+
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/components/release-notes/PatchNotes.tsx b/components/release-notes/PatchNotes.tsx
new file mode 100644
index 0000000000..4aeda5d5bb
--- /dev/null
+++ b/components/release-notes/PatchNotes.tsx
@@ -0,0 +1,84 @@
+import cx from 'classnames'
+import slugger from 'github-slugger'
+import { ReleaseNotePatch } from './types'
+import { Link } from 'components/Link'
+
+const SectionToLabelMap: Record = {
+ features: 'Features',
+ bugs: 'Bug fixes',
+ known_issues: 'Known issues',
+ security_fixes: 'Security fixes',
+ changes: 'Changes',
+ deprecations: 'Deprecations',
+ backups: 'Backups',
+}
+
+type Props = {
+ patch: ReleaseNotePatch
+ withReleaseNoteLabel?: boolean
+}
+export function PatchNotes({ patch, withReleaseNoteLabel }: Props) {
+ return (
+ <>
+ {Object.entries(patch.sections).map(([key, sectionItems], i, arr) => {
+ const isLast = i === arr.length - 1
+ return (
+
+ {withReleaseNoteLabel && (
+
+
+ {SectionToLabelMap[key] || 'INVALID SECTION'}
+
+
+ )}
+
+ {sectionItems.map((item) => {
+ if (typeof item === 'string') {
+ return (
+
+
+
+ )
+ }
+
+ const slug = item.heading ? slugger.slug(item.heading) : ''
+ return (
+
+
+
+ {item.heading}
+
+
+
+
+ {item.notes.map((note) => {
+ return (
+
+ )
+ })}
+
+
+ )
+ })}
+
+
+ )
+ })}
+ >
+ )
+}
diff --git a/components/release-notes/types.ts b/components/release-notes/types.ts
new file mode 100644
index 0000000000..398e4d5b54
--- /dev/null
+++ b/components/release-notes/types.ts
@@ -0,0 +1,49 @@
+export type CurrentVersion = {
+ plan: string
+ planTitle: string
+ versionTitle: string
+ currentRelease: string
+}
+
+export type GHESMessage = {
+ ghes_release_notes_upgrade_patch_only: string
+ ghes_release_notes_upgrade_release_only: string
+ ghes_release_notes_upgrade_patch_and_release: string
+}
+
+type ReleaseNoteSection =
+ | {
+ heading?: string
+ notes: Array
+ }
+ | string
+
+export type ReleaseNotePatch = {
+ patchVersion: string
+ version: string
+ downloadVersion: string
+ intro: string
+ date: string
+ friendlyDate: string
+ title: string
+ release_candidate?: boolean
+ currentWeek: boolean
+ sections: Record>
+}
+
+export type GHAEReleaseNotesContextT = {
+ releaseNotes: Array
+ releases: Array<{ version: string; patches: Array }>
+ currentVersion: CurrentVersion
+}
+
+export type GHESReleaseNotesContextT = {
+ latestPatch: string
+ prevRelease?: string
+ nextRelease?: string
+ latestRelease: string
+ currentVersion: CurrentVersion
+ releaseNotes: Array
+ releases: Array<{ version: string; patches: Array }>
+ message: GHESMessage
+}
diff --git a/data/ui.yml b/data/ui.yml
index ddf09fe0e9..2fc1077a5e 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -20,9 +20,9 @@ header:
early_access: 📣 Please do not share this URL publicly. This page contains content about an early access feature.
release_notes_use_latest: Please use the latest release for the latest security, performance, and bug fixes.
# GHES release notes
- ghes_release_notes_upgrade_patch_only: 📣 This is not the latest patch release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %}
- ghes_release_notes_upgrade_release_only: 📣 This is not the latest release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %}
- ghes_release_notes_upgrade_patch_and_release: 📣 This is not the latest patch release of this release series, and this is not the latest release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %}
+ ghes_release_notes_upgrade_patch_only: 📣 This is not the latest patch release of Enterprise Server.
+ ghes_release_notes_upgrade_release_only: 📣 This is not the latest release of Enterprise Server.
+ ghes_release_notes_upgrade_patch_and_release: 📣 This is not the latest patch release of this release series, and this is not the latest release of Enterprise Server.
release_notes:
banner_text_current: These changes will roll out over the next one week.
banner_text_past: These changes rolled out to enterprises during the week of
diff --git a/includes/enterprise-server-release-notes.html b/includes/enterprise-server-release-notes.html
index 88806054b0..9fadb7c7a6 100644
--- a/includes/enterprise-server-release-notes.html
+++ b/includes/enterprise-server-release-notes.html
@@ -54,15 +54,15 @@
{{ patch.date | date: "%B %d, %Y" }}
{% if patch.version != latestPatch and currentVersionObject.currentRelease == latestRelease %}
- {% data ui.header.notices.ghes_release_notes_upgrade_patch_only %}
+ {% data ui.header.notices.ghes_release_notes_upgrade_patch_only %} {% data ui.header.notices.release_notes_use_latest %}
{% endif %}
{% if patch.version == latestPatch and currentVersionObject.currentRelease != latestRelease %}
- {% data ui.header.notices.ghes_release_notes_upgrade_release_only %}
+ {% data ui.header.notices.ghes_release_notes_upgrade_release_only %} {% data ui.header.notices.release_notes_use_latest %}
{% endif %}
{% if patch.version != latestPatch and currentVersionObject.currentRelease != latestRelease %}
- {% data ui.header.notices.ghes_release_notes_upgrade_patch_and_release %}
+ {% data ui.header.notices.ghes_release_notes_upgrade_patch_and_release %} {% data ui.header.notices.release_notes_use_latest %}
{% endif %}
diff --git a/javascripts/release-notes.js b/javascripts/release-notes.js
index 2ad3e49401..176b0321d1 100644
--- a/javascripts/release-notes.js
+++ b/javascripts/release-notes.js
@@ -1,4 +1,5 @@
export default function releaseNotes () {
+ if (window.next) return
const patches = Array.from(document.querySelectorAll('.js-release-notes-patch'))
if (patches.length === 0) return
diff --git a/package-lock.json b/package-lock.json
index 3eb369e1c1..817269afa0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -93,6 +93,7 @@
"@graphql-inspector/core": "^2.5.0",
"@graphql-tools/load": "^6.2.8",
"@octokit/rest": "^18.5.3",
+ "@types/github-slugger": "^1.3.0",
"@types/lodash": "^4.14.169",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
@@ -4040,6 +4041,12 @@
"integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==",
"devOptional": true
},
+ "node_modules/@types/github-slugger": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz",
+ "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==",
+ "dev": true
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -27663,6 +27670,12 @@
"integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==",
"devOptional": true
},
+ "@types/github-slugger": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz",
+ "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==",
+ "dev": true
+ },
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
diff --git a/package.json b/package.json
index 443b40676c..d75336d655 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,7 @@
"@graphql-inspector/core": "^2.5.0",
"@graphql-tools/load": "^6.2.8",
"@octokit/rest": "^18.5.3",
+ "@types/github-slugger": "^1.3.0",
"@types/lodash": "^4.14.169",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
diff --git a/pages/[versionId]/admin/release-notes.tsx b/pages/[versionId]/admin/release-notes.tsx
new file mode 100644
index 0000000000..5de948f8d0
--- /dev/null
+++ b/pages/[versionId]/admin/release-notes.tsx
@@ -0,0 +1,81 @@
+import { GetServerSideProps } from 'next'
+import { Liquid } from 'liquidjs'
+
+const liquid = new Liquid()
+
+import {
+ MainContextT,
+ MainContext,
+ getMainContextFromRequest,
+} from 'components/context/MainContext'
+import { DefaultLayout } from 'components/DefaultLayout'
+import { GHAEReleaseNotes } from 'components/release-notes/GHAEReleaseNotes'
+import { GHESReleaseNotes } from 'components/release-notes/GHESReleaseNotes'
+import {
+ CurrentVersion,
+ GHAEReleaseNotesContextT,
+ GHESReleaseNotesContextT,
+} from 'components/release-notes/types'
+
+type Props = {
+ mainContext: MainContextT
+ ghaeContext: GHAEReleaseNotesContextT
+ ghesContext: GHESReleaseNotesContextT
+ currentVersion: CurrentVersion
+}
+export default function ReleaseNotes({
+ mainContext,
+ ghesContext,
+ ghaeContext,
+ currentVersion,
+}: Props) {
+ return (
+
+
+ {currentVersion.plan === 'enterprise-server' && }
+
+ {currentVersion.plan === 'github-ae' && }
+
+
+ )
+}
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ const req = context.req as any
+ const currentVersion = req.context.allVersions[req.context.currentVersion]
+ const { latestPatch = '', latestRelease = '' } = req.context
+ return {
+ props: {
+ mainContext: getMainContextFromRequest(req),
+ currentVersion,
+ ghesContext: {
+ currentVersion,
+ latestPatch,
+ latestRelease,
+ prevRelease: req.context.prevRelease || '',
+ nextRelease: req.context.nextRelease || '',
+ releaseNotes: req.context.releaseNotes,
+ releases: req.context.releases,
+ message: {
+ ghes_release_notes_upgrade_patch_only: liquid.parseAndRenderSync(
+ req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_patch_only,
+ { latestPatch, latestRelease }
+ ),
+ ghes_release_notes_upgrade_release_only: liquid.parseAndRenderSync(
+ req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_release_only,
+ { latestPatch, latestRelease }
+ ),
+ ghes_release_notes_upgrade_patch_and_release: liquid.parseAndRenderSync(
+ req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_patch_and_release,
+ { latestPatch, latestRelease }
+ ),
+ },
+ },
+ ghaeContext: {
+ currentVersion,
+ releaseNotes: req.context.releaseNotes,
+ releases: req.context.releases,
+ },
+ },
+ }
+}
diff --git a/stylesheets/release-notes.scss b/stylesheets/release-notes.scss
index ba3397dde5..baa92fe416 100644
--- a/stylesheets/release-notes.scss
+++ b/stylesheets/release-notes.scss
@@ -15,6 +15,10 @@ ul.release-notes-list li.release-notes-list-item {
.release-notes-section-heading {
font-size: 15px !important;
+ scroll-margin-top: 280px !important;
+ @include breakpoint(sm) {
+ scroll-margin-top: 200px !important;
+ }
}
details[open].release-notes-version-picker
@@ -36,7 +40,7 @@ $colors-list: (
security_fixes: var(--color-auto-pink-5),
changes: var(--color-auto-green-5),
deprecations: var(--color-auto-purple-5),
- backups: var(--color-auto-orange-5)
+ backups: var(--color-auto-orange-5),
);
@each $key, $val in $colors-list {
diff --git a/tsconfig.json b/tsconfig.json
index 6f0f8ed243..d994f47b99 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,6 +15,7 @@
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
+ "allowSyntheticDefaultImports": true
},
"exclude": ["node_modules"],
"include": ["*.d.ts", "**/*.ts", "**/*.tsx"]