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
132 changes: 132 additions & 0 deletions .github/workflows/docs-feedback-internal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Route docs feedback internally

on:
issues:
types: [labeled]
# Use the trusted base workflow for fork PRs without checking out contributor code.
pull_request_target:
types: [labeled]

permissions:
contents: read

concurrency:
group: docs-feedback-internal-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.event.label.name }}
cancel-in-progress: false

jobs:
route-feedback:
if: github.repository == 'github/docs' && github.event.label.name == 'docs-feedback-internal'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write

steps:
- name: Record feedback in the private tracker
id: record
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
# GITHUB_TOKEN cannot write to another repository.
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
const sourceParams = { ...context.repo, issue_number: context.issue.number }
const { data: source } = await github.rest.issues.get(sourceParams)
if (!source.labels.some(label => label.name === 'docs-feedback-internal')) {
core.info('The routing label was removed, so no feedback will be recorded.')
return
}

const target = { owner: 'github', repo: 'technical-content', issue_number: 8227 }
const { data: tracker } = await github.rest.issues.get(target)
if (tracker.state !== 'open' || tracker.pull_request) {
throw new Error('The feedback tracker must be an open issue. The source will not be closed.')
}

const marker = `<!-- docs-feedback-internal:${context.repo.owner}/${context.repo.repo}#${source.number} -->`
let recorded = false
for await (const { data: comments } of github.paginate.iterator(
github.rest.issues.listComments,
{ ...target, per_page: 100 },
)) {
if (comments.some(comment => comment.body?.startsWith(`${marker}\n`))) {
recorded = true
break
}
}

if (!recorded) {
const description = source.body || '_No description provided._'
// Leave room for metadata within GitHub's comment size limit.
const excerpt = description.length > 60000
? `${description.slice(0, 60000)}\n\n_Description truncated. See the source for the full text._`
: description
const kind = context.payload.pull_request ? 'Pull request' : 'Issue'
await github.rest.issues.createComment({
...target,
body: [
marker,
`## ${source.title}`,
'',
`${kind}: ${source.html_url}`,
`Author: @${source.user.login}`,
`Routed by: @${context.actor}`,
'',
excerpt,
].join('\n'),
})
}

core.setOutput('recorded', 'true')

- name: Explain the handoff and close the source
if: steps.record.outputs.recorded == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const sourceParams = { ...context.repo, issue_number: context.issue.number }
const { data: source } = await github.rest.issues.get(sourceParams)
if (!source.labels.some(label => label.name === 'docs-feedback-internal')) {
core.info('The routing label was removed, so the source will not be closed.')
return
}

// Do not expose the private tracker URL in this public comment.
const marker = '<!-- docs-feedback-internal:handoff -->'
let explained = false
for await (const { data: comments } of github.paginate.iterator(
github.rest.issues.listComments,
{ ...sourceParams, per_page: 100 },
)) {
if (comments.some(comment =>
comment.user?.login === 'github-actions[bot]' &&
comment.body?.startsWith(`${marker}\n`)
)) {
explained = true
break
}
}

