@@ -35,7 +35,8 @@ By default, the deployments page shows currently active deployments from select
|
||||
|
||||
1. In the right-hand sidebar of the home page of your repository, click **Deployments**.
|
||||
1. Once you are on the "Deployments" page, you can view the following information about your deployment history.
|
||||
- **To view recent deployments for a specific environment**, in the "Environments" section of the left sidebar, click an environment. {% ifversion deployment-dashboard-filter %}To pin an environment to the top of the deployment history list, click {% octicon "pin" aria-label="Pin environment" %} to the right of the environment.{% endif %}
|
||||
- **To view recent deployments for a specific environment**, in the "Environments" section of the left sidebar, click an environment.{% ifversion deployment-dashboard-filter %}
|
||||
- **To pin an environment to the top of the deployment history list**, repository administrators can click {% octicon "pin" aria-label="Pin environment" %} to the right of the environment. You can pin up to ten environments.{% endif %}
|
||||
- **To view the commit that triggered a deployment**, in the deployment history list, click the commit message for the deployment you want to view.
|
||||
>[!NOTE]Deployments from commits that originate from a fork outside of the repository will not show links to the source pull request and branch related to each deployment. For more information about forks, see "[AUTOTITLE](/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks)."
|
||||
- **To view the URL for a deployment**, to the right of the commit message in the deployment history list, click {% octicon "link-external" aria-label="Navigate to deployment URL" %}.
|
||||
|
||||
@@ -27,6 +27,7 @@ import wrapProceduralImages from './wrap-procedural-images.js'
|
||||
import parseInfoString from './parse-info-string.js'
|
||||
import annotate from './annotate.js'
|
||||
import alerts from './alerts.js'
|
||||
import replaceDomain from './replace-domain.js'
|
||||
|
||||
export function createProcessor(context) {
|
||||
return (
|
||||
@@ -44,6 +45,7 @@ export function createProcessor(context) {
|
||||
.use(headingLinks)
|
||||
.use(codeHeader)
|
||||
.use(annotate)
|
||||
.use(replaceDomain)
|
||||
.use(highlight, {
|
||||
languages: { ...common, graphql, dockerfile, http, groovy, erb, powershell },
|
||||
subset: false,
|
||||
|
||||
43
src/content-render/unified/replace-domain.js
Normal file
43
src/content-render/unified/replace-domain.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* This makes it so that the `github.com` or `HOSTNAME` in a code snippet
|
||||
* becomes replacable.
|
||||
*/
|
||||
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
// Don't use `g` on these regexes
|
||||
const VALID_REPLACEMENTS = [[/\bHOSTNAME\b/, 'HOSTNAME']]
|
||||
|
||||
const CODE_FENCE_KEYWORD = 'replacedomain'
|
||||
|
||||
const matcher = (node) => {
|
||||
return (
|
||||
node.type === 'element' &&
|
||||
node.tagName === 'pre' &&
|
||||
node.children[0]?.data?.meta[CODE_FENCE_KEYWORD]
|
||||
)
|
||||
}
|
||||
|
||||
export default function alerts() {
|
||||
return (tree) => {
|
||||
visit(tree, matcher, (node) => {
|
||||
const code = node.children[0].children[0].value
|
||||
let found = false
|
||||
for (const [regex, replacement] of VALID_REPLACEMENTS) {
|
||||
if (regex.test(code)) {
|
||||
const codeTag = node.children[0]
|
||||
const replacements = codeTag.properties['data-replacedomain'] || []
|
||||
if (!replacements.includes(replacement)) {
|
||||
replacements.push(replacement)
|
||||
codeTag.properties['data-replacedomain'] = replacements
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && process.env.NODE_ENV === 'development') {
|
||||
console.warn("The code snippet doesn't contain a valid replacement", { code })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,5 @@ children:
|
||||
- /permissions
|
||||
- /code-annotations
|
||||
- /alerts
|
||||
- /replace-domain
|
||||
---
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Replace domain
|
||||
intro: This demonstrates code snippets that have host names that can be replaced.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghes: '*'
|
||||
ghec: '*'
|
||||
type: how_to
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
If you have an article with code snippets that have the `replacedomain`
|
||||
annotation on its code fence, that means the page *might* take the current
|
||||
user's cookie (indicating their personal hostname) and replace that within
|
||||
the code snippet.
|
||||
|
||||
## Shell code snippet (on)
|
||||
|
||||
```sh replacedomain
|
||||
curl https://HOSTNAME/api/v1
|
||||
```
|
||||
|
||||
## Shell code snippet (off)
|
||||
|
||||
```sh
|
||||
curl https://HOSTNAME/api/v2
|
||||
```
|
||||
|
||||
## JavaScript code snippet (on)
|
||||
|
||||
```js replacedomain
|
||||
await fetch("https://HOSTNAME/api/v1")
|
||||
```
|
||||
|
||||
## JavaScript code snippet (off)
|
||||
|
||||
```js
|
||||
await fetch("https://HOSTNAME/api/v2")
|
||||
```
|
||||
@@ -593,3 +593,35 @@ test.describe('translations', () => {
|
||||
await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('view pages with custom domain cookie', () => {
|
||||
test('view article page', async ({ page }) => {
|
||||
await page.goto(
|
||||
'/enterprise-server@latest/get-started/markdown/replace-domain?ghdomain=example.ghe.com',
|
||||
)
|
||||
|
||||
const content = page.locator('pre')
|
||||
await expect(content.nth(0)).toHaveText(/curl https:\/\/example.ghe.com\/api\/v1/)
|
||||
await expect(content.nth(1)).toHaveText(/curl https:\/\/HOSTNAME\/api\/v2/)
|
||||
await expect(content.nth(2)).toHaveText('await fetch("https://example.ghe.com/api/v1")')
|
||||
await expect(content.nth(3)).toHaveText('await fetch("https://HOSTNAME/api/v2")')
|
||||
|
||||
// Now switch to enterprise-cloud, where replacedomain should not be used
|
||||
await page.getByLabel('Select GitHub product version').click()
|
||||
await page.getByLabel('Enterprise Cloud', { exact: true }).click()
|
||||
|
||||
await expect(content.nth(0)).toHaveText(/curl https:\/\/HOSTNAME\/api\/v1/)
|
||||
await expect(content.nth(1)).toHaveText(/curl https:\/\/HOSTNAME\/api\/v2/)
|
||||
await expect(content.nth(2)).toHaveText('await fetch("https://HOSTNAME/api/v1")')
|
||||
await expect(content.nth(3)).toHaveText('await fetch("https://HOSTNAME/api/v2")')
|
||||
|
||||
// Again switch back to enterprise server again
|
||||
await page.getByLabel('Select GitHub product version').click()
|
||||
await page.getByLabel('Enterprise Server 3.').first().click()
|
||||
|
||||
await expect(content.nth(0)).toHaveText(/curl https:\/\/example.ghe.com\/api\/v1/)
|
||||
await expect(content.nth(1)).toHaveText(/curl https:\/\/HOSTNAME\/api\/v2/)
|
||||
await expect(content.nth(2)).toHaveText('await fetch("https://example.ghe.com/api/v1")')
|
||||
await expect(content.nth(3)).toHaveText('await fetch("https://HOSTNAME/api/v2")')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Breadcrumbs } from 'src/frame/components/page-header/Breadcrumbs'
|
||||
import { Link } from 'src/frame/components/Link'
|
||||
import { useTranslation } from 'src/languages/components/useTranslation'
|
||||
import { LinkPreviewPopover } from 'src/links/components/LinkPreviewPopover'
|
||||
import { ReplaceDomain } from 'src/links/components/replace-domain'
|
||||
|
||||
const ClientSideRefresh = dynamic(() => import('src/frame/components/ClientSideRefresh'), {
|
||||
ssr: false,
|
||||
@@ -103,6 +104,7 @@ export const ArticlePage = () => {
|
||||
<LinkPreviewPopover />
|
||||
{isDev && <ClientSideRefresh />}
|
||||
{router.pathname.includes('/rest/') && <RestRedirect />}
|
||||
<ReplaceDomain />
|
||||
{currentLayout === 'inline' ? (
|
||||
<>
|
||||
<ArticleInlineLayout
|
||||
|
||||
89
src/links/components/replace-domain.ts
Normal file
89
src/links/components/replace-domain.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useVersion } from 'src/versions/components/useVersion'
|
||||
import Cookies from 'src/frame/components/lib/cookies'
|
||||
|
||||
const COOKIE_KEY = 'github_domains'
|
||||
|
||||
// We only want to activate the replace-domain feature for these versions.
|
||||
// This means that if you're on a version we don't want it activated on,
|
||||
// even though you have a your-domain cookie, it *won't* replace the
|
||||
// word HOSTNAME.
|
||||
const REPLACEDOMAIN_VERSION_PREFIXES = ['enterprise-server@']
|
||||
|
||||
// This list needs to match what's in `unified/replace-domain.js`
|
||||
const regexes = {
|
||||
HOSTNAME: /\bHOSTNAME\b/g,
|
||||
} as const
|
||||
|
||||
function replaceDomains(domain: string | null) {
|
||||
document.querySelectorAll<HTMLElement>('pre code[data-replacedomain]').forEach((codeBlock) => {
|
||||
const replaceDomain = codeBlock.dataset.replacedomain
|
||||
if (!replaceDomain) return
|
||||
const replaceDomains = replaceDomain.split(/\s/)
|
||||
const spans = codeBlock.querySelectorAll<HTMLSpanElement>('span[class*="-string"]')
|
||||
if (spans.length) {
|
||||
spans.forEach((span) => {
|
||||
replaceInTextContent(span, replaceDomains, domain)
|
||||
})
|
||||
} else {
|
||||
replaceInTextContent(codeBlock, replaceDomains, domain)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function replaceInTextContent(
|
||||
codeBlock: HTMLElement,
|
||||
replaceDomains: string[],
|
||||
domain: string | null,
|
||||
) {
|
||||
if (!codeBlock.textContent) return
|
||||
for (const replaceDomain of replaceDomains) {
|
||||
if (replaceDomain in regexes) {
|
||||
// If the domain is falsy, it means we're reverting the replacement.
|
||||
// This happens when you used to be on a version where we want to
|
||||
// activate this functionality. Then, when you switch to a version
|
||||
// where you don't want it, we need to revert the replacement to
|
||||
// to what it was before we did the first replacement.
|
||||
if (domain) {
|
||||
const match = codeBlock.textContent.match(regexes[replaceDomain as keyof typeof regexes])
|
||||
for (const matched of match || []) {
|
||||
codeBlock.dataset.replacedomainOriginal = matched
|
||||
codeBlock.dataset.replacedomainReplace = domain
|
||||
}
|
||||
codeBlock.textContent = codeBlock.textContent.replace(
|
||||
regexes[replaceDomain as keyof typeof regexes],
|
||||
domain,
|
||||
)
|
||||
} else {
|
||||
if (codeBlock.dataset.replacedomainOriginal && codeBlock.dataset.replacedomainReplace) {
|
||||
// Reverse it
|
||||
codeBlock.textContent = codeBlock.textContent.replace(
|
||||
codeBlock.dataset.replacedomainReplace,
|
||||
codeBlock.dataset.replacedomainOriginal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ReplaceDomain() {
|
||||
const { asPath } = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
|
||||
const bother = REPLACEDOMAIN_VERSION_PREFIXES.some((prefix) => currentVersion.startsWith(prefix))
|
||||
|
||||
useEffect(() => {
|
||||
const cookieValue = Cookies.get(COOKIE_KEY)
|
||||
if (cookieValue) {
|
||||
if (bother) {
|
||||
replaceDomains(cookieValue.split(',')[0])
|
||||
} else {
|
||||
replaceDomains(null)
|
||||
}
|
||||
}
|
||||
}, [asPath, bother])
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user