diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d59619d5ea2e..cb154f7591c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,16 +53,18 @@ jobs: { name: 'github-apps', path: 'src/github-apps/tests', }, { name: 'graphql', path: 'src/graphql/tests', }, { name: 'landings', path: 'src/landings/tests', }, - // { name: 'learning-track', path: 'src/learning-track/tests', }, + { name: 'learning-track', path: 'src/learning-track/tests', }, { name: 'linting', path: 'src/content-linter/tests', }, { name: 'observability', path: 'src/observability/tests' }, { name: 'pageinfo', path: 'src/pageinfo/tests', }, { name: 'redirects', path: 'src/redirects/tests', }, + { name: 'release-notes', path: 'src/release-notes/tests', }, { name: 'rendering', path: 'tests/rendering', }, { name: 'rendering-fixtures', path: 'tests/rendering-fixtures', }, { name: 'rest', path: 'src/rest/tests', }, { name: 'routing', path: 'tests/routing', }, { name: 'search', path: 'src/search/tests', }, + { name: 'secret-scanning', path: 'src/secret-scanning/tests',}, { name: 'shielding', path: 'src/shielding/tests', }, context.payload.repository.full_name === 'github/docs-internal' && { name: 'languages', path: 'src/languages/tests', }, diff --git a/content/apps/creating-github-apps/writing-code-for-a-github-app/building-ci-checks-with-a-github-app.md b/content/apps/creating-github-apps/writing-code-for-a-github-app/building-ci-checks-with-a-github-app.md index e3ab446a604b..f34579de31be 100644 --- a/content/apps/creating-github-apps/writing-code-for-a-github-app/building-ci-checks-with-a-github-app.md +++ b/content/apps/creating-github-apps/writing-code-for-a-github-app/building-ci-checks-with-a-github-app.md @@ -319,7 +319,6 @@ class GHAapp < Sinatra::Application # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. - # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -571,7 +570,7 @@ You can test that the server is listening to your app by triggering an event for 1. Create a new repository to use for testing your tutorial code. For more information, see "[AUTOTITLE](/repositories/creating-and-managing-repositories/creating-a-new-repository)." 1. Install the {% data variables.product.prodname_github_app %} on the repository you just created. For more information, see "[AUTOTITLE](/apps/using-github-apps/installing-your-own-github-app#installing-your-own-github-app)." During the installation process, choose **Only select repositories**, and select the repository you created in the previous step. -2. After you click **Install**, look at the output in the terminal tab where you're running `server.rb`. You should see something like this: +1. After you click **Install**, look at the output in the terminal tab where you're running `server.rb`. You should see something like this: ```shell > D, [2023-06-08T15:45:43.773077 #30488] DEBUG -- : ---- received event installation @@ -1147,7 +1146,7 @@ To push to a repository, your app must have write permissions for "Contents" in To commit files, Git must know which username and email address to associate with the commit. Next you'll add environment variables to store the name and email address that your app will use when it makes Git commits. 1. Open the `.env` file you created earlier in this tutorial. -2. Add the following environment variables to your `.env` file. Replace `APP_NAME` with the name of your app, and `EMAIL_ADDRESS` with any email you'd like to use for this example. +1. Add the following environment variables to your `.env` file. Replace `APP_NAME` with the name of your app, and `EMAIL_ADDRESS` with any email you'd like to use for this example. ```shell copy GITHUB_APP_USER_NAME="APP_NAME" @@ -1542,7 +1541,6 @@ class GHAapp < Sinatra::Application # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. - # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') diff --git a/lib/ajv-validate.js b/lib/ajv-validate.js new file mode 100644 index 000000000000..0a520e23504f --- /dev/null +++ b/lib/ajv-validate.js @@ -0,0 +1,16 @@ +import Ajv from 'ajv' +import addErrors from 'ajv-errors' +import addFormats from 'ajv-formats' +import semver from 'semver' + +const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }) +addFormats(ajv) +addErrors(ajv) +// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. *** +ajv.addFormat('semver', { + validate: (x) => semver.validRange(x), +}) + +export function ajvValidate(schema) { + return ajv.compile(schema) +} diff --git a/src/content-linter/lib/default-markdownlint-options.js b/src/content-linter/lib/default-markdownlint-options.js index 45970a0f2868..974107fff25c 100644 --- a/src/content-linter/lib/default-markdownlint-options.js +++ b/src/content-linter/lib/default-markdownlint-options.js @@ -1,11 +1,11 @@ -export function testOptions(rule, module, fixtureFile) { +export function testOptions(rule, module, strings) { const config = { default: false, [rule]: true, } const options = { - files: [fixtureFile], + strings, customRules: [module], config, } diff --git a/src/content-linter/lib/helpers/utils.js b/src/content-linter/lib/helpers/utils.js index be7596792f0f..c1f7c0388e6c 100644 --- a/src/content-linter/lib/helpers/utils.js +++ b/src/content-linter/lib/helpers/utils.js @@ -9,3 +9,12 @@ export function getRange(line, content) { const startColumnIndex = line.indexOf(content) return startColumnIndex !== -1 ? [startColumnIndex + 1, content.length] : null } + +export function isStringQuoted(text) { + // String starts with either a single or double quote + // ends with either a single or double quote + // and optionally ends with a question mark or exclamation point + // because that punctuation can exist outside of the quoted string + const regex = /^['"].*['"][?!]?$/ + return text.match(regex) +} diff --git a/src/content-linter/lib/init-test.js b/src/content-linter/lib/init-test.js index 91b84f4edb28..88e74b6ae923 100644 --- a/src/content-linter/lib/init-test.js +++ b/src/content-linter/lib/init-test.js @@ -2,7 +2,7 @@ import markdownlint from 'markdownlint' import { testOptions } from './default-markdownlint-options.js' -export async function runRule(module, fixtureFile) { - const options = testOptions(module.names[0], module, fixtureFile) +export async function runRule(module, strings) { + const options = testOptions(module.names[0], module, strings) return await markdownlint.promises.markdownlint(options) } diff --git a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js index f8a7b7ca8c86..a1889590b546 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js @@ -1,6 +1,6 @@ import { forEachInlineChild } from 'markdownlint-rule-helpers' -import { addFixErrorDetail, getRange } from '../helpers/utils.js' +import { addFixErrorDetail, getRange, isStringQuoted } from '../helpers/utils.js' export const imageAltTextEndPunctuation = { names: ['GHD002', 'image-alt-text-end-punctuation'], @@ -20,7 +20,9 @@ export const imageAltTextEndPunctuation = { ) { addFixErrorDetail(onError, token.lineNumber, imageAltText + '.', imageAltText, range, { lineNumber: token.lineNumber, - editColumn: token.line.indexOf(']') + 1, + editColumn: isStringQuoted(imageAltText) + ? token.line.indexOf(']') + : token.line.indexOf(']') + 1, deleteCount: 0, insertText: '.', }) diff --git a/src/content-linter/lib/linting-rules/internal-links-lang.js b/src/content-linter/lib/linting-rules/internal-links-lang.js index be6b3671ee24..f3f32539de70 100644 --- a/src/content-linter/lib/linting-rules/internal-links-lang.js +++ b/src/content-linter/lib/linting-rules/internal-links-lang.js @@ -1,7 +1,6 @@ import { filterTokens } from 'markdownlint-rule-helpers' - import { addFixErrorDetail, getRange } from '../helpers/utils.js' -import { languageKeys } from '#src/languages/lib/languages.js' +import { allLanguageKeys } from '#src/languages/lib/languages.js' export const internalLinksLang = { names: ['GHD005', 'internal-links-lang'], @@ -24,7 +23,7 @@ export const internalLinksLang = { .filter((attr) => attr[0] === 'href') .filter((attr) => attr[1].startsWith('/') || !attr[1].startsWith('//')) // Filter out link paths that start with language code - .filter((attr) => languageKeys.some((lang) => attr[1].split('/')[1] === lang)) + .filter((attr) => allLanguageKeys.some((lang) => attr[1].split('/')[1] === lang)) // Get the link path from the attribute .map((attr) => attr[1]) // Create errors for each link path that includes a language code diff --git a/src/content-linter/scripts/markdownlint.js b/src/content-linter/scripts/markdownlint.js index c8f723d2c900..6f4ae5712b03 100755 --- a/src/content-linter/scripts/markdownlint.js +++ b/src/content-linter/scripts/markdownlint.js @@ -46,7 +46,7 @@ main() async function main() { // If paths has not been specified, lint all files const files = getFilesToLint((summaryByRule && ALL_CONTENT_DIR) || paths || getChangedFiles()) - const spinner = ora({ text: 'Running content linter', spinner: 'simpleDots' }) + const spinner = ora({ text: 'Running content linter\n\n', spinner: 'simpleDots' }) if (!files.length) { spinner.succeed('No files to lint') diff --git a/src/content-linter/style/github-docs.js b/src/content-linter/style/github-docs.js index d9cd87274f50..633f6e5e8206 100644 --- a/src/content-linter/style/github-docs.js +++ b/src/content-linter/style/github-docs.js @@ -34,6 +34,11 @@ export const githubDocsConfig = { severity: 'error', 'partial-markdown-files': true, }, + 'no-github-docs-domains': { + // GHD020 + severity: 'error', + 'partial-markdown-files': true, + }, 'search-replace': { severity: 'error', 'severity-local-env': 'warning', @@ -45,6 +50,34 @@ export const githubDocsConfig = { search: 'TODOCS', searchScope: 'all', }, + { + name: 'docs-domain', + message: 'Catch occurrences of docs.gitub.com domain.', + search: 'docs.gitub.com', + searchScope: 'all', + }, + { + name: 'help-domain', + message: 'Catch occurrences of help.github.com domain.', + search: 'help.github.com', + searchScope: 'all', + }, + { + name: 'preview-domain', + message: 'Catch occurrences of preview.ghdocs.com domain.', + search: 'preview.ghdocs.com', + searchScope: 'all', + }, + { + name: 'developer-domain', + message: 'Catch occurrences of developer.github.com domain.', + // Do not match developer.github.com/changes or + // developer.github.com/enterprise/[0-9] or + // developer.github.com/enterprise/{{something}} (e.g. liquid). + // There are occurences that will likely always remain in the content. + searchPattern: '/developer.github.com(?!/(changes|enterprise/([0-9]|{))).*/g', + searchScope: 'all', + }, ], }, } diff --git a/src/content-linter/tests/fixtures/code-fence-line-length.md b/src/content-linter/tests/fixtures/code-fence-line-length.md deleted file mode 100644 index b0af0549729f..000000000000 --- a/src/content-linter/tests/fixtures/code-fence-line-length.md +++ /dev/null @@ -1,31 +0,0 @@ -# Heading - -Line length exceeds max length by 1 - -```shell -111 -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -bbb -``` - -Line length equals max length - -```shell -111 -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -bbbbbbb -``` - -Line length is less than max length - -```shell -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -``` - -Multiple lines in code fence exceed max length by 1 - -```shell -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaccc -1 -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb -``` diff --git a/src/content-linter/tests/fixtures/image-alt-text-end-punctuation.md b/src/content-linter/tests/fixtures/image-alt-text-end-punctuation.md deleted file mode 100644 index 896f13ee1b63..000000000000 --- a/src/content-linter/tests/fixtures/image-alt-text-end-punctuation.md +++ /dev/null @@ -1,15 +0,0 @@ -# hi - -![GitHub Documentation is here](./image.png) - -![GitHub Documentation is found on this site.](./image.png) - -GitHub Documentation's logo looks like this: ![logo of GitHub Docs?](./image.png) over here. - -!["image".](./image.png) -!["image!"](./image.png) -!["image"!](./image.png) -!["image?"](./image.png) -!["image"?](./image.png) -!["image."](./image.png) -!["image"](./image.png) diff --git a/src/content-linter/tests/fixtures/image-alt-text-exclude-start-words.md b/src/content-linter/tests/fixtures/image-alt-text-exclude-start-words.md deleted file mode 100644 index 03d7372c011e..000000000000 --- a/src/content-linter/tests/fixtures/image-alt-text-exclude-start-words.md +++ /dev/null @@ -1,6 +0,0 @@ -![This is ok image](/images/this-is-ok.png) -![Image with alt text](/images/image-with-alt-text.png) -![image with alt text](/images/image-with-alt-text.png) -![Graphic with alt text](/images/graphic-with-alt-text.png) -![graphic with alt text](/images/graphic-with-alt-text.png) -![This is ok grapic](/images/this-is-ok.png) diff --git a/src/content-linter/tests/fixtures/image-alt-text-length.md b/src/content-linter/tests/fixtures/image-alt-text-length.md deleted file mode 100644 index 2fef08d2459d..000000000000 --- a/src/content-linter/tests/fixtures/image-alt-text-length.md +++ /dev/null @@ -1,7 +0,0 @@ -![012345678901234567890123456789012345678](./image.png) - -![0123456789012345678901234567890123456789](./image.png) - -![012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789](./image.png) - -![0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891](./image.png) diff --git a/src/content-linter/tests/fixtures/image-file-kebab.md b/src/content-linter/tests/fixtures/image-file-kebab.md deleted file mode 100644 index 0b7130d34393..000000000000 --- a/src/content-linter/tests/fixtures/image-file-kebab.md +++ /dev/null @@ -1,7 +0,0 @@ -# Header - -![Image.](image-file.jpg) -![Image.](/path/to/imageFile.jpg) -![Image.](image_file.jpg) -![Image.](imageFile-Location.png) -![Image.](image-file-Location.jpg) diff --git a/src/content-linter/tests/fixtures/internal-links-lang.md b/src/content-linter/tests/fixtures/internal-links-lang.md deleted file mode 100644 index b92004bba7fa..000000000000 --- a/src/content-linter/tests/fixtures/internal-links-lang.md +++ /dev/null @@ -1,21 +0,0 @@ -# English Docs - -[English Docs](/en/docs) - -This gets caught by the internal-links-slashes rule -[Internal Link Fail Docs](en/docs) - -## Documentation - -These are the [Docs](//ja/actions) we need. - -## Korean Documentation - -This is the [Korean Docs](/actions) - -Should not catch links to enterprise as an english prefix -[Enterprise](/enterprise/overview) - -Hello [GitHub Actions](//example.com) - -[Link to just a landing page in english](/en) diff --git a/src/content-linter/tests/fixtures/internal-links-slash.md b/src/content-linter/tests/fixtures/internal-links-slash.md deleted file mode 100644 index 7c054104c403..000000000000 --- a/src/content-linter/tests/fixtures/internal-links-slash.md +++ /dev/null @@ -1,12 +0,0 @@ -# hi - -Hello [GitHub Actions](/actions/index.md) -[Anchor on page](#anchor-on-page) -[GitHub Actions Quickstart](actions/quickstart.md) -[External Link](https://git-scm.com/) -[External link](http://example.com) -[External Link](mailto:email@example.com) - -## Anchor on page - -Stuff diff --git a/src/content-linter/tests/lint-code-languages.js b/src/content-linter/tests/lint-code-languages.js deleted file mode 100644 index bcf22d99a30a..000000000000 --- a/src/content-linter/tests/lint-code-languages.js +++ /dev/null @@ -1,18 +0,0 @@ -// Code languages must be listed in data/variables/code-languages.yml -import fs from 'fs' -import walkFiles from '../../../script/helpers/walk-files.js' -import yaml from 'js-yaml' - -const allFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md')) -const languages = Object.keys( - yaml.load(fs.readFileSync('./data/variables/code-languages.yml', 'utf8')), -) - -describe.skip('lint-code-languages', () => { - test.each(allFiles)('%s', async (file) => { - const fileContents = await fs.promises.readFile(file, 'utf8') - for (const [, lang] of fileContents.matchAll(/```(\S+)/gm)) { - expect(languages).toContain(lang) - } - }) -}) diff --git a/src/content-linter/tests/lint-files.js b/src/content-linter/tests/lint-files.js index 871e451dfbc0..0ba8e7aeab9c 100755 --- a/src/content-linter/tests/lint-files.js +++ b/src/content-linter/tests/lint-files.js @@ -4,23 +4,16 @@ import slash from 'slash' import walk from 'walk-sync' import { zip } from 'lodash-es' import yaml from 'js-yaml' -import Ajv from 'ajv' -import addErrors from 'ajv-errors' -import addFormats from 'ajv-formats' import { fromMarkdown } from 'mdast-util-from-markdown' import { visit } from 'unist-util-visit' import fs from 'fs/promises' import { existsSync } from 'fs' -import semver from 'semver' import { jest } from '@jest/globals' import { frontmatter, deprecatedProperties } from '../../../lib/frontmatter.js' import languages from '#src/languages/lib/languages.js' -import releaseNotesSchema from '../lib/release-notes-schema.js' -import learningTracksSchema from '../lib/learning-tracks-schema.js' import { liquid } from '#src/content-render/index.js' import { getDiffFiles } from '../lib/diff-files.js' -import { formatAjvErrors } from '../../../tests/helpers/schemas.js' jest.useFakeTimers({ legacyFakeTimers: true }) @@ -31,9 +24,6 @@ const contentDir = path.join(rootDir, 'content') const reusablesDir = path.join(rootDir, 'data/reusables') const variablesDir = path.join(rootDir, 'data/variables') const glossariesDir = path.join(rootDir, 'data/glossaries') -const ghesReleaseNotesDir = path.join(rootDir, 'data/release-notes/enterprise-server') -const ghaeReleaseNotesDir = path.join(rootDir, 'data/release-notes/github-ae') -const learningTracks = path.join(rootDir, 'data/learning-tracks') const fbvDir = path.join(rootDir, 'data/features') const languageCodes = Object.keys(languages) @@ -217,7 +207,7 @@ const yamlWalkOptions = { } // different lint rules apply to different content types -let mdToLint, ymlToLint, ghesReleaseNotesToLint, ghaeReleaseNotesToLint, learningTracksToLint +let mdToLint, ymlToLint // compile lists of all the files we want to lint @@ -294,35 +284,11 @@ const FbvYamlAbsPaths = walk(fbvDir, yamlWalkOptions).sort() const FbvYamlRelPaths = FbvYamlAbsPaths.map((p) => slash(path.relative(rootDir, p))) const fbvTuples = zip(FbvYamlRelPaths, FbvYamlAbsPaths) -// GHES release notes -const ghesReleaseNotesYamlAbsPaths = walk(ghesReleaseNotesDir, yamlWalkOptions).sort() -const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths.map((p) => - slash(path.relative(rootDir, p)), -) -ghesReleaseNotesToLint = zip(ghesReleaseNotesYamlRelPaths, ghesReleaseNotesYamlAbsPaths) - -// GHAE release notes -const ghaeReleaseNotesYamlAbsPaths = walk(ghaeReleaseNotesDir, yamlWalkOptions).sort() -const ghaeReleaseNotesYamlRelPaths = ghaeReleaseNotesYamlAbsPaths.map((p) => - slash(path.relative(rootDir, p)), -) -ghaeReleaseNotesToLint = zip(ghaeReleaseNotesYamlRelPaths, ghaeReleaseNotesYamlAbsPaths) - -// Learning tracks -const learningTracksYamlAbsPaths = walk(learningTracks, yamlWalkOptions).sort() -const learningTracksYamlRelPaths = learningTracksYamlAbsPaths.map((p) => - slash(path.relative(rootDir, p)), -) -learningTracksToLint = zip(learningTracksYamlRelPaths, learningTracksYamlAbsPaths) - // Put all the yaml files together ymlToLint = [].concat( variableYamlTuples, // These "tuples" not tested independently; they are only tested as part of ymlToLint. glossariesYamlTuples, fbvTuples, - ghesReleaseNotesToLint, - ghaeReleaseNotesToLint, - learningTracksToLint, ) function formatLinkError(message, links) { @@ -361,19 +327,9 @@ if (diffFiles.length > 0) { ) mdToLint = filterFiles(mdToLint) ymlToLint = filterFiles(ymlToLint) - ghesReleaseNotesToLint = filterFiles(ghesReleaseNotesToLint) - ghaeReleaseNotesToLint = filterFiles(ghaeReleaseNotesToLint) - learningTracksToLint = filterFiles(learningTracksToLint) } -if ( - mdToLint.length + - ymlToLint.length + - ghesReleaseNotesToLint.length + - ghaeReleaseNotesToLint.length + - learningTracksToLint.length < - 1 -) { +if (mdToLint.length + ymlToLint.length < 1) { // With this in place, at least one `test()` is called and you don't // get the `Your test suite must contain at least one test.` error // from `jest`. @@ -382,18 +338,6 @@ if ( }) } -// ajv for schema validation tests -const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }) -addFormats(ajv) -addErrors(ajv) -// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. *** -ajv.addFormat('semver', { - validate: (x) => semver.validRange(x), -}) -// *** End TODO *** -const ghesValidate = ajv.compile(releaseNotesSchema) -const learningTracksValidate = ajv.compile(learningTracksSchema) - describe('lint markdown content', () => { if (mdToLint.length < 1) return @@ -881,167 +825,3 @@ describe('lint yaml content', () => { }) }) }) - -describe('lint GHES release notes', () => { - if (ghesReleaseNotesToLint.length < 1) return - describe.each(ghesReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => { - let dictionary - let dictionaryError = false - - beforeAll(async () => { - const fileContents = await fs.readFile(yamlAbsPath, 'utf8') - try { - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - } catch (error) { - dictionaryError = error - } - }) - - it('can be parsed as a single yaml document', () => { - expect(dictionaryError).toBe(false) - }) - - it('matches the schema', () => { - const valid = ghesValidate(dictionary) - let errors - - if (!valid) { - errors = formatAjvErrors(ghesValidate.errors) - } - - expect(valid, errors).toBe(true) - }) - - it('contains valid liquid', () => { - const { intro, sections } = dictionary - let toLint = { intro } - for (const key in sections) { - const section = sections[key] - const label = `sections.${key}` - section.forEach((part) => { - if (Array.isArray(part)) { - toLint = { ...toLint, ...{ [label]: section.join('\n') } } - } else { - for (const prop in section) { - toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } - } - } - }) - } - - for (const key in toLint) { - if (!toLint[key]) continue - expect(() => liquid.parse(toLint[key]), `${key} contains invalid liquid`).not.toThrow() - } - }) - }) -}) - -describe('lint GHAE release notes', () => { - if (ghaeReleaseNotesToLint.length < 1) return - const currentWeeksFound = [] - describe.each(ghaeReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => { - let dictionary - let dictionaryError = false - - beforeAll(async () => { - const fileContents = await fs.readFile(yamlAbsPath, 'utf8') - try { - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - } catch (error) { - dictionaryError = error - } - }) - - it('can be parsed as a single yaml document', () => { - expect(dictionaryError).toBe(false) - }) - - it('matches the schema', () => { - const valid = ghesValidate(dictionary) - let errors - - if (!valid) { - errors = formatAjvErrors(ghesValidate.errors) - } - - expect(valid, errors).toBe(true) - }) - - it('does not have more than one yaml file with currentWeek set to true', () => { - if (dictionary.currentWeek) currentWeeksFound.push(yamlRelPath) - const errorMessage = `Found more than one file with currentWeek set to true: ${currentWeeksFound.join( - '\n', - )}` - expect(currentWeeksFound.length, errorMessage).not.toBeGreaterThan(1) - }) - - it('contains valid liquid', () => { - const { intro, sections } = dictionary - let toLint = { intro } - for (const key in sections) { - const section = sections[key] - const label = `sections.${key}` - section.forEach((part) => { - if (Array.isArray(part)) { - toLint = { ...toLint, ...{ [label]: section.join('\n') } } - } else { - for (const prop in section) { - toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } - } - } - }) - } - - for (const key in toLint) { - if (!toLint[key]) continue - expect(() => liquid.parse(toLint[key]), `${key} contains invalid liquid`).not.toThrow() - } - }) - }) -}) - -describe('lint learning tracks', () => { - if (learningTracksToLint.length < 1) return - - describe.each(learningTracksToLint)('%s', (yamlRelPath, yamlAbsPath) => { - let dictionary - let dictionaryError = false - - beforeAll(async () => { - const fileContents = await fs.readFile(yamlAbsPath, 'utf8') - try { - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - } catch (error) { - dictionaryError = error - } - }) - - it('can be parsed as a single yaml document', () => { - expect(dictionaryError).toBe(false) - }) - - it('matches the schema', () => { - const valid = learningTracksValidate(dictionary) - let errors - - if (!valid) { - errors = formatAjvErrors(learningTracksValidate.errors) - } - - expect(valid, errors).toBe(true) - }) - - it('contains valid liquid', () => { - const toLint = [] - Object.values(dictionary).forEach(({ title, description }) => { - toLint.push(title) - toLint.push(description) - }) - - toLint.forEach((element) => { - expect(() => liquid.parse(element), `${element} contains invalid liquid`).not.toThrow() - }) - }) - }) -}) diff --git a/src/content-linter/tests/lint-secret-scanning-data.js b/src/content-linter/tests/lint-secret-scanning-data.js deleted file mode 100644 index 49d106406792..000000000000 --- a/src/content-linter/tests/lint-secret-scanning-data.js +++ /dev/null @@ -1,41 +0,0 @@ -import fs from 'fs' -import yaml from 'js-yaml' -import { get } from 'lodash-es' -import Ajv from 'ajv' -import addErrors from 'ajv-errors' -import semver from 'semver' - -import schema from '../lib/secret-scanning-schema.js' - -const data = yaml.load(fs.readFileSync('data/secret-scanning.yml', 'utf8')) - -const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }) -addErrors(ajv) - -// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. *** -ajv.addFormat('semver', { - validate: (x) => semver.validRange(x), -}) -// *** End TODO *** - -const validate = ajv.compile(schema) - -test('make sure secret scanning data matches the schema', () => { - validate(data) - - const errors = (validate.errors || []).map((errorObj) => { - // We have to use AJV's instancePath, which is an index number, to find out which entries are invalid. - const split = errorObj.instancePath.split('/') - split.shift() - const index = split.shift() - const entry = data[index] - const path = split.length ? split.join('.') : null - return path - ? `The entry with provider '${entry.provider}' (at '${path}: ${get(entry, path)}') ${ - errorObj.message - }` - : `The entry with provider '${entry.provider}' ${errorObj.message}` - }) - - expect(errors.length, errors.join('\n ')).toBe(0) -}) diff --git a/src/content-linter/tests/unit/code-fence-line-length.js b/src/content-linter/tests/unit/code-fence-line-length.js index 2d5587bf8ef3..94cf1ea3250e 100644 --- a/src/content-linter/tests/unit/code-fence-line-length.js +++ b/src/content-linter/tests/unit/code-fence-line-length.js @@ -5,25 +5,48 @@ import { codeFenceLineLength } from '../../lib/linting-rules/code-fence-line-len jest.setTimeout(60 * 1000) -const fixtureFilePath = 'src/content-linter/tests/fixtures/code-fence-line-length.md' -const result = await runRule(codeFenceLineLength, fixtureFilePath) -const errors = result[fixtureFilePath] - describe(codeFenceLineLength.names.join(' - '), () => { - test('line length of max length + 1 fails', async () => { - expect(errors.map((error) => error.lineNumber).includes(7)).toBe(true) - }) - test('line length equals max length passes', async () => { - expect(errors.map((error) => error.lineNumber).includes(15)).toBe(false) + test('line length of max + 1 fails', async () => { + const markdown = [ + '```shell', + '111', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbb', + '```', + ].join('\n') + const result = await runRule(codeFenceLineLength, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(1) + expect(errors[0].lineNumber).toBe(3) + expect(errors[0].errorRange).toEqual([1, 61]) + expect(errors[0].fixInfo).toBeNull() }) - test('line length less than max length passes', async () => { - expect(errors.map((error) => error.lineNumber).includes(22)).toBe(false) + test('line length less than or equal to max length passes', async () => { + const markdown = [ + '```javascript', + '111', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + '```', + ].join('\n') + const result = await runRule(codeFenceLineLength, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) test('multiple lines in code block that exceed max length fail', async () => { - expect(errors.map((error) => error.lineNumber).includes(28)).toBe(true) - expect(errors.map((error) => error.lineNumber).includes(30)).toBe(true) - }) - test('errors only occur on expected lines', async () => { - expect(errors.length).toBe(3) + const markdown = [ + '```', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaccc', + '1', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb', + '```', + ].join('\n') + const result = await runRule(codeFenceLineLength, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(2) + expect(errors[0].lineNumber).toBe(2) + expect(errors[1].lineNumber).toBe(4) + expect(errors[0].errorRange).toEqual([1, 61]) + expect(errors[1].errorRange).toEqual([1, 61]) }) }) diff --git a/src/content-linter/tests/unit/image-alt-text-end-punctuation.js b/src/content-linter/tests/unit/image-alt-text-end-punctuation.js index 49571646e4af..45c3e7d42991 100644 --- a/src/content-linter/tests/unit/image-alt-text-end-punctuation.js +++ b/src/content-linter/tests/unit/image-alt-text-end-punctuation.js @@ -5,14 +5,52 @@ import { imageAltTextEndPunctuation } from '../../lib/linting-rules/image-alt-te jest.setTimeout(60 * 1000) -const fixtureFile = 'src/content-linter/tests/fixtures/image-alt-text-end-punctuation.md' -const result = await runRule(imageAltTextEndPunctuation, fixtureFile) -const errors = result[fixtureFile] - describe(imageAltTextEndPunctuation.names.join(' - '), () => { - test('image alt text must have an end punctuation', () => { - expect(Object.keys(result).length).toBe(1) + test('image alt text without end punctutation errors', async () => { + const markdown = [ + '# Heading', + '', + '![GitHub Documentation is here](./image.png)', + '', + '!["image"](./image.png)', + ].join('\n') + const result = await runRule(imageAltTextEndPunctuation, { markdown }) + const errors = result.markdown expect(errors.length).toBe(2) - expect(errors.map((error) => error.lineNumber)).toEqual([3, 15]) + expect(errors[0].lineNumber).toBe(3) + expect(errors[1].lineNumber).toBe(5) + expect(errors[0].errorRange).toEqual([3, 28]) + expect(errors[1].errorRange).toEqual([3, 7]) + expect(errors[0].fixInfo).toEqual({ + lineNumber: 3, + editColumn: 31, + deleteCount: 0, + insertText: '.', + }) + expect(errors[1].fixInfo).toEqual({ + lineNumber: 5, + editColumn: 9, + deleteCount: 0, + insertText: '.', + }) + }) + test('image alt text with end punctutation passes', async () => { + const markdown = [ + '# Heading', + '', + '![GitHub Documentation is found on this site.](./image.png)', + '', + "GitHub Documentation's logo looks like this: ![logo of GitHub Docs?](./image.png) over here.", + '', + '!["image".](./image.png)', + '!["image!"](./image.png)', + '!["image"!](./image.png)', + '!["image?"](./image.png)', + '!["image"?](./image.png)', + '!["image."](./image.png)', + ].join('\n') + const result = await runRule(imageAltTextEndPunctuation, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/content-linter/tests/unit/image-alt-text-exclude-start-words.js b/src/content-linter/tests/unit/image-alt-text-exclude-start-words.js index 241b45c00480..087563bba835 100644 --- a/src/content-linter/tests/unit/image-alt-text-exclude-start-words.js +++ b/src/content-linter/tests/unit/image-alt-text-exclude-start-words.js @@ -5,14 +5,31 @@ import { imageAltTextExcludeStartWords } from '../../lib/linting-rules/image-alt jest.setTimeout(60 * 1000) -const fixtureFile = 'src/content-linter/tests/fixtures/image-alt-text-exclude-start-words.md' -const result = await runRule(imageAltTextExcludeStartWords, fixtureFile) -const errors = result[fixtureFile] - describe(imageAltTextExcludeStartWords.names.join(' - '), () => { - test('image alt text does not start with exclude words', () => { - expect(Object.keys(result).length).toBe(1) + test('image alt text that starts with exclude words fails', async () => { + const markdown = [ + '![Image with alt text](/images/image-with-alt-text.png)', + '![image with alt text](/images/image-with-alt-text.png)', + '![Graphic with alt text](/images/graphic-with-alt-text.png)', + '![graphic with alt text](/images/graphic-with-alt-text.png)', + ].join('\n') + const result = await runRule(imageAltTextExcludeStartWords, { markdown }) + const errors = result.markdown expect(errors.length).toBe(4) - expect(errors.map((error) => error.lineNumber)).toEqual([2, 3, 4, 5]) + expect(errors[0].lineNumber).toBe(1) + expect(errors[1].lineNumber).toBe(2) + expect(errors[2].lineNumber).toBe(3) + expect(errors[3].lineNumber).toBe(4) + expect(errors[0].errorRange).toEqual([3, 19]) + expect(errors[2].errorRange).toEqual([3, 21]) + }) + test('image alt text with no start exclude words passes', async () => { + const markdown = [ + '![This is ok image](/images/this-is-ok.png)', + '![This is ok grapic](/images/this-is-ok.png)', + ].join('\n') + const result = await runRule(imageAltTextExcludeStartWords, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/content-linter/tests/unit/image-alt-text-length.js b/src/content-linter/tests/unit/image-alt-text-length.js index 7e9c30967f76..04e605101a8a 100644 --- a/src/content-linter/tests/unit/image-alt-text-length.js +++ b/src/content-linter/tests/unit/image-alt-text-length.js @@ -5,14 +5,27 @@ import { incorrectAltTextLength } from '../../lib/linting-rules/image-alt-text-l jest.setTimeout(60 * 1000) -const fixtureFile = 'src/content-linter/tests/fixtures/image-alt-text-length.md' -const result = await runRule(incorrectAltTextLength, fixtureFile) -const errors = result[fixtureFile] - describe(incorrectAltTextLength.names.join(' - '), () => { - test('image with correct length alt text', () => { - expect(Object.keys(result).length).toBe(1) + test('image with incorrect alt text length fails', async () => { + const markdown = [ + '![012345678901234567890123456789012345678](./image.png)', + '![0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891](./image.png)', + ].join('\n') + const result = await runRule(incorrectAltTextLength, { markdown }) + const errors = result.markdown expect(errors.length).toBe(2) - expect(errors.map((error) => error.lineNumber)).toEqual([1, 7]) + expect(errors[0].lineNumber).toBe(1) + expect(errors[1].lineNumber).toBe(2) + expect(errors[0].errorRange).toEqual([3, 39]) + expect(errors[1].errorRange).toEqual([3, 151]) + }) + test('image with correct lenght alt test passes', async () => { + const markdown = [ + '![0123456789012345678901234567890123456789](./image.png)', + '![012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789](./image.png)', + ].join('\n') + const result = await runRule(incorrectAltTextLength, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/content-linter/tests/unit/image-file-kebab.js b/src/content-linter/tests/unit/image-file-kebab.js index b75762b7509c..2814258f29eb 100644 --- a/src/content-linter/tests/unit/image-file-kebab.js +++ b/src/content-linter/tests/unit/image-file-kebab.js @@ -5,14 +5,27 @@ import { imageFileKebab } from '../../lib/linting-rules/image-file-kebab' jest.setTimeout(20 * 1000) -const fixtureFile = 'src/content-linter/tests/fixtures/image-file-kebab.md' -const result = await runRule(imageFileKebab, fixtureFile) -const errors = result[fixtureFile] - describe(imageFileKebab.names.join(' - '), () => { - test('image file with lowercase kebab case', () => { - expect(Object.keys(result).length).toBe(1) + test('image file not using lowercase kebab case fails', async () => { + const markdown = [ + '# Heading', + '', + '![Image.](/path/to/imageFile.jpg)', + '![Image.](image_file.jpg)', + '![Image.](imageFile-Location.png)', + '![Image.](image-file-Location.jpg)', + ].join('\n') + const result = await runRule(imageFileKebab, { markdown }) + const errors = result.markdown expect(errors.length).toBe(4) - expect(errors.map((error) => error.lineNumber)).toEqual([4, 5, 6, 7]) + expect(errors.map((error) => error.lineNumber)).toEqual([3, 4, 5, 6]) + expect(errors[0].errorRange).toEqual([20, 9]) + expect(errors[1].errorRange).toEqual([11, 10]) + }) + test('image file using lowercase kebab case passes', async () => { + const markdown = ['![Image.](image-file.jpg)'].join('\n') + const result = await runRule(imageFileKebab, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/content-linter/tests/unit/internal-links-lang.js b/src/content-linter/tests/unit/internal-links-lang.js index 88a6bc839377..517d0f46763d 100644 --- a/src/content-linter/tests/unit/internal-links-lang.js +++ b/src/content-linter/tests/unit/internal-links-lang.js @@ -4,14 +4,37 @@ import { runRule } from '../../lib/init-test.js' import { internalLinksLang } from '../../lib/linting-rules/internal-links-lang.js' jest.setTimeout(30 * 1000) -const fixtureFilePath = 'src/content-linter/tests/fixtures/internal-links-lang.md' -const result = await runRule(internalLinksLang, fixtureFilePath) -const errors = result[fixtureFilePath] describe(internalLinksLang.names.join(' - '), () => { - test('internal links and hardcoded language codes', () => { - expect(Object.keys(result).length).toBe(1) - expect(errors.length).toBe(2) - expect(errors.map((error) => error.lineNumber)).toEqual([3, 21]) + test('internal links with hardcoded language codes fail', async () => { + const markdown = [ + '[English Docs](/en/docs)', + '[Link to just a landing page in english](/en)', + '[Korean Docs](/ko/actions)', + ].join('\n') + const result = await runRule(internalLinksLang, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(3) + expect(errors.map((error) => error.lineNumber)).toEqual([1, 2, 3]) + expect(errors[0].errorRange).toEqual([16, 8]) + expect(errors[1].errorRange).toEqual([42, 3]) + expect(errors[2].errorRange).toEqual([15, 11]) + expect(errors[0].fixInfo).toEqual({ deleteCount: 3, editColumn: 16, lineNumber: 1 }) + expect(errors[1].fixInfo).toEqual({ deleteCount: 3, editColumn: 42, lineNumber: 2 }) + expect(errors[2].fixInfo).toEqual({ deleteCount: 3, editColumn: 15, lineNumber: 3 }) + }) + test('internal links with no hardcoded language codes pass', async () => { + const markdown = [ + // This is caught by the internal-links-slashes rule + '[Internal Link Fail Docs](en/docs)', + // a // means the link is external + 'These are the [Docs](//ja/actions) we need.', + 'This is the [actions Docs](/actions)', + // A link that starts with a language code + '[Enterprise](/enterprise/overview)', + ].join('\n') + const result = await runRule(internalLinksLang, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/content-linter/tests/unit/internal-links-slash.js b/src/content-linter/tests/unit/internal-links-slash.js index ea169fc0b404..2902ef6a5e90 100755 --- a/src/content-linter/tests/unit/internal-links-slash.js +++ b/src/content-linter/tests/unit/internal-links-slash.js @@ -5,14 +5,45 @@ import { internalLinksSlash } from '../../lib/linting-rules/internal-links-slash jest.setTimeout(60 * 1000) -const fixtureFile = 'src/content-linter/tests/fixtures/internal-links-slash.md' -const result = await runRule(internalLinksSlash, fixtureFile) -const errors = result[fixtureFile] - describe(internalLinksSlash.names.join(' - '), () => { - test('relative links start with /', () => { - expect(Object.keys(result).length).toBe(1) - expect(errors.length).toBe(1) - expect(errors.map((error) => error.lineNumber)).toEqual([5]) + test('relative links that do not start with / fail', async () => { + const markdown = [ + '# heading', + '[GitHub Actions Quickstart](actions/quickstart.md)', + '', + '[GitHub Actions Quickstart](en/quickstart.md)', + ].join('\n') + const result = await runRule(internalLinksSlash, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(2) + expect(errors.map((error) => error.lineNumber)).toEqual([2, 4]) + expect(errors[0].errorRange).toEqual([29, 21]) + expect(errors[1].errorRange).toEqual([29, 16]) + expect(errors[0].fixInfo).toEqual({ + deleteCount: 0, + editColumn: 29, + insertText: '/', + lineNumber: 2, + }) + expect(errors[1].fixInfo).toEqual({ + deleteCount: 0, + editColumn: 29, + insertText: '/', + lineNumber: 4, + }) + }) + test('relative links that start with / pass', async () => { + const markdown = [ + 'Hello [GitHub Actions](/actions/index.md)', + // Not a relative page link + '[Anchor on page](#anchor-on-page)', + // Not internal links + '[External Link](https://git-scm.com/)', + '[External link](http://example.com)', + '[External Link](mailto:email@example.com)', + ].join('\n') + const result = await runRule(internalLinksSlash, { markdown }) + const errors = result.markdown + expect(errors.length).toBe(0) }) }) diff --git a/src/languages/lib/languages.js b/src/languages/lib/languages.js index de04af779534..c2e8aeb714f4 100644 --- a/src/languages/lib/languages.js +++ b/src/languages/lib/languages.js @@ -45,7 +45,7 @@ function getRoot(languageCode) { // Languages in order of accept-language header frequency // 92BD1212-61B8-4E7A: Remove `wip: Boolean` for the public ship of ko, fr, de, ru -const languages = { +const allLanguages = { en: { name: 'English', code: 'en', @@ -122,6 +122,13 @@ const languages = { wip: false, }, } +// Some markdownlint tests depend on having access to all +// language keys. Not modifying the original object makes +// it possible to export all keys, even when those directories +// don't exist on disk. +Object.freeze(allLanguages) +export const allLanguageKeys = Object.keys(allLanguages) +const languages = { ...allLanguages } if (TRANSLATIONS_FIXTURE_ROOT) { // Keep all languages that have a directory in the fixture root. diff --git a/src/content-linter/lib/learning-tracks-schema.js b/src/learning-track/lib/learning-tracks-schema.js similarity index 100% rename from src/content-linter/lib/learning-tracks-schema.js rename to src/learning-track/lib/learning-tracks-schema.js diff --git a/src/learning-track/tests/validate-schema.js b/src/learning-track/tests/validate-schema.js new file mode 100644 index 000000000000..bb5d263dcc21 --- /dev/null +++ b/src/learning-track/tests/validate-schema.js @@ -0,0 +1,56 @@ +import yaml from 'js-yaml' +import { readFile } from 'fs/promises' +import walk from 'walk-sync' +import { jest } from '@jest/globals' + +import { liquid } from '#src/content-render/index.js' +import learningTracksSchema from '../lib/learning-tracks-schema.js' +import { formatAjvErrors } from '../../../tests/helpers/schemas.js' +import { ajvValidate } from '../../../lib/ajv-validate.js' + +const learningTrackRootPath = 'data/learning-tracks' +const jsonValidator = ajvValidate(learningTracksSchema) +const yamlWalkOptions = { + globs: ['**/*.yml'], + directories: false, + includeBasePath: true, +} +const yamlFileList = walk(learningTrackRootPath, yamlWalkOptions).sort() + +jest.useFakeTimers({ legacyFakeTimers: true }) + +describe('lint learning tracks', () => { + if (yamlFileList.length < 1) return + + describe.each(yamlFileList)('%s', (yamlAbsPath) => { + let yamlContent + + beforeAll(async () => { + const fileContents = await readFile(yamlAbsPath, 'utf8') + yamlContent = await yaml.load(fileContents) + }) + + it('matches the schema', () => { + const valid = jsonValidator(yamlContent) + let errors + + if (!valid) { + errors = formatAjvErrors(jsonValidator.errors) + } + + expect(valid, errors).toBe(true) + }) + + it('contains valid liquid', () => { + const toLint = [] + Object.values(yamlContent).forEach(({ title, description }) => { + toLint.push(title) + toLint.push(description) + }) + + toLint.forEach((element) => { + expect(() => liquid.parse(element), `${element} contains invalid liquid`).not.toThrow() + }) + }) + }) +}) diff --git a/src/content-linter/lib/release-notes-schema.js b/src/release-notes/lib/release-notes-schema.js similarity index 100% rename from src/content-linter/lib/release-notes-schema.js rename to src/release-notes/lib/release-notes-schema.js diff --git a/src/release-notes/tests/validate-schema.js b/src/release-notes/tests/validate-schema.js new file mode 100644 index 000000000000..d4e901987190 --- /dev/null +++ b/src/release-notes/tests/validate-schema.js @@ -0,0 +1,42 @@ +import yaml from 'js-yaml' +import { readFile } from 'fs/promises' +import walk from 'walk-sync' +import { jest } from '@jest/globals' + +import releaseNotesSchema from '../lib/release-notes-schema.js' +import { formatAjvErrors } from '../../../tests/helpers/schemas.js' +import { ajvValidate } from '../../../lib/ajv-validate.js' + +const ghesReleaseNoteRootPath = 'data/release-notes' +const jsonValidator = ajvValidate(releaseNotesSchema) +const yamlWalkOptions = { + globs: ['**/*.yml'], + directories: false, + includeBasePath: true, +} +const yamlFileList = walk(ghesReleaseNoteRootPath, yamlWalkOptions).sort() + +jest.useFakeTimers({ legacyFakeTimers: true }) + +describe('lint enterprise release notes', () => { + if (yamlFileList.length < 1) return + describe.each(yamlFileList)('%s', (yamlAbsPath) => { + let yamlContent + + beforeAll(async () => { + const fileContents = await readFile(yamlAbsPath, 'utf8') + yamlContent = yaml.load(fileContents) + }) + + it('matches the schema', () => { + const valid = jsonValidator(yamlContent) + let errors + + if (!valid) { + errors = formatAjvErrors(jsonValidator.errors) + } + + expect(valid, errors).toBe(true) + }) + }) +}) diff --git a/src/release-notes/tests/yaml.js b/src/release-notes/tests/yaml.js new file mode 100644 index 000000000000..be6ec70404b4 --- /dev/null +++ b/src/release-notes/tests/yaml.js @@ -0,0 +1,63 @@ +import yaml from 'js-yaml' +import { readFile } from 'fs/promises' +import walk from 'walk-sync' +import path from 'path' +import { jest } from '@jest/globals' + +import { liquid } from '#src/content-render/index.js' + +const ghesReleaseNoteRootPath = 'data/release-notes' +const yamlWalkOptions = { + globs: ['**/*.yml'], + directories: false, + includeBasePath: true, +} +const yamlFileList = walk(ghesReleaseNoteRootPath, yamlWalkOptions).sort() + +jest.useFakeTimers({ legacyFakeTimers: true }) + +describe('lint enterprise release notes', () => { + if (yamlFileList.length < 1) return + describe.each(yamlFileList)('%s', (yamlAbsPath) => { + let yamlContent + const relativePath = path.relative('', yamlAbsPath) + + beforeAll(async () => { + const fileContents = await readFile(yamlAbsPath, 'utf8') + yamlContent = yaml.load(fileContents) + }) + + it('contains valid liquid', () => { + const { intro, sections } = yamlContent + let toLint = { intro } + for (const key in sections) { + const section = sections[key] + const label = `sections.${key}` + section.forEach((part) => { + if (Array.isArray(part)) { + toLint = { ...toLint, ...{ [label]: section.join('\n') } } + } else { + for (const prop in section) { + toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } + } + } + }) + } + + for (const key in toLint) { + if (!toLint[key]) continue + expect(() => liquid.parse(toLint[key]), `${key} contains invalid liquid`).not.toThrow() + } + }) + + const currentWeeksFound = [] + it('does not have more than one yaml file with currentWeek set to true', () => { + if (!yamlAbsPath.includes('data/release-notes/github-ae')) return + if (yamlContent.currentWeek) currentWeeksFound.push(relativePath) + const errorMessage = `Found more than one file with currentWeek set to true: ${currentWeeksFound.join( + '\n', + )}` + expect(currentWeeksFound.length, errorMessage).not.toBeGreaterThan(1) + }) + }) +}) diff --git a/src/content-linter/lib/secret-scanning-schema.js b/src/secret-scanning/lib/secret-scanning-schema.js similarity index 100% rename from src/content-linter/lib/secret-scanning-schema.js rename to src/secret-scanning/lib/secret-scanning-schema.js diff --git a/src/secret-scanning/tests/validate-schema.js b/src/secret-scanning/tests/validate-schema.js new file mode 100644 index 000000000000..93e6734a40ea --- /dev/null +++ b/src/secret-scanning/tests/validate-schema.js @@ -0,0 +1,25 @@ +import fs from 'fs' +import yaml from 'js-yaml' +import { jest } from '@jest/globals' + +import { ajvValidate } from '../../../lib/ajv-validate.js' +import { formatAjvErrors } from '../../../tests/helpers/schemas.js' +import secretScanningSchema from '../lib/secret-scanning-schema.js' + +jest.useFakeTimers({ legacyFakeTimers: true }) + +describe('lint secret-scanning', () => { + const yamlContent = yaml.load(fs.readFileSync('data/secret-scanning.yml', 'utf8')) + const jsonValidate = ajvValidate(secretScanningSchema) + + test('matches the schema', () => { + const valid = jsonValidate(yamlContent) + let errors + + if (!valid) { + errors = formatAjvErrors(jsonValidate.errors) + } + + expect(valid, errors).toBe(true) + }) +})