Skip to content

Commit 02414f1

Browse files
authored
Refactor ghes deprecation scripts (#54517)
1 parent 5b36c54 commit 02414f1

File tree

13 files changed

+541
-182
lines changed

13 files changed

+541
-182
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
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",
3434
"deleted-features-pr-comment": "tsx src/data-directory/scripts/deleted-features-pr-comment.ts",
35+
"deprecate-ghes": "tsx src/ghes-releases/scripts/deprecate/index.ts",
3536
"dev": "cross-env npm start",
3637
"enable-automerge": "tsx src/workflows/enable-automerge.ts",
3738
"find-orphaned-assets": "tsx src/assets/scripts/find-orphaned-assets.ts",

src/content-linter/tests/category-pages.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,16 @@ import getApplicableVersions from '@/versions/lib/get-applicable-versions.js'
1414
import contextualize from '@/frame/middleware/context/context'
1515
import shortVersions from '@/versions/middleware/short-versions.js'
1616
import { ROOT } from '@/frame/lib/constants.js'
17-
import type { Context, ExtendedRequest, FrontmatterVersions } from '@/types'
17+
import type { Context, ExtendedRequest, MarkdownFrontmatter } from '@/types'
1818

1919
const slugger = new GithubSlugger()
2020

2121
const contentDir = path.join(ROOT, 'content')
2222

23-
type Frontmatter = {
24-
title: string
25-
shortTitle?: string
26-
children: string[]
27-
allowTitleToDifferFromFilename?: boolean
28-
versions: FrontmatterVersions
29-
mapTopic?: boolean
30-
hidden?: boolean
31-
}
32-
33-
function getFrontmatterData(markdown: string): Frontmatter {
23+
function getFrontmatterData(markdown: string): MarkdownFrontmatter {
3424
const parsed = matter(markdown)
3525
if (!parsed.data) throw new Error('No frontmatter')
36-
return parsed.data as Frontmatter
26+
return parsed.data as MarkdownFrontmatter
3727
}
3828

3929
describe.skip('category pages', () => {
@@ -108,7 +98,7 @@ describe.skip('category pages', () => {
10898
const indexContents = await fs.promises.readFile(indexAbsPath, 'utf8')
10999
const parsed = matter(indexContents)
110100
if (!parsed.data) throw new Error('No frontmatter')
111-
const data = parsed.data as Frontmatter
101+
const data = parsed.data as MarkdownFrontmatter
112102
categoryVersions = getApplicableVersions(data.versions, indexAbsPath)
113103
allowTitleToDifferFromFilename = data.allowTitleToDifferFromFilename
114104
categoryChildTypes = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { FeatureData } from '#src/types.js'
2+
3+
declare function dataDirectory(dir: string, opts?: Object): FeatureData
4+
export default dataDirectory

src/ghes-releases/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,11 @@ New release issues get opened in the `docs-content` repo and use the templates l
2525

2626
Templates can be added and removed by simply adding a new file to the `release-template` directory using the naming convention that already exists. For every template in that directory, a new issue will be created. The issues are linked together using the tasklist in the parent template `release-steps-0.md`. Each template file has a corresponding liquid variable for the issue url created from the template. The liquid variable format is `{{ release-steps-<TEMPLATE NUMBER>-url }}`. The liquid variables can be used in the templates to autopopulate issues and link issues together.
2727

28-
## GHES deprecations
29-
30-
Every day a workflow runs to check whether it's time to create new deprecation tracking issues. New deprecation tracking issues get opened 7 days before the deprecation date. There is only one template used to generate the deprecation tracking issue (`src/ghes-releases/lib/deprecation-steps.md`).
31-
32-
## Template format
28+
### Template format
3329

3430
Templates in `src/ghes-releases/lib/release-templates` are Markdown, YAML frontmatter, and Liquid. The Liquid variables available to those templates are _not_ the same as liquid variables used by the Docs team for content and data. See the [Template variables](#template-variables) for the available variables.
3531

36-
## Template variables
32+
### Template variables
3733

3834
- `{{ release-number }}` - The GHES release number. For example, `3.13`.
3935
- `{{ release-target-date }}` - The target GHES release date. For example, `2021-09-01`.
@@ -54,3 +50,7 @@ Templates in `src/ghes-releases/lib/release-templates` are Markdown, YAML frontm
5450
- `{{ release-rc-target-date-minus-5 }}` - Five days before the release candidate target date. For example, `2021-08-25`.
5551
- `{{ release-rc-target-date-minus-6 }}` - Six days before the release candidate target date. For example, `2021-08-24`.
5652
- `{{ release-rc-target-date-minus-7 }}` - Seven days before the release candidate target date. For example, `2021-08-23`.
53+
54+
## GHES deprecations
55+
56+
Every day a workflow runs to check whether it's time to create new deprecation tracking issues. New deprecation tracking issues get opened 7 days before the deprecation date. There is only one template used to generate the deprecation tracking issue (`src/ghes-releases/lib/deprecation-steps.md`).

src/ghes-releases/scripts/archive-version.ts renamed to src/ghes-releases/scripts/deprecate/archive-version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import EnterpriseServerReleases from '#src/versions/lib/enterprise-server-releas
2020
import loadRedirects from '#src/redirects/lib/precompile.js'
2121
import { loadPageMap, loadPages } from '#src/frame/lib/page-data.js'
2222
import { languageKeys } from '#src/languages/lib/languages.js'
23-
import { RewriteAssetPathsPlugin } from '@/ghes-releases/scripts/rewrite-asset-paths'
23+
import { RewriteAssetPathsPlugin } from '@/ghes-releases/scripts/deprecate/rewrite-asset-paths'
2424

2525
const port = '4001'
2626
const host = `http://localhost:${port}`
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { program } from 'commander'
2+
import { updateContentFiles } from '@/ghes-releases/scripts/deprecate/update-content'
3+
import { updateDataFiles } from '@/ghes-releases/scripts/deprecate/update-data'
4+
import { updateAutomatedConfigFiles } from '@/ghes-releases/scripts/deprecate/update-automated-pipelines'
5+
6+
program.option('-f, --foo', 'enable some foo')
7+
8+
program
9+
.description('Update deprecated versions frontmatter and remove deprecated content files.')
10+
.command('content')
11+
.action(updateContentFiles)
12+
13+
program
14+
.description(
15+
'Update deprecated versions in data files, remove empty data files, and remove deleted reusables from content files.',
16+
)
17+
.command('data')
18+
.action(updateDataFiles)
19+
20+
program
21+
.description(
22+
'Removes automated pipeline data files and updates the automated pipeline config files.',
23+
)
24+
.command('pipelines')
25+
.action(updateAutomatedConfigFiles)
26+
27+
program.parse(process.argv)
File renamed without changes.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// [start-readme]
2+
//
3+
// This script adds and removes placeholder data files in the
4+
// automation pipelines data directories and
5+
// data/release-notes/enterprise-server directories. This script
6+
// uses the supported and deprecated versions to determine what
7+
// directories should exist. This script also modifies the `api-versions`
8+
// key if it exists in a pipeline's lib/config.json file.
9+
//
10+
// [end-readme]
11+
12+
import { existsSync } from 'fs'
13+
import { readFile, readdir, writeFile, cp } from 'fs/promises'
14+
import { rimrafSync } from 'rimraf'
15+
import { difference, intersection } from 'lodash-es'
16+
import { mkdirp } from 'mkdirp'
17+
18+
import { deprecated, supported } from '#src/versions/lib/enterprise-server-releases.js'
19+
20+
const [currentReleaseNumber, previousReleaseNumber] = supported
21+
const pipelines = JSON.parse(await readFile('src/automated-pipelines/lib/config.json', 'utf-8'))[
22+
'automation-pipelines'
23+
]
24+
25+
// If the config file for a pipeline includes `api-versions` update that list
26+
// based on the supported and deprecated releases.
27+
export async function updateAutomatedConfigFiles() {
28+
for (const pipeline of pipelines) {
29+
const configFilepath = `src/${pipeline}/lib/config.json`
30+
const configData = JSON.parse(await readFile(configFilepath, 'utf-8'))
31+
const apiVersions = configData['api-versions']
32+
if (!apiVersions) continue
33+
for (const key of Object.keys(apiVersions)) {
34+
// Copy the previous release's calendar date versions to the new release
35+
if (key.endsWith(previousReleaseNumber)) {
36+
const newKey = key.replace(previousReleaseNumber, currentReleaseNumber)
37+
apiVersions[newKey] = apiVersions[key]
38+
}
39+
// Remove any deprecated versions
40+
for (const deprecatedRelease of deprecated) {
41+
if (key.endsWith(deprecatedRelease)) {
42+
delete apiVersions[key]
43+
}
44+
}
45+
}
46+
const newConfigData = Object.assign({}, configData)
47+
newConfigData['api-versions'] = apiVersions
48+
await writeFile(configFilepath, JSON.stringify(newConfigData, null, 2))
49+
}
50+
await updateAutomatedPipelines()
51+
}
52+
53+
export async function updateAutomatedPipelines() {
54+
// The allVersions object uses the 'api-versions' data stored in the
55+
// src/rest/lib/config.json file. We want to update 'api-versions'
56+
// before the allVersions object is created so we need to import it
57+
// after calling updateAutomatedConfigFiles.
58+
const { allVersions } = await import('#src/versions/lib/all-versions.js')
59+
60+
// Gets all of the base names (e.g., ghes-) in the allVersions object
61+
// Currently, this is only ghes- but if we had more than one type of
62+
// numbered release it would get all of them.
63+
const numberedReleaseBaseNames = Array.from(
64+
new Set(
65+
Object.values(allVersions)
66+
.filter((version) => version.hasNumberedReleases)
67+
.map((version) => version.openApiBaseName),
68+
),
69+
)
70+
71+
// A list of currently supported versions (calendar date inclusive)
72+
// in the format using the short name rather than full format
73+
// (e.g., enterprise-server@). The list is filtered
74+
// to only include versions that have numbered releases (e.g. ghes-).
75+
// The list is generated from the `apiVersions` key in allVersions.
76+
// This is currently only needed for the rest and github-apps pipelines.
77+
const versionNamesCalDate = Object.values(allVersions)
78+
.filter((version) => version.hasNumberedReleases)
79+
.map((version) =>
80+
version.apiVersions.length
81+
? version.apiVersions.map((apiVersion) => `${version.openApiVersionName}-${apiVersion}`)
82+
: version.openApiVersionName,
83+
)
84+
.flat()
85+
// A list of currently supported versions in the format using the short name
86+
// rather than the full format (e.g., enterprise-server@). The list is filtered
87+
// to only include versions that have numbered releases (e.g. ghes-).
88+
// Currently, this is used for the graphql and webhooks pipelines.
89+
const versionNames = Object.values(allVersions)
90+
.filter((version) => version.hasNumberedReleases)
91+
.map((version) => version.openApiVersionName)
92+
93+
for (const pipeline of pipelines) {
94+
if (!existsSync(`src/${pipeline}/data`)) continue
95+
const isCalendarDateVersioned = JSON.parse(
96+
await readFile(`src/${pipeline}/lib/config.json`, 'utf-8'),
97+
)['api-versions']
98+
99+
const directoryListing = await readdir(`src/${pipeline}/data`)
100+
// filter the directory list to only include directories that start with
101+
// basenames with numbered releases (e.g., ghes-).
102+
const existingDataDir = directoryListing.filter((directory) =>
103+
numberedReleaseBaseNames.some((basename) => directory.startsWith(basename)),
104+
)
105+
const expectedDirectory = isCalendarDateVersioned ? versionNamesCalDate : versionNames
106+
107+
// Get a list of data directories to remove (deprecate) and remove them
108+
// This should only happen if a release is being deprecated.
109+
const removeFiles = difference(existingDataDir, expectedDirectory)
110+
for (const directory of removeFiles) {
111+
console.log(`Removing src/${pipeline}/data/${directory}`)
112+
rimrafSync(`src/${pipeline}/data/${directory}`)
113+
}
114+
115+
// Get a list of data directories to create (release) and create them
116+
// This should only happen if a relase is being added.
117+
const addFiles = difference(expectedDirectory, existingDataDir)
118+
if (addFiles.length > numberedReleaseBaseNames.length) {
119+
throw new Error(
120+
'Only one new release per numbered release version should be added at a time. Check that the lib/enterprise-server-releases.js is correct.',
121+
)
122+
}
123+
124+
for (const base of numberedReleaseBaseNames) {
125+
const dirToAdd = addFiles.find((item) => item.startsWith(base))
126+
if (!dirToAdd) continue
127+
// The suppported array is ordered from most recent (index 0) to oldest
128+
// Index 1 will be the release prior to the most recent release
129+
const lastRelease = supported[1]
130+
const previousDirName = existingDataDir.filter((directory) => directory.includes(lastRelease))
131+
132+
console.log(
133+
`Copying src/${pipeline}/data/${previousDirName} to src/${pipeline}/data/${dirToAdd}`,
134+
)
135+
await cp(`src/${pipeline}/data/${previousDirName}`, `src/${pipeline}/data/${dirToAdd}`, {
136+
recursive: true,
137+
})
138+
}
139+
}
140+
141+
// Add and remove the GHES release note data. Once we create an automation
142+
// pipeline for release notes, we can remove this because it will use the
143+
// same directory structure as the other pipeline data directories.
144+
const ghesReleaseNotesDirs = await readdir('data/release-notes/enterprise-server')
145+
const supportedHyphenated = supported.map((version) => version.replace('.', '-'))
146+
const deprecatedHyphenated = deprecated.map((version) => version.replace('.', '-'))
147+
const addRelNoteDirs = difference(supportedHyphenated, ghesReleaseNotesDirs)
148+
const removeRelNoteDirs = intersection(deprecatedHyphenated, ghesReleaseNotesDirs)
149+
for (const directory of removeRelNoteDirs) {
150+
console.log(`Removing data/release-notes/enterprise-server/${directory}`)
151+
rimrafSync(`data/release-notes/enterprise-server/${directory}`)
152+
}
153+
for (const directory of addRelNoteDirs) {
154+
console.log(`Create new directory data/release-notes/enterprise-server/${directory}`)
155+
await mkdirp(`data/release-notes/enterprise-server/${directory}`)
156+
await cp(
157+
`data/release-notes/PLACEHOLDER-TEMPLATE.yml`,
158+
`data/release-notes/enterprise-server/${directory}/PLACEHOLDER.yml`,
159+
)
160+
}
161+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import yaml from 'js-yaml'
4+
import walkFiles from 'walk-sync'
5+
6+
import frontmatter from '#src/frame/lib/read-frontmatter.js'
7+
import { supported, deprecated } from '#src/versions/lib/enterprise-server-releases.js'
8+
import { isInAllGhes } from '../version-utils'
9+
import { Versions } from '#src/types.js'
10+
11+
type featureDataType = Versions | undefined
12+
13+
const contentFiles = walkFiles('content', {
14+
includeBasePath: true,
15+
directories: false,
16+
globs: ['**/*.md'],
17+
ignore: ['**/README.md', '**/index.md'],
18+
})
19+
20+
// This module updates the versions frontmatter in content files.
21+
// When a content file contains only deprecated GHES releases, the
22+
// file is deleted and removed from the parent index.md file.
23+
export function updateContentFiles() {
24+
for (const file of contentFiles) {
25+
const oldContents = fs.readFileSync(file, 'utf8')
26+
const { content, data } = frontmatter(oldContents)
27+
if (!data) continue
28+
let featureData = undefined
29+
30+
if (data.versions.feature) {
31+
const featureFilePath = 'data/features/' + data.versions.feature + '.yml'
32+
const featureContent = fs.readFileSync(featureFilePath, 'utf8')
33+
featureData = yaml.load(featureContent) as featureDataType
34+
if (!featureData || !featureData.versions)
35+
throw new Error(`Could not load feature versions from ${featureFilePath}`)
36+
}
37+
38+
// skip files with no Enterprise Server versions frontmatter
39+
if (!data.versions.ghes && !featureData?.versions?.ghes) continue
40+
// skip files with all ghes releases defined
41+
if (data.versions.ghes === '*') continue
42+
43+
const deprecatedRelease = deprecated[0]
44+
const oldestRelease = supported[supported.length - 1]
45+
46+
// If the frontmatter versions.ghes property is now
47+
// applicable to all GHES releases, update the value to '*'.
48+
const featureAppliesToAllVersions =
49+
featureData &&
50+
featureData.versions.ghec &&
51+
featureData.versions.fpt &&
52+
featureData.versions.ghes &&
53+
isInAllGhes(featureData.versions.ghes)
54+
55+
if (isInAllGhes(data.versions.ghes)) {
56+
console.log('Updating GHES version in: ', file)
57+
data.versions.ghes = '*'
58+
// To preserve newlines when stringifying,
59+
// you can set the lineWidth option to -1
60+
// This prevents updates to the file that aren't actual changes.
61+
fs.writeFileSync(file, frontmatter.stringify(content, data, { lineWidth: -1 } as any))
62+
continue
63+
}
64+
if (featureAppliesToAllVersions) {
65+
console.log('Updating frontmatter to all versions in: ', file)
66+
data.versions = {
67+
fpt: '*',
68+
ghec: '*',
69+
ghes: '*',
70+
}
71+
// To preserve newlines when stringifying,
72+
// you can set the lineWidth option to -1
73+
// This prevents updates to the file that aren't actual changes.
74+
fs.writeFileSync(file, frontmatter.stringify(content, data, { lineWidth: -1 } as any))
75+
continue
76+
}
77+
78+
const deprecatedRegex = new RegExp(`(<|<=)\\s?${deprecatedRelease}`, 'g')
79+
const oldestRegex = new RegExp(`<\\s?${oldestRelease}`, 'g')
80+
// If the frontmatter versions.ghes property is now
81+
// deprecated, remove it. If the content file is only
82+
// versioned for GHES, remove the file and update index.md.
83+
const featureGhes = featureData?.versions?.ghes || ''
84+
const appliesToNoSupportedGhesReleases =
85+
deprecatedRegex.test(data.versions.ghes) ||
86+
deprecatedRegex.test(featureGhes) ||
87+
oldestRegex.test(data.versions.ghes) ||
88+
oldestRegex.test(featureGhes)
89+
90+
if (appliesToNoSupportedGhesReleases) {
91+
if (Object.keys(data.versions).length === 1) {
92+
console.log('Removing file: ', file)
93+
fs.unlinkSync(file)
94+
const indexFile = file.replace(path.basename(file), 'index.md')
95+
const indexFileContent = fs.readFileSync(indexFile, 'utf8')
96+
const { content, data } = frontmatter(indexFileContent) as {
97+
content: string | undefined
98+
data: { children: string[] } | undefined
99+
}
100+
if (!data) continue
101+
data.children = data.children.filter((child) => child !== '/' + path.basename(file, '.md'))
102+
console.log('..Updating children in: ', indexFile)
103+
fs.writeFileSync(
104+
indexFile,
105+
frontmatter.stringify(content || '', data, { lineWidth: -1 } as any),
106+
)
107+
continue
108+
}
109+
// Remove the ghes property from versions Fm and return
110+
delete data.versions.ghes
111+
console.log('Removing GHES version from: ', file)
112+
fs.writeFileSync(file, frontmatter.stringify(content, data, { lineWidth: -1 } as any))
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)