Skip to content

Commit 23809bf

Browse files
authored
Merge pull request #46805 from github/repo-sync
Repo sync
2 parents 65c73d2 + ab0a695 commit 23809bf

29 files changed

Lines changed: 419 additions & 71 deletions
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
name: Route docs feedback internally
2+
3+
on:
4+
issues:
5+
types: [labeled]
6+
# Use the trusted base workflow for fork PRs without checking out contributor code.
7+
pull_request_target:
8+
types: [labeled]
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: docs-feedback-internal-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.event.label.name }}
15+
cancel-in-progress: false
16+
17+
jobs:
18+
route-feedback:
19+
if: github.repository == 'github/docs' && github.event.label.name == 'docs-feedback-internal'
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
issues: write
24+
pull-requests: write
25+
26+
steps:
27+
- name: Record feedback in the private tracker
28+
id: record
29+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
30+
with:
31+
# GITHUB_TOKEN cannot write to another repository.
32+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
33+
script: |
34+
const sourceParams = { ...context.repo, issue_number: context.issue.number }
35+
const { data: source } = await github.rest.issues.get(sourceParams)
36+
if (!source.labels.some(label => label.name === 'docs-feedback-internal')) {
37+
core.info('The routing label was removed, so no feedback will be recorded.')
38+
return
39+
}
40+
41+
const target = { owner: 'github', repo: 'technical-content', issue_number: 8227 }
42+
const { data: tracker } = await github.rest.issues.get(target)
43+
if (tracker.state !== 'open' || tracker.pull_request) {
44+
throw new Error('The feedback tracker must be an open issue. The source will not be closed.')
45+
}
46+
47+
const marker = `<!-- docs-feedback-internal:${context.repo.owner}/${context.repo.repo}#${source.number} -->`
48+
let recorded = false
49+
for await (const { data: comments } of github.paginate.iterator(
50+
github.rest.issues.listComments,
51+
{ ...target, per_page: 100 },
52+
)) {
53+
if (comments.some(comment => comment.body?.startsWith(`${marker}\n`))) {
54+
recorded = true
55+
break
56+
}
57+
}
58+
59+
if (!recorded) {
60+
const description = source.body || '_No description provided._'
61+
// Leave room for metadata within GitHub's comment size limit.
62+
const excerpt = description.length > 60000
63+
? `${description.slice(0, 60000)}\n\n_Description truncated. See the source for the full text._`
64+
: description
65+
const kind = context.payload.pull_request ? 'Pull request' : 'Issue'
66+
await github.rest.issues.createComment({
67+
...target,
68+
body: [
69+
marker,
70+
`## ${source.title}`,
71+
'',
72+
`${kind}: ${source.html_url}`,
73+
`Author: @${source.user.login}`,
74+
`Routed by: @${context.actor}`,
75+
'',
76+
excerpt,
77+
].join('\n'),
78+
})
79+
}
80+
81+
core.setOutput('recorded', 'true')
82+
83+
- name: Explain the handoff and close the source
84+
if: steps.record.outputs.recorded == 'true'
85+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
86+
with:
87+
script: |
88+
const sourceParams = { ...context.repo, issue_number: context.issue.number }
89+
const { data: source } = await github.rest.issues.get(sourceParams)
90+
if (!source.labels.some(label => label.name === 'docs-feedback-internal')) {
91+
core.info('The routing label was removed, so the source will not be closed.')
92+
return
93+
}
94+
95+
// Do not expose the private tracker URL in this public comment.
96+
const marker = '<!-- docs-feedback-internal:handoff -->'
97+
let explained = false
98+
for await (const { data: comments } of github.paginate.iterator(
99+
github.rest.issues.listComments,
100+
{ ...sourceParams, per_page: 100 },
101+
)) {
102+
if (comments.some(comment =>
103+
comment.user?.login === 'github-actions[bot]' &&
104+
comment.body?.startsWith(`${marker}\n`)
105+
)) {
106+
explained = true
107+
break
108+
}
109+
}
110+
111+
if (!explained) {
112+
await github.rest.issues.createComment({
113+
...sourceParams,
114+
body: `${marker}\nThank you for the feedback! We've recorded it for internal review by the documentation team, so we're closing this here. This does not mean the underlying feedback has been resolved.`,
115+
})
116+
}
117+
118+
if (source.state === 'open') {
119+
if (context.payload.pull_request) {
120+
await github.rest.pulls.update({
121+
...context.repo,
122+
pull_number: source.number,
123+
state: 'closed',
124+
})
125+
} else {
126+
await github.rest.issues.update({
127+
...sourceParams,
128+
state: 'closed',
129+
state_reason: 'not_planned',
130+
})
131+
}
132+
}

