diff --git a/assets/images/help/package-registry/packages-overview-diagram.png b/assets/images/help/package-registry/packages-overview-diagram.png index 257a406b44d9..cc0ea9aaad61 100644 Binary files a/assets/images/help/package-registry/packages-overview-diagram.png and b/assets/images/help/package-registry/packages-overview-diagram.png differ diff --git a/content/admin/configuration/command-line-utilities.md b/content/admin/configuration/command-line-utilities.md index 204fc49dfe00..e90d64ff38b7 100644 --- a/content/admin/configuration/command-line-utilities.md +++ b/content/admin/configuration/command-line-utilities.md @@ -82,7 +82,7 @@ Allows you to find the uuid of your node in `cluster.conf`. Allows you to exempt a list of users from API rate limits. For more information, see "[Resources in the REST API](/rest/overview/resources-in-the-rest-api#rate-limiting)." ``` shell -$ ghe-config app.github.rate_limiting_exempt_users "hubot github-actions" +$ ghe-config app.github.rate-limiting-exempt-users "hubot github-actions" # Exempts the users hubot and github-actions from rate limits ``` {% endif %} diff --git a/content/admin/release-notes.md b/content/admin/release-notes.md new file mode 100644 index 000000000000..4010feb87963 --- /dev/null +++ b/content/admin/release-notes.md @@ -0,0 +1,7 @@ +--- +title: Release notes +intro: The release notes for {{ allVersions[currentVersion].versionTitle }}. +layout: release-notes +versions: + enterprise-server: '*' +--- diff --git a/content/github/site-policy/github-marketplace-terms-of-service.md b/content/github/site-policy/github-marketplace-terms-of-service.md index 036bc30e35a7..2d3b9ed20348 100644 --- a/content/github/site-policy/github-marketplace-terms-of-service.md +++ b/content/github/site-policy/github-marketplace-terms-of-service.md @@ -6,11 +6,11 @@ versions: free-pro-team: '*' --- -Welcome to GitHub Marketplace ("Marketplace")! We're happy you're here. Please read these Terms of Service ("Marketplace Terms") carefully before accessing or using GitHub Marketplace. GitHub Marketplace is a platform that allows you to purchase developer products (for free or for a charge) that can be used with your GitHub.com account ("Developer Products"). Although sold by GitHub, Inc. ("GitHub", "we", "us"), Developer Products may be developed and maintained by either GitHub or by third-party software providers, and may require you to agree to a separate terms of service. Your use and/or purchase of Developer Products is subject to these Marketplace Terms and to the applicable fees and may also be subject to additional terms as provided by the third party licensor of that Developer Product (the "Product Provider"). +Welcome to GitHub Marketplace ("Marketplace")! We're happy you're here. Please read these Terms of Service ("Marketplace Terms") carefully before accessing or using GitHub Marketplace. GitHub Marketplace is a platform that allows you to select developer apps or actions (for free or for a charge) that can be used with your GitHub.com account ("Developer Products"). Although offered by GitHub, Inc. ("GitHub", "we", "us"), Developer Products may be developed and maintained by either GitHub or by third-party software providers. Your selection or use of Developer Products is subject to these Marketplace Terms and any applicable fees, and may require you to agree to additional terms as provided by the third party licensor of that Developer Product (the "Product Provider"). By using Marketplace, you are agreeing to be bound by these Marketplace Terms. -Effective Date: October 11, 2017 +Effective Date: November 20, 2020 ### A. GitHub.com's Terms of Service @@ -40,11 +40,11 @@ If you would have a question, concern, or dispute regarding your billing, please ### E. Your Data and GitHub's Privacy Policy -**Privacy.** When you purchase or subscribe to a Developer Product, GitHub must share certain Personal Information (as defined in the [GitHub Privacy Statement](/articles/github-privacy-statement/)) with the Product Provider in order to provide you with the Developer Product, regardless of your privacy settings. Depending on the requirements of the Developer Product you choose, GitHub may share as little as your user account name, ID, and primary email address or as much as access to the content in your repositories, including the ability to read and modify your private data. You will be able to view the scope of the permissions the Developer Product is requesting, and accept or deny them, when you grant it authorization via OAuth. In line with [GitHub's Privacy Statement](/articles/github-privacy-statement/), we will only provide the Product Provider with the minimum amount of information necessary for the purpose of the transaction. +**Privacy.** When you select or use a Developer Product, GitHub may share certain Personal Information (as defined in the [GitHub Privacy Statement](/articles/github-privacy-statement/)) with the Product Provider (if any such Personal Information is received from you) in order to provide you with the Developer Product, regardless of your privacy settings. Depending on the requirements of the Developer Product you choose, GitHub may share as little as your user account name, ID, and primary email address or as much as access to the content in your repositories, including the ability to read and modify your private data. You will be able to view the scope of the permissions the Developer Product is requesting, and accept or deny them, when you grant it authorization via OAuth. -If you cancel your Developer Product services and revoke access through your account settings, the Product Provider will no longer be able to access your account. The Product Provider is responsible for deleting your Personal Information from its systems within its defined window. Please contact the Product Provider to ensure that your account has been properly terminated. +If you cease using the Developer Product and revoke access through your account settings, the Product Provider will no longer be able to access your account. The Product Provider is responsible for deleting your Personal Information from its systems within its defined window. Please contact the Product Provider to ensure that your account has been properly terminated. -**Data Security Disclaimer.** When you purchase or subscribe to a Developer Product, the Developer Product security and their custodianship of your data is the responsibility of the Product Provider. You are responsible for understanding the security considerations of the purchase and use of the Developer Product for your own security, risk and compliance considerations. +**Data Security and Privacy Disclaimer.** When you select or use a Developer Product, the security of the Developer Product and the custodianship of your data, including your Personal Information (if any), is the responsibility of the Product Provider. You are responsible for understanding the security and privacy considerations of the selection or use of the Developer Product for your own security and privacy risk and compliance considerations. ### F. Rights to Developer Products diff --git a/data/release-notes/.gitkeep b/data/release-notes/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data/reusables/ssh/add-ssh-key-to-ssh-agent-commandline.md b/data/reusables/ssh/add-ssh-key-to-ssh-agent-commandline.md index af4760bd1725..415f86736de3 100644 --- a/data/reusables/ssh/add-ssh-key-to-ssh-agent-commandline.md +++ b/data/reusables/ssh/add-ssh-key-to-ssh-agent-commandline.md @@ -1,3 +1,3 @@ ```shell -$ ssh-add ~/.ssh/id_rsa +$ ssh-add ~/.ssh/id_ed25519 ``` diff --git a/data/reusables/ssh/add-ssh-key-to-ssh-agent.md b/data/reusables/ssh/add-ssh-key-to-ssh-agent.md index 6a2f98182365..fb1ca5ec3428 100644 --- a/data/reusables/ssh/add-ssh-key-to-ssh-agent.md +++ b/data/reusables/ssh/add-ssh-key-to-ssh-agent.md @@ -1 +1 @@ -If you created your key with a different name, or if you are adding an existing key that has a different name, replace *id_rsa* in the command with the name of your private key file. +If you created your key with a different name, or if you are adding an existing key that has a different name, replace *id_ed25519* in the command with the name of your private key file. diff --git a/includes/article.html b/includes/article.html index 30d3f67b8f02..86ed5636d5be 100644 --- a/includes/article.html +++ b/includes/article.html @@ -55,11 +55,6 @@