if (!explained) {
await github.rest.issues.createComment({
...sourceParams,
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.`,
})
}

if (source.state === 'open') {
if (context.payload.pull_request) {
await github.rest.pulls.update({
...context.repo,
pull_number: source.number,
state: 'closed',
})
} else {
await github.rest.issues.update({
...sourceParams,
state: 'closed',
state_reason: 'not_planned',
})
}
}
164 changes: 122 additions & 42 deletions .github/workflows/notify-non-employee-comments.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Daily non-employee comment digest
name: Daily non-employee comment and review digest

# Include issue/PR conversation comments, not inline review comments.
# Include conversation comments, inline review comments, and submitted reviews without their bodies.
on:
workflow_dispatch:
schedule:
Expand Down Expand Up @@ -51,45 +51,121 @@ jobs:
core.info(`Building digest for ${since.toISOString()} to ${until.toISOString()}.`);

const authors = new Map();
function addActivity({ author, createdAt, url, conversation, kind }) {
const timestamp = Date.parse(createdAt);
if (!(timestamp >= since.getTime() && timestamp < until.getTime())) {
return;
}
if (!author) {
core.warning('Skipping an activity whose author is unavailable.');
return;
}
const login = author.login.toLowerCase();
if (author.type === 'Bot' || author.__typename === 'Bot' ||
['docs-bot', 'copilot'].includes(login)) {
return;
}
if (!authors.has(login)) {
authors.set(login, { login: author.login, activities: [] });
}
authors.get(login).activities.push({ url, conversation, kind });
}

let page = 0;
try {
for await (const { data: comments } of github.paginate.iterator(
for (const listComments of [
github.rest.issues.listCommentsForRepo,
{
owner: 'github',
repo: 'docs',
since: since.toISOString(),
sort: 'created',
direction: 'asc',
per_page: 100,
},
)) {
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
for (const comment of comments) {
const createdAt = Date.parse(comment.created_at);
if (!(createdAt >= since.getTime() && createdAt < until.getTime())) {
continue;
github.rest.pulls.listReviewCommentsForRepo,
]) {
for await (const { data: comments } of github.paginate.iterator(
listComments,
{
owner: 'github',
repo: 'docs',
since: since.toISOString(),
sort: 'created',
direction: 'asc',
per_page: 100,
},
)) {
core.info(`Fetched comments page ${++page} (${comments.length} comments).`);
for (const comment of comments) {
addActivity({
author: comment.user,
createdAt: comment.created_at,
url: comment.html_url,
conversation: comment.issue_url ??
comment.pull_request_url.replace('/pulls/', '/issues/'),
kind: 'comment',
});
}
const author = comment.user;
if (!author) {
core.warning('Skipping a comment whose author is unavailable.');
continue;
}
}
} catch (error) {
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
}

const reviewFields = `
fragment digestReviews on PullRequest {
reviews(first: 100, after: $reviewsCursor) {
nodes {
author { login __typename }
submittedAt
url
}
const login = author.login.toLowerCase();
if (author.type === 'Bot' || ['docs-bot', 'copilot'].includes(login)) {
continue;
pageInfo { hasNextPage endCursor }
}
}
`;
let pullRequestsCursor = null;
try {
do {
const { repository } = await github.graphql(`
query DigestPullRequests($pullRequestsCursor: String, $reviewsCursor: String) {
repository(owner: "github", name: "docs") {
pullRequests(first: 50, after: $pullRequestsCursor,
orderBy: { field: UPDATED_AT, direction: DESC }) {
nodes { id number updatedAt ...digestReviews }
pageInfo { hasNextPage endCursor }
}
}
}
if (!authors.has(login)) {
authors.set(login, { login: author.login, comments: [] });
${reviewFields}
`, { pullRequestsCursor, reviewsCursor: null });
const pullRequests = repository.pullRequests;
const recentPullRequests = pullRequests.nodes.filter(
(pullRequest) => Date.parse(pullRequest.updatedAt) >= since.getTime(),
);
for (const pullRequest of recentPullRequests) {
let reviews = pullRequest.reviews;
while (true) {
for (const review of reviews.nodes) {
addActivity({
author: review.author,
createdAt: review.submittedAt,
url: review.url,
conversation: `https://api.github.com/repos/github/docs/issues/${pullRequest.number}`,
kind: 'review',
});
}
if (!reviews.pageInfo.hasNextPage) {
break;
}
const { node } = await github.graphql(`
query DigestReviewPage($id: ID!, $reviewsCursor: String) {
node(id: $id) { ...digestReviews }
}
${reviewFields}
`, { id: pullRequest.id, reviewsCursor: reviews.pageInfo.endCursor });
reviews = node.reviews;
}
authors.get(login).comments.push({
url: comment.html_url,
conversation: comment.issue_url,
});
}
}
// Updated ordering includes closed/merged PRs and lets us stop at the cutoff.
pullRequestsCursor = recentPullRequests.length === pullRequests.nodes.length &&
pullRequests.pageInfo.hasNextPage ? pullRequests.pageInfo.endCursor : null;
} while (pullRequestsCursor);
} catch (error) {
throw new Error(`Fetching digest comments failed (HTTP ${error.status || 'unknown'}).`);
throw new Error(`Fetching digest reviews failed (HTTP ${error.status || 'unknown'}).`);
}

const nonEmployees = [];
Expand All @@ -110,23 +186,27 @@ jobs:
}
}

const totalComments = nonEmployees.reduce((sum, author) => sum + author.comments.length, 0);
core.info(`Found ${totalComments} comments by ${nonEmployees.length} non-employees.`);
if (totalComments === 0) {
const activities = nonEmployees.flatMap((author) => author.activities);
const totalComments = activities.filter((activity) => activity.kind === 'comment').length;
const totalReviews = activities.length - totalComments;
core.info(`Found ${totalComments} comments and ${totalReviews} reviews by ${nonEmployees.length} non-employees.`);
if (activities.length === 0) {
return;
}

let message = `Daily non-employee comment digest for github/docs\n` +
let message = `Daily non-employee comment and review digest for github/docs\n` +
`${since.toISOString()} to ${until.toISOString()} (UTC)\n` +
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}\n` +
'Comment bodies omitted; follow the links to read them on GitHub.';
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or comment links are omitted.]';
`Non-employee users: ${nonEmployees.length}; comments: ${totalComments}; reviews: ${totalReviews}\n` +
'Comment and review bodies omitted; follow the links to read them on GitHub.';
const shortened = '\n\n[Digest shortened to fit one Slack message; some authors or activity links are omitted.]';
const maximumLength = 35000 - shortened.length;
const lines = nonEmployees.flatMap((author) => [
`\n\n@${author.login} (https://github.com/${author.login})\n` +
`Comments: ${author.comments.length}; conversations: ` +
`${new Set(author.comments.map((comment) => comment.conversation)).size}`,
...author.comments.map((comment) => `\n ${comment.url}`),
`Comments: ${author.activities.filter((activity) => activity.kind === 'comment').length}; ` +
`reviews: ${author.activities.filter((activity) => activity.kind === 'review').length}; conversations: ` +
`${new Set(author.activities.map((activity) => activity.conversation)).size}`,
...author.activities.map((activity) =>
`\n ${activity.kind === 'review' ? 'Review: ' : ''}${activity.url}`),
]);
for (const line of lines) {
if (message.length + line.length > maximumLength) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ If a network level firewall is in place between nodes, these ports will need to
| 8125/UDP | Statsd |
| 8301/UDP | Consul |
| 8302/UDP | Consul |
| {% ifversion ghes-collectd-available %} |
| 25827/UDP | Collectd |
| {% endif %} |

## Configuring a load balancer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ children:
- /answers-to-common-questions-about-high-availability-replicas
- /creating-a-high-availability-replica
- /monitoring-a-high-availability-configuration
- /managing-the-replication-controller
- /initiating-a-failover-to-your-replica-appliance
- /recovering-a-high-availability-configuration
- /removing-a-high-availability-replica
Expand Down
Loading
Loading