Skip to content
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
216 changes: 216 additions & 0 deletions .github/workflows/restrict-merge-queue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
name: Restrict who can queue merges

# **What it does**:
# On a `merge_group` event, checks whether the person who put the pull
# request into the merge queue is on github/technical-content. If they
# are not, comments on the pull request saying so and fails, which
# ejects the entry from the queue.
# **Why we have it**:
# Classic branch protection used to restrict who could push to `main`,
# but that rule was swept org-wide on 2026-06-22, so today anyone with
# write access can merge. Rebuilding it means also enabling a merge
# queue on the same rule, which could collide with the merge queue on
# our ruleset and lock the branch for everyone. This does the same job
# with machinery we own outright.
# **Who does it impact**: Anyone merging to `main`.

# Two things to know before changing this:
#
# 1. `merge-queue-restriction` has to be a required status check on the ruleset targeting `refs/heads/main`, or this
# enforces nothing. Add it there only after this workflow is on `main` and reporting. The other order makes the
# check required before it has ever reported, which blocks every pull request. To turn enforcement off again,
# remove it from the ruleset. This workflow keeps running and keeps passing.
#
# 2. The `pull_request` runs do no work. They exist so the required check reports a passing context on the pull
# request itself. Drop them and the check sits pending forever and nothing can ever be enqueued.

on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
merge_group:

permissions:
contents: read
pull-requests: write

# Keyed on head SHA rather than pull request number. Webhook delivery order is not guaranteed, and keying on the pull
# request would let a late event for an old SHA cancel the run for a newer one.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
cancel-in-progress: true

jobs:
# The job id doubles as the check run name because this job deliberately has no `name:` key. Renaming this job renames
# the required status check, which silently stops enforcing anything.
merge-queue-restriction:
# This repository syncs a subset of files to the public github/docs, including workflows. Nothing here applies there.
if: github.repository == 'github/docs-internal'
runs-on: ubuntu-latest
steps:
- name: Check that the enqueuer is on the Technical Content team
if: github.event_name == 'merge_group'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
# Reading org team membership needs `read:org`, which GITHUB_TOKEN does not have. `merge_group` always runs in
# the base repository, so this secret is always available here, including for pull requests from forks.
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
// Addressed by numeric ID (org github = 9919, team technical-content = 325922) because IDs survive renames
// and slugs do not. This team was called `docs` until recently and the rename broke a pile of automation.
const ORG_ID = 9919
const TEAM_ID = 325922
const TEAM = 'github/technical-content'
const MARKER = '<!-- merge-queue-restriction -->'
const EXEMPT_USERS = ['docs-bot']
const CONTENT_SLACK_CHANNEL = 'C0E9DK082'
const MAX_ATTEMPTS = 3

// `github.actor` is the person who enqueued. On a re-run it stays the original actor, unlike
// `github.triggering_actor`, so re-running cannot launder a failing check into a passing one.
const actor = context.actor
core.info(`This merge group was queued by @${actor}.`)

// A GitHub App actor always ends in `[bot]`, and `[` is not a valid character in a username, so nobody can
// impersonate one. `docs-bot` is a plain User account and has to be named explicitly.
core.info(`Checking whether @${actor} is an automation account...`)
if (actor.endsWith('[bot]') || EXEMPT_USERS.includes(actor)) {
core.info(`Checked: @${actor} is an automation account. Allowing the merge.`)
return
}
core.info(`Checked: @${actor} is a person, so they need to be on the team.`)

// Every request retries transient failures before giving up, then fails closed. Failing closed is safe
// here: github/technical-content is an `always` bypass actor on the ruleset, so a broken check stops
// non-Docs merges but never stops Docs. A 404 comes back as null data rather than as an error, because on
// both of the endpoints below it is an answer rather than a failure.
async function ask(description, route, params) {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
core.info(`${description} (attempt ${attempt} of ${MAX_ATTEMPTS})...`)
try {
const { data } = await github.request(route, params)
return { data }
} catch (error) {
if (error.status === 404) return { data: null }
if (attempt === MAX_ATTEMPTS) return { error }

const seconds = attempt * 2
core.warning(`Asked and failed with HTTP ${error.status}. Retrying in ${seconds}s.`)
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
}
}

// Deliberately says nothing on the pull request. We only comment when we know the answer, and here we
// do not.
function giveUp(detail) {
core.setFailed(`${detail} Failing closed, so this stays out of the queue. Ask in #docs-content.`)
}

// Checking the parent team is enough. Every member of every child team (docs-content, docs-engineering,
// docs-localization, docs-content-systems, docs-product-managers, docs-open-source, docs-design,
// docs-content-design, copilot-docs) also resolves as a member of the parent.
const membershipResult = await ask(
`Asking the API whether @${actor} is on ${TEAM}`,
'GET /organizations/{org_id}/team/{team_id}/memberships/{username}',
{ org_id: ORG_ID, team_id: TEAM_ID, username: actor },
)

