WorkflowsMCP

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

  1. 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.

  2. 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.

  3. 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.

  4. 04

    Queue checked against existing issues

    Each queued alert's ghsa_id + repo fingerprint is searched against open Linear issues before anything gets created.

  5. 05

    Unmatched high-severity alerts filed

    issueCreate with the severity→priority mapping, the dedicated label, and the repo's mapped team.

  6. 06

    Digest assembled by repo

    Filed issues get their Linear link inline; everything else (including medium/low) is listed but unlinked.

  7. 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

  1. 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.

  2. 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.

  3. 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" }
    # }
  4. 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.

  5. 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

  1. 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.

  2. 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" }] }
      ]
    }
  3. 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

  1. 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.

  2. 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 lookup
    curl -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 } } }"}'
  3. 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 priority
    critical → 1 (Urgent)  → file as Linear issue
    high     → 2 (High)    → file as Linear issue
    medium   → —           → digest only, no issue
    low      → —           → digest only, no issue
  4. 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
          }
        }
      }'