From 7e908d5de6697ca270f7a2d7500b68df4820fe33 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:39:19 -0400 Subject: [PATCH 1/4] feat: add include/exclude repo filters to team settings schema Backport of PR #1009 (github-community-projects/safe-settings) onto the yadhav/fix-recent-issues line. Team entries are filtered by the same Diffable include/exclude logic that collaborators use, but the team schema never declared these keys, so editors and linters couldn't validate them. Mirror the collaborators allOf pattern to declare include/exclude on the teams items, rebuild the dereferenced schema, document both in the teams guide, add a sample, and cover the filter path with unit tests. No runtime changes -- filtering already works via Diffable.filterEntries. Adapted to this branch: schemas are consolidated in schema/settings.json (no separate repos/suborgs schema files), teams is defined inline, and team tests mock github.teams/github.repos (not github.rest.*). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5dac2ffd-8dc2-45b4-89c9-789f103d2be8 --- docs/github-settings/4. teams.md | 28 ++++ docs/sample-settings/settings.yml | 10 ++ schema/dereferenced/settings.json | 200 ++++++++++++++++++++-------- schema/settings.json | 25 +++- test/unit/lib/plugins/teams.test.js | 47 +++++++ 5 files changed, 252 insertions(+), 58 deletions(-) diff --git a/docs/github-settings/4. teams.md b/docs/github-settings/4. teams.md index 565398a1e..523bc49c8 100644 --- a/docs/github-settings/4. teams.md +++ b/docs/github-settings/4. teams.md @@ -61,5 +61,33 @@ teams: external_group: "Engineering - Expert Services" ``` + + +

excludearray

+

Exclude a list of repos for this team. The team is applied to every repo in scope except those whose names match one of these glob patterns.

+ + +```yaml +teams: + - name: SuperFriends + permission: maintain + exclude: + - secret-repo +``` + + + +

includearray

+

Include a list of repos for this team. The team is applied only to repos whose names match one of these glob patterns.

