From 7ebc8876f43983e74dd9d4c9caf712180f59b850 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Tue, 10 Jun 2025 08:55:25 -0700 Subject: [PATCH 1/9] Remove domain edit feature code (#55991) --- .github/workflows/test.yml | 1 - data/ui.yml | 12 +- src/content-render/unified/processor.js | 2 - src/content-render/unified/replace-domain.js | 37 --- .../content/get-started/markdown/index.md | 1 - .../get-started/markdown/replace-domain.md | 49 --- src/fixtures/fixtures/data/ui.yml | 12 +- .../tests/playwright-rendering.spec.ts | 32 -- src/frame/components/DefaultLayout.tsx | 183 ++++++----- src/frame/components/article/ArticlePage.tsx | 2 - src/frame/components/context/MainContext.tsx | 1 - .../page-header/HeaderSearchAndWidgets.tsx | 12 - .../page-header/OldHeaderSearchAndWidgets.tsx | 12 - src/frame/middleware/index.ts | 2 - src/frame/stylesheets/index.scss | 1 - src/links/components/DomainNameEdit.tsx | 305 ------------------ src/links/components/pen-icon.tsx | 20 -- src/links/components/replace-domain.ts | 131 -------- .../components/useEditableDomainContext.tsx | 38 --- src/links/stylesheets/domain-edit.scss | 38 --- .../handle-invalid-query-strings.ts | 2 +- src/tracking/README.md | 20 -- .../middleware/handle-query-strings.ts | 86 ----- src/tracking/middleware/index.ts | 9 - src/tracking/tests/handle-query-string.ts | 133 -------- 25 files changed, 93 insertions(+), 1048 deletions(-) delete mode 100644 src/content-render/unified/replace-domain.js delete mode 100644 src/fixtures/fixtures/content/get-started/markdown/replace-domain.md delete mode 100644 src/links/components/DomainNameEdit.tsx delete mode 100644 src/links/components/pen-icon.tsx delete mode 100644 src/links/components/replace-domain.ts delete mode 100644 src/links/components/useEditableDomainContext.tsx delete mode 100644 src/links/stylesheets/domain-edit.scss delete mode 100644 src/tracking/README.md delete mode 100644 src/tracking/middleware/handle-query-strings.ts delete mode 100644 src/tracking/middleware/index.ts delete mode 100644 src/tracking/tests/handle-query-string.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 548f7fd56939..716bfff8a4a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,6 @@ jobs: - shielding # - tests # - tools - - tracking - versions - webhooks - workflows diff --git a/data/ui.yml b/data/ui.yml index e4e48a60ca46..68adbe6032f3 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -320,17 +320,7 @@ alerts: WARNING: Warning TIP: Tip CAUTION: Caution -domain_edit: - name: Domain name - edit: Edit - edit_your: Edit your domain name - experimental: Experimental - your_name: Your domain name - cancel: Cancel - save: Save - snippet_about: Updating will include the new domain name in all code snippets across GitHub Docs. - learn_more: Learn more - submission_failed: Submission failed. Please try again in a minute. + cookbook_landing: spotlight: Spotlight explore_articles: Explore {{ number }} prompt articles diff --git a/src/content-render/unified/processor.js b/src/content-render/unified/processor.js index 0f0c565270d1..62674a283c7a 100644 --- a/src/content-render/unified/processor.js +++ b/src/content-render/unified/processor.js @@ -27,7 +27,6 @@ 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' import removeHtmlComments from 'remark-remove-comments' import remarkStringify from 'remark-stringify' @@ -49,7 +48,6 @@ export function createProcessor(context) { .use(headingLinks) .use(codeHeader) .use(annotate) - .use(replaceDomain) .use(highlight, { languages: { ...common, graphql, dockerfile, http, groovy, erb, powershell }, subset: false, diff --git a/src/content-render/unified/replace-domain.js b/src/content-render/unified/replace-domain.js deleted file mode 100644 index 21f224be25ac..000000000000 --- a/src/content-render/unified/replace-domain.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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 - 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 - } - } - } - }) - } -} diff --git a/src/fixtures/fixtures/content/get-started/markdown/index.md b/src/fixtures/fixtures/content/get-started/markdown/index.md index 6fe218b5e7c9..6e88f4d2485f 100644 --- a/src/fixtures/fixtures/content/get-started/markdown/index.md +++ b/src/fixtures/fixtures/content/get-started/markdown/index.md @@ -10,6 +10,5 @@ children: - /permissions - /code-annotations - /alerts - - /replace-domain - /html-comments --- diff --git a/src/fixtures/fixtures/content/get-started/markdown/replace-domain.md b/src/fixtures/fixtures/content/get-started/markdown/replace-domain.md deleted file mode 100644 index 8bba2655fb21..000000000000 --- a/src/fixtures/fixtures/content/get-started/markdown/replace-domain.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -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") -``` - -## Not always there - -In this next code snippet, the `HOSTNAME` only appears if the current -version is `ghes`. That should be fine. - -```text replacedomain copy -ssh handle@{% ifversion ghes %}HOSTNAME{% else %}github.com{% endif %} -``` diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index e4e48a60ca46..68adbe6032f3 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -320,17 +320,7 @@ alerts: WARNING: Warning TIP: Tip CAUTION: Caution -domain_edit: - name: Domain name - edit: Edit - edit_your: Edit your domain name - experimental: Experimental - your_name: Your domain name - cancel: Cancel - save: Save - snippet_about: Updating will include the new domain name in all code snippets across GitHub Docs. - learn_more: Learn more - submission_failed: Submission failed. Please try again in a minute. + cookbook_landing: spotlight: Spotlight explore_articles: Explore {{ number }} prompt articles diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts index 3c1c30957a17..660869fd07bd 100644 --- a/src/fixtures/tests/playwright-rendering.spec.ts +++ b/src/fixtures/tests/playwright-rendering.spec.ts @@ -844,35 +844,3 @@ 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")') - }) -}) diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index 4142e923de62..69f8db09638e 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -13,7 +13,6 @@ import { useTranslation } from '@/languages/components/useTranslation' import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' import { useLanguages } from '@/languages/components/LanguagesContext' import { ClientSideLanguageRedirect } from './ClientSideLanguageRedirect' -import { DomainNameEditProvider } from '@/links/components/useEditableDomainContext' import { SearchOverlayContextProvider } from '@/search/components/context/SearchOverlayContext' const MINIMAL_RENDER = Boolean(JSON.parse(process.env.MINIMAL_RENDER || 'false')) @@ -76,99 +75,97 @@ export const DefaultLayout = (props: Props) => { } return ( - - - - {error === '404' ? ( - {t('oops')} - ) : (!isHomepageVersion && page.fullTitle) || - (currentPathWithoutLanguage.includes('enterprise-server') && page.fullTitle) ? ( - {page.fullTitle} - ) : null} - - {/* For Google and Bots */} - - {page.hidden && } - {Object.values(languages) - .filter((lang) => lang.code !== router.locale) - .map((variant) => { - return ( - - ) - })} - - {/* For local site search indexing */} - {page.topics.length > 0 && } - - {/* For analytics events */} - {router.locale && } - {currentVersion && } - {currentProduct && } - {relativePath && ( - - )} - {page.type && } - {page.documentType && } - {status && } - - {/* OpenGraph data */} - {page.fullTitle && ( - <> - - - - - - - )} - {/* Twitter Meta Tags */} - - - - - {page.introPlainText && } - - - - Skip to main content - -
- -
- {isHomepageVersion ? null : } - {/* Need to set an explicit height for sticky elements since we also - set overflow to auto */} -
-
- - - - {props.children} -
-
- - - + + {error === '404' ? ( + {t('oops')} + ) : (!isHomepageVersion && page.fullTitle) || + (currentPathWithoutLanguage.includes('enterprise-server') && page.fullTitle) ? ( + {page.fullTitle} + ) : null} + + {/* For Google and Bots */} + + {page.hidden && } + {Object.values(languages) + .filter((lang) => lang.code !== router.locale) + .map((variant) => { + return ( + -
-
+ ) + })} + + {/* For local site search indexing */} + {page.topics.length > 0 && } + + {/* For analytics events */} + {router.locale && } + {currentVersion && } + {currentProduct && } + {relativePath && ( + + )} + {page.type && } + {page.documentType && } + {status && } + + {/* OpenGraph data */} + {page.fullTitle && ( + <> + + + + + + + )} + {/* Twitter Meta Tags */} + + + + + {page.introPlainText && } + + + + Skip to main content + +
+ +
+ {isHomepageVersion ? null : } + {/* Need to set an explicit height for sticky elements since we also + set overflow to auto */} +
+
+ + + + {props.children} +
+
+ + + +
- - +
+ ) } diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx index 5532845fa7f2..882edee0003d 100644 --- a/src/frame/components/article/ArticlePage.tsx +++ b/src/frame/components/article/ArticlePage.tsx @@ -21,7 +21,6 @@ import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' import { Link } from '@/frame/components/Link' import { useTranslation } from '@/languages/components/useTranslation' import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover' -import { ReplaceDomain } from '@/links/components/replace-domain' const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), { ssr: false, @@ -104,7 +103,6 @@ export const ArticlePage = () => { {isDev && } {router.pathname.includes('/rest/') && } - {currentLayout === 'inline' ? ( <>
@@ -105,14 +101,6 @@ export function HeaderSearchAndWidgets({ width, isSearchOpen, SearchButton }: Pr <> - {showDomainNameEdit && ( - <> - - - - - - )} )} {signupCTAVisible && ( diff --git a/src/frame/components/page-header/OldHeaderSearchAndWidgets.tsx b/src/frame/components/page-header/OldHeaderSearchAndWidgets.tsx index 3f8a2c049593..7e64bed8fe7d 100644 --- a/src/frame/components/page-header/OldHeaderSearchAndWidgets.tsx +++ b/src/frame/components/page-header/OldHeaderSearchAndWidgets.tsx @@ -1,11 +1,9 @@ -import { Suspense } from 'react' import cx from 'classnames' import { SearchIcon, XIcon, KebabHorizontalIcon, LinkExternalIcon } from '@primer/octicons-react' import { IconButton, ActionMenu, ActionList } from '@primer/react' import { LanguagePicker } from '@/languages/components/LanguagePicker' import { useTranslation } from '@/languages/components/useTranslation' -import DomainNameEdit from '@/links/components/DomainNameEdit' import { OldSearchInput } from '@/search/components/input/OldSearchInput' import { VersionPicker } from '@/versions/components/VersionPicker' import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion' @@ -29,8 +27,6 @@ export function OldHeaderSearchAndWidgets({ isSearchOpen, setIsSearchOpen, width hasAccount === false && // don't show if `null` (currentVersion === DEFAULT_VERSION || currentVersion === 'enterprise-cloud@latest') - const showDomainNameEdit = currentVersion.startsWith('enterprise-server@') - return (
{/* */} @@ -151,14 +147,6 @@ export function OldHeaderSearchAndWidgets({ isSearchOpen, setIsSearchOpen, width <> - {showDomainNameEdit && ( - <> - - - - - - )} )} {signupCTAVisible && ( diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index e59cce6aaac1..571d52930757 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -62,7 +62,6 @@ import mockVaPortal from './mock-va-portal' import dynamicAssets from '@/assets/middleware/dynamic-assets' import generalSearchMiddleware from '@/search/middleware/general-search-middleware' import shielding from '@/shielding/middleware' -import tracking from '@/tracking/middleware' import { MAX_REQUEST_TIMEOUT } from '@/frame/lib/constants.js' const { NODE_ENV } = process.env @@ -200,7 +199,6 @@ export default function (app: Express) { } // ** Possible early exits after cookies ** - app.use(tracking) // *** Headers *** app.set('etag', false) // We will manage our own ETags if desired diff --git a/src/frame/stylesheets/index.scss b/src/frame/stylesheets/index.scss index 7adc4e7f165e..d109279e492e 100644 --- a/src/frame/stylesheets/index.scss +++ b/src/frame/stylesheets/index.scss @@ -14,4 +14,3 @@ @import "src/content-render/stylesheets/index.scss"; @import "src/links/stylesheets/hover-card.scss"; -@import "src/links/stylesheets/domain-edit.scss"; diff --git a/src/links/components/DomainNameEdit.tsx b/src/links/components/DomainNameEdit.tsx deleted file mode 100644 index a724683e6921..000000000000 --- a/src/links/components/DomainNameEdit.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import { useEffect, useState, useRef } from 'react' -import { useRouter } from 'next/router' -import { BeakerIcon } from '@primer/octicons-react' - -import { useTranslation } from '@/languages/components/useTranslation' -import { Box, Flash, FormControl, Spinner, TextInput } from '@primer/react' -import { Dialog } from '@primer/react/experimental' -import { useEditableDomainName } from './useEditableDomainContext' -import { sendEvent } from '@/events/components/events' -import { EventType } from '@/events/types' - -type Props = { - xs?: boolean -} - -const EXPERIMENT_NAME = 'domain_edit' - -const QUERY_STRING_KEY = 'ghdomain' // Must match the middleware - -const TRANSLATION_NAMESPACE = 'domain_edit' - -export default function DomainNameEdit({ xs }: Props) { - const { t } = useTranslation(TRANSLATION_NAMESPACE) - - const { asPath } = useRouter() - - const { domainName, setDomainName } = useEditableDomainName() - const [localName, setLocalName] = useState('') - useEffect(() => { - setLocalName(domainName) - }, [domainName]) - - const [open, setOpen] = useState(false) - useEffect(() => { - function handler(event: MouseEvent) { - if (event.target) { - const target = event.target as HTMLElement | SVGSVGElement - if ( - (target.tagName === 'BUTTON' && target.classList.contains('replacedomain-edit')) || - (target.tagName === 'SPAN' && target.classList.contains('replacedomain-text')) || - (target.tagName === 'svg' && - target.parentElement && - target.parentElement.classList.contains('replacedomain-edit')) || - (target.tagName === 'path' && - target.parentElement && - target.parentElement.parentElement && - target.parentElement.parentElement.classList.contains('replacedomain-edit')) - ) { - setOpen(true) - - sendEvent({ - type: EventType.experiment, - experiment_name: EXPERIMENT_NAME, - experiment_variation: 'opened', - experiment_success: true, - }) - } - } - } - const main = document.querySelector('#main-content') - if (main) { - main.addEventListener('click', handler) - } - - return () => { - if (main) { - main.removeEventListener('click', handler) - } - } - }, [asPath]) - - useEffect(() => { - if (document.querySelectorAll('code[data-replacedomain]').length > 0) { - sendEvent({ - type: EventType.experiment, - experiment_name: EXPERIMENT_NAME, - experiment_variation: 'available', - experiment_success: true, - }) - } - }, [asPath]) - - const nameInputRef = useRef(null) - useEffect(() => { - if (open) { - if (nameInputRef.current) { - nameInputRef.current.focus() - } - } - }, [open]) - - const [submissionFailed, setSubmissionFailed] = useState(false) - const [loading, setLoading] = useState(false) - - function handlSubmit(name: string) { - const searchParams = new URLSearchParams({ [QUERY_STRING_KEY]: name }) - setLoading(true) - fetch(`/__tracking__?${searchParams.toString()}`) - .then((response) => { - if (response.ok) { - setOpen(false) - setSubmissionFailed(false) - setDomainName(localName.trim().toLowerCase()) - } else { - setSubmissionFailed(true) - } - - sendEvent({ - type: EventType.experiment, - experiment_name: EXPERIMENT_NAME, - experiment_variation: 'saved', - experiment_success: true, - }) - }) - .finally(() => { - setLoading(false) - }) - } - - const validationError = getValidationError(localName) - - return ( -
- {open && ( - - {t('edit_your')}{' '} - - {t('experimental')} - - - } - width="large" - footerButtons={[ - { buttonType: 'default', content: t('cancel'), onClick: () => setOpen(false) }, - { - buttonType: 'primary', - type: 'button', - onClick: () => { - handlSubmit(localName.trim()) - }, - content: loading ? ( - <> - {t('save')} - - ) : ( - t('save') - ), - disabled: !!validationError || loading, - }, - ]} - onClose={() => { - setOpen(false) - - sendEvent({ - type: EventType.experiment, - experiment_name: EXPERIMENT_NAME, - experiment_variation: 'closed', - experiment_success: true, - }) - }} - aria-labelledby="header" - > -
{ - event.preventDefault() - if (!validationError) { - handlSubmit(localName.trim()) - } - }} - > - - - {t('name')} - setLocalName(event.target.value)} - validationStatus={localName.trim() && validationError ? 'error' : undefined} - sx={{ width: '100%' }} - /> - {localName.trim() && validationError && ( - {validationError} - )} - - - - - - - -
- )} - - {/* Deliberately commented out until we decide to include this on all pages */} - {/* setOpen(true)} - returnFocusRef={returnFocusRef} - /> */} -
- ) -} - -function getValidationError(domainName: string) { - const clean = domainName.trim().toLowerCase() - if (/\s/.test(clean)) { - return 'Whitespace' - } - // if (clean === 'hostname' || !clean) { - // return 'Empty' - // } - if (clean === 'github.com' || clean === 'api.github.com') { - return "Can't be github.com" - } - return null -} - -function SubmissionError({ error }: { error: boolean }) { - const { t } = useTranslation(TRANSLATION_NAMESPACE) - if (error) { - return ( - -

{t('submission_failed')}

-
- ) - } - return null -} - -/* Deliberately commented out until we decide to include this on all pages */ -// function DisplayAndToggle({ -// xs, -// domainNames, -// trigger, -// returnFocusRef, -// }: { -// xs?: boolean -// domainNames: string[] -// trigger: () => void -// returnFocusRef: React.RefObject -// }) { -// const { t } = useTranslation(TRANSLATION_NAMESPACE) -// return ( -// -// -// -// {t('name')}: -// {' '} -// {domainNames.length ? domainNames[0] : DEFAULT} -// -// -// -// ) -// } - -function LearnMoreSnippet() { - const { t } = useTranslation(TRANSLATION_NAMESPACE) - return ( - -

- {t('snippet_about')}{' '} - - {t('learn_more')} - -

-
- ) -} diff --git a/src/links/components/pen-icon.tsx b/src/links/components/pen-icon.tsx deleted file mode 100644 index cd5671fb3742..000000000000 --- a/src/links/components/pen-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export function createPenSVG(): SVGSVGElement { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') - svg.setAttribute('focusable', 'false') - svg.setAttribute('role', 'img') - svg.setAttribute('viewBox', '0 0 16 16') - svg.setAttribute('width', '16') - svg.setAttribute('height', '16') - svg.setAttribute('fill', 'currentColor') - svg.setAttribute( - 'style', - 'display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible; color: var(--color-accent-fg); text-decoration: dashed underline;', - ) - - const PEN_PATH = `M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z` - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') - path.setAttribute('d', PEN_PATH) - svg.appendChild(path) - - return svg -} diff --git a/src/links/components/replace-domain.ts b/src/links/components/replace-domain.ts deleted file mode 100644 index 9751a08958c5..000000000000 --- a/src/links/components/replace-domain.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect } from 'react' -import { useRouter } from 'next/router' - -import { useVersion } from '@/versions/components/useVersion' -import { useEditableDomainName } from './useEditableDomainContext' -import { createPenSVG } from './pen-icon' - -// 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@'] - -function replaceDomains(domain: string | null) { - document.querySelectorAll('pre code[data-replacedomain]').forEach((codeBlock) => { - const replaceDomain = codeBlock.dataset.replacedomain - if (!replaceDomain) return - const replaceDomains = replaceDomain.split(/\s/) - const spans = codeBlock.querySelectorAll('span[class*="-string"]') - if (spans.length) { - spans.forEach((span) => { - replaceInTextContent(span, replaceDomains, domain) - }) - } else { - replaceInTextContent(codeBlock, replaceDomains, domain) - } - replaceInClipboard(codeBlock, replaceDomains, domain) - }) -} - -function replaceInClipboard(element: HTMLElement, replaceDomains: string[], domain: string | null) { - if ( - element.parentElement && - element.parentElement.parentElement && - element.parentElement.parentElement.classList.contains('code-example') - ) { - const pre = - element.parentElement.parentElement.querySelector('pre[data-clipboard]') - const regex = new RegExp(`(${replaceDomains.join('|')})`) - if (pre && pre.textContent) { - if (!pre.dataset.originalText) { - pre.dataset.originalText = pre.textContent - } - if (domain) { - pre.textContent = pre.dataset.originalText.replace(regex, domain) - } else { - pre.textContent = pre.dataset.originalText - } - } - } -} - -function replaceInTextContent( - element: HTMLElement, - replaceDomains: string[], - domain: string | null, -) { - if (!element.textContent) return - - if (!element.querySelector('.replacedomain-text')) { - splitElementText(element, replaceDomains) - } - - if (domain !== null) { - element.querySelectorAll('.replacedomain-text').forEach((textSpan) => { - textSpan.textContent = domain - textSpan.classList.add('editable-domain') - }) - element.querySelectorAll('.replacedomain-edit').forEach((toggleElement) => { - toggleElement.classList.remove('visually-hidden') - }) - } else { - element.querySelectorAll('.replacedomain-text').forEach((textSpan) => { - if (element.dataset.replacedomain) { - textSpan.textContent = element.dataset.replacedomain - } - textSpan.classList.remove('editable-domain') - }) - element.querySelectorAll('.replacedomain-edit').forEach((toggleElement) => { - toggleElement.classList.remove('visually-hidden') - }) - } -} - -function splitElementText(element: HTMLElement, replaceDomains: string[]) { - const splitText = element.textContent!.split(new RegExp(`(${replaceDomains.join('|')})`)) - element.textContent = '' - for (const text of splitText) { - if (replaceDomains.includes(text)) { - element.appendChild(createEditWrapper(text)) - } else { - const span = document.createElement('span') - span.textContent = text - element.appendChild(span) - } - } -} - -function createEditWrapper(text: string): HTMLSpanElement { - const element = document.createElement('span') - element.classList.add('replacedomain-edit') - const span = document.createElement('span') - span.classList.add('replacedomain-text') - span.textContent = text - element.appendChild(span) - element.appendChild(createPenSVG()) - - return element -} - -export function ReplaceDomain() { - const { asPath } = useRouter() - const { domainName } = useEditableDomainName() - const { currentVersion } = useVersion() - - const enable = REPLACEDOMAIN_VERSION_PREFIXES.some((prefix) => currentVersion.startsWith(prefix)) - - useEffect(() => { - if (domainName) { - if (enable) { - replaceDomains(domainName.split(',')[0]) - } else { - replaceDomains(null) - } - } else if (enable) { - replaceDomains(null) - } - }, [asPath, enable, domainName]) - - return null -} diff --git a/src/links/components/useEditableDomainContext.tsx b/src/links/components/useEditableDomainContext.tsx deleted file mode 100644 index e11999b056d2..000000000000 --- a/src/links/components/useEditableDomainContext.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useContext, useEffect, createContext, useState } from 'react' -import Cookies from 'js-cookie' - -const COOKIE_KEY = 'github_domains' -const DEFAULT = '' - -type DomainNameEdit = { - domainName: string - setDomainName: (value: string) => void -} -const UseEditableDomainContext = createContext({ - domainName: '', - setDomainName: () => {}, -}) - -export function DomainNameEditProvider({ children }: { children: React.ReactNode }) { - const [name, setName] = useState(DEFAULT) - useEffect(() => { - const cookie = Cookies.get(COOKIE_KEY) - if (cookie) { - setName(cookie.split(',')[0]) - } - }, []) - - return ( - - {children} - - ) -} - -export const useEditableDomainName = () => { - const context = useContext(UseEditableDomainContext) - if (context === undefined) { - throw new Error('useEditableDomainName must be inside a DomainNameEditProvider') - } - return context -} diff --git a/src/links/stylesheets/domain-edit.scss b/src/links/stylesheets/domain-edit.scss deleted file mode 100644 index 3a6de25bba23..000000000000 --- a/src/links/stylesheets/domain-edit.scss +++ /dev/null @@ -1,38 +0,0 @@ -span.replacedomain-edit { - color: var(--fgColor-accent, var(--color-accent-fg, #0969da)); - font-weight: 600; - border-width: 1px; - border-style: solid solid dashed; - border-image: initial; - border-top-color: transparent; - border-right-color: transparent; - border-bottom-color: var( - --borderColor-default, - var(--color-border-default, #d0d7de) - ); - border-left-color: transparent; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; - - :hover { - border-color: var( - --borderColor-default, - var(--color-border-default, #d0d7de) - ); - background-color: var( - --bgColor-default, - var(--color-canvas-default, #ffffff) - ); - } - - svg { - margin: 0 3px; - } -} - -span.editable-domain { - color: var(--color-accent-fg); - cursor: pointer; - // text-decoration: dashed underline; -} diff --git a/src/shielding/middleware/handle-invalid-query-strings.ts b/src/shielding/middleware/handle-invalid-query-strings.ts index 1e498e2701c8..38668ba4ae52 100644 --- a/src/shielding/middleware/handle-invalid-query-strings.ts +++ b/src/shielding/middleware/handle-invalid-query-strings.ts @@ -37,7 +37,7 @@ const RECOGNIZED_KEYS_BY_ANY = new Set([ 'search-overlay-ask-ai', // The drop-downs on "Webhook events and payloads" 'actionType', - // Used by the tracking middleware + // Legacy domain tracking parameter (no longer processed but still recognized) 'ghdomain', // UTM campaign tracking 'utm_source', diff --git a/src/tracking/README.md b/src/tracking/README.md deleted file mode 100644 index 31f5b718dec5..000000000000 --- a/src/tracking/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Tracking - -## Overview - -This is about recording inbound links that helps with "tracking". - -For example, if you arrive on Docs with `?ghdomain=example.ghe.com` we -can pick that up and put it in a cookie so that the user's content, when -they view it, can say `curl https://example.ghe.com/api/v1` instead -of the stock `curl https://HOSTNAME/api/v1`. - -## How it works - -For a certain number of query strings, we "snatch them up" and redirect -to the same URL as you were on but with the query string key removed. -And in the 302 Found response, we might include a `set-cookie`. - -## Notes - -none diff --git a/src/tracking/middleware/handle-query-strings.ts b/src/tracking/middleware/handle-query-strings.ts deleted file mode 100644 index f128bb1d457f..000000000000 --- a/src/tracking/middleware/handle-query-strings.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Response, NextFunction } from 'express' - -import type { ExtendedRequest } from '@/types' -import statsd from '@/observability/lib/statsd.js' -import { noCacheControl } from '@/frame/middleware/cache-control.js' - -const STATSD_KEY = 'middleware.handle_tracking_querystrings' - -// Exported for the sake of end-to-end tests -export const DOMAIN_QUERY_PARAM = 'ghdomain' -export const MAX_DOMAINS_SAVED = 3 - -const DOMAIN_COOKIE_AGE_MS = 365 * 24 * 3600 * 1000 -export const DOMAIN_COOKIE_NAME = 'github_domains' - -export default function handleTrackingQueryStrings( - req: ExtendedRequest, - res: Response, - next: NextFunction, -) { - if (req.path.startsWith('/_next/')) { - return next() - } - - if (req.query[DOMAIN_QUERY_PARAM] || req.query[DOMAIN_QUERY_PARAM] === '') { - if (Array.isArray(req.query[DOMAIN_QUERY_PARAM])) { - res.status(400).send('can only be one') - - const tags = [`key:${DOMAIN_QUERY_PARAM}`, 'domain:_multiple_'] - statsd.increment(STATSD_KEY, 1, tags) - - return - } - - const searchParams = new URLSearchParams(req.query as any) - - const oldCookieValue: string = req.cookies[DOMAIN_COOKIE_NAME] || '' - const oldCookieValueParsed = oldCookieValue - .split(',') - .map((x) => x.trim().toLowerCase()) - .filter(Boolean) - - const domain = (searchParams.get(DOMAIN_QUERY_PARAM) || '').toLowerCase().trim() - if (domain) { - const newCookieValue = [domain, ...oldCookieValueParsed.filter((x) => x !== domain)] - .slice(0, MAX_DOMAINS_SAVED) - .join(',') - res.cookie(DOMAIN_COOKIE_NAME, newCookieValue, { - maxAge: DOMAIN_COOKIE_AGE_MS, - httpOnly: false, - }) - } else { - res.clearCookie(DOMAIN_COOKIE_NAME) - } - - searchParams.delete(DOMAIN_QUERY_PARAM) - - noCacheControl(res) - - let newURL = req.path - if (searchParams.toString()) { - newURL += `?${searchParams.toString()}` - } - - // Ordinarily, you can put the query string on any URL and the server - // will just 302 redirect you to the same URL but with the query string - // key removed. However, when we, from the client-side UI, send a - // fetch() event, as an XHR request, we can't follow the redirect. - // So we have this "dummy" endpoint just to be able return a 200 OK. - if (req.path === '/__tracking__') { - res.send('OK') - } else { - res.redirect(302, newURL) - } - - const tags = [`key:${DOMAIN_QUERY_PARAM}`, `domain:${domain || '_empty_'}`] - statsd.increment(STATSD_KEY, 1, tags) - - return - } else if (req.path === '/__tracking__') { - // E.g. `GET /__tracking__` but not the lack of query string - return res.status(400).type('text').send('Lacking query string') - } - - return next() -} diff --git a/src/tracking/middleware/index.ts b/src/tracking/middleware/index.ts deleted file mode 100644 index 60ac9cd993b1..000000000000 --- a/src/tracking/middleware/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from 'express' - -import handleTrackingQueryStrings from './handle-query-strings.js' - -const router = express.Router() - -router.use(handleTrackingQueryStrings) - -export default router diff --git a/src/tracking/tests/handle-query-string.ts b/src/tracking/tests/handle-query-string.ts deleted file mode 100644 index 0624d93ad186..000000000000 --- a/src/tracking/tests/handle-query-string.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { get } from '@/tests/helpers/e2etest.js' -import { - DOMAIN_QUERY_PARAM, - DOMAIN_COOKIE_NAME, - MAX_DOMAINS_SAVED, -} from '../middleware/handle-query-strings.js' - -describe('setting a cookie', () => { - test('on home page', async () => { - const res = await get(`/en?${DOMAIN_QUERY_PARAM}=acme.example.com`) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=acme.example.com/) - expect(res.headers.location).toBe('/en') - expect(res.headers['cache-control']).toMatch(/private/) - expect(res.headers['cache-control']).toMatch(/max-age=0/) - }) - - test('with other query string things', async () => { - const res = await get(`/en?${DOMAIN_QUERY_PARAM}=acme.example.com&foo=bar`) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=acme.example.com/) - expect(res.headers.location).toBe('/en?foo=bar') - }) - - test('always lowercase', async () => { - const res = await get(`/en?${DOMAIN_QUERY_PARAM}=Acme.example.COM`) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=acme.example.com/) - }) - - test('on root page', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=acme.example.com`) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=acme.example.com/) - expect(res.headers.location).toBe('/') - }) - - test('empty value does nothing if nothing previous', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=`) - expect(res.statusCode).toBe(302) - expect(res.headers['set-cookie'][0]).toMatch(`${DOMAIN_COOKIE_NAME}=;`) - }) - - test('empty value, when trimmed, does nothing if nothing previous', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=%20`) - expect(res.statusCode).toBe(302) - expect(res.headers['set-cookie'][0]).toMatch(`${DOMAIN_COOKIE_NAME}=;`) - }) - - test('empty value resets previous cookie', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=`, { - headers: { - cookie: `${DOMAIN_COOKIE_NAME}=acme.example.com`, - }, - }) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=;/) - }) - - test('append with previous', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=next.example.com`, { - headers: { - cookie: `${DOMAIN_COOKIE_NAME}=previous.example.com`, - }, - }) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - // %2C is a comma - expect(setCookie).toMatch(/github_domains=next.example.com%2Cprevious.example.com;/) - }) - - test('append with too many', async () => { - let cookie = '' - for (const letter of Array.from('abcdef')) { - const next = `${letter}.example.com` - const res = await get(`/?${DOMAIN_QUERY_PARAM}=${next}`, { - headers: { cookie }, - }) - const setCookie: string = res.headers['set-cookie'][0] - cookie = setCookie.split(';').filter((x) => x.startsWith(DOMAIN_COOKIE_NAME))[0] - if (letter === 'a') { - // first - expect(cookie).toBe(`${DOMAIN_COOKIE_NAME}=a.example.com`) - } else if (letter === 'f') { - // last - expect(cookie.split('%2C').length).toBe(MAX_DOMAINS_SAVED) - expect(cookie.startsWith(`${DOMAIN_COOKIE_NAME}=f.example.com`)).toBe(true) - } - } - }) - - test('append with same as before', async () => { - const res = await get(`/?${DOMAIN_QUERY_PARAM}=Acme.example.com`, { - headers: { - cookie: `${DOMAIN_COOKIE_NAME}=acme.example.com`, - }, - }) - expect(res.statusCode).toBe(302) - const setCookie = res.headers['set-cookie'][0] - expect(setCookie).toMatch(/github_domains=acme.example.com;/) - }) - - test('trying to set multiple', async () => { - const res = await get( - `/?${DOMAIN_QUERY_PARAM}=a.example.com&${DOMAIN_QUERY_PARAM}=b.example.com`, - ) - expect(res.statusCode).toBe(400) - expect(res.body).toMatch(/can only be one/) - }) - - test('using the custom end point (200 OK)', async () => { - const res = await get(`/__tracking__?${DOMAIN_QUERY_PARAM}=Acme.example.com`) - expect(res.statusCode).toBe(200) - expect(res.body).toMatch(/OK/) - }) - test('using the custom end point with no value', async () => { - const res = await get(`/__tracking__?${DOMAIN_QUERY_PARAM}=`) - expect(res.statusCode).toBe(200) - expect(res.body).toMatch(/OK/) - }) - test('using the custom end point (400 Bad request)', async () => { - const res = await get('/__tracking__') - expect(res.statusCode).toBe(400) - expect(res.body).toMatch(/Lacking query string/) - }) -}) From cfbc739d5c4bd803ed578944bf1f5010fe158446 Mon Sep 17 00:00:00 2001 From: Greg Mondello <72952982+gmondello@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:57:11 -0500 Subject: [PATCH 2/9] Create usage-reports.md (#55958) Co-authored-by: Andrew Porter Co-authored-by: Sophie <29382425+sophietheking@users.noreply.github.com> Co-authored-by: Shantanu Phadke --- .../about-usage-reports.md | 54 +++++++++++++++++++ .../billing/managing-your-billing/index.md | 1 + 2 files changed, 55 insertions(+) create mode 100644 content/billing/managing-your-billing/about-usage-reports.md diff --git a/content/billing/managing-your-billing/about-usage-reports.md b/content/billing/managing-your-billing/about-usage-reports.md new file mode 100644 index 000000000000..a1cdab9a49cc --- /dev/null +++ b/content/billing/managing-your-billing/about-usage-reports.md @@ -0,0 +1,54 @@ +--- +title: About usage reports +intro: 'Learn how to request and understand a report that shows detailed {% data variables.product.github %} usage and billing information for your account.' +versions: + feature: enhanced-billing-platform +type: how_to +topics: + - Enterprise + - Team +permissions: '{% data reusables.permissions.enhanced-billing-platform %}' +product: '{% data reusables.billing.enhanced-billing-platform-product %}' +--- + +The usage report shows detailed information about your account’s {% data variables.product.github %} usage, including how much of each SKU was used and the resulting billable amount. + +To generate a usage report, see [AUTOTITLE](/billing/managing-your-billing/gathering-insights-on-your-spending). + +## Usage report fields + +The usage report contains the following fields. + +| Field | Description | +|---------------------------|-------------| +| `date` | The day that the usage occurred. All usage is logged in UTC. | +| `product` | The {% data variables.product.github %} product that was used. | +| `sku` | The specific {% data variables.product.github %} product SKU that was used. | +| `quantity` | The amount of the SKU that was used. | +| `unit_type` | The unit of measurement for the product SKU. | +| `applied_cost_per_quantity` | The unit cost of the product SKU. | +| `gross_amount` | The amount of the product SKU that was used. | +| `discount_amount` | The amount of usage that was discounted. Usage that is discounted as part of your account’s included usage is reflected in this field. Also includes discounts for {% data variables.product.prodname_actions %} usage for standard {% data variables.product.github %}-hosted runners in public repositories and for self-hosted runners. | +| `net_amount` | The billable amount of usage after applying the `discount_amount`. This is the amount that your account will be billed. `gross_amount - discount_amount = net_amount`. | +| `username` | The user associated with the usage, if applicable. | +| `organization` | The organization associated with the usage, if applicable. | +| `repository` | The repository associated with the usage, if applicable. | +| `workflow_path` | The path of the {% data variables.product.prodname_actions %} workflow that generated the usage, if applicable. | +| `cost_center_name` | The cost center associated with the usage, if applicable. | + +### Deprecated report fields + +{% data variables.product.github %} aims to minimize changes to the usage report structure, however at times the report structure or fields may change. + +| Deprecated field | Replacement | +|--------------------|---------------------| +| `usage_at` | Refer to `date` instead. | +| `workflow_name` | Refer to `workflow_path` instead. | + +## How usage is summarized + +To reduce the size of the report, similar usage entries are grouped and totaled. The report summarizes the `quantity`, `gross_amount`, `discount_amount`, and `net_amount` fields based on the combination of the following values: `date`, `sku`, `username`, `workflow_path`, `repository`, `cost_center_name`. + +## Receiving the report + +Usage reports are sent via email to the default email address associated with your {% data variables.product.github %} account. diff --git a/content/billing/managing-your-billing/index.md b/content/billing/managing-your-billing/index.md index cdad208c2020..d2e798a69e92 100644 --- a/content/billing/managing-your-billing/index.md +++ b/content/billing/managing-your-billing/index.md @@ -19,6 +19,7 @@ children: - /adding-licenses-to-an-organization - /roles-for-the-new-billing-platform - /estimating-spending + - /about-usage-reports - /gathering-insights-on-your-spending - /charging-business-units - /preventing-overspending From 6336d84501a3cd18868fa8ff8db338664a109a05 Mon Sep 17 00:00:00 2001 From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:07:29 -0700 Subject: [PATCH 3/9] Copilot code reviews - support for all/most languages [GA] (#55989) --- .../code-review/using-copilot-code-review.md | 12 ++++++------ data/reusables/copilot/policies-for-dotcom.md | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/content/copilot/using-github-copilot/code-review/using-copilot-code-review.md b/content/copilot/using-github-copilot/code-review/using-copilot-code-review.md index a815ae71404a..cf555509794d 100644 --- a/content/copilot/using-github-copilot/code-review/using-copilot-code-review.md +++ b/content/copilot/using-github-copilot/code-review/using-copilot-code-review.md @@ -28,13 +28,13 @@ The current functionality and availability of the two types of review is summari {% rowheaders %} -| | Review selection | Review changes | -|------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Available in | {% data variables.product.prodname_vscode %} | {% data variables.product.prodname_vscode %} and the {% data variables.product.github %} website | +| | Review selection | Review changes | +|------------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Available in | {% data variables.product.prodname_vscode %} | {% data variables.product.prodname_vscode %} and the {% data variables.product.github %} website | | Premium/standard feature | Standard feature available to all {% data variables.product.prodname_copilot_short %} subscribers | Premium feature. Available with the {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, {% data variables.copilot.copilot_business_short %}, and {% data variables.copilot.copilot_enterprise_short %} plans. Per-person monthly quota applies. | -| Description | Initial review of a highlighted section of code with feedback and suggestions | Deeper review of all changes | -| Language support | All | C, C#, C++, Go, Java, JavaScript, Kotlin, Markdown, Python, Ruby, Swift, TypeScript

