Skip to content

ci(dependabot): target dev for all update ecosystems - #134

Open
singlerider wants to merge 1 commit into
masterfrom
ci/dependabot-target-dev
Open

singlerider wants to merge 1 commit into
masterfrom
ci/dependabot-target-dev

Conversation

@singlerider

@singlerider singlerider commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Without target-branch, Dependabot opens every update against the default branch, so bumps land on the release branch instead of dev and get closed by hand.

Dependabot only reads dependabot.yml from the default branch, so this targets it directly.

Related (closed, opened against the default branch): #115, #118, #122, #123, #124, #130, #131, #132

Validated: python -c "import yaml; yaml.safe_load(open('.github/dependabot.yml'))" parses cleanly.

Without target-branch, Dependabot opens every update against the
default branch, so bumps land on the release branch instead of dev
and have to be closed by hand. Dependabot only reads this file from
the default branch, so the fix has to land there to take effect.
@singlerider singlerider self-assigned this Nov 9, 2026
@singlerider
singlerider requested a review from en0f November 9, 2026 17:27
@singlerider singlerider added enhancement New feature or request dependencies Pull requests that update a dependency file and removed enhancement New feature or request labels Nov 9, 2026

@en0f en0f left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 28d0e4a.

🔴 Validation doesn't show Dependabot opening a PR against dev

The validation shows the file parses, not that Dependabot opens its update PRs against dev, which is the point of the change. git.md (Pull requests) and the PR template ask for the commands run and their real output, and yaml.safe_load prints nothing. The repo can be forked and dev's uv.lock is behind on direct dependencies (ruff 0.15.2, pytest 9.1.1, charset-normalizer 3.4.4), so a fork run will open PRs. Fork with all branches, make ci/dependabot-target-dev the fork's default (gh repo edit <fork> --default-branch ci/dependabot-target-dev; the fork's master doesn't have this change), enable Dependabot version updates in the fork, trigger a run (Insights > Dependency graph > Dependabot > Check for updates) or wait for the weekly one, and paste the output of gh pr list --repo <fork> --author app/dependabot --json number,baseRefName,title into the template's Validation section.

🟢 Both entries target dev, and dev already has everything they scan

target-branch is per entry, and both uv and github-actions set it. With it set, Dependabot reads manifests only from dev, and dev has pyproject.toml, uv.lock and the same cla.yml and tests.yml as this head, so no workflow, action or lockfile drops out of updates. The commit body says why this has to land on the default branch.

Reproduce this review
# save the script below as verify-134.sh, then:
bash verify-134.sh blacklanternsecurity/MANSPIDER 28d0e4a11c77680af933bea4faec63c80e0c7790
# expected: SUMMARY 4 PASS, 0 FAIL, 0 SKIP, 0 BROKEN   (dev at 0cede156)
# control:  bash verify-134.sh blacklanternsecurity/MANSPIDER 28d0e4a11c77680af933bea4faec63c80e0c7790 control-134.patch   -> V01=FAIL V03=FAIL V04=FAIL V05=FAIL
# the control retargets the uv entry to `kerberos` (no uv.lock there) and adds a workflow using actions/setup-python, which dev never references. Save as control-134.patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0fd6779..1399592 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,7 +3,7 @@ updates:
   # Monitor Python dependencies (uv)
   - package-ecosystem: "uv"
     directory: "/"
-    target-branch: "dev"
+    target-branch: "kerberos"
     schedule:
       interval: "weekly"
     labels:
diff --git a/.github/workflows/control.yml b/.github/workflows/control.yml
new file mode 100644
index 0000000..d552de8
--- /dev/null
+++ b/.github/workflows/control.yml
@@ -0,0 +1,9 @@
+name: control
+on: workflow_dispatch
+permissions:
+  contents: read
+jobs:
+  c:
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/setup-python@v5
#!/usr/bin/env bash
# verify-134.sh: reproducible checks for blacklanternsecurity/MANSPIDER#134
# Usage: bash verify-134.sh <owner/repo> <sha> [<control-patch>]
# Changes nothing outside a mktemp directory and nothing on GitHub (PR-VF-02).
# V03-V05 compare this tree with the live branch named by target-branch; rerun them after dev changes.
#
# CONTROL: patch control-134.patch     # uv entry retargeted to `kerberos` (a poetry-era branch with no uv.lock)
#                                       # and a new workflow using an action dev never references
# CONTROL-EXPECT: V01=FAIL V03=FAIL V04=FAIL V05=FAIL
set -uo pipefail

REPO="${1:?owner/repo}"; SHA="${2:?sha}"; PATCH="${3:-}"
[ -n "$PATCH" ] && PATCH="$(cd "$(dirname "$PATCH")" && pwd)/$(basename "$PATCH")"
PASS=0; FAIL=0; SKIP=0; BROKEN=0
pass()   { echo "PASS $1 $2"; PASS=$((PASS+1)); }
fail()   { echo "FAIL $1 $2 -- $3"; FAIL=$((FAIL+1)); }
skip()   { echo "SKIP $1 $2 -- $3"; SKIP=$((SKIP+1)); }
broken() { echo "BROKEN $1 $2 -- $3"; BROKEN=$((BROKEN+1)); }
summary() { echo "SUMMARY $PASS PASS, $FAIL FAIL, $SKIP SKIP, $BROKEN BROKEN"; [ "$FAIL" -eq 0 ] && [ "$BROKEN" -eq 0 ]; }

