Skip to content

Commit 60c2b78

Browse files
authored
Merge branch 'main' into claude/stacked-prs-auto-merge-docs-rm35z8
2 parents 7a546a9 + 9860344 commit 60c2b78

1,553 files changed

Lines changed: 410705 additions & 53581 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.devcontainer/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Update ARG and FROM together from the Node devcontainer image tags:
22
# https://mcr.microsoft.com/en-us/product/devcontainers/javascript-node/tags
33
ARG VARIANT=dev-24-bookworm
4-
FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:d0445b9546f96869b509aa70b8593bdc0054aca6689bc1dd0324083d0fd2bcc3
4+
FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:42a1a7629b5a2c22c759a1de4fbb277c9e600b6fecd66b89583fc36e9453e698

‎.github/CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ For complete style guidance, see our [style guide](https://docs.github.com/en/co
7676

7777
#### Repository-specific options:
7878

79-
**Make changes in the UI:** Click **Make a contribution** at the bottom of any docs page for small changes like typos or broken links.
79+
**Make changes in the UI:** For small changes like typos or broken links, click **Make a contribution** at the bottom of any docs page. The button opens the article's source file in this repository. Click the pencil icon to edit the file. GitHub asks you to fork the repository, then lets you propose your change as a pull request. See "[Editing files in another user's repository](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files#editing-files-in-another-users-repository)."
8080

8181
<img src="/contributing/images/contribution_cta.png" />
8282

‎.github/workflows/check-for-spammy-issues.yml‎

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
name: Check for Spammy Issues
22

3-
# Closes low-value public issues so spam does not stay open in github/docs.
3+
# Closes low-value public issues and issues with emoji titles in github/docs.
4+
# GitHub employees, docs-bot, and the skip-spammy-check label are exempt.
5+
# Add skip-spammy-check before reopening an issue whose title still contains emoji.
46

57
on:
68
issues:
7-
types: [opened]
9+
types: [opened, edited, reopened]
810

911
permissions:
1012
contents: read
@@ -13,58 +15,78 @@ jobs:
1315
spammy-title-check:
1416
name: Remove issues with spammy titles
1517
if: github.repository == 'github/docs'
18+
concurrency:
19+
group: ${{ github.workflow }}-spammy-title-${{ github.event.issue.number }}
20+
cancel-in-progress: false
1621
runs-on: ubuntu-latest
1722
steps:
1823
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
1924
with:
2025
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
2126
script: |
27+
const params = { ...context.repo, issue_number: context.payload.issue.number }
2228
23-
const issue = context.payload.issue
24-
const owner = 'github'
25-
const repo = 'docs'
29+
// Events can predate title edits or an exemption label, so read the current issue.
30+
const { data: issue } = await github.rest.issues.get(params)
31+
if (issue.state !== 'open' || issue.user.login === 'docs-bot') {
32+
return
33+
}
34+
if (issue.labels.some(label => label.name.toLowerCase() === 'skip-spammy-check')) {
35+
core.info('Skipping because the issue has the skip-spammy-check label.')
36+
return
37+
}
2638
2739
const titleWordCount = issue.title.trim().split(' ').length
2840
const titleWordCountMin = 3
2941
const urlRegex = /https?:\/\/\S+/i
3042
const titleHasUrl = urlRegex.test(issue.title)
3143
const titleHasDollarSign = issue.title.includes('$')
44+
// Text-default symbols need an emoji selector; plain digits, #, and * need a keycap.
45+
const titleHasEmoji = /\p{Emoji_Presentation}(?!\uFE0E)|\p{Extended_Pictographic}\uFE0F|[#*0-9]\uFE0F?\u20E3/u.test(issue.title)
46+
// Keep the existing non-emoji heuristics limited to newly opened issues.
47+
const hasOtherSpam = context.payload.action === 'opened' &&
48+
(titleWordCount < titleWordCountMin || titleHasUrl || titleHasDollarSign)
49+
if (!titleHasEmoji && !hasOtherSpam) {
50+
return
51+
}
3252
3353
try {
3454
await github.rest.teams.getMembershipForUserInOrg({
3555
org: 'github',
3656
team_slug: 'employees',
37-
username: context.payload.sender.login,
38-
});
57+
username: issue.user.login,
58+
})
3959
4060
// Skip GitHub employees so legitimate internal reports stay open.
4161
return
4262
} catch (err) {
43-
// The membership lookup throws for non-employees, so fall through to title checks.
44-
if (titleWordCount >= titleWordCountMin && !titleHasUrl && !titleHasDollarSign) {
45-
return
63+
// Only a missing membership means non-employee; other failures must stop moderation.
64+
if (err.status !== 404) {
65+
throw err
4666
}
4767
}
4868
49-
await github.rest.issues.update({
50-
owner: owner,
51-
repo: repo,
52-
issue_number: issue.number,
69+
await github.rest.issues.addLabels({
70+
...params,
5371
labels: ['invalid'],
54-
state: 'closed'
55-
});
72+
})
73+
74+
await github.rest.issues.update({
75+
...params,
76+
state: 'closed',
77+
})
5678
5779
await github.rest.issues.createComment({
58-
owner: owner,
59-
repo: repo,
60-
issue_number: issue.number,
61-
body: `This issue may have been opened accidentally. I'm going to close it now, but feel free to open a new issue with a more descriptive title! Make sure not to include full URLs in your issue titles, and use paths instead.`
62-
});
80+
...params,
81+
body: titleHasEmoji
82+
? 'Closing because the title contains emoji. Please remove the emoji and open a new issue.'
83+
: `This issue may have been opened accidentally. I'm going to close it now, but feel free to open a new issue with a more descriptive title! Make sure not to include full URLs in your issue titles, and use paths instead.`,
84+
})
6385
6486
try {
6587
await github.rest.projects.createCard({
6688
column_id: 11167427,
67-
content_id: context.payload.issue.id,
89+
content_id: issue.id,
6890
content_type: "Issue"
6991
});
7092
} catch (error) {

‎.github/workflows/check-for-spammy-prs.yml‎

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Check for Spammy PRs
22

3-
# Flags low-value public PRs and closes PRs that target branches other than main.
3+
# Flags low-value public PRs and closes PRs with non-main targets or emoji titles.
4+
# Maintainers add the skip-spammy-check label to exempt a PR.
45

56
on:
67
pull_request_target:
@@ -9,14 +10,63 @@ on:
910
permissions:
1011
contents: read
1112
issues: read
12-
pull-requests: write
1313

1414
jobs:
15+
emoji-title-check:
16+
name: Close PRs with emoji titles
17+
if: github.repository == 'github/docs'
18+
concurrency:
19+
group: ${{ github.workflow }}-emoji-title-${{ github.event.pull_request.number }}
20+
cancel-in-progress: false
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
24+
with:
25+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
26+
script: |
27+
const item = context.payload.pull_request
28+
const params = { ...context.repo, issue_number: item.number }
29+
30+
// Events can predate title edits or an exemption label, so read the current PR.
31+
const { data: current } = await github.rest.issues.get(params)
32+
if (current.state !== 'open' || current.user.login === 'docs-bot') {
33+
return
34+
}
35+
36+
// Text-default symbols need an emoji selector; plain digits, #, and * need a keycap.
37+
const hasEmoji = /\p{Emoji_Presentation}(?!\uFE0E)|\p{Extended_Pictographic}\uFE0F|[#*0-9]\uFE0F?\u20E3/u.test(current.title)
38+
if (!hasEmoji) {
39+
return
40+
}
41+
42+
if (current.labels.some(label => label.name.toLowerCase() === 'skip-spammy-check')) {
43+
core.info('Skipping because the pull request has the skip-spammy-check label.')
44+
return
45+
}
46+
47+
// Mark automated closures so the skip label can trigger the existing recovery workflow.
48+
await github.rest.issues.addLabels({
49+
...params,
50+
labels: ['invalid'],
51+
})
52+
53+
await github.rest.pulls.update({
54+
...context.repo,
55+
pull_number: item.number,
56+
state: 'closed',
57+
})
58+
59+
await github.rest.issues.createComment({
60+
...params,
61+
body: 'Closing because the title contains emoji. Please remove the emoji and open a new PR.',
62+
})
63+
1564
spammy-pr-check:
1665
name: Label spammy PRs and reject non-main targets
1766
if: >
1867
github.repository == 'github/docs' && github.event_name == 'pull_request_target' &&
19-
github.event.pull_request.user.login != 'docs-bot'
68+
github.event.pull_request.user.login != 'docs-bot' &&
69+
!contains(github.event.pull_request.labels.*.name, 'skip-spammy-check')
2070
runs-on: ubuntu-latest
2171
steps:
2272
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
@@ -48,6 +98,21 @@ jobs:
4898
return changedLines.length > 0 &&
4999
changedLines.every(line => line.slice(1).trim() === '')
50100
})
101+
const onlyBreaksOpeningFrontmatter = files.length > 0 && files.every(file => {
102+
if (
103+
file.status !== 'modified' ||
104+
!file.filename.startsWith('content/') ||
105+
!file.filename.endsWith('.md') ||
106+
file.additions !== 1 ||
107+
file.deletions !== 1
108+
) return false
109+
110+
const lines = (file.patch || '').split('\n')
111+
return /^@@ -1(?:,\d+)? \+1(?:,\d+)? @@/.test(lines[0]) &&
112+
lines[1] === '----' && // Diff prefix "-" followed by the original "---".
113+
lines[2]?.startsWith('+') &&
114+
!/^---(?:\s*yaml)?\s*$/i.test(lines[2].slice(1))
115+
})
51116
const onlyRenames = files.length > 0 && files.every(
52117
f => f.status === 'renamed' && f.additions === 0 && f.deletions === 0
53118
)
@@ -99,9 +164,22 @@ jobs:
99164
isEmptyCommit ||
100165
touchesTooMany ||
101166
isBlankLineEdit ||
167+
onlyBreaksOpeningFrontmatter ||
102168
onlyRenames ||
103169
onlyIndexMd
104170
) {
171+
// The payload can predate a label added right after the PR opened, so read the labels again.
172+
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
173+
owner,
174+
repo,
175+
issue_number: pull_number,
176+
per_page: 100,
177+
})
178+
if (currentLabels.some(label => label.name.toLowerCase() === 'skip-spammy-check')) {
179+
core.info('Skipping because the pull request has the skip-spammy-check label.')
180+
return
181+
}
182+
105183
await github.rest.issues.addLabels({
106184
owner,
107185
repo,

‎.github/workflows/close-on-invalid-label.yaml‎

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
name: Close issue/PR on adding invalid label
22

33
# Closes public issues and PRs when the invalid label is added.
4+
# Re-closes invalid PRs that someone without write access reopens.
5+
# Reopens PRs closed as invalid when maintainers add the skip-spammy-check label.
46

57
on:
68
issues:
79
types: [labeled]
8-
# pull_request_target lets the workflow close forked PRs after the invalid label is added.
10+
# pull_request_target lets the workflow close or reopen forked PRs when they're labeled or reopened.
911
pull_request_target:
10-
types: [labeled]
12+
types: [labeled, reopened]
1113

1214
permissions:
1315
contents: read
14-
issues: write
15-
pull-requests: write
1616

1717
jobs:
1818
close-on-adding-invalid-label:
1919
if: github.repository == 'github/docs' && github.event.label.name == 'invalid'
2020
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
issues: write
24+
pull-requests: write
2125

2226
steps:
2327
- name: Close issue
@@ -42,3 +46,99 @@ jobs:
4246
if: ${{ failure() && github.event_name != 'pull_request_target' }}
4347
with:
4448
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
49+
50+
# Authors can reopen PRs they closed themselves, even after the invalid label is added.
51+
reclose-reopened-invalid-pr:
52+
if: >
53+
github.repository == 'github/docs' && github.event_name == 'pull_request_target' &&
54+
github.event.action == 'reopened' &&
55+
contains(github.event.pull_request.labels.*.name, 'invalid')
56+
runs-on: ubuntu-latest
57+
permissions:
58+
pull-requests: write
59+
steps:
60+
- name: Re-close PR unless reopened by someone with write access
61+
env:
62+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
PR_URL: ${{ github.event.pull_request.html_url }}
64+
SENDER: ${{ github.event.sender.login }}
65+
run: |
66+
if ! permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$SENDER/permission" --jq '.permission'); then
67+
echo "::warning::Couldn't look up permission for $SENDER, so treating them as read-only."
68+
permission=none
69+
fi
70+
if [[ "$permission" == "admin" || "$permission" == "write" ]]; then
71+
echo "$SENDER has $permission access, so leaving the PR open."
72+
exit 0
73+
fi
74+
gh pr close "$PR_URL" --comment 'This pull request is labeled `invalid`, so only maintainers can reopen it. If you think this is incorrect, please mention this in a corresponding issue.'
75+
76+
# Maintainers add skip-spammy-check to recover a PR that automated checks closed as invalid.
77+
reopen-on-adding-skip-label:
78+
if: github.repository == 'github/docs' && github.event_name == 'pull_request_target' && github.event.label.name == 'skip-spammy-check'
79+
runs-on: ubuntu-latest
80+
permissions:
81+
contents: read
82+
steps:
83+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
84+
with:
85+
# A PAT makes the reopen trigger other workflows, such as triage, like a manual reopen.
86+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
87+
script: |
88+
const { owner, repo } = context.repo
89+
const pull_number = context.payload.pull_request.number
90+
91+
// The event payload can be stale by the time this job runs, so read the current state.
92+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })
93+
const labels = pr.labels.map(label => label.name.toLowerCase())
94+
95+
if (pr.merged || pr.state !== 'closed') {
96+
core.info('The pull request is not closed, so there is nothing to reopen.')
97+
return
98+
}
99+
100+
if (!labels.includes('skip-spammy-check')) {
101+
core.info('The pull request no longer has the skip-spammy-check label.')
102+
return
103+
}
104+
105+
// Automated checks add the invalid label before closing, so PRs without it were closed for another reason.
106+
if (!labels.includes('invalid')) {
107+
core.info('The pull request was not closed as invalid, so it stays closed.')
108+
return
109+
}
110+
111+
try {
112+
await github.rest.pulls.update({ owner, repo, pull_number, state: 'open' })
113+
} catch (error) {
114+
if (error.status !== 422) throw error
115+
116+
// GitHub refuses to reopen a PR after its head or base branch was deleted or force-pushed.
117+
core.warning(`Could not reopen the pull request: ${error.message}`)
118+
await github.rest.issues.createComment({
119+
owner,
120+
repo,
121+
issue_number: pull_number,
122+
body: 'I could not reopen this pull request automatically, most likely because its head or base branch was deleted or force-pushed after it was closed. Restore the branch, then remove and re-add the `skip-spammy-check` label to retry automatic reopening and removal of the `invalid` label.',
123+
})
124+
return
125+
}
126+
127+
try {
128+
await github.rest.issues.removeLabel({
129+
owner,
130+
repo,
131+
issue_number: pull_number,
132+
name: 'invalid',
133+
})
134+
} catch (error) {
135+
// A maintainer or an earlier run may have removed the label already.
136+
if (error.status !== 404) throw error
137+
}
138+
139+
await github.rest.issues.createComment({
140+
owner,
141+
repo,
142+
issue_number: pull_number,
143+
body: 'The `skip-spammy-check` label was added, so I removed the `invalid` label and reopened this pull request. The spammy pull request check skips it while the label is applied.',
144+
})

‎.github/workflows/close-prs-not-targeting-main.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Reject PRs targeting non-default branch (scheduled)
22

3-
# Backup check for check-for-spammy-prs.yml: closes PRs that don't target main
3+
# Backup check for check-for-spammy-prs.yml: closes PRs that don't target main, unless they have the skip-spammy-check label
44

55
on:
66
schedule:
@@ -39,6 +39,9 @@ jobs:
3939
if (pr.user.login === 'docs-bot') continue
4040
4141
const labels = pr.labels.map(l => l.name)
42+
// Maintainers add this label to allow a PR that targets another branch.
43+
if (labels.some(label => label.toLowerCase() === 'skip-spammy-check')) continue
44+
4245
if (!labels.includes('invalid')) {
4346
await github.rest.issues.addLabels({
4447
owner,

0 commit comments

Comments
 (0)