Skip to content

Commit 7969794

Browse files
feat: Sentinel postrun scripts (#3591)
1 parent 6a1c35f commit 7969794

113 files changed

Lines changed: 2584 additions & 1070 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@
44
2. Prefer function head matching to internal case/cond clauses.
55
3. Never use a nested case when a with expression is possible.
66
4. Defer to ecto for input validation. You should rarely need to use put_change, trust the builtins.
7-
5. Avoid usage of `if` and `cond` if a more elegant case expression is possible.
7+
5. Avoid usage of `if` and `cond` if a more elegant case expression is possible.
8+
9+
## Broad go guidance
10+
11+
The repo has a substantial amount of go code, all in a go workspace under `go/`. Here are some broad rules for interactin with it:
12+
13+
* You must always format all go code, linters will validate.
14+
* You should prefer using code generators in the Makefiles associated with whatever module you're interacting with. Never manually edit generated files, it'll be overwritten.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ RUN mix do db.certs, agent.chart, sentry.package_source_code, release
8585
FROM alpine:3.21.3 as tools
8686

8787
ARG TARGETARCH=amd64
88-
ENV CLI_VERSION=v0.12.51
88+
ENV CLI_VERSION=v0.12.52
8989

9090
COPY AGENT_VERSION AGENT_VERSION
9191

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PagerDuty tool setup
2+
3+
Workbench’s PagerDuty integration calls the **PagerDuty REST API** (`https://api.pagerduty.com`) with a **REST API key**. The backend sends it as `Authorization: Token token=<api_key>`.
4+
5+
Use this when webhook payloads lack incident context (for example `body.details`, notes, or trigger log entry channel details). Agents can fetch that data on demand.
6+
7+
## 1) Create a REST API key
8+
9+
1. In PagerDuty, go to **Integrations → API Access Keys → Create New API Key**.
10+
2. Enter a description and choose read-only if you only need incident lookup.
11+
3. Copy the key immediately — PagerDuty will not show it again.
12+
13+
The key needs permission to read incidents for the services you care about (typically a read-only account or user key is enough).
14+
15+
## 2) Built-in tools
16+
17+
| Built-in tool (name suffix) | PagerDuty API | Purpose |
18+
|-----------------------------|---------------|---------|
19+
| `pagerduty_get_incident_*` | [`GET /incidents/{id}`](https://developer.pagerduty.com/api-reference/367602b1c2e48-get-an-incident) | Full incident record including `body.details` (UI description) |
20+
| `pagerduty_list_incidents_*` | [`GET /incidents`](https://developer.pagerduty.com/api-reference/9d0b0b12e36f9-list-incidents) | Search/filter open or recent incidents |
21+
| `pagerduty_list_incident_notes_*` | [`GET /incidents/{id}/notes`](https://developer.pagerduty.com/api-reference/988fd8460f5f0-list-notes-for-an-incident) | Responder notes |
22+
| `pagerduty_list_incident_log_entries_*` | [`GET /incidents/{id}/log_entries`](https://developer.pagerduty.com/api-reference/3679cad205ac9-list-log-entries-for-an-incident) | Timeline entries; triggers may include `channel.details` |
23+
24+
Pass the incident **id** from webhook payloads (`event.data.id`) or the numeric **incident number** shown in the PagerDuty UI.
25+
26+
## 3) Fill the Workbench tool form
27+
28+
- **API token**: paste the REST API key from step 1.
29+
30+
After saving, associate this tool with a workbench that handles PagerDuty alert workflows.
31+
32+
## Further reading
33+
34+
- [PagerDuty REST API overview](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/)
35+
- [PagerDuty API authentication](https://developer.pagerduty.com/docs/rest-api-v2/authentication/)
36+
- [PagerDuty webhooks (V3)](https://support.pagerduty.com/docs/webhooks)
Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,54 @@
11
# PagerDuty Webhook Setup for Plural
22

3-
Reference: [PagerDuty - Webhooks](https://support.pagerduty.com/main/docs/webhooks)
3+
References:
4+
- [PagerDuty - Webhooks (V3)](https://support.pagerduty.com/main/docs/webhooks)
5+
- [PagerDuty - Verifying webhook signatures](https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTkz-verifying-signatures)
46

5-
## 1. Create the webhook in Plural first
7+
PagerDuty is one provider here where the signing secret is generated by PagerDuty, not manually entered at creation time.
8+
9+
Use **Generic Webhooks (v3)** or a **service-scoped V3 webhook subscription**. V1/V2 webhook extensions use a different payload format and are not supported.
10+
11+
## 1. Create webhook in PagerDuty and copy its signing secret
12+
13+
In your PagerDuty account (`https://<your-subdomain>.pagerduty.com/`):
14+
15+
1. Open **Integrations** -> **Generic Webhooks (v3)**
16+
(Alternatively, open a service in **Services** -> **Service Directory**, select the **Integrations** tab, and add a webhook under **Webhooks** to scope events to that service.)
17+
2. Click **New Webhook**
18+
3. Set **Webhook URL** to a temporary placeholder (for example `https://example.com/plural-webhook-placeholder`)
19+
4. Under **Event Subscription**, select incident lifecycle events at minimum:
20+
- `triggered`
21+
- `acknowledged`
22+
- `resolved`
23+
5. Click **Add Webhook**
24+
6. In the confirmation dialog, copy the webhook signing secret immediately — PagerDuty only shows it once. If you lose it, you must regenerate the secret in PagerDuty and update Plural.
25+
26+
## 2. Create the webhook in Plural
627

728
In Plural:
829

930
1. Set **Type** to `Observability`.
1031
2. Set **Provider** to `PAGERDUTY`.
11-
3. Enter a webhook **Name**.
12-
4. Enter a **Signing secret**.
32+
3. Enter webhook **Name**.
33+
4. Paste the PagerDuty signing secret as Plural **Signing secret**.
1334
5. Click **Create new webhook**.
1435

15-
Copy the generated webhook URL.
16-
17-
## 2. Create webhook subscription in PagerDuty
18-
19-
In your PagerDuty account (`https://<your-subdomain>.pagerduty.com/`):
20-
21-
1. Open webhook subscriptions (generic or service-scoped)
22-
2. Create a new subscription with HTTP delivery
23-
3. Set destination URL to the Plural webhook URL
24-
4. Select service/team/account filter scope
25-
5. Add auth header or Basic Auth using the Plural signing secret (if your PagerDuty webhook mode supports it)
36+
Copy the Plural-generated webhook URL.
2637

27-
## 3. Select event types
38+
## 3. Update PagerDuty webhook URL
2839

29-
Enable incident lifecycle events:
40+
Back in PagerDuty webhook settings:
3041

31-
- `incident.triggered`
32-
- `incident.acknowledged`
33-
- `incident.resolved`
42+
1. Select the webhook URL (or **Manage** -> **Edit**)
43+
2. Replace the placeholder URL with the Plural webhook URL
44+
3. Click **Save Changes**
3445

35-
Include additional incident update events only if needed.
46+
Optional: use **Send Test Event** in PagerDuty to confirm delivery before triggering a real incident.
3647

3748
## 4. Validate
3849

39-
Trigger a test incident in PagerDuty and confirm in Plural:
50+
Trigger a test incident (or send a PagerDuty test event) and confirm in Plural:
4051

41-
- delivery accepted
42-
- auth/secret verification succeeds
43-
- incident state transitions are visible
52+
- request accepted
53+
- `x-pagerduty-signature` verification succeeds
54+
- incident state transitions are visible (`triggered`, `acknowledged`, `resolved`)

assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
useUpsertObservabilityWebhookMutation,
3232
} from 'generated/graphql'
3333

34-
import { InputRevealer } from 'components/cd/providers/InputRevealer'
34+
import { SecretInputWithGenerate } from 'components/utils/SecretInputWithGenerate'
3535
import { useUpdateState } from 'components/hooks/useUpdateState'
3636
import { bindingToBindingAttributes } from 'components/utils/bindings'
3737
import { GqlError } from 'components/utils/Alert'
@@ -226,7 +226,8 @@ export function EditObservabilityWebhook({
226226
label="Secret"
227227
required
228228
>
229-
<InputRevealer
229+
<SecretInputWithGenerate
230+
masked
230231
defaultRevealed={false}
231232
value={formState.secret}
232233
onChange={(e) => updateFormState({ secret: e.target.value })}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Flex, IconFrame, Input2, ReloadIcon } from '@pluralsh/design-system'
2+
import { InputRevealer } from 'components/cd/providers/InputRevealer'
3+
import { generateRandomAlphanumeric } from 'utils/generateRandomAlphanumeric'
4+
import { ChangeEvent, ComponentProps, useCallback } from 'react'
5+
6+
type SecretInputWithGenerateProps = {
7+
value: string
8+
onChange: ComponentProps<typeof Input2>['onChange']
9+
masked?: boolean
10+
defaultRevealed?: boolean
11+
} & Omit<ComponentProps<typeof Input2>, 'value' | 'onChange'>
12+
13+
export function SecretInputWithGenerate({
14+
value,
15+
onChange,
16+
masked = false,
17+
defaultRevealed = false,
18+
...props
19+
}: SecretInputWithGenerateProps) {
20+
const handleGenerate = useCallback(() => {
21+
onChange?.({
22+
target: { value: generateRandomAlphanumeric() },
23+
} as ChangeEvent<HTMLInputElement>)
24+
}, [onChange])
25+
26+
return (
27+
<Flex
28+
align="center"
29+
gap="xsmall"
30+
width="100%"
31+
>
32+
<Flex
33+
flex={1}
34+
minWidth={0}
35+
>
36+
{masked ? (
37+
<InputRevealer
38+
defaultRevealed={defaultRevealed}
39+
value={value}
40+
onChange={onChange}
41+
{...props}
42+
/>
43+
) : (
44+
<Input2
45+
value={value}
46+
onChange={onChange}
47+
{...props}
48+
/>
49+
)}
50+
</Flex>
51+
<IconFrame
52+
clickable
53+
type="secondary"
54+
tooltip="Generate secret"
55+
icon={<ReloadIcon />}
56+
onClick={handleGenerate}
57+
/>
58+
</Flex>
59+
)
60+
}

assets/src/components/workbenches/WorkbenchesConfiguredToolMetadata.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const metadataExtractors: Record<WorkbenchToolType, MetadataExtractor> = {
2727
[WorkbenchToolType.Atlassian]: extractAtlassianMetadata,
2828
[WorkbenchToolType.Linear]: extractLinearMetadata,
2929
[WorkbenchToolType.Slack]: extractSlackMetadata,
30+
[WorkbenchToolType.Pagerduty]: extractPagerdutyMetadata,
3031
[WorkbenchToolType.Teams]: extractTeamsMetadata,
3132
[WorkbenchToolType.Mcp]: () => [],
3233
[WorkbenchToolType.Sentry]: extractSentryMetadata,
@@ -183,6 +184,12 @@ function extractSlackMetadata(
183184
return [{ label: 'URL', value: configuration?.slack?.url }]
184185
}
185186

187+
function extractPagerdutyMetadata(
188+
configuration: WorkbenchToolConfiguration | null
189+
): MetadataRow[] {
190+
return [{ label: 'URL', value: configuration?.pagerduty?.url }]
191+
}
192+
186193
function extractTeamsMetadata(
187194
configuration: WorkbenchToolConfiguration | null
188195
): MetadataRow[] {

assets/src/components/workbenches/WorkbenchesIntegrations.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export function WorkbenchesIntegrations() {
221221
size={20}
222222
type={type}
223223
provider={provider}
224+
fullColor
224225
/>
225226
}
226227
/>

assets/src/components/workbenches/tools/WorkbenchToolCreateOrEdit.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export function WorkbenchToolCreateOrEdit({
193193
<WorkbenchToolIcon
194194
type={type}
195195
provider={provider}
196+
fullColor
196197
/>
197198
}
198199
textValue={capitalize(provider || type)}

assets/src/components/workbenches/tools/WorkbenchToolForm.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ function teamsConfigurationIsComplete(
8282
return clientId.length > 0 && tenantId.length > 0 && clientSecret.length > 0
8383
}
8484

85+
function pagerdutyConfigurationIsComplete(
86+
c: WorkbenchToolConfigurationAttributes['pagerduty'] | null | undefined
87+
): boolean {
88+
return scmTokenIsSet(c?.apiToken)
89+
}
90+
8591
function scmTokenIsSet(token: string | null | undefined): boolean {
8692
return (token ?? '').trim().length > 0
8793
}
@@ -194,6 +200,9 @@ export function WorkbenchToolForm({
194200
scmTokenIsSet(state.configuration?.azureDevops?.token)) &&
195201
(type !== WorkbenchToolType.Teams ||
196202
teamsConfigurationIsComplete(state.configuration?.teams)) &&
203+
(type !== WorkbenchToolType.Pagerduty ||
204+
!!tool?.id ||
205+
pagerdutyConfigurationIsComplete(state.configuration?.pagerduty)) &&
197206
(type !== WorkbenchToolType.Sentry ||
198207
!!tool?.id ||
199208
sentryConfigurationIsComplete(state.configuration?.sentry))
@@ -313,6 +322,16 @@ export function WorkbenchToolForm({
313322
>
314323
{hasUpdates ? 'Cancel' : 'Back'}
315324
</Button>
325+
{currentStep === 'access-policy' ? (
326+
<Button
327+
secondary
328+
type="button"
329+
onClick={() => setCurrentStep('configuration')}
330+
disabled={mutationLoading}
331+
>
332+
Back to configuration
333+
</Button>
334+
) : null}
316335
<Button
317336
disabled={
318337
currentStep === 'configuration'
@@ -530,6 +549,7 @@ export const INITIAL_TOOL_CONFIG_BY_TYPE: {
530549
},
531550
[WorkbenchToolType.Linear]: () => ({ linear: { accessToken: '' } }),
532551
[WorkbenchToolType.Slack]: () => ({ slack: { botToken: '' } }),
552+
[WorkbenchToolType.Pagerduty]: () => ({ pagerduty: { apiToken: '' } }),
533553
[WorkbenchToolType.Teams]: (config) => {
534554
const { clientId, tenantId } = config?.teams ?? {}
535555
return {

0 commit comments

Comments
 (0)