Skip to content

Commit ee969b3

Browse files
hyperpolymathclaude
andcommitted
feat: add fix-dependabot-coverage script
Scans repos for package manager files in subdirectories and ensures dependabot.yml has matching directory entries. Removes semver-patch ignore rules that block security fixes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f9efa2 commit ee969b3

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

‎scripts/fix-dependabot-coverage.sh‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# fix-dependabot-coverage.sh
6+
#
7+
# Scans a repository for package manager files (Cargo.toml, package.json,
8+
# mix.exs, etc.) in subdirectories and ensures .github/dependabot.yml has
9+
# matching directory entries. Also removes semver-patch ignore rules that
10+
# block security patch updates.
11+
#
12+
# Usage:
13+
# fix-dependabot-coverage.sh [repo-path]
14+
#
15+
# If repo-path is omitted, the current directory is used.
16+
17+
set -euo pipefail
18+
19+
# --- Configuration ---
20+
21+
REPO_ROOT="${1:-.}"
22+
DEPENDABOT_FILE="${REPO_ROOT}/.github/dependabot.yml"
23+
24+
# Map of filename -> dependabot ecosystem name
25+
declare -A ECOSYSTEM_MAP=(
26+
["Cargo.toml"]="cargo"
27+
["package.json"]="npm"
28+
["mix.exs"]="mix"
29+
["go.mod"]="gomod"
30+
["Gemfile"]="bundler"
31+
["requirements.txt"]="pip"
32+
["setup.py"]="pip"
33+
["pyproject.toml"]="pip"
34+
["build.gradle"]="gradle"
35+
["build.gradle.kts"]="gradle"
36+
["pom.xml"]="maven"
37+
["composer.json"]="composer"
38+
["flake.nix"]="nix"
39+
["pubspec.yaml"]="pub"
40+
)
41+
42+
# --- Validation ---
43+
44+
if [[ ! -d "${REPO_ROOT}" ]]; then
45+
echo "ERROR: Repository path '${REPO_ROOT}' does not exist." >&2
46+
exit 1
47+
fi
48+
49+
if [[ ! -f "${DEPENDABOT_FILE}" ]]; then
50+
echo "ERROR: ${DEPENDABOT_FILE} not found. Create it first." >&2
51+
exit 1
52+
fi
53+
54+
# --- Functions ---
55+
56+
# Check whether a given ecosystem+directory pair already exists in dependabot.yml.
57+
# Uses a simple text-based check: looks for the ecosystem line followed by
58+
# the directory line within a few lines of each other.
59+
entry_exists() {
60+
local ecosystem="$1"
61+
local directory="$2"
62+
63+
# Normalise directory for matching: dependabot uses "/" for root,
64+
# "/subdir" for subdirectories.
65+
local dir_pattern
66+
if [[ "${directory}" == "/" ]]; then
67+
dir_pattern='directory: "/"'
68+
else
69+
dir_pattern="directory: \"${directory}\""
70+
fi
71+
72+
# Search for the ecosystem+directory pair within the file.
73+
# We look for both on adjacent lines (within 5 lines of each other).
74+
if awk -v eco="\"${ecosystem}\"" -v dir="${dir_pattern}" '
75+
/package-ecosystem:/ && $0 ~ eco { found_eco=NR }
76+
found_eco && NR <= found_eco+5 && $0 ~ dir { exit 0 }
77+
END { exit 1 }
78+
' "${DEPENDABOT_FILE}"; then
79+
return 0
80+
else
81+
return 1
82+
fi
83+
}
84+
85+
# Append a new ecosystem+directory entry to dependabot.yml.
86+
append_entry() {
87+
local ecosystem="$1"
88+
local directory="$2"
89+
90+
cat >> "${DEPENDABOT_FILE}" <<EOF
91+
92+
- package-ecosystem: "${ecosystem}"
93+
directory: "${directory}"
94+
schedule:
95+
interval: "daily"
96+
EOF
97+
echo " ADDED: ${ecosystem} @ ${directory}"
98+
}
99+
100+
# Remove semver-patch ignore rules from dependabot.yml.
101+
# Matches the block:
102+
# ignore:
103+
# - dependency-name: "*"
104+
# update-types: ["version-update:semver-patch"]
105+
remove_semver_patch_ignores() {
106+
local tmp_file
107+
tmp_file="$(mktemp)"
108+
109+
# Use awk to strip ignore blocks that contain semver-patch.
110+
# This handles the 3-line ignore block pattern.
111+
awk '
112+
/^[[:space:]]*ignore:/ {
113+
# Buffer lines to check if this is a semver-patch ignore block
114+
buf[0] = $0; n = 1
115+
while ((getline line) > 0) {
116+
buf[n++] = line
117+
# If we see semver-patch, this entire ignore block should be removed
118+
if (line ~ /version-update:semver-patch/) {
119+
# Skip this entire block (do not print buffered lines)
120+
removed = 1
121+
break
122+
}
123+
# If we hit a non-indented line or a new key at the same level,
124+
# this is not a simple semver-patch block - print and move on
125+
if (line !~ /^[[:space:]]+(- dependency-name:|update-types:)/) {
126+
for (i = 0; i < n; i++) print buf[i]
127+
removed = 0
128+
break
129+
}
130+
}
131+
if (!removed && n > 0) {
132+
# End of file reached while buffering - print everything
133+
for (i = 0; i < n; i++) print buf[i]
134+
}
135+
next
136+
}
137+
{ print }
138+
' "${DEPENDABOT_FILE}" > "${tmp_file}"
139+
140+
if ! diff -q "${DEPENDABOT_FILE}" "${tmp_file}" > /dev/null 2>&1; then
141+
mv "${tmp_file}" "${DEPENDABOT_FILE}"
142+
echo " REMOVED: semver-patch ignore rule(s)"
143+
else
144+
rm -f "${tmp_file}"
145+
fi
146+
}
147+
148+
# --- Main ---
149+
150+
echo "=== fix-dependabot-coverage ==="
151+
echo "Repository: $(cd "${REPO_ROOT}" && pwd)"
152+
echo ""
153+
154+
# Step 1: Remove semver-patch ignore rules
155+
echo "--- Checking for semver-patch ignore rules ---"
156+
remove_semver_patch_ignores
157+
158+
# Step 2: Find all package manager files and ensure coverage
159+
echo ""
160+
echo "--- Scanning for package manager files ---"
161+
162+
changes_made=0
163+
164+
for pkg_file in "${!ECOSYSTEM_MAP[@]}"; do
165+
ecosystem="${ECOSYSTEM_MAP[${pkg_file}]}"
166+
167+
# Find all instances of this package file in the repo,
168+
# excluding common non-project directories.
169+
while IFS= read -r found_path; do
170+
# Determine the directory relative to repo root
171+
dir="$(dirname "${found_path}")"
172+
rel_dir="${dir#"${REPO_ROOT}"}"
173+
174+
# Skip common non-project directories
175+
if [[ "${rel_dir}" == *"/node_modules/"* ]] || \
176+
[[ "${rel_dir}" == *"/target/"* ]] || \
177+
[[ "${rel_dir}" == *"/vendor/"* ]] || \
178+
[[ "${rel_dir}" == *"/.git/"* ]] || \
179+
[[ "${rel_dir}" == *"/deps/"* ]] || \
180+
[[ "${rel_dir}" == *"/_build/"* ]] || \
181+
[[ "${rel_dir}" == *"/.lake/"* ]] || \
182+
[[ "${rel_dir}" == *"/build/"* ]]; then
183+
continue
184+
fi
185+
186+
# Normalise: empty relative dir becomes "/"
187+
if [[ -z "${rel_dir}" ]]; then
188+
rel_dir="/"
189+
fi
190+
191+
# Check if this entry already exists
192+
if entry_exists "${ecosystem}" "${rel_dir}"; then
193+
echo " OK: ${ecosystem} @ ${rel_dir}"
194+
else
195+
append_entry "${ecosystem}" "${rel_dir}"
196+
changes_made=$((changes_made + 1))
197+
fi
198+
done < <(find "${REPO_ROOT}" -name "${pkg_file}" -not -path "*/.git/*" -not -path "*/node_modules/*" -not -path "*/target/*" -not -path "*/vendor/*" -not -path "*/_build/*" -not -path "*/.lake/*" 2>/dev/null)
199+
done
200+
201+
echo ""
202+
if [[ ${changes_made} -gt 0 ]]; then
203+
echo "=== Done: ${changes_made} entries added ==="
204+
else
205+
echo "=== Done: dependabot.yml already has full coverage ==="
206+
fi

0 commit comments

Comments
 (0)