‎.github/workflows/notify-non-employee-comments.yml‎

Lines changed: 122 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
name: Daily non-employee comment digest
1+
name: Daily non-employee comment and review digest
22

3-
# Include issue/PR conversation comments, not inline review comments.
3+
# Include conversation comments, inline review comments, and submitted reviews without their bodies.
44
on:
55
workflow_dispatch:
66
schedule:
@@ -51,45 +51,121 @@ jobs:
5151
core.info(`Building digest for ${since.toISOString()} to ${until.toISOString()}.`);
5252
5353
const authors = new Map();
54+
function addActivity({ author, createdAt, url, conversation, kind }) {
55+
const timestamp = Date.parse(createdAt);
56+
if (!(timestamp >= since.getTime() && timestamp < until.getTime())) {
57+
return;
58+
}
59+
if (!author) {
60+
core.warning('Skipping an activity whose author is unavailable.');
61+
return;
62+
}
63+
const login = author.login.toLowerCase();
64+
if (author.type === 'Bot' || author.__typename === 'Bot' ||
65+
['docs-bot', 'copilot'].includes(login)) {
66+
return;
67+
}
68+
if (!authors.has(login)) {
69+
authors.set(login, { login: author.login, activities: [] });
70+
}
71+
authors.get(login).activities.push({ url, conversation, kind });
72+
}
73+
5474
let page = 0;
5575
try {
56-
for await (const { data: comments } of github.paginate.iterator(
76+
for (const listComments of [
5777
github.rest.issues.listCommentsForRepo,
58-
{
59-
owner: 'github',
60-
repo: 'docs',
61-
since: since.toISOString(),
62-
sort: 'created',
63-
direction: 'asc',
64-
per_page: 100,
65-
},
66-
)) {
67-
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
68-
for (const comment of comments) {
69-
const createdAt = Date.parse(comment.created_at);
70-
if (!(createdAt >= since.getTime() && createdAt < until.getTime())) {
71-
continue;
78+
github.rest.pulls.listReviewCommentsForRepo,
79+
]) {
80+
for await (const { data: comments } of github.paginate.iterator(
81+
listComments,
82+
{
83+
owner: 'github',
84+
repo: 'docs',
85+
since: since.toISOString(),
86+
sort: 'created',
87+
direction: 'asc',
88+
per_page: 100,
89+
},
90+
)) {
91+
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
92+
for (const comment of comments) {
93+
addActivity({
94+
author: comment.user,
95+
createdAt: comment.created_at,
96+
url: comment.html_url,
97+
conversation: comment.issue_url ??
98+
comment.pull_request_url.replace('/pulls/', '/issues/'),
99+
kind: 'comment',
100+
});
72101
}
73-
const author = comment.user;
74-
if (!author) {
75-
core.warning('Skipping a comment whose author is unavailable.');
76-
continue;
102+
}
103+
}
104+
} catch (error) {
105+
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
106+
}
107+
108+
const reviewFields = `
109+
fragment digestReviews on PullRequest {
110+
reviews(first: 100, after: $reviewsCursor) {
111+
nodes {
112+
author { login __typename }
113+
submittedAt
114+
url
77115
}
78-
const login = author.login.toLowerCase();
79-
if (author.type === 'Bot' || ['docs-bot', 'copilot'].includes(login)) {
80-
continue;
116+
pageInfo { hasNextPage endCursor }
117+
}
118+
}
119+
`;
120+
let pullRequestsCursor = null;
121+
try {
122+
do {
123+
const { repository } = await github.graphql(`
124+
query DigestPullRequests($pullRequestsCursor: String, $reviewsCursor: String) {
125+
repository(owner: "github", name: "docs") {
126+
pullRequests(first: 50, after: $pullRequestsCursor,
127+
orderBy: { field: UPDATED_AT, direction: DESC }) {
128+
nodes { id number updatedAt ...digestReviews }
129+
pageInfo { hasNextPage endCursor }
130+
}
131+
}
81132
}
82-
if (!authors.has(login)) {
83-
authors.set(login, { login: author.login, comments: [] });
133+
${reviewFields}
134+
`, { pullRequestsCursor, reviewsCursor: null });
135+
const pullRequests = repository.pullRequests;
136+
const recentPullRequests = pullRequests.nodes.filter(
137+
(pullRequest) => Date.parse(pullRequest.updatedAt) >= since.getTime(),
138+
);
139+
for (const pullRequest of recentPullRequests) {
140+
let reviews = pullRequest.reviews;
141+
while (true) {
142+
for (const review of reviews.nodes) {
143+
addActivity({
144+
author: review.author,
145+
createdAt: review.submittedAt,
146+
url: review.url,
147+
conversation: `https://api.github.com/repos/github/docs/issues/${pullRequest.number}`,
148+
kind: 'review',
149+
});
150+
}
151+
if (!reviews.pageInfo.hasNextPage) {
152+
break;
153+
}
154+
const { node } = await github.graphql(`
155+
query DigestReviewPage($id: ID!, $reviewsCursor: String) {
156+
node(id: $id) { ...digestReviews }
157+
}
158+
${reviewFields}
159+
`, { id: pullRequest.id, reviewsCursor: reviews.pageInfo.endCursor });
160+
reviews = node.reviews;
84161
}
85-
authors.get(login).comments.push({
86-
url: comment.html_url,
87-
conversation: comment.issue_url,
88-
});
89162
}
90-
}
163+
// Updated ordering includes closed/merged PRs and lets us stop at the cutoff.
164+
pullRequestsCursor = recentPullRequests.length === pullRequests.nodes.length &&
165+
pullRequests.pageInfo.hasNextPage ? pullRequests.pageInfo.endCursor : null;
166+
} while (pullRequestsCursor);
91167
} catch (error) {
92-
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
168+
throw new Error(`Fetching digest reviews failed (HTTP ${error.status || 'unknown'}).`);
93169
}
94170
95171
const nonEmployees = [];
@@ -110,23 +186,27 @@ jobs:
110186
}
111187
}
112188
113-
const totalComments = nonEmployees.reduce((sum, author) => sum + author.comments.length, 0);
114-
core.info(`Found ${totalComments} comments by ${nonEmployees.length} non-employees.`);
115-
if (totalComments === 0) {
189+
const activities = nonEmployees.flatMap((author) => author.activities);
190+
const totalComments = activities.filter((activity) => activity.kind === 'comment').length;
191+
const totalReviews = activities.length - totalComments;
192+
core.info(`Found ${totalComments} comments and ${totalReviews} reviews by ${nonEmployees.length} non-employees.`);
193+
if (activities.length === 0) {
116194
return;
117195
}
118196
119-
let message = `Daily non-employee comment digest for github/docs\n` +
197+
let message = `Daily non-employee comment and review digest for github/docs\n` +
120198
`${since.toISOString()} to ${until.toISOString()} (UTC)\n` +
121-
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}\n` +
122-
'Comment bodies omitted; follow the links to read them on GitHub.';
123-
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or comment links are omitted.]';
199+
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}; reviews: ${totalReviews}\n` +
200+
'Comment and review bodies omitted; follow the links to read them on GitHub.';
201+
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or activity links are omitted.]';
124202
const maximumLength = 35000 - shortened.length;
125203
const lines = nonEmployees.flatMap((author) => [
126204
`\n\n@${author.login} (https://github.com/${author.login})\n` +
127-
`Comments: ${author.comments.length}; conversations: ` +
128-
`${new Set(author.comments.map((comment) => comment.conversation)).size}`,
129-
...author.comments.map((comment) => `\n ${comment.url}`),
205+
`Comments: ${author.activities.filter((activity) => activity.kind === 'comment').length}; ` +
206+
`reviews: ${author.activities.filter((activity) => activity.kind === 'review').length}; conversations: ` +
207+
`${new Set(author.activities.map((activity) => activity.conversation)).size}`,
208+
...author.activities.map((activity) =>
209+
`\n ${activity.kind === 'review' ? 'Review: ' : ''}${activity.url}`),
130210
]);
131211
for (const line of lines) {
132212
if (message.length + line.length > maximumLength) {

‎content/admin/monitoring-and-managing-your-instance/configuring-clustering/cluster-network-configuration.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ If a network level firewall is in place between nodes, these ports will need to
8585
| 8125/UDP | Statsd |
8686
| 8301/UDP | Consul |
8787
| 8302/UDP | Consul |
88+
| {% ifversion ghes-collectd-available %} |
8889
| 25827/UDP | Collectd |
90+
| {% endif %} |
8991

9092
## Configuring a load balancer
9193

‎content/admin/monitoring-and-managing-your-instance/configuring-high-availability/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ children:
1717
- /answers-to-common-questions-about-high-availability-replicas
1818
- /creating-a-high-availability-replica
1919
- /monitoring-a-high-availability-configuration
20+
- /managing-the-replication-controller
2021
- /initiating-a-failover-to-your-replica-appliance
2122
- /recovering-a-high-availability-configuration
2223
- /removing-a-high-availability-replica

0 commit comments

Comments
 (0)