1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Merge pull request #41923 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot
2025-12-17 16:45:08 -08:00
committed by GitHub
8 changed files with 124 additions and 29 deletions

View File

@@ -8,7 +8,7 @@
# --------------------------------------------------------------- # ---------------------------------------------------------------
# To update the sha: # To update the sha:
# https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble # https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble
FROM ghcr.io/github/gh-base-image/gh-base-noble:20251119-090131-gb27dc275c AS base FROM ghcr.io/github/gh-base-image/gh-base-noble:20251217-105955-g05726ec4c AS base
# Install curl for Node install and determining the early access branch # Install curl for Node install and determining the early access branch
# Install git for cloning docs-early-access & translations repos # Install git for cloning docs-early-access & translations repos

View File

@@ -1,4 +1,5 @@
import Cookies from '@/frame/components/lib/cookies' import Cookies from '@/frame/components/lib/cookies'
import { ANALYTICS_ENABLED } from '@/frame/lib/constants'
import { parseUserAgent } from './user-agent' import { parseUserAgent } from './user-agent'
import { Router } from 'next/router' import { Router } from 'next/router'
import { isLoggedIn } from '@/frame/components/hooks/useHasAccount' import { isLoggedIn } from '@/frame/components/hooks/useHasAccount'
@@ -436,8 +437,7 @@ function initPrintEvent() {
} }
export function initializeEvents() { export function initializeEvents() {
return if (!ANALYTICS_ENABLED) return
// eslint-disable-next-line no-unreachable
if (initialized) return if (initialized) return
initialized = true initialized = true
initPageAndExitEvent() // must come first initPageAndExitEvent() // must come first

View File

@@ -1,6 +1,7 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { turnOffExperimentsInPage, dismissCTAPopover } from '../helpers/turn-off-experiments' import { turnOffExperimentsInPage, dismissCTAPopover } from '../helpers/turn-off-experiments'
import { HOVERCARDS_ENABLED, ANALYTICS_ENABLED } from '../../frame/lib/constants'
// This exists for the benefit of local testing. // This exists for the benefit of local testing.
// In GitHub Actions, we rely on setting the environment variable directly // In GitHub Actions, we rely on setting the environment variable directly
@@ -347,6 +348,8 @@ test('sidebar custom link functionality works', async ({ page }) => {
}) })
test.describe('hover cards', () => { test.describe('hover cards', () => {
test.skip(!HOVERCARDS_ENABLED, 'Hovercards are disabled')
test('hover over link', async ({ page }) => { test('hover over link', async ({ page }) => {
await page.goto('/pages/quickstart') await page.goto('/pages/quickstart')
await turnOffExperimentsInPage(page) await turnOffExperimentsInPage(page)
@@ -691,6 +694,8 @@ test.describe('test nav at different viewports', () => {
}) })
test.describe('survey', () => { test.describe('survey', () => {
test.skip(!ANALYTICS_ENABLED, 'Analytics are disabled')
test('happy path, thumbs up and enter comment and email', async ({ page }) => { test('happy path, thumbs up and enter comment and email', async ({ page }) => {
let fulfilled = 0 let fulfilled = 0
let hasSurveyPressedEvent = false let hasSurveyPressedEvent = false

View File

@@ -34,3 +34,6 @@ export const minimumNotFoundHtml = `
&bull; <a href=https://docs.github.com/site-policy/privacy-policies/github-privacy-statement>Privacy</a> &bull; <a href=https://docs.github.com/site-policy/privacy-policies/github-privacy-statement>Privacy</a>
</small> </small>
`.replace(/\n/g, '') `.replace(/\n/g, '')
export const ANALYTICS_ENABLED = true
export const HOVERCARDS_ENABLED = true

View File

@@ -68,6 +68,13 @@ type ContentContext = {
// Cache for journey pages so we only filter all pages once // Cache for journey pages so we only filter all pages once
let cachedJourneyPages: JourneyPage[] | null = null let cachedJourneyPages: JourneyPage[] | null = null
// Cache for guide paths to quickly check if a page is part of any journey
let cachedGuidePaths: Set<string> | null = null
let hasDynamicGuides = false
function needsRendering(str: string): boolean {
return str.includes('{{') || str.includes('{%') || str.includes('[') || str.includes('<')
}
function getJourneyPages(pages: Pages): JourneyPage[] { function getJourneyPages(pages: Pages): JourneyPage[] {
if (!cachedJourneyPages) { if (!cachedJourneyPages) {
@@ -78,6 +85,27 @@ function getJourneyPages(pages: Pages): JourneyPage[] {
return cachedJourneyPages return cachedJourneyPages
} }
function getGuidePaths(pages: Pages): Set<string> {
if (!cachedGuidePaths) {
cachedGuidePaths = new Set()
const journeyPages = getJourneyPages(pages)
for (const page of journeyPages) {
if (!page.journeyTracks) continue
for (const track of page.journeyTracks) {
if (!track.guides) continue
for (const guide of track.guides) {
if (needsRendering(guide.href)) {
hasDynamicGuides = true
} else {
cachedGuidePaths.add(normalizeGuidePath(guide.href))
}
}
}
}
}
return cachedGuidePaths
}
function normalizeGuidePath(path: string): string { function normalizeGuidePath(path: string): string {
// First ensure we have a leading slash for consistent processing // First ensure we have a leading slash for consistent processing
const pathWithSlash = path.startsWith('/') ? path : `/${path}` const pathWithSlash = path.startsWith('/') ? path : `/${path}`
@@ -133,6 +161,16 @@ export async function resolveJourneyContext(
): Promise<JourneyContext | null> { ): Promise<JourneyContext | null> {
const normalizedPath = normalizeGuidePath(articlePath) const normalizedPath = normalizeGuidePath(articlePath)
// Optimization: Fast path check
// If we are not forcing a specific journey page, check our global cache
if (!currentJourneyPage) {
const guidePaths = getGuidePaths(pages)
// If we have no dynamic guides and this path isn't in our known guides, return null early.
if (!hasDynamicGuides && !guidePaths.has(normalizedPath)) {
return null
}
}
// Use the current journey page if provided, otherwise find all journey pages // Use the current journey page if provided, otherwise find all journey pages
const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages) const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages)
@@ -165,15 +203,17 @@ export async function resolveJourneyContext(
let renderedGuidePath = guidePath let renderedGuidePath = guidePath
// Handle Liquid conditionals in guide paths // Handle Liquid conditionals in guide paths
try { if (needsRendering(guidePath)) {
renderedGuidePath = await executeWithFallback( try {
context, renderedGuidePath = await executeWithFallback(
() => renderContent(guidePath, context, { textOnly: true }), context,
() => guidePath, () => renderContent(guidePath, context, { textOnly: true }),
) () => guidePath,
} catch { )
// If rendering fails, use the original path rather than erroring } catch {
renderedGuidePath = guidePath // If rendering fails, use the original path rather than erroring
renderedGuidePath = guidePath
}
} }
const normalizedGuidePath = normalizeGuidePath(renderedGuidePath) const normalizedGuidePath = normalizeGuidePath(renderedGuidePath)
@@ -189,15 +229,17 @@ export async function resolveJourneyContext(
let renderedAlternativeNextStep = alternativeNextStep let renderedAlternativeNextStep = alternativeNextStep
// Handle Liquid conditionals in branching text which likely has links // Handle Liquid conditionals in branching text which likely has links
try { if (needsRendering(alternativeNextStep)) {
renderedAlternativeNextStep = await executeWithFallback( try {
context, renderedAlternativeNextStep = await executeWithFallback(
() => renderContent(alternativeNextStep, context), context,
() => alternativeNextStep, () => renderContent(alternativeNextStep, context),
) () => alternativeNextStep,
} catch { )
// If rendering fails, use the original branching text rather than erroring } catch {
renderedAlternativeNextStep = alternativeNextStep // If rendering fails, use the original branching text rather than erroring
renderedAlternativeNextStep = alternativeNextStep
}
} }
result = { result = {
@@ -278,10 +320,14 @@ export async function resolveJourneyTracks(
const result = await Promise.all( const result = await Promise.all(
journeyTracks.map(async (track) => { journeyTracks.map(async (track) => {
// Render Liquid templates in title and description // Render Liquid templates in title and description
const renderedTitle = await renderContent(track.title, context, { textOnly: true }) const renderedTitle = needsRendering(track.title)
const renderedDescription = track.description ? await renderContent(track.title, context, { textOnly: true })
? await renderContent(track.description, context, { textOnly: true }) : track.title
: undefined
const renderedDescription =
track.description && needsRendering(track.description)
? await renderContent(track.description, context, { textOnly: true })
: track.description
const guides = await Promise.all( const guides = await Promise.all(
track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => { track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => {

View File

@@ -1,20 +1,22 @@
import type { Response, NextFunction } from 'express' import type { Response, NextFunction } from 'express'
import type { ExtendedRequest, Context } from '@/types' import type { ExtendedRequest, Context } from '@/types'
import { resolveJourneyTracks, resolveJourneyContext } from '../lib/journey-path-resolver'
export default async function journeyTrack( export default async function journeyTrack(
req: ExtendedRequest & { context: Context }, req: ExtendedRequest & { context: Context },
res: Response, res: Response,
next: NextFunction, next: NextFunction,
) { ) {
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
if (!req.context) throw new Error('request is not contextualized') if (!req.context) throw new Error('request is not contextualized')
if (!req.context.page) return next() if (!req.context.page) return next()
try { try {
const journeyResolver = await import('../lib/journey-path-resolver')
// If this page has journey tracks defined, resolve them for the landing page // If this page has journey tracks defined, resolve them for the landing page
if ((req.context.page as any).journeyTracks) { if ((req.context.page as any).journeyTracks) {
const resolvedTracks = await journeyResolver.resolveJourneyTracks( const resolvedTracks = await resolveJourneyTracks(
(req.context.page as any).journeyTracks, (req.context.page as any).journeyTracks,
req.context, req.context,
) )
@@ -24,7 +26,7 @@ export default async function journeyTrack(
} }
// Always try to resolve journey context (for navigation on guide articles) // Always try to resolve journey context (for navigation on guide articles)
const journeyContext = await journeyResolver.resolveJourneyContext( const journeyContext = await resolveJourneyContext(
req.pagePath || '', req.pagePath || '',
req.context.pages || {}, req.context.pages || {},
req.context, req.context,

View File

@@ -1,4 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { HOVERCARDS_ENABLED } from '@/frame/lib/constants'
// We postpone the initial delay a bit in case the user didn't mean to // We postpone the initial delay a bit in case the user didn't mean to
// hover over the link. Perhaps they just dragged the mouse over on their // hover over the link. Perhaps they just dragged the mouse over on their
@@ -450,6 +451,8 @@ export function LinkPreviewPopover() {
// This is to track if the user entirely tabs out of the window. // This is to track if the user entirely tabs out of the window.
// For example if they go to the address bar. // For example if they go to the address bar.
useEffect(() => { useEffect(() => {
if (!HOVERCARDS_ENABLED) return
function windowBlur() { function windowBlur() {
popoverHide() popoverHide()
} }
@@ -460,6 +463,8 @@ export function LinkPreviewPopover() {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!HOVERCARDS_ENABLED) return
function showPopover(event: MouseEvent) { function showPopover(event: MouseEvent) {
const target = event.currentTarget as HTMLLinkElement const target = event.currentTarget as HTMLLinkElement
popoverShow(target) popoverShow(target)

View File

@@ -57,3 +57,37 @@ Slack: `#docs-engineering`
Repo: `github/docs-engineering` Repo: `github/docs-engineering`
If you have a question about the webhooks pipeline, you can ask in the `#docs-engineering` Slack channel. If you notice a problem with the webhooks pipeline, you can open an issue in the `github/docs-engineering` repository. If you have a question about the webhooks pipeline, you can ask in the `#docs-engineering` Slack channel. If you notice a problem with the webhooks pipeline, you can open an issue in the `github/docs-engineering` repository.
## Ownership & Escalation
### Ownership
- **Team**: Docs Engineering
- **Source data**: API Platform (github/rest-api-description)
### Escalation path
1. **Pipeline failures**#docs-engineering Slack
2. **OpenAPI schema issues**#api-platform Slack
3. **Production incidents**#docs-engineering
### On-call procedures
If the webhooks pipeline fails:
1. Check workflow logs in `.github/workflows/sync-openapi.yml`
2. Verify access to `github/rest-api-description` repo
3. Check for OpenAPI schema validation errors
4. Review changes in generated data files
5. Check `config.json` SHA tracking
6. Escalate to API Platform team if schema issue
### Monitoring
- Pipeline runs automatically on daily schedule (shared with REST/GitHub Apps)
- PRs created with `github-openapi-bot` label
- SHA tracking in `config.json` for version history
- Failures visible in GitHub Actions
This pipeline is in maintenance mode. We will continue to support ongoing improvements incoming from the platform but we are not expecting new functionality moving forward.
### Known limitations
- **Shared pipeline** - Cannot run webhooks independently of REST/GitHub Apps
- **Single page** - All events on one page (may impact performance)
- **Introduction placement** - Manual content must be at start of file
- **Payload complexity** - Some payloads are very large and complex