Repository navigation
Notify status-incident-triage when a public incident opens #1482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { expect, test } from 'vitest' | ||
| import { | ||
| notifyStatusIncidentOpened, | ||
| type StatusIncidentOpenedPayload, | ||
| } from './incident-webhook.ts' | ||
|
|
||
| const payload: StatusIncidentOpenedPayload = { | ||
| event: 'incident.opened', | ||
| component: 'audit_db', | ||
| detail: 'timeout', | ||
| startedAt: 1_755_400_000_000, | ||
| statusUrl: 'https://status.kody.codes', | ||
| } | ||
|
|
||
| test('skips when the webhook URL is unset', async () => { | ||
| const result = await notifyStatusIncidentOpened({ | ||
| webhookUrl: ' ', | ||
| payload, | ||
| fetchImpl: async () => { | ||
| throw new Error('fetch should not run') | ||
| }, | ||
| }) | ||
| expect(result).toEqual({ ok: true, skipped: 'unset' }) | ||
| }) | ||
|
|
||
| test('rejects non-https webhook URLs', async () => { | ||
| const result = await notifyStatusIncidentOpened({ | ||
| webhookUrl: 'http://example.com/hook', | ||
| payload, | ||
| fetchImpl: async () => { | ||
| throw new Error('fetch should not run') | ||
| }, | ||
| }) | ||
| expect(result).toEqual({ ok: false, error: 'insecure-url' }) | ||
| }) | ||
|
|
||
| test('POSTs the incident payload and returns the status', async () => { | ||
| const calls: Array<{ url: string; init: RequestInit }> = [] | ||
| const result = await notifyStatusIncidentOpened({ | ||
| webhookUrl: | ||
| 'https://kody.codes/@kentcdodds/webhooks/status-incident-triage/incident/secret', | ||
| payload, | ||
| fetchImpl: async (input, init) => { | ||
| calls.push({ url: String(input), init: init ?? {} }) | ||
| return new Response(null, { status: 202 }) | ||
| }, | ||
| }) | ||
| expect(result).toEqual({ ok: true, status: 202 }) | ||
| expect(calls).toHaveLength(1) | ||
| expect(calls[0]?.init.method).toBe('POST') | ||
| expect(calls[0]?.init.body).toBe(JSON.stringify(payload)) | ||
| }) | ||
|
|
||
| test('maps non-OK HTTP responses without throwing', async () => { | ||
| const result = await notifyStatusIncidentOpened({ | ||
| webhookUrl: 'https://kody.codes/hook', | ||
| payload, | ||
| fetchImpl: async () => new Response('nope', { status: 503 }), | ||
| }) | ||
| expect(result).toEqual({ ok: false, error: 'http-503' }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /** | ||
| * Optional outbound notify when the status worker opens an incident. | ||
| * | ||
| * The status worker stays dependency-free (ADR 0004): this is a plain HTTPS | ||
| * POST to a minted Kody webhook URL. Probe recording must not fail if the | ||
| * webhook is unset, slow, or down. | ||
| */ | ||
|
|
||
| export const statusIncidentWebhookTimeoutMs = 3000 | ||
|
|
||
| export type StatusIncidentOpenedPayload = { | ||
| event: 'incident.opened' | ||
| component: string | ||
| detail: string | null | ||
| startedAt: number | ||
| statusUrl: string | ||
| } | ||
|
|
||
| export type StatusIncidentWebhookResult = | ||
| | { ok: true; skipped: 'unset' } | ||
| | { ok: true; status: number } | ||
| | { ok: false; error: string } | ||
|
|
||
| export async function notifyStatusIncidentOpened(input: { | ||
| webhookUrl: string | null | undefined | ||
| payload: StatusIncidentOpenedPayload | ||
| fetchImpl?: typeof fetch | ||
| }): Promise<StatusIncidentWebhookResult> { | ||
| const webhookUrl = input.webhookUrl?.trim() | ||
| if (!webhookUrl) return { ok: true, skipped: 'unset' } | ||
|
|
||
| let url: URL | ||
| try { | ||
| url = new URL(webhookUrl) | ||
| } catch { | ||
| return { ok: false, error: 'invalid-url' } | ||
| } | ||
| if (url.protocol !== 'https:') return { ok: false, error: 'insecure-url' } | ||
|
|
||
| try { | ||
| const response = await (input.fetchImpl ?? fetch)(url, { | ||
| method: 'POST', | ||
| headers: { 'content-type': 'application/json' }, | ||
| body: JSON.stringify(input.payload), | ||
| signal: AbortSignal.timeout(statusIncidentWebhookTimeoutMs), | ||
| }) | ||
| if (!response.ok) { | ||
| return { ok: false, error: `http-${response.status}` } | ||
| } | ||
| return { ok: true, status: response.status } | ||
| } catch (error) { | ||
| return { | ||
| ok: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 38035
🌐 Web query:
Cloudflare Wrangler 4.120.0 secret delete absent secret error 10056 "not found" "does not exist"💡 Result:
The error 10056 (Binding not found) in Cloudflare Wrangler when attempting to delete a secret indicates that the Cloudflare API cannot locate a binding with that specific name on the specified Worker [1]. This often occurs when there is a mismatch between how a variable is defined and how Wrangler is attempting to manage it [2]. Common causes and solutions include: 1. Secret vs. Variable Conflict: The most frequent cause is that the name you are trying to delete is already defined as an environment variable (often in your wrangler.toml or via the dashboard) rather than as a secret [3][2]. Because secrets and environment variables are managed differently, the secret delete command will fail to find it, even though a binding exists by that name [2]. - Solution: Check your Cloudflare Dashboard under Workers & Pages > [Your Worker Name] > Settings > Variables and Secrets. If the binding exists as an environment variable, you must remove or modify it there, or change its type to Secret [2]. 2. Conflicting Bindings: You may be encountering a "chicken-and-egg" scenario where the CLI prevents you from creating a secret because the name is "already in use" (Error 10053), yet it fails to delete it because it cannot find the secret binding (Error 10056) [3][2]. - Solution: Manually verify the binding type in the Cloudflare Dashboard. Removing the conflicting definition from the dashboard or your wrangler.toml file often resolves this state [2][4]. 3. Special Characters: In some versions of Wrangler, attempting to manage secrets with special characters (such as @) in the key name can trigger binding errors [5]. Ensure your secret names follow standard naming conventions (alphanumeric and underscores) to avoid parsing issues [5]. If these steps do not resolve the issue, verify that your Wrangler configuration is pointing to the correct environment and Worker name, as the error explicitly states the binding is absent on the targeted script [2][6].
Citations:
🌐 Web query:
site:github.com/cloudflare/workers-sdk "10056" wrangler secret delete💡 Result:
The error code 10056 in Wrangler, typically displayed as Binding 'NAME' not found [code: 10056], indicates that the Cloudflare API cannot locate the specified secret you are attempting to delete [1][2]. This error often occurs in the following scenarios: 1. The binding is actually a Variable, not a Secret: A common cause is that the item exists as a plain text variable in your Worker's configuration rather than a secret [1]. Because wrangler secret list and wrangler secret delete only manage objects specifically typed as secrets, they cannot see or modify plain text variables [1]. To resolve this, you can check your Worker's settings in the Cloudflare Dashboard under "Variables and Secrets" and convert the variable to a secret if necessary [1]. 2. Naming Issues or API Path Errors: In some cases, characters in the secret name (such as @) can cause issues with API path resolution, leading to the same error [2]. 3. Manual Modification/Sync Issues: If the secret was removed manually via the dashboard or through a different process, Wrangler may still be attempting to target a stale binding [1]. To troubleshoot this issue, you can: - Verify the existence and type of the binding by checking your Worker settings in the Cloudflare Dashboard (Workers & Pages > [Your Worker] > Settings > Variables and Secrets) [1]. - If the item is listed as a variable, you may need to delete it from the dashboard or re-create it as a secret [1]. - Ensure your Wrangler version is up to date, as bugs related to secret management are addressed in periodic updates to the Workers SDK [1][3].
Citations:
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 7367
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 30691
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 50372
Restrict the absent-secret match.
Match only Wrangler’s missing-secret response, such as error
10056. The current pattern also accepts unrelated failures such asnpx: command not found,Worker not found, andAPI token not found. These failures can leaveSTATUS_INCIDENT_WEBHOOK_URLdeployed while the workflow passes. Propagate every other non-zero deletion status.🤖 Prompt for AI Agents