Skip to content

Commit 215d8dc

Browse files
authored
Merge branch 'main' into patch-3
2 parents a8d29d2 + f37bce2 commit 215d8dc

File tree

4,290 files changed

+101065
-97065
lines changed

Some content is hidden

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

4,290 files changed

+101065
-97065
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package-lock.json @github/docs-engineering
1717
package.json @github/docs-engineering
1818

1919
# Localization
20+
/.github/actions-scripts/create-translation-batch-pr.js @github/docs-localization
2021
/.github/workflows/create-translation-batch-pr.yml @github/docs-localization
2122
/.github/workflows/crowdin.yml @github/docs-localization
2223
/crowdin*.yml @github/docs-engineering @github/docs-localization
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs'
4+
import github from '@actions/github'
5+
6+
const OPTIONS = Object.fromEntries(
7+
['BASE', 'BODY_FILE', 'GITHUB_TOKEN', 'HEAD', 'LANGUAGE', 'TITLE', 'GITHUB_REPOSITORY'].map(
8+
(envVarName) => {
9+
const envVarValue = process.env[envVarName]
10+
if (!envVarValue) {
11+
throw new Error(`You must supply a ${envVarName} environment variable`)
12+
}
13+
return [envVarName, envVarValue]
14+
}
15+
)
16+
)
17+
18+
if (!process.env.GITHUB_REPOSITORY) {
19+
throw new Error('GITHUB_REPOSITORY environment variable not set')
20+
}
21+
22+
const RETRY_STATUSES = [
23+
422, // Retry the operation if the PR already exists
24+
502, // Retry the operation if the API responds with a `502 Bad Gateway` error.
25+
]
26+
const RETRY_ATTEMPTS = 3
27+
const {
28+
// One of the default environment variables provided by Actions.
29+
GITHUB_REPOSITORY,
30+
31+
// These are passed in from the step in the workflow file.
32+
TITLE,
33+
BASE,
34+
HEAD,
35+
LANGUAGE,
36+
BODY_FILE,
37+
GITHUB_TOKEN,
38+
} = OPTIONS
39+
const [OWNER, REPO] = GITHUB_REPOSITORY.split('/')
40+
41+
const octokit = github.getOctokit(GITHUB_TOKEN)
42+
43+
/**
44+
* @param {object} config Configuration options for finding the PR.
45+
* @returns {Promise<number | undefined>} The PR number.
46+
*/
47+
async function findPullRequestNumber(config) {
48+
// Get a list of PRs and see if one already exists.
49+
const { data: listOfPullRequests } = await octokit.rest.pulls.list({
50+
owner: config.owner,
51+
repo: config.repo,
52+
head: `${config.owner}:${config.head}`,
53+
})
54+
55+
return listOfPullRequests[0]?.number
56+
}
57+
58+
/**
59+
* When this file was first created, we only introduced support for creating a pull request for some translation batch.
60+
* However, some of our first workflow runs failed during the pull request creation due to a timeout error.
61+
* There have been cases where, despite the timeout error, the pull request gets created _anyway_.
62+
* To accommodate this reality, we created this function to look for an existing pull request before a new one is created.
63+
* Although the "find" check is redundant in the first "cycle", it's designed this way to recursively call the function again via its retry mechanism should that be necessary.
64+
*
65+
* @param {object} config Configuration options for creating the pull request.
66+
* @returns {Promise<number>} The PR number.
67+
*/
68+
async function findOrCreatePullRequest(config) {
69+
const found = await findPullRequestNumber(config)
70+
71+
if (found) {
72+
return found
73+
}
74+
75+
try {
76+
const { data: pullRequest } = await octokit.rest.pulls.create({
77+
owner: config.owner,
78+
repo: config.repo,
79+
base: config.base,
80+
head: config.head,
81+
title: config.title,
82+
body: config.body,
83+
draft: false,
84+
})
85+
86+
return pullRequest.number
87+
} catch (error) {
88+
if (!error.response || !config.retryCount) {
89+
throw error
90+
}
91+
92+
if (!config.retryStatuses.includes(error.response.status)) {
93+
throw error
94+
}
95+
96+
console.error(`Error creating pull request: ${error.message}`)
97+
console.warn(`Retrying in 5 seconds...`)
98+
await new Promise((resolve) => setTimeout(resolve, 5000))
99+
100+
config.retryCount -= 1
101+
102+
return findOrCreatePullRequest(config)
103+
}
104+
}
105+
106+
/**
107+
* @param {object} config Configuration options for labeling the PR
108+
* @returns {Promise<undefined>}
109+
*/
110+
async function labelPullRequest(config) {
111+
await octokit.rest.issues.update({
112+
owner: config.owner,
113+
repo: config.repo,
114+
issue_number: config.issue_number,
115+
labels: config.labels,
116+
})
117+
}
118+
119+
async function main() {
120+
const options = {
121+
title: TITLE,
122+
base: BASE,
123+
head: HEAD,
124+
body: fs.readFileSync(BODY_FILE, 'utf8'),
125+
labels: ['translation-batch', `translation-batch-${LANGUAGE}`],
126+
owner: OWNER,
127+
repo: REPO,
128+
retryStatuses: RETRY_STATUSES,
129+
retryCount: RETRY_ATTEMPTS,
130+
}
131+
132+
options.issue_number = await findOrCreatePullRequest(options)
133+
const pr = `${GITHUB_REPOSITORY}#${options.issue_number}`
134+
console.log(`Created PR ${pr}`)
135+
136+
// metadata parameters aren't currently available in `github.rest.pulls.create`,
137+
// but they are in `github.rest.issues.update`.
138+
await labelPullRequest(options)
139+
console.log(`Updated ${pr} with these labels: ${options.labels.join(', ')}`)
140+
}
141+
142+
main()

