Skip to content

Commit 3eb0158

Browse files
authored
Merge pull request #45525 from github/repo-sync
Repo sync
2 parents 24c61b6 + 3de47bd commit 3eb0158

14 files changed

Lines changed: 249 additions & 23 deletions
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
name: Restrict who can queue merges
2+
3+
# **What it does**:
4+
# On a `merge_group` event, checks whether the person who put the pull
5+
# request into the merge queue is on github/technical-content. If they
6+
# are not, comments on the pull request saying so and fails, which
7+
# ejects the entry from the queue.
8+
# **Why we have it**:
9+
# Classic branch protection used to restrict who could push to `main`,
10+
# but that rule was swept org-wide on 2026-06-22, so today anyone with
11+
# write access can merge. Rebuilding it means also enabling a merge
12+
# queue on the same rule, which could collide with the merge queue on
13+
# our ruleset and lock the branch for everyone. This does the same job
14+
# with machinery we own outright.
15+
# **Who does it impact**: Anyone merging to `main`.
16+
17+
# Two things to know before changing this:
18+
#
19+
# 1. `merge-queue-restriction` has to be a required status check on the ruleset targeting `refs/heads/main`, or this
20+
# enforces nothing. Add it there only after this workflow is on `main` and reporting. The other order makes the
21+
# check required before it has ever reported, which blocks every pull request. To turn enforcement off again,
22+
# remove it from the ruleset. This workflow keeps running and keeps passing.
23+
#
24+
# 2. The `pull_request` runs do no work. They exist so the required check reports a passing context on the pull
25+
# request itself. Drop them and the check sits pending forever and nothing can ever be enqueued.
26+
27+
on:
28+
pull_request:
29+
types: [opened, reopened, synchronize, ready_for_review]
30+
merge_group:
31+
32+
permissions:
33+
contents: read
34+
pull-requests: write
35+
36+
# Keyed on head SHA rather than pull request number. Webhook delivery order is not guaranteed, and keying on the pull
37+
# request would let a late event for an old SHA cancel the run for a newer one.
38+
concurrency:
39+
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
40+
cancel-in-progress: true
41+
42+
jobs:
43+
# The job id doubles as the check run name because this job deliberately has no `name:` key. Renaming this job renames
44+
# the required status check, which silently stops enforcing anything.
45+
merge-queue-restriction:
46+
# This repository syncs a subset of files to the public github/docs, including workflows. Nothing here applies there.
47+
if: github.repository == 'github/docs-internal'
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Check that the enqueuer is on the Technical Content team
51+
if: github.event_name == 'merge_group'
52+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
53+
with:
54+
# Reading org team membership needs `read:org`, which GITHUB_TOKEN does not have. `merge_group` always runs in
55+
# the base repository, so this secret is always available here, including for pull requests from forks.
56+
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
57+
script: |
58+
// Addressed by numeric ID (org github = 9919, team technical-content = 325922) because IDs survive renames
59+
// and slugs do not. This team was called `docs` until recently and the rename broke a pile of automation.
60+
const ORG_ID = 9919
61+
const TEAM_ID = 325922
62+
const TEAM = 'github/technical-content'
63+
const MARKER = '<!-- merge-queue-restriction -->'
64+
const EXEMPT_USERS = ['docs-bot']
65+
const CONTENT_SLACK_CHANNEL = 'C0E9DK082'
66+
const MAX_ATTEMPTS = 3
67+
68+
// `github.actor` is the person who enqueued. On a re-run it stays the original actor, unlike
69+
// `github.triggering_actor`, so re-running cannot launder a failing check into a passing one.
70+
const actor = context.actor
71+
core.info(`This merge group was queued by @${actor}.`)
72+
73+
// A GitHub App actor always ends in `[bot]`, and `[` is not a valid character in a username, so nobody can
74+
// impersonate one. `docs-bot` is a plain User account and has to be named explicitly.
75+
core.info(`Checking whether @${actor} is an automation account...`)
76+
if (actor.endsWith('[bot]') || EXEMPT_USERS.includes(actor)) {
77+
core.info(`Checked: @${actor} is an automation account. Allowing the merge.`)
78+
return
79+
}
80+
core.info(`Checked: @${actor} is a person, so they need to be on the team.`)
81+
82+
// Every request retries transient failures before giving up, then fails closed. Failing closed is safe
83+
// here: github/technical-content is an `always` bypass actor on the ruleset, so a broken check stops
84+
// non-Docs merges but never stops Docs. A 404 comes back as null data rather than as an error, because on
85+
// both of the endpoints below it is an answer rather than a failure.
86+
async function ask(description, route, params) {
87+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
88+
core.info(`${description} (attempt ${attempt} of ${MAX_ATTEMPTS})...`)
89+
try {
90+
const { data } = await github.request(route, params)
91+
return { data }
92+
} catch (error) {
93+
if (error.status === 404) return { data: null }
94+
if (attempt === MAX_ATTEMPTS) return { error }
95+
96+
const seconds = attempt * 2
97+
core.warning(`Asked and failed with HTTP ${error.status}. Retrying in ${seconds}s.`)
98+
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
99+
}
100+
}
101+
}
102+
103+
// Deliberately says nothing on the pull request. We only comment when we know the answer, and here we
104+
// do not.
105+
function giveUp(detail) {
106+
core.setFailed(`${detail} Failing closed, so this stays out of the queue. Ask in #docs-content.`)
107+
}
108+
109+
// Checking the parent team is enough. Every member of every child team (docs-content, docs-engineering,
110+
// docs-localization, docs-content-systems, docs-product-managers, docs-open-source, docs-design,
111+
// docs-content-design, copilot-docs) also resolves as a member of the parent.
112+
const membershipResult = await ask(
113+
`Asking the API whether @${actor} is on ${TEAM}`,
114+
'GET /organizations/{org_id}/team/{team_id}/memberships/{username}',
115+
{ org_id: ORG_ID, team_id: TEAM_ID, username: actor },
116+
)
117+
118+
if (membershipResult.error) {
119+
giveUp(
120+
`Could not check ${TEAM} membership for @${actor}. ` +
121+
`The last attempt returned HTTP ${membershipResult.error.status}.`,
122+
)
123+
return
124+
}
125+
126+
const membership = membershipResult.data
127+
128+
if (membership) {
129+
core.info(`Asked: @${actor} has membership state "${membership.state}" on ${TEAM}.`)
130+
} else {
131+
core.info(`Asked: the API reports no membership for @${actor} on ${TEAM} (HTTP 404).`)
132+
133+
// That 404 is ambiguous. It is byte for byte the same response for "not a member", "team no longer
134+
// exists", and "the token lost visibility into the org". Read the team back before believing it,
135+
// otherwise a deleted team or a downgraded token would blame every single person who tries to merge.
136+
const teamResult = await ask(
137+
`A 404 is ambiguous, so reading ${TEAM} back to confirm it is still visible`,
138+
'GET /organizations/{org_id}/team/{team_id}',
139+
{ org_id: ORG_ID, team_id: TEAM_ID },
140+
)
141+
142+
if (teamResult.error || !teamResult.data) {
143+
giveUp(
144+
`Could not read ${TEAM} itself, so the 404 for @${actor} says nothing about their membership. ` +
145+
`The last attempt returned HTTP ${teamResult.error?.status ?? 404}. ` +
146+
'Either the team is gone or this token lost access to it.',
147+
)
148+
return
149+
}
150+
151+
core.info(`Read it back: ${TEAM} is visible as "${teamResult.data.slug}", so the 404 is a real answer.`)
152+
}
153+
154+
if (membership?.state === 'active') {
155+
core.info(`@${actor} is an active member of ${TEAM}. Allowing the merge.`)
156+
return
157+
}
158+
159+
const reason = membership
160+
? `@${actor} has a "${membership.state}" membership on ${TEAM} rather than an active one.`
161+
: `@${actor} is not a member of ${TEAM}.`
162+
core.info(`Blocking the merge. ${reason}`)
163+
164+
// A failed check on a `gh-readonly-queue` ref is not something anyone goes looking for, so say why on the
165+
// pull request itself. The merge group ref is `refs/heads/gh-readonly-queue/<base>/pr-<number>-<sha>`, and
166+
// the pull request it names is the one this actor just enqueued, so the number and the actor correspond.
167+
const ref = context.payload.merge_group?.head_ref ?? context.ref
168+
core.info(`Working out which pull request this merge group is for, from "${ref}"...`)
169+
const number = Number(ref.match(/\/pr-(\d+)-[0-9a-f]+$/)?.[1])
170+
171+
if (!number) {
172+
core.warning(`Worked it out: could not find a pull request number in "${ref}". Skipping the comment.`)
173+
} else {
174+
core.info(`Worked it out: this merge group is for #${number}.`)
175+
176+
// Only comment once. Someone who tries to enqueue again already has the explanation, and repeating it
177+
// turns a useful comment into noise.
178+
core.info(`Reading the existing comments on #${number}...`)
179+
const comments = await github.paginate(github.rest.issues.listComments, {
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
issue_number: number,
183+
per_page: 100,
184+
})
185+
core.info(`Read ${comments.length} comment(s) on #${number}.`)
186+
187+
if (comments.some((comment) => comment.body?.includes(MARKER))) {
188+
core.info(`#${number} already has this explanation, so not commenting again.`)
189+
} else {
190+
core.info(`Commenting on #${number} to explain...`)
191+
await github.rest.issues.createComment({
192+
owner: context.repo.owner,
193+
repo: context.repo.repo,
194+
issue_number: number,
195+
body: [
196+
// The marker has to be on its own line. GitHub parses `<!--` at the start of a block as an
197+
// HTML block that runs to the end of the line containing `-->`, so anything sharing that
198+
// line renders as literal text: no code spans, no links.
199+
MARKER,
200+
[
201+
`👋 Hi @${actor}, this pull request was removed from the merge queue.`,
202+
'Only the GitHub Technical Content team merges to `main` in this repository.',
203+
'Once this is reviewed and ready, ask in',
204+
`[#docs-content](https://github.slack.com/archives/${CONTENT_SLACK_CHANNEL})`,
205+
'and someone on the team can merge it for you.',
206+
].join(' '),
207+
].join('\n'),
208+
})
209+
core.info(`Commented on #${number}.`)
210+
}
211+
}
212+
213+
core.setFailed(
214+
`Only ${TEAM} merges to main in this repository. ${reason} ` +
215+
'Ask in #docs-content and someone on the team can merge this for you.',
216+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: GitHub Copilot integrations
3+
shortTitle: Copilot integrations
4+
intro: Learn how to use {% data variables.copilot.copilot_cloud_agent %} from the chat and project management tools where you already work.
5+
versions:
6+
feature: copilot
7+
children:
8+
- /integrate-cloud-agent-with-slack
9+
- /integrate-cloud-agent-with-teams
10+
- /integrate-cloud-agent-with-jira
11+
- /integrate-cloud-agent-with-linear
12+
- /integrate-cloud-agent-with-azure-boards
13+
contentType: how-tos
14+
---

content/copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-azure-boards.md renamed to content/copilot/how-tos/copilot-integrations/integrate-cloud-agent-with-azure-boards.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ contentType: how-tos
1010
category:
1111
- Integrate Copilot with your tools
1212
redirect_from:
13+
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-azure-boards
1314
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-azure-boards
1415
---
1516

content/copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-jira.md renamed to content/copilot/how-tos/copilot-integrations/integrate-cloud-agent-with-jira.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ product: '{% data reusables.copilot.plans.permission-paid-plans-cfi %}'
77
versions:
88
feature: copilot
99
redirect_from:
10+
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-jira
1011
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-jira
1112
- /early-access/copilot/integrate-coding-agent-with-jira
1213
contentType: how-tos

content/copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-linear.md renamed to content/copilot/how-tos/copilot-integrations/integrate-cloud-agent-with-linear.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ contentType: how-tos
1010
category:
1111
- Integrate Copilot with your tools
1212
redirect_from:
13+
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-linear
1314
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-linear
1415
---
1516

content/copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-slack.md renamed to content/copilot/how-tos/copilot-integrations/integrate-cloud-agent-with-slack.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ contentType: how-tos
1010
category:
1111
- Integrate Copilot with your tools
1212
redirect_from:
13+
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-slack
1314
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-slack
1415
---
1516

content/copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-teams.md renamed to content/copilot/how-tos/copilot-integrations/integrate-cloud-agent-with-teams.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ contentType: how-tos
1010
category:
1111
- Integrate Copilot with your tools
1212
redirect_from:
13+
- /copilot/how-tos/use-copilot-agents/cloud-agent/integrate-cloud-agent-with-teams
1314
- /copilot/how-tos/use-copilot-agents/coding-agent/integrate-coding-agent-with-teams
1415
---
1516

content/copilot/how-tos/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ children:
1515
- /copilot-sdk
1616
- /github-agentic-workflows
1717
- /use-copilot-agents
18+
- /copilot-integrations
1819
- /use-ai-models
1920
- /provide-context
2021
- /configure-custom-instructions-in-your-ide

content/copilot/how-tos/use-copilot-agents/cloud-agent/index.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ children:
1919
- /use-cloud-agent-via-the-api
2020
- /use-cloud-agent-from-cli
2121
- /use-cloud-agent-with-mcp
22-
- /integrate-cloud-agent-with-jira
23-
- /integrate-cloud-agent-with-slack
24-
- /integrate-cloud-agent-with-teams
25-
- /integrate-cloud-agent-with-linear
26-
- /integrate-cloud-agent-with-azure-boards
2722
- /use-cloud-agent-from-raycast
2823
- /troubleshoot-cloud-agent
2924
redirect_from:

src/graphql/data/fpt/changelog.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
[
2+
{
3+
"schemaChanges": [
4+
{
5+
"title": "The GraphQL schema includes these changes:",
6+
"changes": [
7+
"<p>Enum value 'SECURITY_KEY<code>was removed from enum</code>ProofOfPresenceRequirement'</p>"
8+
]
9+
}
10+
],
11+
"previewChanges": [],
12+
"upcomingChanges": [],
13+
"date": "2026-08-17"
14+
},
215
{
316
"schemaChanges": [
417
{

0 commit comments

Comments
 (0)