From 520eaf589f5fc54beb1b1faefd682cdac29f04b6 Mon Sep 17 00:00:00 2001 From: Marcin Date: Mon, 20 Jul 2026 14:03:46 +0200 Subject: [PATCH 1/2] Add central Scala Steward cooldown default config Introduce a central default.scala-steward.conf carrying a 3-day supply-chain cooldown (updates.cooldown.minimumAge) and wire it into the reusable scala-steward workflow via a sparse checkout, passed as repo-config. Per-repo .scala-steward.conf (pins/ignores) is still auto-discovered and deep-merged on top, taking precedence per field. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/scala-steward.yml | 9 ++++++++- README.md | 2 ++ default.scala-steward.conf | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 default.scala-steward.conf diff --git a/.github/workflows/scala-steward.yml b/.github/workflows/scala-steward.yml index a5417ea..104eac6 100644 --- a/.github/workflows/scala-steward.yml +++ b/.github/workflows/scala-steward.yml @@ -26,6 +26,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + - name: Checkout central Scala Steward config + uses: actions/checkout@v6 + with: + repository: softwaremill/github-actions-workflows + path: .steward-central + sparse-checkout: default.scala-steward.conf + sparse-checkout-cone-mode: false - name: Set up JDK uses: actions/setup-java@v5 with: @@ -38,6 +45,6 @@ jobs: with: author-name: scala-steward author-email: scala-steward - repo-config: .scala-steward.conf + repo-config: .steward-central/default.scala-steward.conf ignore-opts-files: false github-token: ${{ secrets.github-token }} diff --git a/README.md b/README.md index de1ceba..ab05414 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ steward and this file belongs to a whitelist specified by `labeler.yml` This workflow is responsible for running Scala Steward. +It applies a central default config ([`default.scala-steward.conf`](./default.scala-steward.conf)) that enforces a supply-chain cooldown (`updates.cooldown.minimumAge = "3 days"`), so new dependency versions are not adopted until they have matured. A repository's own `.scala-steward.conf` (pins, ignores, per-dependency overrides) is still applied on top and takes precedence per field. + ### Usage ```yaml diff --git a/default.scala-steward.conf b/default.scala-steward.conf new file mode 100644 index 0000000..eef1ef1 --- /dev/null +++ b/default.scala-steward.conf @@ -0,0 +1 @@ +updates.cooldown = { minimumAge = "3 days" } From e936d7fa55b2b7322d61434de3798083024cd167 Mon Sep 17 00:00:00 2001 From: Marcin Date: Tue, 21 Jul 2026 11:54:56 +0200 Subject: [PATCH 2/2] Add central Dependabot cooldown discovery + matrix workflow A central, self-maintaining workflow in this repo that enforces a supply-chain cooldown across the org's Dependabot configs. Dependabot has no org-wide config, so a cooldown block must live in each repo's committed .github/dependabot.yml. The workflow discovers target repos via a GitHub App installation (active, non-fork repos that carry a Dependabot config), then fans out a matrix: one job per repo mints a short-lived, repo-scoped App token, patches cooldown.default-days on every updates: entry, and opens a PR. The patch is surgical (per updates: entry, preserving comments and key order) and strict (the central value wins over any local one). PRs are opened with the App token so the target repo's CI runs on them, and are left for review (no auto-merge). Requires a GitHub App exposed as vars.DEPENDABOT_COOLDOWN_APP_CLIENT_ID and secrets.DEPENDABOT_COOLDOWN_APP_KEY. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/dependabot-cooldown.yml | 197 ++++++++++++++++++++++ README.md | 57 +++++++ 2 files changed, 254 insertions(+) create mode 100644 .github/workflows/dependabot-cooldown.yml diff --git a/.github/workflows/dependabot-cooldown.yml b/.github/workflows/dependabot-cooldown.yml new file mode 100644 index 0000000..4405b1c --- /dev/null +++ b/.github/workflows/dependabot-cooldown.yml @@ -0,0 +1,197 @@ +name: Dependabot Cooldown + +# Central, self-maintaining supply-chain cooldown for Dependabot across the org. +# +# Dependabot has no org-wide / remote config, so a `cooldown` block must live statically +# in each repo's committed `.github/dependabot.yml(.yaml)`. Rather than maintain a hand-written +# repo list (which drifts) or ask every repo to opt in with a caller workflow (25+ files to +# keep in sync), this workflow DISCOVERS the target set at run time and fans out over it: +# +# 1. `discover` — enumerate the repos this GitHub App is installed on, keep the active +# (non-archived, non-fork) ones that actually contain a Dependabot config, minus an +# explicit policy exclude list. Emits a matrix. +# 2. `patch` — one matrix job per discovered repo: mint a SHORT-LIVED token scoped to +# just that repo, patch `cooldown.default-days` on every `updates:` entry (surgically, +# preserving comments and key order), and open a PR for review. +# +# Credentials: a GitHub App with `contents: write` + `pull-requests: write`, installed on the +# target repos. The private key is the only standing secret; every runtime token is repo-scoped +# and expires in ~1 hour. Because PRs are opened with an App token (not GITHUB_TOKEN), the +# target repo's own CI runs on the cooldown PR. + +on: + # Re-apply weekly so newly added Dependabot configs are picked up and any drift is corrected. + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + inputs: + cooldown-days: + description: "Cooldown in days. Leave empty to use the central default (3)." + required: false + type: string + repos: + description: "Optional comma/space-separated repo list to patch instead of discovery (e.g. for a targeted re-run)." + required: false + type: string + +# GITHUB_TOKEN is unused — all repo access goes through the App token minted below. +permissions: {} + +env: + COOLDOWN_DAYS: ${{ inputs.cooldown-days || '3' }} + # Policy exclude list (space-separated repo names). Discovery finds every active repo with a + # Dependabot config; list here any that are out of policy scope (throwaway experiments, toy + # bots, demos). Archived repos and forks are already dropped automatically. + EXCLUDE: "" + +jobs: + discover: + name: Discover target repos + runs-on: ubuntu-24.04 + outputs: + repos: ${{ steps.list.outputs.repos }} + steps: + - name: Generate org-scoped App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.DEPENDABOT_COOLDOWN_APP_CLIENT_ID }} + private-key: ${{ secrets.DEPENDABOT_COOLDOWN_APP_KEY }} + owner: softwaremill + + - name: List repos with a Dependabot config + id: list + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + OVERRIDE: ${{ inputs.repos }} + run: | + set -euo pipefail + + if [ -n "${OVERRIDE:-}" ]; then + # Manual override: patch exactly these repos, skip discovery. + candidates=$(echo "$OVERRIDE" | tr ', ' '\n' | sed '/^[[:space:]]*$/d') + else + # Candidate universe = repos this App installation can see, active and first-party. + # (Enumerating the installation is deterministic — no code-search index lag.) + candidates=$(gh api --paginate /installation/repositories \ + --jq '.repositories[] | select(.archived == false and .fork == false) | .name') + fi + + keep=() + for repo in $candidates; do + # Apply the policy exclude list. + case " $EXCLUDE " in *" $repo "*) echo "skip (excluded): $repo"; continue;; esac + + # Keep only repos that actually carry a Dependabot config on the default branch. + if gh api "repos/softwaremill/$repo/contents/.github/dependabot.yml" >/dev/null 2>&1 \ + || gh api "repos/softwaremill/$repo/contents/.github/dependabot.yaml" >/dev/null 2>&1; then + echo "target: $repo" + keep+=("$repo") + fi + done + + # Emit a JSON array for the matrix (empty array if nothing matched). + printf '%s\n' "${keep[@]:-}" | sed '/^$/d' | jq -R . | jq -sc . > repos.json + echo "repos=$(cat repos.json)" >> "$GITHUB_OUTPUT" + echo "Discovered $(jq 'length' repos.json) repo(s): $(cat repos.json)" + + patch: + name: Patch ${{ matrix.repo }} + needs: discover + if: needs.discover.outputs.repos != '[]' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + repo: ${{ fromJSON(needs.discover.outputs.repos) }} + env: + BRANCH: chore/dependabot-cooldown + steps: + - name: Generate repo-scoped App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.DEPENDABOT_COOLDOWN_APP_CLIENT_ID }} + private-key: ${{ secrets.DEPENDABOT_COOLDOWN_APP_KEY }} + owner: softwaremill + repositories: ${{ matrix.repo }} + + - name: Checkout ${{ matrix.repo }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: softwaremill/${{ matrix.repo }} + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 1 + + - name: Patch dependabot cooldown + id: patch + run: | + set -euo pipefail + + FILE="" + if [ -f .github/dependabot.yml ]; then + FILE=.github/dependabot.yml + elif [ -f .github/dependabot.yaml ]; then + FILE=.github/dependabot.yaml + fi + + if [ -z "$FILE" ]; then + echo "No .github/dependabot.yml(.yaml) found — nothing to do." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Found $FILE" + + # yq v4 (mikefarah) is preinstalled on ubuntu-24.04. `env(...)` type-parses, + # so a numeric value is written as a YAML integer (Dependabot expects an int). + # Strict overwrite of `default-days` on every `updates:` entry: + # central policy is the single tunable knob and deliberately wins over any local + # hand-set value. Sibling cooldown keys (semver-*-days, include, exclude) are left + # untouched — the patch is surgical. + DAYS="$COOLDOWN_DAYS" yq -i '.updates[].cooldown.default-days = env(DAYS)' "$FILE" + + if git diff --quiet -- "$FILE"; then + echo "Cooldown already at $COOLDOWN_DAYS days on every entry — no change." + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "file=$FILE" >> "$GITHUB_OUTPUT" + fi + + - name: Open pull request + if: steps.patch.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + FILE: ${{ steps.patch.outputs.file }} + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + BASE="$(git rev-parse --abbrev-ref HEAD)" + + # Fixed branch, force-pushed: a re-run updates the existing PR in place + # instead of opening a duplicate. + git switch -C "$BRANCH" + git add "$FILE" + git commit -m "Set Dependabot cooldown to ${COOLDOWN_DAYS} days" + git push -f origin "$BRANCH" + + TITLE="Set Dependabot cooldown to ${COOLDOWN_DAYS} days" + BODY="$(cat <