.github/actions-scripts/fr-add-docs-reviewers-requests.js

Lines changed: 97 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,88 +8,83 @@ import {
88
generateUpdateProjectNextItemFieldMutation,
99
} from './projects.js'
1010

11-
async function run() {
12-
// Get info about the docs-content review board project
13-
// and about open github/github PRs
14-
const data = await graphql(
15-
`
16-
query ($organization: String!, $repo: String!, $projectNumber: Int!, $num_prs: Int!) {
17-
organization(login: $organization) {
18-
projectNext(number: $projectNumber) {
19-
id
20-
items(last: 100) {
11+
async function getAllOpenPRs() {
12+
let prsRemaining = true
13+
let cursor
14+
let prData = []
15+
while (prsRemaining) {
16+
const data = await graphql(
17+
`
18+
query ($organization: String!, $repo: String!) {
19+
repository(name: $repo, owner: $organization) {
20+
pullRequests(last: 100, states: OPEN${cursor ? ` before:"${cursor}"` : ''}) {
21+
pageInfo{startCursor, hasPreviousPage},
2122
nodes {
2223
id
23-
}
24-
}
25-
fields(first: 20) {
26-
nodes {
27-
id
28-
name
29-
settings
30-
}
31-
}
32-
}
33-
}
34-
repository(name: $repo, owner: $organization) {
35-
pullRequests(last: $num_prs, states: OPEN) {
36-
nodes {
37-
id
38-
isDraft
39-
reviewRequests(first: 10) {
40-
nodes {
41-
requestedReviewer {
42-
... on Team {
43-
name
24+
isDraft
25+
reviewRequests(first: 10) {
26+
nodes {
27+
requestedReviewer {
28+
... on Team {
29+
name
30+
}
4431
}
4532
}
4633
}
47-
}
48-
labels(first: 5) {
49-
nodes {
50-
name
34+
labels(first: 5) {
35+
nodes {
36+
name
37+
}
5138
}
52-
}
53-
reviews(first: 10) {
54-
nodes {
55-
onBehalfOf(first: 1) {
56-
nodes {
57-
name
39+
reviews(first: 10) {
40+
nodes {
41+
onBehalfOf(first: 1) {
42+
nodes {
43+
name
44+
}
5845
}
5946
}
6047
}
61-
}
62-
author {
63-
login
48+
author {
49+
login
50+
}
6451
}
6552
}
6653
}
6754
}
55+
`,
56+
{
57+
organization: process.env.ORGANIZATION,
58+
repo: process.env.REPO,
59+
headers: {
60+
authorization: `token ${process.env.TOKEN}`,
61+
},
6862
}
69-
`,
70-
{
71-
organization: process.env.ORGANIZATION,
72-
repo: process.env.REPO,
73-
projectNumber: parseInt(process.env.PROJECT_NUMBER),
74-
num_prs: parseInt(process.env.NUM_PRS),
75-
headers: {
76-
authorization: `token ${process.env.TOKEN}`,
77-
'GraphQL-Features': 'projects_next_graphql',
78-
},
79-
}
80-
)
63+
)
64+
65+
prsRemaining = data.repository.pullRequests.pageInfo.hasPreviousPage
66+
cursor = data.repository.pullRequests.pageInfo.startCursor
67+
prData = [...prData, ...data.repository.pullRequests.nodes]
68+
}
69+
70+
return prData
71+
}
72+
73+
async function run() {
74+
// Get info about open github/github PRs
75+
const prData = await getAllOpenPRs()
8176

8277
// Get the PRs that are:
8378
// - not draft
8479
// - not a train
8580
// - are requesting a review by docs-reviewers
8681
// - have not already been reviewed on behalf of docs-reviewers
87-
const prs = data.repository.pullRequests.nodes.filter(
82+
const prs = prData.filter(
8883
(pr) =>
8984
!pr.isDraft &&
9085
!pr.labels.nodes.find((label) => label.name === 'Deploy train 🚂') &&
9186
pr.reviewRequests.nodes.find(
92-
(requestedReviewers) => requestedReviewers.requestedReviewer.name === process.env.REVIEWER
87+
(requestedReviewers) => requestedReviewers.requestedReviewer?.name === process.env.REVIEWER
9388
) &&
9489
!pr.reviews.nodes
9590
.flatMap((review) => review.onBehalfOf.nodes)
@@ -104,28 +99,60 @@ async function run() {
10499
const prAuthors = prs.map((pr) => pr.author.login)
105100
console.log(`PRs found: ${prIDs}`)
106101

102+
// Get info about the docs-content review board project
103+
const projectData = await graphql(
104+
`
105+
query ($organization: String!, $projectNumber: Int!) {
106+
organization(login: $organization) {
107+
projectNext(number: $projectNumber) {
108+
id
109+
items(last: 100) {
110+
nodes {
111+
id
112+
}
113+
}
114+
fields(first: 100) {
115+
nodes {
116+
id
117+
name
118+
settings
119+
}
120+
}
121+
}
122+
}
123+
}
124+
`,
125+
{
126+
organization: process.env.ORGANIZATION,
127+
projectNumber: parseInt(process.env.PROJECT_NUMBER),
128+
headers: {
129+
authorization: `token ${process.env.TOKEN}`,
130+
},
131+
}
132+
)
133+
107134
// Get the project ID
108-
const projectID = data.organization.projectNext.id
135+
const projectID = projectData.organization.projectNext.id
109136

110137
// Get the IDs of the last 100 items on the board.
111138
// Until we have a way to check from a PR whether the PR is in a project,
112139
// this is how we (roughly) avoid overwriting PRs that are already on the board.
113140
// If we are overwriting items, query for more items.
114-
const existingItemIDs = data.organization.projectNext.items.nodes.map((node) => node.id)
141+
const existingItemIDs = projectData.organization.projectNext.items.nodes.map((node) => node.id)
115142

116143
// Get the ID of the fields that we want to populate
117-
const datePostedID = findFieldID('Date posted', data)
118-
const reviewDueDateID = findFieldID('Review due date', data)
119-
const statusID = findFieldID('Status', data)
120-
const featureID = findFieldID('Feature', data)
121-
const contributorTypeID = findFieldID('Contributor type', data)
122-
const sizeTypeID = findFieldID('Size', data)
123-
const authorID = findFieldID('Contributor', data)
144+
const datePostedID = findFieldID('Date posted', projectData)
145+
const reviewDueDateID = findFieldID('Review due date', projectData)
146+
const statusID = findFieldID('Status', projectData)
147+
const featureID = findFieldID('Feature', projectData)
148+
const contributorTypeID = findFieldID('Contributor type', projectData)
149+
const sizeTypeID = findFieldID('Size', projectData)
150+
const authorID = findFieldID('Contributor', projectData)
124151

125152
// Get the ID of the single select values that we want to set
126-
const readyForReviewID = findSingleSelectID('Ready for review', 'Status', data)
127-
const hubberTypeID = findSingleSelectID('Hubber or partner', 'Contributor type', data)
128-
const docsMemberTypeID = findSingleSelectID('Docs team', 'Contributor type', data)
153+
const readyForReviewID = findSingleSelectID('Ready for review', 'Status', projectData)
154+
const hubberTypeID = findSingleSelectID('Hubber or partner', 'Contributor type', projectData)
155+
const docsMemberTypeID = findSingleSelectID('Docs team', 'Contributor type', projectData)
129156

130157
// Add the PRs to the project
131158
const itemIDs = await addItemsToProject(prIDs, projectID)

0 commit comments

Comments
 (0)