1
0
mirror of synced 2026-01-25 09:03:25 -05:00

Merge pull request #22040 from github/repo-sync

repo sync
This commit is contained in:
Octomerger Bot
2022-11-15 14:46:17 -08:00
committed by GitHub
27 changed files with 765 additions and 1597 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -6,13 +6,14 @@ import { ArticleGridLayout } from './ArticleGridLayout'
import { MiniTocs } from 'components/ui/MiniTocs'
import { useAutomatedPageContext } from 'components/context/AutomatedPageContext'
import { ClientSideHighlight } from 'components/ClientSideHighlight'
import { Callout } from 'components/ui/Callout'
type Props = {
children: React.ReactNode
}
export const AutomatedPage = ({ children }: Props) => {
const { title, intro, renderedPage, miniTocItems } = useAutomatedPageContext()
const { title, intro, renderedPage, miniTocItems, product } = useAutomatedPageContext()
return (
<DefaultLayout>
@@ -22,11 +23,21 @@ export const AutomatedPage = ({ children }: Props) => {
<ArticleGridLayout
topper={<ArticleTitle>{title}</ArticleTitle>}
intro={
intro && (
<Lead data-testid="lead" data-search="lead">
{intro}
</Lead>
)
<>
{intro && (
<Lead data-testid="lead" data-search="lead">
{intro}
</Lead>
)}
{product && (
<Callout
variant="success"
className="mb-4"
dangerouslySetInnerHTML={{ __html: product }}
/>
)}
</>
}
toc={miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
>

View File

@@ -6,6 +6,7 @@ export type AutomatedPageContextT = {
intro: string
renderedPage: string | JSX.Element[]
miniTocItems: Array<MiniTocItem>
product?: string
}
export const AutomatedPageContext = createContext<AutomatedPageContextT | null>(null)
@@ -30,5 +31,6 @@ export const getAutomatedPageContextFromRequest = (req: any): AutomatedPageConte
intro: page.intro,
renderedPage: req.context.renderedPage || '',
miniTocItems: req.context.miniTocItems || [],
product: page.product || '',
}
}

View File

@@ -7,6 +7,7 @@ import type { ChildParameter } from './types'
import styles from './ChildBodyParametersRows.module.scss'
type Props = {
open: boolean
slug: string
childParamsGroups: ChildParameter[]
parentName: string
@@ -14,6 +15,7 @@ type Props = {
}
export function ChildBodyParametersRows({
open,
slug,
parentName,
parentType,
@@ -24,24 +26,27 @@ export function ChildBodyParametersRows({
return (
<tr className={cx(styles.childBodyParametersRows, 'color-bg-subtle border-top-0')}>
<td colSpan={4} className="has-nested-table">
<details className="box px-3 ml-1 mb-0">
<details className="box px-3 ml-1 mb-0" open={open}>
<summary role="button" aria-expanded="false" className="mb-2 keyboard-focus">
<span id={`${slug}-${parentName}-${parentType}`}>Properties of {parentName}</span>
<span id={`${slug}-${parentName}-${parentType}`}>
Properties of <code>{parentName}</code>
</span>
</summary>
<table id={`${parentName}-object`} className="mb-4 mt-2 color-bg-subtle">
<table id={`${parentName}-object`} className="mb-4 color-bg-subtle">
<thead className="visually-hidden">
<tr>
<th>{`${t('name')}, ${t('type')}, ${t('description')}`}</th>
</tr>
</thead>
<tbody>
{childParamsGroups.map((childParam) => {
{childParamsGroups.map((childParam, index) => {
return (
<ParameterRow
rowParams={childParam}
slug={slug}
isChild={true}
key={childParam.name}
rowIndex={index}
/>
)
})}

View File

@@ -1,6 +1,7 @@
import cx from 'classnames'
import { useTranslation } from 'components/hooks/useTranslation'
import { KeyboardEventHandler } from 'react'
import { ChildBodyParametersRows } from './ChildBodyParametersRows'
import type { ChildParameter } from './types'
@@ -10,14 +11,38 @@ type Props = {
numPreviews?: number
isChild?: boolean
rowIndex?: number
bodyParamExpandCallback?: KeyboardEventHandler<HTMLButtonElement> | undefined
clickedBodyParameterName?: string | undefined
}
// Webhooks have these same properties in common that we describe separately in its
// own section on the webhooks page:
//
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#webhook-payload-object-common-properties
//
// Since there's more details for these particular properties, we chose not
// show their child properties for each webhook and we also don't grab this
// information from the schema.
//
// We use this list of common properties to make sure we don't try and request
// the child properties for these specific properties.
const NO_CHILD_WEBHOOK_PROPERTIES = [
'action',
'enterprise',
'installation',
'organization',
'repository',
'sender',
]
export function ParameterRow({
rowParams,
slug,
numPreviews = 0,
rowIndex = 0,
isChild = false,
rowIndex = 0,
bodyParamExpandCallback = undefined,
clickedBodyParameterName = undefined,
}: Props) {
const { t } = useTranslation(['parameter_table', 'products'])
@@ -97,8 +122,40 @@ export function ParameterRow({
parentName={rowParams.name}
parentType={rowParams.type}
childParamsGroups={rowParams.childParamsGroups}
open={rowParams.name === clickedBodyParameterName}
/>
)}
{/* These conditions tell us:
1. the param is an object or array AND:
2. the param has no child param groups AND:
3. the param isn't one of the common webhook properties
If all these are true, then that means we haven't yet loaded the
nested parameters so we show a stub <details> element that triggers
an API request to get the nested parameter data.
*/}
{(rowParams.type === 'object' || rowParams.type.includes('array of')) &&
rowParams.childParamsGroups &&
rowParams.childParamsGroups.length === 0 &&
!NO_CHILD_WEBHOOK_PROPERTIES.includes(rowParams.name) && (
<tr className="border-top-0">
<td colSpan={4} className="has-nested-table">
<details
data-nested-param-id={rowParams.name}
className="box px-3 ml-1 mb-0"
onToggle={bodyParamExpandCallback}
>
<summary role="button" aria-expanded="false" className="mb-2 keyboard-focus">
<span id={`${slug}-${rowParams.name}`}>
Properties of <code>{rowParams.name}</code>
</span>
</summary>
</details>
</td>
</tr>
)}
</>
)
}

View File

@@ -1,5 +1,6 @@
import cx from 'classnames'
import { useTranslation } from 'components/hooks/useTranslation'
import { KeyboardEventHandler } from 'react'
import { ParameterRow } from './ParameterRow'
import { BodyParameter, ChildParameter, Parameter } from './types'
@@ -8,20 +9,24 @@ import styles from './ParameterTable.module.scss'
type Props = {
slug: string
numPreviews: number
heading: string
headers: Array<ChildParameter>
parameters: Array<Parameter>
numPreviews?: number
heading?: string
headers?: Array<ChildParameter>
parameters?: Array<Parameter>
bodyParameters: Array<BodyParameter>
bodyParamExpandCallback?: KeyboardEventHandler<HTMLButtonElement> | undefined
clickedBodyParameterName?: string | undefined
}
export function ParameterTable({
slug,
numPreviews,
numPreviews = 0,
heading = '',
headers = [],
parameters,
parameters = [],
bodyParameters,
bodyParamExpandCallback = undefined,
clickedBodyParameterName = '',
}: Props) {
const { t } = useTranslation(['parameter_table', 'products'])
const queryParams = parameters.filter((param) => param.in === 'query')
@@ -41,7 +46,11 @@ export function ParameterTable({
>
<thead>
<tr>
<th id="header" scope="col" className="text-bold pl-0">
<th
id="header"
scope="col"
className={cx(headers.length === 0 && 'visually-hidden', 'text-bold pl-0')}
>
{t('headers')}
</th>
</tr>
@@ -122,7 +131,9 @@ export function ParameterTable({
{bodyParameters.length > 0 && (
<>
<tr className="border-top-0">
<th scope="colgroup" className="text-bold pl-0">
{/* webhooks don't have a 'Parameters' table heading text so
we adjust the size of the body params heading in that case */}
<th scope="colgroup" className={cx(heading ? 'text-bold' : 'h4', 'pl-0')}>
{t('body')}
</th>
</tr>
@@ -131,7 +142,13 @@ export function ParameterTable({
</tr>
{bodyParameters.map((param, index) => (
<ParameterRow rowParams={param} slug={slug} key={`${index}-${param}`} />
<ParameterRow
bodyParamExpandCallback={bodyParamExpandCallback}
clickedBodyParameterName={clickedBodyParameterName}
rowParams={param}
slug={slug}
key={`${index}-${param}`}
/>
))}
</>
)}

View File

@@ -0,0 +1,195 @@
import { ActionList, ActionMenu, Flash } from '@primer/react'
import { useState, KeyboardEvent } from 'react'
import useSWR from 'swr'
import { slug } from 'github-slugger'
import cx from 'classnames'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'components/hooks/useVersion'
import { LinkIconHeading } from 'components/article/LinkIconHeading'
import { useTranslation } from 'components/hooks/useTranslation'
import type { WebhookAction, WebhookData } from './types'
import { ParameterTable } from 'components/parameter-table/ParameterTable'
import styles from './WebhookPayloadExample.module.scss'
type Props = {
webhook: WebhookAction
}
// fetcher passed to useSWR() to get webhook data using the given URL
async function webhookFetcher(url: string) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`${response.status} on ${url}`)
}
return response.json()
}
// We manually created decorated webhooks files for GHES versions older than
// 3.7, returns whether the given version is one of these versions of GHES.
//
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this
// anymore.
function isScrapedGhesVersion(version: ReturnType<typeof useVersion>) {
const scrapedVersions = ['3.6', '3.5', '3.4', '3.3', '3.2']
if (!version.isEnterprise) return false
// getting the number part e.g. '3.6' from a version string like
// 'enterprise-server@3.6'
const versionNumber = version.currentVersion.split('@')[1]
return scrapedVersions.includes(versionNumber)
}
export function Webhook({ webhook }: Props) {
// Get version for requests to switch webhook action type
const version = useVersion()
const { t } = useTranslation('products')
const context = useMainContext()
// Get more user friendly language for the different availability options in
// the webhook schema (we can't change it directly in the schema). Note that
// we specifically don't want to translate these strings with useTranslation()
// like we usually do with strings from data/ui.yml.
const rephraseAvailability = context.data.ui.products.webhooks.rephrase_availability
// The param that was clicked so we can expand its property <details> element
const [clickedBodyParameterName, setClickedBodyParameterName] = useState<undefined | string>('')
// The selected webhook action type the user selects via a dropdown
const [selectedWebhookActionType, setSelectedWebhookActionType] = useState('')
const webhookSlug = slug(webhook.data.category)
const webhookFetchUrl = `/api/webhooks/v1?${new URLSearchParams({
category: webhook.data.category,
version: version.currentVersion,
})}`
// callback for the action type dropdown -- besides setting the action type
// state, we also want to clear the clicked body param so that no properties
// are expanded when we re-render the webhook
function handleActionTypeChange(type: string) {
setClickedBodyParameterName('')
setSelectedWebhookActionType(type)
}
// callback to trigger useSWR() hook after a nested property is clicked
function handleBodyParamExpansion(event: KeyboardEvent<HTMLElement>) {
// need to cast it because 'closest' isn't necessarily available on
// event.target
const target = event.target as HTMLElement
setClickedBodyParameterName(target.closest('details')?.dataset.nestedParamId)
}
// fires when the webhook action type changes or someone clicks on a nested
// body param for the first time. In either case, we now have all the data
// for a webhook (i.e. all the data for each action type and all of their
// nested parameters)
const { data, error } = useSWR<WebhookData, Error>(
clickedBodyParameterName || selectedWebhookActionType ? webhookFetchUrl : null,
webhookFetcher,
{
revalidateOnFocus: false,
}
)
const currentWebhookActionType = selectedWebhookActionType || webhook.data.action
const currentWebhookAction = (data && data[currentWebhookActionType]) || webhook.data
return (
<div>
<h2 id={webhookSlug}>
<LinkIconHeading slug={webhookSlug} />
{currentWebhookAction.category}
</h2>
<div>
<div dangerouslySetInnerHTML={{ __html: currentWebhookAction.summaryHtml }}></div>
<h3>{t('webhooks.availability')}</h3>
<ul>
{currentWebhookAction.availability.map((availability) => {
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this anymore.
if (isScrapedGhesVersion(version)) {
return (
<li
dangerouslySetInnerHTML={{ __html: availability }}
key={`availability-${availability}`}
></li>
)
} else {
return (
<li key={`availability-${availability}`}>
{rephraseAvailability[availability] ?? availability}
</li>
)
}
})}
</ul>
<h3>{t('webhooks.webhook_payload_object')}</h3>
{error && (
<Flash className="mb-5" variant="danger">
<p>{t('webhooks.action_type_switch_error')}</p>
<p>
<code className="f6" style={{ background: 'none' }}>
{error.toString()}
</code>
</p>
</Flash>
)}
{webhook.actionTypes.length > 1 && (
<div className="mb-4">
<h4 className="border-bottom pt-2 pb-2 mb-3">{t('webhooks.action_type')}</h4>
<div className="mb-3">
<ActionMenu>
<ActionMenu.Button className="text-bold">
{currentWebhookActionType}
</ActionMenu.Button>
<ActionMenu.Overlay>
<ActionList>
{webhook.actionTypes.map((type) => {
return (
<ActionList.Item
disabled={type === currentWebhookActionType}
key={`${webhook.name}-${type}`}
onSelect={() => handleActionTypeChange(type)}
>
{type}
</ActionList.Item>
)
})}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</div>
</div>
)}
<div
className="mb-4 f5 color-fg-muted"
dangerouslySetInnerHTML={{ __html: currentWebhookAction.descriptionHtml }}
></div>
<div>
<ParameterTable
slug={slug(`${currentWebhookAction.category}-${selectedWebhookActionType}`)}
bodyParameters={currentWebhookAction.bodyParameters || []}
bodyParamExpandCallback={handleBodyParamExpansion}
clickedBodyParameterName={clickedBodyParameterName}
/>
</div>
</div>
{webhook.data.payloadExample && (
<>
<h3>{t('webhooks.webhook_payload_example')}</h3>
<div
className={cx(styles.payloadExample, 'border-top rounded-1 my-0')}
style={{ maxHeight: '32rem' }}
data-highlight={'json'}
>
<code>{JSON.stringify(webhook.data.payloadExample, null, 2)}</code>
</div>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,16 @@
@import "@primer/css/support/index.scss";
.payloadExample {
overflow: auto;
margin-bottom: 1rem;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
font-size: 90%;
max-height: 32rem;
code {
background-color: transparent;
padding: 8px 8px 16px;
white-space: pre;
}
}

View File

@@ -0,0 +1,34 @@
import { Parameter, StatusCode, CodeSample, BodyParameter } from '../rest/types'
export interface WebhookT {
actions: string[]
webhookActions: WebhookAction[]
}
interface WebhookActionData {
verb: string
title: string
descriptionHtml: string
summaryHtml: string
previews: Array<string>
requestPath: string
serverUrl: string
statusCodes: Array<StatusCode>
parameters: Array<Parameter>
bodyParameters: Array<BodyParameter>
category: string
subcategory: string
codeExamples: Array<CodeSample>
availability: Array<string>
action: string
payloadExample?: Object
}
export interface WebhookAction {
name: string
actionTypes: string[]
data: WebhookActionData
}
export type WebhookData = {
[key: string]: WebhookActionData
}

View File

@@ -1,6 +1,6 @@
---
title: Viewing and managing your active SAML sessions
intro: You can view and revoke your active SAML sessions in your security settings.
intro: You can view and revoke your active SAML sessions in your settings.
redirect_from:
- /articles/viewing-and-managing-your-active-saml-sessions
- /github/authenticating-to-github/viewing-and-managing-your-active-saml-sessions
@@ -9,16 +9,24 @@ versions:
ghec: '*'
topics:
- SSO
type: how_to
shortTitle: Active SAML sessions
---
You can view a list of devices that have logged into your account, and revoke any SAML sessions that you don't recognize.
{% data reusables.user-settings.access_settings %}
{% data reusables.user-settings.security %}
3. Under "Sessions," you can see your active SAML sessions.
![List of active SAML sessions](/assets/images/help/settings/saml-active-sessions.png)
4. To see the session details, click **See more**.
![Button to open SAML session details](/assets/images/help/settings/saml-expand-session-details.png)
5. To revoke a session, click **Revoke SAML**.
![Button to revoke a SAML session](/assets/images/help/settings/saml-revoke-session.png)
{% data reusables.user-settings.sessions %}
1. Under "Web sessions," you can see your active SAML sessions.
![Screenshot of the list of active SAML sessions](/assets/images/help/settings/saml-active-sessions.png)
1. To see the session details, click **See more**.
![Screenshot of the active SAML sessions with the button to open SAML session details emphasized](/assets/images/help/settings/saml-expand-session-details.png)
1. To revoke a session, click **Revoke SAML**.
![Screenshot of the Session details page with the button to revoke a SAML session emphasized](/assets/images/help/settings/saml-revoke-session.png)
{% note %}

View File

@@ -32,6 +32,7 @@ children:
- /githubs-ssh-key-fingerprints
- /sudo-mode
- /preventing-unauthorized-access
- /viewing-and-managing-your-sessions
shortTitle: Account security
---

View File

@@ -0,0 +1,44 @@
---
title: Viewing and managing your sessions
intro: You can view and revoke your active sessions in your settings.
versions:
feature: device-and-settings-management-page
type: how_to
topics:
- SSO
shortTitle: Viewing and managing sessions
---
You can view a list of devices that have logged into your account, and revoke any sessions that you don't recognize.
{% data reusables.user-settings.access_settings %}
{% data reusables.user-settings.sessions %}
1. Under "Web sessions", you can see your active web sessions.
![Screenshot of the list of active sessions](/assets/images/help/settings/saml-active-sessions.png)
{% ifversion fpt or ghec %}
Under "{% data variables.product.prodname_mobile %} sessions", you can see a list of devices that have logged into your account via the {% data variables.product.prodname_mobile %} app.
![Screenshot of the list of active sessions](/assets/images/help/settings/github-mobile-active-sessions.png){% endif %}
1. To see the web session details, click **See more**.
![Screenshot of the Sessions page with the button to open session details emphasized](/assets/images/help/settings/saml-expand-session-details.png)
1. To revoke a web session, click **Revoke session**.
![Screenshot of the Sessions details page with the button to revoke a session emphasized](/assets/images/help/settings/revoke-session.png)
{% ifversion fpt or ghec %}
1. Optionally, to revoke a {% data variables.product.prodname_mobile %} session, go back to the Sessions overview page and click **Revoke** next to the device you want to revoke.
{% note %}
**Note:** Revoking a mobile session signs you out of the {% data variables.product.prodname_mobile %} application on that device and removes it as a second-factor option.
{% endnote %}
![Screenshot of the Sessions page with the button to revoke a mobile session emphasized](/assets/images/help/settings/revoke-mobile-session.png)
{% endif %}

View File

@@ -0,0 +1,7 @@
# Reference: #8482.
# Device and session management settings page
versions:
fpt: '*'
ghec: '*'
ghes: '>=3.8'
ghae: '>= 3.8'

View File

@@ -0,0 +1 @@
1. In the "Access" section of the sidebar, click **{% octicon "broadcast" aria-label="The broadcast icon" %} Sessions**.

View File

@@ -101,6 +101,7 @@ parameter_table:
see_preview_notice: See preview notice
see_preview_notices: See preview notices
type: Type
single_enum_description: Value
products:
graphql:
reference:
@@ -141,6 +142,25 @@ products:
preview_notice_to_change: This API is under preview and subject to change
works_with: Works with
api_reference: REST API reference
enum_description_title: Can be one of
required: Required
headers: Headers
query: Query parameters
path: Path parameters
body: Body parameters
webhooks:
action_type_switch_error: There was an error switching webhook action types.
action_type: Action type
availability: Availability
webhook_payload_object: Webhook payload object
webhook_payload_example: Webhook payload example
rephrase_availability:
repository: Repositories
organization: Organizations
app: GitHub Apps
business: Enterprises
marketplace: GitHub Marketplace
sponsors_listing: Sponsored accounts
footer:
all_rights_reserved: All rights reserved
terms: Terms

View File

@@ -3,9 +3,92 @@ import path from 'path'
import fs from 'fs'
import walk from 'walk-sync'
import { set } from 'lodash-es'
import { allVersions } from '../all-versions.js'
import { readCompressedJsonFileFallback } from '../read-json-file.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const staticDir = path.join(__dirname, 'static')
const schemasPath = path.join(__dirname, 'static/decorated')
// cache for webhook data per version
const webhooksCache = new Map()
// cache for webhook data for when you first visit the webhooks page where we
// show all webhooks for the current version but only 1 action type per webhook
// and also no nested parameters
const initialWebhooksCache = new Map()
// return the webhoook data as described for `initialWebhooksCache` for the given
// version
export async function getInitialPageWebhooks(version) {
if (initialWebhooksCache.has(version)) {
return initialWebhooksCache.get(version)
}
const allWebhooks = await getWebhooks(version)
const initialWebhooks = []
// The webhooks page shows all webhooks but for each webhook only a single
// webhook action type at a time. We pick the first webhook type from each
// webhook's set of action types to show.
for (const [key, webhook] of Object.entries(allWebhooks)) {
const actionTypes = Object.keys(webhook)
const defaultAction = actionTypes ? actionTypes[0] : null
const initialWebhook = {
name: key,
actionTypes,
data: webhook[defaultAction],
}
// remove all nested params for the initial webhooks page, we'll load
// them by request
if (initialWebhook.data.bodyParameters) {
initialWebhook.data.bodyParameters.forEach((bodyParam) => {
if (bodyParam.childParamsGroups) {
bodyParam.childParamsGroups = []
}
})
}
initialWebhooks.push({ ...initialWebhook })
}
initialWebhooksCache.set(version, initialWebhooks)
return initialWebhooks
}
// returns the webhook data for the given version and webhook category (e.g.
// `check_run`) -- this includes all the data per webhook action type and all
// nested parameters
export async function getWebhook(version, webhookCategory) {
const webhooks = await getWebhooks(version)
return webhooks[webhookCategory]
}
// returns all the webhook data for the given version
export async function getWebhooks(version) {
const openApiVersion = getOpenApiVersion(version)
if (!webhooksCache.has(openApiVersion)) {
const filename = `${openApiVersion}.json`
// The `readCompressedJsonFileFallback()` function
// will check for both a .br and .json extension.
webhooksCache.set(
openApiVersion,
readCompressedJsonFileFallback(path.join(schemasPath, filename))
)
}
return webhooksCache.get(openApiVersion)
}
function getOpenApiVersion(version) {
if (!(version in allVersions)) {
throw new Error(`Unrecognized version '${version}'. Not found in ${Object.keys(allVersions)}`)
}
return allVersions[version].openApiVersionName
}
// TODO: docs-eng#1937: delete this function
export default function getWebhookPayloads() {
// Compile contents of individual .payload.json files into a single
// object, with versions as top-level keys.
@@ -40,6 +123,7 @@ export default function getWebhookPayloads() {
return payloads
}
// TODO: docs-eng#1937: delete this function
function formatAsJsonCodeBlock(payloadObj) {
// Note the use of `data-highlight="json"`. This is important because
// done like this, it tells the rehype processor to NOT bother syntax

View File

@@ -3,10 +3,12 @@ import { createProxyMiddleware } from 'http-proxy-middleware'
import events from './events.js'
import search from './search.js'
import webhooks from './webhooks.js'
const router = express.Router()
router.use('/events', events)
router.use('/webhooks', webhooks)
// The purpose of this is for convenience to everyone who runs this code
// base locally but don't have an Elasticsearch server locally.

View File

@@ -0,0 +1,42 @@
import express from 'express'
import { getWebhook } from '../../lib/webhooks/index.js'
import { allVersions } from '../../lib/all-versions.js'
import { defaultCacheControl } from '../cache-control.js'
const router = express.Router()
// Returns a webhook for the given category and version
//
// Example request:
//
// /api/webhooks/v1?category=check_run&version=free-pro-team%40latest
router.get('/v1', async function webhooks(req, res, next) {
if (!req.query.category) {
return res.status(400).json({ error: "Missing 'category' in query string" })
}
if (!req.query.version) {
return res.status(400).json({ error: "Missing 'version' in query string" })
}
const webhookVersion = Object.values(allVersions).find(
(version) => version.version === req.query.version
)?.version
const notFoundError = 'No webhook found for given category and version'
if (!webhookVersion) {
return res.status(404).json({ error: notFoundError })
}
const webhook = await getWebhook(webhookVersion, req.query.category)
if (webhook) {
if (process.env.NODE_ENV !== 'development') {
defaultCacheControl(res)
}
return res.status(200).send(webhook)
} else {
res.status(404).json({ error: notFoundError })
}
})
export default router

View File

@@ -5,6 +5,7 @@ import { allVersions } from '../../lib/all-versions.js'
let webhookPayloads = null
// TODO: docs-eng#1937: webhooks-delete-1937: delete this file
export default function webhooksContext(req, res, next) {
const currentVersionObj = allVersions[req.context.currentVersion]
// ignore requests to non-webhook reference paths

View File

@@ -38,6 +38,7 @@ import triggerError from './trigger-error.js'
import ghesReleaseNotes from './contextualizers/ghes-release-notes.js'
import ghaeReleaseNotes from './contextualizers/ghae-release-notes.js'
import whatsNewChangelog from './contextualizers/whats-new-changelog.js'
// TODO: docs-eng#1937: delete this line
import webhooks from './contextualizers/webhooks.js'
import layout from './contextualizers/layout.js'
import currentProductTree from './contextualizers/current-product-tree.js'
@@ -261,6 +262,7 @@ export default function (app) {
// *** Preparation for render-page: contextualizers ***
app.use(asyncMiddleware(instrument(ghesReleaseNotes, './contextualizers/ghes-release-notes')))
app.use(asyncMiddleware(instrument(ghaeReleaseNotes, './contextualizers/ghae-release-notes')))
// TODO: docs-eng#1937: delete this line
app.use(instrument(webhooks, './contextualizers/webhooks'))
app.use(asyncMiddleware(instrument(whatsNewChangelog, './contextualizers/whats-new-changelog')))
app.use(instrument(layout, './contextualizers/layout'))

View File

@@ -0,0 +1,68 @@
import { GetServerSideProps } from 'next'
import { getInitialPageWebhooks } from 'lib/webhooks'
import { getMainContext, MainContext, MainContextT } from 'components/context/MainContext'
import {
getAutomatedPageContextFromRequest,
AutomatedPageContext,
AutomatedPageContextT,
} from 'components/context/AutomatedPageContext'
import { WebhookAction } from 'components/webhooks/types'
import { Webhook } from 'components/webhooks/Webhook'
import { getAutomatedPageMiniTocItems } from 'lib/get-mini-toc-items'
import { AutomatedPage } from 'components/article/AutomatedPage'
type Props = {
mainContext: MainContextT
automatedPageContext: AutomatedPageContextT
webhooks: WebhookAction[]
}
export default function WebhooksEventsAndPayloads({
mainContext,
automatedPageContext,
webhooks,
}: Props) {
const content = webhooks.map((webhook: WebhookAction, index) => {
return (
<div key={`${webhook.data.requestPath}-${index}`}>
<Webhook webhook={webhook} />
</div>
)
})
return (
<MainContext.Provider value={mainContext}>
<AutomatedPageContext.Provider value={automatedPageContext}>
<AutomatedPage>{content}</AutomatedPage>
</AutomatedPageContext.Provider>
</MainContext.Provider>
)
}
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const req = context.req as object
const res = context.res as object
const currentVersion = context.query.versionId as string
const mainContext = await getMainContext(req, res)
const { miniTocItems } = getAutomatedPageContextFromRequest(req)
// Get data for initial webhooks page (i.e. only 1 action type per webhook and
// no nested parameters)
const webhooks = (await getInitialPageWebhooks(currentVersion)) as WebhookAction[]
// Build the minitocs for the webhooks page which is based on the webhook
// categories in addition to the Markdown in the webhook-events-and-payloads.md
// content file
const webhooksMiniTocs = await getAutomatedPageMiniTocItems(
webhooks.map((webhook) => webhook.data.category),
context
)
webhooksMiniTocs && miniTocItems.push(...webhooksMiniTocs)
return {
props: {
webhooks,
mainContext,
automatedPageContext: getAutomatedPageContextFromRequest(req),
},
}
}

View File

@@ -1,9 +1,10 @@
import { difference } from 'lodash-es'
import { getJSON } from '../helpers/e2etest.js'
import { get, getJSON } from '../helpers/e2etest.js'
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
import { latest } from '../../lib/enterprise-server-releases.js'
import { allVersions } from '../../lib/all-versions.js'
import getWebhookPayloads from '../../lib/webhooks'
import { jest } from '@jest/globals'
import { describe, expect, jest } from '@jest/globals'
const allVersionValues = Object.values(allVersions)
@@ -22,6 +23,66 @@ const ghaePayloadVersion = allVersionValues.find(
(version) => version.plan === 'github-ae'
).miscVersionName
describe('webhooks middleware', () => {
test('basic get webhook', async () => {
const sp = new URLSearchParams()
// Based on live data which isn't ideal but it should rarely change at least.
// Just check that we find the webhook and that the result has the `category`
// field which all webhook types should have.
sp.set('category', 'branch_protection_rule')
sp.set('version', 'free-pro-team@latest')
const res = await get('/api/webhooks/v1?' + sp)
expect(res.statusCode).toBe(200)
const results = JSON.parse(res.text)
const actionTypes = Object.keys(results)
expect(actionTypes.length).toBeGreaterThan(2)
expect(Object.keys(results[actionTypes[0]]).includes('category')).toBeTruthy()
// Check that it can be cached at the CDN
expect(res.headers['set-cookie']).toBeUndefined()
expect(res.headers['cache-control']).toContain('public')
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-control']).toContain('public')
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.DEFAULT)
})
test('get non-fpt version webhook', async () => {
const sp = new URLSearchParams()
sp.set('category', 'branch_protection_rule')
sp.set('version', 'enterprise-cloud@latest')
const res = await get('/api/webhooks/v1?' + sp)
expect(res.statusCode).toBe(200)
const results = JSON.parse(res.text)
const actionTypes = Object.keys(results)
expect(actionTypes.length).toBeGreaterThan(2)
expect(Object.keys(results[actionTypes[0]]).includes('category')).toBeTruthy()
expect(res.statusCode).toBe(200)
})
test('unknown webhook category', async () => {
const sp = new URLSearchParams()
sp.set('category', 'no-such-category')
sp.set('version', 'free-pro-team@latest')
const res = await get('/api/webhooks/v1?' + sp)
expect(res.statusCode).toBe(404)
expect(JSON.parse(res.text).error).toBeTruthy()
})
test('unknown version', async () => {
const sp = new URLSearchParams()
sp.set('category', 'branch_protection_rule')
sp.set('version', 'no-such-version')
const res = await get('/api/webhooks/v1?' + sp)
expect(res.statusCode).toBe(404)
expect(JSON.parse(res.text).error).toBeTruthy()
})
})
// TODO: docs-eng#1937: delete this test suite
describe('webhook payloads', () => {
jest.setTimeout(3 * 60 * 1000)

View File

@@ -1,11 +1,65 @@
import { jest } from '@jest/globals'
import { getDOM } from '../helpers/e2etest.js'
import { allVersions } from '../../lib/all-versions.js'
import { getWebhooks } from '../../lib/webhooks/index.js'
describe('webhooks events and payloads', () => {
jest.setTimeout(300 * 1000)
describe('rendering', () => {
test('loads webhook schema data for all versions', async () => {
for (const version in allVersions) {
const webhooks = await getWebhooks(version)
const webhookNames = Object.keys(webhooks)
const $ = await getDOM(
`/en/${version}/developers/webhooks-and-events/webhooks/webhook-events-and-payloads`
)
const domH2Ids = $('h2')
.map((i, h2) => $(h2).attr('id'))
.get()
webhookNames.forEach((webhookName) => {
expect(domH2Ids.includes(webhookName))
})
}
})
test('Non-GHES versions do not load GHES only webhook', async () => {
// available since 3.4, only in GHES (technically also GHAE which is based
// off of GHES)
const ghesOnlyWebhook = 'cache_sync'
for (const version in allVersions) {
if (!version.includes('enterprise-server') && !version.includes('github-ae')) {
const $ = await getDOM(
`/en/${version}/developers/webhooks-and-events/webhooks/webhook-events-and-payloads`
)
const domH2Ids = $('h2')
.map((i, h2) => $(h2).attr('id'))
.get()
expect(domH2Ids.length).toBeGreaterThan(0)
expect(domH2Ids.includes(ghesOnlyWebhook)).toBe(false)
}
}
})
test('Webhooks events and payloads page has DOM markers needed for extracting search content', async () => {
const $ = await getDOM(
'/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads'
)
const rootSelector = '[data-search=article-body]'
const $root = $(rootSelector)
expect($root.length).toBe(1)
// on the webhooks page the lead is separate from the article body (unlike
// the REST pages for example)
const leadSelector = '[data-search=lead] p'
const $lead = $(leadSelector)
expect($lead.length).toBe(1)
})
// All webhook types don't yet have examples in the schema.
describe.skip('rendering', () => {
test('every webhook event has at least one payload example', async () => {
const versions = Object.values(allVersions).map((value) => value.version)