Skip to content

Commit

Permalink
Merge pull request #36186 from github/repo-sync
Browse files Browse the repository at this point in the history
Repo sync
  • Loading branch information
docs-bot authored Feb 6, 2025
2 parents 0c0544a + b20385d commit 9423555
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 10 deletions.
57 changes: 57 additions & 0 deletions config/kubernetes/default/deployments/webapp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
annotations:
# Our internal logs aren't structured so we use logfmt_sloppy to just log stdout and error
# See https://thehub.github.com/epd/engineering/dev-practicals/observability/logging/ for more details
fluentbit.io/parser: logfmt_sloppy
observability.github.com/splunk_index: docs-internal
spec:
dnsPolicy: Default
containers:
- name: webapp
image: docs-internal
resources:
requests:
cpu: 1000m
memory: 4500Mi
limits:
cpu: 8000m
memory: 16Gi
ports:
- name: http
containerPort: 4000
protocol: TCP
envFrom:
- secretRef:
name: vault-secrets
- configMapRef:
name: kube-cluster-metadata
# application-config is created at deploy time from
# configuration set in config/moda/configuration/*/env.yaml
- configMapRef:
name: application-config
# Zero-downtime deploys
# https://thehub.github.com/engineering/products-and-services/internal/moda/feature-documentation/pod-lifecycle/#required-prestop-hook
# https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks
lifecycle:
preStop:
exec:
command: ['sleep', '5']
readinessProbe:
initialDelaySeconds: 5
httpGet:
# WARNING: This should be updated to a meaningful endpoint for your application which will return a 200 once the app is fully started.
# See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes
path: /healthcheck
port: http
19 changes: 19 additions & 0 deletions config/kubernetes/default/services/webapp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: webapp
labels:
service: webapp
annotations:
moda.github.net/domain-name: 'docs-internal-%environment%.service.%region%.github.net'
# HTTP app reachable inside GitHub's network (employee website)
moda.github.net/load-balancer-type: internal-http
spec:
ports:
- name: http
port: 4000
protocol: TCP
targetPort: http
selector:
app: webapp
type: LoadBalancer
2 changes: 1 addition & 1 deletion config/kubernetes/production/deployments/webapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ spec:
name: vault-secrets
- configMapRef:
name: kube-cluster-metadata
# application-config is crated at deploy time from
# application-config is created at deploy time from
# configuration set in config/moda/configuration/*/env.yaml
- configMapRef:
name: application-config
Expand Down
9 changes: 9 additions & 0 deletions config/moda/configuration/default/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
data:
MODA_APP_NAME: docs-internal
NODE_ENV: production
NODE_OPTIONS: '--max-old-space-size=4096'
PORT: '4000'
ENABLED_LANGUAGES: 'en,zh,es,pt,ru,ja,fr,de,ko'
RATE_LIMIT_MAX: '21'
# Moda uses a non-default port for sending datadog metrics
DD_DOGSTATSD_PORT: '28125'
6 changes: 3 additions & 3 deletions config/moda/configuration/production/env.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
data:
MODA_APP_NAME: docs-internal
# Identifies the service deployment environment as production
# Equivalent to HEAVEN_DEPLOYED_ENV === 'production'
MODA_PROD_SERVICE_ENV: 'true'
NODE_ENV: production
NODE_OPTIONS: '--max-old-space-size=4096'
PORT: '4000'
ENABLED_LANGUAGES: 'en,zh,es,pt,ru,ja,fr,de,ko'
RATE_LIMIT_MAX: '21'
# Moda uses a non-default port for sending datadog metrics
DD_DOGSTATSD_PORT: '28125'
# Identifies the service deployment environment as production
# Equivalent to HEAVEN_DEPLOYED_ENV === 'production'
MODA_PROD_SERVICE_ENV: 'true'
21 changes: 21 additions & 0 deletions config/moda/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ environments:
profile: general
region: iad

- name: staging-cedar
require_pipeline: false
notify_still_locked: true # Notify last person to lock this after an hour
cluster_selector:
profile: general
region: iad

- name: staging-pine
require_pipeline: false
notify_still_locked: true # Notify last person to lock this after an hour
cluster_selector:
profile: general
region: iad

- name: staging-spruce
require_pipeline: false
notify_still_locked: true # Notify last person to lock this after an hour
cluster_selector:
profile: general
region: iad

required_builds:
- docs-internal-moda-config-bundle / docs-internal-moda-config-bundle
- docs-internal-docker-image / docs-internal-docker-image
Expand Down
69 changes: 69 additions & 0 deletions src/events/components/dotcom-cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// We cannot use Cookies.get() on the frontend for httpOnly cookies
// so we need to make a request to the server to get the cookies

type DotcomCookies = {
dotcomUsername?: string
isStaff?: boolean
}

let cachedCookies: DotcomCookies | null = null
let inFlightPromise: Promise<DotcomCookies> | null = null
let tries = 0

const GET_COOKIES_ENDPOINT = '/api/cookies'
const MAX_TRIES = 3

