Skip to content
Merged
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
103 changes: 103 additions & 0 deletions .github/workflows/dependabot-alerts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Dependabot alert sweep (#2233), the alert-consuming half of #2229.
#
# Dependabot's SECURITY-update PRs are turned off for this repo; its ALERTS are
# left on. Those are two independent settings, and this workflow depends on the
# split: it reads the alerts and turns them into ordinary board-tracked issues,
# so the fix is written by hand against `v2/main` like any other work.
#
# alert -> this sweep -> issue (labeled, milestoned, boarded) -> maintainer PR -> v2/main
#
# It is a SCHEDULE, not an event handler, because there is no `dependabot_alert`
# workflow trigger — that is a webhook event only. Daily is deliberate: with
# security PRs off there is no mergeable-against-`main` artifact and no window to
# race, so the merge guard #2060 needed has no analogue here. What replaces it is
# a precondition inside the script: `automated-security-fixes` is a repo SETTING
# and can be switched back on from the UI without a commit, so the sweep reads it
# back and fails loudly on an explicit `enabled: true`. That read needs a
# permission `GITHUB_TOKEN` cannot hold, so with the default token it reports
# UNVERIFIED instead — see the token notes below; it is a conditional guard, not
# an invariant.
#
# ⚠️ GitHub computes the dependency graph, and therefore every alert, from the
# DEFAULT branch (`main`), while we ship from `v2/main`. Two consequences:
#
# * An alert is re-checked against `v2/main`'s own lockfile before an issue is
# filed — hence the `ref: v2/main` checkout below. An alert whose vulnerable
# range no longer matches is already fixed on the branch we ship from and is
# waiting on a milestone merge to close, so it is skipped silently.
# * A vulnerable dependency introduced on `v2/main` and not yet merged to
# `main` produces NO alert at all. No approach that consumes GitHub's alerts
# avoids that. The release-time `npm audit --audit-level=high` report from
# #2231 is a second signal that partially covers it — at release time only;
# running that same report over `v2/main`'s lockfiles on a schedule would
# close it fully and is a separable follow-up.
#
# `vulnerability-alerts: read` is the one non-default permission, and
# `GITHUB_TOKEN` supports it — no PAT is needed to read the alerts themselves.
# Two side steps are outside its reach, and `PROJECT_TOKEN` is what covers them
# when it exists:
#
# * Writing the board card, since board #28 is an ORG project
# (`organization projects: write`). Absent, the issue is still filed labeled
# and milestoned and the next `/issue-triage` sweep boards it.
# * Reading back `automated-security-fixes`, which needs `administration: read`
# — a permission `permissions:` has no key for, so `GITHUB_TOKEN` can never
# have it. Absent, that assertion is reported as UNVERIFIED rather than
# failing the run; an explicit `enabled: true` still fails it.
#
# Nothing the sweep exists to do is skipped for want of that secret.
#
# The version-update half of #2229 is the sibling `dependency-refresh.yml`.
name: Dependabot Alert Sweep

on:
schedule:
- cron: "17 6 * * *" # 06:17 UTC daily; alerts are not minute-sensitive
workflow_dispatch:

# The marker check is a read-before-write, not an atomic one, and nothing stops
# a `workflow_dispatch` from landing on top of the scheduled run. Two overlapping
# runs would both see no open issue and both file one, which is the exact
# duplicate this sweep's whole idempotency design exists to prevent (Copilot).
# `cancel-in-progress: false` because the queued run must WAIT and then re-read
# the state the first run wrote — cancelling it would drop a sweep instead.
concurrency:
group: dependabot-alert-sweep
cancel-in-progress: false

permissions:
contents: read
issues: write
vulnerability-alerts: read
Comment thread
cliffhall marked this conversation as resolved.

jobs:
alert-sweep:
runs-on: ubuntu-latest
steps:
- name: Checkout v2/main
uses: actions/checkout@v7
with:
ref: v2/main

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "22.x"
cache: "npm"

# Root install only, and no lifecycle scripts. The sweep's one dependency
# is `semver`; it reads every lockfile as JSON and never needs a client's
# tree on disk, so the postinstall cascade into `clients/*` that
# `dependency-refresh.yml` genuinely needs (it shells out to
# `npm outdated` in each) would be minutes of nothing here.
- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Run the Dependabot alert sweep
run: node scripts/dependabot-alerts.mjs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
# Optional: an org-project PAT with `organization projects: write`.
# Absent, the issue is filed unboarded and triage picks it up.
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
26 changes: 14 additions & 12 deletions .github/workflows/dependency-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
#
# A Dependabot version-update PR carries no issue and no board card, so
# `.github/dependabot.yml` was removed outright in #2235 — npm and
# github-actions alike. This workflow is what replaced those PRs (security
# updates are a separate mechanism and stay on; see below): it runs
# `scripts/dependency-refresh.mjs` against `v2/main` once a month and files or
# updates ONE tracking issue listing every outdated npm package across the root
# install and each client, plus any workflow `uses:` ref behind its action's
# highest released version. No PR is opened automatically. A maintainer
# reviews the
# issue, picks what to bump, and opens a normal PR against `v2/main`.
# github-actions alike. This workflow is what replaced those PRs (the
# security-update half is a separate mechanism, switched off separately; see
# below): it runs `scripts/dependency-refresh.mjs` against `v2/main` once a
# month and files or updates ONE tracking issue listing every outdated npm
# package across the root install and each client, plus any workflow `uses:`
# ref behind its action's highest released version. No PR is opened
# automatically. A maintainer reviews the issue, picks what to bump, and opens
# a normal PR against `v2/main`.
#
# Dependabot SECURITY updates are unaffected: they are enabled in repo
# settings, not in a config file, and kept working while `dependabot.yml` was
# missing entirely (see #1833, #1840). They are raised against the default
# branch and still need retargeting by hand.
# Dependabot SECURITY updates are the other half, and #2233 turned their PRs
# off too — they were enabled in repo settings rather than in a config file,
# which is why they kept working while `dependabot.yml` was missing entirely
# (see #1833, #1840) and why deleting that file did not touch them. Its
# ALERTS stay on and are swept into issues daily by the sibling
# `dependabot-alerts.yml`. Between them, Dependabot opens no PRs here at all.
#
# `GITHUB_TOKEN` is sufficient: it only needs to read milestones and the public
# release feeds of the actions we use, and to create/edit an issue
Expand Down
28 changes: 27 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ inspector/
│ ├── react/ React hooks over the state stores (read during render — see React instructions)
│ └── storage/ File I/O helpers for the OAuth persist backends
├── test-servers/ Composable MCP test servers + JSON configs
├── scripts/ Root build/verify tooling: install cascade, smokes, verify:* guards
├── scripts/ Root build/verify tooling (install cascade, smokes, verify:* guards)
│ plus repo automation run from CI (the dependency + alert sweeps)
├── docs/ Task-oriented guides
├── specification/ Design/build specifications
└── .claude/skills/ The procedures (see the index above)
Expand Down Expand Up @@ -98,6 +99,31 @@ The reasoning behind each of these, and what breaks when it is ignored, is the
- **One version per install-crossing dependency.** When bumping a dependency the shared sources pull in, bump it in every install that declares it. Consolidating to the root is what makes most of these unbumpable in two places at once, but it does not retire the rule — a client's `devDependencies`, and any package that arrives transitively into a client install, can still skew against the root. Never raise the tsc heap to work around one. `npm run verify:dep-lockstep` enforces this.
- **Pin a transitive dependency with an `overrides` entry**, not with `npm audit fix` — which "resolves" an advisory with no upward escape by silently downgrading.

### Dependency updates are issue-driven, like everything else

**Dependabot opens no pull requests against this repo — neither version updates nor security updates.** A Dependabot PR carries no `Closes #N` and no board card, so it was the one standing exception to [Issue-driven Work Style](#issue-driven-work-style), enforced by nothing. Both halves are now replaced by scheduled workflows that file **issues**, and a maintainer writes the fix by hand against `v2/main`.

| Half | Switched off by | Replaced by | Cadence |
| --- | --- | --- | --- |
| Version updates | Deleting `.github/dependabot.yml` outright (#2235) — an empty `updates:` list is not valid config | `.github/workflows/dependency-refresh.yml` → `scripts/dependency-refresh.mjs`: `npm outdated` across every install, plus a `uses:` check against each action's latest release, folded into **one** tracking issue | Monthly |
| Security updates | `DELETE /repos/{owner}/{repo}/automated-security-fixes` — a **repo setting**, not a file | `.github/workflows/dependabot-alerts.yml` → `scripts/dependabot-alerts.mjs`: reads the alerts and files one issue **per bump** | Daily |

Four things about this that are not obvious from the code:

- **Dependabot *alerts* stay on.** Alerts and security-update PRs are independent settings; only the PRs are off. Turning alerts off would blind the sweep that replaced them.
- **The security half is a schedule, not an event handler**, because there is no `dependabot_alert` workflow trigger — it is a webhook event only.
- **Alerts are computed from the default branch (`main`), and we ship from `v2/main`.** So the sweep re-checks each alert's vulnerable range against `v2/main`'s own lockfile before filing, and skips one that is already fixed there. The converse is a real blind spot with no fix on this path: a vulnerable dependency introduced on `v2/main` and not yet merged to `main` produces **no alert at all**. The release-time `npm audit --audit-level=high` report (#2231) is the partial second signal — and only at release time.
- **`automated-security-fixes` can be re-enabled from the UI without a commit**, so nothing in the repo would record it. The sweep reads it back and **fails loudly on an explicit `enabled: true`**. ⚠️ It is a *conditional* guard, not an invariant: the endpoint needs `administration: read`, which `GITHUB_TOKEN` cannot be granted (`permissions:` has no such key), so under the default token the sweep logs **UNVERIFIED** and carries on rather than going red every day for an unrelated reason. Only a token carrying that scope makes it a real assertion.

An issue filed by either sweep is an ordinary board item — `v2` + `chore` + `dependabot`, the current milestone, and a card on #28. **How it gets its card differs, and the two sweeps are not interchangeable here:**

| | files the card itself? |
| --- | --- |
| Monthly version sweep | **No, never.** It does not attempt a board write at all and has no `PROJECT_TOKEN`; the issue arrives labeled and milestoned, and `/issue-triage` places it. |
| Daily security sweep | **Only when it can.** With an org-project PAT it places the card directly at **Todo / High**; without one it degrades to the same triage hand-off. |

The board write needs `organization projects: write`, which `GITHUB_TOKEN` cannot have — hence "only when it can", and hence a filed-but-unboarded issue is a normal outcome rather than a failure. **Todo, not Incoming**, when the security sweep does place it: arriving through this pipeline *is* the approval. **`High` is a standing override** of the [priority rubric](.claude/skills/issue-triage/SKILL.md), which would otherwise score a routine bump Medium; the issue body records the override so it does not read as a mis-score. ⚠️ A milestone is a precondition for placing a card — `Incoming` ⇔ no milestone — so the security sweep leaves an issue **unboarded** rather than parked at Todo when no dated milestone is open. It picks the open milestone with the nearest **due date**, ignoring undated buckets; the monthly sweep's own selection does not yet filter those out (raised on #2239), so don't read this as a guarantee both scripts already implement.
Comment thread
cliffhall marked this conversation as resolved.

## Contributing

External contributions are accepted as **issues, not pull requests** — maintainers handle design and implementation through a prompt-driven workflow.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ inspector/
├── core/ Shared code consumed via the `@inspector/core` alias (no package.json)
├── test-servers/ Composable MCP test servers + fixtures used by integration and smoke tests
├── scripts/ Root build/verify tooling (install cascade, smokes, the verify:* guards)
Comment thread
cliffhall marked this conversation as resolved.
│ and repo automation run from CI (the dependency and Dependabot-alert sweeps)
├── docs/ Task-oriented guides — see below
├── specification/ Design/build specifications
├── .claude/skills/ Agent skills: the repo's procedures, invokable by name
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"express": "^5.2.1",
"globals": "^17.7.0",
"prettier": "3.8.4",
"semver": "^7.8.5",
"typescript": "~5.9.3",
"typescript-eslint": "^8.65.0",
"vitest": "4.1.10"
Expand Down
Loading