Skip to content

Commit cc38d42

Browse files
heiskrCopilotCopilot
authored
Reuse a single rolling Broken External Links Report issue (#62174)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: heiskr <1221423+heiskr@users.noreply.github.com> Copilot-Session: 80d768a6-3069-432e-aae6-4d9cbd1e38be Copilot-Session: d929c559-5a3c-4873-895e-ee8f166a4743
1 parent 4eeb1e7 commit cc38d42

1 file changed

Lines changed: 158 additions & 8 deletions

File tree

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

Lines changed: 158 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,22 @@ jobs:
1818
if: github.repository == 'github/docs-internal'
1919
runs-on: ubuntu-latest
2020
timeout-minutes: 180 # 3 hours for external checks
21+
# Serialize publishing so two overlapping runs can't both create a
22+
# "rolling" issue, or write their results out of order.
23+
concurrency:
24+
group: broken-external-links-report
25+
cancel-in-progress: false
2126
steps:
2227
- name: Checkout
2328
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29+
2430
- uses: ./.github/actions/node-npm-setup
2531

2632
- name: Install dependencies
2733
run: npm ci
2834

2935
- name: Check external links
36+
id: check
3037
env:
3138
ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
3239
CACHE_MAX_AGE_DAYS: '7'
@@ -57,15 +64,158 @@ jobs:
5764
echo "No broken link report generated - all links valid!"
5865
fi
5966
60-
- name: Create issue if broken links found
61-
if: always() && steps.check_report.outputs.has_report == 'true'
62-
uses: peter-evans/create-issue-from-file@65115121ba9a3573cbaded4dc66b90ba1f9b69dc
67+
- name: Create or update the rolling report issue
68+
if: |
69+
always()
70+
&& steps.check.outcome == 'success'
71+
&& steps.check_report.outputs.has_report == 'true'
72+
&& !inputs.max_urls
73+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
6374
with:
64-
token: ${{ secrets.DOCS_BOT_PAT_BASE }}
65-
repository: github/docs-content
66-
title: '🌐 Broken External Links Report'
67-
content-filepath: artifacts/external-link-report.md
68-
labels: broken link report
75+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
76+
script: |
77+
const fs = require('fs')
78+
const title = '🌐 Broken External Links Report'
79+
const owner = 'github'
80+
const repo = 'docs-content'
81+
const label = 'broken link report'
82+
83+
// GitHub rejects issue bodies over 65536 characters with a 422.
84+
// Truncate and point at the run artifact for the full contents.
85+
const MAX_BODY_SIZE = 60000
86+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
87+
let body = fs.readFileSync('artifacts/external-link-report.md', 'utf8')
88+
if (body.length > MAX_BODY_SIZE) {
89+
const notice = `\n\n---\n\n*Report truncated. Download the full report from the [workflow run artifacts](${runUrl}).*`
90+
body = body.slice(0, MAX_BODY_SIZE - notice.length) + notice
91+
core.warning(`Report exceeded ${MAX_BODY_SIZE} characters, so it was truncated.`)
92+
}
93+
94+
// Reuse a single rolling issue instead of opening a new one every
95+
// week, which floods the first responders' board. Find the open
96+
// report issues (newest first).
97+
const open = await github.paginate(github.rest.issues.listForRepo, {
98+
owner,
99+
repo,
100+
state: 'open',
101+
labels: label,
102+
per_page: 100,
103+
})
104+
const reportIssues = open
105+
.filter((issue) => !issue.pull_request && issue.title === title)
106+
.sort((a, b) => b.number - a.number)
107+
108+
if (reportIssues.length === 0) {
109+
const created = await github.rest.issues.create({
110+
owner,
111+
repo,
112+
title,
113+
body,
114+
labels: [label],
115+
})
116+
core.info(`Created rolling report issue: ${created.data.html_url}`)
117+
return
118+
}
119+
120+
// Refresh the newest open report in place and close any older
121+
// duplicates so exactly one canonical issue remains.
122+
const [canonical, ...superseded] = reportIssues
123+
await github.rest.issues.update({
124+
owner,
125+
repo,
126+
issue_number: canonical.number,
127+
title,
128+
body,
129+
})
130+
core.info(`Updated rolling report issue: ${canonical.html_url}`)
131+
132+
// Attempt every duplicate even if one fails, so a single transient
133+
// API error doesn't leave the rest open.
134+
const results = await Promise.allSettled(
135+
superseded.map(async (issue) => {
136+
await github.rest.issues.createComment({
137+
owner,
138+
repo,
139+
issue_number: issue.number,
140+
body: `Superseded by the current rolling report: #${canonical.number}.`,
141+
})
142+
await github.rest.issues.update({
143+
owner,
144+
repo,
145+
issue_number: issue.number,
146+
state: 'closed',
147+
state_reason: 'not_planned',
148+
})
149+
core.info(`Closed superseded report issue #${issue.number}`)
150+
}),
151+
)
152+
const failures = results.filter((result) => result.status === 'rejected')
153+
if (failures.length > 0) {
154+
throw new AggregateError(
155+
failures.map((failure) => failure.reason),
156+
`Failed to close ${failures.length} superseded report issue(s).`,
157+
)
158+
}
159+
160+
- name: Close the rolling report issue when all links are valid
161+
if: |
162+
always()
163+
&& steps.check.outcome == 'success'
164+
&& steps.check_report.outputs.has_report == 'false'
165+
&& !inputs.max_urls
166+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
167+
with:
168+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
169+
script: |
170+
const title = '🌐 Broken External Links Report'
171+
const owner = 'github'
172+
const repo = 'docs-content'
173+
const label = 'broken link report'
174+
175+
// A clean run means the open report is stale. Leaving it open would
176+
// keep fixed failures on the first responders' board.
177+
const open = await github.paginate(github.rest.issues.listForRepo, {
178+
owner,
179+
repo,
180+
state: 'open',
181+
labels: label,
182+
per_page: 100,
183+
})
184+
const reportIssues = open.filter(
185+
(issue) => !issue.pull_request && issue.title === title,
186+
)
187+
188+
if (reportIssues.length === 0) {
189+
core.info('No open report issue to close.')
190+
return
191+
}
192+
193+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
194+
const results = await Promise.allSettled(
195+
reportIssues.map(async (issue) => {
196+
await github.rest.issues.createComment({
197+
owner,
198+
repo,
199+
issue_number: issue.number,
200+
body: `All external links are valid as of the [latest run](${runUrl}). Closing this report. A new one opens if links break again.`,
201+
})
202+
await github.rest.issues.update({
203+
owner,
204+
repo,
205+
issue_number: issue.number,
206+
state: 'closed',
207+
state_reason: 'completed',
208+
})
209+
core.info(`Closed resolved report issue #${issue.number}`)
210+
}),
211+
)
212+
const failures = results.filter((result) => result.status === 'rejected')
213+
if (failures.length > 0) {
214+
throw new AggregateError(
215+
failures.map((failure) => failure.reason),
216+
`Failed to close ${failures.length} resolved report issue(s).`,
217+
)
218+
}
69219
70220
- uses: ./.github/actions/create-workflow-failure-issue
71221
id: create-failure-issue

0 commit comments

Comments
 (0)