add content linter check for yaml scheduled jobs (#43024)
This commit is contained in:
@@ -10,6 +10,7 @@ import { imageAltTextExcludeStartWords } from './image-alt-text-exclude-start-wo
|
||||
import { listFirstWordCapitalization } from './list-first-word-capitalization.js'
|
||||
import { internalLinkPunctuation } from './internal-link-punctuation.js'
|
||||
import { earlyAccessReferences } from './early-access-references.js'
|
||||
import { yamlScheduledJobs } from './yaml-scheduled-jobs.js'
|
||||
|
||||
export const gitHubDocsMarkdownlint = {
|
||||
rules: [
|
||||
@@ -24,5 +25,6 @@ export const gitHubDocsMarkdownlint = {
|
||||
listFirstWordCapitalization,
|
||||
internalLinkPunctuation,
|
||||
earlyAccessReferences,
|
||||
yamlScheduledJobs,
|
||||
],
|
||||
}
|
||||
|
||||
59
src/content-linter/lib/linting-rules/yaml-scheduled-jobs.js
Normal file
59
src/content-linter/lib/linting-rules/yaml-scheduled-jobs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
import { liquid } from '#src/content-render/index.js'
|
||||
import { allVersions } from '#src/versions/lib/all-versions.js'
|
||||
import { addError, filterTokens } from 'markdownlint-rule-helpers'
|
||||
|
||||
const scheduledYamlJobs = []
|
||||
|
||||
export const yamlScheduledJobs = {
|
||||
names: ['GHD009', 'yaml-scheduled-jobs'],
|
||||
description:
|
||||
'YAML snippets that include scheduled workflows must not run on the hour and must be unique',
|
||||
tags: ['actions'],
|
||||
severity: 'error',
|
||||
asynchronous: true,
|
||||
information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'),
|
||||
function: function GHD009(params, onError) {
|
||||
filterTokens(params, 'fence', async (token) => {
|
||||
const lang = token.info.trim().split(/\s+/u).shift().toLowerCase()
|
||||
if (lang !== 'yaml' && lang !== 'yml') return
|
||||
if (!token.content.includes('schedule:')) return
|
||||
if (!token.content.includes('- cron:')) return
|
||||
|
||||
const context = {
|
||||
currentLanguage: 'en',
|
||||
currentVersionObj: allVersions['free-pro-team@latest'],
|
||||
}
|
||||
// If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags
|
||||
const renderedYaml = await liquid.parseAndRender(token.content, context)
|
||||
const yamlObj = yaml.load(renderedYaml)
|
||||
yamlObj.on.schedule.forEach((schedule) => {
|
||||
if (schedule.cron.split(' ')[0] === '0') {
|
||||
addError(
|
||||
onError,
|
||||
getLineNumber(token.content, schedule.cron) + token.lineNumber,
|
||||
`YAML scheduled workflow must not run on the hour`,
|
||||
schedule.cron,
|
||||
)
|
||||
}
|
||||
|
||||
if (scheduledYamlJobs.includes(schedule.cron)) {
|
||||
addError(
|
||||
onError,
|
||||
getLineNumber(token.content, schedule.cron) + token.lineNumber,
|
||||
`YAML scheduled workflow must be unique`,
|
||||
schedule.cron,
|
||||
)
|
||||
} else {
|
||||
scheduledYamlJobs.push(schedule.cron)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
function getLineNumber(tokenContent, schedule) {
|
||||
const contentLines = tokenContent.split('\n')
|
||||
return contentLines.findIndex((line) => line.includes(schedule)) + 1
|
||||
}
|
||||
@@ -39,6 +39,11 @@ export const githubDocsConfig = {
|
||||
severity: 'warning',
|
||||
'partial-markdown-files': true,
|
||||
},
|
||||
'yaml-scheduled-jobs': {
|
||||
// GHD009
|
||||
severity: 'error',
|
||||
'partial-markdown-files': true,
|
||||
},
|
||||
'list-first-word-capitalization': {
|
||||
// GH011
|
||||
severity: 'warning',
|
||||
|
||||
@@ -3,8 +3,8 @@ import { expect, jest } from '@jest/globals'
|
||||
import { runRule } from '../../lib/init-test.js'
|
||||
import { earlyAccessReferences } from '../../lib/linting-rules/early-access-references.js'
|
||||
|
||||
const FIXTURE_FILEPATH_NON_EA = 'src/content-linter/tests/fixutres/not-secret.md'
|
||||
const FIXTURE_FILEPATH_EA = 'src/content-linter/tests/fixutres/early-access/secret.md'
|
||||
const FIXTURE_FILEPATH_NON_EA = 'src/content-linter/tests/fixtures/not-secret.md'
|
||||
const FIXTURE_FILEPATH_EA = 'src/content-linter/tests/fixtures/early-access/secret.md'
|
||||
jest.setTimeout(20 * 1000)
|
||||
|
||||
describe(earlyAccessReferences.names.join(' - '), () => {
|
||||
|
||||
81
src/content-linter/tests/unit/yaml-scheduled-jobs.js
Normal file
81
src/content-linter/tests/unit/yaml-scheduled-jobs.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { runRule } from '../../lib/init-test.js'
|
||||
import { yamlScheduledJobs } from '../../lib/linting-rules/yaml-scheduled-jobs.js'
|
||||
|
||||
describe(yamlScheduledJobs.names.join(' - '), () => {
|
||||
test('yaml scheduled jobs in markdown do not start on the hour', async () => {
|
||||
const markdown = ['```yaml', 'on:', ' schedule:', " - cron: '0 5,17 * * *'", '```'].join(
|
||||
'\n',
|
||||
)
|
||||
const result = await runRule(yamlScheduledJobs, { strings: { markdown } })
|
||||
const errors = result.markdown
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0].lineNumber).toBe(4)
|
||||
expect(errors[0].errorContext).toEqual('0 5,17 * * *')
|
||||
expect(errors[0].fixInfo).toBeNull()
|
||||
})
|
||||
|
||||
test('yaml scheduled job in markdown does not start on the hour', async () => {
|
||||
const markdown = ['```yaml', 'on:', ' schedule:', " - cron: '30 5,17 * * *'", '```'].join(
|
||||
'\n',
|
||||
)
|
||||
const result = await runRule(yamlScheduledJobs, { strings: { markdown } })
|
||||
const errors = result.markdown
|
||||
expect(errors.length).toBe(0)
|
||||
})
|
||||
|
||||
test('yaml scheduled jobs in markdown are all unique', async () => {
|
||||
const markdown = [
|
||||
'```yaml',
|
||||
'on:',
|
||||
' schedule:',
|
||||
" - cron: '30 5,20 * * *'",
|
||||
" - cron: '30 5,20 * * *'",
|
||||
'```',
|
||||
].join('\n')
|
||||
const result = await runRule(yamlScheduledJobs, { strings: { markdown } })
|
||||
const errors = result.markdown
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0].lineNumber).toBe(4)
|
||||
expect(errors[0].errorContext).toEqual('30 5,20 * * *')
|
||||
expect(errors[0].fixInfo).toBeNull()
|
||||
})
|
||||
|
||||
test('yaml scheduled jobs in markdown are not repeated', async () => {
|
||||
const markdown = [
|
||||
'```yaml',
|
||||
'on:',
|
||||
' schedule:',
|
||||
" - cron: '30 4,20 * * *'",
|
||||
'```',
|
||||
'```yaml',
|
||||
'on:',
|
||||
' schedule:',
|
||||
" - cron: '30 3,20 * * 3'",
|
||||
'```',
|
||||
].join('\n')
|
||||
const result = await runRule(yamlScheduledJobs, { strings: { markdown } })
|
||||
const errors = result.markdown
|
||||
expect(errors.length).toBe(0)
|
||||
})
|
||||
|
||||
test('yaml scheduled jobs with liquid', async () => {
|
||||
const markdown = [
|
||||
'```yaml',
|
||||
'on:',
|
||||
' schedule:',
|
||||
" - cron: '30 1,20 * * *'",
|
||||
'jobs:',
|
||||
' upload_tool_cache:',
|
||||
' runs-on: ubuntu-22.04',
|
||||
' steps:',
|
||||
' - name: Clear any existing tool cache',
|
||||
' uses: {% data reusables.actions.action-codeql-action-init %}',
|
||||
' with:',
|
||||
' languages: {% raw %}languages{% endraw %}',
|
||||
'```',
|
||||
].join('\n')
|
||||
const result = await runRule(yamlScheduledJobs, { strings: { markdown } })
|
||||
const errors = result.markdown
|
||||
expect(errors.length).toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user