Skip to content

Commit 72e412e

Browse files
authored
Merge pull request #45314 from github/repo-sync
Repo sync
2 parents b17436d + d611976 commit 72e412e

44 files changed

Lines changed: 6175 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

content/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ You can add MCP servers in the following ways:
2626
* [Using the `/mcp add` command](#using-the-mcp-add-command)
2727
* [Using the `copilot mcp add` subcommand](#using-the-copilot-mcp-add-subcommand)
2828
* [Editing the configuration file](#editing-the-configuration-file)
29+
* [Adding per-repository MCP servers](#adding-per-repository-mcp-servers)
2930
* [Searching and installing from the registry (experimental)](#searching-and-installing-from-the-registry)
3031

3132
For installation instructions, available tools, and URLs for specific MCP servers, see the [{% data variables.product.github %} MCP Registry](https://github.com/mcp).
@@ -138,6 +139,52 @@ The following example shows a configuration file with a local server and a remot
138139

139140
For more information on MCP server configuration, see [AUTOTITLE](/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers#writing-a-json-configuration-for-mcp-servers).
140141

142+
### Adding per-repository MCP servers
143+
144+
You can configure MCP servers for a specific project by adding a JSON file to the repository. This is useful when you want servers to be available only when working in that project, or when you want to share an MCP setup with collaborators by committing it to the repository.
145+
146+
{% data variables.copilot.copilot_cli_short %} looks for project-level configuration in the following locations:
147+
148+
| Path | Recommended use |
149+
|------|-----------------|
150+
| `.mcp.json` (in any directory from your working directory up to the repository root) | Local or per-checkout configuration; commonly placed at the project root |
151+
| `.github/mcp.json` | Shared configuration that is committed to the repository |
152+
153+
When you start {% data variables.copilot.copilot_cli_short %} inside a Git repository, the CLI walks from your current working directory up to the repository root, loading MCP configuration files along the way. If both `.mcp.json` and `.github/mcp.json` exist in the same directory, `.mcp.json` takes precedence. When server names conflict, definitions in files closer to your working directory take precedence. Project-level definitions also take precedence over those in `~/.copilot/mcp-config.json`. For more information on relative trust, see [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference#mcp-server-trust-levels).
154+
155+
Project-level files can use either the `mcpServers` top-level object shown in `~/.copilot/mcp-config.json`, or the bare top-level format where each key is an MCP server name. For example, this configuration uses the `mcpServers` object:
156+
157+
```json copy
158+
{
159+
"mcpServers": {
160+
"playwright": {
161+
"type": "local",
162+
"command": "npx",
163+
"args": ["@playwright/mcp@latest"]
164+
}
165+
}
166+
}
167+
```
168+
169+
The same server can also be configured with the bare top-level format:
170+
171+
```json copy
172+
{
173+
"playwright": {
174+
"type": "local",
175+
"command": "npx",
176+
"args": ["@playwright/mcp@latest"]
177+
}
178+
}
179+
```
180+
181+
> [!NOTE]
182+
> Project-level MCP servers are loaded only after you confirm folder trust on first launch. They are silently skipped in untrusted directories. For more information on folder trust, see [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-copilot-cli#trusted-directories).
183+
184+
In prompt mode (`copilot -p`), project-level MCP servers are loaded automatically if the current directory is already trusted. If the directory is not trusted, project-level MCP servers are skipped by default. To load them anyway, set the `GITHUB_COPILOT_PROMPT_MODE_WORKSPACE_MCP` environment variable to `true`, since prompt mode cannot show an interactive trust prompt. For more information, see [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference#environment-variables).
185+
186+
The `.vscode/mcp.json` file for {% data variables.product.prodname_vscode_shortname %} is not read by {% data variables.copilot.copilot_cli_short %}. It uses the unsupported top-level key `servers`. To migrate an existing `.vscode/mcp.json` to a format the CLI accepts, see [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference#migrating-from-vscodemcpjson).
187+
141188
### Searching and installing from the registry
142189

143190
> [!NOTE]

content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,11 @@ These settings apply across all your sessions and repositories. You can use the
480480
| `renderMarkdown` | `boolean` | `true` | Render Markdown in terminal output. |
481481
| `remoteExport` | `boolean` | `true` | Export sessions remotely when session sync is available. Set to `false` to opt out of remote export by default. The `remoteSessions` setting when set to `true`, or the `--remote` flag, still enables export and steering regardless of this setting. |
482482
| `respectGitignore` | `boolean` | `true` | Exclude gitignored files from the `@` file mention picker. When `false`, the picker includes files normally excluded by `.gitignore`. |
483-
| `sandbox.gitAuth` | `boolean` | `false` | Inject Git credentials into the sandbox so commands running inside it can authenticate with Git. |
484-
| `sandbox.ghAuth` | `boolean` | `false` | Inject {% data variables.product.prodname_cli %} (`gh`) credentials into the sandbox so commands running inside it can authenticate with the {% data variables.product.prodname_cli %}. |
483+
| `sandbox.allowBypass` | `boolean` | `true` | Allow sandboxed commands to request a bypass for specific operations (surfaces a permission prompt) so tools like `grep` and `glob` keep working when the sandbox would otherwise block them. Set to `false` to opt out. |
484+
| `sandbox.enabled` | `boolean` | `false` | Restrict shell commands, MCP/LSP servers, and built-in file/web tools to a sandboxed environment with limited file system and network access. Enable it from the `/sandbox` dialog or with `/sandbox enable`. |
485+
| `sandbox.gitAuth` | `boolean` | `true` | Inject Git credentials into the sandbox so commands running inside it can authenticate with Git. Set to `false` to opt out. |
486+
| `sandbox.ghAuth` | `boolean` | `true` | Inject {% data variables.product.prodname_cli %} (`gh`) credentials into the sandbox so commands running inside it can authenticate with the {% data variables.product.prodname_cli %}. Set to `false` to opt out. |
487+
| `sandbox.userPolicy.network.allowLocalNetwork` | `boolean` | `true` | Allow sandboxed commands to reach local network addresses (for example, local dev servers). Set to `false` to opt out. |
485488
| `sandbox.userPolicy.seatbelt.keychainAccess` | `boolean` | `false` | macOS only. Grant sandboxed commands access to the system keychain. Can also be toggled from the `/sandbox` dialog. |
486489
| `screenReader` | `boolean` | `false` | Enable screen reader optimizations. |
487490
| `scrollbar` | `boolean` | `true` | Show the scrollbar in scrollable views. Set to `false` to hide it and use the full terminal width. |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
00598e76021b5e4f4ba2903053c69c6010fa7190
1+
7a9cc333248da994abe48d847f18a313db0d56f6

src/frame/middleware/cache-control.ts

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,43 @@ import type { Response } from 'express'
33
import { createLogger } from '@/observability/logger'
44
const logger = createLogger(import.meta.url)
55

6+
type CacheControlKey = 'cache-control' | 'surrogate-control'
7+
68
interface CacheControlOptions {
7-
key?: string
8-
public_?: boolean
9+
key?: CacheControlKey
910
immutable?: boolean
10-
maxAgeZero?: boolean
11+
staleWhileRevalidate?: number
12+
staleIfError?: number
1113
}
1214

13-
// Return a function you can pass a Response object to and it will
14-
// set the `Cache-Control` header.
15-
//
16-
// For example:
17-
//
18-
// const cacheControlYear = getCacheControl(60 * 60 * 24 * 365)
19-
// ...
20-
// cacheControlYear(res)
21-
// res.send(body)
22-
//
23-
// Max age is in seconds
24-
// Max age should not be greater than 31536000 https://www.ietf.org/rfc/rfc2616.txt
15+
const ONE_MINUTE = 60
16+
const TEN_MINUTES = 10 * ONE_MINUTE
17+
const ONE_HOUR = 60 * ONE_MINUTE
18+
const ONE_DAY = 24 * ONE_HOUR
19+
const ONE_WEEK = 7 * ONE_DAY
20+
const ONE_YEAR = 365 * ONE_DAY
21+
22+
// Return a function you can pass a Response object to and it will set the `Cache-Control` header.
23+
// Max age is in seconds.
24+
// Max age should not be greater than 31536000, per <https://www.ietf.org/rfc/rfc2616.txt>.
2525
function cacheControlFactory(
26-
maxAge: number = 60 * 60,
26+
maxAge: number = 0,
2727
{
2828
key = 'cache-control',
29-
public_ = true,
3029
immutable = false,
31-
maxAgeZero = false,
30+
staleWhileRevalidate = 0,
31+
staleIfError = 0,
3232
}: CacheControlOptions = {},
3333
): (res: Response) => void {
3434
const directives = [
35-
maxAge && public_ && 'public',
36-
maxAge && `max-age=${maxAge}`,
37-
maxAge && immutable && 'immutable',
38-
!maxAge && 'private',
39-
!maxAge && 'no-store',
40-
maxAge >= 60 * 60 && `stale-while-revalidate=${60 * 60}`,
41-
maxAge >= 60 * 60 && `stale-if-error=${24 * 60 * 60}`,
42-
(maxAgeZero || maxAge === 0) && 'max-age=0',
35+
maxAge > 0 && 'public',
36+
maxAge > 0 && `max-age=${maxAge}`,
37+
maxAge <= 0 && 'max-age=0',
38+
maxAge > 0 && immutable && 'immutable',
39+
maxAge <= 0 && 'private',
40+
maxAge <= 0 && 'no-store',
41+
maxAge > 0 && staleWhileRevalidate > 0 && `stale-while-revalidate=${staleWhileRevalidate}`,
42+
maxAge > 0 && staleIfError > 0 && `stale-if-error=${staleIfError}`,
4343
]
4444
.filter(Boolean)
4545
.join(', ')
@@ -53,64 +53,73 @@ function cacheControlFactory(
5353
}
5454
}
5555

56-
// These are roughly in order from shortest to longest
56+
// ### These are roughly in order from shortest to longest. ###
5757

58-
// If you do not want caching
58+
// If you do not want caching.
5959
export const noCacheControl = cacheControlFactory(0)
6060

61-
// Short cache for 4xx errors
62-
export const errorCacheControl = cacheControlFactory(60) // 1 minute
63-
64-
// This means we tell the browser to cache the XHR request for 1h
65-
const searchBrowserCacheControl = cacheControlFactory(60 * 60)
66-
// This tells the CDN to cache the response for 24 hours
67-
const searchCdnCacheControl = cacheControlFactory(60 * 60 * 24, {
68-
key: 'surrogate-control',
69-
})
70-
export function searchCacheControl(res: Response): void {
71-
searchBrowserCacheControl(res)
72-
searchCdnCacheControl(res)
73-
}
61+
// Short cache for 4xx errors.
62+
export const errorCacheControl = cacheControlFactory(ONE_MINUTE)
7463

75-
// 24 hours for CDN, we soft-purge this with each deploy
76-
const defaultCDNCacheControl = cacheControlFactory(60 * 60 * 24, {
64+
// For default cache control, up to one week in cache but much shorter in browser.
65+
// Most responses are under the default cache control policy.
66+
const browserCacheControl = cacheControlFactory(ONE_MINUTE)
67+
const defaultCDNCacheControl = cacheControlFactory(TEN_MINUTES, {
7768
key: 'surrogate-control',
69+
staleWhileRevalidate: ONE_WEEK,
70+
staleIfError: ONE_WEEK,
7871
})
79-
// Shorter because between deployments and their (sort) purges,
80-
// we don't want the browser to overly cache because with them we
81-
// can't control purging.
82-
const defaultBrowserCacheControl = cacheControlFactory(60)
83-
// A general default configuration that is useful to almost all responses
84-
// that can be cached.
8572
export function defaultCacheControl(res: Response): void {
73+
browserCacheControl(res)
8674
defaultCDNCacheControl(res)
87-
defaultBrowserCacheControl(res)
8875
}
76+
export const searchCacheControl = defaultCacheControl
8977

90-
// Vary on content type for pages that support content negotiation (HTML vs markdown)
78+
// For requests where the response can vary between a HTML and Markdown response
79+
// using the accept header.
9180
export function contentTypeCacheControl(res: Response): void {
9281
defaultCacheControl(res)
9382
res.append('vary', 'accept')
9483
}
9584

96-
// Vary on language when needed
97-
// x-user-language is a custom request header derived from req.cookie:user_language
98-
// accept-language is truncated to one of our available languages
85+
// Vary on language when needed.
86+
// `x-user-language` is a custom request header derived from `req.cookie:user_language`.
87+
// `accept-language` is truncated to one of our available languages.
9988
// https://bit.ly/3u5UeRN
10089
export function languageCacheControl(res: Response): void {
10190
defaultCacheControl(res)
10291
res.append('vary', 'accept-language, x-user-language')
10392
}
10493

105-
// Vary on both language and version for homepage redirects
106-
// x-user-version is a custom request header derived from req.cookie:user_version
94+
// Vary on both language and version for homepage redirects.
95+
// `x-user-version` is a custom request header derived from `req.cookie:user_version`.
10796
export function languageAndVersionCacheControl(res: Response): void {
10897
defaultCacheControl(res)
10998
res.append('vary', 'accept-language, x-user-language, x-user-version')
11099
}
111100

112-
// Long cache control for versioned assets: images, CSS, JS...
113-
export const assetCacheControl = cacheControlFactory(60 * 60 * 24 * 7, { immutable: true })
101+
// Long cache control for versioned assets: such as images, CSS, prebuilt JS.
102+
const assetBrowserCacheControl = cacheControlFactory(TEN_MINUTES)
103+
const assetCDNCacheControl = cacheControlFactory(ONE_WEEK, {
104+
key: 'surrogate-control',
105+
immutable: true,
106+
staleWhileRevalidate: ONE_WEEK,
107+
staleIfError: ONE_WEEK,
108+
})
109+
export function assetCacheControl(res: Response): void {
110+
assetBrowserCacheControl(res)
111+
assetCDNCacheControl(res)
112+
}
114113

115-
// Long caching for archived pages and assets
116-
export const archivedCacheControl = cacheControlFactory(60 * 60 * 24 * 365)
114+
// Long caching for archived pages and assets.
115+
const archivedBrowserCacheControl = cacheControlFactory(TEN_MINUTES)
116+
const archivedCDNCacheControl = cacheControlFactory(ONE_YEAR, {
117+
key: 'surrogate-control',
118+
immutable: true,
119+
staleWhileRevalidate: ONE_WEEK,
120+
staleIfError: ONE_WEEK,
121+
})
122+
export function archivedCacheControl(res: Response): void {
123+
archivedBrowserCacheControl(res)
124+
archivedCDNCacheControl(res)
125+
}

src/frame/tests/favicons.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@ import { describe, expect, test, vi } from 'vitest'
33
import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key'
44
import { get } from '@/tests/helpers/e2etest'
55

6+
function getMaxAge(header: string | undefined): number {
7+
return Number(String(header).match(/(?:^|[ ,])max-age=(\d+)/)?.[1])
8+
}
9+
10+
// The CDN holds favicons for a long time because we can purge it on demand with
11+
// the manual surrogate key. The browser gets a shorter window because we can't.
12+
function expectAggressiveCaching(headers: Record<string, string>) {
13+
const cdnMaxAge = getMaxAge(headers['surrogate-control'])
14+
expect(headers['surrogate-control']).toContain('public')
15+
expect(headers['surrogate-control']).toContain('immutable')
16+
expect(cdnMaxAge).toBeGreaterThanOrEqual(60 * 60)
17+
18+
const browserMaxAge = getMaxAge(headers['cache-control'])
19+
expect(headers['cache-control']).toContain('public')
20+
expect(browserMaxAge).toBeGreaterThan(0)
21+
expect(browserMaxAge).toBeLessThan(cdnMaxAge)
22+
}
23+
624
describe('favicon assets', () => {
725
vi.setConfig({ testTimeout: 60 * 1000 })
826

@@ -12,16 +30,7 @@ describe('favicon assets', () => {
1230
expect(parseInt(res.headers['content-length'], 10)).toBeGreaterThan(0)
1331
expect(res.headers['content-type']).toBe('image/x-icon')
1432
expect(res.headers['set-cookie']).toBeUndefined()
15-
expect(res.headers['cache-control']).toContain('public')
16-
expect(res.headers['cache-control']).toContain('immutable')
17-
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
18-
const maxAgeSeconds = parseInt(
19-
(res.headers['cache-control'] || '').match(/max-age=(\d+)/)?.[1] || '',
20-
10,
21-
)
22-
// Let's not be too specific in the tests, just as long as it's testing
23-
// that it's a reasonably large number of seconds.
24-
expect(maxAgeSeconds).toBeGreaterThanOrEqual(60 * 60)
33+
expectAggressiveCaching(res.headers)
2534
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.MANUAL)
2635
})
2736

@@ -31,16 +40,7 @@ describe('favicon assets', () => {
3140
expect(parseInt(res.headers['content-length'] || '', 10)).toBeGreaterThan(0)
3241
expect(res.headers['content-type']).toBe('image/png')
3342
expect(res.headers['set-cookie']).toBeUndefined()
34-
expect(res.headers['cache-control']).toContain('public')
35-
expect(res.headers['cache-control']).toContain('immutable')
36-
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
37-
const maxAgeSeconds = parseInt(
38-
(res.headers['cache-control'] || '').match(/max-age=(\d+)/)?.[1] || '',
39-
10,
40-
)
41-
// Let's not be too specific in the tests, just as long as it's testing
42-
// that it's a reasonably large number of seconds.
43-
expect(maxAgeSeconds).toBeGreaterThanOrEqual(60 * 60)
43+
expectAggressiveCaching(res.headers)
4444
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.MANUAL)
4545
})
4646

src/frame/tests/server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,25 @@ describe('server', () => {
7575
expect(surrogateKeySplit.includes(makeLanguageSurrogateKey('en'))).toBeTruthy()
7676
})
7777

78+
test('caches responses with short edge freshness and a week-long stale window', async () => {
79+
const week = 60 * 60 * 24 * 7
80+
for (const path of ['/en/get-started', '/robots.txt']) {
81+
const res = await get(path)
82+
expect(res.statusCode).toBe(200)
83+
const surrogate = res.headers['surrogate-control']
84+
expect(surrogate).toContain(`stale-while-revalidate=${week}`)
85+
expect(surrogate).toContain(`stale-if-error=${week}`)
86+
// Edge freshness is short (well under the week-long stale window).
87+
const maxAge = Number(surrogate.match(/(?:^|[ ,])max-age=(\d+)/)?.[1])
88+
expect(maxAge).toBeGreaterThan(0)
89+
expect(maxAge).toBeLessThan(week)
90+
}
91+
// Browser cache stays short: 60s, unpurgeable.
92+
const res = await get('/en/get-started')
93+
expect(res.headers['cache-control']).toMatch(/(^|[ ,])max-age=60([ ,]|$)/)
94+
})
95+
7896
test('sets fine-grained product and version surrogate keys on content pages', async () => {
79-
// docs-engineering#6719: content pages emit language, product, version, and
80-
// a product,language compound key so per-deploy purges can target the
81-
// tightest key instead of a whole language.
8297
const res = await get('/en/get-started')
8398
expect(res.statusCode).toBe(200)
8499
const keys = res.headers['surrogate-key'].split(/\s/g)

src/ghes-releases/scripts/deprecate/create-docs-ghes-version-repo.sh

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,34 @@ gh api -X PUT "/orgs/github/teams/docs-engineering/repos/github/docs-ghes-$versi
3232
-f 'permission=admin' --silent
3333
echo "--- github/employees as read"
3434
gh api -X PUT "/orgs/github/teams/employees/repos/github/docs-ghes-$version" --silent
35-
echo "--- Require a pull request review before merging"
36-
repositoryId="$(gh api graphql -f query="{repository(owner:\"github\",name:\"docs-ghes-$version\"){id}}" -q .data.repository.id)"
37-
gh api graphql -f query='
38-
mutation($repositoryId:ID!,$branch:String!,$requiredReviews:Int!) {
39-
createBranchProtectionRule(input: {
40-
repositoryId: $repositoryId
41-
pattern: $branch
42-
requiresApprovingReviews: true
43-
requiredApprovingReviewCount: $requiredReviews
44-
requiresCodeOwnerReviews: true
45-
}) { clientMutationId }
46-
}' -f repositoryId="$repositoryId" -f branch=main -F requiredReviews=1 --silent
35+
echo "--- Protect main with a repository ruleset (1 review + code owners, no force-push or deletion)"
36+
gh api -X POST "/repos/github/docs-ghes-$version/rulesets" --input - --silent <<'RULESET'
37+
{
38+
"name": "main branch protection",
39+
"target": "branch",
40+
"enforcement": "active",
41+
"bypass_actors": [
42+
{ "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" }
43+
],
44+
"conditions": {
45+
"ref_name": { "include": ["refs/heads/main"], "exclude": [] }
46+
},
47+
"rules": [
48+
{ "type": "deletion" },
49+
{ "type": "non_fast_forward" },
50+
{
51+
"type": "pull_request",
52+
"parameters": {
53+
"required_approving_review_count": 1,
54+
"dismiss_stale_reviews_on_push": false,
55+
"require_code_owner_review": true,
56+
"require_last_push_approval": false,
57+
"required_review_thread_resolution": false
58+
}
59+
}
60+
]
61+
}
62+
RULESET
4763
echo "--- Enable GitHub Pages, set source to main in root directory, and make the pages site public"
4864
gh api -X POST "/repos/github/docs-ghes-$version/pages" \
4965
-f "source[branch]=main" -f "source[path]=/" -F "public=true" --silent

0 commit comments

Comments
 (0)