{% data variables.release-phases.public_preview_caps %} support for HTML and Text. | -| Custom coding guidelines support | No | Yes, see [Customizing {% data variables.product.prodname_copilot_short %}'s reviews with coding guidelines](#customizing-copilots-reviews-with-coding-guidelines) | +| Description | Initial review of a highlighted section of code with feedback and suggestions | Deeper review of all changes | +| Language support | All | All | +| Custom coding guidelines support | No | Yes, see [Customizing {% data variables.product.prodname_copilot_short %}'s reviews with coding guidelines](#customizing-copilots-reviews-with-coding-guidelines) | {% endrowheaders %} diff --git a/data/reusables/copilot/policies-for-dotcom.md b/data/reusables/copilot/policies-for-dotcom.md index 89beb382a92d..ae2e4cf35077 100644 --- a/data/reusables/copilot/policies-for-dotcom.md +++ b/data/reusables/copilot/policies-for-dotcom.md @@ -1,5 +1,2 @@ * **Opt in to user feedback collection:** If enabled, users can provide feedback on {% data variables.product.prodname_copilot_short %} pull request summaries. For more information, see [AUTOTITLE](/enterprise-cloud@latest/copilot/github-copilot-enterprise/copilot-pull-request-summaries/creating-a-pull-request-summary-with-github-copilot). -* **Opt in to preview features:** If enabled, users can test new {% data variables.product.prodname_copilot_short %} features that are not yet generally available. Be aware that previews of features may have flaws, and the features may be changed or discontinued at any time. Current previews of {% data variables.product.prodname_copilot_short %} features include: - - * Experimental languages in {% data variables.product.prodname_copilot_short %} code review. See [AUTOTITLE](/copilot/using-github-copilot/code-review/using-copilot-code-review). - * {% data variables.copilot.copilot_spaces %}. See [AUTOTITLE](/copilot/using-github-copilot/copilot-spaces/about-organizing-and-sharing-context-with-copilot-spaces). +* **Opt in to preview features:** If enabled, users can test new {% data variables.product.prodname_copilot_short %} features that are not yet generally available. Be aware that previews of features may have flaws, and the features may be changed or discontinued at any time. Current previews of {% data variables.product.prodname_copilot_short %} features include {% data variables.copilot.copilot_spaces %}. See [AUTOTITLE](/copilot/using-github-copilot/copilot-spaces/about-organizing-and-sharing-context-with-copilot-spaces). From e48b693c0681f09a03de98135afcd7e7450111af Mon Sep 17 00:00:00 2001 From: Felicity Chapman Date: Tue, 10 Jun 2025 18:14:00 +0100 Subject: [PATCH 4/9] Revert "Revert "Update ready-for-docs-review workflow to handle Copilot-authored PRs"" (#56008) --- src/workflows/ready-for-docs-review.ts | 80 +++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/workflows/ready-for-docs-review.ts b/src/workflows/ready-for-docs-review.ts index 5d3d51a4267d..539556567d0a 100644 --- a/src/workflows/ready-for-docs-review.ts +++ b/src/workflows/ready-for-docs-review.ts @@ -12,6 +12,58 @@ import { getSize, } from './projects.js' +/** + * Determines if a PR is authored by Copilot and extracts the human assignee + * @param data GraphQL response data containing PR information + * @returns Object with isCopilotAuthor boolean and copilotAssignee string + */ +function getCopilotAuthorInfo(data: Record): { + isCopilotAuthor: boolean + copilotAssignee: string +} { + // Check if this is a Copilot-authored PR + const isCopilotAuthor = + data.item.__typename === 'PullRequest' && + data.item.author && + data.item.author.login === 'Copilot' + + // For Copilot PRs, find the appropriate assignee (excluding Copilot itself) + let copilotAssignee = '' + if (isCopilotAuthor && data.item.assignees && data.item.assignees.nodes) { + const assignees = data.item.assignees.nodes + .map((assignee: Record) => assignee.login) + .filter((login: string) => login !== 'Copilot') + + // Use the first non-Copilot assignee + copilotAssignee = assignees.length > 0 ? assignees[0] : '' + } + + return { isCopilotAuthor, copilotAssignee } +} + +/** + * Determines the appropriate author field value based on contributor type + * @param isCopilotAuthor Whether the PR is authored by Copilot + * @param copilotAssignee The human assignee for Copilot PRs (empty string if none) + * @param firstTimeContributor Whether this is a first-time contributor + * @returns The formatted author field value + */ +function getAuthorFieldValue( + isCopilotAuthor: boolean, + copilotAssignee: string, + firstTimeContributor: boolean | undefined, +): string { + if (isCopilotAuthor) { + return copilotAssignee ? `Copilot + ${copilotAssignee}` : 'Copilot' + } + + if (firstTimeContributor) { + return ':star: first time contributor' + } + + return process.env.AUTHOR_LOGIN || '' +} + async function run() { // Get info about the docs-content review board project const data: Record = await graphql( @@ -48,6 +100,14 @@ async function run() { path } } + author { + login + } + assignees(first: 10) { + nodes { + login + } + } } } } @@ -141,17 +201,31 @@ async function run() { } } const turnaround = process.env.REPO === 'github/docs' ? 3 : 2 + + // Check if this is a Copilot-authored PR and get the human assignee + const { isCopilotAuthor, copilotAssignee } = getCopilotAuthorInfo(data) + + // Determine the author field value + const authorFieldValue = getAuthorFieldValue( + isCopilotAuthor, + copilotAssignee, + firstTimeContributor, + ) + // Generate a mutation to populate fields for the new project item const updateProjectV2ItemMutation = generateUpdateProjectV2ItemFieldMutation({ item: newItemID, - author: firstTimeContributor ? ':star: first time contributor' : process.env.AUTHOR_LOGIN || '', + author: authorFieldValue, turnaround, feature, }) // Determine which variable to use for the contributor type let contributorType - if (await isDocsTeamMember(process.env.AUTHOR_LOGIN || '')) { + if (isCopilotAuthor) { + // Treat Copilot PRs as Docs team + contributorType = docsMemberTypeID + } else if (await isDocsTeamMember(process.env.AUTHOR_LOGIN || '')) { contributorType = docsMemberTypeID } else if (await isGitHubOrgMember(process.env.AUTHOR_LOGIN || '')) { contributorType = hubberTypeID @@ -185,6 +259,8 @@ async function run() { return newItemID } +export { run } + run().catch((error) => { console.log(`#ERROR# ${error}`) process.exit(1) From b97d1c19a3e245bc201ac13d6e4590237a1ec1c3 Mon Sep 17 00:00:00 2001 From: Felicity Chapman Date: Tue, 10 Jun 2025 18:35:03 +0100 Subject: [PATCH 5/9] Revert "Revert "Revert "Update ready-for-docs-review workflow to handle Copilot-authored PRs""" (#56009) --- src/workflows/ready-for-docs-review.ts | 80 +------------------------- 1 file changed, 2 insertions(+), 78 deletions(-) diff --git a/src/workflows/ready-for-docs-review.ts b/src/workflows/ready-for-docs-review.ts index 539556567d0a..5d3d51a4267d 100644 --- a/src/workflows/ready-for-docs-review.ts +++ b/src/workflows/ready-for-docs-review.ts @@ -12,58 +12,6 @@ import { getSize, } from './projects.js' -/** - * Determines if a PR is authored by Copilot and extracts the human assignee - * @param data GraphQL response data containing PR information - * @returns Object with isCopilotAuthor boolean and copilotAssignee string - */ -function getCopilotAuthorInfo(data: Record): { - isCopilotAuthor: boolean - copilotAssignee: string -} { - // Check if this is a Copilot-authored PR - const isCopilotAuthor = - data.item.__typename === 'PullRequest' && - data.item.author && - data.item.author.login === 'Copilot' - - // For Copilot PRs, find the appropriate assignee (excluding Copilot itself) - let copilotAssignee = '' - if (isCopilotAuthor && data.item.assignees && data.item.assignees.nodes) { - const assignees = data.item.assignees.nodes - .map((assignee: Record) => assignee.login) - .filter((login: string) => login !== 'Copilot') - - // Use the first non-Copilot assignee - copilotAssignee = assignees.length > 0 ? assignees[0] : '' - } - - return { isCopilotAuthor, copilotAssignee } -} - -/** - * Determines the appropriate author field value based on contributor type - * @param isCopilotAuthor Whether the PR is authored by Copilot - * @param copilotAssignee The human assignee for Copilot PRs (empty string if none) - * @param firstTimeContributor Whether this is a first-time contributor - * @returns The formatted author field value - */ -function getAuthorFieldValue( - isCopilotAuthor: boolean, - copilotAssignee: string, - firstTimeContributor: boolean | undefined, -): string { - if (isCopilotAuthor) { - return copilotAssignee ? `Copilot + ${copilotAssignee}` : 'Copilot' - } - - if (firstTimeContributor) { - return ':star: first time contributor' - } - - return process.env.AUTHOR_LOGIN || '' -} - async function run() { // Get info about the docs-content review board project const data: Record = await graphql( @@ -100,14 +48,6 @@ async function run() { path } } - author { - login - } - assignees(first: 10) { - nodes { - login - } - } } } } @@ -201,31 +141,17 @@ async function run() { } } const turnaround = process.env.REPO === 'github/docs' ? 3 : 2 - - // Check if this is a Copilot-authored PR and get the human assignee - const { isCopilotAuthor, copilotAssignee } = getCopilotAuthorInfo(data) - - // Determine the author field value - const authorFieldValue = getAuthorFieldValue( - isCopilotAuthor, - copilotAssignee, - firstTimeContributor, - ) - // Generate a mutation to populate fields for the new project item const updateProjectV2ItemMutation = generateUpdateProjectV2ItemFieldMutation({ item: newItemID, - author: authorFieldValue, + author: firstTimeContributor ? ':star: first time contributor' : process.env.AUTHOR_LOGIN || '', turnaround, feature, }) // Determine which variable to use for the contributor type let contributorType - if (isCopilotAuthor) { - // Treat Copilot PRs as Docs team - contributorType = docsMemberTypeID - } else if (await isDocsTeamMember(process.env.AUTHOR_LOGIN || '')) { + if (await isDocsTeamMember(process.env.AUTHOR_LOGIN || '')) { contributorType = docsMemberTypeID } else if (await isGitHubOrgMember(process.env.AUTHOR_LOGIN || '')) { contributorType = hubberTypeID @@ -259,8 +185,6 @@ async function run() { return newItemID } -export { run } - run().catch((error) => { console.log(`#ERROR# ${error}`) process.exit(1) From f5aeb1170cae3290367b8b9816be1ea43a491790 Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:31:47 -0700 Subject: [PATCH 6/9] Sync secret scanning data (#56003) Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com> --- src/secret-scanning/data/public-docs.yml | 4 ++-- src/secret-scanning/lib/config.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/secret-scanning/data/public-docs.yml b/src/secret-scanning/data/public-docs.yml index e91f3f899000..8fbe8df80966 100644 --- a/src/secret-scanning/data/public-docs.yml +++ b/src/secret-scanning/data/public-docs.yml @@ -1908,7 +1908,7 @@ ghec: '*' isPublic: false isPrivateWithGhas: true - hasPushProtection: false + hasPushProtection: true hasValidityCheck: false isduplicate: false - provider: Heroku @@ -3375,7 +3375,7 @@ ghec: '*' isPublic: false isPrivateWithGhas: true - hasPushProtection: false + hasPushProtection: true hasValidityCheck: false isduplicate: false - provider: Salesforce diff --git a/src/secret-scanning/lib/config.json b/src/secret-scanning/lib/config.json index 88343e11da0d..6e2c7ba299ec 100644 --- a/src/secret-scanning/lib/config.json +++ b/src/secret-scanning/lib/config.json @@ -1,5 +1,5 @@ { - "sha": "c9b7b73ca9c4e088a890201c66d27cd2f57d6dd7", - "blob-sha": "90cee686b4f51c447b7987d3cb9d05ca06212c8e", + "sha": "de330412222eaea5838c723eb6e3e2ebb124d35e", + "blob-sha": "06bbb1448f72fb3171b30d33d0f59334e3bba539", "targetFilename": "code-security/secret-scanning/introduction/supported-secret-scanning-patterns" } \ No newline at end of file From 1c3dca4c56829cbfd0f3abe9b2d812f6a80f9a8b Mon Sep 17 00:00:00 2001 From: Evan Bonsignori Date: Tue, 10 Jun 2025 10:46:50 -0700 Subject: [PATCH 7/9] use `placeholder` for aria label is same as visual label (#55994) --- data/ui.yml | 1 - src/fixtures/fixtures/data/ui.yml | 1 - src/search/components/input/SearchBarButton.tsx | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/data/ui.yml b/data/ui.yml index 68adbe6032f3..d5e6fa9dea6b 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -25,7 +25,6 @@ release_notes: banner_text: GitHub began rolling these changes out to enterprises on search: input: - aria_label: Open search overlay placeholder: Search or ask {{icon}} Copilot placeholder_no_icon: Search or ask Copilot shortcut: Type {{icon}} to search diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index 68adbe6032f3..d5e6fa9dea6b 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -25,7 +25,6 @@ release_notes: banner_text: GitHub began rolling these changes out to enterprises on search: input: - aria_label: Open search overlay placeholder: Search or ask {{icon}} Copilot placeholder_no_icon: Search or ask Copilot shortcut: Type {{icon}} to search diff --git a/src/search/components/input/SearchBarButton.tsx b/src/search/components/input/SearchBarButton.tsx index 43a6aa56f8bf..d8640750e61f 100644 --- a/src/search/components/input/SearchBarButton.tsx +++ b/src/search/components/input/SearchBarButton.tsx @@ -58,14 +58,14 @@ export function SearchBarButton({ isSearchOpen, setIsSearchOpen, params, searchB className={styles.searchIconButton} onClick={handleClick} tabIndex={0} - aria-label={t('search.input.aria_label')} + aria-label={t('search.input.placeholder_no_icon')} icon={SearchIcon} /> {/* On large and up the SearchBarButton is shown */}