Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repo sync #36186

Merged
merged 4 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading