|
20 | 20 | required: false |
21 | 21 | default: false |
22 | 22 | 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." |
24 | 24 | type: boolean |
25 | 25 | required: false |
26 | | - default: true |
| 26 | + default: false |
27 | 27 |
|
28 | 28 | permissions: |
29 | 29 | contents: read |
@@ -202,6 +202,11 @@ jobs: |
202 | 202 | if: always() && github.repository == 'github/docs-internal' |
203 | 203 | needs: [setup-matrix, check-internal-links] |
204 | 204 | 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 |
205 | 210 | permissions: |
206 | 211 | contents: read |
207 | 212 | issues: write |
@@ -239,15 +244,157 @@ jobs: |
239 | 244 | echo "No broken link reports generated - all links valid!" |
240 | 245 | fi |
241 | 246 |
|
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 |
245 | 253 | 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 | + } |
251 | 398 |
|
252 | 399 | - uses: ./.github/actions/create-workflow-failure-issue |
253 | 400 | id: create-failure-issue |
|
0 commit comments