From 5ea0f0fbfe8aa91f27c132e81bbd6a1d382c39d1 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 19:53:50 +1200 Subject: [PATCH 01/14] initial version --- specs/OP0xx-dependabot-config-conventions.md | 394 +++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 specs/OP0xx-dependabot-config-conventions.md diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md new file mode 100644 index 0000000..2fabfa0 --- /dev/null +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -0,0 +1,394 @@ +# OP??? — Dependabot config conventions for charm-tech repos + +| Field | Value | +| --- | --- | +| Status | Draft | +| Type | Process | +| Created | 16 Jun 2026 | + +## Abstract + +This spec proposes a canonical `.github/dependabot.yml` shape for +Charm Tech repositories, plus the per-repo deltas needed to apply it. The aim +is **fewer, larger, better-batched PRs at a steady cadence** without +lengthening security-patch latency. Adoption should materially reduce the +~58 Dependabot PRs/month the team currently fields across ten repos while +keeping CVE patches on a daily lane. + +## Rationale + +The Charm Tech repos emit a steady stream of Dependabot PRs. The cost is +**reviewer time and context-switching**, not the updates themselves: + +* Even a clean patch-bump needs eyes on the diff and on CI. +* Five PRs in a morning, each ~5 min, is closer to an hour once the + reviewer has re-loaded the repo's context. +* When something *does* need to land urgently — typically a CVE patch — being + two minors behind is fine; being a major behind is not, and shipping a major + as a security fix is the worst time to do it. + +The strategy is not to *update less*. It is to (a) batch routine bumps along +sensible seams, (b) keep a fast, ungrouped lane for security advisories so the +batching does not delay CVE response, and (c) give majors a longer cooldown +window and their own PR so they cannot silently ride a patch bundle. + +### Goals + +* Reduce reviewer load per repo without losing coverage of patch + minor bumps. +* Keep CVE-patch latency at "next day" or better on every repo. +* Make majors visible: they get their own PR with a longer cooldown. +* Normalise the shape of `dependabot.yml` across repos so drift is one-glance + visible. (Also, we should normalise the `.yml`/`.yaml`.) + +### Non-goals + +* Switching from Dependabot to Renovate. +* Auto-merge rules. Humans still merge; this spec is config-tuning only. +* Reviewing transitive dependencies, since we do not pick those. +* Standalone vulnerability scanning in CI (e.g. `uv audit`); see + [§Future work](#future-work). + +## Specification + +### Baseline (snapshot, 2026-06-08) + +The shape proposed below is grounded in 90 days of actual Dependabot PR +history. Window: `created:>=2026-03-08`, captured 2026-06-06. 90 days +provides a good window size, but this is slightly complicated by +including two different dependabot config shapes during the window. + +| Repo | Lang | Ecosystems | Schedule | Cooldown | Grouping | Security lane | +|---|---|---|---|---|---|---| +| `operator` | Python | github-actions, uv ×4 (root + 3 examples) | monthly | 7d | none | no | +| `charmlibs` | Python | github-actions, pip (root + `/[a-z]*`) | monthly + daily security-only | 7d on routine; none on sec | `test-deps: ["*"]` | yes | +| `jubilant` | Python | github-actions, uv | monthly | 7d | none | no | +| `pytest-jubilant` | Python | github-actions, uv | monthly | 7d | none | no | +| `pebble` | Go | github-actions, gomod | monthly / daily security-only (gomod) | 7d on gh-actions; none on sec | none | yes | +| `concierge` | Go | github-actions, gomod | monthly | 7d | none | none | no | +| `api_demo_server` | Python | github-actions, pip, docker | monthly | 7d | none | no | +| `charmhub-listing-review` | Python | github-actions, uv | monthly | 7d | none | no | +| `charm-ubuntu` | Python | github-actions, pip | monthly | 7d | none | no | +| `hyrum` | Python | github-actions, uv | monthly | 7d | none | no | + +Aggregate noise across the ten in-scope repos in the 90-day window: + +| Metric | Value | +|---|---| +| Total Dependabot PRs | **174** | +| Per-month average across the ten repos | **~58** | +| Open at snapshot | 14 | +| Merged in window | 122 | +| Closed-without-merge | 38 (22 %) | + +Per-repo volume (sorted high → low): + +| Repo | Total | Merged | Open | Closed-unmerged | TTM median | +|---|---|---|---|---|---| +| `operator` | 42 | 20 | 1 | 21 | 3.9 d | +| `charmhub-listing-review` | 26 | 25 | 0 | 1 | 26 min | +| `charmlibs` | 22 | 10 | 10 | 2 | 3.2 d | +| `api_demo_server` | 21 | 15 | 1 | 5 | 6.0 d | +| `jubilant` | 17 | 15 | 1 | 1 | 1.3 h | +| `pytest-jubilant` | 15 | 11 | 0 | 4 | 4.0 d | +| `concierge` | 11 | 11 | 0 | 0 | 4.9 d | +| `charm-ubuntu` | 10 | 8 | 0 | 2 | 17 min | +| `pebble` | 6 | 4 | 1 | 1 | 40.3 h | +| `hyrum` | 4 | 3 | 0 | 1 | 5.0 h | + +Three observations from this data drive the design: + +1. **Grouping is essentially absent** outside `charmlibs`. Five small bumps + become five PRs in every other repo. This is the single biggest noise + source. +2. **A security-only daily lane exists only on `pebble` (gomod) and + `charmlibs` (pip).** Everywhere else, a CVE patch waits for the next + monthly window. +3. **The two repos that already run two lanes have the lowest PR volume in + the set** (`pebble` 6/90d, and `charmlibs`'s low *merged* count masks an + open-PR backlog that the existing wildcard group is failing to consolidate + — see [§charmlibs delta](#charmlibs)). + +`pebble`'s shape is the proof-of-concept: two lanes work, and they work +without inflating CVE latency. + +### The canonical template + +Designed against `canonical/operator` (highest PR volume in the set, the +flagship). Root block only; per-ecosystem deltas in +[§Per-repo deltas](#per-repo-deltas). + +```yaml +version: 2 + +updates: + # =================================================================== + # GitHub Actions — routine lane (monthly, single grouped PR) + # =================================================================== + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "dependencies" + open-pull-requests-limit: 5 + cooldown: + default-days: 7 + semver-major-days: 14 + groups: + actions: + patterns: + - "*" + + # =================================================================== + # Python (uv) — routine lane (monthly, grouped along four seams) + # =================================================================== + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "monthly" + labels: + - "dependencies" + open-pull-requests-limit: 5 + cooldown: + default-days: 7 + semver-major-days: 14 + groups: + # Linters / type-checkers / formatters. Majors ride along — we do not + # pin these and a major ruff/pyright is low-risk to review in a batch. + dev-tooling: + patterns: + - "ruff" + - "pyright" + - "ty" + - "codespell" + - "coverage" + - "pre-commit" + - "types-*" + # Test runner + the jubilant/scenario test stack. + test-deps: + patterns: + - "pytest" + - "pytest-*" + - "jubilant" + - "ops-scenario" + # Docs toolchain. pygments is operator's single noisiest package + # (7 bumps/90d) and is a docs/highlighting dep — grouping it here is + # the biggest single noise win. + docs: + patterns: + - "sphinx" + - "sphinx-*" + - "furo" + - "myst-parser" + - "pygments" + # Everything else, minor + patch only. A runtime MAJOR falls through + # to its own ungrouped PR so it never silently rides a patch bundle. + runtime: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # =================================================================== + # Python (uv) — security-only lane (daily, ungrouped) + # + # open-pull-requests-limit: 0 ⇒ no version-update PRs from this entry; + # security-update PRs (which ignore the limit) still flow, daily. NO + # cooldown here on purpose — a security feed must not be delayed. + # =================================================================== + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + labels: + - "dependencies" +``` + +### Design rationale — two lanes + +Per ecosystem, **two `updates:` entries**: + +* **Routine lane** — `monthly`, grouped, `open-pull-requests-limit: 5`. Batches + the steady patch/minor stream into a handful of grouped PRs per month. +* **Security-only lane** — `daily`, ungrouped, `open-pull-requests-limit: 0`. + +`open-pull-requests-limit: 0` does **not** disable the entry — it suppresses +*version-update* PRs while *security-update* PRs (which ignore the limit) keep +flowing, checked daily. This is the pattern already proven on `pebble` +(gomod, six PRs in 90 days) and `charmlibs` (pip). The two-lane split is what +lets the routine lane go monthly without lengthening CVE-patch latency. + +Two deliberate choices: + +* **The security lane sets no `cooldown:`.** A cooldown on a security feed + defeats its purpose; no repo in the baseline does this, and the canonical + template must not paste a default cooldown there by reflex. +* **The security lane is ungrouped.** A CVE patch should be its own + reviewable PR, not bundled. + +**Multi-config note.** The routine and security lanes are two entries for the +*same* ecosystem + `directory: "/"`. Dependabot's multiple-configurations +support allows this (`charmlibs` already ships overlapping `pip` entries on +`/`). If a future Dependabot validation ever rejects the overlap, the +fallback is to keep the single routine entry and rely on Dependabot's +always-on security updates — but the explicit daily lane matches the +established precedent and makes the intent legible. + +### Group patterns + +Validated against `operator`'s actual 90-day bump stream. Four Python seams +plus one actions group: + +| Group | Patterns | Why these | +|---|---|---| +| `dev-tooling` | `ruff`, `pyright`, `ty`, `codespell`, `coverage`, `pre-commit`, `types-*` | Linters/checkers we do not pin; safe to batch incl. majors. `ruff` is a top-5 bump in `operator`/`jubilant`/`pytest-jubilant`/`charmhub-listing-review`. | +| `test-deps` | `pytest`, `pytest-*`, `jubilant`, `ops-scenario` | `pytest` is the single noisiest package in `charmlibs` (7×) and recurs everywhere. | +| `docs` | `sphinx`, `sphinx-*`, `furo`, `myst-parser`, `pygments` | `pygments` is `operator`'s #1 bump (7×/90d) and `charmlibs`' #2 (4×) — a docs/highlighting dep, pure lockfile churn. Folding it (with sphinx) into one group is the biggest single noise win. | +| `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group → its own ungrouped PR, so a major never silently rides a patch bundle. | +| `actions` | `*` | The github-actions surface is small and homogeneous; one group is plenty. | + +**Group precedence.** Dependabot assigns a dependency to the *first* matching +group in file order. `dev-tooling` / `test-deps` / `docs` are listed before +`runtime`, so e.g. `ruff` lands in `dev-tooling` (all update-types), never in +`runtime`. The `runtime` catch-all is last and only claims minor + patch. + +**Why not copy `charmlibs`' `test-deps: ["*"]`?** The baseline shows +`charmlibs` sitting on 10 open PRs (45 % of its window) *despite* that group — +because the grouped lane only targeted `directory: "/"` while the bumps were +in `/interfaces/*`. The lesson is the opposite of "one wildcard group": +sharper seams **plus** the right directory reach (see [§charmlibs delta](#charmlibs)). + +### Cooldowns and majors + +* **`cooldown.default-days: 7`** everywhere (unchanged from baseline) — gives + a week for a bad release to be yanked before we look. +* **`cooldown.semver-major-days: 14`** is new: majors get a longer settle + window to flush regressions. Cheap, and pairs with majors arriving as their + own PR. +* **Majors as their own PR** is enforced *structurally*, not by `ignore:`: + the `runtime` group's `update-types: [minor, patch]` lets a runtime major + fall through to an individual PR. This deliberately avoids the + hand-maintained per-directory `ignore:` lists that the baseline caught + drifting (`PyYAML` present in one of `operator`'s example lists, missing + from the other). +* Indentation is normalised to 2-space throughout. + +### Resolved scoring rules + +1. **Routine lane stays monthly.** Status quo. Weekly + groups produces + tighter feedback but more context-switches; the job of grouping is to + right-size the *PR*, not the cadence. Revisit only if data shows the + monthly grouped PR is so large that group-PR review itself is the + bottleneck. +2. **`charmlibs` uses per-charmlib groups.** One group config per charmlib, + not a single shared group across the monorepo. Mirrors the + per-`pyproject.toml` reality and lets reviewer routing fan out along + `CODEOWNERS` lines. +3. **SHA-pin all third-party GitHub Actions.** Security-posture win pays back + the review-cost increase; Dependabot still raises tag-tracking PRs against + the pinned SHA so updates remain visible. First-party / verified-publisher + actions stay tag-pinned. +4. **Reviewer auto-routing is off, except in `charmlibs`.** Most repos are + small enough that auto-assignment is noise. `charmlibs` follows + `CODEOWNERS` so Dependabot PRs land on the right reviewer automatically. +5. **Conventional Commits prefix is `chore: …`, no scope.** Match the repo + convention of not using scopes; do not introduce `chore(deps): …` as a + special case. + +### Per-repo deltas + +All repos use the canonical shape above; only the deltas below differ. + +| Repo | Ecosystem(s) | Delta from canonical | +|---|---|---| +| `operator` (root) | github-actions, uv | None — self-check. `examples/*` blocks are deferred (see below). | +| `charmhub-listing-review` | github-actions, uv | `+ zizmor` in `dev-tooling` (repo runs the zizmor GH-Actions linter). | +| `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | +| `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | +| `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; main win is adding the security lane. | +| `api_demo_server` | github-actions, **pip**, **docker** | `pip` + `versioning-strategy: increase`; `+ docker` ecosystem (base image, monthly grouped); `+ flit` in `dev-tooling`. | +| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine + security lanes use `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves & widens the existing glob. | +| `concierge` | github-actions, **gomod** | Add the daily security-only `gomod` lane to match `pebble`. The github-actions / gomod shape otherwise matches the template. | +| `pebble` | github-actions, **gomod** | Already two-lane; the proof-of-concept. Only normalisation at replication time (`.yaml`→`.yml`, `master`→`main` lookup path). | +| `hyrum` | github-actions, uv | Same shape as `jubilant` / `pytest-jubilant`; lowest volume in the set, fine as-is. | + +**`operator/examples/*` blocks** are out of scope of this spec. `uv.lock` +is being removed from the charmcraft profiles; the count has already dropped +5 → 4 → 3 on its own. When the remaining blocks are revisited, copy the +routine-lane shape into each surviving `examples/*` block and drop the +hand-rolled `ignore:` lists. + +**Replication hygiene.** Normalise the filename to `.github/dependabot.yml` +(`charmlibs` and `pebble` currently use `.yaml`). `pebble`'s config lives on +the `master` branch, not `main`. + +### Rollout + +Smallest-blast-radius first, one PR per repo so a regression in one does not +block the others: + +1. `charmhub-listing-review` +2. `pytest-jubilant` +3. `jubilant` +4. `charm-ubuntu` +5. `api_demo_server` +6. `charmlibs` +7. `operator` root +8. `concierge` (independent — adds the daily security `gomod` lane) +9. `pebble` (normalisation only) +10. `hyrum` (optional; lowest priority) + +### Acceptance criteria + +* Each in-scope repo has a `dependabot.yml` matching the canonical template, + or with deltas documented in this spec. +* Each in-scope repo has the daily security-only lane for every ecosystem it + configures. +* Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. +* Volume of Dependabot PRs over a 4-week window after rollout is materially + lower than the 4-week pre-window baseline. (Concrete target deferred to + step-1 data check after rollout.) + +## Future work + +* **`uv audit` as a CI gate.** Once `uv audit` + ([blog post](https://astral.sh/blog/uv-audit), Astral, 2026) leaves preview, + adding it as a CI step across the uv-managed repos is a natural complement + to the security-only Dependabot lane. The Dependabot lane alerts when a + published advisory matches a dep already locked; `uv audit` blocks a PR + that *introduces* a newly-vulnerable dep. It also adds deprecation + detection (signal for the dep-audit work) and an opt-in malware check + (`UV_MALWARE_CHECK=1`) that Dependabot provides no equivalent for. Requires + a `uv.lock`; pip-managed repos would continue with `pip-audit` or wait + for a migration path. +* **Direct-dependency audit.** Per-repo walk of direct deps and GitHub + Actions asking three questions — "do we use enough of this to justify it?", + "is there a tighter-focused alternative?", "is the action still the right + action?" — with verdicts feeding follow-up removal / replacement work. + Tracked separately from this spec. +* **Conventions note.** Once the template stabilises, fold a short + "charm-tech repo conventions" reference into this repo's README so new + repos start from this shape. + +## Open questions + +These are not blockers for the rollout above but should be settled with repo +owners during or shortly after adoption: + +1. **Weekly vs monthly routine cadence.** Defaulted to monthly. `operator`'s + 50 % closed-without-merge ratio hints that monthly batching is letting + PRs age out before merge — a point *for* weekly. Decide from the + merge-cadence data once a few months of the new template are in. +2. **Auto-merge-on-green.** Three repos look like they already auto-merge: + `charm-ubuntu` (17 min median TTM), `charmhub-listing-review` (26 min), + `jubilant` (1.3 h). Confirm whether this is a convention this spec should + leave alone or formalise. +3. **SHA-pinning posture for third-party actions.** Two repos currently + tag-pin third-party actions: `DavidAnson/markdownlint-cli2-action` + (`charmlibs`) and `aquasecurity/trivy-action` (`pebble`). Scoring rule #3 + says SHA-pin; track those two as the first migration targets. +4. **Canonical Sec org-standard.** Verify there is no org-wide + `dependabot.yml` standard from Canonical Sec that this spec needs to + align with. If one emerges later, fold a Sec review in then. From a1d324f6554da7c198d2847c97b15517ca5b201d Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:04:41 +1200 Subject: [PATCH 02/14] fix the security check bit --- specs/OP0xx-dependabot-config-conventions.md | 76 +++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 2fabfa0..22f2bae 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -101,15 +101,22 @@ Three observations from this data drive the design: become five PRs in every other repo. This is the single biggest noise source. 2. **A security-only daily lane exists only on `pebble` (gomod) and - `charmlibs` (pip).** Everywhere else, a CVE patch waits for the next - monthly window. + `charmlibs` (pip).** This is a missing *re-scan-cadence + in-repo + documentation* signal, not a missing CVE path — see the note below. 3. **The two repos that already run two lanes have the lowest PR volume in the set** (`pebble` 6/90d, and `charmlibs`'s low *merged* count masks an open-PR backlog that the existing wildcard group is failing to consolidate — see [§charmlibs delta](#charmlibs)). -`pebble`'s shape is the proof-of-concept: two lanes work, and they work -without inflating CVE latency. +**On CVE latency and the repo-level setting.** The `schedule:` field in +`dependabot.yml` governs *version-update* sweeps only. Security-update PRs +are driven by the **repo-level "Dependabot security updates" toggle** +(canonical-repo-automation sets `features.dependabot_security_updates = true` +group-wide for charm-tech, see `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`), +and open as soon as a matching GHSA advisory publishes — independent of the +YAML schedule. So a monthly routine lane does **not** delay CVE patches on any +repo where that toggle is on. The two-lane shape below is therefore a +re-scan-cadence + in-repo-documentation pattern, not a CVE-latency fix. ### The canonical template @@ -216,25 +223,51 @@ Per ecosystem, **two `updates:` entries**: `open-pull-requests-limit: 0` does **not** disable the entry — it suppresses *version-update* PRs while *security-update* PRs (which ignore the limit) keep -flowing, checked daily. This is the pattern already proven on `pebble` -(gomod, six PRs in 90 days) and `charmlibs` (pip). The two-lane split is what -lets the routine lane go monthly without lengthening CVE-patch latency. - -Two deliberate choices: - -* **The security lane sets no `cooldown:`.** A cooldown on a security feed - defeats its purpose; no repo in the baseline does this, and the canonical - template must not paste a default cooldown there by reflex. -* **The security lane is ungrouped.** A CVE patch should be its own - reviewable PR, not bundled. +flowing, checked daily. + +**This is belt-and-braces, not the primary CVE path.** The +[repo-level `dependabot_security_updates` toggle](#rationale) is what gets a +CVE patch into a PR within minutes of the advisory publishing; it works whether +or not a `dependabot.yml` ships a security lane, and it is already set +group-wide for charm-tech (with one caveat — see [§charm-tech settings +caveat](#charm-tech-settings-caveat)). What the daily YAML lane actually adds +on top is: + +* **Faster manifest re-scan.** Dependabot re-checks the lockfile against the + advisory database at the entry's `schedule:` cadence. A daily lane shortens + the worst-case window between locking a now-vulnerable transitive and + Dependabot noticing. +* **In-repo legibility.** A reviewer reading `dependabot.yml` can see that + this repo has an active security path without having to open repo settings. + +Two deliberate choices for the lane itself: + +* **No `cooldown:`.** A cooldown on a security feed defeats its purpose; no + repo in the baseline does this, and the canonical template must not paste a + default cooldown there by reflex. +* **Ungrouped.** A CVE patch should be its own reviewable PR, not bundled. **Multi-config note.** The routine and security lanes are two entries for the *same* ecosystem + `directory: "/"`. Dependabot's multiple-configurations support allows this (`charmlibs` already ships overlapping `pip` entries on -`/`). If a future Dependabot validation ever rejects the overlap, the -fallback is to keep the single routine entry and rely on Dependabot's -always-on security updates — but the explicit daily lane matches the -established precedent and makes the intent legible. +`/`). If a future Dependabot validation ever rejects the overlap, the lane +can be dropped without functional loss — the repo-level toggle still raises +security PRs on its own. + +#### charm-tech settings caveat + +`pebble/terragrunt.hcl` redeclares `features = { projects = false, wiki = +false }`, which overrides the entire `features` object set at the group level +and leaves `dependabot_security_updates` at its `null` default. The module +gates the resource on `count = var.features.dependabot_security_updates != +null`, so terraform does not manage that setting on pebble at all. + +Verified 2026-06-16: pebble's "Dependabot security updates" is in fact +**enabled** in the GitHub UI, so there is no live exposure — but the state is +unmanaged and could drift on a manual toggle. Fix in canonical-repo-automation +by adding `dependabot_security_updates = true` to pebble's `features` block +(or by merging rather than redeclaring). Pre-rollout housekeeping, not a +blocker for this spec. ### Group patterns @@ -346,6 +379,11 @@ block the others: or with deltas documented in this spec. * Each in-scope repo has the daily security-only lane for every ecosystem it configures. +* Each in-scope repo has the repo-level **"Dependabot security updates"** + setting enabled (`features.dependabot_security_updates = true`, applied via + canonical-repo-automation). This is the actual CVE path; the YAML lane is + belt-and-braces. Verify on the GitHub Settings → Code security page, not + only in the HCL — see [§charm-tech settings caveat](#charm-tech-settings-caveat). * Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. * Volume of Dependabot PRs over a 4-week window after rollout is materially lower than the 4-week pre-window baseline. (Concrete target deferred to From 2cd7f16c8cda6963f77ad5e733d6419289e6aa28 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:14:34 +1200 Subject: [PATCH 03/14] more security cleanup --- specs/OP0xx-dependabot-config-conventions.md | 141 ++++++++----------- 1 file changed, 59 insertions(+), 82 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 22f2bae..085a57c 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -1,4 +1,4 @@ -# OP??? — Dependabot config conventions for charm-tech repos +# OP??? — Dependabot config conventions for Charm Tech repos | Field | Value | | --- | --- | @@ -12,8 +12,10 @@ This spec proposes a canonical `.github/dependabot.yml` shape for Charm Tech repositories, plus the per-repo deltas needed to apply it. The aim is **fewer, larger, better-batched PRs at a steady cadence** without lengthening security-patch latency. Adoption should materially reduce the -~58 Dependabot PRs/month the team currently fields across ten repos while -keeping CVE patches on a daily lane. +~58 Dependabot PRs/month the team currently fields across ten repos. CVE +patches stay fast because they are raised by the repo-level "Dependabot +security updates" toggle (managed group-wide via canonical-repo-automation), +which is event-driven and independent of `dependabot.yml`'s schedule. ## Rationale @@ -28,9 +30,10 @@ The Charm Tech repos emit a steady stream of Dependabot PRs. The cost is as a security fix is the worst time to do it. The strategy is not to *update less*. It is to (a) batch routine bumps along -sensible seams, (b) keep a fast, ungrouped lane for security advisories so the -batching does not delay CVE response, and (c) give majors a longer cooldown -window and their own PR so they cannot silently ride a patch bundle. +sensible seams, (b) rely on the repo-level "Dependabot security updates" +toggle for the CVE path (it is event-driven, so batching the routine lane +does not delay CVE response), and (c) give majors a longer cooldown window +and their own PR so they cannot silently ride a patch bundle. ### Goals @@ -95,28 +98,28 @@ Per-repo volume (sorted high → low): | `pebble` | 6 | 4 | 1 | 1 | 40.3 h | | `hyrum` | 4 | 3 | 0 | 1 | 5.0 h | -Three observations from this data drive the design: +Two observations from this data drive the design: 1. **Grouping is essentially absent** outside `charmlibs`. Five small bumps become five PRs in every other repo. This is the single biggest noise source. -2. **A security-only daily lane exists only on `pebble` (gomod) and - `charmlibs` (pip).** This is a missing *re-scan-cadence + in-repo - documentation* signal, not a missing CVE path — see the note below. -3. **The two repos that already run two lanes have the lowest PR volume in - the set** (`pebble` 6/90d, and `charmlibs`'s low *merged* count masks an - open-PR backlog that the existing wildcard group is failing to consolidate - — see [§charmlibs delta](#charmlibs)). +2. **`charmlibs`' grouped lane is not delivering** — 10 open PRs (45 % of + its window) despite a wildcard `test-deps: ["*"]` group, because the + group only targeted `directory: "/"` while the bumps were in + `/interfaces/*`. The fix is sharper seams plus the right directory reach + (see [§charmlibs delta](#charmlibs)). **On CVE latency and the repo-level setting.** The `schedule:` field in `dependabot.yml` governs *version-update* sweeps only. Security-update PRs are driven by the **repo-level "Dependabot security updates" toggle** (canonical-repo-automation sets `features.dependabot_security_updates = true` -group-wide for charm-tech, see `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`), -and open as soon as a matching GHSA advisory publishes — independent of the -YAML schedule. So a monthly routine lane does **not** delay CVE patches on any -repo where that toggle is on. The two-lane shape below is therefore a -re-scan-cadence + in-repo-documentation pattern, not a CVE-latency fix. +group-wide for Charm Tech, see `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`), +and open as soon as a matching GHSA advisory publishes — independent of any +YAML schedule. A monthly routine lane therefore does not delay CVE patches. +The two repos that currently ship a separate daily "security lane" in YAML +(`pebble` gomod, `charmlibs` pip) get nothing measurable from it that the +repo toggle does not already provide; this spec drops the pattern (see +[§no security lane in YAML](#no-security-lane-in-yaml)). ### The canonical template @@ -125,6 +128,10 @@ flagship). Root block only; per-ecosystem deltas in [§Per-repo deltas](#per-repo-deltas). ```yaml +# Routine version-update sweeps only. CVE patches are raised by the +# repo-level "Dependabot security updates" toggle (managed in +# canonical-repo-automation: features.dependabot_security_updates = true), +# which is event-driven and does not honour the schedule below. version: 2 updates: @@ -196,65 +203,35 @@ updates: update-types: - "minor" - "patch" - - # =================================================================== - # Python (uv) — security-only lane (daily, ungrouped) - # - # open-pull-requests-limit: 0 ⇒ no version-update PRs from this entry; - # security-update PRs (which ignore the limit) still flow, daily. NO - # cooldown here on purpose — a security feed must not be delayed. - # =================================================================== - - package-ecosystem: "uv" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 0 - labels: - - "dependencies" ``` -### Design rationale — two lanes - -Per ecosystem, **two `updates:` entries**: - -* **Routine lane** — `monthly`, grouped, `open-pull-requests-limit: 5`. Batches - the steady patch/minor stream into a handful of grouped PRs per month. -* **Security-only lane** — `daily`, ungrouped, `open-pull-requests-limit: 0`. - -`open-pull-requests-limit: 0` does **not** disable the entry — it suppresses -*version-update* PRs while *security-update* PRs (which ignore the limit) keep -flowing, checked daily. +### Design rationale — one lane per ecosystem -**This is belt-and-braces, not the primary CVE path.** The -[repo-level `dependabot_security_updates` toggle](#rationale) is what gets a -CVE patch into a PR within minutes of the advisory publishing; it works whether -or not a `dependabot.yml` ships a security lane, and it is already set -group-wide for charm-tech (with one caveat — see [§charm-tech settings -caveat](#charm-tech-settings-caveat)). What the daily YAML lane actually adds -on top is: +Per ecosystem, **one `updates:` entry**: the routine lane — `monthly`, +grouped, `open-pull-requests-limit: 5`. Batches the steady patch/minor +stream into a handful of grouped PRs per month. -* **Faster manifest re-scan.** Dependabot re-checks the lockfile against the - advisory database at the entry's `schedule:` cadence. A daily lane shortens - the worst-case window between locking a now-vulnerable transitive and - Dependabot noticing. -* **In-repo legibility.** A reviewer reading `dependabot.yml` can see that - this repo has an active security path without having to open repo settings. + -Two deliberate choices for the lane itself: +**No separate security lane in YAML.** GHSA-driven security PRs come from the +repo-level "Dependabot security updates" toggle, which is event-driven +(advisory publishes → alert → PR within minutes) and ignores `dependabot.yml` +schedules entirely. The toggle is set group-wide for Charm Tech in +canonical-repo-automation (`features.dependabot_security_updates = true` in +`groups/charm-engineering/charm-tech/repos/repos-settings.hcl`). -* **No `cooldown:`.** A cooldown on a security feed defeats its purpose; no - repo in the baseline does this, and the canonical template must not paste a - default cooldown there by reflex. -* **Ungrouped.** A CVE patch should be its own reviewable PR, not bundled. +A second `updates:` entry with `schedule: daily` and +`open-pull-requests-limit: 0` — as `pebble` and `charmlibs` currently ship — +adds no measurable benefit on top of the repo toggle: the daily schedule only +governs *version-update* sweeps (suppressed here by `limit: 0` anyway), and +security PRs are event-driven, not scan-driven. The pattern is dropped from +the canonical template; the comment at the top of `dependabot.yml` points a +reader at the repo setting instead. -**Multi-config note.** The routine and security lanes are two entries for the -*same* ecosystem + `directory: "/"`. Dependabot's multiple-configurations -support allows this (`charmlibs` already ships overlapping `pip` entries on -`/`). If a future Dependabot validation ever rejects the overlap, the lane -can be dropped without functional loss — the repo-level toggle still raises -security PRs on its own. +If for any reason the repo toggle gets turned off, the recovery is to turn it +back on in canonical-repo-automation, not to paper over it in YAML. -#### charm-tech settings caveat +#### Charm Tech settings caveat `pebble/terragrunt.hcl` redeclares `features = { projects = false, wiki = false }`, which overrides the entire `features` object set at the group level @@ -340,11 +317,11 @@ All repos use the canonical shape above; only the deltas below differ. | `charmhub-listing-review` | github-actions, uv | `+ zizmor` in `dev-tooling` (repo runs the zizmor GH-Actions linter). | | `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | | `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | -| `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; main win is adding the security lane. | +| `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; mostly a grouping win. | | `api_demo_server` | github-actions, **pip**, **docker** | `pip` + `versioning-strategy: increase`; `+ docker` ecosystem (base image, monthly grouped); `+ flit` in `dev-tooling`. | -| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine + security lanes use `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves & widens the existing glob. | -| `concierge` | github-actions, **gomod** | Add the daily security-only `gomod` lane to match `pebble`. The github-actions / gomod shape otherwise matches the template. | -| `pebble` | github-actions, **gomod** | Already two-lane; the proof-of-concept. Only normalisation at replication time (`.yaml`→`.yml`, `master`→`main` lookup path). | +| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine lane uses `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves & widens the existing glob. Drop the existing daily security-only `pip` entry — superseded by the repo-level toggle. | +| `concierge` | github-actions, **gomod** | None of substance; matches the template. | +| `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry — superseded by the repo-level toggle. Normalise at replication time (`.yaml`→`.yml`, `master`→`main` lookup path). | | `hyrum` | github-actions, uv | Same shape as `jubilant` / `pytest-jubilant`; lowest volume in the set, fine as-is. | **`operator/examples/*` blocks** are out of scope of this spec. `uv.lock` @@ -369,21 +346,21 @@ block the others: 5. `api_demo_server` 6. `charmlibs` 7. `operator` root -8. `concierge` (independent — adds the daily security `gomod` lane) -9. `pebble` (normalisation only) +8. `concierge` +9. `pebble` (normalisation + drop the existing security lane) 10. `hyrum` (optional; lowest priority) ### Acceptance criteria * Each in-scope repo has a `dependabot.yml` matching the canonical template, or with deltas documented in this spec. -* Each in-scope repo has the daily security-only lane for every ecosystem it - configures. * Each in-scope repo has the repo-level **"Dependabot security updates"** setting enabled (`features.dependabot_security_updates = true`, applied via - canonical-repo-automation). This is the actual CVE path; the YAML lane is - belt-and-braces. Verify on the GitHub Settings → Code security page, not - only in the HCL — see [§charm-tech settings caveat](#charm-tech-settings-caveat). + canonical-repo-automation). This is the **only** CVE path under the new + template — verify on the GitHub Settings → Code security page for every + in-scope repo, not only in the HCL. See [§Charm Tech settings + caveat](#charm-tech-settings-caveat) for the pebble-specific terraform fix + this prompts. * Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. * Volume of Dependabot PRs over a 4-week window after rollout is materially lower than the 4-week pre-window baseline. (Concrete target deferred to @@ -407,7 +384,7 @@ block the others: action?" — with verdicts feeding follow-up removal / replacement work. Tracked separately from this spec. * **Conventions note.** Once the template stabilises, fold a short - "charm-tech repo conventions" reference into this repo's README so new + "Charm Tech repo conventions" reference into this repo's README so new repos start from this shape. ## Open questions From 999efcd79b232e14d371d4442ee6dca343377fc7 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:17:49 +1200 Subject: [PATCH 04/14] drop docs --- specs/OP0xx-dependabot-config-conventions.md | 48 ++++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 085a57c..4e45a66 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -154,7 +154,8 @@ updates: - "*" # =================================================================== - # Python (uv) — routine lane (monthly, grouped along four seams) + # Python (uv) — routine lane (monthly, grouped along three seams; + # docs toolchain is excluded — see `ignore:` block below) # =================================================================== - package-ecosystem: "uv" directory: "/" @@ -166,6 +167,22 @@ updates: cooldown: default-days: 7 semver-major-days: 14 + # Docs toolchain is tracked upstream by the Sphinx Stack project; we + # take version bumps from there, not from Dependabot. `update-types:` + # scopes the ignore to *version* updates — security PRs for these + # packages still flow via the repo-level "Dependabot security updates" + # toggle. + ignore: + - dependency-name: "sphinx" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "sphinx-*" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "furo" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "myst-parser" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "pygments" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] groups: # Linters / type-checkers / formatters. Majors ride along — we do not # pin these and a major ruff/pyright is low-risk to review in a batch. @@ -185,18 +202,9 @@ updates: - "pytest-*" - "jubilant" - "ops-scenario" - # Docs toolchain. pygments is operator's single noisiest package - # (7 bumps/90d) and is a docs/highlighting dep — grouping it here is - # the biggest single noise win. - docs: - patterns: - - "sphinx" - - "sphinx-*" - - "furo" - - "myst-parser" - - "pygments" # Everything else, minor + patch only. A runtime MAJOR falls through # to its own ungrouped PR so it never silently rides a patch bundle. + # Docs deps are filtered out at the `ignore:` block above. runtime: patterns: - "*" @@ -248,20 +256,30 @@ blocker for this spec. ### Group patterns -Validated against `operator`'s actual 90-day bump stream. Four Python seams +Validated against `operator`'s actual 90-day bump stream. Three Python seams plus one actions group: | Group | Patterns | Why these | |---|---|---| | `dev-tooling` | `ruff`, `pyright`, `ty`, `codespell`, `coverage`, `pre-commit`, `types-*` | Linters/checkers we do not pin; safe to batch incl. majors. `ruff` is a top-5 bump in `operator`/`jubilant`/`pytest-jubilant`/`charmhub-listing-review`. | | `test-deps` | `pytest`, `pytest-*`, `jubilant`, `ops-scenario` | `pytest` is the single noisiest package in `charmlibs` (7×) and recurs everywhere. | -| `docs` | `sphinx`, `sphinx-*`, `furo`, `myst-parser`, `pygments` | `pygments` is `operator`'s #1 bump (7×/90d) and `charmlibs`' #2 (4×) — a docs/highlighting dep, pure lockfile churn. Folding it (with sphinx) into one group is the biggest single noise win. | | `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group → its own ungrouped PR, so a major never silently rides a patch bundle. | | `actions` | `*` | The github-actions surface is small and homogeneous; one group is plenty. | +**Docs toolchain is excluded entirely**, not grouped. `sphinx`, `sphinx-*`, +`furo`, `myst-parser`, and `pygments` are filtered out by the entry's +`ignore:` block — we take version bumps for these from the upstream +**Sphinx Stack** project rather than from per-repo Dependabot, since the docs +stack is coupled and best updated together. This is the biggest single noise +win in the spec: `pygments` alone is `operator`'s #1 bump (7×/90d) and +`charmlibs`' #2 (4×) — pure lockfile churn that produces no per-repo signal. +Security PRs for these packages still flow (the `update-types:` on the +ignore block scopes it to *version* updates only; the repo-level "Dependabot +security updates" toggle is unaffected). + **Group precedence.** Dependabot assigns a dependency to the *first* matching -group in file order. `dev-tooling` / `test-deps` / `docs` are listed before -`runtime`, so e.g. `ruff` lands in `dev-tooling` (all update-types), never in +group in file order. `dev-tooling` / `test-deps` are listed before `runtime`, +so e.g. `ruff` lands in `dev-tooling` (all update-types), never in `runtime`. The `runtime` catch-all is last and only claims minor + patch. **Why not copy `charmlibs`' `test-deps: ["*"]`?** The baseline shows From 73ec292c3962bbbbf63ba2d78083bd475dfc6e5c Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:20:05 +1200 Subject: [PATCH 05/14] dynamic uv.lock for examples --- specs/OP0xx-dependabot-config-conventions.md | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 4e45a66..df2c4f5 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -331,7 +331,7 @@ All repos use the canonical shape above; only the deltas below differ. | Repo | Ecosystem(s) | Delta from canonical | |---|---|---| -| `operator` (root) | github-actions, uv | None — self-check. `examples/*` blocks are deferred (see below). | +| `operator` (root) | github-actions, uv | None at the root — self-check. `+ examples/httpbin-demo` as a second `uv` entry on the canonical routine-lane shape; drop the existing `k8s-5-observe` and `machine-tinyproxy` blocks (their `uv.lock` files are being removed from the repo, see below). | | `charmhub-listing-review` | github-actions, uv | `+ zizmor` in `dev-tooling` (repo runs the zizmor GH-Actions linter). | | `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | | `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | @@ -342,11 +342,20 @@ All repos use the canonical shape above; only the deltas below differ. | `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry — superseded by the repo-level toggle. Normalise at replication time (`.yaml`→`.yml`, `master`→`main` lookup path). | | `hyrum` | github-actions, uv | Same shape as `jubilant` / `pytest-jubilant`; lowest volume in the set, fine as-is. | -**`operator/examples/*` blocks** are out of scope of this spec. `uv.lock` -is being removed from the charmcraft profiles; the count has already dropped -5 → 4 → 3 on its own. When the remaining blocks are revisited, copy the -routine-lane shape into each surviving `examples/*` block and drop the -hand-rolled `ignore:` lists. +**`operator/examples/*` blocks.** The current `dependabot.yml` carries three +`examples/*` entries (`httpbin-demo`, `k8s-5-observe`, `machine-tinyproxy`), +each with a hand-rolled `ignore:` list. + +Two of those — the k8s tutorial (`k8s-5-observe`) and the machine tutorial +(`machine-tinyproxy`) — are dropping their committed `uv.lock` and will +generate the lockfile in their test runs instead. With no `uv.lock` in the +tree, Dependabot has nothing to track in those directories: **drop their +`examples/*` entries entirely** as part of this rollout. + +That leaves `httpbin-demo` as the only surviving `examples/*` block. Convert +it to the canonical routine-lane shape (groups, cooldown, no per-directory +`ignore:` list) and inherit the docs-toolchain ignore from the canonical +template. The hand-rolled `ignore:` lists then go away in all three places. **Replication hygiene.** Normalise the filename to `.github/dependabot.yml` (`charmlibs` and `pebble` currently use `.yaml`). `pebble`'s config lives on From 121c6359ae8e9843181a51a14042d228377eb757 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:22:18 +1200 Subject: [PATCH 06/14] drop irrelevant block, pr open for this already --- specs/OP0xx-dependabot-config-conventions.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index df2c4f5..2bd31bb 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -239,21 +239,6 @@ reader at the repo setting instead. If for any reason the repo toggle gets turned off, the recovery is to turn it back on in canonical-repo-automation, not to paper over it in YAML. -#### Charm Tech settings caveat - -`pebble/terragrunt.hcl` redeclares `features = { projects = false, wiki = -false }`, which overrides the entire `features` object set at the group level -and leaves `dependabot_security_updates` at its `null` default. The module -gates the resource on `count = var.features.dependabot_security_updates != -null`, so terraform does not manage that setting on pebble at all. - -Verified 2026-06-16: pebble's "Dependabot security updates" is in fact -**enabled** in the GitHub UI, so there is no live exposure — but the state is -unmanaged and could drift on a manual toggle. Fix in canonical-repo-automation -by adding `dependabot_security_updates = true` to pebble's `features` block -(or by merging rather than redeclaring). Pre-rollout housekeeping, not a -blocker for this spec. - ### Group patterns Validated against `operator`'s actual 90-day bump stream. Three Python seams From 324d5c605e3eb883254a2ec6fa12dbf91e8762c7 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:29:07 +1200 Subject: [PATCH 07/14] more tweaks --- specs/OP0xx-dependabot-config-conventions.md | 66 ++------------------ 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 2bd31bb..8bd2ee5 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -48,8 +48,6 @@ and their own PR so they cannot silently ride a patch bundle. * Switching from Dependabot to Renovate. * Auto-merge rules. Humans still merge; this spec is config-tuning only. * Reviewing transitive dependencies, since we do not pick those. -* Standalone vulnerability scanning in CI (e.g. `uv audit`); see - [§Future work](#future-work). ## Specification @@ -286,7 +284,6 @@ sharper seams **plus** the right directory reach (see [§charmlibs delta](#charm hand-maintained per-directory `ignore:` lists that the baseline caught drifting (`PyYAML` present in one of `operator`'s example lists, missing from the other). -* Indentation is normalised to 2-space throughout. ### Resolved scoring rules @@ -299,16 +296,9 @@ sharper seams **plus** the right directory reach (see [§charmlibs delta](#charm not a single shared group across the monorepo. Mirrors the per-`pyproject.toml` reality and lets reviewer routing fan out along `CODEOWNERS` lines. -3. **SHA-pin all third-party GitHub Actions.** Security-posture win pays back - the review-cost increase; Dependabot still raises tag-tracking PRs against - the pinned SHA so updates remain visible. First-party / verified-publisher - actions stay tag-pinned. -4. **Reviewer auto-routing is off, except in `charmlibs`.** Most repos are +3. **Reviewer auto-routing is off, except in `charmlibs`.** Most repos are small enough that auto-assignment is noise. `charmlibs` follows `CODEOWNERS` so Dependabot PRs land on the right reviewer automatically. -5. **Conventional Commits prefix is `chore: …`, no scope.** Match the repo - convention of not using scopes; do not introduce `chore(deps): …` as a - special case. ### Per-repo deltas @@ -343,8 +333,8 @@ it to the canonical routine-lane shape (groups, cooldown, no per-directory template. The hand-rolled `ignore:` lists then go away in all three places. **Replication hygiene.** Normalise the filename to `.github/dependabot.yml` -(`charmlibs` and `pebble` currently use `.yaml`). `pebble`'s config lives on -the `master` branch, not `main`. +(`charmlibs` and `pebble` currently use `.yaml`). Normalise indentation to +two spaces. ### Rollout @@ -357,10 +347,10 @@ block the others: 4. `charm-ubuntu` 5. `api_demo_server` 6. `charmlibs` -7. `operator` root +7. `operator` 8. `concierge` 9. `pebble` (normalisation + drop the existing security lane) -10. `hyrum` (optional; lowest priority) +10. `hyrum` ### Acceptance criteria @@ -370,52 +360,8 @@ block the others: setting enabled (`features.dependabot_security_updates = true`, applied via canonical-repo-automation). This is the **only** CVE path under the new template — verify on the GitHub Settings → Code security page for every - in-scope repo, not only in the HCL. See [§Charm Tech settings - caveat](#charm-tech-settings-caveat) for the pebble-specific terraform fix - this prompts. + in-scope repo, not only in the HCL. * Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. * Volume of Dependabot PRs over a 4-week window after rollout is materially lower than the 4-week pre-window baseline. (Concrete target deferred to step-1 data check after rollout.) - -## Future work - -* **`uv audit` as a CI gate.** Once `uv audit` - ([blog post](https://astral.sh/blog/uv-audit), Astral, 2026) leaves preview, - adding it as a CI step across the uv-managed repos is a natural complement - to the security-only Dependabot lane. The Dependabot lane alerts when a - published advisory matches a dep already locked; `uv audit` blocks a PR - that *introduces* a newly-vulnerable dep. It also adds deprecation - detection (signal for the dep-audit work) and an opt-in malware check - (`UV_MALWARE_CHECK=1`) that Dependabot provides no equivalent for. Requires - a `uv.lock`; pip-managed repos would continue with `pip-audit` or wait - for a migration path. -* **Direct-dependency audit.** Per-repo walk of direct deps and GitHub - Actions asking three questions — "do we use enough of this to justify it?", - "is there a tighter-focused alternative?", "is the action still the right - action?" — with verdicts feeding follow-up removal / replacement work. - Tracked separately from this spec. -* **Conventions note.** Once the template stabilises, fold a short - "Charm Tech repo conventions" reference into this repo's README so new - repos start from this shape. - -## Open questions - -These are not blockers for the rollout above but should be settled with repo -owners during or shortly after adoption: - -1. **Weekly vs monthly routine cadence.** Defaulted to monthly. `operator`'s - 50 % closed-without-merge ratio hints that monthly batching is letting - PRs age out before merge — a point *for* weekly. Decide from the - merge-cadence data once a few months of the new template are in. -2. **Auto-merge-on-green.** Three repos look like they already auto-merge: - `charm-ubuntu` (17 min median TTM), `charmhub-listing-review` (26 min), - `jubilant` (1.3 h). Confirm whether this is a convention this spec should - leave alone or formalise. -3. **SHA-pinning posture for third-party actions.** Two repos currently - tag-pin third-party actions: `DavidAnson/markdownlint-cli2-action` - (`charmlibs`) and `aquasecurity/trivy-action` (`pebble`). Scoring rule #3 - says SHA-pin; track those two as the first migration targets. -4. **Canonical Sec org-standard.** Verify there is no org-wide - `dependabot.yml` standard from Canonical Sec that this spec needs to - align with. If one emerges later, fold a Sec review in then. From 6040513009701aca1f00bf484cf8fb18fbfaad92 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:40:30 +1200 Subject: [PATCH 08/14] transitive deps --- specs/OP0xx-dependabot-config-conventions.md | 67 +++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 8bd2ee5..2910b7b 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -47,7 +47,9 @@ and their own PR so they cannot silently ride a patch bundle. * Switching from Dependabot to Renovate. * Auto-merge rules. Humans still merge; this spec is config-tuning only. -* Reviewing transitive dependencies, since we do not pick those. +* **Suppressing transitive bumps** is *not* a non-goal — it is open work, + blocked on upstream Dependabot. See [§Transitive dependencies](#transitive-dependencies) + and the corresponding open question. ## Specification @@ -170,6 +172,11 @@ updates: # scopes the ignore to *version* updates — security PRs for these # packages still flow via the repo-level "Dependabot security updates" # toggle. + # + # Caveat: dependabot-core#12354 — uv honours the ignore in the PR + # description but can still write the ignored dep into uv.lock as a + # side effect of another PR. The PR title/body will say "ignored"; + # the lockfile diff may show a sphinx bump anyway. Tracked upstream. ignore: - dependency-name: "sphinx" update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] @@ -285,6 +292,49 @@ sharper seams **plus** the right directory reach (see [§charmlibs delta](#charm drifting (`PyYAML` present in one of `operator`'s example lists, missing from the other). +### Transitive dependencies + +`uv.lock` and `pip` lockfile bumps for **transitive** (indirect) deps — +i.e., things we did not pick, pulled in through someone else's range — show +up as Dependabot PRs and account for a non-trivial chunk of the noise. The +canonical template does **not** filter them; they match the `runtime` +group's `"*"` pattern (by name) with `update-types: [minor, patch]`, so they +ride the monthly `runtime` PR alongside direct deps. This is honest about +the volume but does not reduce it. + +The clean fix is "raise PRs only for *direct* deps; let transitives move +when something direct pulls them, and rely on the repo-level security +toggle to catch CVEs in transitives in between". The supported way to +express that in `dependabot.yml` is `allow: [{ dependency-type: "direct" }]` +on the relevant entries. + +**We are not doing that yet** because the Dependabot integration for uv — +which is the dominant ecosystem here and where most repos are headed — is +not ready: + +* [dependabot-core#13202](https://github.com/dependabot/dependabot-core/issues/13202) + — uv classifies every dep as `production`, so `dependency-type`-based + filtering is incomplete in practice. Until that lands, an `allow:` filter + on a uv entry would silently filter the wrong set. +* [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354) + — separate bug: uv honours `ignore:` in the PR description but still + modifies `uv.lock` for the ignored dep. This already affects the + docs-toolchain `ignore:` in the canonical template: the PR will read + "ignored", but a sphinx bump can still land in `uv.lock` as a side effect + of a runtime PR. Acceptable for now (no CVE-path implication), tracked + upstream. A comment in the canonical template's `ignore:` block points + here so the next reviewer is not surprised. + +`pip` entries *could* take the `allow:` filter today (the bugs above are +uv-specific), and `pip` is documented to support `dependency-type: direct` +cleanly. We are deliberately not splitting the spec by ecosystem for this: +`charm-ubuntu` is migrating to uv soon and `charmlibs` is a likely uv +migration after that, so the pip-only window is short-lived. Cleaner to +revisit transitives once everything is on uv and the upstream classifier +issues are fixed. + +See the [corresponding open question](#open-questions). + ### Resolved scoring rules 1. **Routine lane stays monthly.** Status quo. Weekly + groups produces @@ -365,3 +415,18 @@ block the others: * Volume of Dependabot PRs over a 4-week window after rollout is materially lower than the 4-week pre-window baseline. (Concrete target deferred to step-1 data check after rollout.) + +### Open questions + +1. **Filter transitive deps once upstream is + ready.** Goal: stop raising PRs for indirect deps in the routine lane; + let them move when something direct pulls them, and rely on the + repo-level security toggle for CVEs in between. Blocked on + [dependabot-core#13202](https://github.com/dependabot/dependabot-core/issues/13202) + (uv classifies every dep as `production`, so `dependency-type`-based + filtering is incomplete). Revisit once that lands and `charm-ubuntu` / + `charmlibs` have migrated to uv, then add `allow: [{ dependency-type: + "direct" }]` to the canonical template. Track + [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354) + in parallel — it would also be nice to have the docs `ignore:` actually + keep sphinx out of `uv.lock`. From a88abb69840b254b8b26a1ea719074f369f93d5c Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 16 Jun 2026 20:54:39 +1200 Subject: [PATCH 09/14] English --- specs/OP0xx-dependabot-config-conventions.md | 114 ++++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 2910b7b..86fb07e 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -1,4 +1,4 @@ -# OP??? — Dependabot config conventions for Charm Tech repos +# OP???: Dependabot config conventions for Charm Tech repos | Field | Value | | --- | --- | @@ -25,9 +25,9 @@ The Charm Tech repos emit a steady stream of Dependabot PRs. The cost is * Even a clean patch-bump needs eyes on the diff and on CI. * Five PRs in a morning, each ~5 min, is closer to an hour once the reviewer has re-loaded the repo's context. -* When something *does* need to land urgently — typically a CVE patch — being - two minors behind is fine; being a major behind is not, and shipping a major - as a security fix is the worst time to do it. +* When something *does* need to land urgently (typically a CVE patch), + being two minors behind is fine; being a major behind is not, and + shipping a major as a security fix is the worst time to do it. The strategy is not to *update less*. It is to (a) batch routine bumps along sensible seams, (b) rely on the repo-level "Dependabot security updates" @@ -47,7 +47,7 @@ and their own PR so they cannot silently ride a patch bundle. * Switching from Dependabot to Renovate. * Auto-merge rules. Humans still merge; this spec is config-tuning only. -* **Suppressing transitive bumps** is *not* a non-goal — it is open work, +* **Suppressing transitive bumps** is *not* a non-goal: it is open work, blocked on upstream Dependabot. See [§Transitive dependencies](#transitive-dependencies) and the corresponding open question. @@ -83,7 +83,7 @@ Aggregate noise across the ten in-scope repos in the 90-day window: | Merged in window | 122 | | Closed-without-merge | 38 (22 %) | -Per-repo volume (sorted high → low): +Per-repo volume (sorted high to low): | Repo | Total | Merged | Open | Closed-unmerged | TTM median | |---|---|---|---|---|---| @@ -103,7 +103,7 @@ Two observations from this data drive the design: 1. **Grouping is essentially absent** outside `charmlibs`. Five small bumps become five PRs in every other repo. This is the single biggest noise source. -2. **`charmlibs`' grouped lane is not delivering** — 10 open PRs (45 % of +2. **`charmlibs`' grouped lane is not delivering**: 10 open PRs (45 % of its window) despite a wildcard `test-deps: ["*"]` group, because the group only targeted `directory: "/"` while the bumps were in `/interfaces/*`. The fix is sharper seams plus the right directory reach @@ -114,7 +114,7 @@ Two observations from this data drive the design: are driven by the **repo-level "Dependabot security updates" toggle** (canonical-repo-automation sets `features.dependabot_security_updates = true` group-wide for Charm Tech, see `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`), -and open as soon as a matching GHSA advisory publishes — independent of any +and open as soon as a matching GHSA advisory publishes, independent of any YAML schedule. A monthly routine lane therefore does not delay CVE patches. The two repos that currently ship a separate daily "security lane" in YAML (`pebble` gomod, `charmlibs` pip) get nothing measurable from it that the @@ -136,7 +136,7 @@ version: 2 updates: # =================================================================== - # GitHub Actions — routine lane (monthly, single grouped PR) + # GitHub Actions: routine lane (monthly, single grouped PR) # =================================================================== - package-ecosystem: "github-actions" directory: "/" @@ -154,8 +154,8 @@ updates: - "*" # =================================================================== - # Python (uv) — routine lane (monthly, grouped along three seams; - # docs toolchain is excluded — see `ignore:` block below) + # Python (uv): routine lane (monthly, grouped along three seams; + # docs toolchain is excluded, see `ignore:` block below) # =================================================================== - package-ecosystem: "uv" directory: "/" @@ -169,11 +169,11 @@ updates: semver-major-days: 14 # Docs toolchain is tracked upstream by the Sphinx Stack project; we # take version bumps from there, not from Dependabot. `update-types:` - # scopes the ignore to *version* updates — security PRs for these + # scopes the ignore to *version* updates. Security PRs for these # packages still flow via the repo-level "Dependabot security updates" # toggle. # - # Caveat: dependabot-core#12354 — uv honours the ignore in the PR + # Caveat: dependabot-core#12354. uv honours the ignore in the PR # description but can still write the ignored dep into uv.lock as a # side effect of another PR. The PR title/body will say "ignored"; # the lockfile diff may show a sphinx bump anyway. Tracked upstream. @@ -189,7 +189,7 @@ updates: - dependency-name: "pygments" update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] groups: - # Linters / type-checkers / formatters. Majors ride along — we do not + # Linters / type-checkers / formatters. Majors ride along; we do not # pin these and a major ruff/pyright is low-risk to review in a batch. dev-tooling: patterns: @@ -218,9 +218,9 @@ updates: - "patch" ``` -### Design rationale — one lane per ecosystem +### Design rationale: one lane per ecosystem -Per ecosystem, **one `updates:` entry**: the routine lane — `monthly`, +Per ecosystem, **one `updates:` entry**: the routine lane, `monthly`, grouped, `open-pull-requests-limit: 5`. Batches the steady patch/minor stream into a handful of grouped PRs per month. @@ -228,18 +228,19 @@ stream into a handful of grouped PRs per month. **No separate security lane in YAML.** GHSA-driven security PRs come from the repo-level "Dependabot security updates" toggle, which is event-driven -(advisory publishes → alert → PR within minutes) and ignores `dependabot.yml` -schedules entirely. The toggle is set group-wide for Charm Tech in -canonical-repo-automation (`features.dependabot_security_updates = true` in +(advisory publishes, alert raised, PR opened within minutes) and ignores +`dependabot.yml` schedules entirely. The toggle is set group-wide for +Charm Tech in canonical-repo-automation +(`features.dependabot_security_updates = true` in `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`). A second `updates:` entry with `schedule: daily` and -`open-pull-requests-limit: 0` — as `pebble` and `charmlibs` currently ship — -adds no measurable benefit on top of the repo toggle: the daily schedule only -governs *version-update* sweeps (suppressed here by `limit: 0` anyway), and -security PRs are event-driven, not scan-driven. The pattern is dropped from -the canonical template; the comment at the top of `dependabot.yml` points a -reader at the repo setting instead. +`open-pull-requests-limit: 0`, as `pebble` and `charmlibs` currently ship, +adds no measurable benefit on top of the repo toggle: the daily schedule +only governs *version-update* sweeps (suppressed here by `limit: 0` +anyway), and security PRs are event-driven, not scan-driven. The pattern +is dropped from the canonical template; the comment at the top of +`dependabot.yml` points a reader at the repo setting instead. If for any reason the repo toggle gets turned off, the recovery is to turn it back on in canonical-repo-automation, not to paper over it in YAML. @@ -251,36 +252,37 @@ plus one actions group: | Group | Patterns | Why these | |---|---|---| -| `dev-tooling` | `ruff`, `pyright`, `ty`, `codespell`, `coverage`, `pre-commit`, `types-*` | Linters/checkers we do not pin; safe to batch incl. majors. `ruff` is a top-5 bump in `operator`/`jubilant`/`pytest-jubilant`/`charmhub-listing-review`. | +| `dev-tooling` | `ruff`, `pyright`, `ty`, `codespell`, `coverage`, `pre-commit`, `types-*` | Linters/checkers we do not pin; safe to batch including majors. `ruff` is a top-5 bump in `operator`/`jubilant`/`pytest-jubilant`/`charmhub-listing-review`. | | `test-deps` | `pytest`, `pytest-*`, `jubilant`, `ops-scenario` | `pytest` is the single noisiest package in `charmlibs` (7×) and recurs everywhere. | -| `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group → its own ungrouped PR, so a major never silently rides a patch bundle. | +| `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group and so gets its own ungrouped PR, so a major never silently rides a patch bundle. | | `actions` | `*` | The github-actions surface is small and homogeneous; one group is plenty. | **Docs toolchain is excluded entirely**, not grouped. `sphinx`, `sphinx-*`, `furo`, `myst-parser`, and `pygments` are filtered out by the entry's -`ignore:` block — we take version bumps for these from the upstream +`ignore:` block. We take version bumps for these from the upstream **Sphinx Stack** project rather than from per-repo Dependabot, since the docs stack is coupled and best updated together. This is the biggest single noise win in the spec: `pygments` alone is `operator`'s #1 bump (7×/90d) and -`charmlibs`' #2 (4×) — pure lockfile churn that produces no per-repo signal. +`charmlibs`' #2 (4×), pure lockfile churn that produces no per-repo signal. Security PRs for these packages still flow (the `update-types:` on the ignore block scopes it to *version* updates only; the repo-level "Dependabot security updates" toggle is unaffected). **Group precedence.** Dependabot assigns a dependency to the *first* matching group in file order. `dev-tooling` / `test-deps` are listed before `runtime`, -so e.g. `ruff` lands in `dev-tooling` (all update-types), never in +so for example `ruff` lands in `dev-tooling` (all update-types), never in `runtime`. The `runtime` catch-all is last and only claims minor + patch. **Why not copy `charmlibs`' `test-deps: ["*"]`?** The baseline shows -`charmlibs` sitting on 10 open PRs (45 % of its window) *despite* that group — -because the grouped lane only targeted `directory: "/"` while the bumps were -in `/interfaces/*`. The lesson is the opposite of "one wildcard group": -sharper seams **plus** the right directory reach (see [§charmlibs delta](#charmlibs)). +`charmlibs` sitting on 10 open PRs (45 % of its window) *despite* that +group, because the grouped lane only targeted `directory: "/"` while the +bumps were in `/interfaces/*`. The lesson is the opposite of "one wildcard +group": sharper seams **plus** the right directory reach (see +[§charmlibs delta](#charmlibs)). ### Cooldowns and majors -* **`cooldown.default-days: 7`** everywhere (unchanged from baseline) — gives +* **`cooldown.default-days: 7`** everywhere (unchanged from baseline): gives a week for a bad release to be yanked before we look. * **`cooldown.semver-major-days: 14`** is new: majors get a longer settle window to flush regressions. Cheap, and pairs with majors arriving as their @@ -294,13 +296,13 @@ sharper seams **plus** the right directory reach (see [§charmlibs delta](#charm ### Transitive dependencies -`uv.lock` and `pip` lockfile bumps for **transitive** (indirect) deps — -i.e., things we did not pick, pulled in through someone else's range — show -up as Dependabot PRs and account for a non-trivial chunk of the noise. The -canonical template does **not** filter them; they match the `runtime` -group's `"*"` pattern (by name) with `update-types: [minor, patch]`, so they -ride the monthly `runtime` PR alongside direct deps. This is honest about -the volume but does not reduce it. +`uv.lock` and `pip` lockfile bumps for **transitive** (indirect) deps, +meaning things we did not pick that get pulled in through someone else's +range, show up as Dependabot PRs and account for a non-trivial chunk of the +noise. The canonical template does **not** filter them; they match the +`runtime` group's `"*"` pattern (by name) with `update-types: [minor, +patch]`, so they ride the monthly `runtime` PR alongside direct deps. This +is honest about the volume but does not reduce it. The clean fix is "raise PRs only for *direct* deps; let transitives move when something direct pulls them, and rely on the repo-level security @@ -308,16 +310,16 @@ toggle to catch CVEs in transitives in between". The supported way to express that in `dependabot.yml` is `allow: [{ dependency-type: "direct" }]` on the relevant entries. -**We are not doing that yet** because the Dependabot integration for uv — -which is the dominant ecosystem here and where most repos are headed — is -not ready: +**We are not doing that yet** because the Dependabot integration for uv +(the dominant ecosystem here, and where most repos are headed) is not +ready: -* [dependabot-core#13202](https://github.com/dependabot/dependabot-core/issues/13202) - — uv classifies every dep as `production`, so `dependency-type`-based +* [dependabot-core#13202](https://github.com/dependabot/dependabot-core/issues/13202): + uv classifies every dep as `production`, so `dependency-type`-based filtering is incomplete in practice. Until that lands, an `allow:` filter on a uv entry would silently filter the wrong set. -* [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354) - — separate bug: uv honours `ignore:` in the PR description but still +* [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354): + separate bug. uv honours `ignore:` in the PR description but still modifies `uv.lock` for the ignored dep. This already affects the docs-toolchain `ignore:` in the canonical template: the PR will read "ignored", but a sphinx bump can still land in `uv.lock` as a side effect @@ -356,23 +358,23 @@ All repos use the canonical shape above; only the deltas below differ. | Repo | Ecosystem(s) | Delta from canonical | |---|---|---| -| `operator` (root) | github-actions, uv | None at the root — self-check. `+ examples/httpbin-demo` as a second `uv` entry on the canonical routine-lane shape; drop the existing `k8s-5-observe` and `machine-tinyproxy` blocks (their `uv.lock` files are being removed from the repo, see below). | +| `operator` (root) | github-actions, uv | None at the root: self-check. `+ examples/httpbin-demo` as a second `uv` entry on the canonical routine-lane shape; drop the existing `k8s-5-observe` and `machine-tinyproxy` blocks (their `uv.lock` files are being removed from the repo, see below). | | `charmhub-listing-review` | github-actions, uv | `+ zizmor` in `dev-tooling` (repo runs the zizmor GH-Actions linter). | | `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | | `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | | `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; mostly a grouping win. | | `api_demo_server` | github-actions, **pip**, **docker** | `pip` + `versioning-strategy: increase`; `+ docker` ecosystem (base image, monthly grouped); `+ flit` in `dev-tooling`. | -| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine lane uses `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves & widens the existing glob. Drop the existing daily security-only `pip` entry — superseded by the repo-level toggle. | +| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine lane uses `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves and widens the existing glob. Drop the existing daily security-only `pip` entry; superseded by the repo-level toggle. | | `concierge` | github-actions, **gomod** | None of substance; matches the template. | -| `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry — superseded by the repo-level toggle. Normalise at replication time (`.yaml`→`.yml`, `master`→`main` lookup path). | +| `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry; superseded by the repo-level toggle. Normalise at replication time (`.yaml` to `.yml`, `master` to `main` lookup path). | | `hyrum` | github-actions, uv | Same shape as `jubilant` / `pytest-jubilant`; lowest volume in the set, fine as-is. | **`operator/examples/*` blocks.** The current `dependabot.yml` carries three `examples/*` entries (`httpbin-demo`, `k8s-5-observe`, `machine-tinyproxy`), each with a hand-rolled `ignore:` list. -Two of those — the k8s tutorial (`k8s-5-observe`) and the machine tutorial -(`machine-tinyproxy`) — are dropping their committed `uv.lock` and will +Two of those, the k8s tutorial (`k8s-5-observe`) and the machine tutorial +(`machine-tinyproxy`), are dropping their committed `uv.lock` and will generate the lockfile in their test runs instead. With no `uv.lock` in the tree, Dependabot has nothing to track in those directories: **drop their `examples/*` entries entirely** as part of this rollout. @@ -409,7 +411,7 @@ block the others: * Each in-scope repo has the repo-level **"Dependabot security updates"** setting enabled (`features.dependabot_security_updates = true`, applied via canonical-repo-automation). This is the **only** CVE path under the new - template — verify on the GitHub Settings → Code security page for every + template; verify on the GitHub Settings then Code security page for every in-scope repo, not only in the HCL. * Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. * Volume of Dependabot PRs over a 4-week window after rollout is materially @@ -428,5 +430,5 @@ block the others: `charmlibs` have migrated to uv, then add `allow: [{ dependency-type: "direct" }]` to the canonical template. Track [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354) - in parallel — it would also be nice to have the docs `ignore:` actually + in parallel; it would also be nice to have the docs `ignore:` actually keep sphinx out of `uv.lock`. From be02d29be71cc00e23435e9e12a1c4d59b2a243a Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 17 Jun 2026 08:58:59 +1200 Subject: [PATCH 10/14] address review: sequence api_demo_server behind #45; drop docs-toolchain ignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api_demo_server: row + rollout point at api_demo_server#45 baseline (pip→uv, drop Dockerfile, flit→uv_build); no-substance delta after that lands. - httpbin-demo: drop the docs-toolchain inheritance mention; canonical shape inherits as-is. - Docs toolchain: remove the `ignore:` block (sphinx/furo/myst aren't in tracked lockfiles; pygments noise belongs to the transitive-deps story). Reframe paragraph as a structural statement that the routine lane never reaches docs deps, with Sphinx Stack owning those bumps. Drop the now-moot dependabot-core#12354 caveat. Co-Authored-By: Claude Opus 4.7 --- specs/OP0xx-dependabot-config-conventions.md | 65 +++++--------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 86fb07e..10e67c1 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -154,8 +154,7 @@ updates: - "*" # =================================================================== - # Python (uv): routine lane (monthly, grouped along three seams; - # docs toolchain is excluded, see `ignore:` block below) + # Python (uv): routine lane (monthly, grouped along three seams) # =================================================================== - package-ecosystem: "uv" directory: "/" @@ -167,27 +166,6 @@ updates: cooldown: default-days: 7 semver-major-days: 14 - # Docs toolchain is tracked upstream by the Sphinx Stack project; we - # take version bumps from there, not from Dependabot. `update-types:` - # scopes the ignore to *version* updates. Security PRs for these - # packages still flow via the repo-level "Dependabot security updates" - # toggle. - # - # Caveat: dependabot-core#12354. uv honours the ignore in the PR - # description but can still write the ignored dep into uv.lock as a - # side effect of another PR. The PR title/body will say "ignored"; - # the lockfile diff may show a sphinx bump anyway. Tracked upstream. - ignore: - - dependency-name: "sphinx" - update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - - dependency-name: "sphinx-*" - update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - - dependency-name: "furo" - update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - - dependency-name: "myst-parser" - update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - - dependency-name: "pygments" - update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] groups: # Linters / type-checkers / formatters. Majors ride along; we do not # pin these and a major ruff/pyright is low-risk to review in a batch. @@ -209,7 +187,6 @@ updates: - "ops-scenario" # Everything else, minor + patch only. A runtime MAJOR falls through # to its own ungrouped PR so it never silently rides a patch bundle. - # Docs deps are filtered out at the `ignore:` block above. runtime: patterns: - "*" @@ -257,16 +234,15 @@ plus one actions group: | `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group and so gets its own ungrouped PR, so a major never silently rides a patch bundle. | | `actions` | `*` | The github-actions surface is small and homogeneous; one group is plenty. | -**Docs toolchain is excluded entirely**, not grouped. `sphinx`, `sphinx-*`, -`furo`, `myst-parser`, and `pygments` are filtered out by the entry's -`ignore:` block. We take version bumps for these from the upstream -**Sphinx Stack** project rather than from per-repo Dependabot, since the docs -stack is coupled and best updated together. This is the biggest single noise -win in the spec: `pygments` alone is `operator`'s #1 bump (7×/90d) and -`charmlibs`' #2 (4×), pure lockfile churn that produces no per-repo signal. -Security PRs for these packages still flow (the `update-types:` on the -ignore block scopes it to *version* updates only; the repo-level "Dependabot -security updates" toggle is unaffected). +**Docs-toolchain bumps are out of scope.** Version bumps for `sphinx`, +`furo`, `myst-parser`, and the rest of the docs stack are managed upstream +by the **Sphinx Stack** project rather than per-repo Dependabot, since the +docs stack is coupled and best updated together. The routine lane +structurally never sees these packages: docs deps are not present in the +tracked `uv.lock`/`requirements.txt` files, and no `dependabot.yml` entry +in this spec targets a `docs/` directory, so no `ignore:` block is needed. +Security PRs for docs packages still flow via the repo-level "Dependabot +security updates" toggle. **Group precedence.** Dependabot assigns a dependency to the *first* matching group in file order. `dev-tooling` / `test-deps` are listed before `runtime`, @@ -318,14 +294,6 @@ ready: uv classifies every dep as `production`, so `dependency-type`-based filtering is incomplete in practice. Until that lands, an `allow:` filter on a uv entry would silently filter the wrong set. -* [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354): - separate bug. uv honours `ignore:` in the PR description but still - modifies `uv.lock` for the ignored dep. This already affects the - docs-toolchain `ignore:` in the canonical template: the PR will read - "ignored", but a sphinx bump can still land in `uv.lock` as a side effect - of a runtime PR. Acceptable for now (no CVE-path implication), tracked - upstream. A comment in the canonical template's `ignore:` block points - here so the next reviewer is not surprised. `pip` entries *could* take the `allow:` filter today (the bugs above are uv-specific), and `pip` is documented to support `dependency-type: direct` @@ -363,7 +331,7 @@ All repos use the canonical shape above; only the deltas below differ. | `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | | `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | | `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; mostly a grouping win. | -| `api_demo_server` | github-actions, **pip**, **docker** | `pip` + `versioning-strategy: increase`; `+ docker` ecosystem (base image, monthly grouped); `+ flit` in `dev-tooling`. | +| `api_demo_server` | github-actions, uv | None of substance once [api_demo_server#45](https://github.com/canonical/api_demo_server/pull/45) lands (that PR converts pip→uv, drops the `Dockerfile` for a rock, and switches flit→`uv_build`). | | `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine lane uses `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves and widens the existing glob. Drop the existing daily security-only `pip` entry; superseded by the repo-level toggle. | | `concierge` | github-actions, **gomod** | None of substance; matches the template. | | `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry; superseded by the repo-level toggle. Normalise at replication time (`.yaml` to `.yml`, `master` to `main` lookup path). | @@ -381,8 +349,8 @@ tree, Dependabot has nothing to track in those directories: **drop their That leaves `httpbin-demo` as the only surviving `examples/*` block. Convert it to the canonical routine-lane shape (groups, cooldown, no per-directory -`ignore:` list) and inherit the docs-toolchain ignore from the canonical -template. The hand-rolled `ignore:` lists then go away in all three places. +`ignore:` list). The hand-rolled `ignore:` lists then go away in all three +places. **Replication hygiene.** Normalise the filename to `.github/dependabot.yml` (`charmlibs` and `pebble` currently use `.yaml`). Normalise indentation to @@ -397,7 +365,7 @@ block the others: 2. `pytest-jubilant` 3. `jubilant` 4. `charm-ubuntu` -5. `api_demo_server` +5. `api_demo_server` (after [#45](https://github.com/canonical/api_demo_server/pull/45) lands; spec baseline assumes that PR's state) 6. `charmlibs` 7. `operator` 8. `concierge` @@ -428,7 +396,4 @@ block the others: (uv classifies every dep as `production`, so `dependency-type`-based filtering is incomplete). Revisit once that lands and `charm-ubuntu` / `charmlibs` have migrated to uv, then add `allow: [{ dependency-type: - "direct" }]` to the canonical template. Track - [dependabot-core#12354](https://github.com/dependabot/dependabot-core/issues/12354) - in parallel; it would also be nice to have the docs `ignore:` actually - keep sphinx out of `uv.lock`. + "direct" }]` to the canonical template. From 0116f66bac5524abd37c49c587ea431361c803bf Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 17 Jun 2026 18:09:27 +1200 Subject: [PATCH 11/14] Apply suggestion from @james-garner-canonical Co-authored-by: James Garner --- specs/OP0xx-dependabot-config-conventions.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 10e67c1..cfdf814 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -25,9 +25,10 @@ The Charm Tech repos emit a steady stream of Dependabot PRs. The cost is * Even a clean patch-bump needs eyes on the diff and on CI. * Five PRs in a morning, each ~5 min, is closer to an hour once the reviewer has re-loaded the repo's context. -* When something *does* need to land urgently (typically a CVE patch), - being two minors behind is fine; being a major behind is not, and - shipping a major as a security fix is the worst time to do it. + +When something *does* need to land urgently (typically a CVE patch), +being two minors behind is fine; being a major behind is not, and +shipping a major as a security fix is the worst time to do it. The strategy is not to *update less*. It is to (a) batch routine bumps along sensible seams, (b) rely on the repo-level "Dependabot security updates" From 225526a1c5b941d476f047359de00b6ed32337b8 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 17 Jun 2026 19:59:06 +1200 Subject: [PATCH 12/14] docs(dependabot-spec): drop the 'not a non-goal' bullet address @dwilding: the bullet had ended up in a messy double-negative state. The transitive-deps direction is already covered by the Transitive dependencies section and the open question; the bullet added nothing. Co-Authored-By: Claude Opus 4.7 --- specs/OP0xx-dependabot-config-conventions.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index cfdf814..dcc9dde 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -48,9 +48,6 @@ and their own PR so they cannot silently ride a patch bundle. * Switching from Dependabot to Renovate. * Auto-merge rules. Humans still merge; this spec is config-tuning only. -* **Suppressing transitive bumps** is *not* a non-goal: it is open work, - blocked on upstream Dependabot. See [§Transitive dependencies](#transitive-dependencies) - and the corresponding open question. ## Specification From 30ceb27a60c6bbc380772748f104cdd410b47991 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 17 Jun 2026 20:14:03 +1200 Subject: [PATCH 13/14] docs(dependabot-spec): condense baseline; move tables to Further information address @james-garner-canonical: the Specification section opened with 60-odd lines of tables before reaching any spec text. Keep a tight takeaways block at the head of Baseline (key totals + the two observations that drive the design), and move the per-repo config snapshot, aggregate volume, and per-repo PR-volume tables into a Further information section at the end of the doc. Co-Authored-By: Claude Opus 4.7 --- specs/OP0xx-dependabot-config-conventions.md | 101 +++++++++++-------- 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index dcc9dde..83cd875 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -53,50 +53,9 @@ and their own PR so they cannot silently ride a patch bundle. ### Baseline (snapshot, 2026-06-08) -The shape proposed below is grounded in 90 days of actual Dependabot PR -history. Window: `created:>=2026-03-08`, captured 2026-06-06. 90 days -provides a good window size, but this is slightly complicated by -including two different dependabot config shapes during the window. - -| Repo | Lang | Ecosystems | Schedule | Cooldown | Grouping | Security lane | -|---|---|---|---|---|---|---| -| `operator` | Python | github-actions, uv ×4 (root + 3 examples) | monthly | 7d | none | no | -| `charmlibs` | Python | github-actions, pip (root + `/[a-z]*`) | monthly + daily security-only | 7d on routine; none on sec | `test-deps: ["*"]` | yes | -| `jubilant` | Python | github-actions, uv | monthly | 7d | none | no | -| `pytest-jubilant` | Python | github-actions, uv | monthly | 7d | none | no | -| `pebble` | Go | github-actions, gomod | monthly / daily security-only (gomod) | 7d on gh-actions; none on sec | none | yes | -| `concierge` | Go | github-actions, gomod | monthly | 7d | none | none | no | -| `api_demo_server` | Python | github-actions, pip, docker | monthly | 7d | none | no | -| `charmhub-listing-review` | Python | github-actions, uv | monthly | 7d | none | no | -| `charm-ubuntu` | Python | github-actions, pip | monthly | 7d | none | no | -| `hyrum` | Python | github-actions, uv | monthly | 7d | none | no | - -Aggregate noise across the ten in-scope repos in the 90-day window: - -| Metric | Value | -|---|---| -| Total Dependabot PRs | **174** | -| Per-month average across the ten repos | **~58** | -| Open at snapshot | 14 | -| Merged in window | 122 | -| Closed-without-merge | 38 (22 %) | - -Per-repo volume (sorted high to low): - -| Repo | Total | Merged | Open | Closed-unmerged | TTM median | -|---|---|---|---|---|---| -| `operator` | 42 | 20 | 1 | 21 | 3.9 d | -| `charmhub-listing-review` | 26 | 25 | 0 | 1 | 26 min | -| `charmlibs` | 22 | 10 | 10 | 2 | 3.2 d | -| `api_demo_server` | 21 | 15 | 1 | 5 | 6.0 d | -| `jubilant` | 17 | 15 | 1 | 1 | 1.3 h | -| `pytest-jubilant` | 15 | 11 | 0 | 4 | 4.0 d | -| `concierge` | 11 | 11 | 0 | 0 | 4.9 d | -| `charm-ubuntu` | 10 | 8 | 0 | 2 | 17 min | -| `pebble` | 6 | 4 | 1 | 1 | 40.3 h | -| `hyrum` | 4 | 3 | 0 | 1 | 5.0 h | - -Two observations from this data drive the design: +Grounded in 90 days of Dependabot PR history across the ten in-scope +repos: **174 PRs, ~58/month, 22 % closed without merge**. Two observations +drive the design: 1. **Grouping is essentially absent** outside `charmlibs`. Five small bumps become five PRs in every other repo. This is the single biggest noise @@ -107,6 +66,9 @@ Two observations from this data drive the design: `/interfaces/*`. The fix is sharper seams plus the right directory reach (see [§charmlibs delta](#charmlibs)). +Per-repo config snapshot, aggregate volume, and per-repo PR counts are in +[further information](#baseline-data). + **On CVE latency and the repo-level setting.** The `schedule:` field in `dependabot.yml` governs *version-update* sweeps only. Security-update PRs are driven by the **repo-level "Dependabot security updates" toggle** @@ -395,3 +357,54 @@ block the others: filtering is incomplete). Revisit once that lands and `charm-ubuntu` / `charmlibs` have migrated to uv, then add `allow: [{ dependency-type: "direct" }]` to the canonical template. + +## Further information + +### Baseline data + + + +Window: `created:>=2026-03-08`, captured 2026-06-06. The window includes +two different dependabot config shapes (the cutover landed mid-window), +which slightly muddies per-repo comparisons but does not change the +aggregate picture. + +Per-repo config snapshot at the start of the window: + +| Repo | Lang | Ecosystems | Schedule | Cooldown | Grouping | Security lane | +|---|---|---|---|---|---|---| +| `operator` | Python | github-actions, uv ×4 (root + 3 examples) | monthly | 7d | none | no | +| `charmlibs` | Python | github-actions, pip (root + `/[a-z]*`) | monthly + daily security-only | 7d on routine; none on sec | `test-deps: ["*"]` | yes | +| `jubilant` | Python | github-actions, uv | monthly | 7d | none | no | +| `pytest-jubilant` | Python | github-actions, uv | monthly | 7d | none | no | +| `pebble` | Go | github-actions, gomod | monthly / daily security-only (gomod) | 7d on gh-actions; none on sec | none | yes | +| `concierge` | Go | github-actions, gomod | monthly | 7d | none | none | no | +| `api_demo_server` | Python | github-actions, pip, docker | monthly | 7d | none | no | +| `charmhub-listing-review` | Python | github-actions, uv | monthly | 7d | none | no | +| `charm-ubuntu` | Python | github-actions, pip | monthly | 7d | none | no | +| `hyrum` | Python | github-actions, uv | monthly | 7d | none | no | + +Aggregate PR volume across the ten in-scope repos in the 90-day window: + +| Metric | Value | +|---|---| +| Total Dependabot PRs | **174** | +| Per-month average across the ten repos | **~58** | +| Open at snapshot | 14 | +| Merged in window | 122 | +| Closed-without-merge | 38 (22 %) | + +Per-repo volume (sorted high to low): + +| Repo | Total | Merged | Open | Closed-unmerged | TTM median | +|---|---|---|---|---|---| +| `operator` | 42 | 20 | 1 | 21 | 3.9 d | +| `charmhub-listing-review` | 26 | 25 | 0 | 1 | 26 min | +| `charmlibs` | 22 | 10 | 10 | 2 | 3.2 d | +| `api_demo_server` | 21 | 15 | 1 | 5 | 6.0 d | +| `jubilant` | 17 | 15 | 1 | 1 | 1.3 h | +| `pytest-jubilant` | 15 | 11 | 0 | 4 | 4.0 d | +| `concierge` | 11 | 11 | 0 | 0 | 4.9 d | +| `charm-ubuntu` | 10 | 8 | 0 | 2 | 17 min | +| `pebble` | 6 | 4 | 1 | 1 | 40.3 h | +| `hyrum` | 4 | 3 | 0 | 1 | 5.0 h | From a74c1bd5df4fb7576be1a9bf750f9c1a0ac135ff Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 17 Jun 2026 22:12:37 +1200 Subject: [PATCH 14/14] docs(dependabot-spec): address review feedback - Add charm-tech group (ops/ops-scenario/ops-tracing/jubilant/pytest-jubilant); remove jubilant and ops-scenario from test-deps. - Raise open-pull-requests-limit to 100 on both routine lanes; grouping is what keeps PR volume sane. - Note that requirements.txt only exists in charm-ubuntu. - Drop redundant 'why not test-deps: ["*"]' paragraph. - Trim 'majors as own PR' bullet; drop the orthogonal per-example ignore-drift parenthetical. - Rename 'Resolved scoring rules' section to 'Other decisions'. - Reshape charmlibs delta: one updates: entry per top-level library directory, so per-codeowner PR routing actually works. Add a follow-up open question on whether to tool-generate the file. - Clarify the operator row: both adding examples/httpbin-demo and dropping the obsolete k8s-5-observe / machine-tinyproxy blocks; link to context. - Drop the misleading '+ zizmor' charmhub-listing-review delta. - Spell out the 'repo-level toggle' inline and link to the rationale section. - Remove the bogus master->main pebble normalisation. - Standardise the filename on .yaml (both .yaml and .yml are GitHub- supported; .yaml matches charmcraft.yaml etc). - Replace 'None of substance' with 'No per-repo changes' to be clearer to non-author readers. - Drop the duplicate Replication hygiene paragraph; fold rationale into the Acceptance criteria bullet. - Expand Rollout lead-in to spell out what 'regression' and 'block' actually mean. Co-Authored-By: Claude Opus 4.7 --- specs/OP0xx-dependabot-config-conventions.md | 127 +++++++++++-------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/specs/OP0xx-dependabot-config-conventions.md b/specs/OP0xx-dependabot-config-conventions.md index 83cd875..a1d68b5 100644 --- a/specs/OP0xx-dependabot-config-conventions.md +++ b/specs/OP0xx-dependabot-config-conventions.md @@ -8,14 +8,14 @@ ## Abstract -This spec proposes a canonical `.github/dependabot.yml` shape for +This spec proposes a canonical `.github/dependabot.yaml` shape for Charm Tech repositories, plus the per-repo deltas needed to apply it. The aim is **fewer, larger, better-batched PRs at a steady cadence** without lengthening security-patch latency. Adoption should materially reduce the ~58 Dependabot PRs/month the team currently fields across ten repos. CVE patches stay fast because they are raised by the repo-level "Dependabot security updates" toggle (managed group-wide via canonical-repo-automation), -which is event-driven and independent of `dependabot.yml`'s schedule. +which is event-driven and independent of `dependabot.yaml`'s schedule. ## Rationale @@ -41,8 +41,8 @@ and their own PR so they cannot silently ride a patch bundle. * Reduce reviewer load per repo without losing coverage of patch + minor bumps. * Keep CVE-patch latency at "next day" or better on every repo. * Make majors visible: they get their own PR with a longer cooldown. -* Normalise the shape of `dependabot.yml` across repos so drift is one-glance - visible. (Also, we should normalise the `.yml`/`.yaml`.) +* Normalise the shape of `dependabot.yaml` across repos so drift is one-glance + visible, including the filename extension (`.yaml`, not `.yml`). ### Non-goals @@ -70,7 +70,7 @@ Per-repo config snapshot, aggregate volume, and per-repo PR counts are in [further information](#baseline-data). **On CVE latency and the repo-level setting.** The `schedule:` field in -`dependabot.yml` governs *version-update* sweeps only. Security-update PRs +`dependabot.yaml` governs *version-update* sweeps only. Security-update PRs are driven by the **repo-level "Dependabot security updates" toggle** (canonical-repo-automation sets `features.dependabot_security_updates = true` group-wide for Charm Tech, see `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`), @@ -104,7 +104,7 @@ updates: interval: "monthly" labels: - "dependencies" - open-pull-requests-limit: 5 + open-pull-requests-limit: 100 cooldown: default-days: 7 semver-major-days: 14 @@ -122,11 +122,19 @@ updates: interval: "monthly" labels: - "dependencies" - open-pull-requests-limit: 5 + open-pull-requests-limit: 100 cooldown: default-days: 7 semver-major-days: 14 groups: + # Charm Tech's own releases (lockstep-versioned; we trust the release). + charm-tech: + patterns: + - "ops" + - "ops-scenario" + - "ops-tracing" + - "jubilant" + - "pytest-jubilant" # Linters / type-checkers / formatters. Majors ride along; we do not # pin these and a major ruff/pyright is low-risk to review in a batch. dev-tooling: @@ -138,13 +146,11 @@ updates: - "coverage" - "pre-commit" - "types-*" - # Test runner + the jubilant/scenario test stack. + # Test runner + other shared test deps. test-deps: patterns: - "pytest" - "pytest-*" - - "jubilant" - - "ops-scenario" # Everything else, minor + patch only. A runtime MAJOR falls through # to its own ungrouped PR so it never silently rides a patch bundle. runtime: @@ -158,15 +164,17 @@ updates: ### Design rationale: one lane per ecosystem Per ecosystem, **one `updates:` entry**: the routine lane, `monthly`, -grouped, `open-pull-requests-limit: 5`. Batches the steady patch/minor -stream into a handful of grouped PRs per month. +grouped, `open-pull-requests-limit: 100` (effectively unlimited — grouping +is what keeps PR volume sane; a numeric cap just defers updates +arbitrarily). Batches the steady patch/minor stream into a handful of +grouped PRs per month. **No separate security lane in YAML.** GHSA-driven security PRs come from the repo-level "Dependabot security updates" toggle, which is event-driven (advisory publishes, alert raised, PR opened within minutes) and ignores -`dependabot.yml` schedules entirely. The toggle is set group-wide for +`dependabot.yaml` schedules entirely. The toggle is set group-wide for Charm Tech in canonical-repo-automation (`features.dependabot_security_updates = true` in `groups/charm-engineering/charm-tech/repos/repos-settings.hcl`). @@ -177,7 +185,7 @@ adds no measurable benefit on top of the repo toggle: the daily schedule only governs *version-update* sweeps (suppressed here by `limit: 0` anyway), and security PRs are event-driven, not scan-driven. The pattern is dropped from the canonical template; the comment at the top of -`dependabot.yml` points a reader at the repo setting instead. +`dependabot.yaml` points a reader at the repo setting instead. If for any reason the repo toggle gets turned off, the recovery is to turn it back on in canonical-repo-automation, not to paper over it in YAML. @@ -189,8 +197,9 @@ plus one actions group: | Group | Patterns | Why these | |---|---|---| +| `charm-tech` | `ops`, `ops-scenario`, `ops-tracing`, `jubilant`, `pytest-jubilant` | Charm Tech's own releases, versioned in lockstep. Bundle them so an `ops` bump and its sidecars land as one PR rather than several. | | `dev-tooling` | `ruff`, `pyright`, `ty`, `codespell`, `coverage`, `pre-commit`, `types-*` | Linters/checkers we do not pin; safe to batch including majors. `ruff` is a top-5 bump in `operator`/`jubilant`/`pytest-jubilant`/`charmhub-listing-review`. | -| `test-deps` | `pytest`, `pytest-*`, `jubilant`, `ops-scenario` | `pytest` is the single noisiest package in `charmlibs` (7×) and recurs everywhere. | +| `test-deps` | `pytest`, `pytest-*` | `pytest` is the single noisiest package in `charmlibs` (7×) and recurs everywhere. | | `runtime` | `*` (catch-all), `update-types: [minor, patch]` | Everything else. The update-type filter means a runtime **major** matches no group and so gets its own ungrouped PR, so a major never silently rides a patch bundle. | | `actions` | `*` | The github-actions surface is small and homogeneous; one group is plenty. | @@ -199,22 +208,17 @@ plus one actions group: by the **Sphinx Stack** project rather than per-repo Dependabot, since the docs stack is coupled and best updated together. The routine lane structurally never sees these packages: docs deps are not present in the -tracked `uv.lock`/`requirements.txt` files, and no `dependabot.yml` entry -in this spec targets a `docs/` directory, so no `ignore:` block is needed. +tracked `uv.lock` files (or `charm-ubuntu`'s `requirements.txt`), and no +`dependabot.yaml` entry in this spec targets a `docs/` directory, so no +`ignore:` block is needed. Security PRs for docs packages still flow via the repo-level "Dependabot security updates" toggle. **Group precedence.** Dependabot assigns a dependency to the *first* matching -group in file order. `dev-tooling` / `test-deps` are listed before `runtime`, -so for example `ruff` lands in `dev-tooling` (all update-types), never in -`runtime`. The `runtime` catch-all is last and only claims minor + patch. - -**Why not copy `charmlibs`' `test-deps: ["*"]`?** The baseline shows -`charmlibs` sitting on 10 open PRs (45 % of its window) *despite* that -group, because the grouped lane only targeted `directory: "/"` while the -bumps were in `/interfaces/*`. The lesson is the opposite of "one wildcard -group": sharper seams **plus** the right directory reach (see -[§charmlibs delta](#charmlibs)). +group in file order. `charm-tech` / `dev-tooling` / `test-deps` are listed +before `runtime`, so for example `ruff` lands in `dev-tooling` (all +update-types), never in `runtime`. The `runtime` catch-all is last and only +claims minor + patch. ### Cooldowns and majors @@ -225,10 +229,7 @@ group": sharper seams **plus** the right directory reach (see own PR. * **Majors as their own PR** is enforced *structurally*, not by `ignore:`: the `runtime` group's `update-types: [minor, patch]` lets a runtime major - fall through to an individual PR. This deliberately avoids the - hand-maintained per-directory `ignore:` lists that the baseline caught - drifting (`PyYAML` present in one of `operator`'s example lists, missing - from the other). + fall through to an individual PR. ### Transitive dependencies @@ -243,7 +244,7 @@ is honest about the volume but does not reduce it. The clean fix is "raise PRs only for *direct* deps; let transitives move when something direct pulls them, and rely on the repo-level security toggle to catch CVEs in transitives in between". The supported way to -express that in `dependabot.yml` is `allow: [{ dependency-type: "direct" }]` +express that in `dependabot.yaml` is `allow: [{ dependency-type: "direct" }]` on the relevant entries. **We are not doing that yet** because the Dependabot integration for uv @@ -265,17 +266,22 @@ issues are fixed. See the [corresponding open question](#open-questions). -### Resolved scoring rules +### Other decisions 1. **Routine lane stays monthly.** Status quo. Weekly + groups produces tighter feedback but more context-switches; the job of grouping is to right-size the *PR*, not the cadence. Revisit only if data shows the monthly grouped PR is so large that group-PR review itself is the bottleneck. -2. **`charmlibs` uses per-charmlib groups.** One group config per charmlib, - not a single shared group across the monorepo. Mirrors the - per-`pyproject.toml` reality and lets reviewer routing fan out along - `CODEOWNERS` lines. +2. **`charmlibs` PRs are separated per charmlib.** One `updates:` entry per + top-level library directory, not a single shared lane spanning the + monorepo. Required because multiple teams own different libraries (per + `CODEOWNERS`), so each PR needs to land with one team's reviewers, not + batch every team's bumps together. Dependabot's `groups:` matches on + dependency *name*, not on which directory or library depends on it, so + the only way to get per-library PRs is per-library `updates:` entries. + This is verbose enough to be a candidate for generation (see + [§open questions](#charmlibs-generation)). 3. **Reviewer auto-routing is off, except in `charmlibs`.** Most repos are small enough that auto-assignment is noise. `charmlibs` follows `CODEOWNERS` so Dependabot PRs land on the right reviewer automatically. @@ -286,18 +292,20 @@ All repos use the canonical shape above; only the deltas below differ. | Repo | Ecosystem(s) | Delta from canonical | |---|---|---| -| `operator` (root) | github-actions, uv | None at the root: self-check. `+ examples/httpbin-demo` as a second `uv` entry on the canonical routine-lane shape; drop the existing `k8s-5-observe` and `machine-tinyproxy` blocks (their `uv.lock` files are being removed from the repo, see below). | -| `charmhub-listing-review` | github-actions, uv | `+ zizmor` in `dev-tooling` (repo runs the zizmor GH-Actions linter). | -| `pytest-jubilant` | github-actions, uv | None of substance (actions-heavy; the `actions` group is the win). | -| `jubilant` | github-actions, uv | None of substance (`ops` is a dev dep, caught by `runtime`). | +| `operator` (root) | github-actions, uv | Root block matches the canonical template directly (it was designed against this repo). Add a second `uv` entry for `examples/httpbin-demo` on the canonical routine-lane shape. Drop the existing `examples/k8s-5-observe` and `examples/machine-tinyproxy` `uv` entries from the current `dependabot.yaml` (their `uv.lock` files are being removed from the repo). See [§operator/examples](#operator-examples) for context. | +| `charmhub-listing-review` | github-actions, uv | No per-repo changes; matches the template. | +| `pytest-jubilant` | github-actions, uv | No per-repo changes (actions-heavy; the `actions` group is the win). | +| `jubilant` | github-actions, uv | No per-repo changes (`ops` is a dev dep, caught by `runtime`). | | `charm-ubuntu` | github-actions, **pip** | `pip` not `uv`; `+ versioning-strategy: increase` (constraint-style requirements). Tiny surface; mostly a grouping win. | -| `api_demo_server` | github-actions, uv | None of substance once [api_demo_server#45](https://github.com/canonical/api_demo_server/pull/45) lands (that PR converts pip→uv, drops the `Dockerfile` for a rock, and switches flit→`uv_build`). | -| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** routine lane uses `directories: ["/", "/*", "/interfaces/*"]` instead of a lone `directory: "/"`, so the grouped lane actually reaches the nested lib dirs where the 10-PR backlog lives. Preserves and widens the existing glob. Drop the existing daily security-only `pip` entry; superseded by the repo-level toggle. | -| `concierge` | github-actions, **gomod** | None of substance; matches the template. | -| `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry; superseded by the repo-level toggle. Normalise at replication time (`.yaml` to `.yml`, `master` to `main` lookup path). | +| `api_demo_server` | github-actions, uv | No per-repo changes once [api_demo_server#45](https://github.com/canonical/api_demo_server/pull/45) lands (that PR converts pip→uv, drops the `Dockerfile` for a rock, and switches flit→`uv_build`). | +| `charmlibs` | github-actions, **pip** (monorepo) | **Biggest delta:** one `pip` `updates:` entry per top-level library directory (under `/` and `/interfaces/*`), each carrying the canonical groups. Required for per-codeowner PR routing — see decision 2 above. More verbose than the other repos; possibly tool-generated in future. Drop the existing daily security-only `pip` entry; superseded by the repo-level "Dependabot security updates" toggle (see [§no security lane in YAML](#no-security-lane-in-yaml)). | +| `concierge` | github-actions, **gomod** | No per-repo changes; matches the template. | +| `pebble` | github-actions, **gomod** | Drop the existing daily security-only `gomod` entry; superseded by the repo-level "Dependabot security updates" toggle (see [§no security lane in YAML](#no-security-lane-in-yaml)). | | `hyrum` | github-actions, uv | Same shape as `jubilant` / `pytest-jubilant`; lowest volume in the set, fine as-is. | -**`operator/examples/*` blocks.** The current `dependabot.yml` carries three + + +**`operator/examples/*` blocks.** The current `dependabot.yaml` carries three `examples/*` entries (`httpbin-demo`, `k8s-5-observe`, `machine-tinyproxy`), each with a hand-rolled `ignore:` list. @@ -312,14 +320,15 @@ it to the canonical routine-lane shape (groups, cooldown, no per-directory `ignore:` list). The hand-rolled `ignore:` lists then go away in all three places. -**Replication hygiene.** Normalise the filename to `.github/dependabot.yml` -(`charmlibs` and `pebble` currently use `.yaml`). Normalise indentation to -two spaces. - ### Rollout -Smallest-blast-radius first, one PR per repo so a regression in one does not -block the others: +One PR per repo, ordered smallest-blast-radius first. A "regression" here +would be the canonical template behaving unexpectedly in a repo: malformed +YAML, no PRs being raised, a grouped PR that's wildly too big to review, +or a dep silently no longer being tracked. Doing one PR per repo means if +the early adopters surface any of those, the rollout pauses there until +we adjust the template — repos later in the order are not yet committed +to the new shape, so they are not affected. 1. `charmhub-listing-review` 2. `pytest-jubilant` @@ -334,21 +343,29 @@ block the others: ### Acceptance criteria -* Each in-scope repo has a `dependabot.yml` matching the canonical template, +* Each in-scope repo has a `dependabot.yaml` matching the canonical template, or with deltas documented in this spec. * Each in-scope repo has the repo-level **"Dependabot security updates"** setting enabled (`features.dependabot_security_updates = true`, applied via canonical-repo-automation). This is the **only** CVE path under the new template; verify on the GitHub Settings then Code security page for every in-scope repo, not only in the HCL. -* Indentation is 2-space throughout. Filename is `.github/dependabot.yml`. +* Indentation is 2-space throughout. Filename is `.github/dependabot.yaml` + (`.yaml` and `.yml` are both supported by GitHub; `.yaml` matches the + extension we use elsewhere in our repos, e.g. `charmcraft.yaml`). * Volume of Dependabot PRs over a 4-week window after rollout is materially lower than the 4-week pre-window baseline. (Concrete target deferred to step-1 data check after rollout.) ### Open questions -1. **Filter transitive deps once upstream is +1. **Should `charmlibs`' `dependabot.yaml` + be tool-generated?** One `updates:` entry per top-level library directory + is verbose and easy to let drift as libraries are added or moved. A + small generator (driven by the repo's actual directory layout) with a + CI check that the committed file matches would solve both. Out of scope + for this spec; flagged for follow-up. +2. **Filter transitive deps once upstream is ready.** Goal: stop raising PRs for indirect deps in the routine lane; let them move when something direct pulls them, and rely on the repo-level security toggle for CVEs in between. Blocked on