diff --git a/AGENTS.md b/AGENTS.md index 7cff5ed3a7..a4866343ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,4 +4,11 @@ 2. Prefer function head matching to internal case/cond clauses. 3. Never use a nested case when a with expression is possible. 4. Defer to ecto for input validation. You should rarely need to use put_change, trust the builtins. -5. Avoid usage of `if` and `cond` if a more elegant case expression is possible. \ No newline at end of file +5. Avoid usage of `if` and `cond` if a more elegant case expression is possible. + +## Broad go guidance + +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: + +* You must always format all go code, linters will validate. +* 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. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f944a30a56..2f40ea38c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,7 +85,7 @@ RUN mix do db.certs, agent.chart, sentry.package_source_code, release FROM alpine:3.21.3 as tools ARG TARGETARCH=amd64 -ENV CLI_VERSION=v0.12.51 +ENV CLI_VERSION=v0.12.52 COPY AGENT_VERSION AGENT_VERSION diff --git a/assets/public/setup-guides/tools/pagerduty.md b/assets/public/setup-guides/tools/pagerduty.md new file mode 100644 index 0000000000..61d9224de3 --- /dev/null +++ b/assets/public/setup-guides/tools/pagerduty.md @@ -0,0 +1,36 @@ +# PagerDuty tool setup + +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=`. + +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. + +## 1) Create a REST API key + +1. In PagerDuty, go to **Integrations → API Access Keys → Create New API Key**. +2. Enter a description and choose read-only if you only need incident lookup. +3. Copy the key immediately — PagerDuty will not show it again. + +The key needs permission to read incidents for the services you care about (typically a read-only account or user key is enough). + +## 2) Built-in tools + +| Built-in tool (name suffix) | PagerDuty API | Purpose | +|-----------------------------|---------------|---------| +| `pagerduty_get_incident_*` | [`GET /incidents/{id}`](https://developer.pagerduty.com/api-reference/367602b1c2e48-get-an-incident) | Full incident record including `body.details` (UI description) | +| `pagerduty_list_incidents_*` | [`GET /incidents`](https://developer.pagerduty.com/api-reference/9d0b0b12e36f9-list-incidents) | Search/filter open or recent incidents | +| `pagerduty_list_incident_notes_*` | [`GET /incidents/{id}/notes`](https://developer.pagerduty.com/api-reference/988fd8460f5f0-list-notes-for-an-incident) | Responder notes | +| `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` | + +Pass the incident **id** from webhook payloads (`event.data.id`) or the numeric **incident number** shown in the PagerDuty UI. + +## 3) Fill the Workbench tool form + +- **API token**: paste the REST API key from step 1. + +After saving, associate this tool with a workbench that handles PagerDuty alert workflows. + +## Further reading + +- [PagerDuty REST API overview](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/) +- [PagerDuty API authentication](https://developer.pagerduty.com/docs/rest-api-v2/authentication/) +- [PagerDuty webhooks (V3)](https://support.pagerduty.com/docs/webhooks) diff --git a/assets/public/setup-guides/webhooks/pagerduty.md b/assets/public/setup-guides/webhooks/pagerduty.md index 40baab6614..762cea7430 100644 --- a/assets/public/setup-guides/webhooks/pagerduty.md +++ b/assets/public/setup-guides/webhooks/pagerduty.md @@ -1,43 +1,54 @@ # PagerDuty Webhook Setup for Plural -Reference: [PagerDuty - Webhooks](https://support.pagerduty.com/main/docs/webhooks) +References: +- [PagerDuty - Webhooks (V3)](https://support.pagerduty.com/main/docs/webhooks) +- [PagerDuty - Verifying webhook signatures](https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTkz-verifying-signatures) -## 1. Create the webhook in Plural first +PagerDuty is one provider here where the signing secret is generated by PagerDuty, not manually entered at creation time. + +Use **Generic Webhooks (v3)** or a **service-scoped V3 webhook subscription**. V1/V2 webhook extensions use a different payload format and are not supported. + +## 1. Create webhook in PagerDuty and copy its signing secret + +In your PagerDuty account (`https://.pagerduty.com/`): + +1. Open **Integrations** -> **Generic Webhooks (v3)** + (Alternatively, open a service in **Services** -> **Service Directory**, select the **Integrations** tab, and add a webhook under **Webhooks** to scope events to that service.) +2. Click **New Webhook** +3. Set **Webhook URL** to a temporary placeholder (for example `https://example.com/plural-webhook-placeholder`) +4. Under **Event Subscription**, select incident lifecycle events at minimum: + - `triggered` + - `acknowledged` + - `resolved` +5. Click **Add Webhook** +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. + +## 2. Create the webhook in Plural In Plural: 1. Set **Type** to `Observability`. 2. Set **Provider** to `PAGERDUTY`. -3. Enter a webhook **Name**. -4. Enter a **Signing secret**. +3. Enter webhook **Name**. +4. Paste the PagerDuty signing secret as Plural **Signing secret**. 5. Click **Create new webhook**. -Copy the generated webhook URL. - -## 2. Create webhook subscription in PagerDuty - -In your PagerDuty account (`https://.pagerduty.com/`): - -1. Open webhook subscriptions (generic or service-scoped) -2. Create a new subscription with HTTP delivery -3. Set destination URL to the Plural webhook URL -4. Select service/team/account filter scope -5. Add auth header or Basic Auth using the Plural signing secret (if your PagerDuty webhook mode supports it) +Copy the Plural-generated webhook URL. -## 3. Select event types +## 3. Update PagerDuty webhook URL -Enable incident lifecycle events: +Back in PagerDuty webhook settings: -- `incident.triggered` -- `incident.acknowledged` -- `incident.resolved` +1. Select the webhook URL (or **Manage** -> **Edit**) +2. Replace the placeholder URL with the Plural webhook URL +3. Click **Save Changes** -Include additional incident update events only if needed. +Optional: use **Send Test Event** in PagerDuty to confirm delivery before triggering a real incident. ## 4. Validate -Trigger a test incident in PagerDuty and confirm in Plural: +Trigger a test incident (or send a PagerDuty test event) and confirm in Plural: -- delivery accepted -- auth/secret verification succeeds -- incident state transitions are visible +- request accepted +- `x-pagerduty-signature` verification succeeds +- incident state transitions are visible (`triggered`, `acknowledged`, `resolved`) diff --git a/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx b/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx index ffe8c383ba..f66769af77 100644 --- a/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx +++ b/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx @@ -31,7 +31,7 @@ import { useUpsertObservabilityWebhookMutation, } from 'generated/graphql' -import { InputRevealer } from 'components/cd/providers/InputRevealer' +import { SecretInputWithGenerate } from 'components/utils/SecretInputWithGenerate' import { useUpdateState } from 'components/hooks/useUpdateState' import { bindingToBindingAttributes } from 'components/utils/bindings' import { GqlError } from 'components/utils/Alert' @@ -226,7 +226,8 @@ export function EditObservabilityWebhook({ label="Secret" required > - updateFormState({ secret: e.target.value })} diff --git a/assets/src/components/utils/SecretInputWithGenerate.tsx b/assets/src/components/utils/SecretInputWithGenerate.tsx new file mode 100644 index 0000000000..b23679a277 --- /dev/null +++ b/assets/src/components/utils/SecretInputWithGenerate.tsx @@ -0,0 +1,60 @@ +import { Flex, IconFrame, Input2, ReloadIcon } from '@pluralsh/design-system' +import { InputRevealer } from 'components/cd/providers/InputRevealer' +import { generateRandomAlphanumeric } from 'utils/generateRandomAlphanumeric' +import { ChangeEvent, ComponentProps, useCallback } from 'react' + +type SecretInputWithGenerateProps = { + value: string + onChange: ComponentProps['onChange'] + masked?: boolean + defaultRevealed?: boolean +} & Omit, 'value' | 'onChange'> + +export function SecretInputWithGenerate({ + value, + onChange, + masked = false, + defaultRevealed = false, + ...props +}: SecretInputWithGenerateProps) { + const handleGenerate = useCallback(() => { + onChange?.({ + target: { value: generateRandomAlphanumeric() }, + } as ChangeEvent) + }, [onChange]) + + return ( + + + {masked ? ( + + ) : ( + + )} + + } + onClick={handleGenerate} + /> + + ) +} diff --git a/assets/src/components/workbenches/WorkbenchesConfiguredToolMetadata.tsx b/assets/src/components/workbenches/WorkbenchesConfiguredToolMetadata.tsx index e5f0df01ed..0a7dd8d1c5 100644 --- a/assets/src/components/workbenches/WorkbenchesConfiguredToolMetadata.tsx +++ b/assets/src/components/workbenches/WorkbenchesConfiguredToolMetadata.tsx @@ -27,6 +27,7 @@ const metadataExtractors: Record = { [WorkbenchToolType.Atlassian]: extractAtlassianMetadata, [WorkbenchToolType.Linear]: extractLinearMetadata, [WorkbenchToolType.Slack]: extractSlackMetadata, + [WorkbenchToolType.Pagerduty]: extractPagerdutyMetadata, [WorkbenchToolType.Teams]: extractTeamsMetadata, [WorkbenchToolType.Mcp]: () => [], [WorkbenchToolType.Sentry]: extractSentryMetadata, @@ -183,6 +184,12 @@ function extractSlackMetadata( return [{ label: 'URL', value: configuration?.slack?.url }] } +function extractPagerdutyMetadata( + configuration: WorkbenchToolConfiguration | null +): MetadataRow[] { + return [{ label: 'URL', value: configuration?.pagerduty?.url }] +} + function extractTeamsMetadata( configuration: WorkbenchToolConfiguration | null ): MetadataRow[] { diff --git a/assets/src/components/workbenches/WorkbenchesIntegrations.tsx b/assets/src/components/workbenches/WorkbenchesIntegrations.tsx index e50fe79aab..6fd51702b1 100644 --- a/assets/src/components/workbenches/WorkbenchesIntegrations.tsx +++ b/assets/src/components/workbenches/WorkbenchesIntegrations.tsx @@ -221,6 +221,7 @@ export function WorkbenchesIntegrations() { size={20} type={type} provider={provider} + fullColor /> } /> diff --git a/assets/src/components/workbenches/tools/WorkbenchToolCreateOrEdit.tsx b/assets/src/components/workbenches/tools/WorkbenchToolCreateOrEdit.tsx index d7a9e55727..deb15412bb 100644 --- a/assets/src/components/workbenches/tools/WorkbenchToolCreateOrEdit.tsx +++ b/assets/src/components/workbenches/tools/WorkbenchToolCreateOrEdit.tsx @@ -193,6 +193,7 @@ export function WorkbenchToolCreateOrEdit({ } textValue={capitalize(provider || type)} diff --git a/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx b/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx index 1479ebd5db..5225b16b2b 100644 --- a/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx +++ b/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx @@ -82,6 +82,12 @@ function teamsConfigurationIsComplete( return clientId.length > 0 && tenantId.length > 0 && clientSecret.length > 0 } +function pagerdutyConfigurationIsComplete( + c: WorkbenchToolConfigurationAttributes['pagerduty'] | null | undefined +): boolean { + return scmTokenIsSet(c?.apiToken) +} + function scmTokenIsSet(token: string | null | undefined): boolean { return (token ?? '').trim().length > 0 } @@ -194,6 +200,9 @@ export function WorkbenchToolForm({ scmTokenIsSet(state.configuration?.azureDevops?.token)) && (type !== WorkbenchToolType.Teams || teamsConfigurationIsComplete(state.configuration?.teams)) && + (type !== WorkbenchToolType.Pagerduty || + !!tool?.id || + pagerdutyConfigurationIsComplete(state.configuration?.pagerduty)) && (type !== WorkbenchToolType.Sentry || !!tool?.id || sentryConfigurationIsComplete(state.configuration?.sentry)) @@ -313,6 +322,16 @@ export function WorkbenchToolForm({ > {hasUpdates ? 'Cancel' : 'Back'} + {currentStep === 'access-policy' ? ( + + ) : null}