echo "script_sha256 $(shasum -a 256 "$0" | cut -d' ' -f1)"
echo "git $(git --version | cut -d' ' -f3)"

WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0
# Public repo, anonymous HTTPS. The full (not blob:none) clone means no call needs a credential to fetch blobs.
git clone --quiet --no-checkout "https://github.com/$REPO.git" "$WORK/src" || { broken V00 "clone" "clone failed"; summary; exit 1; }
cd "$WORK/src"
git cat-file -e "$SHA^{commit}" 2>/dev/null || git fetch --quiet origin "+refs/pull/*/head:refs/remotes/pr/*" || true
git checkout --quiet --detach "$SHA" || { broken V00 "checkout $SHA" "checkout failed"; summary; exit 1; }
[ "$(git rev-parse HEAD)" = "$SHA" ] || { broken V00 "HEAD is $SHA" "HEAD mismatch"; summary; exit 1; }
if [ -n "$PATCH" ]; then git apply "$PATCH" || { broken V00 "apply control patch" "git apply failed"; summary; exit 1; }; fi

F=.github/dependabot.yml
[ -f "$F" ] || { broken V00 "$F present" "missing"; summary; exit 1; }
# target-branch of one ecosystem's entry; empty means the default branch, i.e. this tree.
target_of() { awk -v eco="\"$1\"" -F: '/^[[:space:]]*-[[:space:]]+package-ecosystem:/{e=(index($0, eco)>0)} e && /^[[:space:]]+target-branch:/{gsub(/[" ]/,"",$2); print $2; exit}' "$F"; }
tree_of() {
  if [ -z "$1" ]; then echo HEAD; return; fi
  git rev-parse --verify --quiet "refs/remotes/origin/$1" >/dev/null || return 1
  echo "refs/remotes/origin/$1"
}
GHA_TARGET=$(target_of github-actions); UV_TARGET=$(target_of uv)
GHA_TREE=$(tree_of "$GHA_TARGET") || { broken V00 "origin/$GHA_TARGET exists" "no such branch"; summary; exit 1; }
UV_TREE=$(tree_of "$UV_TARGET")   || { broken V00 "origin/$UV_TARGET exists" "no such branch"; summary; exit 1; }
echo "github-actions reads: ${GHA_TARGET:-<default branch = this tree>}   uv reads: ${UV_TARGET:-<default branch = this tree>}"
echo "origin/dev $(git rev-parse refs/remotes/origin/dev 2>/dev/null)  origin/master $(git rev-parse refs/remotes/origin/master 2>/dev/null)"

# ---- checks: each line states the desired property --------------------------
# V01: every Dependabot update entry targets dev.
ECO=$(grep -cE '^[[:space:]]*-[[:space:]]+package-ecosystem:' "$F")
TGT=$(grep -cE '^[[:space:]]+target-branch:[[:space:]]*"?dev"?[[:space:]]*$' "$F")
if [ "$ECO" -gt 0 ] && [ "$ECO" -eq "$TGT" ]; then pass V01 "every dependabot update entry sets target-branch: dev ($TGT/$ECO)"
else fail V01 "every dependabot update entry sets target-branch: dev" "$TGT of $ECO entries"; fi

# (No V02: a target branch that does not exist is reported as BROKEN V00 above.)

# V03: every workflow in this tree is still scanned by github-actions version updates
#      (docs: with target-branch, "Only manifest files on the target branch are checked").
MISSING=""
for wf in $(find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) | sort); do  # working tree, so a control patch counts
  git cat-file -e "$GHA_TREE:$wf" 2>/dev/null || MISSING="$MISSING $wf"
done
if [ -z "$MISSING" ]; then pass V03 "every workflow in this tree is scanned by github-actions version updates"
else fail V03 "every workflow in this tree is scanned by github-actions version updates" "absent on ${GHA_TARGET}:$MISSING"; fi

# V04: every action this tree uses is still offered version updates.
uses() { if [ "$1" = WT ]; then cat $(find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \)); \
         else for wf in $(git ls-tree --name-only "$1" .github/workflows/); do git show "$1:$wf"; done; fi \
         | sed -nE 's/^[[:space:]-]*uses:[[:space:]]*([^@[:space:]]+)@.*/\1/p' | sort -u; }
LOST=$(comm -23 <(uses WT) <(uses "$GHA_TREE") | tr '\n' ' ')
if [ -z "$LOST" ]; then pass V04 "every action used in this tree is offered version updates"
else fail V04 "every action used in this tree is offered version updates" "not referenced on ${GHA_TARGET}: $LOST"; fi

# V05: the uv manifests this tree has at directory "/" exist on the branch uv version updates read.
UMISS=""
for m in pyproject.toml uv.lock; do
  [ -f "$m" ] || continue
  git cat-file -e "$UV_TREE:$m" 2>/dev/null || UMISS="$UMISS $m"
done
if [ -z "$UMISS" ]; then pass V05 "uv version updates still find pyproject.toml and uv.lock"
else fail V05 "uv version updates still find pyproject.toml and uv.lock" "absent on ${UV_TARGET}:$UMISS"; fi
# ------------------------------------------------------------------------------
summary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants