Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 158 additions & 8 deletions .github/workflows/link-check-external.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ jobs:
if: github.repository == 'github/docs-internal'
runs-on: ubuntu-latest
timeout-minutes: 180 # 3 hours for external checks
# Serialize publishing so two overlapping runs can't both create a
# "rolling" issue, or write their results out of order.
concurrency:
group: broken-external-links-report
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: ./.github/actions/node-npm-setup

- name: Install dependencies
run: npm ci

- name: Check external links
id: check
env:
ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
CACHE_MAX_AGE_DAYS: '7'
Expand Down Expand Up @@ -57,15 +64,158 @@ jobs:
echo "No broken link report generated - all links valid!"
fi

- name: Create issue if broken links found
if: always() && steps.check_report.outputs.has_report == 'true'
uses: peter-evans/create-issue-from-file@65115121ba9a3573cbaded4dc66b90ba1f9b69dc
- name: Create or update the rolling report issue
if: |
always()
&& steps.check.outcome == 'success'
&& steps.check_report.outputs.has_report == 'true'
&& !inputs.max_urls
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
token: ${{ secrets.DOCS_BOT_PAT_BASE }}
repository: github/docs-content
title: '🌐 Broken External Links Report'
content-filepath: artifacts/external-link-report.md
labels: broken link report
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
const fs = require('fs')
const title = '🌐 Broken External Links Report'
const owner = 'github'
const repo = 'docs-content'
const label = 'broken link report'

// GitHub rejects issue bodies over 65536 characters with a 422.
// Truncate and point at the run artifact for the full contents.
const MAX_BODY_SIZE = 60000
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
let body = fs.readFileSync('artifacts/external-link-report.md', 'utf8')
if (body.length > MAX_BODY_SIZE) {
const notice = `\n\n---\n\n*Report truncated. Download the full report from the [workflow run artifacts](${runUrl}).*`
body = body.slice(0, MAX_BODY_SIZE - notice.length) + notice
core.warning(`Report exceeded ${MAX_BODY_SIZE} characters, so it was truncated.`)
}

// Reuse a single rolling issue instead of opening a new one every
// week, which floods the first responders' board. Find the open
// report issues (newest first).
const open = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: label,
per_page: 100,
})
const reportIssues = open
.filter((issue) => !issue.pull_request && issue.title === title)
.sort((a, b) => b.number - a.number)

if (reportIssues.length === 0) {
const created = await github.rest.issues.create({
owner,
repo,
title,
body,
labels: [label],
})
core.info(`Created rolling report issue: ${created.data.html_url}`)
return
}

// Refresh the newest open report in place and close any older
// duplicates so exactly one canonical issue remains.
const [canonical, ...superseded] = reportIssues
await github.rest.issues.update({
owner,
repo,
issue_number: canonical.number,
title,
body,
})
core.info(`Updated rolling report issue: ${canonical.html_url}`)

// Attempt every duplicate even if one fails, so a single transient
// API error doesn't leave the rest open.
const results = await Promise.allSettled(
superseded.map(async (issue) => {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: `Superseded by the current rolling report: #${canonical.number}.`,
})
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned',
})
core.info(`Closed superseded report issue #${issue.number}`)
}),
)
const failures = results.filter((result) => result.status === 'rejected')
if (failures.length > 0) {
throw new AggregateError(
failures.map((failure) => failure.reason),
`Failed to close ${failures.length} superseded report issue(s).`,
)
}

- name: Close the rolling report issue when all links are valid
if: |
always()
&& steps.check.outcome == 'success'
&& steps.check_report.outputs.has_report == 'false'
&& !inputs.max_urls
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
const title = '🌐 Broken External Links Report'
const owner = 'github'
const repo = 'docs-content'
const label = 'broken link report'

// A clean run means the open report is stale. Leaving it open would
// keep fixed failures on the first responders' board.
const open = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: label,
per_page: 100,
})
const reportIssues = open.filter(
(issue) => !issue.pull_request && issue.title === title,
)

if (reportIssues.length === 0) {
core.info('No open report issue to close.')
return
}

const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
const results = await Promise.allSettled(
reportIssues.map(async (issue) => {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: `All external links are valid as of the [latest run](${runUrl}). Closing this report. A new one opens if links break again.`,
})
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'completed',
})
core.info(`Closed resolved report issue #${issue.number}`)
}),
)
const failures = results.filter((result) => result.status === 'rejected')
if (failures.length > 0) {
throw new AggregateError(
failures.map((failure) => failure.reason),
`Failed to close ${failures.length} resolved report issue(s).`,
)
}

- uses: ./.github/actions/create-workflow-failure-issue
id: create-failure-issue
Expand Down
167 changes: 157 additions & 10 deletions .github/workflows/link-check-internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ on:
required: false
default: false
create_report:
description: 'Create the combined broken links report issue in docs-content'
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."
type: boolean
required: false
default: true
default: false

