Skip to content
Open
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
2 changes: 1 addition & 1 deletion examples/single-repo-team-bot/.paseo/workflows/github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on: github.issue_comment
max_runtime: 2h
filters:
repo: your-org/your-repo
from_users: [your-github-login]
from_teams: [your-org/your-team]
contains: "@your-bot"
steps:
- id: classify
Expand Down
9 changes: 6 additions & 3 deletions examples/single-repo-team-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ Copy `.paseo` to your repository root, then replace these values:
| `your-github-connection` | GitHub connection slug in Hub |
| `YOUR_DISCORD_USER_ID` | Discord user allowed to trigger runs |
| `YOUR_SLACK_USER_ID` | Slack user allowed to trigger runs |
| `your-github-login` | GitHub user allowed to trigger runs |
| `your-org/your-team` | GitHub team allowed to trigger runs |
| `@your-bot` | Mention that starts the GitHub workflow |

Connect Discord, Slack, and GitHub to the project before enabling their triggers. Keep the user
allowlists narrow; wildcards are not supported.
Connect Discord, Slack, and GitHub to the project before enabling their triggers. GitHub team
filters use `organization/team-slug` and require the GitHub App's organization **Members**
permission with read access. If membership cannot be checked, Hub does not start a run. A GitHub
`from_users` allowlist can be combined with `from_teams`; either grants access. Keep allowlists
Comment thread
michaelmwu marked this conversation as resolved.
narrow; wildcards are not supported.

## Deploy

Expand Down
39 changes: 39 additions & 0 deletions src/config/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,45 @@ describe("workflow compiler", () => {
});
});

it("supports GitHub team trigger allowlists and rejects invalid team filter uses", () => {
const raw = configuration();
const trigger = raw.triggers[0]!;
const githubTrigger = {
...trigger,
on: "github.issue_comment",
filters: { from_teams: ["getpaseo/maintainers"] },
};

assert.deepEqual(compileHubConfig({ ...raw, triggers: [githubTrigger] }).triggers[0]?.filters, {
from_teams: ["getpaseo/maintainers"],
});
assert.throws(
() =>
compileHubConfig({
...raw,
triggers: [{ ...trigger, on: "slack.mention", filters: githubTrigger.filters }],
}),
/filters\.from_teams only for GitHub events/iu,
);
assert.throws(
() =>
compileHubConfig({
...raw,
triggers: [
{
...githubTrigger,
filters: { from_teams: ["maintainers"] },
},
],
}),
/organization\/team-slug/iu,
);
assert.throws(
() => compileHubConfig({ ...raw, triggers: [{ ...trigger, on: "github.issue_comment" }] }),
/filters\.from_users or filters\.from_teams/iu,
);
});