+ + +```yaml +teams: + - name: SuperFriends + permission: maintain + include: + - public-* +``` + diff --git a/docs/sample-settings/settings.yml b/docs/sample-settings/settings.yml index 85f50df9d..7f57d55a7 100644 --- a/docs/sample-settings/settings.yml +++ b/docs/sample-settings/settings.yml @@ -161,6 +161,16 @@ teams: - name: globalteam permission: push visibility: closed + - name: docs-team + permission: pull + # You can include a list of repos for this team and only those repos would have this team + include: + - actions-demo + - name: ops-team + permission: push + # You can exclude a list of repos for this team and all repos except these repos would have this team + exclude: + - actions-demo # Branch protection rules # See https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2022-11-28#update-branch-protection for available options diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index d4955ad26..8b886358d 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -419,62 +419,85 @@ "description": "Teams", "type": "array", "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the team." - }, - "description": { - "type": "string", - "description": "The description of the team." - }, - "maintainers": { - "type": "array", - "description": "List GitHub usernames for organization members who will become team maintainers.", - "items": { - "type": "string" - } - }, - "repo_names": { - "type": "array", - "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", - "items": { - "type": "string" - } - }, - "privacy": { - "type": "string", - "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", - "enum": [ - "secret", - "closed" - ] - }, - "notification_setting": { - "type": "string", - "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", - "enum": [ - "notifications_enabled", - "notifications_disabled" + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "maintainers": { + "type": "array", + "description": "List GitHub usernames for organization members who will become team maintainers.", + "items": { + "type": "string" + } + }, + "repo_names": { + "type": "array", + "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", + "enum": [ + "secret", + "closed" + ] + }, + "notification_setting": { + "type": "string", + "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", + "enum": [ + "notifications_enabled", + "notifications_disabled" + ] + }, + "permission": { + "type": "string", + "description": "**Closing down notice**. The permission that new repositories will be added to the team with when none is specified.", + "enum": [ + "pull", + "push" + ], + "default": "pull" + }, + "parent_team_id": { + "type": "integer", + "description": "The ID of a team to set as the parent team." + } + }, + "required": [ + "name" ] }, - "permission": { - "type": "string", - "description": "**Closing down notice**. The permission that new repositories will be added to the team with when none is specified.", - "enum": [ - "pull", - "push" - ], - "default": "pull" - }, - "parent_team_id": { - "type": "integer", - "description": "The ID of a team to set as the parent team." + { + "type": "object", + "properties": { + "exclude": { + "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this team and only those repos would have this team", + "type": "array", + "items": { + "type": "string" + } + } + } } - }, - "required": [ - "name" ] } }, @@ -1223,6 +1246,49 @@ "type": "boolean", "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." }, + "dismissal_restriction": { + "title": "DismissalRestriction", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "type": "object", + "properties": { + "allowed_actors": { + "type": "array", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "items": { + "title": "Actor", + "description": "An actor allowed to dismiss pull request reviews", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the actor that can dismiss reviews." + }, + "type": { + "type": "string", + "description": "The type of the actor", + "enum": [ + "User", + "Team", + "IntegrationInstallation", + "RepositoryRole" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "enabled": { + "type": "boolean", + "description": "Whether to restrict review dismissal to specific actors." + } + }, + "required": [ + "enabled" + ] + }, "require_code_owner_review": { "type": "boolean", "description": "Require an approving review in pull requests that modify files that have a designated code owner." @@ -1937,8 +2003,25 @@ } } }, + "app_installations": { + "description": "Manage which repositories a GitHub App installation can access. The target is a GitHub App installation rather than a repository. Repo selection follows the config hierarchy: org-level settings.yml selects all repos in the org; suborgs/*.yml selects repos by the suborg's targeting criteria; repos/*.yml adds the specific repo. Requires safe-settings to be installed on the enterprise with 'Enterprise organization installations' permission.", + "type": "array", + "items": { + "type": "object", + "required": [ + "app_slug" + ], + "additionalProperties": false, + "properties": { + "app_slug": { + "type": "string", + "description": "The slug of the GitHub App installation to manage." + } + } + } + }, "additive_plugins": { - "description": "List of Diffable plugins to run in additive mode. In additive mode the plugin will only add and update entries; it will never call remove(), so items that exist on GitHub but are absent from the YAML are preserved. Only Diffable-extending plugins are supported (labels, collaborators, teams, milestones, autolinks, environments, custom_properties, variables, rulesets, custom_repository_roles). Declare only in settings.yml (org level) to keep behavior consistent across all repos.", + "description": "List of plugins to run in additive mode. In additive mode the plugin will only add and update entries; it will never call remove(), so items that exist on GitHub but are absent from the YAML are preserved. Supported plugins: labels, collaborators, teams, milestones, autolinks, environments, custom_properties, variables, rulesets, custom_repository_roles, app_installations. Declare only in settings.yml (org level) to keep behavior consistent across all repos.", "type": "array", "items": { "type": "string", @@ -1952,7 +2035,8 @@ "custom_properties", "variables", "rulesets", - "custom_repository_roles" + "custom_repository_roles", + "app_installations" ] } }, @@ -1977,7 +2061,8 @@ "custom_properties", "custom_repository_roles", "variables", - "archive" + "archive", + "app_installations" ] }, { @@ -2003,7 +2088,8 @@ "custom_properties", "custom_repository_roles", "variables", - "archive" + "archive", + "app_installations" ] }, "target": { diff --git a/schema/settings.json b/schema/settings.json index 02c8b36fd..941f6cc5d 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -136,7 +136,30 @@ "description": "Teams", "type": "array", "items": { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "exclude": { + "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this team and only those repos would have this team", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] } }, "branches": { diff --git a/test/unit/lib/plugins/teams.test.js b/test/unit/lib/plugins/teams.test.js index 8879611c3..f7c599d8b 100644 --- a/test/unit/lib/plugins/teams.test.js +++ b/test/unit/lib/plugins/teams.test.js @@ -97,6 +97,53 @@ describe('Teams', () => { } }) + describe('filtering teams by include/exclude', () => { + beforeEach(() => { + github.repos.listTeams.mockResolvedValue({ data: [] }) + }) + + it('does not add a team when the repo matches an exclude glob', async () => { + const plugin = configure([ + { name: addedTeamName, permission: 'pull', exclude: ['test*'] } + ]) + + await plugin.sync() + + expect(github.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + }) + + it('does not add a team when the repo is not in an include glob', async () => { + const plugin = configure([ + { name: addedTeamName, permission: 'pull', include: ['other-*'] } + ]) + + await plugin.sync() + + expect(github.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + }) + + it('adds a team when the repo matches an include glob', async () => { + when(github.teams.getByName) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin = configure([ + { name: addedTeamName, permission: 'pull', include: ['test*'] } + ]) + + await plugin.sync() + + expect(github.teams.addOrUpdateRepoPermissionsInOrg).toHaveBeenCalledWith({ + org, + team_id: addedTeamId, + team_slug: addedTeamName, + owner: org, + repo: 'test', + permission: 'pull' + }) + }) + }) + describe('external_group linking', () => { const externalGroupName = 'Engineering - Expert Services' const externalGroupId = 42 From 0d3bc76f190ac6883125fc2cf7585bdc4178a2a4 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:46:17 -0400 Subject: [PATCH 2/4] test(smoke): add Phase 18 for team include/exclude repo filters Exercises the backported team include/exclude schema end-to-end: creates a repo via a repo-level config whose team entries carry include/exclude globs and asserts safe-settings applies only the team whose include glob matches the repo (and skips the excluded / non-matching teams). Self-contained so it runs alone via `--phase 18`; teardown cleans up the repo and teams. Verified live against org decyjphr-emu: 7/7 assertions passed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5dac2ffd-8dc2-45b4-89c9-789f103d2be8 --- smoke-test.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/smoke-test.js b/smoke-test.js index 2b4d9dba7..5770e30b5 100644 --- a/smoke-test.js +++ b/smoke-test.js @@ -70,8 +70,11 @@ const CONFIG_PATH = process.env.CONFIG_PATH || '.github' const APP_ID = process.env.APP_ID const PRIVATE_KEY = (process.env.PRIVATE_KEY || '').replace(/\\n/g, '\n') -const TEST_REPOS = ['test', 'demo-repo-service1', 'demo-repo-service2', 'combined-settings-repo'] +const TEST_REPOS = ['test', 'demo-repo-service1', 'demo-repo-service2', 'combined-settings-repo', 'smoke-team-filter'] const TEST_TEAMS = ['AD-GRP-PAYMENTS-PLATFORM-OWNERS', 'awesometeam-a-approvers', 'jefeish-edj-test'] +// Teams exercised by the team include/exclude filter phase (Phase 18). +const SMOKE_FILTER_REPO = 'smoke-team-filter' +const SMOKE_FILTER_TEAMS = ['smoke-filter-included', 'smoke-filter-excluded', 'smoke-filter-nomatch'] // Principals created on demand for the ruleset name-resolution phase (Phase 16) const SMOKE_NR_TEAM = 'safe-settings-smoke-nr-team' @@ -1206,6 +1209,34 @@ const REPO_YML_NO_VARS = `repository: variables: [] ` +// Config for Phase 18: a repo-level config whose team entries carry +// include/exclude repo filters. Only the team whose include glob matches the +// repo name (or that is not excluded) should be applied by safe-settings. +// - smoke-filter-included: include matches -> applied +// - smoke-filter-excluded: exclude matches -> NOT applied +// - smoke-filter-nomatch : include does not match -> NOT applied +const REPO_TEAM_FILTER_YML = `repository: + name: ${SMOKE_FILTER_REPO} + description: Repo for team include/exclude smoke test + private: true + auto_init: true + force_create: true + +teams: + - name: ${SMOKE_FILTER_TEAMS[0]} + permission: pull + include: + - ${SMOKE_FILTER_REPO} + - name: ${SMOKE_FILTER_TEAMS[1]} + permission: pull + exclude: + - ${SMOKE_FILTER_REPO} + - name: ${SMOKE_FILTER_TEAMS[2]} + permission: pull + include: + - no-such-repo-* +` + // ─── Test Phases ───────────────────────────────────────────────────────────── async function setup () { @@ -2471,6 +2502,7 @@ async function teardown () { log('Deleting test teams...') for (const team of TEST_TEAMS) { await deleteTeam(ORG, team.toLowerCase()) } try { await deleteTeam(ORG, SMOKE_NR_TEAM) } catch { /* ok */ } + for (const team of SMOKE_FILTER_TEAMS) { try { await deleteTeam(ORG, team) } catch { /* ok */ } } log('Deleting custom repository role...') try { await deleteCustomRepositoryRole(ORG, 'security-engineer') } catch { /* ok */ } @@ -3095,6 +3127,61 @@ async function phase17AppInstallations () { // ─── Main ──────────────────────────────────────────────────────────────────── +async function phase18TeamIncludeExclude () { + logPhase('Phase 18: Team include/exclude repo filters') + const branch = 'smoke-test-phase18' + const defaultBranch = await getDefaultBranch() + + // Clean any leftover teams from a previous aborted run so the "not applied" + // assertions can't be satisfied by stale repo-team associations. + await deleteRepo(ORG, SMOKE_FILTER_REPO) + for (const t of SMOKE_FILTER_TEAMS) { await deleteTeam(ORG, t) } + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${SMOKE_FILTER_REPO}.yml`, REPO_TEAM_FILTER_YML, branch, 'Add team include/exclude filter config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: team include/exclude filters', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: SMOKE_FILTER_REPO })).data } catch { return null } + }, { desc: `${SMOKE_FILTER_REPO} to be created` }) + assert(repo !== null, `Repo "${SMOKE_FILTER_REPO}" was created`) + + // The included team should be applied (poll — safe-settings may still be working). + const includedTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: SMOKE_FILTER_REPO }) + return teams.find(t => t.slug === SMOKE_FILTER_TEAMS[0]) || null + } catch { return null } + }, { desc: `included team ${SMOKE_FILTER_TEAMS[0]} to be added`, timeout: 60000 }) + assert(includedTeam !== null, `Team "${SMOKE_FILTER_TEAMS[0]}" applied (include glob matches repo)`) + if (includedTeam) assert(includedTeam.permission === 'pull', `Included team has pull permission (got: ${includedTeam.permission})`) + + // The excluded and non-matching teams must NOT be applied. + const finalTeams = await (async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: SMOKE_FILTER_REPO }) + return teams.map(t => t.slug) + } catch { return [] } + })() + assert(!finalTeams.includes(SMOKE_FILTER_TEAMS[1]), `Team "${SMOKE_FILTER_TEAMS[1]}" NOT applied (exclude glob matches repo)`) + assert(!finalTeams.includes(SMOKE_FILTER_TEAMS[2]), `Team "${SMOKE_FILTER_TEAMS[2]}" NOT applied (include glob does not match repo)`) + + // Cleanup for standalone --phase 18 runs (teardown also cleans these). + await deleteRepo(ORG, SMOKE_FILTER_REPO) + for (const t of SMOKE_FILTER_TEAMS) { await deleteTeam(ORG, t) } + await deleteBranch(ORG, ADMIN_REPO, branch) +} + async function main () { const { App } = await import('octokit') const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY }) @@ -3162,7 +3249,8 @@ async function main () { ['Phase 14: regressions', phase14RegressionCoverage], ['Phase 15: Ruleset array drift', phase15RulesetArrayDrift], ['Phase 16: Ruleset name/slug resolution', phase16RulesetNameResolution], - ['Phase 17: App installation management', phase17AppInstallations] + ['Phase 17: App installation management', phase17AppInstallations], + ['Phase 18: Team include/exclude filters', phase18TeamIncludeExclude] ] // When --phase is given, only run setup (phase 0) + the requested phase(s). From 199809897732d2ccb2d58eb02a672ec9f1cb0060 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:03:49 -0400 Subject: [PATCH 3/4] fix(schema): use team-repo-permissions schema for team permission values The teams settings schema referenced the "Create a team" POST requestBody, whose `permission` enum is limited to pull/push. safe-settings actually grants repo access via the "Add or update team repository permissions" PUT endpoint, which supports triage/maintain/admin and custom repository roles. As a result, valid team entries (e.g. `permission: maintain`, matching the docs examples) were incorrectly rejected by schema validation. Switch teams.items to reference the team-repo-permissions PUT schema (where `permission` is an unconstrained string) and declare the plugin's supported keys locally: name (required), privacy, external_group, include, exclude. Also strengthen smoke Phase 18 to grant the included team `maintain` permission, proving a value the old schema rejected now validates and applies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5dac2ffd-8dc2-45b4-89c9-789f103d2be8 --- schema/dereferenced/settings.json | 67 +++++++++---------------------- schema/settings.json | 21 +++++++++- smoke-test.js | 7 +++- 3 files changed, 43 insertions(+), 52 deletions(-) diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index 8b886358d..4a0c48467 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -423,65 +423,34 @@ { "type": "object", "properties": { - "name": { - "type": "string", - "description": "The name of the team." - }, - "description": { + "permission": { "type": "string", - "description": "The description of the team." - }, - "maintainers": { - "type": "array", - "description": "List GitHub usernames for organization members who will become team maintainers.", - "items": { - "type": "string" - } - }, - "repo_names": { - "type": "array", - "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", - "items": { - "type": "string" - } + "description": "The permission to grant the team on this repository. We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any. If no permission is specified, the team's `permission` attribute will be used to determine what permission to grant the team on this repository." + } + } + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name (or slug) of the team to grant access to the repo.", + "type": "string" }, "privacy": { + "description": "The level of privacy the team should have when it is created by safe-settings. The options are `secret` (visible only to organization owners and members of the team) or `closed` (visible to all members of the organization).", "type": "string", - "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", "enum": [ "secret", "closed" ] }, - "notification_setting": { - "type": "string", - "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", - "enum": [ - "notifications_enabled", - "notifications_disabled" - ] - }, - "permission": { - "type": "string", - "description": "**Closing down notice**. The permission that new repositories will be added to the team with when none is specified.", - "enum": [ - "pull", - "push" - ], - "default": "pull" + "external_group": { + "description": "The display name of an external IdP group to link to this team.", + "type": "string" }, - "parent_team_id": { - "type": "integer", - "description": "The ID of a team to set as the parent team." - } - }, - "required": [ - "name" - ] - }, - { - "type": "object", - "properties": { "exclude": { "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", "type": "array", diff --git a/schema/settings.json b/schema/settings.json index 941f6cc5d..f4f277482 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -138,11 +138,30 @@ "items": { "allOf": [ { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1teams~1{team_slug}~1repos~1{owner}~1{repo}/put/requestBody/content/application~1json/schema" }, { "type": "object", + "required": [ + "name" + ], "properties": { + "name": { + "description": "The name (or slug) of the team to grant access to the repo.", + "type": "string" + }, + "privacy": { + "description": "The level of privacy the team should have when it is created by safe-settings. The options are `secret` (visible only to organization owners and members of the team) or `closed` (visible to all members of the organization).", + "type": "string", + "enum": [ + "secret", + "closed" + ] + }, + "external_group": { + "description": "The display name of an external IdP group to link to this team.", + "type": "string" + }, "exclude": { "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", "type": "array", diff --git a/smoke-test.js b/smoke-test.js index 5770e30b5..30ef1a643 100644 --- a/smoke-test.js +++ b/smoke-test.js @@ -1224,7 +1224,7 @@ const REPO_TEAM_FILTER_YML = `repository: teams: - name: ${SMOKE_FILTER_TEAMS[0]} - permission: pull + permission: maintain include: - ${SMOKE_FILTER_REPO} - name: ${SMOKE_FILTER_TEAMS[1]} @@ -3164,7 +3164,10 @@ async function phase18TeamIncludeExclude () { } catch { return null } }, { desc: `included team ${SMOKE_FILTER_TEAMS[0]} to be added`, timeout: 60000 }) assert(includedTeam !== null, `Team "${SMOKE_FILTER_TEAMS[0]}" applied (include glob matches repo)`) - if (includedTeam) assert(includedTeam.permission === 'pull', `Included team has pull permission (got: ${includedTeam.permission})`) + if (includedTeam) { + const hasMaintain = includedTeam.permission === 'maintain' || (includedTeam.permissions && includedTeam.permissions.maintain === true) + assert(hasMaintain, `Included team has maintain permission (got: ${includedTeam.permission})`) + } // The excluded and non-matching teams must NOT be applied. const finalTeams = await (async () => { From be51369753c7e15614996e52db765082c11c4166 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:04:48 -0400 Subject: [PATCH 4/4] docs(schema): clarify team include/exclude are repo-name glob patterns The previous descriptions were vague and did not mention that values are glob patterns matched against the repository name (as documented and as implemented by Diffable.filterEntries() via minimatch). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5dac2ffd-8dc2-45b4-89c9-789f103d2be8 --- schema/dereferenced/settings.json | 4 ++-- schema/settings.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index 4a0c48467..fd500ae60 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -452,14 +452,14 @@ "type": "string" }, "exclude": { - "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", + "description": "Exclude this team from repos whose names match one of these glob patterns.", "type": "array", "items": { "type": "string" } }, "include": { - "description": "You can include a list of repos for this team and only those repos would have this team", + "description": "Apply this team only to repos whose names match one of these glob patterns.", "type": "array", "items": { "type": "string" diff --git a/schema/settings.json b/schema/settings.json index f4f277482..2580ecff1 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -163,14 +163,14 @@ "type": "string" }, "exclude": { - "description": "You can exclude a list of repos for this team and all repos except these repos would have this team", + "description": "Exclude this team from repos whose names match one of these glob patterns.", "type": "array", "items": { "type": "string" } }, "include": { - "description": "You can include a list of repos for this team and only those repos would have this team", + "description": "Apply this team only to repos whose names match one of these glob patterns.", "type": "array", "items": { "type": "string"