permissions:
contents: read
Expand Down Expand Up @@ -202,6 +202,11 @@ jobs:
if: always() && github.repository == 'github/docs-internal'
needs: [setup-matrix, check-internal-links]
runs-on: ubuntu-latest
# Serialize publishing so two overlapping runs can't both create a
# "rolling" issue, or write their results out of order.
concurrency:
group: broken-internal-links-report
cancel-in-progress: false
permissions:
contents: read
issues: write
Expand Down Expand Up @@ -239,15 +244,157 @@ jobs:
echo "No broken link reports generated - all links valid!"
fi

- name: Create issue if broken links found
if: steps.combine.outputs.has_reports == 'true' && (github.event_name != 'workflow_dispatch' || inputs.create_report != false)
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v5
- name: Create or update the rolling report issue
if: |
steps.combine.outputs.has_reports == 'true'
&& needs.check-internal-links.result == 'success'
&& (github.event_name != 'workflow_dispatch' || inputs.create_report)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
token: ${{ secrets.DOCS_BOT_PAT_BASE }}
repository: github/docs-content
title: '🔗 Broken Internal Links Report'
content-filepath: combined-report.md
labels: broken link report
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
const fs = require('fs')
const title = '🔗 Broken Internal Links Report'
const owner = 'github'
const repo = 'docs-content'
const label = 'broken link report'

// GitHub rejects issue bodies over 65536 characters with a 422. The
// internal report routinely exceeds that, so truncate and point at
// the run artifact for the full contents.
const MAX_BODY_SIZE = 60000
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
let body = fs.readFileSync('combined-report.md', 'utf8')
if (body.length > MAX_BODY_SIZE) {
const notice = `\n\n---\n\n*Report truncated. Download the full report from the [workflow run artifacts](${runUrl}).*`
body = body.slice(0, MAX_BODY_SIZE - notice.length) + notice
core.warning(`Report exceeded ${MAX_BODY_SIZE} characters, so it was truncated.`)
}

// Reuse a single rolling issue instead of opening a new one every
// week, which floods the first responders' board. Find the open
// report issues (newest first).
const open = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: label,
per_page: 100,
})
const reportIssues = open
.filter((issue) => !issue.pull_request && issue.title === title)
.sort((a, b) => b.number - a.number)

if (reportIssues.length === 0) {
const created = await github.rest.issues.create({
owner,
repo,
title,
body,
labels: [label],
})
core.info(`Created rolling report issue: ${created.data.html_url}`)
return
}

// Refresh the newest open report in place and close any older
// duplicates so exactly one canonical issue remains.
const [canonical, ...superseded] = reportIssues
await github.rest.issues.update({
owner,
repo,
issue_number: canonical.number,
title,
body,
})
core.info(`Updated rolling report issue: ${canonical.html_url}`)

// Attempt every duplicate even if one fails, so a single transient
// API error doesn't leave the rest open.
const results = await Promise.allSettled(
superseded.map(async (issue) => {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: `Superseded by the current rolling report: #${canonical.number}.`,
})
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned',
})
core.info(`Closed superseded report issue #${issue.number}`)
}),
)
const failures = results.filter((result) => result.status === 'rejected')
if (failures.length > 0) {
throw new AggregateError(
failures.map((failure) => failure.reason),
`Failed to close ${failures.length} superseded report issue(s).`,
)
}

- name: Close the rolling report issue when all links are valid
if: |
steps.combine.outputs.has_reports == 'false'
&& needs.check-internal-links.result == 'success'
&& github.event_name != 'workflow_dispatch'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
const title = '🔗 Broken Internal Links Report'
const owner = 'github'
const repo = 'docs-content'
const label = 'broken link report'

// A clean run means the open report is stale. Leaving it open would
// keep fixed failures on the first responders' board.
const open = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: label,
per_page: 100,
})
const reportIssues = open.filter(
(issue) => !issue.pull_request && issue.title === title,
)

if (reportIssues.length === 0) {
core.info('No open report issue to close.')
return
}

const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
const results = await Promise.allSettled(
reportIssues.map(async (issue) => {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: `All internal links are valid as of the [latest run](${runUrl}). Closing this report. A new one opens if links break again.`,
})
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'completed',
})
core.info(`Closed resolved report issue #${issue.number}`)
}),
)
const failures = results.filter((result) => result.status === 'rejected')
if (failures.length > 0) {
throw new AggregateError(
failures.map((failure) => failure.reason),
`Failed to close ${failures.length} resolved report issue(s).`,
)
}

- uses: ./.github/actions/create-workflow-failure-issue
id: create-failure-issue
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# ---------------------------------------------------------------
# To update the sha:
# https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble
FROM ghcr.io/github/gh-base-image/gh-base-noble:20260722-151519-g000ce495e@sha256:f722b1fb6d02a18f85d45ab2e064a0736e64992f20371a063c4076bac57832c9 AS base
FROM ghcr.io/github/gh-base-image/gh-base-noble:20260727-152635-gd2e4a1fa6@sha256:22e3a406a3f0f9bf6d3544ce4189bb947e467330bbe7210af5ecc727232a569c AS base

# Install curl for Node install and determining the early access branch
# Install git for cloning docs-early-access & translations repos
Expand Down
Loading
Loading