Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .github/policies/prs.external-reviewers.generated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This file is generated by the sync-labels script DO NOT EDIT manually
# cspell:ignore steverice swatkatz
id: prs.external-reviewers
name: Notify external area owners on PRs
description: GitHub doesn't allow CODEOWNERS entries or formal review requests
for users that are not repository collaborators, but some areas are
contributed by external teams. Mention those owners in a comment instead.
https://github.com/orgs/community/discussions/23042
resource: repository
disabled: false
configuration:
resourceManagementConfiguration:
eventResponderTasks:
- if:
- payloadType: Pull_Request
then:
- if:
- includesModifiedFiles:
files:
- packages/graphql/**/*
- not:
hasLabel:
label: emitter:graphql
then:
- addReply:
reply: "@steverice @swatkatz — this PR modifies files in the `emitter:graphql`
area, which your team owns. Please take a look. (You can't
be added as a formal reviewer because you're not a
repository collaborator, so this is a heads-up instead.)"
21 changes: 0 additions & 21 deletions .github/policies/prs.external-reviewers.yml

This file was deleted.

12 changes: 12 additions & 0 deletions eng/common/config/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ export const AreaPaths: Record<keyof typeof AreaLabels, string[]> = {
spector: ["packages/spector/", "packages/http-specs"],
};

/**
* External area owners: teams/users outside the org that should be notified on PRs
* touching their area. GitHub does not allow non-collaborators in CODEOWNERS, nor can
* they be added as formal reviewers, so they are @-mentioned in a comment instead.
*
* Keyed by the same area names as {@link AreaPaths}; the paths are reused from there.
*/
export const ExternalOwners: Partial<Record<keyof typeof AreaLabels, string[]>> = {
// cspell:ignore swatkatz
"emitter:graphql": ["steverice", "swatkatz"],
};

/**
* Path that should trigger every CI build.
*/
Expand Down
3 changes: 2 additions & 1 deletion eng/common/config/labels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// cspell:ignore bfff
import { defineConfig, defineLabels } from "../scripts/labels/config.js";
import { repo } from "../scripts/utils/common.js";
import { AreaPaths } from "./area.js";
import { AreaPaths, ExternalOwners } from "./area.js";

/**
* Labels that are used to categorize issue for which area they belong to.
Expand Down Expand Up @@ -230,4 +230,5 @@ export default defineConfig({
},
},
areaPaths: AreaPaths,
externalOwners: ExternalOwners,
});
61 changes: 59 additions & 2 deletions eng/common/scripts/labels/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CheckOptions, syncFile } from "../utils/common.js";
import { expandFolder } from "../utils/find-area-changed.js";
import {
PolicyServiceConfig,
addReply,
and,
eventResponderTask,
hasLabel,
Expand All @@ -24,15 +25,35 @@ export interface SyncLabelAutomationOptions extends CheckOptions {}
export async function syncLabelAutomation(config: RepoConfig, options: SyncLabelAutomationOptions) {
await syncPolicyFile(createIssueTriageConfig(config), options);
await syncPolicyFile(createPrTriageConfig(config), options);
await syncPolicyFile(
createExternalReviewersConfig(config),
options,
collectExternalOwners(config),
);
}

async function syncPolicyFile(policy: PolicyServiceConfig, options: CheckOptions) {
async function syncPolicyFile(
policy: PolicyServiceConfig,
options: CheckOptions,
ignoreWords: string[] = [],
) {
const yaml = stringify(policy);
const content = `# This file is generated by the sync-labels script DO NOT EDIT manually\n${yaml}`;
const cspell = ignoreWords.length > 0 ? `# cspell:ignore ${ignoreWords.join(" ")}\n` : "";
const content = `# This file is generated by the sync-labels script DO NOT EDIT manually\n${cspell}${yaml}`;
const filename = resolve(policyFolder, `${policy.id}.generated.yml`);
await syncFile(filename, content, options);
}

function collectExternalOwners(config: RepoConfig): string[] {
const all = new Set<string>();
for (const owners of Object.values(config.externalOwners)) {
for (const owner of owners) {
all.add(owner);
}
}
return [...all].sort();
}

function createIssueTriageConfig(config: RepoConfig): PolicyServiceConfig {
const areaLabels = config.labels.area.labels;
return {
Expand Down Expand Up @@ -129,3 +150,39 @@ function createPrTriageConfig(config: RepoConfig): PolicyServiceConfig {
},
};
}
function createExternalReviewersConfig(config: RepoConfig): PolicyServiceConfig {
const entries = Object.entries(config.externalOwners).filter(([, owners]) => owners.length > 0);
return {
id: "prs.external-reviewers",
name: "Notify external area owners on PRs",
description:
"GitHub doesn't allow CODEOWNERS entries or formal review requests for users that are not repository collaborators, but some areas are contributed by external teams. Mention those owners in a comment instead. https://github.com/orgs/community/discussions/23042",
resource: "repository",
disabled: false,
configuration: {
resourceManagementConfiguration: {
eventResponderTasks: [
eventResponderTask({
if: [payloadType("Pull_Request")],
then: entries.map(([area, owners]) => {
const globs = (config.areaPaths[area] ?? []).map(expandFolder);
const mentions = owners.map((owner) => `@${owner}`).join(" ");
// The area label (added by the prs.triage policy when these paths are
// modified) persists across events, so it doubles as the "already notified"
// guard: owners are pinged the first time the paths match and not on every
// subsequent push.
return {
if: [includesModifiedFiles(globs), not(hasLabel(area))],
then: [
addReply(
`${mentions} — this PR modifies files in the \`${area}\` area, which your team owns. Please take a look. (You can't be added as a formal reviewer because you're not a repository collaborator, so this is a heads-up instead.)`,
),
],
};
}),
}),
],
},
},
};
}
20 changes: 20 additions & 0 deletions eng/common/scripts/labels/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,23 @@ export type EventResponderTask = {
export function eventResponderTask(options: EventResponderTask): EventResponderTask {
return options;
}

export type AddReply = {
addReply: { reply: string };
};

/**
* Post a comment on the issue/PR. @-mentions in the body notify the mentioned users,
* including users that are not repository collaborators.
*/
export function addReply(reply: string): AddReply {
return { addReply: { reply } };
}

export type AddLabel = {
addLabel: { label: string };
};

export function addLabel(label: string): AddLabel {
return { addLabel: { label } };
}
1 change: 1 addition & 0 deletions eng/common/scripts/labels/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface RepoConfig {
};
labels: LabelsConfig;
areaPaths: Record<string, string[]>;
externalOwners: Record<string, string[]>;
}

export interface LabelsConfig {
Expand Down
Loading