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
1 change: 1 addition & 0 deletions .github/workflows/no-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ on:
- '!oblt-cli/cluster-name-validation/**'
- '!oblt-cli/list/**'
- '!oblt-cli/run/**'
- '!pagerduty/alert/**'
- '!pre-commit/**'
- '!updatecli/run/**'

Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/test-pagerduty-alert.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: test-pagerduty-alert

on:
merge_group: ~
workflow_dispatch: ~
pull_request:
branches:
- main
paths:
- '.github/workflows/test-pagerduty-alert.yml'
- 'pagerduty/alert/**'

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./pagerduty/alert
if: github.event_name == 'pull_request'
id: pagerduty-alert
with:
summary: 'Test Alert from OBLT Actions'
source: 'OBLT Actions Test Suite'
api-key: ${{ secrets.PD_SECRET }}
component: 'OBLT Actions'
routing-key: ${{ secrets.PD_ROUTING_KEY }}
severity: 'critical'

- name: Assert output is not empty
if: github.event_name == 'pull_request'
run: |
[ -n "${{ steps.pagerduty-alert.outputs.incident-url }}" ]
51 changes: 51 additions & 0 deletions pagerduty/alert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# <!--name-->pagerduty-alert<!--/name-->

[![usages](https://img.shields.io/badge/usages-white?logo=githubactions&logoColor=blue)](https://github.com/search?q=elastic%2Foblt-actions%2Fpagerduty%2Falert+%28path%3A.github%2Fworkflows+OR+path%3A**%2Faction.yml+OR+path%3A**%2Faction.yaml%29&type=code)
[![test-pagerduty-alert](https://github.com/elastic/oblt-actions/actions/workflows/test-pagerduty-alert.yml/badge.svg?branch=main)](https://github.com/elastic/oblt-actions/actions/workflows/test-pagerduty-alert.yml)

<!--description-->
Raise a PagerDuty alert and return the incident URL
<!--/description-->

## Inputs
<!--inputs-->
| Name | Description | Required | Default |
|---------------|------------------------------------|----------|------------|
| `summary` | The PagerDuty summary of the alert | `true` | ` ` |
| `source` | The PagerDuty source of the alert | `true` | ` ` |
| `api-key` | The PagerDuty API key | `true` | ` ` |
| `component` | The PagerDuty component | `true` | ` ` |
| `routing-key` | The PagerDuty integration key | `true` | ` ` |
| `severity` | The PagerDuty severity | `false` | `critical` |
<!--/inputs-->

## Outputs

<!--outputs-->
| Name | Description |
|----------------|----------------------------------------|
| `incident-url` | The HTML URL of the PagerDuty incident |
<!--/outputs-->

## Usage
<!--usage action="elastic/oblt-actions/**" version="env:VERSION"-->
```yaml
jobs:
assign-engineer-urgent-now:
steps:
- uses: elastic/oblt-actions/pagerduty/alert@v1
id: pagerduty
with:
summary: "Reported some errors with XYZ"
source: "https://..."
api-key: "${{ secrets.PD_SECRET }}"
component: "my-component"
severity: "critical"
routing-key: "${{ secrets.PD_ROUTING_KEY }}"

- name: Notify a pagerduty incident has been created
run: echo "${{steps.pagerduty.outputs.incident-url}} has been created"

```

<!--/usage-->
82 changes: 82 additions & 0 deletions pagerduty/alert/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: 'pagerduty-alert'
description: 'Raise a PagerDuty alert and return the incident URL'
inputs:
summary:
description: 'The PagerDuty summary of the alert'
required: true
source:
description: 'The PagerDuty source of the alert'
required: true
api-key:
description: 'The PagerDuty API key'
required: true
component:
description: 'The PagerDuty component'
required: true
routing-key:
description: 'The PagerDuty integration key'
required: true
severity:
description: 'The PagerDuty severity'
default: 'critical'
required: false

outputs:
incident-url:
description: 'The HTML URL of the PagerDuty incident'
value: ${{ steps.get_incident.outputs.result }}

runs:
using: "composite"
steps:
- uses: actions/github-script@v8
id: sanitize
env:
SUMMARY: ${{ inputs.summary }}
with:
script: |
const sanitizedTitle = JSON.stringify(process.env.SUMMARY).slice(1, -1)
console.log(`sanitized summary is: ${sanitizedTitle}`)
core.setOutput("summary", sanitizedTitle)

// Time window (15 minutes) for searching recent incidents, in milliseconds
const now = new Date()
const fifteenMinutesAgo = new Date(now.getTime() - (15 * 60 * 1000))
const isoDate = fifteenMinutesAgo.toISOString()
core.setOutput("time-search", isoDate)

- name: Trigger PagerDuty Alert
uses: fjogeleit/http-request-action@1297c6fc63a79b147d1676540a3fd9d2e37817c5 # v1.16.5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this is a copy of an existing composite action - so I prefered to keep the same implementation from now

Copy link
Member Author

@v1v v1v Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with:
url: 'https://events.pagerduty.com/v2/enqueue'
method: 'POST'
customHeaders: '{"Accept": "application/json", "Content-Type": "application/json", "Authorization" : "Token token=${{ inputs.api-key }}"}'
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API key is exposed in plain text in the workflow logs via customHeaders. Consider using a masked approach or ensure that the http-request-action properly masks sensitive headers to prevent accidental exposure in logs.

Copilot uses AI. Check for mistakes.
data: '{"event_action": "trigger", "routing_key": "${{ inputs.routing-key }}", "payload": {"summary": "${{steps.sanitize.outputs.summary}}", "source": "${{ inputs.source }}", "custom_details":"${{ inputs.source }}", "severity": "${{ inputs.severity }}", "component": "${{ inputs.component }}"}}'
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom_details field is set to the same value as source (${{ inputs.source }}), which appears to be unintentional. According to PagerDuty API documentation, custom_details should contain additional context as an object, not duplicate the source string.

Suggested change
data: '{"event_action": "trigger", "routing_key": "${{ inputs.routing-key }}", "payload": {"summary": "${{steps.sanitize.outputs.summary}}", "source": "${{ inputs.source }}", "custom_details":"${{ inputs.source }}", "severity": "${{ inputs.severity }}", "component": "${{ inputs.component }}"}}'
data: '{"event_action": "trigger", "routing_key": "${{ inputs.routing-key }}", "payload": {"summary": "${{steps.sanitize.outputs.summary}}", "source": "${{ inputs.source }}", "custom_details": {"source": "${{ inputs.source }}", "component": "${{ inputs.component }}", "summary": "${{ steps.sanitize.outputs.summary }}"}, "severity": "${{ inputs.severity }}", "component": "${{ inputs.component }}"}}'

Copilot uses AI. Check for mistakes.

- name: Fetch PagerDuty Incidents
id: pagerduty_incident
uses: fjogeleit/http-request-action@1297c6fc63a79b147d1676540a3fd9d2e37817c5 # v1.16.5
with:
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged'
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetching all incidents from the last 15 minutes without pagination could be inefficient if there are many incidents. Consider adding pagination support or filtering by additional parameters to reduce response size.

Suggested change
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged'
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged&limit=20'

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition: the incident may not be immediately available when fetching. Consider adding retry logic or a small delay between triggering the alert (line 47) and fetching incidents (line 55).

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incident search retrieves all triggered and acknowledged incidents from the last 15 minutes without pagination limits. In high-traffic environments, this could return many incidents. Consider adding &limit=100 parameter to bound the response size.

Suggested change
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged'
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged&limit=100'

Copilot uses AI. Check for mistakes.
method: 'GET'
customHeaders: '{"Accept": "application/json", "Content-Type": "application/json", "Authorization" : "Token token=${{ inputs.api-key }}"}'

- name: Search the incident
uses: actions/github-script@v8
id: get_incident
env:
SUMMARY: ${{ steps.sanitize.outputs.summary }}
with:
script: |
const responseData = `${{ steps.pagerduty_incident.outputs.response }}`
const parsedData = JSON.parse(responseData)
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing the response data without error handling could cause the action to fail silently if the API returns invalid JSON or the response is unexpectedly structured. Add try-catch error handling to provide meaningful error messages.

Suggested change
const parsedData = JSON.parse(responseData)
let parsedData;
try {
parsedData = JSON.parse(responseData)
} catch (error) {
console.error('Failed to parse PagerDuty incident response as JSON:', error)
throw new Error('Invalid JSON response from PagerDuty incidents API')
}

Copilot uses AI. Check for mistakes.
const summary = process.env.SUMMARY
Comment on lines +71 to +73
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using template literals to embed GitHub Actions output can break if the response contains backticks or special characters. Use fromJSON directly: const parsedData = JSON.parse('${{ steps.pagerduty_incident.outputs.response }}')

Suggested change
const responseData = `${{ steps.pagerduty_incident.outputs.response }}`
const parsedData = JSON.parse(responseData)
const summary = process.env.SUMMARY
const parsedData = ${{ fromJSON(steps.pagerduty_incident.outputs.response) }}
const summary = process.env.SUMMARY

Copilot uses AI. Check for mistakes.
const specificIncident = parsedData.incidents.find(incident => incident.title === summary)
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search assumes 'parsedData.incidents' exists and is an array. If the PagerDuty API returns an error or unexpected structure, this will throw an error. Add validation to check if 'incidents' exists before accessing it.

Suggested change
const specificIncident = parsedData.incidents.find(incident => incident.title === summary)
let specificIncident = null;
if (parsedData && Array.isArray(parsedData.incidents)) {
specificIncident = parsedData.incidents.find(incident => incident.title === summary)
} else {
console.log('No incidents array found in PagerDuty response.');
}

Copilot uses AI. Check for mistakes.
if (specificIncident) {
console.log(`Found HTML URL: ${specificIncident.html_url}`)
return specificIncident.html_url
} else {
console.log('No incident found.')
return ''
}
Comment on lines +56 to +81
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The incident search queries PagerDuty immediately after triggering the alert (lines 56-62), but PagerDuty may not have processed and indexed the incident yet. Consider adding a retry mechanism with exponential backoff or a brief delay before searching.

Suggested change
- name: Fetch PagerDuty Incidents
id: pagerduty_incident
uses: fjogeleit/http-request-action@1297c6fc63a79b147d1676540a3fd9d2e37817c5 # v1.16.5
with:
url: 'https://api.pagerduty.com/incidents?since=${{steps.sanitize.outputs.time-search}}&statuses[]=triggered&statuses[]=acknowledged'
method: 'GET'
customHeaders: '{"Accept": "application/json", "Content-Type": "application/json", "Authorization" : "Token token=${{ inputs.api-key }}"}'
- name: Search the incident
uses: actions/github-script@v8
id: get_incident
env:
SUMMARY: ${{ steps.sanitize.outputs.summary }}
with:
script: |
const responseData = `${{ steps.pagerduty_incident.outputs.response }}`
const parsedData = JSON.parse(responseData)
const summary = process.env.SUMMARY
const specificIncident = parsedData.incidents.find(incident => incident.title === summary)
if (specificIncident) {
console.log(`Found HTML URL: ${specificIncident.html_url}`)
return specificIncident.html_url
} else {
console.log('No incident found.')
return ''
}
# Removed "Fetch PagerDuty Incidents" step; incident fetching and retry logic moved to next step.
- name: Search the incident with retry
uses: actions/github-script@v8
id: get_incident
env:
SUMMARY: ${{ steps.sanitize.outputs.summary }}
API_KEY: ${{ inputs.api-key }}
TIME_SEARCH: ${{ steps.sanitize.outputs.time-search }}
with:
script: |
const fetch = require('node-fetch');
const summary = process.env.SUMMARY;
const apiKey = process.env.API_KEY;
const timeSearch = process.env.TIME_SEARCH;
const maxAttempts = 5;
let attempt = 0;
let delay = 1000; // start with 1s
let specificIncident = null;
const url = `https://api.pagerduty.com/incidents?since=${encodeURIComponent(timeSearch)}&statuses[]=triggered&statuses[]=acknowledged`;
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": `Token token=${apiKey}`
};
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function findIncident() {
for (attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetch(url, { method: 'GET', headers });
if (!res.ok) {
throw new Error(`PagerDuty API error: ${res.status} ${res.statusText}`);
}
const parsedData = await res.json();
specificIncident = parsedData.incidents.find(incident => incident.title === summary);
if (specificIncident) {
console.log(`Found HTML URL: ${specificIncident.html_url} on attempt ${attempt}`);
return specificIncident.html_url;
} else {
console.log(`Attempt ${attempt}: Incident not found, retrying after ${delay}ms...`);
await sleep(delay);
delay *= 2; // exponential backoff
}
} catch (err) {
console.log(`Attempt ${attempt}: Error fetching incidents: ${err.message}`);
await sleep(delay);
delay *= 2;
}
}
console.log('No incident found after retries.');
return '';
}
return await findIncident();

Copilot uses AI. Check for mistakes.
result-encoding: string