From 08d548a9782337f53f1b0d496e7ba037e7884162 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 25 Nov 2020 20:03:49 -0500 Subject: [PATCH 01/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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 dca55a0257c734eae1d5fb2fa7cf0601411e7288 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Mon, 14 Dec 2020 20:25:01 -0500 Subject: [PATCH 13/40] Revert "Revert "sitetree optimizations"" --- includes/breadcrumbs.html | 2 +- includes/sidebar-guides.html | 8 ++--- includes/sidebar-specific-product.html | 10 +++--- lib/find-page.js | 8 +++-- lib/get-map-topic-content.js | 22 ++++--------- lib/page.js | 3 +- lib/site-tree.js | 43 ++++++++++++-------------- middleware/breadcrumbs.js | 22 +++---------- middleware/early-access-breadcrumbs.js | 6 ++-- tests/content/site-tree.js | 12 +++---- tests/rendering/breadcrumbs.js | 26 ++++++++-------- 11 files changed, 70 insertions(+), 92 deletions(-) diff --git a/includes/breadcrumbs.html b/includes/breadcrumbs.html index 47dc43c17a..9fdc33acc4 100644 --- a/includes/breadcrumbs.html +++ b/includes/breadcrumbs.html @@ -3,7 +3,7 @@ {% if breadcrumb[1].href == '' %} {{breadcrumb[1].title}} {% else %} - + {{breadcrumb[1].title}} {% endif %} {% endfor %} diff --git a/includes/sidebar-guides.html b/includes/sidebar-guides.html index c24e084944..ef2e81b62b 100644 --- a/includes/sidebar-guides.html +++ b/includes/sidebar-guides.html @@ -1,17 +1,17 @@