From 526c05f4ce6f236b5d1aa9c482b8ad8432c76b7f Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Mon, 7 Dec 2020 11:32:39 -0500 Subject: [PATCH 01/12] support hardcoded versions in links --- lib/rewrite-local-links.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/rewrite-local-links.js b/lib/rewrite-local-links.js index 4854a739cd10..e0f4a12b1ece 100644 --- a/lib/rewrite-local-links.js +++ b/lib/rewrite-local-links.js @@ -2,6 +2,7 @@ const externalRedirects = Object.keys(require('./redirects/external-sites')) const pathUtils = require('./path-utils') const assert = require('assert') const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version') +const supportedPlans = Object.values(require('./all-versions')).map(v => v.plan) // Content authors write links like `/some/article/path`, but they need to be // rewritten on the fly to match the current language and page version @@ -24,9 +25,17 @@ function getNewHref (link, languageCode, version) { // e.g. `/contact` should not be replaced with `/en/contact` if (externalRedirects.includes(href)) return + let newHref + + // If the link has a hardcoded plan name in it (e.g., /enterprise-server/rest/reference/oauth-authorizations), + // only rewrite it with a language code + if (supportedPlans.includes(href.split('/')[1])) { + newHref = pathUtils.getPathWithLanguage(href, languageCode) + } + // If link is dotcom-only, just get the language code // Otherwise, get the versioned path with language code - const newHref = link.hasClass('dotcom-only') + if (!newHref) newHref = link.hasClass('dotcom-only') ? pathUtils.getVersionedPathWithLanguage(href, nonEnterpriseDefaultVersion, languageCode) : pathUtils.getVersionedPathWithLanguage(href, version, languageCode) From ba2ebcfb30fb4b72da61dcacbf8ef7a14afb3ddf Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Mon, 7 Dec 2020 11:32:55 -0500 Subject: [PATCH 02/12] remove version number and leave plan name in Enterprise link --- content/rest/overview/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/rest/overview/troubleshooting.md b/content/rest/overview/troubleshooting.md index 0a22fab27b13..a430cfd7be44 100644 --- a/content/rest/overview/troubleshooting.md +++ b/content/rest/overview/troubleshooting.md @@ -63,7 +63,7 @@ curl -H 'Authorization: token my-oauth-token' https://api.github.com/user/repos #### Calls to OAuth Authorizations API -If you're making [OAuth Authorization API](/enterprise-server@2.22/rest/reference/oauth-authorizations) calls to manage your OAuth app's authorizations or to generate access tokens, similar to this example: +If you're making [OAuth Authorization API](/enterprise-server/rest/reference/oauth-authorizations) calls to manage your OAuth app's authorizations or to generate access tokens, similar to this example: ```bash curl -u my_username:my_password -X POST "https://api.github.com/authorizations" -d '{"scopes":["public_repo"], "note":"my token", "client_id":"my_client_id", "client_secret":"my_client_secret"}' From e2d114b96bf342001490874c6b27044d04c86ac8 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Tue, 8 Dec 2020 11:47:14 -0500 Subject: [PATCH 03/12] lint --- lib/rewrite-local-links.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rewrite-local-links.js b/lib/rewrite-local-links.js index e0f4a12b1ece..2cd18221c076 100644 --- a/lib/rewrite-local-links.js +++ b/lib/rewrite-local-links.js @@ -35,9 +35,11 @@ function getNewHref (link, languageCode, version) { // If link is dotcom-only, just get the language code // Otherwise, get the versioned path with language code - if (!newHref) newHref = link.hasClass('dotcom-only') - ? pathUtils.getVersionedPathWithLanguage(href, nonEnterpriseDefaultVersion, languageCode) - : pathUtils.getVersionedPathWithLanguage(href, version, languageCode) + if (!newHref) { + newHref = link.hasClass('dotcom-only') + ? pathUtils.getVersionedPathWithLanguage(href, nonEnterpriseDefaultVersion, languageCode) + : pathUtils.getVersionedPathWithLanguage(href, version, languageCode) + } if (href !== newHref) link.attr('href', newHref) } From 62889986b60d75c928470188ceab815c17d4e652 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 9 Dec 2020 09:40:58 -0800 Subject: [PATCH 04/12] =?UTF-8?q?Revert=20"Revert=20"Speed=20up=20warmServ?= =?UTF-8?q?er=20by=20loading=20pages=20(and=20files)=20asyn=E2=80=A6=20(#1?= =?UTF-8?q?6837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "Speed up warmServer by loading pages (and files) asynchronously (#16752)" (#16835)" This reverts commit a14517115feba539b13492d91585944a36bd6523. * Move async to regular dependencies * Update package-lock.json --- lib/page.js | 25 ++++- lib/pages.js | 54 +++++----- package-lock.json | 3 +- package.json | 4 +- tests/browser/browser.js | 4 + tests/content/crowdin-config.js | 2 + tests/content/site-data-references.js | 2 + tests/routing/redirects.js | 10 +- tests/unit/find-page.js | 4 +- tests/unit/liquid-helpers.js | 2 + tests/unit/page.js | 140 +++++++++++++------------- tests/unit/pages.js | 2 + 12 files changed, 145 insertions(+), 107 deletions(-) diff --git a/lib/page.js b/lib/page.js index c282f1792fd1..f9274f0119e8 100644 --- a/lib/page.js +++ b/lib/page.js @@ -1,5 +1,5 @@ const assert = require('assert') -const fs = require('fs') +const fs = require('fs').promises const path = require('path') const cheerio = require('cheerio') const patterns = require('./patterns') @@ -23,15 +23,30 @@ const slash = require('slash') const statsd = require('./statsd') class Page { - constructor (opts) { + static async init (opts) { assert(opts.relativePath, 'relativePath is required') assert(opts.basePath, 'basePath is required') + + const relativePath = slash(opts.relativePath) + const fullPath = slash(path.join(opts.basePath, relativePath)) + const raw = await fs.readFile(fullPath, 'utf8') + + return new Page({ ...opts, relativePath, fullPath, raw }) + } + + static async exists (path) { + try { + return await fs.stat(path) + } catch (err) { + if (err.code === 'ENOENT') return false + console.error(err) + } + } + + constructor (opts) { assert(opts.languageCode, 'languageCode is required') Object.assign(this, { ...opts }) - this.relativePath = slash(this.relativePath) - this.fullPath = slash(path.join(this.basePath, this.relativePath)) - this.raw = fs.readFileSync(this.fullPath, 'utf8') // TODO remove this when crowdin-support issue 66 has been resolved if (this.languageCode !== 'en' && this.raw.includes(': verdadero')) { diff --git a/lib/pages.js b/lib/pages.js index cdca1fae1e2a..4db70e7dd5cb 100644 --- a/lib/pages.js +++ b/lib/pages.js @@ -2,42 +2,50 @@ const path = require('path') const walk = require('walk-sync').entries const Page = require('./page') const languages = require('./languages') -const fs = require('fs') +const { mapLimit, filterLimit } = require('async') +const FILE_READ_LIMIT = 500 async function loadPageList () { const pageList = [] // load english pages const englishPath = path.join(__dirname, '..', languages.en.dir, 'content') - const englishPages = walk(englishPath) - .filter(({ relativePath }) => { - return relativePath.endsWith('.md') && - !relativePath.includes('README') - }) - .map(fileData => new Page({ ...fileData, languageCode: languages.en.code })) - + const englishPaths = walk(englishPath) + .filter(({ relativePath }) => + relativePath.endsWith('.md') && !relativePath.includes('README') + ) + const englishPages = await mapLimit( + englishPaths, + FILE_READ_LIMIT, + async fileData => await Page.init({ ...fileData, languageCode: languages.en.code }) + ) pageList.push(...englishPages) // load matching pages in other languages - for (const page of englishPages) { - for (const language of Object.values(languages)) { - if (language.code === 'en') continue - + let localizedPaths = Object.values(languages) + .filter(({ code }) => code !== 'en') + .map(language => { const basePath = path.join(__dirname, '..', language.dir, 'content') - const localizedPath = path.join(basePath, page.relativePath) - try { - fs.statSync(localizedPath) - } catch (_) { - continue - } - - pageList.push(new Page({ - relativePath: page.relativePath, + return englishPages.map(page => ({ basePath, + relativePath: page.relativePath, + localizedPath: path.join(basePath, page.relativePath), languageCode: language.code })) - } - } + }) + .flat() + localizedPaths = await filterLimit( + localizedPaths, + FILE_READ_LIMIT, + async ({ localizedPath }) => Page.exists(localizedPath) + ) + const localizedPages = await mapLimit( + localizedPaths, + FILE_READ_LIMIT, + async ({ basePath, relativePath, languageCode }) => + await Page.init({ basePath, relativePath, languageCode }) + ) + pageList.push(...localizedPages) return pageList } diff --git a/package-lock.json b/package-lock.json index b33963cf8bda..b0f2a9c3e7e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5577,8 +5577,7 @@ "async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", - "dev": true + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" }, "async-each": { "version": "1.0.3", diff --git a/package.json b/package.json index bb0f35f58899..a0cae4097a62 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@primer/css": "^15.1.0", "@primer/octicons": "^11.0.0", "algoliasearch": "^3.35.1", + "async": "^3.2.0", "babel-loader": "^8.1.0", "babel-preset-env": "^1.7.0", "browser-date-formatter": "^3.0.3", @@ -94,7 +95,6 @@ "devDependencies": { "@actions/core": "^1.2.6", "ajv": "^6.11.0", - "async": "^3.2.0", "await-sleep": "0.0.1", "aws-sdk": "^2.610.0", "babel-eslint": "^10.1.0", @@ -181,4 +181,4 @@ "pre-push": "npm run prevent-pushes-to-main" } } -} \ No newline at end of file +} diff --git a/tests/browser/browser.js b/tests/browser/browser.js index 64fdb083ab33..aca6b864a720 100644 --- a/tests/browser/browser.js +++ b/tests/browser/browser.js @@ -3,6 +3,8 @@ const sleep = require('await-sleep') const querystring = require('querystring') describe('homepage', () => { + jest.setTimeout(60 * 1000) + test('should be titled "GitHub Documentation"', async () => { await page.goto('http://localhost:4001') await expect(page.title()).resolves.toMatch('GitHub Documentation') @@ -10,6 +12,8 @@ describe('homepage', () => { }) describe('algolia browser search', () => { + jest.setTimeout(60 * 1000) + it('works on the homepage', async () => { await page.goto('http://localhost:4001/en') await page.click('#search-input-container input[type="search"]') diff --git a/tests/content/crowdin-config.js b/tests/content/crowdin-config.js index 63301e09935d..fd26e74b42e7 100644 --- a/tests/content/crowdin-config.js +++ b/tests/content/crowdin-config.js @@ -4,6 +4,8 @@ const ignoredPagePaths = config.files[0].ignore const ignoredDataPaths = config.files[2].ignore describe('crowdin.yml config file', () => { + jest.setTimeout(60 * 1000) + let pages beforeAll(async (done) => { pages = await loadPages() diff --git a/tests/content/site-data-references.js b/tests/content/site-data-references.js index d9a38bc4e684..045e33843ba4 100644 --- a/tests/content/site-data-references.js +++ b/tests/content/site-data-references.js @@ -7,6 +7,8 @@ const fs = require('fs').promises const path = require('path') describe('data references', () => { + jest.setTimeout(60 * 1000) + let data, pages beforeAll(async (done) => { diff --git a/tests/routing/redirects.js b/tests/routing/redirects.js index 740cd67d55d0..b41a0888a8c1 100644 --- a/tests/routing/redirects.js +++ b/tests/routing/redirects.js @@ -17,8 +17,8 @@ describe('redirects', () => { done() }) - test('page.redirects is an array', () => { - const page = new Page({ + test('page.redirects is an array', async () => { + const page = await Page.init({ relativePath: 'github/collaborating-with-issues-and-pull-requests/about-branches.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -26,8 +26,8 @@ describe('redirects', () => { expect(isPlainObject(page.redirects)).toBe(true) }) - test('dotcom homepage page.redirects', () => { - const page = new Page({ + test('dotcom homepage page.redirects', async () => { + const page = await Page.init({ relativePath: 'github/index.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -41,7 +41,7 @@ describe('redirects', () => { }) test('converts single `redirect_from` strings values into arrays', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'github/collaborating-with-issues-and-pull-requests/about-conversations-on-github.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' diff --git a/tests/unit/find-page.js b/tests/unit/find-page.js index bb422df1b3e5..5772db5f010d 100644 --- a/tests/unit/find-page.js +++ b/tests/unit/find-page.js @@ -8,7 +8,7 @@ describe('find page', () => { jest.setTimeout(1000 * 1000) test('falls back to the English page if it can\'t find a localized page', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'page-that-does-not-exist-in-translations-dir.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' @@ -24,7 +24,7 @@ describe('find page', () => { }) test('follows redirects', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'page-with-redirects.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' diff --git a/tests/unit/liquid-helpers.js b/tests/unit/liquid-helpers.js index ee6b8d3ecf17..41c54a2e3d27 100644 --- a/tests/unit/liquid-helpers.js +++ b/tests/unit/liquid-helpers.js @@ -5,6 +5,8 @@ const { set } = require('lodash') const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version') describe('liquid helper tags', () => { + jest.setTimeout(60 * 1000) + const context = {} let pageMap beforeAll(async (done) => { diff --git a/tests/unit/page.js b/tests/unit/page.js index 5d1b46a0c3ad..44f98c94a567 100644 --- a/tests/unit/page.js +++ b/tests/unit/page.js @@ -15,14 +15,14 @@ const opts = { } describe('Page class', () => { - test('preserves file path info', () => { - const page = new Page(opts) + test('preserves file path info', async () => { + const page = await Page.init(opts) expect(page.relativePath).toBe('github/collaborating-with-issues-and-pull-requests/about-branches.md') expect(page.fullPath.includes(page.relativePath)).toBe(true) }) - test('does not error out on translated TOC with no links', () => { - const page = new Page({ + test('does not error out on translated TOC with no links', async () => { + const page = await Page.init({ relativePath: 'translated-toc-with-no-links-index.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'ja' @@ -31,30 +31,34 @@ describe('Page class', () => { }) describe('showMiniToc page property', () => { - const article = new Page({ - relativePath: 'sample-article.md', - basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' - }) + let article, articleWithFM, tocPage, mapTopic - const articleWithFM = new Page({ - showMiniToc: false, - relativePath: article.relativePath, - basePath: article.basePath, - languageCode: article.languageCode - }) + beforeAll(async () => { + article = await Page.init({ + relativePath: 'sample-article.md', + basePath: path.join(__dirname, '../fixtures'), + languageCode: 'en' + }) - const tocPage = new Page({ - relativePath: 'sample-toc-index.md', - basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' - }) + articleWithFM = await Page.init({ + showMiniToc: false, + relativePath: article.relativePath, + basePath: article.basePath, + languageCode: article.languageCode + }) + + tocPage = await Page.init({ + relativePath: 'sample-toc-index.md', + basePath: path.join(__dirname, '../fixtures'), + languageCode: 'en' + }) - const mapTopic = new Page({ - mapTopic: true, - relativePath: article.relativePath, - basePath: article.basePath, - languageCode: article.languageCode + mapTopic = await Page.init({ + mapTopic: true, + relativePath: article.relativePath, + basePath: article.basePath, + languageCode: article.languageCode + }) }) test('is true by default on articles', () => { @@ -76,7 +80,7 @@ describe('Page class', () => { describe('page.render(context)', () => { test('rewrites links to include the current language prefix and version', async () => { - const page = new Page(opts) + const page = await Page.init(opts) const context = { page: { version: nonEnterpriseDefaultVersion }, currentVersion: nonEnterpriseDefaultVersion, @@ -99,7 +103,7 @@ describe('Page class', () => { }) test('rewrites links in the intro to include the current language prefix and version', async () => { - const page = new Page(opts) + const page = await Page.init(opts) page.rawIntro = '[Pull requests](/articles/about-pull-requests)' const context = { page: { version: nonEnterpriseDefaultVersion }, @@ -114,7 +118,7 @@ describe('Page class', () => { }) test('does not rewrite links that include deprecated enterprise release numbers', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'admin/enterprise-management/migrating-from-github-enterprise-1110x-to-2123.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -133,7 +137,7 @@ describe('Page class', () => { }) test('does not rewrite links to external redirects', async () => { - const page = new Page(opts) + const page = await Page.init(opts) page.markdown = `${page.markdown}\n\nSee [Capistrano](/capistrano).` const context = { page: { version: nonEnterpriseDefaultVersion }, @@ -150,7 +154,7 @@ describe('Page class', () => { // But they don't have access to our currently supported versions, which we're testing here. // This test ensures that this works as expected: {% if enterpriseServerVersions contains currentVersion %} test('renders the expected Enterprise Server versioned content', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'page-versioned-for-all-enterprise-releases.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' @@ -184,27 +188,27 @@ describe('Page class', () => { }) }) - test('preserves `languageCode`', () => { - const page = new Page(opts) + test('preserves `languageCode`', async () => { + const page = await Page.init(opts) expect(page.languageCode).toBe('en') }) - test('parentProductId getter', () => { - let page = new Page({ + test('parentProductId getter', async () => { + let page = await Page.init({ relativePath: 'github/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), languageCode: 'en' }) expect(page.parentProductId).toBe('github') - page = new Page({ + page = await Page.init({ relativePath: 'actions/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), languageCode: 'en' }) expect(page.parentProductId).toBe('actions') - page = new Page({ + page = await Page.init({ relativePath: 'admin/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), languageCode: 'en' @@ -213,26 +217,26 @@ describe('Page class', () => { }) describe('permalinks', () => { - test('is an array', () => { - const page = new Page(opts) + test('is an array', async () => { + const page = await Page.init(opts) expect(Array.isArray(page.permalinks)).toBe(true) }) - test('has a key for every supported enterprise version (and no deprecated versions)', () => { - const page = new Page(opts) + test('has a key for every supported enterprise version (and no deprecated versions)', async () => { + const page = await Page.init(opts) const pageVersions = page.permalinks.map(permalink => permalink.pageVersion) expect(enterpriseServerReleases.supported.every(version => pageVersions.includes(`enterprise-server@${version}`))).toBe(true) expect(enterpriseServerReleases.deprecated.every(version => !pageVersions.includes(`enterprise-server@${version}`))).toBe(true) }) - test('sets versioned values', () => { - const page = new Page(opts) + test('sets versioned values', async () => { + const page = await Page.init(opts) expect(page.permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion).href).toBe(`/en/${nonEnterpriseDefaultVersion}/github/collaborating-with-issues-and-pull-requests/about-branches`) expect(page.permalinks.find(permalink => permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.oldestSupported}`).href).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}/github/collaborating-with-issues-and-pull-requests/about-branches`) }) - test('homepage permalinks', () => { - const page = new Page({ + test('homepage permalinks', async () => { + const page = await Page.init({ relativePath: 'index.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -242,8 +246,8 @@ describe('Page class', () => { expect(page.permalinks.find(permalink => permalink.pageVersion === 'homepage').href).toBe('/en') }) - test('permalinks for dotcom-only pages', () => { - const page = new Page({ + test('permalinks for dotcom-only pages', async () => { + const page = await Page.init({ relativePath: 'github/getting-started-with-github/signing-up-for-a-new-github-account.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -252,8 +256,8 @@ describe('Page class', () => { expect(page.permalinks.length).toBe(1) }) - test('permalinks for enterprise-only pages', () => { - const page = new Page({ + test('permalinks for enterprise-only pages', async () => { + const page = await Page.init({ relativePath: 'products/admin/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' @@ -264,8 +268,8 @@ describe('Page class', () => { expect(pageVersions.includes(nonEnterpriseDefaultVersion)).toBe(false) }) - test('permalinks for non-GitHub.com products without Enterprise versions', () => { - const page = new Page({ + test('permalinks for non-GitHub.com products without Enterprise versions', async () => { + const page = await Page.init({ relativePath: 'products/actions/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' @@ -274,8 +278,8 @@ describe('Page class', () => { expect(page.permalinks.length).toBe(1) }) - test('permalinks for non-GitHub.com products with Enterprise versions', () => { - const page = new Page({ + test('permalinks for non-GitHub.com products with Enterprise versions', async () => { + const page = await Page.init({ relativePath: '/insights/installing-and-configuring-github-insights/about-github-insights.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -318,7 +322,7 @@ describe('Page class', () => { }) test('fixes translated frontmatter that includes verdadero', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'article-with-mislocalized-frontmatter.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'ja' @@ -333,7 +337,7 @@ describe('Page class', () => { // Note this test will go out of date when we deprecate 2.20 test('pages that apply to newer enterprise versions', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'github/administering-a-repository/comparing-releases.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -343,7 +347,7 @@ describe('Page class', () => { }) test('index page', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'index.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -352,7 +356,7 @@ describe('Page class', () => { }) test('enterprise admin index page', async () => { - const page = new Page({ + const page = await Page.init({ relativePath: 'admin/index.md', basePath: path.join(__dirname, '../../content'), languageCode: 'en' @@ -366,50 +370,50 @@ describe('Page class', () => { describe('catches errors thrown in Page class', () => { test('frontmatter parsing error', () => { - function getPage () { - return new Page({ + async function getPage () { + return await Page.init({ relativePath: 'page-with-frontmatter-error.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' }) } - expect(getPage).toThrowError('invalid frontmatter entry') + expect(getPage).rejects.toThrowError('invalid frontmatter entry') }) test('missing versions frontmatter', () => { - function getPage () { - return new Page({ + async function getPage () { + return await Page.init({ relativePath: 'page-with-missing-product-versions.md', basePath: path.join(__dirname, '../fixtures'), languageCode: 'en' }) } - expect(getPage).toThrowError('versions') + expect(getPage).rejects.toThrowError('versions') }) test('English page with a version in frontmatter that its parent product is not available in', () => { - function getPage () { - return new Page({ + async function getPage () { + return await Page.init({ relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md', basePath: path.join(__dirname, '../fixtures/products'), languageCode: 'en' }) } - expect(getPage).toThrowError(/`versions` frontmatter.*? product is not available in/) + expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/) }) test('non-English page with a version in frontmatter that its parent product is not available in', () => { - function getPage () { - return new Page({ + async function getPage () { + return await Page.init({ relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md', basePath: path.join(__dirname, '../fixtures/products'), languageCode: 'es' }) } - expect(getPage).toThrowError(/`versions` frontmatter.*? product is not available in/) + expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/) }) }) diff --git a/tests/unit/pages.js b/tests/unit/pages.js index f27b73ff1be2..bb405b5e9c61 100644 --- a/tests/unit/pages.js +++ b/tests/unit/pages.js @@ -10,6 +10,8 @@ const entities = new Entities() const { chain, difference } = require('lodash') describe('pages module', () => { + jest.setTimeout(60 * 1000) + let pages beforeAll(async (done) => { From 64d38468c6201ad97a3810b8d664ffba807b49ed Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Wed, 9 Dec 2020 13:06:32 -0500 Subject: [PATCH 05/12] Add await to warmServer in all environments (#16841) --- server.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index ce00b05169f1..744d1fa924c3 100644 --- a/server.js +++ b/server.js @@ -18,14 +18,8 @@ if (!module.parent) { if (status === false) { // If in a deployed environment, warm the server at the start if (process.env.NODE_ENV === 'production') { - // If in a true production environment, wait for the cache to be fully warmed. - if (process.env.HEROKU_PRODUCTION_APP) { - await warmServer() - } else { - // If not in a true production environment, don't wait for the cache to be fully warmed. - // This avoids deployment timeouts in environments with slower servers. - warmServer() - } + // If in a production environment, wait for the cache to be fully warmed. + await warmServer() } // workaround for https://github.com/expressjs/express/issues/1101 From 9fef52449c9437d30bb77bb68310f54d1b2217b1 Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Wed, 9 Dec 2020 13:23:46 -0500 Subject: [PATCH 06/12] Move to a version of this that works with a cron (#16830) Move to a version of this that works with a cron Co-authored-by: Chiedo --- .github/workflows/repo-sync-stalls.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-sync-stalls.yml b/.github/workflows/repo-sync-stalls.yml index 2784f4703a97..fb605313c39f 100644 --- a/.github/workflows/repo-sync-stalls.yml +++ b/.github/workflows/repo-sync-stalls.yml @@ -22,8 +22,8 @@ jobs: github-token: ${{ secrets.DOCUBOT_FR_PROJECT_BOARD_WORKFLOWS_REPO_ORG_READ_SCOPES }} script: | let pulls; - const owner = context.payload.repository.owner.login - const repo = context.payload.repository.name + const owner = context.repo.owner + const repo = context.repo.repo try { pulls = await github.pulls.list({ owner: owner, From 575752a715df65196eabb93965fcd3744549b2c6 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 9 Dec 2020 13:38:49 -0500 Subject: [PATCH 07/12] re-add unsafe-eval to the CSP for deprecated docs --- middleware/csp.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/middleware/csp.js b/middleware/csp.js index a081f0490c23..81f354e77ee2 100644 --- a/middleware/csp.js +++ b/middleware/csp.js @@ -30,7 +30,8 @@ module.exports = contentSecurityPolicy({ ], scriptSrc: [ "'self'", - 'data:' + 'data:', + "'unsafe-eval'" // exception for Algolia instantsearch in deprecated Enterprise docs ], frameSrc: [ // exceptions for GraphQL Explorer 'https://graphql-explorer.githubapp.com', // production env From 1b424dfdc4fc81ea7597a0a39ff4066cf1f4e65a Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 9 Dec 2020 11:20:24 -0800 Subject: [PATCH 08/12] Bring in data-directory, let's go async file reads (#16782) * Bring in data-directory, let's go async file reads * Lint fixes * Update glossary.js --- lib/data-directory.js | 68 +++++++++++++++++++ lib/filename-to-key.js | 28 ++++++++ lib/site-data.js | 10 +-- package-lock.json | 42 ++---------- package.json | 1 - tests/unit/data-directory/filename-to-key.js | 15 ++++ tests/unit/data-directory/fixtures/README.md | 1 + tests/unit/data-directory/fixtures/bar.yaml | 1 + tests/unit/data-directory/fixtures/foo.json | 1 + .../data-directory/fixtures/nested/baz.md | 1 + tests/unit/data-directory/index.js | 40 +++++++++++ 11 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 lib/data-directory.js create mode 100644 lib/filename-to-key.js create mode 100644 tests/unit/data-directory/filename-to-key.js create mode 100644 tests/unit/data-directory/fixtures/README.md create mode 100644 tests/unit/data-directory/fixtures/bar.yaml create mode 100644 tests/unit/data-directory/fixtures/foo.json create mode 100644 tests/unit/data-directory/fixtures/nested/baz.md create mode 100644 tests/unit/data-directory/index.js diff --git a/lib/data-directory.js b/lib/data-directory.js new file mode 100644 index 000000000000..d72abcbf555b --- /dev/null +++ b/lib/data-directory.js @@ -0,0 +1,68 @@ +const assert = require('assert') +const fs = require('fs').promises +const path = require('path') +const walk = require('walk-sync') +const yaml = require('js-yaml') +const { isRegExp, set } = require('lodash') +const filenameToKey = require('./filename-to-key') + +module.exports = async function dataDirectory (dir, opts = {}) { + const defaultOpts = { + preprocess: (content) => { return content }, + ignorePatterns: [/README\.md$/i], + extensions: [ + '.json', + '.md', + '.markdown', + '.yaml', + '.yml' + ] + } + + opts = Object.assign({}, defaultOpts, opts) + + // validate input + assert(Array.isArray(opts.ignorePatterns)) + assert(opts.ignorePatterns.every(isRegExp)) + assert(Array.isArray(opts.extensions)) + assert(opts.extensions.length) + + // start with an empty data object + const data = {} + + // find YAML and Markdown files in the given directory, recursively + await Promise.all(walk(dir, { includeBasePath: true }) + .filter(filename => { + // ignore files that match any of ignorePatterns regexes + if (opts.ignorePatterns.some(pattern => pattern.test(filename))) return false + + // ignore files that don't have a whitelisted file extension + return opts.extensions.includes(path.extname(filename).toLowerCase()) + }) + .map(async filename => { + // derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename + const key = filenameToKey(path.relative(dir, filename)) + const extension = path.extname(filename).toLowerCase() + + let fileContent = await fs.readFile(filename, 'utf8') + + if (opts.preprocess) fileContent = opts.preprocess(fileContent) + + // add this file's data to the global data object + switch (extension) { + case '.json': + set(data, key, JSON.parse(fileContent)) + break + case '.yaml': + case '.yml': + set(data, key, yaml.safeLoad(fileContent, { filename })) + break + case '.md': + case '.markdown': + set(data, key, fileContent) + break + } + })) + + return data +} diff --git a/lib/filename-to-key.js b/lib/filename-to-key.js new file mode 100644 index 000000000000..568552285bef --- /dev/null +++ b/lib/filename-to-key.js @@ -0,0 +1,28 @@ +/* eslint-disable prefer-regex-literals */ +const path = require('path') +const { escapeRegExp } = require('lodash') + +// slash at the beginning of a filename +const leadingPathSeparator = new RegExp(`^${escapeRegExp(path.sep)}`) +const windowsLeadingPathSeparator = new RegExp('^/') + +// all slashes in the filename. path.sep is OS agnostic (windows, mac, etc) +const pathSeparator = new RegExp(escapeRegExp(path.sep), 'g') +const windowsPathSeparator = new RegExp('/', 'g') + +// handle MS Windows style double-backslashed filenames +const windowsDoubleSlashSeparator = new RegExp('\\\\', 'g') + +// derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename +module.exports = function filenameToKey (filename) { + const extension = new RegExp(`${path.extname(filename)}$`) + const key = filename + .replace(extension, '') + .replace(leadingPathSeparator, '') + .replace(windowsLeadingPathSeparator, '') + .replace(pathSeparator, '.') + .replace(windowsPathSeparator, '.') + .replace(windowsDoubleSlashSeparator, '.') + + return key +} diff --git a/lib/site-data.js b/lib/site-data.js index 7c4ba0c988ae..bdd47f1ce7dc 100755 --- a/lib/site-data.js +++ b/lib/site-data.js @@ -2,12 +2,12 @@ const path = require('path') const flat = require('flat') const { get, set } = require('lodash') const languages = require('./languages') -const dataDirectory = require('@github-docs/data-directory') +const dataDirectory = require('./data-directory') const encodeBracketedParentheticals = require('./encode-bracketed-parentheticals') -const loadSiteDataFromDir = dir => ({ +const loadSiteDataFromDir = async dir => ({ site: { - data: dataDirectory(path.join(dir, 'data'), { + data: await dataDirectory(path.join(dir, 'data'), { preprocess: dataString => encodeBracketedParentheticals(dataString.trimEnd()), ignorePatterns: [/README\.md$/] @@ -18,7 +18,7 @@ const loadSiteDataFromDir = dir => ({ module.exports = async function loadSiteData () { // load english site data const siteData = { - en: loadSiteDataFromDir(languages.en.dir) + en: await loadSiteDataFromDir(languages.en.dir) } // load and add other language data to siteData where keys match english keys, @@ -26,7 +26,7 @@ module.exports = async function loadSiteData () { const englishKeys = Object.keys(flat(siteData.en)) for (const language of Object.values(languages)) { if (language.code === 'en') continue - const data = loadSiteDataFromDir(language.dir) + const data = await loadSiteDataFromDir(language.dir) for (const key of englishKeys) { set( siteData, diff --git a/package-lock.json b/package-lock.json index b0f2a9c3e7e9..e4cee8054d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2969,36 +2969,6 @@ } } }, - "@github-docs/data-directory": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@github-docs/data-directory/-/data-directory-1.2.0.tgz", - "integrity": "sha512-hp+Ubwl8e77EdnR4OncSUIE7G/cMn9ENOo6ABy8FjqdYCbAWgb/79w7yXVebIV5P3q5r6KAAqPzHj1N5SSrBgw==", - "requires": { - "lodash": "^4.17.15", - "walk-sync": "^2.0.2" - }, - "dependencies": { - "matcher-collection": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", - "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", - "requires": { - "@types/minimatch": "^3.0.3", - "minimatch": "^3.0.2" - } - }, - "walk-sync": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.1.0.tgz", - "integrity": "sha512-KpH9Xw64LNSx7/UI+3guRZvJWlDxVA4+KKb/4puRoVrG8GkvZRxnF3vhxdjgpoKJGL2TVg1OrtkXIE/VuGPLHQ==", - "requires": { - "@types/minimatch": "^3.0.3", - "ensure-posix-path": "^1.1.0", - "matcher-collection": "^2.0.0" - } - } - } - }, "@github-docs/frontmatter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@github-docs/frontmatter/-/frontmatter-1.3.1.tgz", @@ -5301,7 +5271,7 @@ }, "agentkeepalive": { "version": "2.2.0", - "resolved": "http://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", "integrity": "sha1-xdG9SxKQCPEWPyNvhuX66iAm4u8=" }, "aggregate-error": { @@ -5447,7 +5417,7 @@ "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { "sprintf-js": "~1.0.2" } @@ -6824,7 +6794,7 @@ }, "brfs": { "version": "1.6.1", - "resolved": "http://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", "requires": { "quote-stream": "^1.0.1", @@ -9433,7 +9403,7 @@ "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "requires": { "is-arrayish": "^0.2.1" } @@ -12577,7 +12547,7 @@ "dependencies": { "mkdirp": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" }, "nopt": { @@ -17703,7 +17673,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "requires": { "vlq": "^0.2.2" diff --git a/package.json b/package.json index a0cae4097a62..545de5882648 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@babel/preset-env": "^7.12.7", "@babel/preset-react": "^7.12.7", "@babel/runtime": "^7.11.2", - "@github-docs/data-directory": "^1.2.0", "@github-docs/frontmatter": "^1.3.1", "@graphql-inspector/core": "^2.3.0", "@graphql-tools/load": "^6.2.5", diff --git a/tests/unit/data-directory/filename-to-key.js b/tests/unit/data-directory/filename-to-key.js new file mode 100644 index 000000000000..052f78cf98d6 --- /dev/null +++ b/tests/unit/data-directory/filename-to-key.js @@ -0,0 +1,15 @@ +const filenameToKey = require('../../../lib/filename-to-key') + +describe('filename-to-key', () => { + test('converts filenames to object keys', () => { + expect(filenameToKey('foo/bar/baz.txt')).toBe('foo.bar.baz') + }) + + test('ignores leading slash on filenames', () => { + expect(filenameToKey('/foo/bar/baz.txt')).toBe('foo.bar.baz') + }) + + test('supports MS Windows paths', () => { + expect(filenameToKey('path\\to\\file.txt')).toBe('path.to.file') + }) +}) diff --git a/tests/unit/data-directory/fixtures/README.md b/tests/unit/data-directory/fixtures/README.md new file mode 100644 index 000000000000..fb62e0bc60fc --- /dev/null +++ b/tests/unit/data-directory/fixtures/README.md @@ -0,0 +1 @@ +I am a README. I am ignored by default. \ No newline at end of file diff --git a/tests/unit/data-directory/fixtures/bar.yaml b/tests/unit/data-directory/fixtures/bar.yaml new file mode 100644 index 000000000000..d028f54fed92 --- /dev/null +++ b/tests/unit/data-directory/fixtures/bar.yaml @@ -0,0 +1 @@ +another_markup_language: 'yes' diff --git a/tests/unit/data-directory/fixtures/foo.json b/tests/unit/data-directory/fixtures/foo.json new file mode 100644 index 000000000000..8fd3eb5c428c --- /dev/null +++ b/tests/unit/data-directory/fixtures/foo.json @@ -0,0 +1 @@ +{"meaningOfLife": 42} \ No newline at end of file diff --git a/tests/unit/data-directory/fixtures/nested/baz.md b/tests/unit/data-directory/fixtures/nested/baz.md new file mode 100644 index 000000000000..c2be33651cee --- /dev/null +++ b/tests/unit/data-directory/fixtures/nested/baz.md @@ -0,0 +1 @@ +I am markdown! \ No newline at end of file diff --git a/tests/unit/data-directory/index.js b/tests/unit/data-directory/index.js new file mode 100644 index 000000000000..0a6bdcf79355 --- /dev/null +++ b/tests/unit/data-directory/index.js @@ -0,0 +1,40 @@ +const path = require('path') +const dataDirectory = require('../../../lib/data-directory') +const fixturesDir = path.join(__dirname, 'fixtures') + +describe('data-directory', () => { + test('works', async () => { + const data = await dataDirectory(fixturesDir) + const expected = { + bar: { another_markup_language: 'yes' }, + foo: { meaningOfLife: 42 }, + nested: { baz: 'I am markdown!' } + } + expect(data).toEqual(expected) + }) + + test('option: preprocess function', async () => { + const preprocess = function (content) { + return content.replace('markdown', 'MARKDOWN') + } + const data = await dataDirectory(fixturesDir, { preprocess }) + expect(data.nested.baz).toBe('I am MARKDOWN!') + }) + + test('option: extensions array', async () => { + const extensions = ['.yaml', 'markdown'] + const data = await dataDirectory(fixturesDir, { extensions }) + expect('bar' in data).toBe(true) + expect('foo' in data).toBe(false) // JSON file should be ignored + }) + + test('option: ignorePatterns', async () => { + const ignorePatterns = [] + + // README is ignored by default + expect('README' in await dataDirectory(fixturesDir)).toBe(false) + + // README can be included by setting empty ignorePatterns array + expect('README' in await dataDirectory(fixturesDir, { ignorePatterns })).toBe(true) + }) +}) From 910a94b1d66076b8ba85f18c4f3fd4c483fab96a Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Wed, 9 Dec 2020 14:33:11 -0500 Subject: [PATCH 09/12] Remove skip-duplicate-actions for linting (#16845) Co-authored-by: Chiedo --- .github/workflows/js-lint.yml | 15 --------------- .github/workflows/yml-lint.yml | 15 --------------- 2 files changed, 30 deletions(-) diff --git a/.github/workflows/js-lint.yml b/.github/workflows/js-lint.yml index 3da0e854a213..4c138c5955f9 100644 --- a/.github/workflows/js-lint.yml +++ b/.github/workflows/js-lint.yml @@ -10,23 +10,8 @@ on: - translations jobs: - see_if_should_skip: - runs-on: ubuntu-latest - - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@36feb0d8d062137530c2e00bd278d138fe191289 - with: - cancel_others: 'false' - github_token: ${{ github.token }} - paths: '["**/*.js", "package*.json", ".github/workflows/js-lint.yml", ".eslint*"]' - lint: runs-on: ubuntu-latest - needs: see_if_should_skip - if: ${{ needs.see_if_should_skip.outputs.should_skip != 'true' }} steps: - name: Check out repo uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f diff --git a/.github/workflows/yml-lint.yml b/.github/workflows/yml-lint.yml index 69d665ecdeda..f8248961e082 100644 --- a/.github/workflows/yml-lint.yml +++ b/.github/workflows/yml-lint.yml @@ -10,23 +10,8 @@ on: - translations jobs: - see_if_should_skip: - runs-on: ubuntu-latest - - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@36feb0d8d062137530c2e00bd278d138fe191289 - with: - cancel_others: 'false' - github_token: ${{ github.token }} - paths: '["**/*.yml", "**/*.yaml", "package*.json", ".github/workflows/yml-lint.yml"]' - lint: runs-on: ubuntu-latest - needs: see_if_should_skip - if: ${{ needs.see_if_should_skip.outputs.should_skip != 'true' }} steps: - name: Check out repo uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f From f21c2f322c85420750e3199d980e8e7cf4a23b80 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 9 Dec 2020 14:46:55 -0500 Subject: [PATCH 10/12] update CSP for specific versions only --- middleware/csp.js | 104 +++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 43 deletions(-) diff --git a/middleware/csp.js b/middleware/csp.js index 81f354e77ee2..647448e92851 100644 --- a/middleware/csp.js +++ b/middleware/csp.js @@ -2,48 +2,66 @@ // inline scripts and content from untrusted sources. const { contentSecurityPolicy } = require('helmet') +const isArchivedVersion = require('../lib/is-archived-version') +const versionSatisfiesRange = require('../lib/version-satisfies-range') -module.exports = contentSecurityPolicy({ - directives: { - defaultSrc: ["'none'"], - connectSrc: [ - "'self'", - '*.algolia.net', - '*.algolianet.com' - ], - fontSrc: [ - "'self'", - 'data:', - 'github-images.s3.amazonaws.com' - ], - imgSrc: [ - "'self'", - 'github.githubassets.com', - 'github-images.s3.amazonaws.com', - 'octodex.github.com', - 'placehold.it', - '*.githubusercontent.com', - 'github.com' - ], - objectSrc: [ - "'self'" - ], - scriptSrc: [ - "'self'", - 'data:', - "'unsafe-eval'" // exception for Algolia instantsearch in deprecated Enterprise docs - ], - frameSrc: [ // exceptions for GraphQL Explorer - 'https://graphql-explorer.githubapp.com', // production env - 'http://localhost:3000', // development env - 'https://www.youtube-nocookie.com' - ], - styleSrc: [ - "'self'", - "'unsafe-inline'" - ], - childSrc: [ - "'self'" // exception for search in deprecated GHE versions - ] +// module.exports = contentSecurityPolicy({ +module.exports = async (req, res, next) => { + const csp = { + directives: { + defaultSrc: ["'none'"], + connectSrc: [ + "'self'", + '*.algolia.net', + '*.algolianet.com' + ], + fontSrc: [ + "'self'", + 'data:', + 'github-images.s3.amazonaws.com' + ], + imgSrc: [ + "'self'", + 'github.githubassets.com', + 'github-images.s3.amazonaws.com', + 'octodex.github.com', + 'placehold.it', + '*.githubusercontent.com', + 'github.com' + ], + objectSrc: [ + "'self'" + ], + scriptSrc: [ + "'self'", + 'data:' + ], + frameSrc: [ // exceptions for GraphQL Explorer + 'https://graphql-explorer.githubapp.com', // production env + 'http://localhost:3000', // development env + 'https://www.youtube-nocookie.com' + ], + styleSrc: [ + "'self'", + "'unsafe-inline'" + ], + childSrc: [ + "'self'" // exception for search in deprecated GHE versions + ] + } } -}) + + const { requestedVersion } = isArchivedVersion(req) + + // Exception for Algolia instantsearch in deprecated Enterprise docs (Node.js era) + if (versionSatisfiesRange(requestedVersion, '<=2.19') && versionSatisfiesRange(requestedVersion, '>2.12')) { + csp.directives.scriptSrc.push("'unsafe-eval'") + } + + // Exception for search in deprecated Enterprise docs <=2.12 (static site era) + if (versionSatisfiesRange(requestedVersion, '<=2.12')) { + csp.directives.scriptSrc.push("'unsafe-inline'") + } + + return contentSecurityPolicy(csp)(req, res, next) +} From 6a3d326fcfed900671dc3bff020041c744ebd3e1 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Wed, 9 Dec 2020 11:50:46 -0800 Subject: [PATCH 11/12] add one-off cases for hooks and pre-receive-hooks (#16784) * add one-off cases for hooks and pre-receive-hooks * add forum redirect * update redirect test * update 2.19-2.22 fixutres * revert 2.18/2.19 changes * remove dotcom test * update dotcom test --- .../working-with-github-support/index.md | 1 + .../get-docs-path-from-developer-path.js | 21 +++++++++++++++++- tests/fixtures/rest-redirects.json | 22 +++++++++---------- tests/routing/developer-site-redirects.js | 4 ++-- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/content/github/working-with-github-support/index.md b/content/github/working-with-github-support/index.md index 176ea10515e1..1ea83fac6bd8 100644 --- a/content/github/working-with-github-support/index.md +++ b/content/github/working-with-github-support/index.md @@ -2,6 +2,7 @@ title: Working with GitHub Support redirect_from: - /categories/working-with-github-support + - /forum versions: free-pro-team: '*' --- diff --git a/lib/redirects/get-docs-path-from-developer-path.js b/lib/redirects/get-docs-path-from-developer-path.js index 35395143d92b..5286bba04b39 100644 --- a/lib/redirects/get-docs-path-from-developer-path.js +++ b/lib/redirects/get-docs-path-from-developer-path.js @@ -15,7 +15,25 @@ module.exports = function getDocsPathFromDeveloperPath (oldDeveloperPath, allRed // oneoff redirect const v3OrgPreReceiveHooks = '/v3/orgs/pre_receive_hooks' if (newPath.endsWith(v3OrgPreReceiveHooks)) { - newPath = newPath.replace(v3OrgPreReceiveHooks, '/v3/enterprise-admin/org_pre_receive_hooks') + newPath = newPath.replace(v3OrgPreReceiveHooks, '/v3/enterprise-admin/organization_pre_receive_hooks') + } + + // oneoff redirect + const v3RepoPreReceiveHooks = '/v3/repos/pre_receive_hooks' + if (newPath.endsWith(v3RepoPreReceiveHooks)) { + newPath = newPath.replace(v3RepoPreReceiveHooks, '/v3/enterprise-admin/repository_pre_receive_hooks') + } + + // oneoff redirect + const v3OrgHooks = '/v3/orgs/hooks' + if (newPath.endsWith(v3OrgHooks)) { + newPath = newPath.replace(v3OrgHooks, '/v3/orgs/webhooks') + } + + // oneoff redirect + const v3RepoHooks = '/v3/repos/hooks' + if (newPath.endsWith(v3RepoHooks)) { + newPath = newPath.replace(v3RepoHooks, '/v3/repos/webhooks') } // oneoff redirect for a dotcom developer path to Enterprise-only path on docs.github.com @@ -46,6 +64,7 @@ module.exports = function getDocsPathFromDeveloperPath (oldDeveloperPath, allRed .replace(/_/g, '-') // this is a special oneoff replacement .replace('org-pre-receive-hooks', 'organization-pre-receive-hooks') + .replace('repo-pre-receive-hooks', 'repository-pre-receive-hooks') : lastSegment } diff --git a/tests/fixtures/rest-redirects.json b/tests/fixtures/rest-redirects.json index 01470158f7cf..d6b4e852f2ea 100644 --- a/tests/fixtures/rest-redirects.json +++ b/tests/fixtures/rest-redirects.json @@ -219,7 +219,7 @@ "/en/enterprise/2.20/v3/enterprise-admin/orgs": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#orgs", "/en/enterprise/2.20/v3/enterprise-admin/pre_receive_environments": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#pre-receive-environments", "/en/enterprise/2.20/v3/enterprise-admin/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#pre-receive-hooks", - "/en/enterprise/2.20/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repo-pre-receive-hooks", + "/en/enterprise/2.20/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repository-pre-receive-hooks", "/en/enterprise/2.20/v3/enterprise-admin/search_indexing": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#search-indexing", "/en/enterprise/2.20/v3/enterprise-admin/users": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#users", "/en/enterprise/2.20/v3/gists/comments": "/en/enterprise-server@2.20/rest/reference/gists#comments", @@ -243,7 +243,7 @@ "/en/enterprise/2.20/v3/markdown": "/en/enterprise-server@2.20/rest/reference/markdown", "/en/enterprise/2.20/v3/meta": "/en/enterprise-server@2.20/rest/reference/meta", "/en/enterprise/2.20/v3/oauth_authorizations": "/en/enterprise-server@2.20/rest/reference/oauth-authorizations", - "/en/enterprise/2.20/v3/orgs/hooks": "/en/enterprise-server@2.20/rest/reference/orgs#hooks", + "/en/enterprise/2.20/v3/orgs/hooks": "/en/enterprise-server@2.20/rest/reference/orgs#webhooks", "/en/enterprise/2.20/v3/orgs": "/en/enterprise-server@2.20/rest/reference/orgs", "/en/enterprise/2.20/v3/orgs/members": "/en/enterprise-server@2.20/rest/reference/orgs#members", "/en/enterprise/2.20/v3/orgs/outside_collaborators": "/en/enterprise-server@2.20/rest/reference/orgs#outside-collaborators", @@ -267,13 +267,13 @@ "/en/enterprise/2.20/v3/repos/deployments": "/en/enterprise-server@2.20/rest/reference/repos#deployments", "/en/enterprise/2.20/v3/repos/downloads": "/en/enterprise-server@2.20/rest/reference/repos#downloads", "/en/enterprise/2.20/v3/repos/forks": "/en/enterprise-server@2.20/rest/reference/repos#forks", - "/en/enterprise/2.20/v3/repos/hooks": "/en/enterprise-server@2.20/rest/reference/repos#hooks", + "/en/enterprise/2.20/v3/repos/hooks": "/en/enterprise-server@2.20/rest/reference/repos#webhooks", "/en/enterprise/2.20/v3/repos": "/en/enterprise-server@2.20/rest/reference/repos", "/en/enterprise/2.20/v3/repos/invitations": "/en/enterprise-server@2.20/rest/reference/repos#invitations", "/en/enterprise/2.20/v3/repos/keys": "/en/enterprise-server@2.20/rest/reference/repos#keys", "/en/enterprise/2.20/v3/repos/merging": "/en/enterprise-server@2.20/rest/reference/repos#merging", "/en/enterprise/2.20/v3/repos/pages": "/en/enterprise-server@2.20/rest/reference/repos#pages", - "/en/enterprise/2.20/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/repos#pre-receive-hooks", + "/en/enterprise/2.20/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repository-pre-receive-hooks", "/en/enterprise/2.20/v3/repos/releases": "/en/enterprise-server@2.20/rest/reference/repos#releases", "/en/enterprise/2.20/v3/repos/statistics": "/en/enterprise-server@2.20/rest/reference/repos#statistics", "/en/enterprise/2.20/v3/repos/statuses": "/en/enterprise-server@2.20/rest/reference/repos#statuses", @@ -316,7 +316,7 @@ "/en/enterprise/2.21/v3/enterprise-admin/orgs": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#orgs", "/en/enterprise/2.21/v3/enterprise-admin/pre_receive_environments": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#pre-receive-environments", "/en/enterprise/2.21/v3/enterprise-admin/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#pre-receive-hooks", - "/en/enterprise/2.21/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repo-pre-receive-hooks", + "/en/enterprise/2.21/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repository-pre-receive-hooks", "/en/enterprise/2.21/v3/enterprise-admin/search_indexing": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#search-indexing", "/en/enterprise/2.21/v3/enterprise-admin/users": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#users", "/en/enterprise/2.21/v3/gists/comments": "/en/enterprise-server@2.21/rest/reference/gists#comments", @@ -340,7 +340,7 @@ "/en/enterprise/2.21/v3/markdown": "/en/enterprise-server@2.21/rest/reference/markdown", "/en/enterprise/2.21/v3/meta": "/en/enterprise-server@2.21/rest/reference/meta", "/en/enterprise/2.21/v3/oauth_authorizations": "/en/enterprise-server@2.21/rest/reference/oauth-authorizations", - "/en/enterprise/2.21/v3/orgs/hooks": "/en/enterprise-server@2.21/rest/reference/orgs#hooks", + "/en/enterprise/2.21/v3/orgs/hooks": "/en/enterprise-server@2.21/rest/reference/orgs#webhooks", "/en/enterprise/2.21/v3/orgs": "/en/enterprise-server@2.21/rest/reference/orgs", "/en/enterprise/2.21/v3/orgs/members": "/en/enterprise-server@2.21/rest/reference/orgs#members", "/en/enterprise/2.21/v3/orgs/outside_collaborators": "/en/enterprise-server@2.21/rest/reference/orgs#outside-collaborators", @@ -364,13 +364,13 @@ "/en/enterprise/2.21/v3/repos/deployments": "/en/enterprise-server@2.21/rest/reference/repos#deployments", "/en/enterprise/2.21/v3/repos/downloads": "/en/enterprise-server@2.21/rest/reference/repos#downloads", "/en/enterprise/2.21/v3/repos/forks": "/en/enterprise-server@2.21/rest/reference/repos#forks", - "/en/enterprise/2.21/v3/repos/hooks": "/en/enterprise-server@2.21/rest/reference/repos#hooks", + "/en/enterprise/2.21/v3/repos/hooks": "/en/enterprise-server@2.21/rest/reference/repos#webhooks", "/en/enterprise/2.21/v3/repos": "/en/enterprise-server@2.21/rest/reference/repos", "/en/enterprise/2.21/v3/repos/invitations": "/en/enterprise-server@2.21/rest/reference/repos#invitations", "/en/enterprise/2.21/v3/repos/keys": "/en/enterprise-server@2.21/rest/reference/repos#keys", "/en/enterprise/2.21/v3/repos/merging": "/en/enterprise-server@2.21/rest/reference/repos#merging", "/en/enterprise/2.21/v3/repos/pages": "/en/enterprise-server@2.21/rest/reference/repos#pages", - "/en/enterprise/2.21/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/repos#pre-receive-hooks", + "/en/enterprise/2.21/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repository-pre-receive-hooks", "/en/enterprise/2.21/v3/repos/releases": "/en/enterprise-server@2.21/rest/reference/repos#releases", "/en/enterprise/2.21/v3/repos/statistics": "/en/enterprise-server@2.21/rest/reference/repos#statistics", "/en/enterprise/2.21/v3/repos/statuses": "/en/enterprise-server@2.21/rest/reference/repos#statuses", @@ -453,7 +453,7 @@ "/v3/migrations/users": "/en/free-pro-team@latest/rest/reference/migrations#users", "/v3/oauth_authorizations": "/en/enterprise-server/rest/reference/oauth-authorizations", "/v3/orgs/blocking": "/en/free-pro-team@latest/rest/reference/orgs#blocking", - "/v3/orgs/hooks": "/en/free-pro-team@latest/rest/reference/orgs#hooks", + "/v3/orgs/hooks": "/en/free-pro-team@latest/rest/reference/orgs#webhooks", "/v3/orgs": "/en/free-pro-team@latest/rest/reference/orgs", "/v3/orgs/members": "/en/free-pro-team@latest/rest/reference/orgs#members", "/v3/orgs/migrations": "/en/free-pro-team@latest/rest/reference/orgs#migrations", @@ -479,13 +479,13 @@ "/v3/repos/deployments": "/en/free-pro-team@latest/rest/reference/repos#deployments", "/v3/repos/downloads": "/en/free-pro-team@latest/rest/reference/repos#downloads", "/v3/repos/forks": "/en/free-pro-team@latest/rest/reference/repos#forks", - "/v3/repos/hooks": "/en/free-pro-team@latest/rest/reference/repos#hooks", + "/v3/repos/hooks": "/en/free-pro-team@latest/rest/reference/repos#webhooks", "/v3/repos": "/en/free-pro-team@latest/rest/reference/repos", "/v3/repos/invitations": "/en/free-pro-team@latest/rest/reference/repos#invitations", "/v3/repos/keys": "/en/free-pro-team@latest/rest/reference/repos#keys", "/v3/repos/merging": "/en/free-pro-team@latest/rest/reference/repos#merging", "/v3/repos/pages": "/en/free-pro-team@latest/rest/reference/repos#pages", - "/v3/repos/pre_receive_hooks": "/en/free-pro-team@latest/rest/reference/repos#pre-receive-hooks", + "/v3/repos/pre_receive_hooks": "/en/enterprise-server/rest/reference/enterprise-admin#repository-pre-receive-hooks", "/v3/repos/releases": "/en/free-pro-team@latest/rest/reference/repos#releases", "/v3/repos/statistics": "/en/free-pro-team@latest/rest/reference/repos#statistics", "/v3/repos/statuses": "/en/free-pro-team@latest/rest/reference/repos#statuses", diff --git a/tests/routing/developer-site-redirects.js b/tests/routing/developer-site-redirects.js index 7378066dc9d7..8e6e7b6c8a09 100644 --- a/tests/routing/developer-site-redirects.js +++ b/tests/routing/developer-site-redirects.js @@ -126,7 +126,7 @@ describe('developer redirects', () => { ) }) - // TODO temprarily ensure we redirect old links using the new enterprise format + // TODO temporarily ensure we redirect old links using the new enterprise format // for currently supported enterprise releases only // EXAMPLE: /en/enterprise-server@2.20/v3/pulls/comments -> /en/enterprise-server@2.20/rest/reference/pulls#comments // We can remove test after we update all the old `/v3` links to point to `/rest` @@ -165,7 +165,7 @@ describe('developer redirects', () => { ) }) - // TODO temprarily ensure we redirect old links using the new enterprise format + // TODO temporarily ensure we redirect old links using the new enterprise format // for currently supported enterprise releases only // EXAMPLE: /en/enterprise-server@2.20/v4/interface/actor -> /en/enterprise-server@2.20/graphql/reference/interfaces#actor // We can remove test after we update all the old `/v4` links to point to `/graphql` From 5b0207bb7b3760e7d9c1360522bb5f839bf7708d Mon Sep 17 00:00:00 2001 From: Meg Bird Date: Wed, 9 Dec 2020 13:02:21 -0700 Subject: [PATCH 12/12] remove reusable (#16844) --- .../help/discussions/public-repo-settings.png | Bin 0 -> 23630 bytes content/discussions/quickstart.md | 4 +++- ...ng-github-discussions-for-your-repository.md | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 assets/images/help/discussions/public-repo-settings.png diff --git a/assets/images/help/discussions/public-repo-settings.png b/assets/images/help/discussions/public-repo-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..c337a3a2207602b4d6ffa60383b3a08c8708145c GIT binary patch literal 23630 zcmd?QgRQ^xE6OQQY^T;Q@pqnoZ?%6~rD@vmx5h6iBL7~dZNPK~Uf{T8w2OzwE{Z^3ttOo^!v~49W zt}H7qPOj`^Z*FB{1_dP(k(dmxrK*kVf8wRpu)5!|7Rh=hbbJaTSsHM&;!$YDdvYUW7`uiqH|4H&fI zcJeUx&i}^{Ze$G*2g*r;O;)~=Oz;zLNQ@WKAAhLuPTaV8;@>F*BqV4Usk45G=nc>u zIrj64z4te}FY42c0-xZa=y2AZ%M;>*9EjN39ec3|py+}F1}RhB1G6YgnMJWAZa3jB zXbBWD*DgO5DBbt=-BKN*KxK}Y%iTi7`C#%3X7xyK#7$w;<2i;<26Iy$j$&3eA-GWp zFwoMv1$L0zMBGL~XQxn($h@e^KO1foGHlc=c)wGc;rw(9m814^)ZpB&J?({wcj0`W zbL6`yu8f#*0e*9OB2j^nil^x`2XfQY^I!b{fsejvykA}ttahqlyRktvR6T{f;`qH^ zdcRj~6r|FkMrD}G+C>?cp+pFS(iVQCx@>EoA+`7qeCY*X^inH=+F2-^((<*dfb2rg zNG%eizy4&=B3-9zbIYiB#at>~A!&bWViVO1&vm_)e-^+5(G+mVekV%C!{FKcP;rVk z{D^HGQS%Gux%JQ}L_dwJB!xkoYj*@}*~dR(8YZR09u5mb4o}(nPJkSGpB&E=mLNL_ ziX83>xzuwNbajSlYhQK5#v`at?xR?{h?E5-ZJe}41dnUYUd(mOHI)3%-QP^-F`7g-uM-p&T{{pr+B;x#AmCEw5fs7N8bQhX zQ=j)az?FA?xD6D+K+28#{G$t|Q}kd^lwKPyDPSfWA;CY|h@l9krOUtpiuk>>C_4}0 zSYYHD{T@t1C+uJ3_nmSqC_?_M2!Re5?{dW!<0wquFNnloz@CS~Q z>o1!nlNwB-xcXnxBD8w~KIn`PX7S~}f<@?rfh%HJKy3NY8KBQRaV9Fe5e@PCfvyXn z&`)^0&XCjhjacOY=v@k@+PC!H$j2du-Bfh`lsT4kR0&Mq0?wr+bG|4O&>?<(SB$S7 zP%O%st)oIxL{wxj%`i>NfvX@nHBoP=aIx4!GqnRx?p}q&ZM|0_MiRw(|iT{Ku z-n9p`VIjrGr9tc&VC=>Q(z86U4zS{}G_b&?pldNEh1PX81H~%`=j9K-*nD!Ot;bvn zJ?g(Uk#^wu$u;(VHzcyV%8^YE(~kcb;~1qJp4LByBa}LfWB{BK7bf(Vx04u5-b2exwj%#k2f8Wzz)I1im5O zA(J6#>f#?6SaK01wO@Yg{FePKyP8LE)N+#(t^Kl-rdq~=)9 z0KV)P^#)~&-xkHmMPwQ$YGkv@Qx3B-f3-?g${aucN)F9Uozkkc(IGgZJK{R3DQKrr zjW296s1dD^2C2zu4r@jhs+Z0bxyV6kxb*pz`Q^OgU2CqXNNQv1#`H2I`C6Qr_gW{_ z=dPx=_i!PkGi?>{5N$}`9@ETHiJOAG#Hxs}mSJg!{>#s22H3QK3ekct+@PuV;-PBd z_To9~xIOHlfIUKI8o_h}Sz#qIu1R(z)*nPitOp!~?DZ@$+y*9K%f~;#mfb9aEDOey zc5F6ZEeV0mBeg4+?F3y>KpK`M^I0I*S36*)W&2p`!ediN(!1f@&fJxa9O}Nw0k-k# z{__Hy*>Au^v%0C`VZwUXDqJm1UBiX|TRq!gE`Hl)3!r(^IM!%!s>?u<&2a5#Ls`R7 z&98-!rLrpPdN08xXV;-ag&KZyzZ$oy@%mMZmrl`9ifLf9cu9}0w6%1MN!_Lb?Kjo9 zsn3PDh1_FYV*4@5LQ)ByTJ4!_K7_fmc=AwfmLlRNGXm-{tWY>pAdY z`c~kK^KAS6_=dz(hm}1wET!V<@ad!Pz?0OI;A8Sb!@F#lK-h1ve6TYxo3JG?Wbg#= zXF&%+p+WkA$?qFNjKsRdxI)0d{TiN9zFCJN-M#a_7r?E``O4x-+V$D>AE&bjJd_;9 zpxz#t8W|N+5O%n+q-~5Rwk>7`-V;Rc5WLqk1Ip^->1B-E56|ysjuOOwpcn&`Gaie! znzahF3yShmaON-rIatZ71gf~LTqn=-N(T7`ySI|C^*)OYJtutk2)L*pBa(5EW%}GE zxN7{oahHs@$N0y<;cL#h*mbvwRZ4AdZIG4WyviZrwG1DFkMupuKvqrk#Sh5vayQ9_ z$Jvla5P<=~Ho^Fv!Q<#7ML=w}*y-mzKq*%HAj%*(gEtK)Q9YeAGb3d=C1_lBn3v+q z2eXK=UBWHqu_N52@W|da4^j`^i%@FdkEbs(e1?mZs?vmo)=qMFZs+aZCyxh@v5{D~ zB)X-|TD7-}SP*JRG72sn&SNchTS85(PwGVV%nl1`CfV{zq6%i767WWu+Pl5VpeWcQ$;=PXpefw@ zqz$H%24=8lQ+Jc`4c7+OUH!_?=}^*9!4kLK%NOdRX7wY@NlWN0l}*9LEl_Ok+1 zQo>*4R#{3~o;-(6@>y`&g=ft{F37egQ%c8V5Ys`zcdKvQN_bU>(6#mne^aXmVBT@a zqbO(qhT~+iBDBNhxT*ix;GBgZII(iD;Q6T8VrZ>Im{BT}?^t}uapt_ff5!gKLTa(s zwqopj{JH^Pd9-f8XLo}+**WES7X~P;*d4@P>X+yf-v9g#-MOTbXQ(LU37?ltGnRhPK>IGOnMxk(R!LwcUg;YHR9!n%Zv_lF@xF>H zMGp#)EthsNkH-*-pZ8OLheX=?77tEjlyiDY?B?dlC3I&U%&9t?kWf9y`w=4aals3! zc3a`>X`?0l?`z!=bD$ti{k=)m~SHnz8 z)?7gWivG2Z00kXp1qJt7gMPgTUoR*q*dL)#@UM5Q*GnQB=D&L3qO)QDs}2?L&p=UC zaaq~dyQ+zknVB8P(%xB-YnAnNsyQn)EoUtSc|H?+TV|lCy|EdyyRE}NAW#DCe6Lko zGiM;VyRD5Kh|gV+@?RtPUhDrf11QP=HN@FkkWx!QnOxl7$&8$nnS+^yQV5BhoLs=k z)ST~&gw%h6zy1=Wv~+fM-~#~M+}xPm*qQB}EC8&$yu1JwHUJwN)9VN(kcXW!(4EN+ zMD-t&{MS4ZW*`$MD+gyQdpq)f<^>wtyEqF{QvQSJzd!%+oM!G;|AWa6^q*wC5(xOG z2f)hA0{HK_UqJ=_Y2{P4ayPTllCZLUWzQ=PAy!r{PJw>`{GYD>LHQq`>L4>GaeLcW zNN1t{Veo$f|L@NKFW|p$YX2{sY`p&)<^SsWPe=j4KcxRJR{RH_|JC}+Xdxs4z<)2A z5K`*Leg!Bf5hz&+Q8jnyqc->y{l1JHeUj-PIs~%Wa9?v6KPr<;zeZRnFeqGHJW+Ed z<0EL-?z9V=vez+fE2;MF%i}HV{5ACh{qsgPoBmx~G`k-^214chR2JivW2(D+@|)4u zoGCmJyjht&I`-1F7aVV+o%yZ0>vo{#4l#5oM&srradDy~hk3hTU>3#9*3ti)UUHaB ze+epTI7|%aw@LsP+(*JX(Oi_U~< zFcngVxZw^!v`s+Sj zwz9bQl*N`ki&`q&?vCvZ^uT^=HTJIIzF|dUM4^%JZS`lYta)Dk@i{R6kX&!GJPSO{ z^t7nZt@P|j=P500@s;YBh6pq`S0FNkfy2fK)}+w)zaKQD1Pya>Hq$bZ?ndn&F{e)# zrl>C6`gl!X-5lSe9{Xq-Xe3zHdfWhSI;~rL+-sQ7EOhm@-jrmCY-~6u24QWnfo{w5 zUPrsh-ZSl({+1-yB1?{1!y3ua!KYZsmn%+qF!Jzq6LSAdON}A~N4lVOE|#wWI7I`% zz1mkbTya!Cd~R5pRlc}R6J1K>^a66 z1@%5kpB(WW658M|iSU!QNJ7w?gTi8(*+;+q=EEvE1r_ZV>6)BTD2gAZnJ67=pFYY$ zxNR4RQP47^l`)iZ$%64%j@wRUY4iG zx9p2EJd7DN;gj7uX~XFriOKVCEjcel4#xDfQ_KO9dj*;XITcU#4_97MIhLygz%aEy;oR<|os2+SCE>=5=co2%;K&(Qq} zqK_v1=@Ub3dpQya*`9dOU22%1zQQ7ZXA_wMLR%jkM(%y}U zBd62;u-k45k8YSHaeh$XX?7QDYjHI96Sj$&B4hn-m7#r-*AxU#w45pEbyQ+S+f+=f zA+9C;Wcp zvDC>q1@7;#VSm`O-J%}F9L{xj^S5^XY1US5HU=WQifbA>spp%pS1 z@s?Pt2?bPRM5CqRAosLlFc;9ad-q>&MqZl+$6r$pzzmk~GC@aiVEUhf)`ckyg z8++YI!BwdAC68}B)DOs6=V;NKKXH#bnmcg{Y@+Q#1Hof94AWc>N>nwB_BmgttZ)UF zUO7~`>6WH@&T5#!Q7LZ7fqHD|$a<#h4|xwUhEjf6bv!vBd^rGgCc$HleS3yyxqE^R5V<^7o=qNn&)M3e1>VOfTa?XDD08zXns zv3mW(k4QMe0#E4 zZeH$9{`FgY>U2DLGSJAiQ=+Kzn~4|IMA*?+_9UcP)d#Ero_5+r>Bh6TR-0#DT32?d z8&_4SOQ(v`#gn8Z)1{NtI!wEVb{e)bVlr8lMfgi7`VbU6Az(+u_+Xaj;kR&3`Lv8t+j+-7=vUv~;rq3F z^o!B2kY#oF_DFT4a%EwXhNA8PK}ekFAQRtTR$N)i4w2lX>3{hSiG(V{bgjjwD|x0!<9g z%%-^qHVuoP*Ap~%P0m&Ff>>+qn^D0(G%0Mco1V(rT`jsiqxqectTx{cjG=N#kMnam zm*F!|k)9Kz)FJWsE70M#KCaUmvWP8ktWoSZYqd7`kJ%eJ$rmHa&I%GU*> ziNSPNDP>e>)cQSf%_GcOc4qi#J^@Evb8byROg9MxQs#f8QvL*so8BK0wJf7;HuSe^ z+z_?EA4jiR;f5)2$sY&Ixjw93a1~<4P}rPRc4!@5kiM-f%2jaRsyX;xz}j}hNoBnD zuFux?jpAWxWhw`@<(bA{Fu~KZTjt_UUxd9-YhNVh(lr;d2oPqc_VoQy@O0xpep>$_ z9eig3d77J>9FryGA8l{$KV=U7VJ}oY_$r{3dfD^K2YRLtk^@QM_w3V`BVaJ#)3*`ZSWnP!=-RvZz3;VIiiT*wC0}VW{Ud zbKJBz@+0saE;vPA_upwA-Oyx_H2MP!o|tqb5leQ~CLY*jPH)>T%`5svU(mvJUjOe8 zUROJQzid2G3~`Jo41*oTljJI~&r_Nhj0LA593V*jMM$N2*C&()q+y%yi0j6mF1}y9 zZ}!E-BTf=PIZI>DuOR!{-)_o-x5!ea&Gj+zE=40f1g(3_@ak&+{5!# zl{ut9j7G=Yl1O~m^SbCO)oI>9_SFQet9~fxikl%lPkDNAx2jB^3k4-767lyGcxt?T zuikFyxV*(Y1~RcZe_B%u>Shg1V_k4YH&G22zuYMKs)dZRQJ=0AW)z%~FqO(u-&5L@ z0*g4DA7z|)0Y~y?Qdh(QBZ;aMY0r!sv2iu8#0t^6YOUY#_p*|#Yq>jwzV^5!yvZD0 zz5rM6+<`D?k?z=%6+Ucz)oJh77`ck~ZPK8aiyRj>8rqFXDGo8>$oA{VeMi;JtoDd_ zM(3KDBg*CBW?Bj>g`OtS+%B7hOa5!Nc3Tvo z8l+&+Tc}TGB!SHU>EA#ra{LpSW>}uObiAEaE0@EaD|M@uvud`!nJ&CG@=0HoF%C~|le*`t=*90)7Cx;=Cdd=>`a@0Hj4Fj59cOBG^Yg9-64S78X zG8_jiwMbfTuMd6lD|d!`T)#EFO`bD;z# z#4W6-K&=G5K!!v9t_k(WBlv*uU4q#jg{lm&lFlbJo*$OjGG9NvfeuH895*Tu0~-J; z8))#n+*27)Dt|;&Q&;EptwJ`&9JUctIVF0;p(mE*K-zlCy9Xdq#mTXV(S80V2{ddK z2A50x63Kl`mYtwO5EW%Xk~Cih8+_y~7a?_ru;2KGlq66D-H#^t~S^uiH zpb?*}e`D5gkp--9$t-e&J~2$l8Q>OC&W;f+$TmVsnT`xEQnHsmTu7Ae)oxH`nqhp? zIzQ}jJ?1|1((^UMQtm%m_>)$o@ckywW>yRQh|>q3JD4y5w&7`v(J^P!X>Sm& zMjSwni|IJq6Feu%WBhVSz^?r#0}8IVqy!vzchMF?kc%NaDWOFhy*59#`;l;)DsS@* z)1WK`7nrER_E}Uwj2s~lQ?BVlJk+=lf+QnBzZBsh4eoX|*ARdqBgMvL?k#9mxK|bc zf*jOXxpTsk5=7je9n%mXAqFzJDmi&uN^jVbp>QY|C}zeUzIaV|N0B5c(h0=7fg1FE z^1KKr%DmJ{l!y3N5t5}`e8Ua~AW>j&-}iW8Ak3Q=rGX=4$wzD@`49}dkgeb-&pesM zu%ey5Px!vIrWi1yP(}nVuQu=x?4iG9f3Awm2R)j+L^0-16Ga%G&#B(hJ}D?ia;+3K zt!bQPEDQu(i-8Xb62CTc3PEpa$p}KP1~pdn(BZnXv2%A#?38#_a18 zAN^%8f?;xL<$kew%VgkFPr_J{H=5ofJWM5FMNjVSrM$J|2mv;^B8-&ke{AW}s43&T zuNNkz+vR3^$Btf_@o+cebGn+^`>HC0tNzgkF`5hp&QbDb-PTx^LIe^XYM-$mx@it61vr z>*y$1Y4d9F^|@~hfW2>et-HkYGd*$<5~1@VEqvic((g8nveeg){qk<<(|VkcVlT(v zPY(QRl=+G+5>-E@?BU5Z;&7xy%=GRAD^ZD7K_|8g2mI>aBJ>p$>WF-Y29LLLf+Bg3 zf=_kjR}5u6_)bn{ZTZ15`Yn$j;`xpaBJW9&895~RnZCS8Z&8ge&4X?LzRV1$@f1Ug z7W>Cd;TAHmPi)5_t&nT_!=&M}hlRB0TqFXqJztz9cGn?u$%*aC!HRyz#H!+|(3Rb3 zwy3X9R)jPWm&)%Yr*8GXpS3$_rera4hZJ~_aBiEM`Q{uB{>Ym}eOo-JgMdeB#$;B$ z)epysNEhhaHGn4p`|;l29uz@eEej_0R|eGj6L&8pSq_#qN-CJ(Hj4_p^w$>( z{j)2r9pLjz2Tf5+ZVg6)i8Z zTm>qusJ%R#VIMxiZLq#MUbZ`0bX!jN6d%c7h3o?`Z0bJ(*Dc}oq83Jn96h7HHTklZxJgh|BhR-=DOtj)qbZe`(F@8Y9Xf{4>{ZG$5`;>{O@P^fwWoX*iMd zBzc3~vgax?+t+CsGVUkN4}X$fk=LEqI-!NpHTCt61*(3&GW5qKkNe-w=_SZs=#GYU zr_FrhghGZ+!A5bSTLnOGPm%vhjhha=L@f@-wQ_Q(F_A&@j`viN|7aSCO5fZ0w;?$` zDw(U3BkdBulE;j?9c0$l?kN=2PKTfkGLU|gteQCn(r+`*SZel>mck}9%EhpH_Z>lc zaRC^Jb@in*_Ee65<-WEX0w--7^LY_Uul}PPlru%^vw^?N-}gO7>|y?c zQnU4{UmX#90fkUPv*IRjrg)}33c(CI(Xv!f32uTeRD7VX1;X9iFwfp_!8w`pv7~g;{4X zE8wO%Gq_x7QlF8zm-+>M{%*$Qtr*Nb=Lxz?uAc&VNAoVBD!|Nd40+j|hVh6}9X@+* zG0E3Hn)>^SyxZ96*^LuSdbddr3jp7JySp`o`U-0#)Qvf9G**8KtCV{h2H@Zjp<_3C z8y^a6PDA18oh_fA{NWjkgz`v!xt*tdJYE!Y)2*@LdR`98Nus9rwy($wbo+U%k@JdT(-ynTrMYBbw*H|>EAfGkW4~RqCUoD$M><2{Wjd3u)q_lnZiDSj^t9=7GNctfy;I7>ceUNeZg{qA_U!b>FBQ+3pA0@d zXMxCL7se=UI@^1HYJ@NLmCQyXhgjPOhze`jn04om0GmHuuom~KlS zT(t{Pji;;%Gt9R|K&0+9GopM&;vCk*LuRy~PyOQZ1f*pA661fHFi!xcPqXM}$JfqY zwqyM`44Tb`FV_^@H_hm`?~N#SX`lWsuv+ho{6LTje%jlNZMiXgYL_?pagU;$o$)n+g<#k=<1SxH00M4=P^9CeWI;HX?r%nl3*N z`8}l(@n1XRMM}A?f+}Wm3>`q_tpZep%RX~AU{VHM#W9LvbJRz;g@xa_D(#TTyYs)IW@rH9@0G9o;@>z28}QfSDPoNG289*p@=+j`tD(Y-NKZ9 zSD>%~uWfTLHOK8*phk*-$3bMJk{w!#fo{499DrLw6zVoTJAIEtn~V8?8GWEI*#1~O z%orVYDjG+M3((#`BNm9O$?3lu66MJ4=5#ric=5)J+PI0nDK04JUsW~yfZ}NNvo(y- zFF1fD5Mob>Grkv=1YM!qu_8R|<>#FRAP=k~YqLQYxV3hhLIvq(B|*#)>qnULw~te&T1GHqDg%YGklJ&xQTJ)IkpPqQ@8HtPkU^W z>?#@`Uyl+A-_LIz|7!4^pPA;W(Q3A7ilDb|O1Jw7*%S%d%1<_|vaaBQWC+=H-=5js zU$Q^3(yJrmw(eM}k=h7Y_6QJp=a(9Gv>)$wG_ImscL%=1+<4M7+ad)2u>}DGHE2l zO66&uQN$jcX>u_HIG5B%j7Co!A{K zjdtUs#|=5a-FcuB3f``+IHP?gk7J;4mdTR*?PD||d0vLc1zsablA(%ps?S2c5Ysv> zlc2++gu~M)WrkbwdD!{85|K%c6YBk+yL}iK=d0E$FGBWRY1eiiSFdVY$=aJHp{#Z_ z(FOL%patgv?YGq}PxZQO;VL67iAgsn)sJD-maQ{Q#}5nR7edEQ<5Qp+rHd7D^ERJ+cLkf{!Cv!ADxx$l8(adb!5DDE01m|Ynw4Ew2|j5uj6-yZXymN8q%po zvrDgHNM!h)3aZu21+Nt7=YA6lmM=LWt-J1B(r9Sf4&pWsU5G1;^A&cVLM=Q0=`eAf zhkX1Hr+sps`BR|w6z$SUksBepvC&}IwVjK`*NHciLOe?fg>1*&VCx~C(Vhm-f6r4k zCQZU9U_IpRE^MEnJgixIx_wxhI<+iS!%QgT-B_U;%%g1HYJ4Yf_uh;rX)VDdzgRpJ z;@p33RzkFn5fZ4}iw*F5YA|HQd_o-Hb9n(gZD*Oq2+y7%0thF{ceI#qenx-nep)B< z?jQZhx?G{eQlh;00R@PHhFtLAca>eyFj{&2BrWV&Qn$|Ra#F6l`+Ny#-)if6>}l`; zBRAh@^F|v5KU`Y8{JD7P-aT+rf&al0-}J?8pQ z5vbM29~yqOv?_RL`Qj9HPj|G;-|@f%5K<9(+I+cq@r=}1=^JKl>g9X#9Y4Srwbdny zK)M|k5|x9O5b#Z%>Lrh%pa&v$@!k;&_Hv8`vS2?*^X555=$fNKc|u|Gn2)zl>q^IU z7UivL37tY#Z~HIJk_~JGC482r#n*)^WWGA6UVR&cdUoXM7|eTsO9|;3^%q(BJz}riiFw>BXYz~i!$@Tf zEK@T1z9!g%RXx1^W=5WTANk-~BBIL`qh-RJERI5GlsIm2$P3V~pHBCxA(408PqebM ztgNjXO%E71JT2yiEnH6=&esRb@siXD=Y>>8Rm3yeY?>VD4@eLX@R-nly$m;A$0AOQ z%iz+4?)m%85uWKH|1`f!0w}Tmnc``!!Vj`K$55Vfw5l1Z6q=)@2F0>EV3T!2fEf}P zb{R%x(A(EeyWVj3Ya*%-JBGB)@MVXBjPzYkqoc{uBc69(>}I$M;MGneN=_{&CXc(O zGkHs>5w|gn%mibIh6GU{Sv^0=YCoUnQ}@zKjDbplt1X(=!_3F14J&YUWGkRo7d{}& z6<}SZUYai1&X|1Ba<0!l=dlcY!SFKg8TsN;<3hM5mA9j0+|8-01Fn zJ9P;?8ulFhJaN4zw28L#Fi3EBB*i!BM((K--MXQyjmFe?G*tW#`6{KpGI~zb`Xe=s z=b9L)Y>(qX!l&8BpT}M{s|K}y$i|aNTMIuNRZt^W)H}gT^GSoZ3G|)LLVO#Y+uf*q z=PtINtgNzE+Lh%pW77p3b6ftbuoK^jvz}<^2t~1QO_fT0wL$9>)6>xk`Duvg+aFcs zndb6C;C}u?`&6txrm+6%sW{G)CbD^IW#V{LRQgRel3S<>8;M|fwf(PgN%`Nhh{OKlpo$@fQ%`ty()_Nt{Z5v?fzdVmoika3QKdxKT8eoZVPHs-0&i`XFHyw{Vqv5evQTjpalruj%R`3%SQbl#@tSvU(WrWm)&UOtN7M# z+FxLA%1*rJvZSA)oSrm36kAe})2VQI-;ABK*uM;9JU@QEhhTpopy3+j&g2cnm7pFI zbx}xP3S)Z;4WkMvyE4_NjfZZOL&H(6 zbWo(6HGXr7>0&?y>aTK!Pv;Ker>@>5p}(%r6D=g|KQ!bq)ST*dfVxqe)N7Kt6bj>L z!le$L3!18vF*r`BY;){|3d7+(9QPd;ZTvoc9>Kpao66G}Z0L8Jh?Ce*EI!us>!1$Q z#mAS7rVFo@cdU?gOj_8DCaQK&oh7@r1<%>lMQE{~B))sj4AmAdrUL5`wGh**ju?sn z*u}qi7?=!1RZ&PSm1Z04SUz?D1%p~o@bstktjMPRuK7BQ$0EhdzAvG(srR~{rWMLe zRldlM$k5`;aO}qhFn>3UQ(}SU+qX;}r3n6Y-hUNgSP3N>HMA|PnRlL_KX;@r+WC{k z_b1+`4TnIU&>55-fSdOA6^`7%5^+4S)pB^& z{QIM_31~qiR^!$Z#<<%%{leOQSkHLybmOSDaw?_|SeaQ7)iaJ!g0BzEjjLg_NkF_< zeR$SiF*YAbc`qz6zPrsd>zQXo&%eDfImF{hL=YpV-JeVu^p4sQP#jgW5xCM@GsY1q zW+Fk2Uoyu+z?T%#Dvl7Ali8`PCqcoNW{I+afd{EUnb`I|XBpY7lF4U{xRq;?Hozhd zRQtn^Kr|8b&5d=RB3PYSj($Jiwbf7y_6q<(gGLW_BGUqU=v-_i8l%D6!%v5>^FTnL zA%66}E@z#Cj4>mV|Zof%_6q+hW$STTWKvY?{qR z&kV#Hm@d$z%zFaa?AE3+O_irvu1qm*A(0^z9>pyMe_`0ozqZ&MC(&|p` zt?1p|kjr`~SiYR3f%9&#YN!di5$P-YmzyO6sOFpF6J3(mKe$}=G&H8$HO=VSvYuUcJfFObEAWB} z$TIu+(1y#$pV!H}tbh64_s>0Ye0&y35|)}W@{kBy$9oMwT5;H1zVf|4uCC}_Kfdd_ z@wWJ&Avw2v+9gk?l?sFk2)}I-ex@>XYrIao(Y;GJ`@+hQK}qQH5w$n%i71`DPj8Nb zU1~yjcpAKhfg%-6xWBRl2J_2~vn0`~!3BvUAm*x6%7}#+*zP{bG?T;pbkX9w*lcG@kS|_BLSKv?T58v$_mXwTLnVVOEQk}2oKIAE_9D|!jkeXZ4 zLlA$DUIm0D8u45M3S(R4e(LICEyPSj^YkKsaqhXAD>P?bZ#{08Py;$O&r=i|XUdJs zeIf~z!-zt_E8)r()Mk6+Xe6Z(GHH|A%*BwVCcBwz^&mJ&$*9f> z6H4W&0z4A=N}MV67~k|k)v$E2;?~F5xBN^9qv5IFzY>4O4D-vA1!;Z=M z&j#KurwMvbWGYXF8|0GJ#5=!fqkSl;B{uu6;NnsB*@&dGl6{DH=ZPGU^Ekj3X&@h( z(RUU67degA((iS%f!or*S3G4fxd>$dh9r7$tx`%XxF08tw7Y4$79}GfKYXg;Wz(xH zFcsK#mBStji;yYGLVvl80KGH(uoVBkxvvEWstzP;5M8%++IvUk&T5q~E3pRB@0u?-&k7 z)yr|==_S)+G*)$0_6sMWG7U89_2o;WgLi5YtH^>pRxe~te$Qfvsja}Yc~!4Zt6Zk@ zJSV8)L+!7MV~pEA)uxWV>vuNnw?vQMyY_`AG$-_qY0x?x*06Ka<3e1mn~xwYJlcpv z2v@=aYv5IG#yvV_h5@HASHwgxhE~TZVv_-^178pVs9V8z^ypP-Kh4b~5`ODV$PEO~ z1MPk|Fc^Gl`1~fLnB?~Cq^`XUGOX*Y^-F&<`lfXX6HgJn4evo)?D{&=h zVw;dCvp*fg)1Va=GSFC6_+Wt5e{^9&%%x#yHSp--q;}X-NzJB7bB%i33K}~z%r@e; zpI^^QqD~CZ6QoglF4CjKU&pr}ak2VHPFSYbG^r_IMgBvZ1?)dqnJOI_QB~8VeD}w- zwB_iRf4(%av^}XIVg!-u;KS$@@sH{Lf^H3#)4Ajl2BM}~V5 z;R%0;C$SJ@#IMiCp(5*X{>V3mW5=HYJ=Y?hSH8N*`00a`&f+%cv~eKIy&KT8`h+g9 zMr(IsK}p0s*5l5_kv*x$n`hYm{6{l6B`@QwJOMgKK74bm?KS9q{K5xEPU9%gH}CU! z9edAPn(}bm(D9Iw<^O%K7L*7WWZ5|4Da#MZaAjmZaq6RB-fR@Q!0%&LB;#mIxy7U) zfuO>vh88QZfgnx}R%XOEUEtR@WypOAj#{7A@QX?LQOXk zWgqP{BwZ5S+x}k00Z20(e?B(-mGY}PYdRQW@O~bzG2-z6=ZI(liwGeh?L@bI+y9dL zZ$1r^=g&J@xX*@Y9(+~X1gv=s>HNf_a@~%6@Rf{P)0q7+d!+Y9K(iqBa(hi)ktcN znQZXEvfK~uWxG;BLLN!H)>cfg0$Y_iuA`q>5j|vfz55}_{`t=>UujrHJl<0*y+%kLcOi+AS!^g6vBAYd?5^uJ$|b4X@mgN zZ;t1l2j;Y2?y}ld{_LA=7G4oe223DzOA4*iaiIN36)!Jh{RwrmNM!FmRk0R}G`}p| zJg56%v>pf>E>{$Y^80+uNnTzuHGx2yhwcO$W&KqY5tvZ|erd9J^Cep=Bd~SZY4W%G z_tQ>)ve^uUI@77Pp{DjvXQ|Io=Qtbgt?E%4d|nvU$oz#$uMjmVc~{^dh4DDiCVbY$ zznkHvIA$WRI5v#yd5+-J5sg^r_D=eQfqfmldS$t0+>W$o$XEWa*F18%tA6*`PO&Xa zOF_Y5A64pU$#ukR9T!J5>1`bs;`*^*%W^G+#clTOM(^W0WH2#<%$}oRb)LscL?a%L zheEj~GqYBAh*`aRhzT%tRH@uY4aP(jGH;k-4-H9D`6YEP4z!d!tZ0L9*v&0H5EOnW zm2mfym6m^wf)cs!Ju7E}W)hcipqN?(_VkO922!gy2$)Q6*IHSaGVWm%%iO3jvWS+~ zD9y1OWUQv!w{O#SAmN5T>iYVlp zA1+qfAA7PxQ%=PU+|NSr?gtt6M;Y)Y=rjqW^`WciwDS6HH024xv^r1Y~Z{!absXQQEHj985p8aqBRH_T@1V% zcs-)HesO~_M?I|R;pn*LZ$c6x?>XKQ*=J~A;qCW4{Lp6h(F5e5wY^@12UilScXh*( zz@bU1=<%)Y%i{_=AH~+wdfHu5&IRu+WQi@$rLS1sZvP8WTGB64qQ_TdJyR)DL6=Yxi!Y6(uq{-Wv=If% zcKqt>5QD^3QHTeKCWDFM_1TpsGH9SefQze@K&uh!%k$xjQro%#boEW-uRM>BC-%?i zH(oS^j&|h{m6%TzE$imP=x8m5;2RSF%vJ3Kb1fuQF1-Hr7?p5A+--N3_n6;1RH|U^ z(nE81&%+H*-`1=qcn|?>@AD7H`l5GK0Q!_XzVMhI2DKK!de%Ng3hjOn_sX8o!$a1! zZM?3KQ%e@@MThUUy&R1V_2w+*(w+4u>ejD}#5riRhSz#KVOA<4Bfy_)k$|?tww&pB z)`tr4d{FaF)Xm+K7R9O6*XirO=|gdxG;n>avdcusc=w3NXL3RDcXENhC}ig49YQA; zh6$raw7cebh3+C^oA@C6ho1*%XH$vouF~8_Im$hVeM)Y%^11dkQVro~#+dq94Ur57 zUynJ1+Pa6e40qTLEI>YaB%6dBpWRdc=&>Z5ozKrJWO5ANv^t=uSZoie`#xSVT`w5Z zaE!Pt^F+C4I7@9)rStdD5K8`8yJ+k>I~<56+T~WMVCDC|tQzPBzHm~M#yd6rg?xv& zTO83+nK@|@i`mxSlwk-TMFUXjcM{h~y5W)y0xdFq+S|J7c+puAa&ANL44vXfdcD%~ zyTW{LDqBt;FP5VSEbgkdM!b4iC6G}Z)_G9AD?sX6Q z6!_g&zHRF)j`S0%Rb}|VRrap@a(VoTtlo!q2U314_oF&%&iHd(f{?Mj#kLO1FN_)Z zLe`SOMK&4y681CZ{;|k(N3&&e1hU5Lox7DNffO8XgU}A(t6l>bPW@*ODJ{mPlb?HA zYDuxLd4^fJU;Y-AzV^WPwly+$mZZJfk2*E8Gb=H)jn-pbbFA5PPuT$U+i3j6+b7zx zd_88yrhhr;{lrM=KJCzZOvT>ALzcWRUs4AH44Xwpb)CwD$Q^+LamSEP4;JdrbO@MD z?6|?hy}UefyCU;`sMo~#i;V_Cj-m{ArGd%}ZwAOx9uL7Q+LUL4pn$hM!y(o`DjV9n zj5IfrJ~H4Qu8>wJ?^O1(8HJB$QkYWmrLzVeP~oXO+n>MOc6`X&kmhekqfQy0atDSx zbYA**isu{*QJu>&Z~TlR<#_X^fP5Tam7$5VSxI3>u*plv-fJ*O2f}TKdG1HFk|*Snd zP0vfMbfsugxvV zyDto{+bcODz%lBu%u89v(`_z2qwwf_D*qsJhy2#wWdFftkKpH5U&I6~{bBg|{PW_p z>=?iT!9E@lg=YcOJY2h*L?(AOp0aQA6m=%XE!%e|)LiP;@fIaVPWa3p$zdOun$Gu5 z?T!Sz4XU;Ufr~OhHh)uU-7cRj_$cR-Sz=BN$OSv++g-8GpTYg}S%M;0d$r^7#>9|# zLH+OMk_1h12Us|KX?-DY3*t#f8=#}dX`@+uy}#!=SOi22jx$zqGONjTY6MbZc*-h_ zS|*UcWsy{}gmJiwW+dUL8WV~;3a>Lh1O=2Bp-sBkv`X^3Ie`po=WXQlytL6t5WUal z@&0|4>*C_YP(xSD!&0rlx^FKz<0PH2*`^aB7K*k64K=sZeXKDf$@^*wZNJ-1z$OH_ z*Sj1vT=u>li{YKG9hZQQ$Saf_Of87~9?a2x6brS*4V(~&b03Tj!Dgq%iBJrfyC2@a zPT*dB970^6V`8F)M>Wp;yY<$Z3wANwtj3l)tNoBbx08b|;XPFAX-loEMuP4=lX<%QThS>%@oDc$lQzP(p#gKXg_lB7iz+DcmmBJ373vG7 z#&O6{R%c<6`bA=t_^v38OX!_VJ|l(DaA$^n!L^*hUg#Lt>W0`;=SN7KHAJ?;8$XK0 zC6DfsAXIMnT+@6)Lz1p`VS*q>^t+w^yOlY&-wmQ>N5;9#qzxDtk0$?~1z=NZ>Y5v= zZXWbaqUV9_B(VtGY!+@}5r4?0CvU6nzqi@M5sTL$*@C(J{ezx(%M)t*>jM+d)RtrwIEs0>xKF>pX! zFb15fI|xf<;2-T&SkyVY4SsUH7#6a?r*xEn7gm1Nc=TLO`77400r?LqIS;{FWmKhpQ~ zLbK|47NYN`j91c!AlHqW&31WN+MsqAW{$P`2o~<%PIVH%W7@dzW=b;rX&Wjr3xh&~ z{^TC9aEnCwHmN55Nwvl!m^Gs;Q1gtXD$Ij%Mm4}mfS?gsxySI8XRc-b>X$N0yWZ{b}5M1Eow%nLVM#Ezqz6<~POf;t}cD_|{{K+ha_;G+dU4U|k=Jx*L>01McJ_oA> z_B0(E;j5y>gF>1f0BRlA&Xpjk@6ET|Hc-@XF_$XoTWKd9ZIfVZwb~^T4k>pkx-=9nBWBb zb6rmIS*L-i$-Z&29H_BI+2LYUX@epC7%86anOQt(#7iHee`m}cau=uBMrW~+5?KG< zu^zAd`H>Z8TvTp)rG_Q;-g_bqH6CE|92P?eGX0+?w`t~I{}+^Q;#-Z6N^KfNKgFoE zgz>gBW|JM|2JHEO$x>;K6pH!~z3GI}IA=av3>@bs#!Cd7{Dt|+m4``bf80kK1V3NV z;3+P_s>X&nMP9Z0N^PNHPxJPf?Z)QY!+ucGDt z&&ukyD{_NZ4E5Cj`#iqdX zG=mW0+uoU&-j>wHU5hluRK>3QYtfdUZ9@XwFq}Y7DR`43`97>jf zQ%W7C+&OGlT3vb&#qzmE+0PFV17*Ek=awuioZxOMKAG<)G)#9T3vS?#dlr+z(uB9; zmK%D?emoAs0Im_?kXyhOsEzy08v1!4a|JV=8Kci5$PHgsN%+Ew*S8m_R*AW1Y|l2% z#8oM1J4My+r&*msb0Kq8b8wGDt{fjoaZxsSY@<0Ym2k3{_al=5*O!f9aosmppWtg% z`ffa^a{P6Pez}R&{1+;))b`j2|GWEy-y7JG7cGQT&g^*a1eQG37QBqPQ?nGSqCJ5CD!@7EtyyEbmxNyYxEgdpjB->#z* z5{?dF7Wa{kPV10TNqc;Hy`%opx@OF5s;=NKi{44in62tI@kjSgiVg~EiXSdV#A4gy zJLkvsq>?2p(e)&f1dq|bYKNx#u-|x1PfmFL6>aYYu_lfJH@1td%p&uPilYPm6nZ4b zC&ZyV79$F-sXiV*!;t|28(I2Be=)a_ViqsHJOz6S{52igM3@^l*jAz|;%w$Quj0)_ zIPaOug6^DS%^!1_>!TqY|NI{Z>Q{fMUbdn&IYEFFHWYWWyR-yyq0tzqk@%^K;%==; zomE_D;03BxcB&{IkSR&7@Gs)8H3{X>s*0+e(1NoF>CDFoj`|-6701yKGZfGXu3|)S zI*#p~?`TxZ*I=x&vNBO#4{Y->EY-tZlvO(>FW}q>eQ5)2fHc-5v$Yqs`W8s>D zvnF`tp=*3DjxAMGiW;xu~ruyK;U14fu=X= zfTt3EeTenJLQ;#VeDi8N(pF7J(>e-eA4l*PF?B`nnPVxBB8N81jOT-B)c#@%x0K04QfNz5vB z51`NtBCo|S&~efDR9;#7TIicFvG8M==+A;42cM@!jzsS2Pa7NA-%01QYwlY}P#&;TQJj1UqDb5Q)^^c3~9|ZF)|IQiK~U^kMSr`z88TpM5jvX%Mj(!1~}NQ$C2sB zVjeFcH#y=!6Nq-oposgNYY+_$4T=E+onQF(#IL|yebAz>J8FXLDJj=z4wR72BnHT0 zjN|Tf_k^Mtn%&uFQ9jXHMVm$Nu$A@Fv!x zNa_D|QN?gZViz<4FC9kw{xI*FjxtoGo@3Vwh|)^S7lS=Q$juMuD(U~hVhluB zK_JFj>d->alTfCsL$bgH_MO^fBjDuE%hMtsAl%#MgH^DKF(N7rK|HuD&^Vgyzi8D=DkMrq3#{sk*AO`|&(6Xgbd z;SHK)OIrpjiC()s*Qq1qrIya99Oy~N>q-*&08Xt0%>o@sSy6QCnN$|^BwRKHZ;|ZU z$=DaG4o4{1(pebJ^YZ>w%O{kQr literal 0 HcmV?d00001 diff --git a/content/discussions/quickstart.md b/content/discussions/quickstart.md index 2bdf5404256f..252da3b6cce5 100644 --- a/content/discussions/quickstart.md +++ b/content/discussions/quickstart.md @@ -21,7 +21,9 @@ Repository owners and people with write access can enable {% data variables.prod When you first enable a {% data variables.product.prodname_discussions %}, you will be invited to configure a welcome post. {% data reusables.repositories.navigate-to-repo %} -{% data reusables.repositories.sidebar-settings %} +1. Under your repository name, click {% octicon "gear" aria-label="The gear icon" %} +**Settings**. +![Public settings button](/assets/images/help/discussions/public-repo-settings.png) 1. Under "Features", click **Set up discussions**. ![Set up a discussion button under "Features" for enabling or disabling discussions for a repository](/assets/images/help/discussions/setup-discussions-button.png) 1. Under "Start a new discussion," edit the template to align with the resources and tone you want to set for your community. diff --git a/data/reusables/discussions/enabling-or-disabling-github-discussions-for-your-repository.md b/data/reusables/discussions/enabling-or-disabling-github-discussions-for-your-repository.md index 343f6d8a22d7..2c1343c7f7ec 100644 --- a/data/reusables/discussions/enabling-or-disabling-github-discussions-for-your-repository.md +++ b/data/reusables/discussions/enabling-or-disabling-github-discussions-for-your-repository.md @@ -1,6 +1,8 @@ You can enable or disable discussions for a repository. {% data reusables.repositories.navigate-to-repo %} -{% data reusables.repositories.sidebar-settings %} +1. Under your repository name, click {% octicon "gear" aria-label="The gear icon" %} +**Settings**. +![Repository settings button](/assets/images/help/discussions/public-repo-settings.png) 1. Under "Features", select **Discussions**. ![Checkbox under "Features" for enabling or disabling discussions for a repository](/assets/images/help/discussions/select-discussions-checkbox.png)