if (membershipResult.error) {
giveUp(
`Could not check ${TEAM} membership for @${actor}. ` +
`The last attempt returned HTTP ${membershipResult.error.status}.`,
)
return
}

const membership = membershipResult.data

if (membership) {
core.info(`Asked: @${actor} has membership state "${membership.state}" on ${TEAM}.`)
} else {
core.info(`Asked: the API reports no membership for @${actor} on ${TEAM} (HTTP 404).`)

// That 404 is ambiguous. It is byte for byte the same response for "not a member", "team no longer
// exists", and "the token lost visibility into the org". Read the team back before believing it,
// otherwise a deleted team or a downgraded token would blame every single person who tries to merge.
const teamResult = await ask(
`A 404 is ambiguous, so reading ${TEAM} back to confirm it is still visible`,
'GET /organizations/{org_id}/team/{team_id}',
{ org_id: ORG_ID, team_id: TEAM_ID },
)

if (teamResult.error || !teamResult.data) {
giveUp(
`Could not read ${TEAM} itself, so the 404 for @${actor} says nothing about their membership. ` +
`The last attempt returned HTTP ${teamResult.error?.status ?? 404}. ` +
'Either the team is gone or this token lost access to it.',
)
return
}

core.info(`Read it back: ${TEAM} is visible as "${teamResult.data.slug}", so the 404 is a real answer.`)
}

if (membership?.state === 'active') {
core.info(`@${actor} is an active member of ${TEAM}. Allowing the merge.`)
return
}

const reason = membership
? `@${actor} has a "${membership.state}" membership on ${TEAM} rather than an active one.`
: `@${actor} is not a member of ${TEAM}.`
core.info(`Blocking the merge. ${reason}`)

// A failed check on a `gh-readonly-queue` ref is not something anyone goes looking for, so say why on the
// pull request itself. The merge group ref is `refs/heads/gh-readonly-queue/<base>/pr-<number>-<sha>`, and
// the pull request it names is the one this actor just enqueued, so the number and the actor correspond.
const ref = context.payload.merge_group?.head_ref ?? context.ref
core.info(`Working out which pull request this merge group is for, from "${ref}"...`)
const number = Number(ref.match(/\/pr-(\d+)-[0-9a-f]+$/)?.[1])

if (!number) {
core.warning(`Worked it out: could not find a pull request number in "${ref}". Skipping the comment.`)
} else {
core.info(`Worked it out: this merge group is for #${number}.`)

// Only comment once. Someone who tries to enqueue again already has the explanation, and repeating it
// turns a useful comment into noise.
core.info(`Reading the existing comments on #${number}...`)
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
per_page: 100,
})
core.info(`Read ${comments.length} comment(s) on #${number}.`)

if (comments.some((comment) => comment.body?.includes(MARKER))) {
core.info(`#${number} already has this explanation, so not commenting again.`)
} else {
core.info(`Commenting on #${number} to explain...`)
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
body: [
// The marker has to be on its own line. GitHub parses `<!--` at the start of a block as an
// HTML block that runs to the end of the line containing `-->`, so anything sharing that
// line renders as literal text: no code spans, no links.
MARKER,
[
`👋 Hi @${actor}, this pull request was removed from the merge queue.`,
'Only the GitHub Technical Content team merges to `main` in this repository.',
'Once this is reviewed and ready, ask in',
`[#docs-content](https://github.slack.com/archives/${CONTENT_SLACK_CHANNEL})`,
'and someone on the team can merge it for you.',
].join(' '),
].join('\n'),
})
core.info(`Commented on #${number}.`)
}
}

core.setFailed(
`Only ${TEAM} merges to main in this repository. ${reason} ` +
'Ask in #docs-content and someone on the team can merge this for you.',
)
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Before adding a new SSH key to the ssh-agent to manage your keys, you should hav

