Skip to content

Commit 4eeb1e7

Browse files
heiskrCopilot
andauthored
Reuse a single rolling Broken Internal Links Report issue (#62099)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc9a0aaa-5b93-4608-aa00-657344755a7e Copilot-Session: d929c559-5a3c-4873-895e-ee8f166a4743
1 parent 4e3bfac commit 4eeb1e7

1 file changed

Lines changed: 157 additions & 10 deletions

File tree

.github/workflows/link-check-internal.yml

Lines changed: 157 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ on:
2020
required: false
2121
default: false
2222
create_report:
23-
description: 'Create the combined broken links report issue in docs-content'
23+
description: "Publish the combined report to the rolling docs-content issue. A manual run only covers one version/language, so it will overwrite the scheduled run's fuller report."
2424
type: boolean
2525
required: false
26-
default: true
26+
default: false
2727

2828
permissions:
2929
contents: read
@@ -202,6 +202,11 @@ jobs:
202202
if: always() && github.repository == 'github/docs-internal'
203203
needs: [setup-matrix, check-internal-links]
204204
runs-on: ubuntu-latest
205+
# Serialize publishing so two overlapping runs can't both create a
206+
# "rolling" issue, or write their results out of order.
207+
concurrency:
208+
group: broken-internal-links-report
209+
cancel-in-progress: false
205210
permissions:
206211
contents: read
207212
issues: write
@@ -239,15 +244,157 @@ jobs:
239244
echo "No broken link reports generated - all links valid!"
240245
fi
241246
242-
- name: Create issue if broken links found
243-
if: steps.combine.outputs.has_reports == 'true' && (github.event_name != 'workflow_dispatch' || inputs.create_report != false)
244-
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v5
247+
- name: Create or update the rolling report issue
248+
if: |
249+
steps.combine.outputs.has_reports == 'true'
250+
&& needs.check-internal-links.result == 'success'
251+
&& (github.event_name != 'workflow_dispatch' || inputs.create_report)
252+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
245253
with:
246-
token: ${{ secrets.DOCS_BOT_PAT_BASE }}
247-
repository: github/docs-content
248-
title: '🔗 Broken Internal Links Report'
249-
content-filepath: combined-report.md
250-
labels: broken link report
254+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
255+
script: |
256+
const fs = require('fs')
257+
const title = '🔗 Broken Internal Links Report'
258+
const owner = 'github'
259+
const repo = 'docs-content'
260+
const label = 'broken link report'
261+
262+
// GitHub rejects issue bodies over 65536 characters with a 422. The
263+
// internal report routinely exceeds that, so truncate and point at
264+
// the run artifact for the full contents.
265+
const MAX_BODY_SIZE = 60000
266+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
267+
let body = fs.readFileSync('combined-report.md', 'utf8')
268+
if (body.length > MAX_BODY_SIZE) {
269+
const notice = `\n\n---\n\n*Report truncated. Download the full report from the [workflow run artifacts](${runUrl}).*`
270+
body = body.slice(0, MAX_BODY_SIZE - notice.length) + notice
271+
core.warning(`Report exceeded ${MAX_BODY_SIZE} characters, so it was truncated.`)
272+
}
273+
274+
// Reuse a single rolling issue instead of opening a new one every
275+
// week, which floods the first responders' board. Find the open
276+
// report issues (newest first).
277+
const open = await github.paginate(github.rest.issues.listForRepo, {
278+
owner,
279+
repo,
280+
state: 'open',
281+
labels: label,
282+
per_page: 100,
283+
})
284+
const reportIssues = open
285+
.filter((issue) => !issue.pull_request && issue.title === title)
286+
.sort((a, b) => b.number - a.number)
287+
288+
if (reportIssues.length === 0) {
289+
const created = await github.rest.issues.create({
290+
owner,
291+
repo,
292+
title,
293+
body,
294+
labels: [label],
295+
})
296+
core.info(`Created rolling report issue: ${created.data.html_url}`)
297+
return
298+
}
299+
300+
// Refresh the newest open report in place and close any older
301+
// duplicates so exactly one canonical issue remains.
302+
const [canonical, ...superseded] = reportIssues
303+
await github.rest.issues.update({
304+
owner,
305+
repo,
306+
issue_number: canonical.number,
307+
title,
308+
body,
309+
})
310+
core.info(`Updated rolling report issue: ${canonical.html_url}`)
311+
312+
// Attempt every duplicate even if one fails, so a single transient
313+
// API error doesn't leave the rest open.
314+
const results = await Promise.allSettled(
315+
superseded.map(async (issue) => {
316+
await github.rest.issues.createComment({
317+
owner,
318+
repo,
319+
issue_number: issue.number,
320+
body: `Superseded by the current rolling report: #${canonical.number}.`,
321+
})
322+
await github.rest.issues.update({
323+
owner,
324+
repo,
325+
issue_number: issue.number,
326+
state: 'closed',
327+
state_reason: 'not_planned',
328+
})
329+
core.info(`Closed superseded report issue #${issue.number}`)
330+
}),
331+
)
332+
const failures = results.filter((result) => result.status === 'rejected')
333+
if (failures.length > 0) {
334+
throw new AggregateError(
335+
failures.map((failure) => failure.reason),
336+
`Failed to close ${failures.length} superseded report issue(s).`,
337+
)
338+
}
339+
340+
- name: Close the rolling report issue when all links are valid
341+
if: |
342+
steps.combine.outputs.has_reports == 'false'
343+
&& needs.check-internal-links.result == 'success'
344+
&& github.event_name != 'workflow_dispatch'
345+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
346+
with:
347+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
348+
script: |
349+
const title = '🔗 Broken Internal Links Report'
350+
const owner = 'github'
351+
const repo = 'docs-content'
352+
const label = 'broken link report'
353+
354+
// A clean run means the open report is stale. Leaving it open would
355+
// keep fixed failures on the first responders' board.
356+
const open = await github.paginate(github.rest.issues.listForRepo, {
357+
owner,
358+
repo,
359+
state: 'open',
360+
labels: label,
361+
per_page: 100,
362+
})
363+
const reportIssues = open.filter(
364+
(issue) => !issue.pull_request && issue.title === title,
365+
)
366+
367+
if (reportIssues.length === 0) {
368+
core.info('No open report issue to close.')
369+
return
370+
}
371+
372+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
373+
const results = await Promise.allSettled(
374+
reportIssues.map(async (issue) => {
375+
await github.rest.issues.createComment({
376+
owner,
377+
repo,
378+
issue_number: issue.number,
379+
body: `All internal links are valid as of the [latest run](${runUrl}). Closing this report. A new one opens if links break again.`,
380+
})
381+
await github.rest.issues.update({
382+
owner,
383+
repo,
384+
issue_number: issue.number,
385+
state: 'closed',
386+
state_reason: 'completed',
387+
})
388+
core.info(`Closed resolved report issue #${issue.number}`)
389+
}),
390+
)
391+
const failures = results.filter((result) => result.status === 'rejected')
392+
if (failures.length > 0) {
393+
throw new AggregateError(
394+
failures.map((failure) => failure.reason),
395+
`Failed to close ${failures.length} resolved report issue(s).`,
396+
)
397+
}
251398
252399
- uses: ./.github/actions/create-workflow-failure-issue
253400
id: create-failure-issue

0 commit comments

Comments
 (0)