it("allows a project-scoped Linear scout but keeps reactive Linear triggers actor-allowlisted", () => {
const raw = configuration();
const trigger = raw.triggers[0]!;
Expand Down
19 changes: 16 additions & 3 deletions src/config/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const EVENT_NAME = /^[a-z][a-z0-9_-]*\.[a-z][a-z0-9_-]*$/u;
const DURATION = /^([1-9][0-9]*)(ms|s|m|h)$/u;
const MAX_DURATION_MS = 24 * 60 * 60_000;
const INPUT_NAME = /^[a-z][a-z0-9_-]*$/u;
const GITHUB_TEAM_REFERENCE = /^[^/\s]+\/[^/\s]+$/u;
const DYNAMIC_INPUT_REFERENCE = /^\$\{\{\s*paseo\.inputs\.([a-z][a-z0-9_-]*)\s*\}\}$/u;
const EXPRESSION_START = "${{";
const EXPRESSION_END = "}}";
Expand Down Expand Up @@ -102,6 +103,9 @@ const AuthoredTriggerFilterSchema = z
assignees: z.array(z.string().min(1)).min(1).optional(),
channels: z.array(z.string().min(1)).optional(),
from_users: z.array(z.string().min(1)).optional(),
from_teams: z
.array(z.string().regex(GITHUB_TEAM_REFERENCE, "must be formatted as organization/team-slug"))
.optional(),
inputs: z.record(z.string(), InputValueSchema).optional(),
connection: z
.string()
Expand Down Expand Up @@ -264,10 +268,11 @@ export type CompiledSteps = readonly CompiledStep[];
export type CompiledTriggerFilter = Readonly<
Omit<
AuthoredTriggerFilter,
"channels" | "from_users" | "states" | "labels" | "exclude_labels" | "assignees"
"channels" | "from_users" | "from_teams" | "states" | "labels" | "exclude_labels" | "assignees"
> & {
channels?: readonly string[] | undefined;
from_users?: readonly string[] | undefined;
from_teams?: readonly string[] | undefined;
states?: readonly string[] | undefined;
labels?: readonly string[] | undefined;
exclude_labels?: readonly string[] | undefined;
Expand Down Expand Up @@ -1377,6 +1382,10 @@ function validateAuthoredIds(config: AuthoredHubConfig): void {
}

function validateTriggerLaunchSecurity(trigger: CompiledTrigger): void {
const fromTeams = trigger.filters?.from_teams ?? [];
if (fromTeams.length > 0 && !trigger.on.startsWith("github.")) {
throw new Error(`trigger ${trigger.name} may use filters.from_teams only for GitHub events`);
}
if (isEditorEvent(trigger.on) && eventDefinition(trigger.on).origin === "hub") return;
// A project scout is an intentionally autonomous, project-scoped policy. Every other
// externally-originated Linear action remains actor-allowlisted below.
Expand All @@ -1388,9 +1397,13 @@ function validateTriggerLaunchSecurity(trigger: CompiledTrigger): void {
}
return;
}
if ((trigger.filters?.from_users?.length ?? 0) === 0) {
const fromUsers = trigger.filters?.from_users ?? [];
if (fromUsers.length === 0 && fromTeams.length === 0) {
const allowlist = trigger.on.startsWith("github.")
? "filters.from_users or filters.from_teams"
: "filters.from_users";
throw new Error(
`trigger ${trigger.name} requires a non-empty filters.from_users allowlist for externally sourced events`,
`trigger ${trigger.name} requires a non-empty ${allowlist} allowlist for externally sourced events`,
);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/provider-applications/guides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ test("GitHub renders permissions as a mapping and events as a list, never as pro
{ name: "Issues", access: "Read and write" },
{ name: "Pull requests", access: "Read and write" },
{ name: "Metadata", access: "Read-only" },
{ name: "Members", access: "Read-only" },
]);
assert.deepEqual(
steps.flatMap((step) => step.events ?? []),
Expand Down
8 changes: 8 additions & 0 deletions src/provider-applications/guides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ export const GITHUB_GUIDE: ProviderGuide = {
"Push",
],
},
{
segments: [
{ kind: "text", value: "If a workflow uses a GitHub team allowlist, under " },
{ kind: "term", value: "Organization permissions" },
{ kind: "text", value: ", grant:" },
],
permissions: [{ name: "Members", access: "Read-only" }],
},
],
fields: [
{
Expand Down
3 changes: 3 additions & 0 deletions src/providers/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createGitHubTriggerProvider,
type GitHubReactionClient,
} from "../../triggers/github/provider.js";
import { createGitHubTeamMembershipClient } from "../../triggers/github/team-membership.js";
import { organizationBillingUrl } from "../../triggers/failure-notice.js";
import { createWebhookSource } from "../../triggers/github/webhook.js";
import {
Expand Down Expand Up @@ -90,6 +91,7 @@ export function createGitHubRegistration(
const appAuth =
options.appAuth ??
createGitHubAuth({ appId: configuration.appId, privateKey: configuration.privateKey });
const teamMemberships = createGitHubTeamMembershipClient(appAuth);
const client =
options.connectionClient ??
createGitHubConnectionClient({
Expand Down Expand Up @@ -249,6 +251,7 @@ export function createGitHubRegistration(
return createGitHubTriggerProvider({
configurationStoreForProject,
reactions,
teamMemberships,
comments,
billingUrlForOrganization: (organizationId) =>
organizationBillingUrl(database, options.publicBaseUrl!, organizationId),
Expand Down
11 changes: 11 additions & 0 deletions src/triggers/github/classification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
PullRequestPayloadSchema,
PullRequestReviewCommentPayloadSchema,
PullRequestReviewPayloadSchema,
PushPayloadSchema,
} from "../../auth/github-events.js";
import type { NormalizedGitHubEvent } from "../../auth/github-events.js";

Expand Down Expand Up @@ -52,9 +53,19 @@ export function classifyGitHubEvent(event: NormalizedGitHubEvent): GitHubClassif
if (event.type === "issue_comment") return classifyIssueComment(event);
if (event.type === "pull_request_review") return classifyReview(event);
if (event.type === "pull_request_review_comment") return classifyReviewComment(event);
if (event.type === "push") return classifyPush(event);
return emptyClassification();
}

function classifyPush(event: NormalizedGitHubEvent): GitHubClassifiedEvent {
const parsed = PushPayloadSchema.safeParse(event.payload);
if (!parsed.success) return emptyClassification();
return {
...emptyClassification(),
actor: parsed.data.sender?.login ?? "",
};
}

function classifyIssue(event: NormalizedGitHubEvent): GitHubClassifiedEvent {
const payload = IssuesPayloadSchema.parse(event.payload);
const item = payload.issue === undefined ? null : itemFor("issue", payload.issue);
Expand Down
Loading
Loading