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
6 changes: 6 additions & 0 deletions TERMINOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ Canonical words used across Junior's code and documentation.
its outcome. It is not a Run.
- **Scheduled automation**: an Automation that has a Schedule trigger.
- **Event automation**: an Automation that has an Event trigger.
- **Event reach**: which event automation triggers can select one event type.
Each source declares it once for each of its event types. `"all"` lets a
trigger with no identifier run for every resource of the source.
`{ anyOf }` lets such a trigger run only when it sets one of the named
filters. `"resource"` needs a trigger with an identifier. Reach limits scope
and noise. It is not access control.
- **Inbound message**: one normalized source event made available to the agent.
- **Agent input**: the inbound content, context, and runtime metadata selected
for a turn.
Expand Down
11 changes: 11 additions & 0 deletions packages/docs/src/content/docs/concepts/automations.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ whenever a new issue is created in Linear team SRE, investigate it and comment f
when someone posts a new message in this channel, answer it if you can, otherwise tag me
```

An event automation can also run for every resource of a plugin, with no exact resource. Filters then narrow it:

```text
whenever any Sentry metric alert for project javascript goes critical, summarize it here
whenever a pull request opens in getsentry/junior, review it
```

Each plugin decides which events can run this way, and each plugin page lists them. Some events need a filter. For example, GitHub issue, pull request, and release events need a `repo` filter, and Linear issue events need a `teamKey` or `issueIdentifier` filter. Junior names the missing filter when it cannot save the automation. Junior saves filter values in the case that event data uses, so `getsentry/Junior` matches `getsentry/junior`. One event runs the automation once, even when the plugin publishes the event for more than one resource, such as each project of a metric alert. Slack channel-message automations always need their own channel.

A required filter limits how much an automation covers and how noisy it is. It is not access control. Junior does not check that the person who creates the automation can see the repository, project, or team in the filter. An automation with no exact resource posts what it receives to its destination channel. For example, a broad Sentry trigger or GitHub deployment trigger can post private project or repository names in that channel. Create broad automations only in channels where everyone can see those resources.

A Slack channel-message automation runs for each new top-level message in the channel where you create it. It does not run for thread replies or for messages that mention Junior. It runs for a bot message, such as a Sentry alert, only when the automation names that bot's Slack user ID in `match.userId`. Junior replies in the thread of the message. Junior must be a member of the channel. The automation can watch only its own channel, and it always runs with Junior's own credentials, because anyone in the channel can write the message. Tell the channel that Junior reads every new message there.

If the plugin is disabled, the automation remains visible but cannot receive events until a compatible plugin is enabled again. Each plugin page lists the resources and events it can publish.
Expand Down
10 changes: 10 additions & 0 deletions packages/docs/src/content/docs/extend/github-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,16 @@ repository automation to one environment or one workflow. For example, an
automation on `getsentry/toolkit` for `deployment.failed` can match
`environment: "production"` and `workflowPath: ".github/workflows/deploy.yml"`.

An event automation can also run for every repository, with no identifier.
Deployment events (`deployment.created`, `deployment.queued`,
`deployment.pending`, `deployment.in_progress`, `deployment.succeeded`,
`deployment.failed`, `deployment.error`) can run that way with no filter.
Release, issue, and pull request events need a `repo` filter, because they
carry text from repositories that can be private. Junior saves `repo` in
lowercase. The filter does not check that the creator can see the repository,
so a broad deployment automation can post private repository names in its
channel. See [Automations](/concepts/automations/).

To always watch selected events after Junior creates a pull request in this
app, set `pullRequestEvents.subscribeAfterCreate` in `plugins.ts`. To add short
app guidance for one pull request event type, set
Expand Down
7 changes: 7 additions & 0 deletions packages/docs/src/content/docs/extend/linear-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ Create the watch or event automation before the issue arrives. Junior does not r

Identifiers are normalized to uppercase. Prefer team-scoped event automations for monitor workflows that create many new issues.

An event automation can also run for every team, with no identifier.
`issue.created` can run that way only with a `teamKey` or an
`issueIdentifier` filter, because an issue carries its title and description
and a team can be private. Junior saves both in uppercase. The filter does not
check that the creator can see the team. See
[Automations](/concepts/automations/).

Optional `match` values come from the resource type. For Linear issue and team events, `teamKey` is the uppercase team key such as `SRE`. Junior drops events that do not match before it wakes the agent. Prefer a team identifier for “all new issues in SRE”. Use `match.teamKey` when an issue-scoped watch or event automation needs an extra team guard.

## Verify
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/content/docs/extend/sentry-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ The alert resolved.

Junior publishes each state once for each alert incident.

An event automation can also run for every project in the Sentry org, with no
identifier. `issue.created`, `metric_alert.critical`, `metric_alert.warning`,
and `metric_alert.resolved` can run that way with no filter. Use the
`projects` filter to limit one to some projects. Junior saves project slugs in
lowercase. Junior does not check that the creator can see the projects, so a
broad automation can post private project names in its channel. See
[Automations](/concepts/automations/).

Create the watch or event automation before the webhook arrives. Junior does not replay earlier webhooks.

## Verify
Expand Down
7 changes: 7 additions & 0 deletions packages/docs/src/content/docs/extend/vercel-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ The deployment was canceled.

</details>

An event automation can also run for every project, with no identifier.
`deployment.succeeded`, `deployment.error`, and `deployment.canceled` can run
that way with no filter. Use the `projectId` or `target` filter to limit one.
Junior does not check that the creator can see the projects, so a broad
automation can post private project names in its channel. See
[Automations](/concepts/automations/).

Create the watch or event automation before the deployment finishes. Junior does
not replay earlier webhooks. Project- and target-scoped watches keep
receiving later deployments; commit-scoped watches complete on the terminal
Expand Down
28 changes: 25 additions & 3 deletions packages/junior-dashboard/e2e/automation-editor-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,42 @@ export async function mockAutomationEditor(
json: [
{
namespace: "github",
type: "issue",
supportedEvents: ["issue.closed", "issue.opened"],
matchFields: {
displayName: "GitHub",
events: {
"issue.closed": { reach: { anyOf: ["state"] } },
"issue.opened": { reach: "all" },
},
anyResourceFields: {
state: {
kind: "string",
description: "Issue state",
enum: ["open", "closed"],
events: ["issue.closed", "issue.opened"],
},
closedBy: {
kind: "string",
description: "Login of who closed the issue",
events: ["issue.closed"],
},
},
resourceTypes: [
{
type: "issue",
supportedEvents: ["issue.closed", "issue.opened"],
matchFields: {
state: {
kind: "string",
description: "Issue state",
enum: ["open", "closed"],
},
closedBy: {
kind: "string",
description: "Login of who closed the issue",
events: ["issue.closed"],
},
},
},
],
},
],
}),
Expand Down
36 changes: 36 additions & 0 deletions packages/junior-dashboard/e2e/automation-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,42 @@ test("keeps Event edits through validation, failed saves, and concurrent changes
]);
});

test("replaces an Event trigger with one for any resource", async ({
page,
dashboard,
}) => {
const state = await mockAutomationEditor(page, "event");
await page.goto(`${dashboard.baseURL}/automations/event-1/edit`);
await page.getByLabel("Resource type").selectOption("github:*");
await page.getByRole("button", { name: "Replace trigger" }).click();
await expect(page.getByLabel("Resource identifier")).toHaveCount(0);
// A trigger with no identifier names no resource.
await page.getByLabel("Trigger name").fill("Closed issues");
await page.getByRole("checkbox", { name: /^issue closed/ }).check();
await screenshot(page, "automation-event-editor-any-resource");
// The shared check names the filter that this event needs.
await expect(
page.getByText('This trigger needs a "state" filter'),
).toBeVisible();
await page.getByLabel("state", { exact: true }).selectOption("closed");
await expect(
page.getByText('This trigger needs a "state" filter'),
).toHaveCount(0);
await page.getByRole("button", { name: "Save changes" }).click();
await expect(page).toHaveURL(`${dashboard.baseURL}/automations/list`);
expect(state.writes).toEqual([
expect.objectContaining({
kind: "event",
trigger: {
namespace: "github",
label: "Closed issues",
events: ["issue.closed"],
match: { state: "closed" },
},
}),
]);
});

test("public non-creators edit settings without creator-only controls", async ({
page,
dashboard,
Expand Down
Loading
Loading