From 650348e59c9265157c19824a1f777aa016643296 Mon Sep 17 00:00:00 2001 From: Mark Malstrom Date: Mon, 15 Jun 2026 12:24:27 -0500 Subject: [PATCH 1/2] templates: replace projected Dependabot with a local Renovate deps gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (SpecKit-idiomatic scope would be 'scaffold'; using 'templates' so the cross-repo session commit hook accepts it.) Every stack scaffold (web, go-service, apple, swift-cli, swift-package) now ships a local dependency gate instead of an autonomously-opened Dependabot config: - renovate.json + scripts/deps-check.sh projected into each repo - a deps mise task (renovate --platform=local --dry-run=full, never opens PRs, always exits 0) wired into a new check aggregate next to the stack's existing quality gates - node + npm:renovate + jq pinned per stack (the Swift stacks gain a [tools] block; Renovate is node-based, so node tooling is the cost of a stack-agnostic gate) The web scaffold drops .github/dependabot.yml.tmpl. Renovate is ecosystem-agnostic — verified end-to-end against a Go+Swift+Docker+npm tree, surfacing go, Swift-package (github-tags), docker and npm updates with majors flagged separately. web_test.go now asserts the gate files + tasks ship and that no dependabot.yml is projected. --- .../scaffolds/apple/files/mise.toml.tmpl | 15 ++++ .../scaffolds/apple/files/renovate.json | 5 ++ .../apple/files/scripts/deps-check.sh | 90 +++++++++++++++++++ .../scaffolds/go-service/files/mise.toml.tmpl | 15 ++++ .../scaffolds/go-service/files/renovate.json | 5 ++ .../go-service/files/scripts/deps-check.sh | 90 +++++++++++++++++++ .../scaffolds/swift-cli/files/mise.toml.tmpl | 18 ++++ .../scaffolds/swift-cli/files/renovate.json | 5 ++ .../swift-cli/files/scripts/deps-check.sh | 90 +++++++++++++++++++ .../swift-package/files/mise.toml.tmpl | 18 ++++ .../swift-package/files/renovate.json | 5 ++ .../swift-package/files/scripts/deps-check.sh | 90 +++++++++++++++++++ .../templates/scaffolds/web/files/mise.toml | 15 ++++ .../scaffolds/web/files/renovate.json | 5 ++ .../scaffolds/web/files/scripts/deps-check.sh | 90 +++++++++++++++++++ .../web/github/.github/dependabot.yml.tmpl | 10 --- internal/scaffold/web_test.go | 21 +++-- 17 files changed, 568 insertions(+), 19 deletions(-) create mode 100644 internal/coreassets/templates/scaffolds/apple/files/renovate.json create mode 100644 internal/coreassets/templates/scaffolds/apple/files/scripts/deps-check.sh create mode 100644 internal/coreassets/templates/scaffolds/go-service/files/renovate.json create mode 100644 internal/coreassets/templates/scaffolds/go-service/files/scripts/deps-check.sh create mode 100644 internal/coreassets/templates/scaffolds/swift-cli/files/renovate.json create mode 100644 internal/coreassets/templates/scaffolds/swift-cli/files/scripts/deps-check.sh create mode 100644 internal/coreassets/templates/scaffolds/swift-package/files/renovate.json create mode 100644 internal/coreassets/templates/scaffolds/swift-package/files/scripts/deps-check.sh create mode 100644 internal/coreassets/templates/scaffolds/web/files/renovate.json create mode 100644 internal/coreassets/templates/scaffolds/web/files/scripts/deps-check.sh delete mode 100644 internal/coreassets/templates/scaffolds/web/github/.github/dependabot.yml.tmpl diff --git a/internal/coreassets/templates/scaffolds/apple/files/mise.toml.tmpl b/internal/coreassets/templates/scaffolds/apple/files/mise.toml.tmpl index f477e4a7f4..43fda0c33f 100644 --- a/internal/coreassets/templates/scaffolds/apple/files/mise.toml.tmpl +++ b/internal/coreassets/templates/scaffolds/apple/files/mise.toml.tmpl @@ -9,6 +9,10 @@ # engine's `swift` format joins. [tools] tuist = "4.196.1" +# node runs Renovate (the local dependency gate); jq formats its report. +node = "24" +"npm:renovate" = "latest" +jq = "latest" [tasks.test] description = "run the Core suite, writing the event-stream report the engine joins" @@ -62,3 +66,14 @@ for d in Core macOS iOS; do done if [ -n "$dirs" ]; then swift format lint --strict --recursive $dirs; fi ''' + +# Local dependency check — advisory (never opens PRs, always exits 0). Replaces +# Dependabot: surfaces available updates as part of `mise run check`. +[tasks.deps] +description = "report available dependency updates (local Renovate dry-run; no PRs)" +run = "bash scripts/deps-check.sh" + +[tasks.check] +description = "all local quality gates: lint + the dependency heads-up" +depends = ["lint", "deps"] +run = ":" diff --git a/internal/coreassets/templates/scaffolds/apple/files/renovate.json b/internal/coreassets/templates/scaffolds/apple/files/renovate.json new file mode 100644 index 0000000000..c3a9215224 --- /dev/null +++ b/internal/coreassets/templates/scaffolds/apple/files/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false +} diff --git a/internal/coreassets/templates/scaffolds/apple/files/scripts/deps-check.sh b/internal/coreassets/templates/scaffolds/apple/files/scripts/deps-check.sh new file mode 100644 index 0000000000..e8f494199f --- /dev/null +++ b/internal/coreassets/templates/scaffolds/apple/files/scripts/deps-check.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Local dependency-update gate. +# +# Runs Renovate against the working tree in local/dry-run mode — it NEVER opens +# PRs or writes anything; it just reports which dependencies (npm packages, Go +# modules, Swift packages, GitHub Actions, mise tools, …) have newer versions +# available. Wired into `mise run check` so updates surface as ongoing work +# instead of as autonomously-opened Dependabot PRs. +# +# Advisory only: always exits 0. Available updates are a heads-up, not a failure, +# and a Renovate/network hiccup must not break the rest of `check`. +set -uo pipefail + +if ! command -v renovate >/dev/null 2>&1; then + echo "deps: renovate not found — enable the gate with: mise use 'npm:renovate@latest' jq" + exit 0 +fi + +# Renovate needs a github.com token to look up Actions / Go-module / Swift-package +# release versions (npm needs none). Reuse the user's existing gh auth when +# present; anonymous still works for npm, just rate-limited on github.com. +github_token="$(gh auth token 2>/dev/null || true)" + +log="$(mktemp -t renovate.XXXXXX)" +trap 'rm -f "$log"' EXIT + +GITHUB_COM_TOKEN="$github_token" \ +RENOVATE_ONBOARDING=false \ +LOG_LEVEL=debug \ +LOG_FORMAT=json \ + renovate --platform=local --dry-run=full >"$log" 2>&1 +status=$? + +if [ "$status" -ne 0 ]; then + echo "deps: renovate exited $status (skipping the update check this run)" + grep -iE '"level":(50|60)|FATAL|ERROR' "$log" | head -5 + exit 0 +fi + +# All available updates live in the single "packageFiles with updates" debug +# record: walk every dependency that carries a non-empty updates[] array and +# emit one row per update — updateType, ecosystem, name, current → target. +# Everything Renovate found is surfaced; scope what it *looks at* in renovate.json. +updates="$(jq -r ' + select(.msg == "packageFiles with updates") + | [.. | objects | select(.depName? and (.updates? | type == "array") and ((.updates | length) > 0))] + | .[] as $dep + | $dep.updates[] + | "\(.updateType // "?")\t\($dep.datasource // "-")\t\($dep.depName)\t\($dep.currentValue // $dep.currentVersion // "-")\t\(.newVersion // .newValue)" +' "$log" | sort -u | sort -t"$(printf '\t')" -k2,2 -k1,1 -k3,3)" + +if [ -z "$updates" ]; then + echo "deps: ✓ all dependencies up to date" + exit 0 +fi + +count="$(printf '%s\n' "$updates" | grep -c '')" +echo "deps: ⚠ $count update(s) available — local Renovate dry-run, no PRs opened" +echo +# Majors get their own flagged section (each is a breaking-change decision, not +# routine work); the in-range minor/patch updates follow, grouped by ecosystem. +printf '%s\n' "$updates" | awk -F'\t' ' + { + type[NR] = $1; eco[NR] = $2; name[NR] = $3; cur[NR] = $4; tgt[NR] = $5 + if ($1 == "major") majors++ + else inrange[$2]++ + } + END { + if (majors > 0) { + printf " major — review before upgrading (%d)\n", majors + for (i = 1; i <= NR; i++) + if (type[i] == "major") + printf " %-7s %-34s %s → %s\n", eco[i], name[i], cur[i], tgt[i] + print "" + } + prev = "" + for (i = 1; i <= NR; i++) { + if (type[i] == "major") continue + if (eco[i] != prev) { + if (prev != "") print "" + printf " %s (%d)\n", eco[i], inrange[eco[i]] + prev = eco[i] + } + printf " %-6s %-34s %s → %s\n", type[i], name[i], cur[i], tgt[i] + } + }' +echo +echo " In-range bumps: apply them with your stack's package manager, then re-run \`mise run deps\`." +echo " Majors are listed separately — review each one's changelog before upgrading." +exit 0 diff --git a/internal/coreassets/templates/scaffolds/go-service/files/mise.toml.tmpl b/internal/coreassets/templates/scaffolds/go-service/files/mise.toml.tmpl index bb83c2bcc8..54b70f5561 100644 --- a/internal/coreassets/templates/scaffolds/go-service/files/mise.toml.tmpl +++ b/internal/coreassets/templates/scaffolds/go-service/files/mise.toml.tmpl @@ -3,6 +3,10 @@ # engine's gotest format joins. [tools] go = "1.26" +# node runs Renovate (the local dependency gate); jq formats its report. +node = "24" +"npm:renovate" = "latest" +jq = "latest" [tasks.dev] description = "run the service" @@ -35,3 +39,14 @@ run = 'test -z "$(gofmt -l .)"' description = "regenerate the Go server from the OpenAPI contract (oapi-codegen)" run = "mkdir -p internal/api && go tool oapi-codegen -config oapi-codegen.yaml openapi.yaml" {{end}} + +# Local dependency check — advisory (never opens PRs, always exits 0). Replaces +# Dependabot: surfaces available updates as part of `mise run check`. +[tasks.deps] +description = "report available dependency updates (local Renovate dry-run; no PRs)" +run = "bash scripts/deps-check.sh" + +[tasks.check] +description = "all local quality gates: fmt:check, vet + the dependency heads-up" +depends = ["fmt:check", "vet", "deps"] +run = ":" diff --git a/internal/coreassets/templates/scaffolds/go-service/files/renovate.json b/internal/coreassets/templates/scaffolds/go-service/files/renovate.json new file mode 100644 index 0000000000..c3a9215224 --- /dev/null +++ b/internal/coreassets/templates/scaffolds/go-service/files/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false +} diff --git a/internal/coreassets/templates/scaffolds/go-service/files/scripts/deps-check.sh b/internal/coreassets/templates/scaffolds/go-service/files/scripts/deps-check.sh new file mode 100644 index 0000000000..e8f494199f --- /dev/null +++ b/internal/coreassets/templates/scaffolds/go-service/files/scripts/deps-check.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Local dependency-update gate. +# +# Runs Renovate against the working tree in local/dry-run mode — it NEVER opens +# PRs or writes anything; it just reports which dependencies (npm packages, Go +# modules, Swift packages, GitHub Actions, mise tools, …) have newer versions +# available. Wired into `mise run check` so updates surface as ongoing work +# instead of as autonomously-opened Dependabot PRs. +# +# Advisory only: always exits 0. Available updates are a heads-up, not a failure, +# and a Renovate/network hiccup must not break the rest of `check`. +set -uo pipefail + +if ! command -v renovate >/dev/null 2>&1; then + echo "deps: renovate not found — enable the gate with: mise use 'npm:renovate@latest' jq" + exit 0 +fi + +# Renovate needs a github.com token to look up Actions / Go-module / Swift-package +# release versions (npm needs none). Reuse the user's existing gh auth when +# present; anonymous still works for npm, just rate-limited on github.com. +github_token="$(gh auth token 2>/dev/null || true)" + +log="$(mktemp -t renovate.XXXXXX)" +trap 'rm -f "$log"' EXIT + +GITHUB_COM_TOKEN="$github_token" \ +RENOVATE_ONBOARDING=false \ +LOG_LEVEL=debug \ +LOG_FORMAT=json \ + renovate --platform=local --dry-run=full >"$log" 2>&1 +status=$? + +if [ "$status" -ne 0 ]; then + echo "deps: renovate exited $status (skipping the update check this run)" + grep -iE '"level":(50|60)|FATAL|ERROR' "$log" | head -5 + exit 0 +fi + +# All available updates live in the single "packageFiles with updates" debug +# record: walk every dependency that carries a non-empty updates[] array and +# emit one row per update — updateType, ecosystem, name, current → target. +# Everything Renovate found is surfaced; scope what it *looks at* in renovate.json. +updates="$(jq -r ' + select(.msg == "packageFiles with updates") + | [.. | objects | select(.depName? and (.updates? | type == "array") and ((.updates | length) > 0))] + | .[] as $dep + | $dep.updates[] + | "\(.updateType // "?")\t\($dep.datasource // "-")\t\($dep.depName)\t\($dep.currentValue // $dep.currentVersion // "-")\t\(.newVersion // .newValue)" +' "$log" | sort -u | sort -t"$(printf '\t')" -k2,2 -k1,1 -k3,3)" + +if [ -z "$updates" ]; then + echo "deps: ✓ all dependencies up to date" + exit 0 +fi + +count="$(printf '%s\n' "$updates" | grep -c '')" +echo "deps: ⚠ $count update(s) available — local Renovate dry-run, no PRs opened" +echo +# Majors get their own flagged section (each is a breaking-change decision, not +# routine work); the in-range minor/patch updates follow, grouped by ecosystem. +printf '%s\n' "$updates" | awk -F'\t' ' + { + type[NR] = $1; eco[NR] = $2; name[NR] = $3; cur[NR] = $4; tgt[NR] = $5 + if ($1 == "major") majors++ + else inrange[$2]++ + } + END { + if (majors > 0) { + printf " major — review before upgrading (%d)\n", majors + for (i = 1; i <= NR; i++) + if (type[i] == "major") + printf " %-7s %-34s %s → %s\n", eco[i], name[i], cur[i], tgt[i] + print "" + } + prev = "" + for (i = 1; i <= NR; i++) { + if (type[i] == "major") continue + if (eco[i] != prev) { + if (prev != "") print "" + printf " %s (%d)\n", eco[i], inrange[eco[i]] + prev = eco[i] + } + printf " %-6s %-34s %s → %s\n", type[i], name[i], cur[i], tgt[i] + } + }' +echo +echo " In-range bumps: apply them with your stack's package manager, then re-run \`mise run deps\`." +echo " Majors are listed separately — review each one's changelog before upgrading." +exit 0 diff --git a/internal/coreassets/templates/scaffolds/swift-cli/files/mise.toml.tmpl b/internal/coreassets/templates/scaffolds/swift-cli/files/mise.toml.tmpl index 8930d407fc..51290feb06 100644 --- a/internal/coreassets/templates/scaffolds/swift-cli/files/mise.toml.tmpl +++ b/internal/coreassets/templates/scaffolds/swift-cli/files/mise.toml.tmpl @@ -5,6 +5,13 @@ # `mise run test` writes the Swift Testing event stream the engine's `swift` format # joins. +# The local dependency gate (`mise run deps`) runs Renovate (a node tool) + jq; +# pin them here since the stack otherwise leans on the ambient Swift toolchain. +[tools] +node = "24" +"npm:renovate" = "latest" +jq = "latest" + [tasks.test] description = "run the test suite, writing the event-stream report the engine joins" run = "swift test --event-stream-output-path test.swift-events.ndjson --event-stream-version 0" @@ -41,3 +48,14 @@ for d in Sources Tests; do done if [ -n "$dirs" ]; then swift format lint --strict --recursive $dirs; fi ''' + +# Local dependency check — advisory (never opens PRs, always exits 0). Replaces +# Dependabot: surfaces available updates as part of `mise run check`. +[tasks.deps] +description = "report available dependency updates (local Renovate dry-run; no PRs)" +run = "bash scripts/deps-check.sh" + +[tasks.check] +description = "all local quality gates: lint + the dependency heads-up" +depends = ["lint", "deps"] +run = ":" diff --git a/internal/coreassets/templates/scaffolds/swift-cli/files/renovate.json b/internal/coreassets/templates/scaffolds/swift-cli/files/renovate.json new file mode 100644 index 0000000000..c3a9215224 --- /dev/null +++ b/internal/coreassets/templates/scaffolds/swift-cli/files/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false +} diff --git a/internal/coreassets/templates/scaffolds/swift-cli/files/scripts/deps-check.sh b/internal/coreassets/templates/scaffolds/swift-cli/files/scripts/deps-check.sh new file mode 100644 index 0000000000..e8f494199f --- /dev/null +++ b/internal/coreassets/templates/scaffolds/swift-cli/files/scripts/deps-check.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Local dependency-update gate. +# +# Runs Renovate against the working tree in local/dry-run mode — it NEVER opens +# PRs or writes anything; it just reports which dependencies (npm packages, Go +# modules, Swift packages, GitHub Actions, mise tools, …) have newer versions +# available. Wired into `mise run check` so updates surface as ongoing work +# instead of as autonomously-opened Dependabot PRs. +# +# Advisory only: always exits 0. Available updates are a heads-up, not a failure, +# and a Renovate/network hiccup must not break the rest of `check`. +set -uo pipefail + +if ! command -v renovate >/dev/null 2>&1; then + echo "deps: renovate not found — enable the gate with: mise use 'npm:renovate@latest' jq" + exit 0 +fi + +# Renovate needs a github.com token to look up Actions / Go-module / Swift-package +# release versions (npm needs none). Reuse the user's existing gh auth when +# present; anonymous still works for npm, just rate-limited on github.com. +github_token="$(gh auth token 2>/dev/null || true)" + +log="$(mktemp -t renovate.XXXXXX)" +trap 'rm -f "$log"' EXIT + +GITHUB_COM_TOKEN="$github_token" \ +RENOVATE_ONBOARDING=false \ +LOG_LEVEL=debug \ +LOG_FORMAT=json \ + renovate --platform=local --dry-run=full >"$log" 2>&1 +status=$? + +if [ "$status" -ne 0 ]; then + echo "deps: renovate exited $status (skipping the update check this run)" + grep -iE '"level":(50|60)|FATAL|ERROR' "$log" | head -5 + exit 0 +fi + +# All available updates live in the single "packageFiles with updates" debug +# record: walk every dependency that carries a non-empty updates[] array and +# emit one row per update — updateType, ecosystem, name, current → target. +# Everything Renovate found is surfaced; scope what it *looks at* in renovate.json. +updates="$(jq -r ' + select(.msg == "packageFiles with updates") + | [.. | objects | select(.depName? and (.updates? | type == "array") and ((.updates | length) > 0))] + | .[] as $dep + | $dep.updates[] + | "\(.updateType // "?")\t\($dep.datasource // "-")\t\($dep.depName)\t\($dep.currentValue // $dep.currentVersion // "-")\t\(.newVersion // .newValue)" +' "$log" | sort -u | sort -t"$(printf '\t')" -k2,2 -k1,1 -k3,3)" + +if [ -z "$updates" ]; then + echo "deps: ✓ all dependencies up to date" + exit 0 +fi + +count="$(printf '%s\n' "$updates" | grep -c '')" +echo "deps: ⚠ $count update(s) available — local Renovate dry-run, no PRs opened" +echo +# Majors get their own flagged section (each is a breaking-change decision, not +# routine work); the in-range minor/patch updates follow, grouped by ecosystem. +printf '%s\n' "$updates" | awk -F'\t' ' + { + type[NR] = $1; eco[NR] = $2; name[NR] = $3; cur[NR] = $4; tgt[NR] = $5 + if ($1 == "major") majors++ + else inrange[$2]++ + } + END { + if (majors > 0) { + printf " major — review before upgrading (%d)\n", majors + for (i = 1; i <= NR; i++) + if (type[i] == "major") + printf " %-7s %-34s %s → %s\n", eco[i], name[i], cur[i], tgt[i] + print "" + } + prev = "" + for (i = 1; i <= NR; i++) { + if (type[i] == "major") continue + if (eco[i] != prev) { + if (prev != "") print "" + printf " %s (%d)\n", eco[i], inrange[eco[i]] + prev = eco[i] + } + printf " %-6s %-34s %s → %s\n", type[i], name[i], cur[i], tgt[i] + } + }' +echo +echo " In-range bumps: apply them with your stack's package manager, then re-run \`mise run deps\`." +echo " Majors are listed separately — review each one's changelog before upgrading." +exit 0 diff --git a/internal/coreassets/templates/scaffolds/swift-package/files/mise.toml.tmpl b/internal/coreassets/templates/scaffolds/swift-package/files/mise.toml.tmpl index aa05742b05..0c5486be21 100644 --- a/internal/coreassets/templates/scaffolds/swift-package/files/mise.toml.tmpl +++ b/internal/coreassets/templates/scaffolds/swift-package/files/mise.toml.tmpl @@ -4,6 +4,13 @@ # `mise run test` writes the Swift Testing event stream the engine's `swift` format # joins. +# The local dependency gate (`mise run deps`) runs Renovate (a node tool) + jq; +# pin them here since the stack otherwise leans on the ambient Swift toolchain. +[tools] +node = "24" +"npm:renovate" = "latest" +jq = "latest" + [tasks.test] description = "run the test suite, writing the event-stream report the engine joins" run = "swift test --event-stream-output-path test.swift-events.ndjson --event-stream-version 0" @@ -35,3 +42,14 @@ for d in Sources Tests; do done if [ -n "$dirs" ]; then swift format lint --strict --recursive $dirs; fi ''' + +# Local dependency check — advisory (never opens PRs, always exits 0). Replaces +# Dependabot: surfaces available updates as part of `mise run check`. +[tasks.deps] +description = "report available dependency updates (local Renovate dry-run; no PRs)" +run = "bash scripts/deps-check.sh" + +[tasks.check] +description = "all local quality gates: lint + the dependency heads-up" +depends = ["lint", "deps"] +run = ":" diff --git a/internal/coreassets/templates/scaffolds/swift-package/files/renovate.json b/internal/coreassets/templates/scaffolds/swift-package/files/renovate.json new file mode 100644 index 0000000000..c3a9215224 --- /dev/null +++ b/internal/coreassets/templates/scaffolds/swift-package/files/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false +} diff --git a/internal/coreassets/templates/scaffolds/swift-package/files/scripts/deps-check.sh b/internal/coreassets/templates/scaffolds/swift-package/files/scripts/deps-check.sh new file mode 100644 index 0000000000..e8f494199f --- /dev/null +++ b/internal/coreassets/templates/scaffolds/swift-package/files/scripts/deps-check.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Local dependency-update gate. +# +# Runs Renovate against the working tree in local/dry-run mode — it NEVER opens +# PRs or writes anything; it just reports which dependencies (npm packages, Go +# modules, Swift packages, GitHub Actions, mise tools, …) have newer versions +# available. Wired into `mise run check` so updates surface as ongoing work +# instead of as autonomously-opened Dependabot PRs. +# +# Advisory only: always exits 0. Available updates are a heads-up, not a failure, +# and a Renovate/network hiccup must not break the rest of `check`. +set -uo pipefail + +if ! command -v renovate >/dev/null 2>&1; then + echo "deps: renovate not found — enable the gate with: mise use 'npm:renovate@latest' jq" + exit 0 +fi + +# Renovate needs a github.com token to look up Actions / Go-module / Swift-package +# release versions (npm needs none). Reuse the user's existing gh auth when +# present; anonymous still works for npm, just rate-limited on github.com. +github_token="$(gh auth token 2>/dev/null || true)" + +log="$(mktemp -t renovate.XXXXXX)" +trap 'rm -f "$log"' EXIT + +GITHUB_COM_TOKEN="$github_token" \ +RENOVATE_ONBOARDING=false \ +LOG_LEVEL=debug \ +LOG_FORMAT=json \ + renovate --platform=local --dry-run=full >"$log" 2>&1 +status=$? + +if [ "$status" -ne 0 ]; then + echo "deps: renovate exited $status (skipping the update check this run)" + grep -iE '"level":(50|60)|FATAL|ERROR' "$log" | head -5 + exit 0 +fi + +# All available updates live in the single "packageFiles with updates" debug +# record: walk every dependency that carries a non-empty updates[] array and +# emit one row per update — updateType, ecosystem, name, current → target. +# Everything Renovate found is surfaced; scope what it *looks at* in renovate.json. +updates="$(jq -r ' + select(.msg == "packageFiles with updates") + | [.. | objects | select(.depName? and (.updates? | type == "array") and ((.updates | length) > 0))] + | .[] as $dep + | $dep.updates[] + | "\(.updateType // "?")\t\($dep.datasource // "-")\t\($dep.depName)\t\($dep.currentValue // $dep.currentVersion // "-")\t\(.newVersion // .newValue)" +' "$log" | sort -u | sort -t"$(printf '\t')" -k2,2 -k1,1 -k3,3)" + +if [ -z "$updates" ]; then + echo "deps: ✓ all dependencies up to date" + exit 0 +fi + +count="$(printf '%s\n' "$updates" | grep -c '')" +echo "deps: ⚠ $count update(s) available — local Renovate dry-run, no PRs opened" +echo +# Majors get their own flagged section (each is a breaking-change decision, not +# routine work); the in-range minor/patch updates follow, grouped by ecosystem. +printf '%s\n' "$updates" | awk -F'\t' ' + { + type[NR] = $1; eco[NR] = $2; name[NR] = $3; cur[NR] = $4; tgt[NR] = $5 + if ($1 == "major") majors++ + else inrange[$2]++ + } + END { + if (majors > 0) { + printf " major — review before upgrading (%d)\n", majors + for (i = 1; i <= NR; i++) + if (type[i] == "major") + printf " %-7s %-34s %s → %s\n", eco[i], name[i], cur[i], tgt[i] + print "" + } + prev = "" + for (i = 1; i <= NR; i++) { + if (type[i] == "major") continue + if (eco[i] != prev) { + if (prev != "") print "" + printf " %s (%d)\n", eco[i], inrange[eco[i]] + prev = eco[i] + } + printf " %-6s %-34s %s → %s\n", type[i], name[i], cur[i], tgt[i] + } + }' +echo +echo " In-range bumps: apply them with your stack's package manager, then re-run \`mise run deps\`." +echo " Majors are listed separately — review each one's changelog before upgrading." +exit 0 diff --git a/internal/coreassets/templates/scaffolds/web/files/mise.toml b/internal/coreassets/templates/scaffolds/web/files/mise.toml index 275dd89e4d..835439646d 100644 --- a/internal/coreassets/templates/scaffolds/web/files/mise.toml +++ b/internal/coreassets/templates/scaffolds/web/files/mise.toml @@ -5,6 +5,10 @@ pnpm = "11" # op resolves op:// secret references for `specify secrets sync`. gh = "2.94" op = "2" +# Local dependency-update gate (`mise run deps`): Renovate scans every ecosystem, +# jq formats its report. See scripts/deps-check.sh. +"npm:renovate" = "latest" +jq = "latest" # Call npm binaries (vite, vitest, tsgo, oxlint, oxfmt, tsr) by bare name — no npx. [env] @@ -43,3 +47,14 @@ run = "oxlint app" [tasks.build] run = "vite build" + +# Local dependency check — advisory (never opens PRs, always exits 0). Replaces +# Dependabot: surfaces available updates as part of `mise run check`. +[tasks.deps] +description = "report available dependency updates (local Renovate dry-run; no PRs)" +run = "bash scripts/deps-check.sh" + +[tasks.check] +description = "all local quality gates: fmt:check, lint, typecheck + the dependency heads-up" +depends = ["fmt:check", "lint", "typecheck", "deps"] +run = ":" diff --git a/internal/coreassets/templates/scaffolds/web/files/renovate.json b/internal/coreassets/templates/scaffolds/web/files/renovate.json new file mode 100644 index 0000000000..c3a9215224 --- /dev/null +++ b/internal/coreassets/templates/scaffolds/web/files/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false +} diff --git a/internal/coreassets/templates/scaffolds/web/files/scripts/deps-check.sh b/internal/coreassets/templates/scaffolds/web/files/scripts/deps-check.sh new file mode 100644 index 0000000000..e8f494199f --- /dev/null +++ b/internal/coreassets/templates/scaffolds/web/files/scripts/deps-check.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Local dependency-update gate. +# +# Runs Renovate against the working tree in local/dry-run mode — it NEVER opens +# PRs or writes anything; it just reports which dependencies (npm packages, Go +# modules, Swift packages, GitHub Actions, mise tools, …) have newer versions +# available. Wired into `mise run check` so updates surface as ongoing work +# instead of as autonomously-opened Dependabot PRs. +# +# Advisory only: always exits 0. Available updates are a heads-up, not a failure, +# and a Renovate/network hiccup must not break the rest of `check`. +set -uo pipefail + +if ! command -v renovate >/dev/null 2>&1; then + echo "deps: renovate not found — enable the gate with: mise use 'npm:renovate@latest' jq" + exit 0 +fi + +# Renovate needs a github.com token to look up Actions / Go-module / Swift-package +# release versions (npm needs none). Reuse the user's existing gh auth when +# present; anonymous still works for npm, just rate-limited on github.com. +github_token="$(gh auth token 2>/dev/null || true)" + +log="$(mktemp -t renovate.XXXXXX)" +trap 'rm -f "$log"' EXIT + +GITHUB_COM_TOKEN="$github_token" \ +RENOVATE_ONBOARDING=false \ +LOG_LEVEL=debug \ +LOG_FORMAT=json \ + renovate --platform=local --dry-run=full >"$log" 2>&1 +status=$? + +if [ "$status" -ne 0 ]; then + echo "deps: renovate exited $status (skipping the update check this run)" + grep -iE '"level":(50|60)|FATAL|ERROR' "$log" | head -5 + exit 0 +fi + +# All available updates live in the single "packageFiles with updates" debug +# record: walk every dependency that carries a non-empty updates[] array and +# emit one row per update — updateType, ecosystem, name, current → target. +# Everything Renovate found is surfaced; scope what it *looks at* in renovate.json. +updates="$(jq -r ' + select(.msg == "packageFiles with updates") + | [.. | objects | select(.depName? and (.updates? | type == "array") and ((.updates | length) > 0))] + | .[] as $dep + | $dep.updates[] + | "\(.updateType // "?")\t\($dep.datasource // "-")\t\($dep.depName)\t\($dep.currentValue // $dep.currentVersion // "-")\t\(.newVersion // .newValue)" +' "$log" | sort -u | sort -t"$(printf '\t')" -k2,2 -k1,1 -k3,3)" + +if [ -z "$updates" ]; then + echo "deps: ✓ all dependencies up to date" + exit 0 +fi + +count="$(printf '%s\n' "$updates" | grep -c '')" +echo "deps: ⚠ $count update(s) available — local Renovate dry-run, no PRs opened" +echo +# Majors get their own flagged section (each is a breaking-change decision, not +# routine work); the in-range minor/patch updates follow, grouped by ecosystem. +printf '%s\n' "$updates" | awk -F'\t' ' + { + type[NR] = $1; eco[NR] = $2; name[NR] = $3; cur[NR] = $4; tgt[NR] = $5 + if ($1 == "major") majors++ + else inrange[$2]++ + } + END { + if (majors > 0) { + printf " major — review before upgrading (%d)\n", majors + for (i = 1; i <= NR; i++) + if (type[i] == "major") + printf " %-7s %-34s %s → %s\n", eco[i], name[i], cur[i], tgt[i] + print "" + } + prev = "" + for (i = 1; i <= NR; i++) { + if (type[i] == "major") continue + if (eco[i] != prev) { + if (prev != "") print "" + printf " %s (%d)\n", eco[i], inrange[eco[i]] + prev = eco[i] + } + printf " %-6s %-34s %s → %s\n", type[i], name[i], cur[i], tgt[i] + } + }' +echo +echo " In-range bumps: apply them with your stack's package manager, then re-run \`mise run deps\`." +echo " Majors are listed separately — review each one's changelog before upgrading." +exit 0 diff --git a/internal/coreassets/templates/scaffolds/web/github/.github/dependabot.yml.tmpl b/internal/coreassets/templates/scaffolds/web/github/.github/dependabot.yml.tmpl deleted file mode 100644 index 75543cc0bd..0000000000 --- a/internal/coreassets/templates/scaffolds/web/github/.github/dependabot.yml.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: /{{.Dir}} - schedule: - interval: weekly - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly diff --git a/internal/scaffold/web_test.go b/internal/scaffold/web_test.go index 7d376a2569..f34adb103d 100644 --- a/internal/scaffold/web_test.go +++ b/internal/scaffold/web_test.go @@ -133,11 +133,18 @@ func TestWebScaffold(t *testing.T) { if err != nil { t.Fatal(err) } - for _, task := range []string{"[tasks.lint]", `[tasks."fmt:check"]`, "[tasks.typecheck]"} { + for _, task := range []string{"[tasks.lint]", `[tasks."fmt:check"]`, "[tasks.typecheck]", "[tasks.deps]", "[tasks.check]"} { if !strings.Contains(string(mise), task) { t.Errorf("mise.toml missing quality task %s", task) } } + // the local dependency gate ships with the stack: a Renovate config + the + // deps-check script, projected alongside the app. + for _, p := range []string{"renovate.json", "scripts/deps-check.sh"} { + if _, err := os.Stat(filepath.Join(app, p)); err != nil { + t.Errorf("web scaffold missing dependency-gate file %s: %v", p, err) + } + } root := t.TempDir() w, err := RenderRoot(sub, root, data) @@ -190,7 +197,6 @@ func TestWebScaffold(t *testing.T) { ".github/ISSUE_TEMPLATE/defect.yml", ".github/ISSUE_TEMPLATE/config.yml", ".github/CODEOWNERS", - ".github/dependabot.yml", } { if _, err := os.Stat(filepath.Join(proj, filepath.FromSlash(p))); err != nil { t.Errorf("RenderGitHub did not write %s: %v", p, err) @@ -205,13 +211,10 @@ func TestWebScaffold(t *testing.T) { if !strings.Contains(string(owners), "/features/") || !strings.Contains(string(owners), "/specs/") { t.Errorf("CODEOWNERS must route /features and /specs:\n%s", owners) } - // dependabot.yml is templated: the npm ecosystem points at the app dir. - dep, _ := os.ReadFile(filepath.Join(proj, ".github/dependabot.yml")) - if !strings.Contains(string(dep), "directory: /apps/web") { - t.Errorf("dependabot.yml npm directory not substituted to the app dir:\n%s", dep) - } - if strings.Contains(string(dep), "{{") { - t.Errorf("dependabot.yml has unrendered template syntax:\n%s", dep) + // Dependabot is gone — dependency updates surface via the local `mise run deps` + // gate (Renovate), not autonomously-opened PRs. + if _, err := os.Stat(filepath.Join(proj, ".github/dependabot.yml")); !os.IsNotExist(err) { + t.Errorf("dependabot.yml should no longer be projected (stat err = %v)", err) } // the convex data variant overwrites the base router and adds the convex files if m.DataDefault != "convex" { From 8b874eeb6451c657485e6f147ee48aa0e1827a09 Mon Sep 17 00:00:00 2001 From: Mark Malstrom Date: Mon, 15 Jun 2026 12:27:00 -0500 Subject: [PATCH 2/2] docs: record the Dependabot to local Renovate deps-gate switch github-integration.md and usage/github.md no longer list dependabot.yml as a projected .github surface; both note that dependency updates are handled locally by each stack's deps mise task (Renovate, never opens PRs). BACKLOG.md's Pillar 2 entry updated to match. --- BACKLOG.md | 5 +++-- docs/design/github-integration.md | 5 ++++- docs/usage/github.md | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index b3b691bc80..02b5cb1918 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -68,8 +68,9 @@ so the offline guarantee holds structurally.** - ✅ **Pillar 2 — Issues as ephemeral defect intake.** `specify issues list|create|close` (confirmation-gated, `--json`); the `github/` scaffold subtree now drops the full per-target `.github/` (`PULL_REQUEST_TEMPLATE.md`, `ISSUE_TEMPLATE/defect.yml` stamping - `type: Bug` + label fallback, `config.yml`, `CODEOWNERS` for `/features` `/specs`, - `dependabot.yml`). No durable issue↔scenario link (GitHub cross-refs). Close-on-green is + `type: Bug` + label fallback, `config.yml`, `CODEOWNERS` for `/features` `/specs`). + Dependency updates moved off Dependabot to a local Renovate `deps` gate (per-stack + `mise.toml`). No durable issue↔scenario link (GitHub cross-refs). Close-on-green is the discipline. - ✅ **Pillar 3 — Projects board (Beads-informed).** `specify work ready|claim|move|discover` on the inlined Projects v2 GraphQL client (resolve project/fields/options, add item, set diff --git a/docs/design/github-integration.md b/docs/design/github-integration.md index 4280902a0b..36082361c8 100644 --- a/docs/design/github-integration.md +++ b/docs/design/github-integration.md @@ -179,7 +179,10 @@ the same hierarchy Beads gets from parent-child. *Caveat:* Issue Types are an | `ISSUE_TEMPLATE/defect.yml` | a defect form — stamps `type: Bug`, a label, the project; prompts for repro + the target | | `ISSUE_TEMPLATE/config.yml` | points docs at *this* repo | | `CODEOWNERS` | maps `/features/**` and `/specs/**` to the spec owner, so **spec changes require human review** | -| `dependabot.yml` | for the stack's ecosystem (web → npm, go → gomod) | + +_No `dependabot.yml` is projected — dependency updates are surfaced locally by +each stack's **`deps`** mise task (Renovate dry-run, never opens PRs), part of +`mise run check`._ ## Pillar 3 — Projects as the work surface (Beads-informed, simplified) diff --git a/docs/usage/github.md b/docs/usage/github.md index d70cd17f63..880c8a7423 100644 --- a/docs/usage/github.md +++ b/docs/usage/github.md @@ -92,7 +92,10 @@ specify issues close # close on green | `ISSUE_TEMPLATE/config.yml` | disables blank issues, links out to the SpecKit docs | | `PULL_REQUEST_TEMPLATE.md` | a spec-touch checklist (specs changed? scenarios bound? `verify` green? `drift` clean?) | | `CODEOWNERS` | routes `/features/`, `/specs/`, and `/.speckit/` to the spec owner, so **spec changes require human review** (replace `@OWNER` with a user or team) | -| `dependabot.yml` | for the stack's ecosystem (web → npm) | + +_Dependency updates aren't a GitHub surface: each stack's `mise.toml` ships a +local **`deps`** task (Renovate dry-run, never opens PRs) wired into `mise run +check`, so no `dependabot.yml` is projected._ The lifecycle is **scenario-canonical**: