From 08d548a9782337f53f1b0d496e7ba037e7884162 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:03:49 -0500 Subject: [PATCH 01/21] add new link checking code --- tests/helpers/links-checker.js | 255 +++++++++++++++++++++ tests/links-and-images/links-and-images.js | 113 ++------- 2 files changed, 278 insertions(+), 90 deletions(-) create mode 100644 tests/helpers/links-checker.js diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js new file mode 100644 index 0000000000..b85e1c47c4 --- /dev/null +++ b/tests/helpers/links-checker.js @@ -0,0 +1,255 @@ +const cheerio = require('cheerio') +const { union, uniq } = require('lodash') +const fs = require('fs') +const path = require('path') + +const { getVersionStringFromPath } = require('../../lib/path-utils') +const patterns = require('../../lib/patterns') +const { deprecated, latest } = require('../../lib/enterprise-server-releases') +const findPageInVersion = require('../../lib/find-page-in-version') +const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version') +const rest = require('../../middleware/contextualizers/rest') +const graphql = require('../../middleware/contextualizers/graphql') +const contextualize = require('../../middleware/context') +// TODO enable when release notes are live +// const releaseNotes = require('../../middleware/contextualizers/enterprise-release-notes') + +class LinksChecker { + constructor (opts = { languageCode: 'en', internalHrefPrefixes: ['/', '#'] }) { + Object.assign(this, { ...opts }) + + // Some caching mechanism so we do not load pages unnecessarily, + // nor check links that have been checked + this.pageCache = new Map() + this.checkedLinksCache = new Set() + + // stores images to check all at once in a Map: + // imageSrc => { + // "usedBy": [version:path, ...] + // } + this.imagesToCheck = new Map() + + // Stores broken images in a Map, formatted the same way as imagesToCheck + this.brokenImages = new Map() + + // Stores broken links in a Map in the format of: + // link => { + // linkedFrom: [ version:filePath, ... ] + // }, ... + this.brokenLinks = new Map() + + // stores anchor links to check all at once in a Map: + // version:filePath => { + // '#anchor-link' : { + // linkedFrom: ['url1', 'url2'] + // }, + // '#anchor-link2': {...} + // } + this.anchorLinksToCheck = new Map() + + // Stores broken anchors in a Map, formatted the same way as anchorLinksToCheck + this.brokenAnchors = new Map() + } + + async setRenderedPageObj (pathCacheKey, context, reRender = false) { + if (this.pageCache.has(pathCacheKey) && !reRender) return + let pageHTML = await context.page.render(context) + + // handle special pre-rendered snowflake + if (context.page.relativePath.endsWith('graphql/reference/objects.md')) { + pageHTML += context.graphql.prerenderedObjectsForCurrentVersion.html + } + + const pageObj = cheerio.load(pageHTML, { xmlMode: true }) + this.pageCache.set(pathCacheKey, pageObj) + } + + async getRenderedPageObj (pathCacheKey, context) { + if (!this.pageCache.has(pathCacheKey)) { + if (context) { + await this.setRenderedPageObj(pathCacheKey, context) + } else { + console.error('cannot find pre-rendered page, and does not have enough context to render one.') + } + } + return this.pageCache.get(pathCacheKey) + } + + addAnchorForLater (pagePath, anchor, linkedFrom) { + const anchorsInPath = this.anchorLinksToCheck.get(pagePath) || {} + const anchorLink = anchorsInPath[anchor] ? anchorsInPath[anchor] : { linkedFrom: [] } + anchorLink.linkedFrom = union(anchorLink.linkedFrom, [linkedFrom]) + anchorsInPath[anchor] = anchorLink + this.anchorLinksToCheck.set(pagePath, anchorsInPath) + } + + addImagesForLater (images, pagePath) { + uniq(images).forEach(imageSrc => { + const imageUsage = this.imagesToCheck.get(imageSrc) || { usedBy: [] } + imageUsage.usedBy = union(imageUsage.usedBy, [pagePath]) + this.imagesToCheck.set(imageSrc, imageUsage) + }) + } + + async checkPage (context, checkExternalAnchors) { + const path = context.relativePath + const version = context.currentVersion + + const pathCacheKey = `${version}:${path}` + const $ = await this.getRenderedPageObj(pathCacheKey, context) + + const imageSrcs = $('img[src^="/assets"]').map((i, el) => $(el).attr('src')).toArray() + + this.addImagesForLater(imageSrcs, pathCacheKey) + + for (const href of this.internalHrefPrefixes) { + const internalLinks = $(`a[href^="${href}"]`).get() + + for (const internalLink of internalLinks) { + const href = $(internalLink).attr('href') + + let [link, anchor] = href.split('#') + // remove trailing slash + link = link.replace(patterns.trailingSlash, '$1') + + // if it's an external link and has been checked before, skip + if (link && this.checkedLinksCache.has(link)) { + // if it's been determined this link is broken, add to the linkedFrom field + if (this.brokenLinks.has(link)) { + const brokenLink = this.brokenLinks.get(link) + brokenLink.linkedFrom = union(brokenLink.linkedFrom, [pathCacheKey]) + this.brokenLinks.set(link, brokenLink) + } + if (!anchor) continue + } + + // if it's an internal anchor (e.g., #foo), save for later + if (anchor && !link) { + // ignore anchors that are autogenerated from headings + if (anchor === $(internalLink).parent().attr('id')) continue + this.addAnchorForLater(pathCacheKey, anchor, 'same page') + continue + } + + //------ BEGIN ONEOFF EXCLUSIONS -------/// + // skip GraphQL public schema paths (these are checked by separate tests) + if (link.startsWith('/public/') && link.endsWith('.graphql')) continue + + // skip links that start with /assets/images, as these are not in the pages collection + // and /assets/images paths should be checked during the image check + if (link.startsWith('/assets/images')) continue + + // skip rare hardcoded links to old GHE versions + // these paths will always be in the old versioned format + // example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release + const gheVersionInLink = link.match(patterns.getEnterpriseVersionNumber) + if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue + //------ END ONEOFF EXCLUSIONS -------/// + + // look for linked page + const versionFromHref = getVersionStringFromPath(link) + const linkedPage = findPageInVersion(link, context.pages, context.redirects, this.languageCode, versionFromHref) + this.checkedLinksCache.add(link) + + if (!linkedPage) { + this.brokenLinks.set(link, { linkedFrom: [pathCacheKey] }) + continue + } + + // if we're not checking external anchors, we're done + if (!checkExternalAnchors) { + continue + } + + // find the permalink for the current version + const linkedPagePermalink = linkedPage.permalinks.find(permalink => permalink.pageVersion === version) + + if (linkedPagePermalink) { + const linkedPageContext = await buildPathContext(context, linkedPage, linkedPagePermalink) + + if (anchor) { + await this.setRenderedPageObj(`${version}:${linkedPage.relativePath}`, linkedPageContext) + this.addAnchorForLater(`${version}:${linkedPage.relativePath}`, anchor, pathCacheKey) + } + } + } + } + } + + async checkAnchors () { + for await (const [pathCacheKey, anchors] of this.anchorLinksToCheck) { + const $ = await this.getRenderedPageObj(pathCacheKey) + for (const anchorText in anchors) { + const matchingHeadings = $(`[id="${anchorText}"], [name="${anchorText}"]`) + if (matchingHeadings.length === 0) { + const brokenAnchorPath = this.brokenAnchors.get(pathCacheKey) || {} + brokenAnchorPath[anchorText] = anchors[anchorText] + this.brokenAnchors.set(pathCacheKey, brokenAnchorPath) + } + } + } + } + + getBrokenLinks () { + return this.brokenLinks + } + + async getBrokenAnchors () { + await this.checkAnchors() + return this.brokenAnchors + } + + async getBrokenImages () { + for await (const [imageSrc, imageUsage] of this.imagesToCheck) { + try { + await fs.promises.access(path.join(process.cwd(), imageSrc)) + } catch (e) { + this.brokenImages.set(imageSrc, imageUsage) + } + } + return this.brokenImages + } +} + +// this function is async because the middleware functions are likely async +async function applyMiddleware (middleware, req) { + return middleware(req, null, () => {}) +} + +async function buildInitialContext () { + const req = { + path: '/en', + language: 'en', + query: {} + } + await applyMiddleware(contextualize, req) + return req.context +} + +async function buildPathContext (context, page, permalink) { + const pathContext = { + page, + currentVersion: permalink.pageVersion, + relativePath: permalink.relativePath + } + + const req = { + path: permalink.href, + context: Object.assign({}, context, pathContext), + language: 'en', + query: {} + } + + await applyMiddleware(rest, req) + await applyMiddleware(graphql, req) + // TODO enable when release notes are live + // await applyMiddleware(releaseNotes, req) + + return Object.assign({}, context, req.context) +} + +module.exports = { + LinksChecker, + buildPathContext, + buildInitialContext +} \ No newline at end of file diff --git a/tests/links-and-images/links-and-images.js b/tests/links-and-images/links-and-images.js index 4eb607f80a..29fae220b9 100644 --- a/tests/links-and-images/links-and-images.js +++ b/tests/links-and-images/links-and-images.js @@ -1,113 +1,46 @@ -const cheerio = require('cheerio') -const loadPages = require('../../lib/pages') -const loadSiteData = require('../../lib/site-data') -const getApplicableVersions = require('../../lib/get-applicable-versions') -const renderContent = require('../../lib/render-content') -const checkImages = require('../../lib/check-images') -const checkLinks = require('../../lib/check-links') -const enterpriseServerVersions = Object.keys(require('../../lib/all-versions')) - .filter(version => version.startsWith('enterprise-server@')) -const flat = require('flat') -const { last } = require('lodash') - -// english only for now +const { LinksChecker, buildInitialContext, buildPathContext, updateContextPerPath } = require('../helpers/links-checker') const languageCode = 'en' -const context = { currentLanguage: languageCode } - -const loadRedirects = require('../../lib/redirects/precompile') +// TODO set to true when we're ready to report and fix broken anchors +const checkExternalAnchors = false describe('page rendering', () => { jest.setTimeout(1000 * 1000) - const brokenImages = {} - const brokenAnchors = {} - const brokenLinks = {} + const linksChecker = new LinksChecker() - let pages, siteData, redirects beforeAll(async (done) => { - pages = await loadPages() - siteData = await loadSiteData() - redirects = await loadRedirects(pages) + // fetch context.pages, context.redirects, etc. + // we only want to build these one time + const context = await buildInitialContext() - context.pages = pages - context.site = siteData[languageCode].site - context.redirects = redirects - - let checkedLinks = {} - let checkedImages = {} - - const englishPages = pages + const englishPages = context.pages .filter(page => page.languageCode === languageCode) - // ignore developers content, to be checked separately - .filter(page => !page.relativePath.match(/^(rest|graphql|developers)/)) for (const page of englishPages) { - // skip map topics because they have no content of their own - if (page.mapTopic) continue - - const brokenImagesPerPage = {} - const brokenAnchorsPerPage = {} - const brokenLinksPerPage = {} - - // get an array of the pages product versions - const pageVersions = getApplicableVersions(page.versions, page.relativePath) - - for (const pageVersion of pageVersions) { - // attach page-specific properties to context - page.version = pageVersion - context.page = page - context.currentVersion = pageVersion - context.enterpriseServerVersions = enterpriseServerVersions - - // collect elements of the page that may contain links - const pageContent = page.intro + page.permissions + page.markdown - - // renderContent is much faster than page.render, even though we later have to run - // rewriteLocalLinks in check-images and rewriteAssetPathsToS3 in check-links - const pageHtml = await renderContent(pageContent, context) - const $ = cheerio.load(pageHtml, { xmlMode: true }) - - // check images - const { brokenImages: brokenImagesPerVersion, checkedImageCache } = await checkImages($, pageVersion, page.relativePath, checkedImages) - if (brokenImagesPerVersion.length) brokenImagesPerPage[pageVersion] = brokenImagesPerVersion - checkedImages = checkedImageCache - - // check anchors and links - const { brokenLinks: brokenLinksPerVersion, checkedLinkCache } = await checkLinks($, page, context, pageVersion, checkedLinks) - if (brokenLinksPerVersion.anchors.length) brokenAnchorsPerPage[pageVersion] = brokenLinksPerVersion.anchors - if (brokenLinksPerVersion.links.length) brokenLinksPerPage[pageVersion] = brokenLinksPerVersion.links - checkedLinks = checkedLinkCache + for (const permalink of page.permalinks) { + const pathContext = await buildPathContext(context, page, permalink) + await linksChecker.checkPage(pathContext, checkExternalAnchors) } - - if (Object.keys(brokenImagesPerPage).length) brokenImages[page.fullPath] = brokenImagesPerPage - if (Object.keys(brokenAnchorsPerPage).length) brokenAnchors[page.fullPath] = brokenAnchorsPerPage - if (Object.keys(brokenLinksPerPage).length) brokenLinks[page.fullPath] = brokenLinksPerPage } + done() }) test('every page has image references that can be resolved', async () => { - const numbrokenImages = getNumBrokenItems(brokenImages) - expect(numbrokenImages, `Found ${numbrokenImages} total broken images: ${JSON.stringify(brokenImages, null, 2)}`).toBe(0) + const result = await linksChecker.getBrokenImages() + expect(result.size, `Found ${result.size} total broken images: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) - test('every page has links with anchors that can be resolved', async () => { - const numbrokenAnchors = getNumBrokenItems(brokenAnchors) - expect(numbrokenAnchors, `Found ${numbrokenAnchors} total broken anchors: ${JSON.stringify(brokenAnchors, null, 2)}`).toBe(0) + // When ready to unskip this, + test.skip('every page has links with anchors that can be resolved', async () => { + const result = await linksChecker.getBrokenAnchors() + const numBrokenAnchors = [...result].reduce((accumulator, [path, anchors]) => accumulator + Object.keys(anchors).length, 0) + expect(numBrokenAnchors, `Found ${numBrokenAnchors} total broken anchors in ${result.size} pages: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) - test('every page has links that can be resolved', async () => { - const numbrokenLinks = getNumBrokenItems(brokenLinks) - expect(numbrokenLinks, `Found ${numbrokenLinks} total broken links: ${JSON.stringify(brokenLinks, null, 2)}`).toBe(0) + test('every page has links that can be resolved', () => { + const result = linksChecker.getBrokenLinks() + expect(result.size, `Found ${result.size} total broken links: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) -}) - -// count all the nested items -function getNumBrokenItems (items) { - // filter for entries like this: - // '/article-path-here.md.dotcom.1.broken link': '/en/articles/foo', - return Object.keys(flat(items)) - .filter(key => last(key.split('.')).includes('broken')) - .length -} +}) \ No newline at end of file From 7ee7d1ef266bdea82ffc88daea6c48a47e1f4cf7 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:04:30 -0500 Subject: [PATCH 02/21] delete no longer needed link checking code --- lib/check-images.js | 27 --- .../developer-links-and-images.js | 157 ------------------ 2 files changed, 184 deletions(-) delete mode 100644 lib/check-images.js delete mode 100644 tests/links-and-images/developer-links-and-images.js diff --git a/lib/check-images.js b/lib/check-images.js deleted file mode 100644 index 9560437bee..0000000000 --- a/lib/check-images.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs') -const path = require('path') -const rewriteAssetPathsToS3 = require('./rewrite-asset-paths-to-s3') -const { promisify } = require('util') - -module.exports = async function checkImages ($, version, relativePath, checkedImageCache = {}) { - rewriteAssetPathsToS3($, version, relativePath) - - const brokenImages = [] - - // this does not check S3 images because those live outside of the repo - const images = $('img[src^="/assets"]').get() - - for (const image of images) { - const src = $(image).attr('src') - - if (checkedImageCache[src]) continue - - try { - await promisify(fs.access)(path.join(__dirname, '..', src)) - } catch (e) { - brokenImages.push({ 'broken image reference': src }) - } - } - - return { brokenImages, checkedImageCache } -} diff --git a/tests/links-and-images/developer-links-and-images.js b/tests/links-and-images/developer-links-and-images.js deleted file mode 100644 index 437cd3c6fd..0000000000 --- a/tests/links-and-images/developer-links-and-images.js +++ /dev/null @@ -1,157 +0,0 @@ -const flat = require('flat') -const { last } = require('lodash') -const cheerio = require('cheerio') -const loadPages = require('../../lib/pages') -const loadSiteData = require('../../lib/site-data') -const getApplicableVersions = require('../../lib/get-applicable-versions') -const loadRedirects = require('../../lib/redirects/precompile') -const { getVersionedPathWithLanguage } = require('../../lib/path-utils') -const renderContent = require('../../lib/render-content') -const checkImages = require('../../lib/check-images') -const checkLinks = require('../../lib/check-developer-links') -const allVersions = require('../../lib/all-versions') -const enterpriseServerVersions = Object.keys(require('../../lib/all-versions')) - .filter(version => version.startsWith('enterprise-server@')) - -// schema-derived data to add to context object -const rest = require('../../lib/rest') -const previews = require('../../lib/graphql/static/previews') -const upcomingChanges = require('../../lib/graphql/static/upcoming-changes') -const changelog = require('../../lib/graphql/static/changelog') -const prerenderedObjects = require('../../lib/graphql/static/prerendered-objects') - -// english only -const languageCode = 'en' - -const context = { - currentLanguage: languageCode, - rest -} - -// developer content only -const developerContentRegex = /^(rest|graphql|developers)/ - -describe('page rendering', () => { - jest.setTimeout(1000 * 1000) - - const brokenImages = {} - const brokenAnchors = {} - const brokenLinks = {} - - beforeAll(async (done) => { - const pages = await loadPages() - const siteData = await loadSiteData() - const redirects = await loadRedirects(pages) - - context.pages = pages - context.site = siteData[languageCode].site - context.redirects = redirects - - const developerPages = pages - .filter(page => page.relativePath.match(developerContentRegex) && page.languageCode === languageCode) - - let checkedLinks = {} - let checkedImages = {} - - for (const page of developerPages) { - const brokenImagesPerPage = {} - const brokenAnchorsPerPage = {} - const brokenLinksPerPage = {} - - // get an array of the pages product versions - const pageVersions = getApplicableVersions(page.versions, page.relativePath) - - for (const pageVersion of pageVersions) { - // attach page-specific properties to context - page.version = pageVersion - context.page = page - context.currentVersion = pageVersion - context.enterpriseServerVersions = enterpriseServerVersions - - const relevantPermalink = page.permalinks.find(permalink => permalink.pageVersion === pageVersion) - - const graphqlVersion = allVersions[pageVersion].miscVersionName - - // borrowed from middleware/contextualizers/graphql.js - context.graphql = { - schemaForCurrentVersion: require(`../../lib/graphql/static/schema-${graphqlVersion}`), - previewsForCurrentVersion: previews[graphqlVersion], - upcomingChangesForCurrentVersion: upcomingChanges[graphqlVersion], - prerenderedObjectsForCurrentVersion: prerenderedObjects[graphqlVersion], - changelog - } - - // borrowed from middleware/contextualizers/rest.js - context.restGitHubAppsLink = getVersionedPathWithLanguage( - '/developers/apps', - pageVersion, - languageCode - ) - - context.operationsForCurrentProduct = context.rest.operations[pageVersion] || [] - - if (relevantPermalink.href.includes('rest/reference/')) { - const docsPath = relevantPermalink.href - .split('rest/reference/')[1] - .split('#')[0] // do not include #fragments - - // find all operations that with an operationID that matches the requested docs path - context.currentRestOperations = context.operationsForCurrentProduct - .filter(operation => operation.operationId.startsWith(docsPath)) - } - - // collect elements of the page that may contain links - const pageContent = relevantPermalink.href.includes('graphql/reference/objects') - ? page.markdown + context.graphql.prerenderedObjectsForCurrentVersion.html - : page.intro + page.permissions + page.markdown - - // renderContent is much faster than page.render, even though we later have to run - // rewriteLocalLinks in check-images and rewriteAssetPathsToS3 in check-links - const pageHtml = await renderContent(pageContent, context) - const $ = cheerio.load(pageHtml, { xmlMode: true }) - - // check images - const { brokenImages: brokenImagesPerVersion, checkedImageCache } = await checkImages($, pageVersion, page.relativePath, checkedImages) - if (brokenImagesPerVersion.length) brokenImagesPerPage[pageVersion] = brokenImagesPerVersion - checkedImages = checkedImageCache - - // check anchors and links - const { brokenLinks: brokenLinksPerVersion, checkedLinkCache } = await checkLinks($, page, context, pageVersion, checkedLinks) - if (brokenLinksPerVersion.anchors.length) brokenAnchorsPerPage[pageVersion] = brokenLinksPerVersion.anchors - if (brokenLinksPerVersion.links.length) brokenLinksPerPage[pageVersion] = brokenLinksPerVersion.links - checkedLinks = checkedLinkCache - } - - if (Object.keys(brokenImagesPerPage).length) brokenImages[page.fullPath] = brokenImagesPerPage - if (Object.keys(brokenAnchorsPerPage).length) brokenAnchors[page.fullPath] = brokenAnchorsPerPage - if (Object.keys(brokenLinksPerPage).length) brokenLinks[page.fullPath] = brokenLinksPerPage - } - - done() - }) - - test('every page has image references that can be resolved', async () => { - const numbrokenImages = getNumBrokenItems(brokenImages) - expect(numbrokenImages, `Found ${numbrokenImages} total broken images: ${JSON.stringify(brokenImages, null, 2)}`).toBe(0) - }) - - test.skip('every page has links with anchors that can be resolved', async () => { - const numbrokenAnchors = getNumBrokenItems(brokenAnchors) - expect(numbrokenAnchors, `Found ${numbrokenAnchors} total broken anchors: ${JSON.stringify(brokenAnchors, null, 2)}`).toBe(0) - }) - - // disable anchor test til we resolve broken anchors - test.skip('every page has links that can be resolved', async () => { - const numbrokenLinks = getNumBrokenItems(brokenLinks) - expect(numbrokenLinks, `Found ${numbrokenLinks} total broken links: ${JSON.stringify(brokenLinks, null, 2)}`).toBe(0) - }) -}) - -// count all the nested items -function getNumBrokenItems (items) { - // filter for entries like this: - // '/article-path-here.md.dotcom.1.broken link': '/en/articles/foo', - return Object.keys(flat(items)) - .filter(key => last(key.split('.')).includes('broken')) - .length -} From 3469049d70518b044042bf7fe36e035d2144a142 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:05:18 -0500 Subject: [PATCH 03/21] delete no longer needed link checking code --- lib/check-links.js | 122 --------------------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 lib/check-links.js diff --git a/lib/check-links.js b/lib/check-links.js deleted file mode 100644 index 1d000da7f0..0000000000 --- a/lib/check-links.js +++ /dev/null @@ -1,122 +0,0 @@ -const cheerio = require('cheerio') -const findPageInVersion = require('./find-page-in-version') -const renderContent = require('./render-content') -const rewriteLocalLinks = require('./rewrite-local-links') -const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version') -const { getPathWithoutLanguage } = require('./path-utils') -const { getEnterpriseVersionNumber, adminProduct } = require('./patterns') -const { deprecated, latest } = require('./enterprise-server-releases') - -// internal links will have a language code by the time we're testing them -// we also want to capture same-page anchors (#foo) -const languageCode = 'en' -const internalHrefs = ['/en', '#'] - -const renderedPageCache = {} -const checkedAnchorCache = {} - -module.exports = async function checkLinks ($, page, context, version, checkedLinkCache = {}) { - // run rewriteLocalLinks to version links and add language codes - rewriteLocalLinks($, version, languageCode) - - const brokenLinks = { - anchors: [], - links: [] - } - - // internal link check - for (const href of internalHrefs) { - const internalLinks = $(`a[href^="${href}"]`).get() - - for (const internalLink of internalLinks) { - const href = $(internalLink).attr('href') - - // enable caching so we don't check links more than once - // anchor links are cached locally (within this run) since they are specific to the page - if (checkedLinkCache[href] || checkedAnchorCache[href]) continue - - const [link, anchor] = href.split('#') - - // if anchor only (e.g., #foo), look for heading on same page - if (anchor && !link) { - // ignore anchors that are autogenerated from headings - if (anchor === $(internalLink).parent().attr('id')) continue - - const matchingHeadings = getMatchingHeadings($, anchor) - - if (matchingHeadings.length === 0) { - brokenLinks.anchors.push({ 'broken same-page anchor': `#${anchor}`, reason: 'heading not found on page' }) - } - checkedAnchorCache[href] = true - continue - } - checkedLinkCache[href] = true - - // skip rare hardcoded links to old GHE versions - // these paths will always be in the old versioned form - // example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release - const gheVersionInLink = link.match(getEnterpriseVersionNumber) - if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue - - // look for linked page - const isDotcomOnly = $(internalLink).attr('class') - - // special case for GHES Admin links on dotcom, which are not broken; they go to the latest GHES version - let versionToCheck = version - if (version === nonEnterpriseDefaultVersion && adminProduct.test(link)) { - versionToCheck = `enterprise-server@${latest}` - } - - const linkedPage = findPageInVersion(link, context.pages, context.redirects, languageCode, versionToCheck, isDotcomOnly) - - if (!linkedPage) { - brokenLinks.links.push({ 'broken link': link, reason: 'linked page not found' }) - continue - } - - // don't check anchors on developers content - if (linkedPage.relativePath.match(/^(rest|graphql|developers)/)) continue - - // create a unique string for caching purposes - const pathToCache = version + linkedPage.relativePath - - const anchorToCheck = anchor - - // if link with anchor (e.g., /some/path#foo), look for heading on linked page - if (anchorToCheck) { - // either render page or fetch it from cache if we've already rendered it - let linkedPageObject - if (!renderedPageCache[pathToCache]) { - const linkedPageHtml = await renderContent(linkedPage.markdown, context) - linkedPageObject = cheerio.load(linkedPageHtml, { xmlMode: true }) - renderedPageCache[pathToCache] = linkedPageObject - } else { - linkedPageObject = renderedPageCache[pathToCache] - } - - const matchingHeadings = getMatchingHeadings(linkedPageObject, anchorToCheck) - - if (matchingHeadings.length === 0) { - if (anchor) { - brokenLinks.anchors.push({ 'broken anchor': `#${anchor}`, 'full link': `${getPathWithoutLanguage(link)}#${anchor}`, reason: 'heading not found on linked page', 'linked page': linkedPage.fullPath }) - } - continue - } - } - } - } - - return { brokenLinks, checkedLinkCache } -} - -// article titles are h1s; headings can be any subsequent level -function getMatchingHeadings ($, anchor) { - return $(` - h2[id="${anchor}"], - h3[id="${anchor}"], - h4[id="${anchor}"], - h5[id="${anchor}"], - h6[id="${anchor}"], - a[name="${anchor}"] - `) -} From a1517ce1da17e562be454aea96452c1f793b4d1c Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:05:34 -0500 Subject: [PATCH 04/21] delete no longer needed link checking code --- lib/check-developer-links.js | 137 ----------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 lib/check-developer-links.js diff --git a/lib/check-developer-links.js b/lib/check-developer-links.js deleted file mode 100644 index e5e708be88..0000000000 --- a/lib/check-developer-links.js +++ /dev/null @@ -1,137 +0,0 @@ -const cheerio = require('cheerio') -const findPageInVersion = require('./find-page-in-version') -const renderContent = require('./render-content') -const rewriteLocalLinks = require('./rewrite-local-links') -const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version') -const { getPathWithoutLanguage } = require('./path-utils') -const { getEnterpriseVersionNumber, adminProduct } = require('./patterns') -const { deprecated, latest } = require('./enterprise-server-releases') - -// internal links will have a language code by the time we're testing them -// we also want to capture same-page anchors (#foo) -const languageCode = 'en' -const internalHrefs = ['/en', '#'] - -const renderedPageCache = {} -const checkedAnchorCache = {} - -module.exports = async function checkLinks ($, page, context, version, checkedLinkCache = {}) { - // run rewriteLocalLinks to version links and add language codes - rewriteLocalLinks($, version, languageCode) - - const brokenLinks = { - anchors: [], - links: [] - } - - // internal link check - for (const href of internalHrefs) { - const internalLinks = $(`a[href^="${href}"]`).get() - - for (const internalLink of internalLinks) { - const href = $(internalLink).attr('href') - - // enable caching so we don't check links more than once - // anchor links are cached locally (within this run) since they are specific to the page - if (checkedLinkCache[href] || checkedAnchorCache[href]) continue - - const [link, anchor] = href.split('#') - - // if anchor only (e.g., #foo), look for heading on same page - if (anchor && !link) { - // ignore anchors that are autogenerated from headings - if (anchor === $(internalLink).parent().attr('id')) continue - - const matchingHeadings = getMatchingHeadings($, anchor) - - if (matchingHeadings.length === 0) { - brokenLinks.anchors.push({ 'broken same-page anchor': `#${anchor}`, reason: 'heading not found on page' }) - } - checkedAnchorCache[href] = true - continue - } - checkedLinkCache[href] = true - - // skip rare hardcoded links to old GHE versions - // these paths will always be in the old versioned form - // example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release - const gheVersionInLink = link.match(getEnterpriseVersionNumber) - if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue - - // look for linked page - const isDotcomOnly = $(internalLink).attr('class') - - // special case for GHES Admin links on dotcom, which are not broken; they go to the latest GHES version - let versionToCheck = version - if (version === nonEnterpriseDefaultVersion && adminProduct.test(link)) { - versionToCheck = `enterprise-server@${latest}` - } - - const linkedPage = findPageInVersion(link, context.pages, context.redirects, languageCode, versionToCheck, isDotcomOnly) - - if (!linkedPage) { - brokenLinks.links.push({ 'broken link': link, reason: 'linked page not found' }) - continue - } - - if (linkedPage.relativePath.includes('rest/reference/') && linkedPage.relativePath !== 'rest/reference/index.md') { - const linkedPageRelevantPermalink = linkedPage.permalinks.find(permalink => permalink.pageVersion === version) - if (!linkedPageRelevantPermalink) continue - - const docsPath = linkedPageRelevantPermalink.href - .split('rest/reference/')[1] - .split('#')[0] // do not include #fragments - - // find all operations that with an operationID that matches the requested docs path - context.currentRestOperations = context.operationsForCurrentProduct - .filter(operation => operation.operationId.startsWith(docsPath)) - } - - // collect elements of the page that may contain links - const linkedPageContent = linkedPage.relativePath.includes('graphql/reference/objects') - ? linkedPage.markdown + context.graphql.prerenderedObjectsForCurrentVersion.html - : linkedPage.markdown - - // create a unique string for caching purposes - const pathToCache = version + linkedPage.relativePath - - const anchorToCheck = anchor - - // if link with anchor (e.g., /some/path#foo), look for heading on linked page - if (anchorToCheck) { - // either render page or fetch it from cache if we've already rendered it - let linkedPageObject - if (!renderedPageCache[pathToCache]) { - const linkedPageHtml = await renderContent(linkedPageContent, context) - linkedPageObject = cheerio.load(linkedPageHtml, { xmlMode: true }) - renderedPageCache[pathToCache] = linkedPageObject - } else { - linkedPageObject = renderedPageCache[pathToCache] - } - - const matchingHeadings = getMatchingHeadings(linkedPageObject, anchorToCheck) - - if (matchingHeadings.length === 0) { - if (anchor) { - brokenLinks.anchors.push({ 'broken anchor': `#${anchor}`, 'full link': `${getPathWithoutLanguage(link)}#${anchor}`, reason: 'heading not found on linked page', 'linked page': linkedPage.fullPath }) - } - continue - } - } - } - } - - return { brokenLinks, checkedLinkCache } -} - -// article titles are h1s; headings can be any subsequent level -function getMatchingHeadings ($, anchor) { - return $(` - h2[id="${anchor}"], - h3[id="${anchor}"], - h4[id="${anchor}"], - h5[id="${anchor}"], - h6[id="${anchor}"], - a[name="${anchor}"] - `).get() -} From f4838746878cc3826eece8793fa89e5a76d72672 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:12:45 -0500 Subject: [PATCH 05/21] lint --- tests/helpers/links-checker.js | 29 +++++++++++----------- tests/links-and-images/links-and-images.js | 6 ++--- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js index b85e1c47c4..0725bbf0db 100644 --- a/tests/helpers/links-checker.js +++ b/tests/helpers/links-checker.js @@ -5,9 +5,8 @@ const path = require('path') const { getVersionStringFromPath } = require('../../lib/path-utils') const patterns = require('../../lib/patterns') -const { deprecated, latest } = require('../../lib/enterprise-server-releases') +const { deprecated } = require('../../lib/enterprise-server-releases') const findPageInVersion = require('../../lib/find-page-in-version') -const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version') const rest = require('../../middleware/contextualizers/rest') const graphql = require('../../middleware/contextualizers/graphql') const contextualize = require('../../middleware/context') @@ -97,7 +96,7 @@ class LinksChecker { const pathCacheKey = `${version}:${path}` const $ = await this.getRenderedPageObj(pathCacheKey, context) - + const imageSrcs = $('img[src^="/assets"]').map((i, el) => $(el).attr('src')).toArray() this.addImagesForLater(imageSrcs, pathCacheKey) @@ -131,7 +130,7 @@ class LinksChecker { continue } - //------ BEGIN ONEOFF EXCLUSIONS -------/// + // ------ BEGIN ONEOFF EXCLUSIONS -------/// // skip GraphQL public schema paths (these are checked by separate tests) if (link.startsWith('/public/') && link.endsWith('.graphql')) continue @@ -144,7 +143,7 @@ class LinksChecker { // example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release const gheVersionInLink = link.match(patterns.getEnterpriseVersionNumber) if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue - //------ END ONEOFF EXCLUSIONS -------/// + // ------ END ONEOFF EXCLUSIONS -------/// // look for linked page const versionFromHref = getVersionStringFromPath(link) @@ -155,7 +154,7 @@ class LinksChecker { this.brokenLinks.set(link, { linkedFrom: [pathCacheKey] }) continue } - + // if we're not checking external anchors, we're done if (!checkExternalAnchors) { continue @@ -165,7 +164,7 @@ class LinksChecker { const linkedPagePermalink = linkedPage.permalinks.find(permalink => permalink.pageVersion === version) if (linkedPagePermalink) { - const linkedPageContext = await buildPathContext(context, linkedPage, linkedPagePermalink) + const linkedPageContext = await buildPathContext(context, linkedPage, linkedPagePermalink) if (anchor) { await this.setRenderedPageObj(`${version}:${linkedPage.relativePath}`, linkedPageContext) @@ -217,10 +216,10 @@ async function applyMiddleware (middleware, req) { } async function buildInitialContext () { - const req = { + const req = { path: '/en', - language: 'en', - query: {} + language: 'en', + query: {} } await applyMiddleware(contextualize, req) return req.context @@ -231,13 +230,13 @@ async function buildPathContext (context, page, permalink) { page, currentVersion: permalink.pageVersion, relativePath: permalink.relativePath - } - - const req = { + } + + const req = { path: permalink.href, context: Object.assign({}, context, pathContext), language: 'en', - query: {} + query: {} } await applyMiddleware(rest, req) @@ -252,4 +251,4 @@ module.exports = { LinksChecker, buildPathContext, buildInitialContext -} \ No newline at end of file +} diff --git a/tests/links-and-images/links-and-images.js b/tests/links-and-images/links-and-images.js index 29fae220b9..200f597bfa 100644 --- a/tests/links-and-images/links-and-images.js +++ b/tests/links-and-images/links-and-images.js @@ -1,4 +1,4 @@ -const { LinksChecker, buildInitialContext, buildPathContext, updateContextPerPath } = require('../helpers/links-checker') +const { LinksChecker, buildInitialContext, buildPathContext } = require('../helpers/links-checker') const languageCode = 'en' // TODO set to true when we're ready to report and fix broken anchors @@ -32,7 +32,7 @@ describe('page rendering', () => { expect(result.size, `Found ${result.size} total broken images: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) - // When ready to unskip this, + // When ready to unskip this, test.skip('every page has links with anchors that can be resolved', async () => { const result = await linksChecker.getBrokenAnchors() const numBrokenAnchors = [...result].reduce((accumulator, [path, anchors]) => accumulator + Object.keys(anchors).length, 0) @@ -43,4 +43,4 @@ describe('page rendering', () => { const result = linksChecker.getBrokenLinks() expect(result.size, `Found ${result.size} total broken links: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) -}) \ No newline at end of file +}) From a8e12608fafc2d0f70e81a19499d6ac8d185a54d Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Mon, 30 Nov 2020 13:26:41 -0500 Subject: [PATCH 06/21] Update tests/helpers/links-checker.js Co-authored-by: Kevin Heis --- tests/helpers/links-checker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js index 0725bbf0db..5e02cb581a 100644 --- a/tests/helpers/links-checker.js +++ b/tests/helpers/links-checker.js @@ -76,7 +76,7 @@ class LinksChecker { addAnchorForLater (pagePath, anchor, linkedFrom) { const anchorsInPath = this.anchorLinksToCheck.get(pagePath) || {} - const anchorLink = anchorsInPath[anchor] ? anchorsInPath[anchor] : { linkedFrom: [] } + const anchorLink = anchorsInPath[anchor] || { linkedFrom: [] } anchorLink.linkedFrom = union(anchorLink.linkedFrom, [linkedFrom]) anchorsInPath[anchor] = anchorLink this.anchorLinksToCheck.set(pagePath, anchorsInPath) From 74fe86d91ebbed58e74ec4e3ce73cd79aecf90e6 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Tue, 1 Dec 2020 10:16:05 -0500 Subject: [PATCH 07/21] clarify what is happening with the context objects --- tests/helpers/links-checker.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js index 5e02cb581a..a86409666d 100644 --- a/tests/helpers/links-checker.js +++ b/tests/helpers/links-checker.js @@ -10,8 +10,8 @@ const findPageInVersion = require('../../lib/find-page-in-version') const rest = require('../../middleware/contextualizers/rest') const graphql = require('../../middleware/contextualizers/graphql') const contextualize = require('../../middleware/context') -// TODO enable when release notes are live -// const releaseNotes = require('../../middleware/contextualizers/enterprise-release-notes') +const releaseNotes = require('../../middleware/contextualizers/enterprise-release-notes') +const versionSatisfiesRange = require('../../lib/version-satisfies-range') class LinksChecker { constructor (opts = { languageCode: 'en', internalHrefPrefixes: ['/', '#'] }) { @@ -225,26 +225,35 @@ async function buildInitialContext () { return req.context } -async function buildPathContext (context, page, permalink) { +async function buildPathContext (initialContext, page, permalink) { + // Create a new object with path-specific properties const pathContext = { page, currentVersion: permalink.pageVersion, relativePath: permalink.relativePath } + // Combine it with the initial context object that has pages, redirects, etc. + const combinedContext = Object.assign({}, initialContext, pathContext) + + // Create a new req object using the combined context const req = { path: permalink.href, - context: Object.assign({}, context, pathContext), + context: combinedContext, language: 'en', query: {} } + // Pass the req to the contextualizing middlewares await applyMiddleware(rest, req) await applyMiddleware(graphql, req) - // TODO enable when release notes are live - // await applyMiddleware(releaseNotes, req) + // Release notes are on docs site starting with GHES 3.0 + if (versionSatisfiesRange(permalink.pageVersion, '>=3.0')) { + await applyMiddleware(releaseNotes, req) + } - return Object.assign({}, context, req.context) + // Return the resulting context object with REST, GraphQL, and release notes data now attached + return req.context } module.exports = { From 432d13fd320c65af923ddec50de2485435807c54 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Tue, 1 Dec 2020 10:25:23 -0500 Subject: [PATCH 08/21] Add more detail in comment --- tests/helpers/links-checker.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js index a86409666d..ca69d76644 100644 --- a/tests/helpers/links-checker.js +++ b/tests/helpers/links-checker.js @@ -226,7 +226,12 @@ async function buildInitialContext () { } async function buildPathContext (initialContext, page, permalink) { - // Create a new object with path-specific properties + // Create a new object with path-specific properties. + // Note this is cherry-picking properties currently only needed by the middlware below; + // See middleware/context.js for the rest of the properties we are NOT refreshing per page. + // If we find this causes problems for link checking, we can call `contextualize` on + // every page. For now, this cherry-picking approach is intended to improve performance so + // we don't have to build the expensive `pages`, `redirects`, etc. data on every page we check. const pathContext = { page, currentVersion: permalink.pageVersion, @@ -247,7 +252,7 @@ async function buildPathContext (initialContext, page, permalink) { // Pass the req to the contextualizing middlewares await applyMiddleware(rest, req) await applyMiddleware(graphql, req) - // Release notes are on docs site starting with GHES 3.0 + // Release notes are available on docs site starting with GHES 3.0 if (versionSatisfiesRange(permalink.pageVersion, '>=3.0')) { await applyMiddleware(releaseNotes, req) } From f27002810f3b44e187767ab6a841303141d02459 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Tue, 8 Dec 2020 16:02:15 -0500 Subject: [PATCH 09/21] req.context.pages is now an object, we need it to be an array --- tests/links-and-images/links-and-images.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/links-and-images/links-and-images.js b/tests/links-and-images/links-and-images.js index 8e4f06e598..aa9d104d74 100644 --- a/tests/links-and-images/links-and-images.js +++ b/tests/links-and-images/links-and-images.js @@ -1,4 +1,5 @@ const { LinksChecker, buildInitialContext, buildPathContext } = require('../helpers/links-checker') +const { uniq } = require('lodash') const languageCode = 'en' // TODO set to true when we're ready to report and fix broken anchors @@ -14,7 +15,7 @@ describe('page rendering', () => { // we only want to build these one time const context = await buildInitialContext() - const englishPages = context.pages + const englishPages = uniq(Object.values(context.pages)) .filter(page => page.languageCode === languageCode) for (const page of englishPages) { From 10152fa15a665cf6a506d8d97fdb97911770f1e9 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 9 Dec 2020 15:12:39 -0500 Subject: [PATCH 10/21] lint --- tests/links-and-images/links-and-images.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/links-and-images/links-and-images.js b/tests/links-and-images/links-and-images.js index aa9d104d74..8a907576e3 100644 --- a/tests/links-and-images/links-and-images.js +++ b/tests/links-and-images/links-and-images.js @@ -44,4 +44,4 @@ describe('page rendering', () => { const result = linksChecker.getBrokenLinks() expect(result.size, `Found ${result.size} total broken links: ${JSON.stringify([...result], null, 2)}`).toBe(0) }) -}) \ No newline at end of file +}) From 8e1455ebe151fc9efd3b4ddef8aa10fdae4bd32b Mon Sep 17 00:00:00 2001 From: Sarah Edwards Date: Thu, 10 Dec 2020 10:28:12 -0800 Subject: [PATCH 11/21] Fix broken links (#16700) * delete section describing use of oauth api in dotcom only section * version mention of oauth api for ghae only * version code scanning alert not for ghae * version security alerts to not be for GHAE * version Libraries article to include GHAE * version links to beta and suspending apps to not be for GHAE or GHES * version link to authorization API to be GHES only Co-authored-by: Sarah Schneider --- .../apps/creating-a-github-app-using-url-parameters.md | 4 ++-- .../apps/identifying-and-authorizing-users-for-github-apps.md | 2 +- .../developers/apps/suspending-a-github-app-installation.md | 2 -- content/developers/overview/managing-deploy-keys.md | 2 ++ .../webhooks-and-events/webhook-events-and-payloads.md | 4 +++- content/rest/overview/libraries.md | 1 + content/rest/reference/enterprise-admin.md | 2 +- .../pre-release-program/suspend-installation-beta.md | 3 --- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/content/developers/apps/creating-a-github-app-using-url-parameters.md b/content/developers/apps/creating-a-github-app-using-url-parameters.md index e2008f06bf..99d0f03a93 100644 --- a/content/developers/apps/creating-a-github-app-using-url-parameters.md +++ b/content/developers/apps/creating-a-github-app-using-url-parameters.md @@ -79,8 +79,8 @@ Permission | Description [`single_file`](/rest/reference/permissions-required-for-github-apps/#permission-on-single-file) | Grants access to the [Contents API](/rest/reference/repos#contents). Can be one of: `none`, `read`, or `write`. [`starring`](/rest/reference/permissions-required-for-github-apps/#permission-on-starring) | Grants access to the [Starring API](/rest/reference/activity#starring). Can be one of: `none`, `read`, or `write`. [`statuses`](/rest/reference/permissions-required-for-github-apps/#permission-on-statuses) | Grants access to the [Statuses API](/rest/reference/repos#statuses). Can be one of: `none`, `read`, or `write`. -[`team_discussions`](/rest/reference/permissions-required-for-github-apps/#permission-on-team-discussions) | Grants access to the [Team Discussions API](/rest/reference/teams#discussions) and the [Team Discussion Comments API](/rest/reference/teams#discussion-comments). Can be one of: `none`, `read`, or `write`. -`vulnerability_alerts`| Grants access to receive security alerts for vulnerable dependencies in a repository. See "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies)" to learn more. Can be one of: `none` or `read`. +[`team_discussions`](/rest/reference/permissions-required-for-github-apps/#permission-on-team-discussions) | Grants access to the [Team Discussions API](/rest/reference/teams#discussions) and the [Team Discussion Comments API](/rest/reference/teams#discussion-comments). Can be one of: `none`, `read`, or `write`.{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@1.19" %} +`vulnerability_alerts`| Grants access to receive security alerts for vulnerable dependencies in a repository. See "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies)" to learn more. Can be one of: `none` or `read`.{% endif %} `watching` | Grants access to list and change repositories a user is subscribed to. Can be one of: `none`, `read`, or `write`. ### {% data variables.product.prodname_github_app %} webhook events diff --git a/content/developers/apps/identifying-and-authorizing-users-for-github-apps.md b/content/developers/apps/identifying-and-authorizing-users-for-github-apps.md index 20826403c5..ae02a509fc 100644 --- a/content/developers/apps/identifying-and-authorizing-users-for-github-apps.md +++ b/content/developers/apps/identifying-and-authorizing-users-for-github-apps.md @@ -67,7 +67,7 @@ If the user accepts your request, GitHub redirects back to your site with a temp {% endnote %} -Exchange this `code` for an access token. {% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %} When expiring tokens are enabled, the access token expires in 8 hours and the refresh token expires in 6 months. Every time you refresh the token, you get a new refresh token. For more information, see "[Refreshing user-to-server access tokens](/developers/apps/refreshing-user-to-server-access-tokens)." +Exchange this `code` for an access token. {% if currentVersion == "free-pro-team@latest" %} When expiring tokens are enabled, the access token expires in 8 hours and the refresh token expires in 6 months. Every time you refresh the token, you get a new refresh token. For more information, see "[Refreshing user-to-server access tokens](/developers/apps/refreshing-user-to-server-access-tokens)." Expiring user tokens are currently part of the user-to-server token expiration beta and subject to change. To opt-in to the user-to-server token expiration beta feature, see "[Activating beta features for apps](/developers/apps/activating-beta-features-for-apps)."{% endif %} diff --git a/content/developers/apps/suspending-a-github-app-installation.md b/content/developers/apps/suspending-a-github-app-installation.md index ac4cf127d3..34b6df1e3b 100644 --- a/content/developers/apps/suspending-a-github-app-installation.md +++ b/content/developers/apps/suspending-a-github-app-installation.md @@ -7,13 +7,11 @@ versions: free-pro-team: '*' --- -{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" %} {% note %} **Note:** {% data reusables.pre-release-program.suspend-installation-beta %} {% endnote %} -{% endif %} ### Suspending a GitHub App diff --git a/content/developers/overview/managing-deploy-keys.md b/content/developers/overview/managing-deploy-keys.md index b1c958c590..d97cec6391 100644 --- a/content/developers/overview/managing-deploy-keys.md +++ b/content/developers/overview/managing-deploy-keys.md @@ -44,7 +44,9 @@ If you don't want to use SSH keys, you can use [HTTPS with OAuth tokens][git-aut * Users don't have to change their local SSH settings. * Multiple tokens (one for each user) are not needed; one token per server is enough. * A token can be revoked at any time, turning it essentially into a one-use password. +{% if enterpriseServerVersions contains currentVersion %} * Generating new tokens can be easily scripted using [the OAuth API](/rest/reference/oauth-authorizations#create-a-new-authorization). +{% endif %} ##### Cons diff --git a/content/developers/webhooks-and-events/webhook-events-and-payloads.md b/content/developers/webhooks-and-events/webhook-events-and-payloads.md index 922e672102..a2dfc5a9d7 100644 --- a/content/developers/webhooks-and-events/webhook-events-and-payloads.md +++ b/content/developers/webhooks-and-events/webhook-events-and-payloads.md @@ -430,7 +430,7 @@ Key | Type | Description {% endnote %} -{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %} +{% if currentVersion == "free-pro-team@latest" %} {% note %} **Note:** {% data reusables.pre-release-program.suspend-installation-beta %} For more information, see "[Suspending a {% data variables.product.prodname_github_app %} installation](/apps/managing-github-apps/suspending-a-github-app-installation/)." @@ -1124,9 +1124,11 @@ Key | Type | Description {{ webhookPayloadsForCurrentVersion.secret_scanning_alert.reopened }} {% endif %} +{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@1.19" %} ### security_advisory Activity related to a security advisory. A security advisory provides information about security-related vulnerabilities in software on GitHub. The security advisory dataset also powers the GitHub security alerts, see "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies/)." +{% endif %} #### Availability diff --git a/content/rest/overview/libraries.md b/content/rest/overview/libraries.md index f72b9b3b65..68c9ab862b 100644 --- a/content/rest/overview/libraries.md +++ b/content/rest/overview/libraries.md @@ -7,6 +7,7 @@ redirect_from: versions: free-pro-team: '*' enterprise-server: '*' + github-ae: '*' ---
diff --git a/content/rest/reference/enterprise-admin.md b/content/rest/reference/enterprise-admin.md index a35266becc..1f8fda8b12 100644 --- a/content/rest/reference/enterprise-admin.md +++ b/content/rest/reference/enterprise-admin.md @@ -40,7 +40,7 @@ http(s)://hostname/ {% if currentVersion == "github-ae@latest" or enterpriseServerVersions contains currentVersion %} ### Authentication -Your {% data variables.product.product_name %} installation's API endpoints accept [the same authentication methods](/rest/overview/resources-in-the-rest-api#authentication) as the GitHub.com API. You can authenticate yourself with **[OAuth tokens](/apps/building-integrations/setting-up-and-registering-oauth-apps/)** (which can be created using the [Authorizations API](/rest/reference/oauth-authorizations#create-a-new-authorization)) or **[basic authentication](/rest/overview/resources-in-the-rest-api#basic-authentication)**. {% if enterpriseServerVersions contains currentVersion %} +Your {% data variables.product.product_name %} installation's API endpoints accept [the same authentication methods](/rest/overview/resources-in-the-rest-api#authentication) as the GitHub.com API. You can authenticate yourself with **[OAuth tokens](/apps/building-integrations/setting-up-and-registering-oauth-apps/)** {% if enterpriseServerVersions contains currentVersion %}(which can be created using the [Authorizations API](/rest/reference/oauth-authorizations#create-a-new-authorization)) {% endif %}or **[basic authentication](/rest/overview/resources-in-the-rest-api#basic-authentication)**. {% if enterpriseServerVersions contains currentVersion %} OAuth tokens must have the `site_admin` [OAuth scope](/developers/apps/scopes-for-oauth-apps#available-scopes) when used with Enterprise-specific endpoints.{% endif %} Enterprise administration API endpoints are only accessible to authenticated {% data variables.product.product_name %} site administrators{% if enterpriseServerVersions contains currentVersion %}, except for the [Management Console](#management-console) API, which requires the [Management Console password](/enterprise/admin/articles/accessing-the-management-console/){% endif %}. diff --git a/data/reusables/pre-release-program/suspend-installation-beta.md b/data/reusables/pre-release-program/suspend-installation-beta.md index 42ef3db1bc..2bab45542f 100644 --- a/data/reusables/pre-release-program/suspend-installation-beta.md +++ b/data/reusables/pre-release-program/suspend-installation-beta.md @@ -1,4 +1 @@ -{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %} Suspending a {% data variables.product.prodname_github_app %} installation is currently in beta and subject to change. Before you can suspend a {% data variables.product.prodname_github_app %}, the app owner must enable suspending installations for the app by opting-in to the beta. To opt-in to the suspending installations beta feature, see "[Activating beta features for apps](/developers/apps/activating-beta-features-for-apps)." - -{% endif %} From 9973c61b41a1d9ed2b44c7623b59b5f17d31164e Mon Sep 17 00:00:00 2001 From: Sarah Edwards Date: Thu, 10 Dec 2020 14:25:14 -0800 Subject: [PATCH 12/21] fix broken links (#16881) --- .../pricing-plans-for-github-marketplace-apps.md | 2 +- .../github-marketplace/requirements-for-listing-an-app.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/developers/github-marketplace/pricing-plans-for-github-marketplace-apps.md b/content/developers/github-marketplace/pricing-plans-for-github-marketplace-apps.md index aa9d924b49..8e76577810 100644 --- a/content/developers/github-marketplace/pricing-plans-for-github-marketplace-apps.md +++ b/content/developers/github-marketplace/pricing-plans-for-github-marketplace-apps.md @@ -48,7 +48,7 @@ Customers can start a free trial for any paid plan on a Marketplace listing that Free trials have a fixed length of 14 days. Customers are notified 4 days before the end of their trial period (on day 11 of the free trial) that their plan will be upgraded. At the end of a free trial, customers will be auto-enrolled into the plan they are trialing if they do not cancel. -For more information, see: "[Handling new purchases and free trials](/developers/github-marketplace/integrating-with-the-github-marketplace-api/handling-new-purchases-and-free-trials/)." +For more information, see: "[Handling new purchases and free trials](/developers/github-marketplace/handling-new-purchases-and-free-trials/)." {% note %} diff --git a/content/developers/github-marketplace/requirements-for-listing-an-app.md b/content/developers/github-marketplace/requirements-for-listing-an-app.md index e54424b74a..39b4c2dc99 100644 --- a/content/developers/github-marketplace/requirements-for-listing-an-app.md +++ b/content/developers/github-marketplace/requirements-for-listing-an-app.md @@ -53,7 +53,7 @@ In addition to the requirements for all apps above, each app that you offer as a - {% data variables.product.prodname_github_app %}s should have a minimum of 100 installations. - {% data variables.product.prodname_oauth_app %}s should have a minimum of 200 users. - All paid apps must handle {% data variables.product.prodname_marketplace %} purchase events for new purchases, upgrades, downgrades, cancellations, and free trials. For more information, see "[Billing requirements for paid apps](#billing-requirements-for-paid-apps)" below. -- Publishing organizations must have a verified domain and must enable two-factor authentication. For more information, see "[Requiring two-factor authentication in your organization](/github/setting-up-and-managing-organizations-and-teams/requiring-two-factor-authentication-in-your-organization.") +- Publishing organizations must have a verified domain and must enable two-factor authentication. For more information, see "[Requiring two-factor authentication in your organization](/github/setting-up-and-managing-organizations-and-teams/requiring-two-factor-authentication-in-your-organization)." When you are ready to publish the app on {% data variables.product.prodname_marketplace %} you must request verification for the listing. From 05ecacaa933aef0c4f6772bea8d32905004fc72e Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 16 Dec 2020 21:24:26 -0500 Subject: [PATCH 13/21] change external /contact link to contact variable --- .../managing-your-subscriptions.md | 2 +- .../watching-and-unwatching-repositories.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/github/managing-subscriptions-and-notifications-on-github/managing-your-subscriptions.md b/content/github/managing-subscriptions-and-notifications-on-github/managing-your-subscriptions.md index e1a631af52..a0a9f79168 100644 --- a/content/github/managing-subscriptions-and-notifications-on-github/managing-your-subscriptions.md +++ b/content/github/managing-subscriptions-and-notifications-on-github/managing-your-subscriptions.md @@ -11,7 +11,7 @@ To help you understand your subscriptions and decide whether to unsubscribe, see {% note %} -**Note:** Instead of unsubscribing, you have the option to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you're experiencing abuse and want to ignore a repository, please [contact support](/contact) so we can help. {% data reusables.policies.abuse %}{% endif %} +**Note:** Instead of unsubscribing, you have the option to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you're experiencing abuse and want to ignore a repository, please contact {% data variables.contact.contact_support %} so we can help. {% data reusables.policies.abuse %}{% endif %} {% endnote %} diff --git a/content/github/receiving-notifications-about-activity-on-github/watching-and-unwatching-repositories.md b/content/github/receiving-notifications-about-activity-on-github/watching-and-unwatching-repositories.md index 831b6f6936..716fb5541a 100644 --- a/content/github/receiving-notifications-about-activity-on-github/watching-and-unwatching-repositories.md +++ b/content/github/receiving-notifications-about-activity-on-github/watching-and-unwatching-repositories.md @@ -39,7 +39,7 @@ You can also watch and unwatch releases in a repository. For more information, s {% note %} -**Note:** You can also choose to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you experiencing abuse and want to ignore a repository, please [contact support](/contact) so we can help. {% data reusables.policies.abuse %}{% endif %} +**Note:** You can also choose to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you experiencing abuse and want to ignore a repository, please contact {% data variables.contact.contact_support %} so we can help. {% data reusables.policies.abuse %}{% endif %} {% endnote %} From 8d84a90e6fa9b532fa6c36aceef345882a82b4c2 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 16 Dec 2020 21:24:50 -0500 Subject: [PATCH 14/21] do more path checking --- lib/path-utils.js | 51 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/path-utils.js b/lib/path-utils.js index 02e75aab1d..cd5077a170 100644 --- a/lib/path-utils.js +++ b/lib/path-utils.js @@ -4,6 +4,8 @@ const patterns = require('./patterns') const { deprecated } = require('./enterprise-server-releases') const allProducts = require('./all-products') const allVersions = require('./all-versions') +const supportedVersions = Object.keys(allVersions) +const supportedPlans = Object.values(allVersions).map(v => v.plan) const { getNewVersionedPath } = require('./old-versions-utils') // construct appropriate versioned path for any given HREF @@ -22,10 +24,13 @@ function getVersionedPathWithoutLanguage (href, version) { // example: enterprise-server@2.22 or free-pro-team@latest let versionFromPath = getVersionStringFromPath(href) - // if the version found is not a currently supported version... + // update the path with the full version so we can go through the rest of the checks + href = href.replace(href.split('/')[1], versionFromPath) + + // if the version found is NOT a currently supported version... let productObjectFromPath - if (!Object.keys(allVersions).includes(versionFromPath)) { - // first check if the first segment is instead a current product; + if (![...supportedPlans, ...supportedVersions, 'enterprise-server@latest'].includes(versionFromPath)) { + // first check if the first segment is instead a current product; // example: /admin/foo or /desktop/foo productObjectFromPath = allProducts[versionFromPath] @@ -78,26 +83,45 @@ function getPathWithoutLanguage (href) { return slash(newHref) } +// Remove the version segment from the path function getPathWithoutVersion (href) { return href.replace(`/${getVersionStringFromPath(href)}`, '') } +// Return the version segment in a path function getVersionStringFromPath (href) { href = getPathWithoutLanguage(href) - const versionString = href.split('/')[1] - return versionString || 'homepage' + const versionFromPath = href.split('/')[1] + + // return checkVersionFromPath(href.split('/')[1], href) + if (allVersions[versionFromPath]) { + return versionFromPath + } + + // if the version segment is the latest enterprise-server release, return the latest release + if (versionFromPath === 'enterprise-server@latest') { + const enterpriseServerObject = Object.values(allVersions).find(v => v.plan === 'enterprise-server') + return allVersions[enterpriseServerObject.latestVersion].version + } + + // if it's just a plan with no @release, find the plan's latest release + const planObject = Object.values(allVersions).find(v => v.plan === versionFromPath) + if (planObject) { + return allVersions[planObject.latestVersion].version + } + + return versionFromPath || 'homepage' } +// Return the corresponding object for the version segment in a path function getVersionObjectFromPath (href) { - const versionId = getVersionStringFromPath(href) - const version = allVersions[versionId] + const versionFromPath = getVersionStringFromPath(href) - if (!version) throw new Error(`No version found for ${href}`) - - return version + return allVersions[versionFromPath] } +// Return the product segment from the path function getProductStringFromPath (href) { href = getPathWithoutLanguage(href) const productString = href.split('/')[2] @@ -105,10 +129,11 @@ function getProductStringFromPath (href) { return productString || 'homepage' } +// Return the corresponding object for the product segment in a path function getProductObjectFromPath (href) { - const productId = getProductStringFromPath(href) - // Return undefined if product id derived from path can't be found in allProducts - return allProducts[productId] + const productFromPath = getProductStringFromPath(href) + + return allProducts[productFromPath] } module.exports = { From 51a4b9afbb14191ba32d7e83c8122c2f700820ea Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Thu, 17 Dec 2020 09:59:05 -0500 Subject: [PATCH 15/21] fix double slash typo in links --- .../github/authenticating-to-github/authorizing-oauth-apps.md | 2 +- .../connecting-with-third-party-applications.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/github/authenticating-to-github/authorizing-oauth-apps.md b/content/github/authenticating-to-github/authorizing-oauth-apps.md index a63b81f180..dca4ac4c69 100644 --- a/content/github/authenticating-to-github/authorizing-oauth-apps.md +++ b/content/github/authenticating-to-github/authorizing-oauth-apps.md @@ -38,7 +38,7 @@ When an {% data variables.product.prodname_oauth_app %} wants to identify you by *Scopes* are named groups of permissions that an {% data variables.product.prodname_oauth_app %} can request to access both public and non-public data. -When you want to use an {% data variables.product.prodname_oauth_app %} that integrates with {% data variables.product.product_name %}, that app lets you know what type of access to your data will be required. If you grant access to the app, then the app will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](//apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)." +When you want to use an {% data variables.product.prodname_oauth_app %} that integrates with {% data variables.product.product_name %}, that app lets you know what type of access to your data will be required. If you grant access to the app, then the app will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)." {% tip %} diff --git a/content/github/authenticating-to-github/connecting-with-third-party-applications.md b/content/github/authenticating-to-github/connecting-with-third-party-applications.md index e792daf5ec..5af7c4e731 100644 --- a/content/github/authenticating-to-github/connecting-with-third-party-applications.md +++ b/content/github/authenticating-to-github/connecting-with-third-party-applications.md @@ -32,7 +32,7 @@ Applications can have *read* or *write* access to your {% data variables.product *Scopes* are named groups of permissions that an application can request to access both public and non-public data. -When you want to use a third-party application that integrates with {% data variables.product.product_name %}, that application lets you know what type of access to your data will be required. If you grant access to the application, then the application will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](//apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)." +When you want to use a third-party application that integrates with {% data variables.product.product_name %}, that application lets you know what type of access to your data will be required. If you grant access to the application, then the application will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)." {% tip %} From 78895184a7ddd7d8fd9cecbcb287312f903b44cb Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Thu, 17 Dec 2020 10:19:44 -0500 Subject: [PATCH 16/21] updates for clarity --- lib/path-utils.js | 22 +++++++++++++++------- tests/helpers/links-checker.js | 4 +++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/path-utils.js b/lib/path-utils.js index 3896baeaa6..43b08fd4c2 100644 --- a/lib/path-utils.js +++ b/lib/path-utils.js @@ -1,7 +1,7 @@ const slash = require('slash') const path = require('path') const patterns = require('./patterns') -const { deprecated } = require('./enterprise-server-releases') +const { deprecated, latest } = require('./enterprise-server-releases') const allProducts = require('./all-products') const allVersions = require('./all-versions') const supportedVersions = new Set(Object.keys(allVersions)) @@ -23,7 +23,7 @@ function getVersionedPathWithoutLanguage (href, version) { // example: enterprise-server@2.22 or free-pro-team@latest let versionFromPath = getVersionStringFromPath(href) - // update the path with the full version so we can go through the rest of the checks + // if a real version was found, add it to the path so we can go through the rest of the checks if (supportedVersions.has(versionFromPath)) { href = href.replace(href.split('/')[1], versionFromPath) } @@ -36,6 +36,7 @@ function getVersionedPathWithoutLanguage (href, version) { productObjectFromPath = allProducts[versionFromPath] // if so, add the first supported version for that product to the href + // (this is just to get a path with all the expected segments; the version will be updated later if needed) if (productObjectFromPath) { href = path.join('/', productObjectFromPath.versions[0], href) versionFromPath = productObjectFromPath.versions[0] @@ -89,25 +90,32 @@ function getPathWithoutVersion (href) { function getVersionStringFromPath (href) { href = getPathWithoutLanguage(href) + // return immediately if this is a link to the homepage + if (href === '/') { + return 'homepage' + } + + // check if the first segment is a supported version const versionFromPath = href.split('/')[1] - if (allVersions[versionFromPath]) { + if (supportedVersions.has(versionFromPath)) { return versionFromPath } // if the version segment is the latest enterprise-server release, return the latest release if (versionFromPath === 'enterprise-server@latest') { - const enterpriseServerObject = Object.values(allVersions).find(v => v.plan === 'enterprise-server') - return allVersions[enterpriseServerObject.latestVersion].version + return `enterprise-server@${latest}` } - // if it's just a plan with no @release, find the plan's latest release + // if it's just a plan with no @release (e.g., `enterprise-server`), return the latest release const planObject = Object.values(allVersions).find(v => v.plan === versionFromPath) if (planObject) { return allVersions[planObject.latestVersion].version } - return versionFromPath || 'homepage' + // otherwise, return the first segment as-is, which may not be a proper version + // but additional checks are done on this segment in getVersionedPathWithoutLanguage + return versionFromPath } // Return the corresponding object for the version segment in a path diff --git a/tests/helpers/links-checker.js b/tests/helpers/links-checker.js index ca69d76644..815823c8ba 100644 --- a/tests/helpers/links-checker.js +++ b/tests/helpers/links-checker.js @@ -145,8 +145,10 @@ class LinksChecker { if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue // ------ END ONEOFF EXCLUSIONS -------/// - // look for linked page + // the link at this point should include a version via lib/rewrite-local-links const versionFromHref = getVersionStringFromPath(link) + + // look for linked page const linkedPage = findPageInVersion(link, context.pages, context.redirects, this.languageCode, versionFromHref) this.checkedLinksCache.add(link) From ae9beaae5bb0f0300ec71136b3894e0f4d8a59f6 Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:25:40 -0500 Subject: [PATCH 17/21] Remove DE language from staging (#17038) Co-authored-by: chiedo --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index ce6f572a46..d7fa376e93 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "env": { "NODE_ENV": "production", "NPM_CONFIG_PRODUCTION": "true", - "ENABLED_LANGUAGES": "en, de" + "ENABLED_LANGUAGES": "en" }, "buildpacks": [ { "url": "https://github.com/DataDog/heroku-buildpack-datadog.git#1.21" }, From 4262cae231e42bc959662aa248f08c56520bdf13 Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:42:46 -0500 Subject: [PATCH 18/21] Remove DataDog from Staging (#17036) Remove DataDog from staging Co-authored-by: chiedo --- app.json | 1 - 1 file changed, 1 deletion(-) diff --git a/app.json b/app.json index d7fa376e93..4373896fff 100644 --- a/app.json +++ b/app.json @@ -6,7 +6,6 @@ "ENABLED_LANGUAGES": "en" }, "buildpacks": [ - { "url": "https://github.com/DataDog/heroku-buildpack-datadog.git#1.21" }, { "url": "heroku/nodejs" } ], "formation": { From 75deecd386e22a1f39b6f215aaa12dcccdc4716a Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Thu, 17 Dec 2020 10:50:52 -0500 Subject: [PATCH 19/21] add some clarifying comments --- lib/path-utils.js | 49 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/path-utils.js b/lib/path-utils.js index 43b08fd4c2..d20a800ebc 100644 --- a/lib/path-utils.js +++ b/lib/path-utils.js @@ -7,41 +7,46 @@ const allVersions = require('./all-versions') const supportedVersions = new Set(Object.keys(allVersions)) const { getNewVersionedPath } = require('./old-versions-utils') -// construct appropriate versioned path for any given HREF +// This function constructs an appropriate versioned path for any given HREF. +// NOTE: this gets called by findPage and various other functions, and +// has to return a proper versioned link given a wide variety of incoming +// modern or legacy-formatted links, so it is somewhat overloaded. At some point +// this could probably be broken up into separate functions to handle different incoming +// paths. But it is currently optimized to handle lots of edge cases. function getVersionedPathWithoutLanguage (href, version) { - // start clean without language code or trailing slash + // Start clean without language code or trailing slash href = getPathWithoutLanguage(href.replace(patterns.trailingSlash, '$1')) - // if this is an old versioned path that includes a deprecated version, do not change! + // If this is an old versioned path that includes a deprecated version, do not change! // example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release const oldEnterpriseVersionNumber = href.match(patterns.getEnterpriseVersionNumber) if (oldEnterpriseVersionNumber && deprecated.includes(oldEnterpriseVersionNumber[1])) { return href } - // try to derive the current version from the path + // Try to derive the current version from the path // example: enterprise-server@2.22 or free-pro-team@latest let versionFromPath = getVersionStringFromPath(href) - // if a real version was found, add it to the path so we can go through the rest of the checks + // If a supported version was found, add it to the path so we can go through the rest of the checks if (supportedVersions.has(versionFromPath)) { href = href.replace(href.split('/')[1], versionFromPath) } - // if the version found is NOT a currently supported version... + // If a currently supported version was NOT found... let productObjectFromPath if (!supportedVersions.has(versionFromPath)) { - // first check if the first segment is instead a current product; + // First check if the segment is instead a current product; // example: /admin/foo or /desktop/foo productObjectFromPath = allProducts[versionFromPath] - // if so, add the first supported version for that product to the href + // If so, add the first supported version for that product to the href // (this is just to get a path with all the expected segments; the version will be updated later if needed) if (productObjectFromPath) { href = path.join('/', productObjectFromPath.versions[0], href) versionFromPath = productObjectFromPath.versions[0] } else { - // otherwise, this may be an old path that should be converted to new path; + // Otherwise, this may be an old path that should be converted to new path; // OLD: /enterprise/2.22/admin/installation OR /enterprise/admin/installation // NEW: /enterprise-server@2.22/admin/installation href = getNewVersionedPath(href) @@ -49,34 +54,34 @@ function getVersionedPathWithoutLanguage (href, version) { } } - // if not previously found, derive the product object from the path (e.g., github or admin) + // If not previously found, derive the product object from the path (e.g., github or admin) if (!productObjectFromPath) { productObjectFromPath = getProductObjectFromPath(href) } - // if the product's versions don't include the specified version, nothing to change! + // If the product's versions don't include the specified version, nothing to change! if (productObjectFromPath && !productObjectFromPath.versions.includes(version)) { return slash(href) } - // update the version + // Update the version and return the path return slash(href.replace(versionFromPath, version)) } -// add language code +// Add language code to a versioned path function getVersionedPathWithLanguage (href, version, languageCode) { return getPathWithLanguage(getVersionedPathWithoutLanguage(href, version), languageCode) } -// add the language to the given HREF -// /en/articles/foo -> /articles/foo +// Add the language to the given HREF +// /articles/foo -> /en/articles/foo function getPathWithLanguage (href, languageCode) { return slash(path.posix.join('/', languageCode, getPathWithoutLanguage(href))) .replace(patterns.trailingSlash, '$1') } -// remove the language from the given HREF -// /articles/foo -> /en/articles/foo +// Remove the language from the given HREF +// /en/articles/foo -> /articles/foo function getPathWithoutLanguage (href) { return slash(href.replace(patterns.hasLanguageCode, '/')) } @@ -90,30 +95,30 @@ function getPathWithoutVersion (href) { function getVersionStringFromPath (href) { href = getPathWithoutLanguage(href) - // return immediately if this is a link to the homepage + // Return immediately if this is a link to the homepage if (href === '/') { return 'homepage' } - // check if the first segment is a supported version + // Check if the first segment is a supported version const versionFromPath = href.split('/')[1] if (supportedVersions.has(versionFromPath)) { return versionFromPath } - // if the version segment is the latest enterprise-server release, return the latest release + // If the version segment is the latest enterprise-server release, return the latest release if (versionFromPath === 'enterprise-server@latest') { return `enterprise-server@${latest}` } - // if it's just a plan with no @release (e.g., `enterprise-server`), return the latest release + // If it's just a plan with no @release (e.g., `enterprise-server`), return the latest release const planObject = Object.values(allVersions).find(v => v.plan === versionFromPath) if (planObject) { return allVersions[planObject.latestVersion].version } - // otherwise, return the first segment as-is, which may not be a proper version + // Otherwise, return the first segment as-is, which may not be a real supported version, // but additional checks are done on this segment in getVersionedPathWithoutLanguage return versionFromPath } From d31d89c62bf2b2f229c671abd39d9c3c0480cfa7 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Thu, 17 Dec 2020 10:51:07 -0500 Subject: [PATCH 20/21] borrow initial findPage lookup from #16965 --- lib/find-page.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/find-page.js b/lib/find-page.js index f859c6db9f..03e2b10c9a 100644 --- a/lib/find-page.js +++ b/lib/find-page.js @@ -8,6 +8,10 @@ module.exports = function findPage (href, pageMap, redirects = {}, languageCode // remove trailing slash href = slash(href).replace(patterns.trailingSlash, '$1') + // do an initial lookup on the path as-is + let page = pageMap[removeFragment(href)] + if (page) return page + // check all potential versions const versionedPathsToCheck = [...new Set(allVersions.map(version => { return getVersionedPathWithLanguage(href, version, languageCode) @@ -22,8 +26,8 @@ module.exports = function findPage (href, pageMap, redirects = {}, languageCode // need to account for redirects again pathToPage = redirects[pathToPage] || pathToPage - // find the page - const page = pageMap[removeFragment(pathToPage)] + // try finding the page again + page = pageMap[removeFragment(pathToPage)] if (page) return page From 92d85b8d4a2542d26101110a1fabee80d6636c86 Mon Sep 17 00:00:00 2001 From: Shati Patel <42641846+shati-patel@users.noreply.github.com> Date: Thu, 17 Dec 2020 15:58:26 +0000 Subject: [PATCH 21/21] Code scanning docs: small fixes (#17035) --- .../running-codeql-code-scanning-in-your-ci-system.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/github/finding-security-vulnerabilities-and-errors-in-your-code/running-codeql-code-scanning-in-your-ci-system.md b/content/github/finding-security-vulnerabilities-and-errors-in-your-code/running-codeql-code-scanning-in-your-ci-system.md index 3c26119a81..79fdf066f1 100644 --- a/content/github/finding-security-vulnerabilities-and-errors-in-your-code/running-codeql-code-scanning-in-your-ci-system.md +++ b/content/github/finding-security-vulnerabilities-and-errors-in-your-code/running-codeql-code-scanning-in-your-ci-system.md @@ -53,7 +53,7 @@ On Windows, the `codeql-runner-win.exe` file usually requires no change to permi Once you have downloaded the {% data variables.product.prodname_codeql_runner %} and verified that it can be executed, you should make the runner available to each CI server that you intend to use for {% data variables.product.prodname_code_scanning %}. It is important to notice that each CI server that you intend to use for {% data variables.product.prodname_code_scanning %} needs to have the {% data variables.product.prodname_codeql_runner %}. You might configure each server to copy the runner from a central, internal location, or you could use the REST API to get the runner direct from GitHub, for example: ```shell -wget https://github.com/github/codeql-action/releases/download/codeql-bundle-20200826/codeql-runner-linux +wget https://github.com/github/codeql-action/releases/latest/download/codeql-runner-linux chmod +x codeql-runner-linux ``` @@ -127,7 +127,7 @@ This example is similar to the previous example, however this time the repositor > ... > CodeQL environment output to "/srv/checkout/example-repo-2/codeql-runner/codeql-env.json" and "/srv/checkout/example-repo-2/codeql-runner/codeql-env.sh". - Please export these variables to future processes so the build can be traced, for example by running " + Please export these variables to future processes so that CodeQL can monitor the build, for example by running " . /srv/checkout/example-repo-2/codeql-runner/codeql-env.sh". ```