Skip to content

Commit 886e294

Browse files
heiskrCopilot
andauthored
Fail GHES archive scrape on error responses (#63580)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e3c6f1-af4b-4bb9-abc4-a5fef82d83ef
1 parent 23f3fc7 commit 886e294

1 file changed

Lines changed: 57 additions & 8 deletions

File tree

‎src/ghes-releases/scripts/deprecate/archive-version.ts‎

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const port = '4001'
1919
const host = `http://localhost:${port}`
2020
const version = EnterpriseServerReleases.oldestSupported
2121
const GH_PAGES_URL = `https://github.github.com/docs-ghes-${version}`
22+
const DRY_RUN_PAGES_PER_LANGUAGE = 3
2223

2324
type PageList = Page[]
2425
type MapObj = { [key: string]: string }
@@ -32,7 +33,10 @@ program
3233
`output directory to place scraped HTML files and redirects. By default, this temp directory is named 'tmpArchivalDir_<VERSION_TO_DEPRECATE>'`,
3334
)
3435
.option('-l, --local-dev', 'Do not rewrite asset paths to enable testing scraped content locally')
35-
.option('-d, --dry-run', 'only scrape the first 10 pages for testing purposes')
36+
.option(
37+
'-d, --dry-run',
38+
`only scrape the first ${DRY_RUN_PAGES_PER_LANGUAGE} pages in each language for testing purposes`,
39+
)
3640
.option(
3741
'-p, --page <PATH>',
3842
'Note: this option is only used to re-scrape a page after the version was deprecated. Redirects will not be re-created because most of the deprecated content is already removed. This option scrapes a specific page in all languages. Pass the relative path to the page without a version or language prefix. ex: /admin/release-notes',
@@ -44,14 +48,12 @@ const dryRun = program.opts().dryRun
4448
const singlePage = program.opts().page
4549
if (singlePage && dryRun) {
4650
console.log(
47-
'A dry run cannot be performed when the --page/-p option is used because a dry run scrapes 10 pages at a time.',
51+
'A dry run cannot be performed when the --page/-p option is used because a dry run scrapes a sample of pages.',
4852
)
4953
process.exit(1)
5054
}
5155
const localDev = program.opts().localDev
52-
const tmpArchivalDirectory = output
53-
? path.join(process.cwd(), output)
54-
: path.join(process.cwd(), `tmpArchivalDir_${version}`)
56+
const tmpArchivalDirectory = path.resolve(output || `tmpArchivalDir_${version}`)
5557
// rimraf refused to remove a filesystem root. fs.rm does not.
5658
if (path.resolve(tmpArchivalDirectory) === path.parse(path.resolve(tmpArchivalDirectory)).root) {
5759
throw new Error(`Refusing to remove filesystem root: ${tmpArchivalDirectory}`)
@@ -75,10 +77,10 @@ async function main() {
7577
const permalinksPerVersion = Object.keys(pageMap)
7678
.filter((key) => key.includes(`/enterprise-server@${version}`))
7779
.map((href) => `${host}${href}`)
78-
urls = dryRun ? permalinksPerVersion.slice(0, 10) : permalinksPerVersion
80+
urls = dryRun ? sampleEachLanguage(permalinksPerVersion) : permalinksPerVersion
7981
if (dryRun) {
8082
console.log(
81-
`\nThis is a dry run! Creating HTML for redirects and scraping the first 10 pages only:\n${urls.join('\n')}\n`,
83+
`\nThis is a dry run! Creating HTML for redirects and scraping the first ${DRY_RUN_PAGES_PER_LANGUAGE} pages in each language only:\n${urls.join('\n')}\n`,
8284
)
8385
} else {
8486
console.log(`Found ${urls.length} pages for version ${version}`)
@@ -87,6 +89,7 @@ async function main() {
8789

8890
await fs.promises.rm(tmpArchivalDirectory, { recursive: true, force: true })
8991

92+
const errorResponses: string[] = []
9093
const app = createApp()
9194
const server = http.createServer(app)
9295
server
@@ -103,7 +106,10 @@ async function main() {
103106
directory: tmpArchivalDirectory,
104107
filenameGenerator: 'bySiteStructure',
105108
requestConcurrency: 6,
106-
plugins: [new RewriteAssetPathsPlugin(tmpArchivalDirectory, localDev, GH_PAGES_URL)],
109+
plugins: [
110+
new RewriteAssetPathsPlugin(tmpArchivalDirectory, localDev, GH_PAGES_URL),
111+
new ErrorResponsesPlugin(errorResponses),
112+
],
107113
})
108114
} catch (err) {
109115
console.error('scraping error')
@@ -112,6 +118,16 @@ async function main() {
112118
return
113119
}
114120

121+
// website-scraper saves error pages like any other response, so fail loudly instead.
122+
if (errorResponses.length) {
123+
console.error(`\n\n${errorResponses.length} responses had an error status:`)
124+
console.error(errorResponses.slice(0, 50).join('\n'))
125+
if (errorResponses.length > 50) console.error(`...and ${errorResponses.length - 50} more`)
126+
console.error('\nRe-run with DEBUG_MIDDLEWARE_TESTS=true to see server error details.')
127+
server.close(() => process.exit(1))
128+
return
129+
}
130+
115131
fs.renameSync(
116132
path.join(tmpArchivalDirectory, `/localhost_${port}`),
117133
path.join(tmpArchivalDirectory, version),
@@ -133,6 +149,39 @@ async function main() {
133149
})
134150
}
135151

152+
function sampleEachLanguage(urls: string[]) {
153+
const counts: Record<string, number> = {}
154+
return urls.filter((url) => {
155+
const language = new URL(url).pathname.split('/')[1]
156+
counts[language] = (counts[language] || 0) + 1
157+
return counts[language] <= DRY_RUN_PAGES_PER_LANGUAGE
158+
})
159+
}
160+
161+
type ScraperResponse = { statusCode: number; url: string }
162+
163+
class ErrorResponsesPlugin {
164+
errorResponses: string[]
165+
166+
constructor(errorResponses: string[]) {
167+
this.errorResponses = errorResponses
168+
}
169+
170+
apply(
171+
registerAction: (
172+
event: string,
173+
callback: (args: { response: ScraperResponse }) => Promise<ScraperResponse>,
174+
) => void,
175+
) {
176+
registerAction('afterResponse', async ({ response }) => {
177+
if (response.statusCode >= 400) {
178+
this.errorResponses.push(`${response.statusCode} ${response.url}`)
179+
}
180+
return response
181+
})
182+
}
183+
}
184+
136185
async function createRedirectsFile(pageList: PageList, outputDirectory: string) {
137186
console.log('Creating redirects file...')
138187
const redirects = await loadRedirects(pageList)

0 commit comments

Comments
 (0)