diff --git a/.github/workflows/check-broken-links-github-github.yml b/.github/workflows/check-broken-links-github-github.yml index 92d5521ffa09..9ec1037fc099 100644 --- a/.github/workflows/check-broken-links-github-github.yml +++ b/.github/workflows/check-broken-links-github-github.yml @@ -7,7 +7,7 @@ name: Check Broken Docs Links in github/github on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme permissions: contents: read diff --git a/.github/workflows/delete-orphan-translation-files.yml b/.github/workflows/delete-orphan-translation-files.yml index ceadbc0b8bc9..3615b25161ff 100644 --- a/.github/workflows/delete-orphan-translation-files.yml +++ b/.github/workflows/delete-orphan-translation-files.yml @@ -14,7 +14,7 @@ name: Delete orphan translation files on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme permissions: contents: write diff --git a/.github/workflows/docs-review-collect.yml b/.github/workflows/docs-review-collect.yml index a70d29678d3d..49a514c9c872 100644 --- a/.github/workflows/docs-review-collect.yml +++ b/.github/workflows/docs-review-collect.yml @@ -7,7 +7,7 @@ name: Add docs-reviewers request to the docs-content review board on: workflow_dispatch: schedule: - - cron: '20 */6 * * *' # Run every 6 hours at 20 minutes after + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/enterprise-dates.yml b/.github/workflows/enterprise-dates.yml index bcf58810d2fc..ebc927c2fa3c 100644 --- a/.github/workflows/enterprise-dates.yml +++ b/.github/workflows/enterprise-dates.yml @@ -11,7 +11,7 @@ name: Enterprise date updater on: workflow_dispatch: schedule: - - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme permissions: contents: write diff --git a/.github/workflows/enterprise-release-issue.yml b/.github/workflows/enterprise-release-issue.yml index 83ef2f61fe3e..3b6e87ad768b 100644 --- a/.github/workflows/enterprise-release-issue.yml +++ b/.github/workflows/enterprise-release-issue.yml @@ -7,7 +7,7 @@ name: Open Enterprise release or deprecation issue on: workflow_dispatch: schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme permissions: contents: read diff --git a/.github/workflows/index-autocomplete-search.yml b/.github/workflows/index-autocomplete-search.yml index aa498f2b5f23..c4ec2fa40b58 100644 --- a/.github/workflows/index-autocomplete-search.yml +++ b/.github/workflows/index-autocomplete-search.yml @@ -7,7 +7,7 @@ name: Index autocomplete search in Elasticsearch on: workflow_dispatch: schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST pull_request: paths: - .github/workflows/index-autocomplete-search.yml diff --git a/.github/workflows/index-general-search.yml b/.github/workflows/index-general-search.yml index 0b6a6a518653..e15c21140fb3 100644 --- a/.github/workflows/index-general-search.yml +++ b/.github/workflows/index-general-search.yml @@ -17,7 +17,7 @@ on: required: false default: '' schedule: - - cron: '20 16 * * *' # Run every 24 hours at 20 minutes past the hour + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST workflow_run: workflows: ['Purge Fastly'] types: diff --git a/.github/workflows/link-check-external.yml b/.github/workflows/link-check-external.yml index 663b0ab5a04a..af32710aa49e 100644 --- a/.github/workflows/link-check-external.yml +++ b/.github/workflows/link-check-external.yml @@ -5,7 +5,7 @@ name: Check External Links on: schedule: - - cron: '20 16 * * 3' # Wednesday at 16:20 UTC + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme workflow_dispatch: inputs: max_urls: diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml index ebd0b08765a8..433df82438c1 100644 --- a/.github/workflows/link-check-internal.yml +++ b/.github/workflows/link-check-internal.yml @@ -6,7 +6,7 @@ name: Check Internal Links on: schedule: - - cron: '20 16 * * 2' # Tuesday at 16:20 UTC + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme workflow_dispatch: inputs: version: diff --git a/.github/workflows/lint-entire-content-data-markdown.yml b/.github/workflows/lint-entire-content-data-markdown.yml index 12208a455150..b99f5ff3385c 100644 --- a/.github/workflows/lint-entire-content-data-markdown.yml +++ b/.github/workflows/lint-entire-content-data-markdown.yml @@ -7,7 +7,7 @@ name: 'Lint entire content and data markdown files' on: workflow_dispatch: schedule: - - cron: '20 16 * * 0' # Run every day at 16:20 UTC / 8:20 PST every Sunday + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme permissions: contents: read diff --git a/.github/workflows/moda-allowed-ips.yml b/.github/workflows/moda-allowed-ips.yml index fc484cb224f7..35609ce02f33 100644 --- a/.github/workflows/moda-allowed-ips.yml +++ b/.github/workflows/moda-allowed-ips.yml @@ -6,7 +6,7 @@ name: Update Moda allowed IPs on: schedule: - - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme workflow_dispatch: permissions: diff --git a/.github/workflows/needs-sme-stale-check.yaml b/.github/workflows/needs-sme-stale-check.yaml index baa55711cf19..afcd5ec7755b 100644 --- a/.github/workflows/needs-sme-stale-check.yaml +++ b/.github/workflows/needs-sme-stale-check.yaml @@ -6,7 +6,7 @@ name: Stale check for issues or PRs with "needs SME" label on: schedule: - - cron: '20 16 * * 3' # Run each Wedneday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme permissions: contents: read diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml index 5cfc94688018..007faee4a4c7 100644 --- a/.github/workflows/no-response.yaml +++ b/.github/workflows/no-response.yaml @@ -12,7 +12,7 @@ on: types: [created] schedule: - - cron: '20 16 * * 1' # Run each Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme permissions: contents: read diff --git a/.github/workflows/orphaned-features-check.yml b/.github/workflows/orphaned-features-check.yml index d3ec2d401f20..0c951646a1e9 100644 --- a/.github/workflows/orphaned-features-check.yml +++ b/.github/workflows/orphaned-features-check.yml @@ -7,7 +7,7 @@ name: 'Orphaned features check' on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme pull_request: paths: - .github/workflows/orphaned-features-check.yml diff --git a/.github/workflows/orphaned-files-check.yml b/.github/workflows/orphaned-files-check.yml index d425621ac707..e72147eb7005 100644 --- a/.github/workflows/orphaned-files-check.yml +++ b/.github/workflows/orphaned-files-check.yml @@ -7,7 +7,7 @@ name: 'Orphaned files check' on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme pull_request: paths: - .github/workflows/orphaned-assets-check.yml diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index 197af81bf964..c16d053027a2 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -10,7 +10,7 @@ name: Repo Sync on: workflow_dispatch: schedule: - - cron: '20 */3 * * *' # Run every 3rd hour at 20 minutes after + - cron: '20 14-23/3 * * 1-5' # Mon-Fri 6:20a, 9:20a, 12:20p, 3:20p PST permissions: contents: write diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 752ad73513f1..6ae4c489f342 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ name: Stale check for stalled pull requests in the docs-internal repository on: schedule: - - cron: '20 16 * * 1' # Run each Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme push: paths: - .github/workflows/stale.yml diff --git a/.github/workflows/sync-audit-logs.yml b/.github/workflows/sync-audit-logs.yml index 66da9494d5e3..8d122131b48a 100644 --- a/.github/workflows/sync-audit-logs.yml +++ b/.github/workflows/sync-audit-logs.yml @@ -7,7 +7,7 @@ name: Sync Audit Log data on: workflow_dispatch: schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/sync-graphql.yml b/.github/workflows/sync-graphql.yml index c1c763395c87..b2992859f7d6 100644 --- a/.github/workflows/sync-graphql.yml +++ b/.github/workflows/sync-graphql.yml @@ -7,7 +7,7 @@ name: Sync GraphQL schema on: workflow_dispatch: schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/sync-openapi.yml b/.github/workflows/sync-openapi.yml index 3093ff511714..f564aac181ee 100644 --- a/.github/workflows/sync-openapi.yml +++ b/.github/workflows/sync-openapi.yml @@ -13,7 +13,7 @@ on: required: true default: 'main' schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/sync-secret-scanning.yml b/.github/workflows/sync-secret-scanning.yml index 53e5b9a9dd73..a0ec485db4d6 100644 --- a/.github/workflows/sync-secret-scanning.yml +++ b/.github/workflows/sync-secret-scanning.yml @@ -7,7 +7,7 @@ name: Sync Secret Scanning data on: workflow_dispatch: schedule: - - cron: '20 16 * * *' # Run every day at 16:20 UTC / 8:22 PST + - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml index 175e5d3e389a..58f71fe8e68b 100644 --- a/.github/workflows/triage-stale-check.yml +++ b/.github/workflows/triage-stale-check.yml @@ -6,7 +6,7 @@ name: Stale check for no activity on: schedule: - - cron: '20 16 * * 2' # Run each Tuesday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme permissions: contents: read diff --git a/.github/workflows/validate-github-github-docs-urls.yml b/.github/workflows/validate-github-github-docs-urls.yml index cb76eb5e73c0..96afda1e0194 100644 --- a/.github/workflows/validate-github-github-docs-urls.yml +++ b/.github/workflows/validate-github-github-docs-urls.yml @@ -7,7 +7,7 @@ name: Validate github/github docs URLs on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme # See https://gh.io/AAsyyao before uncommenting: # pull_request: # paths: diff --git a/content/code-security/how-tos/secure-your-secrets/index.md b/content/code-security/how-tos/secure-your-secrets/index.md index a5a41f1815b9..5d4ddf8b6d7b 100644 --- a/content/code-security/how-tos/secure-your-secrets/index.md +++ b/content/code-security/how-tos/secure-your-secrets/index.md @@ -15,8 +15,8 @@ redirect_from: children: - /detect-secret-leaks - /customize-leak-detection - - /troubleshooting-secret-scanning - /prevent-future-leaks - /work-with-leak-prevention - /manage-bypass-requests --- + diff --git a/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/index.md b/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/index.md index 33fa574ccee1..2d05f2fc9b2d 100644 --- a/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/index.md +++ b/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/index.md @@ -10,9 +10,9 @@ contentType: how-tos redirect_from: - /code-security/dependabot/troubleshooting-dependabot children: - - /troubleshooting-dependabot-errors - /troubleshooting-the-detection-of-vulnerable-dependencies - /dependabot-updates-stopped - /troubleshooting-the-dependency-graph - /troubleshooting-dependabot-on-github-actions --- + diff --git a/content/code-security/reference/secret-security/index.md b/content/code-security/reference/secret-security/index.md index e1982eaf7cb8..9e05b4611de0 100644 --- a/content/code-security/reference/secret-security/index.md +++ b/content/code-security/reference/secret-security/index.md @@ -12,6 +12,8 @@ contentType: reference children: - /understanding-github-secret-types - /supported-secret-scanning-patterns + - /secret-scanning-detection-scope - /risk-report-csv-contents - /secret-scanning-pattern-configuration-data --- + diff --git a/content/code-security/how-tos/secure-your-secrets/troubleshooting-secret-scanning.md b/content/code-security/reference/secret-security/secret-scanning-detection-scope.md similarity index 91% rename from content/code-security/how-tos/secure-your-secrets/troubleshooting-secret-scanning.md rename to content/code-security/reference/secret-security/secret-scanning-detection-scope.md index 822ae6f36841..28036c2b5fb7 100644 --- a/content/code-security/how-tos/secure-your-secrets/troubleshooting-secret-scanning.md +++ b/content/code-security/reference/secret-security/secret-scanning-detection-scope.md @@ -1,7 +1,7 @@ --- -title: Troubleshooting secret scanning -shortTitle: Troubleshoot secret scanning -intro: When using {% data variables.product.prodname_secret_scanning %} to detect secrets in your repository, or secrets about to be committed into your repository, you may need to troubleshoot unexpected issues. +title: Secret scanning detection scope +shortTitle: Secret scanning scope +intro: Secret scanning uses pattern matching and validation to detect secrets. Detection varies based on pattern pairs, token types, and push protection settings. product: '{% data reusables.gated-features.secret-scanning %}' versions: fpt: '*' @@ -15,7 +15,8 @@ redirect_from: - /code-security/secret-scanning/troubleshooting-secret-scanning - /code-security/secret-scanning/troubleshooting-secret-scanning-and-push-protection/troubleshooting-secret-scanning - /code-security/secret-scanning/troubleshooting-secret-scanning-and-push-protection -contentType: how-tos + - /code-security/how-tos/secure-your-secrets/troubleshooting-secret-scanning +contentType: reference --- {% data reusables.secret-scanning.enterprise-enable-secret-scanning %} diff --git a/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors.md b/content/code-security/reference/supply-chain-security/dependabot-errors.md similarity index 98% rename from content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors.md rename to content/code-security/reference/supply-chain-security/dependabot-errors.md index 17cbd2f5e394..42676bb23f26 100644 --- a/content/code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors.md +++ b/content/code-security/reference/supply-chain-security/dependabot-errors.md @@ -1,6 +1,6 @@ --- -title: Troubleshooting Dependabot errors -intro: Sometimes {% data variables.product.prodname_dependabot %} is unable to raise a pull request to update your dependencies. You can review the error and unblock {% data variables.product.prodname_dependabot %}. +title: Dependabot errors +intro: '{% data variables.product.prodname_dependabot %} automatically maintains your dependencies, keeping your code secure and current. This reference helps you diagnose and resolve issues so automated updates can continue.' shortTitle: Troubleshoot Dependabot errors redirect_from: - /github/managing-security-vulnerabilities/troubleshooting-github-dependabot-errors @@ -9,6 +9,7 @@ redirect_from: - /code-security/supply-chain-security/managing-vulnerabilities-in-your-projects-dependencies/troubleshooting-dependabot-errors - /code-security/dependabot/working-with-dependabot/troubleshooting-dependabot-errors - /code-security/dependabot/troubleshooting-dependabot/troubleshooting-dependabot-errors + - /code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors versions: fpt: '*' ghec: '*' @@ -22,7 +23,7 @@ topics: - Troubleshooting - Errors - Dependencies -contentType: how-tos +contentType: reference --- This article provides troubleshooting information to help you resolve issues when {% data variables.product.prodname_dependabot %} doesn't work as expected. If you encounter errors when {% data variables.product.prodname_dependabot %} tries to update your dependencies, you can use this guidance to diagnose and fix common problems. diff --git a/content/code-security/reference/supply-chain-security/index.md b/content/code-security/reference/supply-chain-security/index.md index 5b8ed1f82cf7..a65b132567bb 100644 --- a/content/code-security/reference/supply-chain-security/index.md +++ b/content/code-security/reference/supply-chain-security/index.md @@ -24,6 +24,7 @@ children: - /supported-ecosystems-and-repositories - /dependency-graph-supported-package-ecosystems - /dependabot-on-actions + - /dependabot-errors redirect_from: - /code-security/dependabot/ecosystems-supported-by-dependabot --- diff --git a/data/learning-tracks/code-security.yml b/data/learning-tracks/code-security.yml index 1171589ed77d..5f8d1ef387a0 100644 --- a/data/learning-tracks/code-security.yml +++ b/data/learning-tracks/code-security.yml @@ -56,7 +56,7 @@ dependabot_alerts: - >- /code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-the-detection-of-vulnerable-dependencies - >- - /code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors + /code-security/reference/supply-chain-security/dependabot-errors dependabot_security_updates: title: Get pull requests to update your vulnerable dependencies description: >- @@ -104,7 +104,7 @@ dependency_version_updates: - >- /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/managing-pull-requests-for-dependency-updates - >- - /code-security/how-tos/secure-your-supply-chain/troubleshoot-dependency-security/troubleshooting-dependabot-errors + /code-security/reference/supply-chain-security/dependabot-errors secret_scanning: title: Scan for secrets description: >- @@ -140,7 +140,7 @@ secret_scanning: %}/code-security/how-tos/secure-your-secrets/work-with-leak-prevention/working-with-push-protection-in-the-github-ui{% endif %} - >- - /code-security/how-tos/secure-your-secrets/troubleshooting-secret-scanning + /code-security/reference/secret-security/secret-scanning-detection-scope security_alerts: title: Explore and manage security alerts description: Learn where to find and resolve security alerts. diff --git a/src/frame/lib/constants.ts b/src/frame/lib/constants.ts index 8dd839bd6f3c..626fbf5f1239 100644 --- a/src/frame/lib/constants.ts +++ b/src/frame/lib/constants.ts @@ -12,6 +12,7 @@ const DEFAULT_MAX_REQUEST_TIMEOUT = isDev ? 15_000 : 10_000 export const ROOT = process.env.ROOT || '.' export const USER_LANGUAGE_COOKIE_NAME = 'user_language' +export const USER_VERSION_COOKIE_NAME = 'user_version' export const TRANSLATIONS_ROOT = process.env.TRANSLATIONS_ROOT || 'translations' export const MAX_REQUEST_TIMEOUT = process.env.REQUEST_TIMEOUT ? parseInt(process.env.REQUEST_TIMEOUT, 10) diff --git a/src/frame/middleware/cache-control.ts b/src/frame/middleware/cache-control.ts index 27a912def830..7132b94329d7 100644 --- a/src/frame/middleware/cache-control.ts +++ b/src/frame/middleware/cache-control.ts @@ -93,6 +93,13 @@ export function languageCacheControl(res: Response): void { res.set('vary', 'accept-language, x-user-language') } +// Vary on both language and version for homepage redirects +// x-user-version is a custom request header derived from req.cookie:user_version +export function languageAndVersionCacheControl(res: Response): void { + defaultCacheControl(res) + res.set('vary', 'accept-language, x-user-language, x-user-version') +} + // Long cache control for versioned assets: images, CSS, JS... export const assetCacheControl = cacheControlFactory(60 * 60 * 24 * 7, { immutable: true }) diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index d78637d6030a..c2ef6d86b824 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -16,6 +16,7 @@ import { import handleErrors from '@/observability/middleware/handle-errors' import handleNextDataPath from './handle-next-data-path' import detectLanguage from '@/languages/middleware/detect-language' +import detectVersion from '@/versions/middleware/detect-version' import reloadTree from './reload-tree' import context from './context/context' import shortVersions from '@/versions/middleware/short-versions' @@ -213,6 +214,7 @@ export default function index(app: Express) { // *** Config and context for redirects *** app.use(urlDecode) // Must come before detectLanguage to decode @ symbols in version segments app.use(detectLanguage) // Must come before context, breadcrumbs, find-page, handle-errors, homepages + app.use(detectVersion) // Must come before handle-redirects for version cookie support app.use(asyncMiddleware(reloadTree)) // Must come before context app.use(asyncMiddleware(context)) // Must come before early-access-*, handle-redirects app.use(shortVersions) // Support version shorthands diff --git a/src/observability/logger/lib/logger-context.ts b/src/observability/logger/lib/logger-context.ts index f67b6adcb31a..2e29734a8232 100644 --- a/src/observability/logger/lib/logger-context.ts +++ b/src/observability/logger/lib/logger-context.ts @@ -16,6 +16,7 @@ export type LoggerContext = { body?: any language?: string userLanguage?: string + userVersion?: string version?: string pagePath?: string } @@ -51,6 +52,8 @@ const INCLUDE_HEADERS = [ // Language 'x-user-language', 'accept-language', + // Version + 'x-user-version', // Host 'host', 'x-host', diff --git a/src/redirects/middleware/handle-redirects.ts b/src/redirects/middleware/handle-redirects.ts index cffaefbd3a33..606744a7c8b9 100644 --- a/src/redirects/middleware/handle-redirects.ts +++ b/src/redirects/middleware/handle-redirects.ts @@ -4,7 +4,11 @@ import patterns from '@/frame/lib/patterns' import { pathLanguagePrefixed } from '@/languages/lib/languages-server' import { deprecatedWithFunctionalRedirects } from '@/versions/lib/enterprise-server-releases' import getRedirect from '../lib/get-redirect' -import { defaultCacheControl, languageCacheControl } from '@/frame/middleware/cache-control' +import { + defaultCacheControl, + languageCacheControl, + languageAndVersionCacheControl, +} from '@/frame/middleware/cache-control' import { ExtendedRequest, URLSearchParamsTypes } from '@/types' export default function handleRedirects(req: ExtendedRequest, res: Response, next: NextFunction) { @@ -27,13 +31,21 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex // blanket redirects for languageless homepage if (req.path === '/') { const language = getLanguage(req) - languageCacheControl(res) + languageAndVersionCacheControl(res) + + // Build redirect path, optionally including user's preferred version + let redirectPath = `/${language}` + const userVersion = req.userVersion + if (userVersion && userVersion !== 'free-pro-team@latest') { + redirectPath += `/${userVersion}` + } + // Forward query params to the new URL let queryParams = new URLSearchParams((req?.query as any) || '').toString() if (queryParams) { queryParams = `?${queryParams}` } - return res.redirect(302, `/${language}${queryParams}`) + return res.redirect(302, redirectPath + queryParams) } // begin redirect handling diff --git a/src/types/types.ts b/src/types/types.ts index 4fb53e8fb92b..9163719eb123 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -24,6 +24,7 @@ export type ExtendedRequest = Request & { context?: Context language?: string userLanguage?: string + userVersion?: string FailBot?: Failbot } diff --git a/src/versions/components/VersionPicker.tsx b/src/versions/components/VersionPicker.tsx index 49fcb22c9d15..33718fb93a5d 100644 --- a/src/versions/components/VersionPicker.tsx +++ b/src/versions/components/VersionPicker.tsx @@ -1,10 +1,14 @@ import { useRouter } from 'next/router' +import { useState } from 'react' +import { ActionMenu, ActionList } from '@primer/react' import { ArrowRightIcon, InfoIcon } from '@primer/octicons-react' +import cx from 'classnames' +import Cookies from '@/frame/components/lib/cookies' +import { USER_VERSION_COOKIE_NAME } from '@/frame/lib/constants' import { useMainContext } from '@/frame/components/context/MainContext' import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion' import { useTranslation } from '@/languages/components/useTranslation' -import { Picker } from '@/tools/components/Picker' import styles from './VersionPicker.module.scss' @@ -16,8 +20,9 @@ export const VersionPicker = ({ xs }: Props) => { const router = useRouter() const { currentVersion } = useVersion() const mainContext = useMainContext() - // Use TypeScript's "not null assertion" because mainContext.page` should - // will present in mainContext if it's gotten to the stage of React + const [open, setOpen] = useState(false) + // Use TypeScript's "not null assertion" because mainContext.page should + // be present in mainContext if it's gotten to the stage of React // rendering. const page = mainContext.page! const { allVersions, enterpriseServerVersions } = mainContext @@ -32,13 +37,26 @@ export const VersionPicker = ({ xs }: Props) => { return prefix + router.asPath.replace(`/${currentVersion}`, '') } - const allLinks = (page.applicableVersions || []).map((pageVersion) => ({ + type VersionPickerLink = { + text: string + selected: boolean + href: string + extra: { + arrow: boolean + info: boolean + version?: string + } + divider: boolean + } + + const allLinks: VersionPickerLink[] = (page.applicableVersions || []).map((pageVersion) => ({ text: allVersions[pageVersion].versionTitle, selected: currentVersion === pageVersion, href: versionToHref(pageVersion), extra: { arrow: false, info: false, + version: pageVersion, }, divider: false, })) @@ -54,6 +72,7 @@ export const VersionPicker = ({ xs }: Props) => { extra: { arrow: false, info: false, + version: undefined, }, divider: true, }) @@ -66,6 +85,7 @@ export const VersionPicker = ({ xs }: Props) => { extra: { arrow: true, info: false, + version: undefined, }, divider: false, }) @@ -81,32 +101,73 @@ export const VersionPicker = ({ xs }: Props) => { extra: { arrow: false, info: true, + version: undefined, }, divider: false, }) } + const selectedOption = allLinks.find((item) => item.selected) + + const handleVersionSelect = (item: VersionPickerLink) => { + // Save the user's version preference when they actively select one + if (item.extra?.version) { + try { + Cookies.set(USER_VERSION_COOKIE_NAME, item.extra.version) + } catch (err) { + console.warn('Unable to set preferred version cookie', err) + } + } + setOpen(false) + // Navigate after setting cookie + if (item.href) { + router.push(item.href) + } + } + return (
- { - return ( -
- {item.text} - {item.extra?.arrow && ( - - )} - {item.extra?.info && } -
- ) - }} - /> + + + {xs ? `Version\n` : `Version: `} + + {selectedOption?.text || t('version_picker_default_text')} + + + + + {allLinks.map((item, i) => + item.divider ? ( + + ) : ( + { + e.preventDefault() + handleVersionSelect(item) + }} + className={cx((item.extra?.arrow || item.extra?.info) && styles.extrasDisplay)} + role={item.extra?.arrow || item.extra?.info ? 'menuitem' : 'menuitemradio'} + > +
+ {item.text} + {item.extra?.arrow && ( + + )} + {item.extra?.info && ( + + )} +
+
+ ), + )} +
+
+
) } diff --git a/src/versions/middleware/detect-version.ts b/src/versions/middleware/detect-version.ts new file mode 100644 index 000000000000..e21bb13c4483 --- /dev/null +++ b/src/versions/middleware/detect-version.ts @@ -0,0 +1,26 @@ +import type { Response, NextFunction } from 'express' + +import { USER_VERSION_COOKIE_NAME } from '@/frame/lib/constants' +import { allVersionKeys } from '@/versions/lib/all-versions' +import { updateLoggerContext } from '@/observability/logger/lib/logger-context' +import type { ExtendedRequest } from '@/types' + +function isValidVersion(version: string): boolean { + return allVersionKeys.includes(version) +} + +export function getUserVersionFromCookie(req: ExtendedRequest): string | undefined { + const value = req.cookies?.[USER_VERSION_COOKIE_NAME] + if (value && isValidVersion(value)) { + return value + } + return undefined +} + +export default function detectVersion(req: ExtendedRequest, res: Response, next: NextFunction) { + req.userVersion = getUserVersionFromCookie(req) + updateLoggerContext({ + userVersion: req.userVersion, + }) + return next() +} diff --git a/src/versions/tests/version-cookie.ts b/src/versions/tests/version-cookie.ts new file mode 100644 index 000000000000..005a71fa11af --- /dev/null +++ b/src/versions/tests/version-cookie.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from 'vitest' + +import { get } from '@/tests/helpers/e2etest' +import { USER_VERSION_COOKIE_NAME } from '@/frame/lib/constants' + +describe('version cookie redirects', () => { + test('homepage redirects to preferred version from cookie', async () => { + const res = await get('/', { + headers: { + Cookie: `${USER_VERSION_COOKIE_NAME}=enterprise-cloud@latest`, + }, + followRedirects: false, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en/enterprise-cloud@latest') + expect(res.headers.vary).toContain('x-user-version') + }) + + test('homepage redirects to /en when no version cookie', async () => { + const res = await get('/', { followRedirects: false }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en') + expect(res.headers.vary).toContain('x-user-version') + }) + + test('homepage redirects to /en when fpt version in cookie', async () => { + const res = await get('/', { + headers: { + Cookie: `${USER_VERSION_COOKIE_NAME}=free-pro-team@latest`, + }, + followRedirects: false, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en') + }) + + test('ignores invalid version in cookie', async () => { + const res = await get('/', { + headers: { + Cookie: `${USER_VERSION_COOKIE_NAME}=invalid-version`, + }, + followRedirects: false, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en') + }) + + test('homepage redirects to enterprise-server version from cookie', async () => { + const res = await get('/', { + headers: { + Cookie: `${USER_VERSION_COOKIE_NAME}=enterprise-server@3.15`, + }, + followRedirects: false, + }) + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en/enterprise-server@3.15') + }) +}) diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts index c4ee30d4d94a..053884f0d4c1 100644 --- a/src/workflows/tests/actions-workflows.ts +++ b/src/workflows/tests/actions-workflows.ts @@ -68,7 +68,12 @@ const alertWorkflows = workflows // to generate list, console.log(new Set(workflows.map(({ data }) => Object.keys(data.on)).flat())) const dailyWorkflows = scheduledWorkflows.filter(({ data }) => - data.on.schedule.find(({ cron }: { cron: string }) => /^20 [^*]/.test(cron)), + data.on.schedule.find(({ cron }: { cron: string }) => /^20 \d{1,2} /.test(cron)), +) + +// Weekly workflows have a single day-of-week digit (e.g. "20 16 * * 1") +const weeklyWorkflows = dailyWorkflows.filter(({ data }) => + data.on.schedule.find(({ cron }: { cron: string }) => /^20 16 \* \* \d$/.test(cron)), ) describe('GitHub Actions workflows', () => { @@ -97,6 +102,25 @@ describe('GitHub Actions workflows', () => { }, ) + test.each(dailyWorkflows)('daily scheduled workflows only run Mon-Fri $filename', ({ data }) => { + for (const { cron } of data.on.schedule) { + const dayOfWeek = cron.split(' ')[4] + // Day-of-week must be 1-5 (Mon-Fri) or a range within 1-5 + expect(dayOfWeek).toMatch(/^[1-5](-[1-5])?$/) + } + }) + + test.each(weeklyWorkflows)( + 'weekly scheduled workflows only run Mon-Fri $filename', + ({ data }) => { + for (const { cron } of data.on.schedule) { + const dayOfWeek = cron.split(' ')[4] + // Day-of-week must be a single day 1 (Mon) through 5 (Fri) + expect(dayOfWeek).toMatch(/^[1-5]$/) + } + }, + ) + test.each(workflows)( 'contains contents:read permissions when permissions are used $filename', ({ data }) => {