Skip to content

Commit d4e5576

Browse files
authored
Merge pull request #28283 from github/repo-sync
Repo sync
2 parents b5f5ebb + 641d566 commit d4e5576

File tree

5 files changed

+282
-136
lines changed

5 files changed

+282
-136
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
export async function createReportIssue({
2+
core,
3+
octokit,
4+
reportTitle,
5+
reportBody,
6+
reportRepository,
7+
reportLabel,
8+
}) {
9+
const [owner, repo] = reportRepository.split('/')
10+
// Create issue
11+
let newReport
12+
try {
13+
const { data } = await octokit.request('POST /repos/{owner}/{repo}/issues', {
14+
owner,
15+
repo,
16+
title: reportTitle,
17+
body: reportBody,
18+
labels: [reportLabel],
19+
})
20+
newReport = data
21+
core.info(`Created new report issue at ${newReport.html_url}\n`)
22+
} catch (error) {
23+
core.error(error)
24+
core.setFailed('Error creating new issue')
25+
throw error
26+
}
27+
28+
return newReport
29+
}
30+
31+
export async function linkReports({
32+
core,
33+
octokit,
34+
newReport,
35+
reportRepository,
36+
reportAuthor,
37+
reportLabel,
38+
}) {
39+
const [owner, repo] = reportRepository.split('/')
40+
41+
core.info('Attempting to link reports...')
42+
// Find previous report issue
43+
let previousReports
44+
try {
45+
previousReports = await octokit.rest.issues.listForRepo({
46+
owner,
47+
repo,
48+
creator: reportAuthor,
49+
labels: reportLabel,
50+
state: 'all', // We want to get the previous report, even if it is closed
51+
sort: 'created',
52+
direction: 'desc',
53+
per_page: 25,
54+
})
55+
previousReports = previousReports.data
56+
} catch (error) {
57+
core.setFailed('Error listing issues for repo')
58+
throw error
59+
}
60+
core.info(`Found ${previousReports.length} previous reports`)
61+
62+
if (previousReports.length <= 1) {
63+
core.info('No previous reports to link to')
64+
return
65+
}
66+
67+
// 2nd report should be most recent previous report
68+
const previousReport = previousReports[1]
69+
70+
// Comment the old report link on the new report
71+
try {
72+
await octokit.rest.issues.createComment({
73+
owner,
74+
repo,
75+
issue_number: newReport.number,
76+
body: `⬅️ [Previous report](${previousReport.html_url})`,
77+
})
78+
core.info(`Linked old report to new report via comment on new report, #${newReport.number}`)
79+
} catch (error) {
80+
core.setFailed(`Error commenting on newReport, #${newReport.number}`)
81+
throw error
82+
}
83+
84+
// Comment on all previous reports that are still open
85+
for (const previousReport of previousReports) {
86+
if (previousReport.state === 'closed' || previousReport.html_url === newReport.html_url) {
87+
continue
88+
}
89+
90+
// If an old report is not assigned to someone we close it
91+
const shouldClose = !previousReport.assignees.length
92+
let body = `➡️ [Newer report](${newReport.html_url})`
93+
if (shouldClose) {
94+
body += '\n\nClosing in favor of newer report since there are no assignees on this issue'
95+
}
96+
try {
97+
await octokit.rest.issues.createComment({
98+
owner,
99+
repo,
100+
issue_number: previousReport.number,
101+
body,
102+
})
103+
core.info(
104+
`Linked old report to new report via comment on old report: #${previousReport.number}.`,
105+
)
106+
} catch (error) {
107+
core.setFailed(`Error commenting on previousReport, #${previousReport.number}`)
108+
throw error
109+
}
110+
if (shouldClose) {
111+
try {
112+
await octokit.rest.issues.update({
113+
owner,
114+
repo,
115+
issue_number: previousReport.number,
116+
state: 'closed',
117+
})
118+
core.info(`Closing old report: #${previousReport.number} because it doesn't have assignees`)
119+
} catch (error) {
120+
core.setFailed(`Error closing previousReport, #${previousReport.number}`)
121+
throw error
122+
}
123+
}
124+
}
125+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env node
2+
3+
import { program } from 'commander'
4+
import fs from 'fs'
5+
import coreLib from '@actions/core'
6+
7+
import github from '../../script/helpers/github.js'
8+
import { getEnvInputs } from './lib/get-env-inputs.js'
9+
import { createReportIssue, linkReports } from './lib/issue-report.js'
10+
11+
// [start-readme]
12+
//
13+
// This script runs once a week via a scheduled GitHub Action to lint
14+
// the entire content and data directories based on our
15+
// markdownlint.js rules.
16+
//
17+
// If errors are found, it will open up a new issue in the
18+
// docs-content repo with the label "broken content markdown report".
19+
//
20+
// The Content FR will go through the issue and update the content and
21+
// data files accordingly.
22+
//
23+
// [end-readme]
24+
25+
program
26+
.description('Opens an issue for Content FR with the errors from the weekly content/data linter.')
27+
.option(
28+
'-p, --path <path>',
29+
'provide a path to the errors output json file that will be in the issue body',
30+
)
31+
.parse(process.argv)
32+
33+
const { path } = program.opts()
34+
35+
main()
36+
async function main() {
37+
const errors = fs.readFileSync(`${path}`, 'utf8')
38+
const core = coreLib
39+
const { REPORT_REPOSITORY, REPORT_AUTHOR, REPORT_LABEL } = process.env
40+
41+
const octokit = github()
42+
// `GITHUB_TOKEN` is optional. If you need the token to post a comment
43+
// or open an issue report, you might get cryptic error messages from Octokit.
44+
getEnvInputs(['GITHUB_TOKEN'])
45+
46+
core.info(`Creating issue for errors...`)
47+
48+
const reportProps = {
49+
core,
50+
octokit,
51+
reportTitle: `Error(s) in content markdown file(s)`,
52+
reportBody: JSON.parse(errors),
53+
reportRepository: REPORT_REPOSITORY,
54+
reportLabel: REPORT_LABEL,
55+
}
56+
57+
await createReportIssue(reportProps)
58+
59+
const linkProps = {
60+
core,
61+
octokit,
62+
newReport: await createReportIssue(reportProps),
63+
reportRepository: REPORT_REPOSITORY,
64+
reportAuthor: REPORT_AUTHOR,
65+
reportLabel: REPORT_LABEL,
66+
}
67+
68+
await linkReports(linkProps)
69+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: 'Lint entire content and data markdown files'
2+
3+
# **What it does**: Lints our content markdown weekly to ensure the content matches the specified styleguide. If errors exists, it opens a PR for the Docs content team to review.
4+
# **Why we have it**: Extra precaution to run linter on the entire content/data directories.
5+
# **Who does it impact**: Docs content.
6+
7+
on:
8+
workflow_dispatch:
9+
schedule:
10+
- cron: '20 16 * * 0' # Run every day at 16:20 UTC / 8:20 PST every Sunday
11+
12+
permissions:
13+
contents: read
14+
issues: write
15+
16+
jobs:
17+
lint-entire-content-data:
18+
name: Lint entire content and data directories
19+
if: github.repository == 'github/docs-internal'
20+
runs-on: ubuntu-20.04-xl
21+
steps:
22+
- name: Check that gh CLI is installed
23+
run: gh --version
24+
25+
- name: Check out repo's default branch
26+
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
27+
28+
- name: Set up Node and dependencies
29+
uses: ./.github/actions/node-npm-setup
30+
31+
- name: Run content linter
32+
env:
33+
GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_WRITEORG_PROJECT }}
34+
REPORT_AUTHOR: docs-bot
35+
REPORT_LABEL: broken content markdown report
36+
REPORT_REPOSITORY: github/docs-content
37+
timeout-minutes: 10
38+
run: |
39+
node src/content-linter/scripts/markdownlint.js --errors-only --paths content data --output-file /tmp/error-lints.json
40+
node .github/actions-scripts/post-lints.js --path /tmp/error-lints.json

src/content-linter/scripts/markdownlint.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { program, Option } from 'commander'
33
import markdownlint from 'markdownlint'
44
import { applyFixes } from 'markdownlint-rule-helpers'
5-
import { readFile, writeFile } from 'fs/promises'
5+
import fs from 'fs'
66
import ora from 'ora'
77
import { extname } from 'path'
88
import { execSync } from 'child_process'
@@ -36,9 +36,12 @@ program
3636
`Specify rules to run. For example, by short name MD001 or long name heading-increment \n${listRules()}\n\n`,
3737
).conflicts('error'),
3838
)
39+
.addOption(
40+
new Option('-o, --output-file <filepath>', `Outputs the errors/warnings to the filepath.`),
41+
)
3942
.parse(process.argv)
4043

41-
const { fix, paths, errorsOnly, rules, summaryByRule, verbose } = program.opts()
44+
const { fix, paths, errorsOnly, rules, summaryByRule, outputFile, verbose } = program.opts()
4245
const ALL_CONTENT_DIR = ['content', 'data']
4346

4447
main()
@@ -65,21 +68,28 @@ async function main() {
6568
// Apply markdownlint fixes if available and rewrite the files
6669
if (fix) {
6770
for (const file of [...files.content, ...files.data]) {
68-
const content = await readFile(file, 'utf8')
71+
const content = fs.readFileSync(file, 'utf8')
6972
const applied = applyFixes(content, results[file])
70-
await writeFile(file, applied)
73+
fs.writeFileSync(file, applied, 'utf-8')
7174
}
7275
}
7376

7477
const errorFileCount = getErrorCountByFile(results)
75-
// Used for a temparary way to allow us to see how many errors currently
78+
// Used for a temporary way to allow us to see how many errors currently
7679
// exist for each rule in the content directory.
7780
if (summaryByRule && errorFileCount > 0) {
7881
reportSummaryByRule(results, config)
7982
} else if (errorFileCount > 0) {
80-
reportResults(results)
83+
const errorReport = getResults(results)
84+
if (outputFile) {
85+
fs.writeFileSync(`${outputFile}`, JSON.stringify(errorReport, undefined, 2), function (err) {
86+
if (err) throw err
87+
})
88+
console.log(`Output written to ${outputFile}`)
89+
} else {
90+
console.log(errorReport)
91+
}
8192
}
82-
8393
const end = Date.now()
8494
console.log(`\n🕦 Markdownlint finished in ${(end - start) / 1000} s`)
8595

@@ -154,23 +164,23 @@ function reportSummaryByRule(results, config) {
154164
console.log(JSON.stringify(ruleCount, null, 2))
155165
}
156166

157-
function reportResults(allResults) {
158-
console.log('\n\nMarkdownlint results:\n')
167+
function getResults(allResults) {
168+
const output = {}
159169
Object.entries(allResults)
160170
// Each result key always has an array value, but it may be empty
161171
.filter(([, results]) => results.length)
162172
.forEach(([key, results]) => {
163-
console.log(key)
164173
if (!verbose) {
165174
const formattedResults = results.map((flaw) => formatResult(flaw))
166175
const errors = formattedResults.filter((result) => result.severity === 'error')
167176
const warnings = formattedResults.filter((result) => result.severity === 'warning')
168177
const sortedResult = [...errors, ...warnings]
169-
console.log(sortedResult)
178+
output[key] = [...sortedResult]
170179
} else {
171-
console.log(results)
180+
output[key] = [...results]
172181
}
173182
})
183+
return output
174184
}
175185

176186
// Results are formatted with the key being the filepath

0 commit comments

Comments
 (0)