1
0
mirror of synced 2025-12-22 11:26:57 -05:00

add content linter check for yaml scheduled jobs (#43024)

This commit is contained in:
Grace Park
2023-09-25 11:59:57 -07:00
committed by GitHub
parent e5aece3a41
commit 69ac3e620c
7 changed files with 149 additions and 2 deletions

View File

@@ -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,
],
}

View 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
}

View File

@@ -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',

View File

@@ -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(' - '), () => {

View 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)
})
})