1 %} border-top border-gray-light mt-4{% endif %}"> - {% assign helpId = 'xl' %} - {% include helpfulness %} - {% include contribution %} -
@@ -70,8 +65,7 @@

- {% assign helpId = 'sm' %} +
{% include helpfulness %} {% include contribution %}
diff --git a/includes/helpfulness.html b/includes/helpfulness.html index b4d8a04d5beb..ccbea5746db6 100644 --- a/includes/helpfulness.html +++ b/includes/helpfulness.html @@ -1,5 +1,5 @@ {% unless enterpriseServerReleases.isOldestReleaseDeprecated and currentVersion contains enterpriseServerReleases.oldestSupported %} -
+

-
+ {% include header %} + {% include deprecation-banner %} + + + + {% include support %} + {% include small-footer %} +
+ + diff --git a/lib/liquid-tags/extended-markdown.js b/lib/liquid-tags/extended-markdown.js index 1037b6d7c2ac..cf018b0845e1 100644 --- a/lib/liquid-tags/extended-markdown.js +++ b/lib/liquid-tags/extended-markdown.js @@ -11,7 +11,7 @@ const tags = { danger: 'border rounded-1 mb-4 p-3 border-red bg-red-light f5' } -const template = '
{{ output }}
' +const template = '
\n{{ output }}\n
' class ExtendedMarkdown extends Liquid.Block { async render (context) { diff --git a/lib/release-notes-schema.js b/lib/release-notes-schema.js new file mode 100644 index 000000000000..e58278e29ad4 --- /dev/null +++ b/lib/release-notes-schema.js @@ -0,0 +1,34 @@ +module.exports = { + properties: { + intro: { + type: 'string', + required: true + }, + date: { + type: 'string', + format: 'date', + required: true + }, + release_candidate: { + type: 'boolean', + default: false + }, + notes: { + required: true, + type: 'array', + items: { + type: 'object', + properties: { + note: { + type: 'string', + required: true + }, + type: { + type: 'string', + required: true + } + } + } + } + } +} diff --git a/lib/render-content.js b/lib/render-content.js index ff18773c5d41..99cc81072803 100644 --- a/lib/render-content.js +++ b/lib/render-content.js @@ -26,6 +26,13 @@ renderContent.liquid.registerFilters({ obj_size: (input) => { if (!input) return 0 return Object.keys(input).length + }, + /** + * Returns the version number of a GHES version string + * ex: enterprise-server@2.22 => 2.22 + */ + version_num: (input) => { + return input.split('@')[1] } }) diff --git a/lib/warm-server.js b/lib/warm-server.js index f9c64f99ebc3..d98f5a914723 100644 --- a/lib/warm-server.js +++ b/lib/warm-server.js @@ -1,7 +1,8 @@ +const statsd = require('./statsd') const fetchEarlyAccessPaths = require('./fetch-early-access-paths') let pages, site, redirects, siteTree, earlyAccessPaths -module.exports = async function warmServer () { +async function warmServer () { if (!pages) { if (process.env.NODE_ENV !== 'test') { console.log('Priming context information') @@ -22,3 +23,7 @@ module.exports = async function warmServer () { pages, site, redirects, siteTree, earlyAccessPaths } } + +// Instrument the `warmServer` function so that +// it's wrapped in a timer that reports to Datadog +module.exports = statsd.asyncTimer(warmServer, 'warm_server') diff --git a/middleware/contextualizers/enterprise-release-notes.js b/middleware/contextualizers/enterprise-release-notes.js new file mode 100644 index 000000000000..e871114426cb --- /dev/null +++ b/middleware/contextualizers/enterprise-release-notes.js @@ -0,0 +1,65 @@ +const renderContent = require('../../lib/render-content') +const patterns = require('../../lib/patterns') + +module.exports = async (req, res, next) => { + // The `/release-notes` sub-path + if (!req.path.endsWith('/release-notes')) return next() + + // ignore paths that don't have an enterprise version number + if (!patterns.getEnterpriseServerNumber.test(req.path)) return next() + + // extract enterprise version from path, e.g. 2.16 + const requestedVersion = req.path.match(patterns.getEnterpriseServerNumber)[1] + + const versionString = `${requestedVersion.replace(/\./g, '-')}` + + const allReleaseNotes = req.context.site.data['release-notes'] + + // This version doesn't have any release notes - let's be helpful and redirect + // to the notes on `enterprise.github.com` + if (!allReleaseNotes || !allReleaseNotes[versionString]) { + return res.redirect(`https://enterprise.github.com/releases/${requestedVersion}.0/notes`) + } + + const releaseNotes = allReleaseNotes[versionString] + const keys = Object.keys(releaseNotes) + // Turn { [key]: { notes, intro, date } } + // into [{ version, notes, intro, date }] + const patches = keys + .sort((a, b) => { + if (a > b) return -1 + if (a < b) return 1 + return 0 + }) + .map(key => ({ version: `${requestedVersion}.${key}`, ...releaseNotes[key] })) + + const renderedPatches = await Promise.all(patches.map(async patch => { + // Render the intro block, it might contain markdown formatting + patch.intro = await renderContent(patch.intro, req.context) + + // Run the notes through the markdown rendering pipeline + patch.notes = await Promise.all(patch.notes.map(async note => { + if (note.note) note.note = await renderContent(note.note, req.context) + return note + })) + + // Sort the notes into sections + // Takes an array of notes: Array<{ note, type }> + // Turns it into { [type]: [{ note }] } + patch.sortedNotes = patch.notes.reduce((prev, curr) => { + const existingObj = prev.find(o => o.title === curr.type) + if (!existingObj) { + prev.push({ title: curr.type, notes: [curr] }) + } else { + existingObj.notes.push(curr) + } + return prev + }, []) + + return patch + })) + + req.context.releaseNotes = renderedPatches + + return next() +} diff --git a/middleware/index.js b/middleware/index.js index ce3ea112904a..7c7d7f672e2b 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -69,6 +69,7 @@ module.exports = function (app) { app.get('/_500', asyncMiddleware(require('./trigger-error'))) // *** Preparation for render-page *** + app.use(asyncMiddleware(require('./contextualizers/enterprise-release-notes'))) app.use(require('./contextualizers/graphql')) app.use(require('./contextualizers/rest')) app.use(require('./contextualizers/webhooks')) diff --git a/script/README.md b/script/README.md index bb718663da24..ec7beb9881fd 100644 --- a/script/README.md +++ b/script/README.md @@ -59,7 +59,7 @@ The `ignore` array is for client-side or build-time stuff that doesn't get `requ ### [`check-english-links.js`](check-english-links.js) -This script runs once per day via a scheduled GitHub Action to check all links in English content, not including deprecated Enterprise Server content. It opens an issue if it finds broken links. To exclude a link, add it to `lib/excluded-links.js`. +This script runs once per day via a scheduled GitHub Action to check all links in English content, not including deprecated Enterprise Server content. It opens an issue if it finds broken links. To exclude a link path, add it to `lib/excluded-links.js`. --- @@ -134,7 +134,14 @@ Run this script after an Enterprise deprecation to remove Liquid statements and --- -### [`enterprise-server-releases/create-webhooks-for-new-version.js`](enterprise-server-releases/create-webhooks-for-new-version.js) +### [`enterprise-server-releases/create-graphql-files.js`](enterprise-server-releases/create-graphql-files.js) + +This script creates the static GraphQL files for a new version. + +--- + + +### [`enterprise-server-releases/create-webhook-files.js`](enterprise-server-releases/create-webhook-files.js) This script creates new static webhook payload files for a new version. diff --git a/script/enterprise-server-releases/create-graphql-files.js b/script/enterprise-server-releases/create-graphql-files.js new file mode 100755 index 000000000000..1688c016cbf0 --- /dev/null +++ b/script/enterprise-server-releases/create-graphql-files.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') +const program = require('commander') +const allVersions = require('../../lib/all-versions') +const graphqlDir = path.join(process.cwd(), 'lib/graphql/static') + +// [start-readme] +// +// This script creates the static GraphQL files for a new version. +// +// [end-readme] + +program + .description('Create GraphQL files in lib/graphql/static based on an existing version.') + .option('-n, --newVersion ', 'The version to copy the files to. Must be in format.') + .option('-o, --oldVersion ', 'The version to copy the files from. Must be in format.') + .parse(process.argv) + +const newVersion = program.newVersion +const oldVersion = program.oldVersion + +if (!(newVersion && oldVersion)) { + console.log('Error! You must provide --newVersion and --oldVersion.') + process.exit(1) +} + +if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion))) { + console.log('Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.') + process.exit(1) +} + +const newVersionId = allVersions[newVersion].miscVersionName +const oldVersionId = allVersions[oldVersion].miscVersionName + +// copy the schema file wholesale (there are separate schema files per version) +const newSchemaFile = path.join(graphqlDir, `schema-${newVersionId}.json`) +const oldSchemaFile = path.join(graphqlDir, `schema-${oldVersionId}.json`) +fs.copyFileSync(oldSchemaFile, newSchemaFile) + +// check that it worked +if (!fs.existsSync(newSchemaFile)) { + console.log(`Error! Can't find ${newSchemaFile}.`) + process.exit(1) +} + +// the other files are objects with vers3091iuions as keys, so we need to require them +const previewsFile = path.join(graphqlDir, 'previews.json') +const changesFile = path.join(graphqlDir, 'upcoming-changes.json') +const objectsFile = path.join(graphqlDir, 'prerendered-objects.json') + +const previews = require(previewsFile) +const changes = require(changesFile) +const objects = require(objectsFile) + +previews[newVersionId] = previews[oldVersionId] +changes[newVersionId] = changes[oldVersionId] +objects[newVersionId] = objects[oldVersionId] + +// check that it worked +if (!Object.keys(previews).includes(newVersionId)) { + console.log(`Error! Can't find ${newVersionId} in ${previewsFile}.`) + process.exit(1) +} + +if (!Object.keys(changes).includes(newVersionId)) { + console.log(`Error! Can't find ${newVersionId} in ${changesFile}.`) + process.exit(1) +} + +if (!Object.keys(objects).includes(newVersionId)) { + console.log(`Error! Can't find ${newVersionId} in ${objectsFile}.`) + process.exit(1) +} + +// write the new files +fs.writeFileSync(previewsFile, JSON.stringify(previews, null, 2)) +fs.writeFileSync(changesFile, JSON.stringify(changes, null, 2)) +fs.writeFileSync(objectsFile, JSON.stringify(objects, null, 2)) + +// print success message +console.log(`Done! Copied ${oldVersion} GraphQL files to ${newVersion} files.`) diff --git a/script/enterprise-server-releases/create-webhook-files.js b/script/enterprise-server-releases/create-webhook-files.js index c7a3373b498a..bdded804c327 100755 --- a/script/enterprise-server-releases/create-webhook-files.js +++ b/script/enterprise-server-releases/create-webhook-files.js @@ -5,6 +5,7 @@ const mkdirp = require('mkdirp').sync const path = require('path') const program = require('commander') const allVersions = require('../../lib/all-versions') +const payloadsDir = 'lib/webhooks/static' // [start-readme] // @@ -18,20 +19,22 @@ program .option('-o, --oldVersion ', 'The version to copy the payloads from. Must be in format.') .parse(process.argv) -if (!(program.newVersion && program.oldVersion)) { +const newVersion = program.newVersion +const oldVersion = program.oldVersion + +if (!(newVersion && oldVersion)) { console.log('Error! You must provide --newVersion and --oldVersion.') process.exit(1) } -if (!(Object.keys(allVersions).includes(program.newVersion) && Object.keys(allVersions).includes(program.oldVersion))) { - console.log('Error! You must provide the full name of a supported version, e.g., enterprise-server@2.22.') +if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion))) { + console.log('Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.') process.exit(1) } -const newVersionDirName = allVersions[program.newVersion].miscVersionName -const oldVersionDirName = allVersions[program.oldVersion].miscVersionName +const newVersionDirName = allVersions[newVersion].miscVersionName +const oldVersionDirName = allVersions[oldVersion].miscVersionName -const payloadsDir = 'lib/webhooks/static' const srcDir = path.join(payloadsDir, oldVersionDirName) const destDir = path.join(payloadsDir, newVersionDirName) diff --git a/tests/browser/browser.js b/tests/browser/browser.js index 1d99bd64e91f..f050f4e6a46c 100644 --- a/tests/browser/browser.js +++ b/tests/browser/browser.js @@ -134,20 +134,20 @@ describe('helpfulness', () => { }) // When I click the "Yes" button - await page.click('#helpfulness-sm [for=helpfulness-yes-sm]') + await page.click('.js-helpfulness [for=helpfulness-yes]') // (sent a POST request to /events) // I see the request for my email - await page.waitForSelector('#helpfulness-sm [type="email"]') + await page.waitForSelector('.js-helpfulness [type="email"]') // When I fill in my email and submit the form - await page.type('#helpfulness-sm [type="email"]', 'test@example.com') + await page.type('.js-helpfulness [type="email"]', 'test@example.com') await sleep(1000) - await page.click('#helpfulness-sm [type="submit"]') + await page.click('.js-helpfulness [type="submit"]') // (sent a PUT request to /events/{id}) // I see the feedback - await page.waitForSelector('#helpfulness-sm [data-help-end]') + await page.waitForSelector('.js-helpfulness [data-help-end]') }) }) diff --git a/tests/content/lint-files.js b/tests/content/lint-files.js index 1714f0b041ce..10e188a24a36 100644 --- a/tests/content/lint-files.js +++ b/tests/content/lint-files.js @@ -7,6 +7,8 @@ const { zip } = require('lodash') const yaml = require('js-yaml') const languages = require('../../lib/languages') const { tags } = require('../../lib/liquid-tags/extended-markdown') +const ghesReleaseNotesSchema = require('../../lib/release-notes-schema') +const revalidator = require('revalidator') const rootDir = path.join(__dirname, '../..') const contentDir = path.join(rootDir, 'content') @@ -373,6 +375,32 @@ describe('lint-files', () => { }) } ) + + // GHES release notes + const ghesReleaseNotesDir = path.join(__dirname, '../../data/release-notes') + const ghesReleaseNotesYamlAbsPaths = walk(ghesReleaseNotesDir, yamlWalkOptions).sort() + const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths.map(p => path.relative(rootDir, p)) + const ghesReleaseNotesYamlTuples = zip(ghesReleaseNotesYamlRelPaths, ghesReleaseNotesYamlAbsPaths) + + if (ghesReleaseNotesYamlTuples.length > 0) { + describe.each(ghesReleaseNotesYamlTuples)( + 'in "%s"', + (yamlRelPath, yamlAbsPath) => { + let dictionary + + beforeAll(async () => { + const fileContents = await fs.promises.readFile(yamlAbsPath, 'utf8') + dictionary = yaml.safeLoad(fileContents, { filename: yamlRelPath }) + }) + + it('matches the schema', () => { + const { errors } = revalidator.validate(dictionary, ghesReleaseNotesSchema) + const errorMessage = errors.map(error => `- [${error.property}]: ${error.attribute}, ${error.message}`).join('\n') + expect(errors.length, errorMessage).toBe(0) + }) + } + ) + } }) function formatLinkError (message, links) { diff --git a/tests/routing/enterprise-release-notes.js b/tests/routing/enterprise-release-notes.js new file mode 100644 index 000000000000..c88701f8be33 --- /dev/null +++ b/tests/routing/enterprise-release-notes.js @@ -0,0 +1,21 @@ +const { get } = require('../helpers') + +describe('enterprise release notes', () => { + jest.setTimeout(60 * 1000) + + beforeAll(async () => { + // The first page load takes a long time so let's get it out of the way in + // advance to call out that problem specifically rather than misleadingly + // attributing it to the first test + await get('/') + }) + + it('redirects to the release notes on enterprise.github.com if none are present for this version here', async () => { + const res = await get('/en/enterprise-server@2.21/admin/release-notes') + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('https://enterprise.github.com/releases/2.21.0/notes') + }) + + // We can't write this test until we have real release notes + it.todo('renders the release-notes layout if this version\'s release notes are in this repo') +})