> ### Troubleshooting SSH agent conflicts in Windows
>
> In Windows environments, the native Windows OpenSSH implementation and the one included with Git for Windows (based on MSYS2/Bash) can coexist.
> In Windows environments, the native Windows OpenSSH implementation and the one included with [Git for Windows](https://gitforwindows.org/) (based on MSYS2/Bash) can coexist.
>
> If you configure and save your passphrases in the Windows agent using PowerShell, Git may still prompt you for your passphrase during operations like `git push`. This can happen when Git for Windows uses its bundled `ssh.exe` (from MSYS2) instead of the Windows system OpenSSH client, and therefore can't talk to the Windows `ssh-agent` service.
>
Expand All @@ -176,6 +176,14 @@ Before adding a new SSH key to the ssh-agent to manage your keys, you should hav
> ```powershell
> git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
> ```
>
> You may need to specify which `ssh-keygen` binary Git should use to avoid conflicts with the binary bundled with Git for Windows. To define which binary is used, run the following command:
>
> ```powershell
> git config --global gpg.ssh.program "C:/Windows/System32/OpenSSH/ssh-keygen.exe"
> ```
>
> Alternatively, you can reinstall Git for Windows and select the **Use external OpenSSH** option during the installation process.


{% endwindows %}
Expand Down
14 changes: 14 additions & 0 deletions content/copilot/how-tos/copilot-integrations/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: GitHub Copilot integrations
shortTitle: Copilot integrations
intro: Learn how to use {% data variables.copilot.copilot_cloud_agent %} from the chat and project management tools where you already work.
versions:
feature: copilot
children:
- /integrate-cloud-agent-with-slack
- /integrate-cloud-agent-with-teams
- /integrate-cloud-agent-with-jira
- /integrate-cloud-agent-with-linear
- /integrate-cloud-agent-with-azure-boards
contentType: how-tos
---
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contentType: how-tos
category:
- Integrate Copilot with your tools
redirect_from:
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-azure-boards
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-azure-boards
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ product: '{% data reusables.copilot.plans.permission-paid-plans-cfi %}'
versions:
feature: copilot
redirect_from:
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-jira
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-jira
- /early-access/copilot/integrate-coding-agent-with-jira
contentType: how-tos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contentType: how-tos
category:
- Integrate Copilot with your tools
redirect_from:
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-linear
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-linear
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contentType: how-tos
category:
- Integrate Copilot with your tools
redirect_from:
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-slack
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-slack
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contentType: how-tos
category:
- Integrate Copilot with your tools
redirect_from:
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-teams
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-teams
---

Expand Down
1 change: 1 addition & 0 deletions content/copilot/how-tos/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ children:
- /copilot-sdk
- /github-agentic-workflows
- /use-copilot-agents
- /copilot-integrations
- /use-ai-models
- /provide-context
- /configure-custom-instructions-in-your-ide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ children:
- /use-cloud-agent-via-the-api
- /use-cloud-agent-from-cli
- /use-cloud-agent-with-mcp
- /integrate-cloud-agent-with-jira
- /integrate-cloud-agent-with-slack
- /integrate-cloud-agent-with-teams
- /integrate-cloud-agent-with-linear
- /integrate-cloud-agent-with-azure-boards
- /use-cloud-agent-from-raycast
- /troubleshoot-cloud-agent
redirect_from:
Expand Down
13 changes: 13 additions & 0 deletions src/graphql/data/fpt/changelog.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
[
{
"schemaChanges": [
{
"title": "The GraphQL schema includes these changes:",
"changes": [
"<p>Enum value 'SECURITY_KEY<code>was removed from enum</code>ProofOfPresenceRequirement'</p>"
]
}
],
"previewChanges": [],
"upcomingChanges": [],
"date": "2026-08-17"
},
{
"schemaChanges": [
{
Expand Down
4 changes: 0 additions & 4 deletions src/graphql/data/fpt/schema-enterprise-admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -11825,10 +11825,6 @@
{
"name": "REAUTH",
"description": "<p>Members must complete a fresh re-authentication against the enterprise identity provider.</p>"
},
{
"name": "SECURITY_KEY",
"description": "<p>Members must satisfy a phishing-resistant security key re-authentication (Microsoft Entra only).</p>"
}
],
"category": "enterprise-admin"
Expand Down
5 changes: 0 additions & 5 deletions src/graphql/data/fpt/schema.docs.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -44014,11 +44014,6 @@ enum ProofOfPresenceRequirement @docsCategory(name: "enterprise-admin") {
Members must complete a fresh re-authentication against the enterprise identity provider.
"""
REAUTH

"""
Members must satisfy a phishing-resistant security key re-authentication (Microsoft Entra only).
"""
SECURITY_KEY
}

"""
Expand Down
4 changes: 0 additions & 4 deletions src/graphql/data/ghec/schema-enterprise-admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -11825,10 +11825,6 @@
{
"name": "REAUTH",
"description": "<p>Members must complete a fresh re-authentication against the enterprise identity provider.</p>"
},
{
"name": "SECURITY_KEY",
"description": "<p>Members must satisfy a phishing-resistant security key re-authentication (Microsoft Entra only).</p>"
}
],
"category": "enterprise-admin"
Expand Down
5 changes: 0 additions & 5 deletions src/graphql/data/ghec/schema.docs.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -44014,11 +44014,6 @@ enum ProofOfPresenceRequirement @docsCategory(name: "enterprise-admin") {
Members must complete a fresh re-authentication against the enterprise identity provider.
"""
REAUTH

"""
Members must satisfy a phishing-resistant security key re-authentication (Microsoft Entra only).
"""
SECURITY_KEY
}

"""
Expand Down
Loading