Dependency vulnerability digest: GitHub → Slack → Linear
Org-wide Dependabot alerts digested weekly to Slack; critical/high CVEs auto-filed in Linear.
GitHub already ships a Dependabot digest — a personal setting capped at 10 repos, email-only. For an org running dozens of repos that's four gaps: nothing org-wide, no ceiling past 10, no delivery beyond email, no severity routing. This pulls open Dependabot alerts across the org, routes critical/high findings into Linear (deduplicated by GHSA id so an open CVE doesn't refile weekly), and posts everything — filed and unfiled — as one weekly Slack digest grouped by repo. Runs on Dependabot's own REST API and webhooks, no paid-tier gate.
How it flows
- 01
Weekly trigger, Monday 07:00
A platform-level cron, not tied to any single app — the same trigger shape as weekly-investor-update-draft.
- 02
Open alerts pulled for every repo in the org
Cursor-paginated GET against the org-level endpoint, filtered to state=open and severity in {high, critical} plus the medium/low pull for the digest body.
- 03
Bucketed by severity
Critical and high go into the "needs an issue" queue; medium and low go straight to the digest and are never filed.
- 04
Queue checked against existing issues
Each queued alert's ghsa_id + repo fingerprint is searched against open Linear issues before anything gets created.
- 05
Unmatched high-severity alerts filed
issueCreate with the severity→priority mapping, the dedicated label, and the repo's mapped team.
- 06
Digest assembled by repo
Filed issues get their Linear link inline; everything else (including medium/low) is listed but unlinked.
- 07
Weekly digest posted to Slack
One chat.postMessage call, Block Kit grouped by repo, into the shared channel the team actually reads.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
GitHub
Source of org-wide Dependabot alerts
- 01
Turn on Dependabot alerts for the whole org
A single repo does this at Repo → Settings → Security (the "Advanced Security" group) → check Dependabot alerts, which also switches on dependency graph since alerts need it to compute affected dependencies. For dozens of repos at once, do not use the Coverage view — it is a read-only adoption-assessment dashboard, not an enablement control (docs.github.com, confirmed 2026-08-08). Instead use a security configuration: organization Settings → the "Security" section of the sidebar → open the Advanced Security dropdown → Configurations → the Repositories tab → select the target repositories (individually, by page, or org-wide via "Select all") → open the Apply configuration dropdown → pick the GitHub-recommended configuration or a custom one with the Dependabot alerts toggle on → Apply. Confirmed current on docs.github.com 2026-08-08.
- 02
Create a read-only, org-scoped credential
Use a fine-grained PAT with the "Dependabot alerts" permission set to Read-only, scoped to the organization — this workflow never dismisses or writes an alert, so it gets nothing more. The caller must be an organization owner or hold the security manager role (Organization Settings → assign it to the team or member running this workflow); a normal repo collaborator's token is rejected on the org-level endpoint even though the same token works fine scoped to one repo. A classic PAT with the security_events scope (public_repo for public-repo-only use) also works, but the fine-grained permission is the narrower grant.
- 03
Pull open critical/high alerts across the org
GET /orgs/{org}/dependabot/alerts uses cursor pagination, not page numbers — keep following the `after` cursor in the response's Link header until it stops appearing, rather than incrementing a page count. Filter severity and state at the query level so the response does not also carry every already-fixed alert in the org's history.
GET /orgs/{org}/dependabot/alerts# $GH_TOKEN: fine-grained PAT, "Dependabot alerts: Read-only" # $ORG: organization slug curl -s -H "Authorization: Bearer $GH_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "https://api.github.com/orgs/$ORG/dependabot/alerts?state=open&severity=high,critical&per_page=100" # Link header carries the next cursor — there is no ?page= parameter: # Link: <https://api.github.com/orgs/acme/dependabot/alerts?...&after=Y3Vyc29yOnYyOpK5...>; rel="next" # One alert object (fields this workflow reads): # { # "number": 42, # "state": "open", # "dependency": { "package": { "ecosystem": "npm", "name": "lodash" } }, # "security_advisory": { "ghsa_id": "GHSA-xxxx-xxxx-xxxx", "cve_id": "CVE-2024-XXXXX", "severity": "high" }, # "security_vulnerability": { "vulnerable_version_range": "< 4.17.21", "first_patched_version": { "identifier": "4.17.21" } }, # "html_url": "https://github.com/acme/web/security/dependabot/42", # "repository": { "full_name": "acme/web" } # } - 04
(Optional) wire the real-time branch
For an alert that should not wait for the weekly run, subscribe the organization to the dependabot_alert webhook and filter to action "created". The action enum is documented (created, dismissed, fixed, reintroduced, reopened, auto_dismissed, auto_reopened, assignees_changed). The webhook reference page's payload table does not spell out severity, but the canonical payload schema (octokit/webhooks, which GitHub's own doc generation draws its examples from) confirms security_advisory.severity and security_vulnerability.severity are both present — filtering directly on severity from the delivery body is safe, but a one-time real-delivery check in your own tenant before shipping the filter is still good practice. This branch is optional: the weekly pull above is what the digest itself depends on.
- 05
Map each repo to the Linear team that owns it
Build a small repo-team-map.json keyed by the repository's full_name (e.g. "acme/web"), valued with the Linear teamId that should receive issues filed against it — the same shape as the reviewer-map.json in github-pr-review-pipeline, just pointing at Linear team ids instead of Slack ids. Give unmapped repos an explicit fallback team (e.g. a Triage team) rather than letting a missing entry throw away a high-severity alert.
Slack
Where the team actually reads the weekly digest
- 01
Create the app and invite it to the digest channel
api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes → add chat:write (add chat:write.public too if you would rather not invite it manually). Install to the workspace, then /invite @dependency-digest into the channel that should get the weekly summary — a dedicated #dependency-digest, or an existing #eng-security.
- 02
Group the message by repo and severity
Build one Block Kit message per week: a header, one section per repo naming its critical/high counts with links to any Linear issues just filed, and a context line summarizing what did not meet the bar for an issue. Keeping medium/low alerts visible in the digest — instead of only showing what got filed — is what makes it a complete picture rather than just an issue-filing log.
chat.postMessage body{ "channel": "C0DIGEST", "text": "Dependency vulnerability digest — 3 critical, 5 high, 12 medium/low across 4 repos", "blocks": [ { "type": "header", "text": { "type": "plain_text", "text": "Dependency vulnerability digest" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "*acme/web* — 1 critical (lodash), 2 high\n<https://linear.app/acme/issue/ENG-88|ENG-88> filed for the critical" } }, { "type": "context", "elements": [{ "type": "mrkdwn", "text": "12 medium/low alerts not filed — full list in the Security overview" }] } ] } - 03
Post it, and check the body — not the status code
chat.postMessage returns HTTP 200 even when it fails: a bad or missing token comes back 200 with {"ok":false,"error":"not_authed"} in the body. Read the `ok` field, not the status code, or a broken credential looks like a working integration until someone notices the digest stopped arriving.
Linear
Only critical/high severity alerts become issues
- 01
Generate a Personal API key
Linear → Settings → Security & access → Personal API keys → Create key. It goes into the Authorization header as the raw value — no "Bearer " prefix, unlike most APIs.
- 02
Look up team ids and create a dedicated label once
Query teams and issueLabels once to resolve the team ids for your repo→team map, and to create a label scoped to this workflow — call it "dependabot" or "security-vulnerability", not customer-feedback-triage's "customer-reported". Reusing a label across unrelated workflows pollutes both of their filtered views.
One-time lookupcurl -s -X POST https://api.linear.app/graphql \ -H "Authorization: $LINEAR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"{ teams { nodes { id key name } } issueLabels { nodes { id name } } }"}' - 03
Fix the severity → priority mapping before the first run
Only critical and high severity alerts become issues; medium and low stay in the Slack digest and are never filed. Write the priority mapping down explicitly rather than leaving it to the model's judgment call each run.
severity → Linear prioritycritical → 1 (Urgent) → file as Linear issue high → 2 (High) → file as Linear issue medium → — → digest only, no issue low → — → digest only, no issue - 04
De-duplicate by GHSA id, then file
An unresolved alert stays "open" across every weekly run, so pulling the same alert twice must not file it twice. Fingerprint each critical/high alert as security_advisory.ghsa_id + "::" + repository.full_name — a stable identifier, unlike the free-text titles customer-feedback-triage matches on — and search Linear for an open issue whose description already contains that fingerprint before calling issueCreate. customer-feedback-triage runs a comparable duplicate-check query shape (query Duplicates($filter: IssueFilter!) { issues(filter: $filter) { ... } }) but filters on title + a 7-day window, since its fingerprint is a fuzzy phrase; this workflow filters on description containing the exact GHSA id instead, with no date window needed because an unresolved alert stays open indefinitely, not just for a week. A hit means comment on the existing issue instead of filing a second one.
issueCreate (after the dedup check misses)# $LINEAR_API_KEY: Personal API key, raw value, no Bearer prefix curl -s -X POST https://api.linear.app/graphql \ -H "Authorization: $LINEAR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url } } }", "variables": { "input": { "teamId": "<team id from repo-team-map.json>", "title": "npm/lodash — high vulnerability in acme/web", "description": "GHSA-xxxx-xxxx-xxxx · CVE-2024-XXXXX · fingerprint: GHSA-xxxx-xxxx-xxxx::acme/web · vulnerable < 4.17.21, patched 4.17.21 · https://github.com/acme/web/security/dependabot/42", "labelIds": ["<dependabot label id, created once>"], "priority": 2 } } }'