// Fetches httpOnly cookies from the server and cache the result
// We use an in-flight promise to avoid duplicate requests
async function fetchCookies(): Promise<DotcomCookies> {
if (cachedCookies) {
return cachedCookies
}

// If request is already in progress, return the same promise
if (inFlightPromise) {
return inFlightPromise
}

if (tries > MAX_TRIES) {
// In prod, fail without a serious error
console.error('Failed to fetch cookies after 3 tries')
// In dev, be loud about the issue
if (process.env.NODE_ENV === 'development') {
throw new Error('Failed to fetch cookies after 3 tries')
}

return Promise.resolve({})
}

inFlightPromise = fetch(GET_COOKIES_ENDPOINT)
.then((response) => {
tries++
if (!response.ok) {
throw new Error(`Failed to fetch cookies: ${response.statusText}`)
}
return response.json() as Promise<DotcomCookies>
})
.then((data) => {
cachedCookies = data
return data
})
.finally(() => {
// Clear the in-flight promise regardless of success or failure
// On success, subsequent calls will return the cached value
// On failure, subsequent calls will retry the request up to MAX_TRIES times
inFlightPromise = null
})

return inFlightPromise
}

export async function getIsStaff(): Promise<boolean> {
const cookies = await fetchCookies()
return cookies.isStaff || false
}

export async function getDotcomUsername(): Promise<string> {
const cookies = await fetchCookies()
return cookies.dotcomUsername || ''
}
5 changes: 2 additions & 3 deletions src/events/components/experiments/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
getActiveExperiments,
} from './experiments'
import { getUserEventsId } from '../events'
import Cookies from 'src/frame/components/lib/cookies'

let experimentsInitialized = false

export function shouldShowExperiment(
experimentKey: ExperimentNames | { key: ExperimentNames },
locale: string,
isStaff: boolean,
) {
// Accept either EXPERIMENTS.<experiment_key> or EXPERIMENTS.<experiment_key>.key
if (typeof experimentKey === 'object') {
Expand All @@ -25,8 +25,7 @@ export function shouldShowExperiment(
if (experiment.key === experimentKey) {
// If the user has staffonly cookie, and staff override is true, show the experiment
if (experiment.alwaysShowForStaff) {
const staffCookie = Cookies.get('staffonly')
if (staffCookie && staffCookie.startsWith('yes')) {
if (isStaff) {
console.log(`Staff cookie is set, showing '${experiment.key}' experiment`)
return true
}
Expand Down
6 changes: 4 additions & 2 deletions src/events/components/experiments/useShouldShowExperiment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { shouldShowExperiment } from './experiment'
import { ExperimentNames } from './experiments'
import { getIsStaff } from '../dotcom-cookies'

export function useShouldShowExperiment(
experimentKey: ExperimentNames | { key: ExperimentNames },
Expand All @@ -13,8 +14,9 @@ export function useShouldShowExperiment(
const [showExperiment, setShowExperiment] = useState(false)

useEffect(() => {
const updateShouldShow = () => {
setShowExperiment(shouldShowExperiment(experimentKey, locale))
const updateShouldShow = async () => {
const isStaff = await getIsStaff()
setShowExperiment(shouldShowExperiment(experimentKey, locale, isStaff))
}

updateShouldShow()
Expand Down
12 changes: 12 additions & 0 deletions src/frame/middleware/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import pageInfo from '@/pageinfo/middleware'
import pageList from '@/pagelist/middleware'
import webhooks from '@/webhooks/middleware/webhooks.js'
import { ExtendedRequest } from '@/types'
import { noCacheControl } from './cache-control'

const router = express.Router()

Expand Down Expand Up @@ -56,6 +57,17 @@ if (process.env.ELASTICSEARCH_URL) {
)
}

// We need access to specific httpOnly cookies set on github.com from the client
// The only way to access these on the client is to fetch them from the server
router.get('/cookies', (req, res) => {
noCacheControl(res)
const cookies = {
isStaff: Boolean(req.cookies?.staffonly?.startsWith('yes')) || false,
dotcomUsername: req.cookies?.dotcom_user || '',
}
return res.json(cookies)
})

router.get('*', (req, res) => {
res.status(404).json({ error: `${req.path} not found` })
})
Expand Down
16 changes: 15 additions & 1 deletion src/search/components/input/SearchOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { AutocompleteSearchHit } from '@/search/types'
import { useAISearchAutocomplete } from '@/search/components/hooks/useAISearchAutocomplete'
import { AskAIResults } from './AskAIResults'
import { uuidv4 } from '@/events/components/events'
import { getIsStaff } from '@/events/components/dotcom-cookies'

type Props = {
searchOverlayOpen: boolean
Expand Down Expand Up @@ -465,7 +466,20 @@ export function SearchOverlay({
backgroundColor: 'var(--overlay-bg-color)',
}}
/>
<Link as="button">Give Feedback</Link>
<Link
onClick={async () => {
if (await getIsStaff()) {
// Hubbers users use an internal discussion for feedback
window.open('https://github.com/github/docs-team/discussions/4952', '_blank')
} else {
// TODO: On ship date set this value
// window.open('TODO', '_blank')
}
}}
as="button"
>
{t('search.overlay.give_feedback')}
</Link>
</footer>
</Overlay>
</>
Expand Down

0 comments on commit 9423555

Please sign in to comment.