Skip to content

fix(ci): auto-merge patch-level constraints.txt range bumps, and resolve constraints in CI - #353

Open
docdyhr wants to merge 1 commit into
mainfrom
fix/auto-merge-range-bumps
Open

docdyhr wants to merge 1 commit into
mainfrom
fix/auto-merge-range-bumps

Conversation

@docdyhr

@docdyhr docdyhr commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

The native patch auto-merge (#344) never fired on pip range updates in constraints.txt. dependabot/fetch-metadata reads previous-version from the range string (10.0.0,>=9.1.0) and reports version-update:semver-major even for a true patch, so every such run showed green while skipping the approve and merge steps (sphinx 9.1.0 → 9.1.1 in run 40478242807, virtualenv 21.14.6 → 21.14.13 in run 40478270225). Ruled out: the ruff exclusion (dependency-names had no ruff) and permissions (the steps were skipped, not failed).

Classifying them correctly would not be enough on its own, because CI never resolved these ranges. setup-python-deps never passed -c constraints.txt (its README claimed it did), and pip ignores a constraint for any package an install doesn't pull in. sphinx, tox, virtualenv and pre-commit aren't installed in CI at all.

Changes

  • dependabot-auto-merge.yml: a new level step derives the bump from the PR title's old and new bounds: a changed upper bound counts as major, and a title it can't read counts as skip. Exact pins and Actions bumps keep fetch-metadata's type. The approve, merge and skip steps gate on steps.level.outputs.update-type. The title reaches the script only through env:, and the output is one of three fixed strings.
  • setup-python-deps/action.yml: the requirements and package installs use -c constraints.txt when the file exists.
  • lint.yml (the required "Code Linting and Formatting" check): a new step resolves the whole file with pip install --dry-run --ignore-installed -r constraints.txt (about 5 s). A floor that doesn't exist or conflicts now fails a required check before auto-merge can land it. The step runs on pull_request with a read-only token.
  • tests/test_dependabot_auto_merge_workflow.py: 23 tests run the workflow's and the action's own scripts against the metadata of the real runs, under bash 3.2 and bash 5.
  • CHANGELOG: entry under Unreleased → Fixed.

Verification

After merge, the open range PRs need @dependabot rebase before the workflow re-evaluates them. Only #346 (sphinx) and #348 (virtualenv) are patch-level; the rest stay manual.

🤖 Generated with Claude Code

Summary by Sourcery

Make Dependabot safely auto-merge patch-level constraints updates while ensuring all constraint ranges are resolved by required CI checks.

New Features:

  • Classify Dependabot requirement-range updates from their old and new bounds so patch-level bumps can be auto-merged safely.

Bug Fixes:

  • Restore patch auto-merge for eligible constraints.txt range updates and prevent unresolvable constraint changes from passing CI unnoticed.

Enhancements:

  • Apply constraints.txt consistently to dependency and editable package installations in the shared Python dependency action.
  • Add a required lint check that resolves the complete constraints file before merging.
  • Handle unclassifiable range-update titles as skipped and preserve metadata classifications for exact-version and GitHub Actions updates.

CI:

  • Update Dependabot auto-merge gating to use the derived update level.
  • Add coverage for workflow classification, constrained installs, security of title handling, and constraints resolution.

Documentation:

  • Document the constraints range auto-merge and CI resolution fixes in the changelog.

Tests:

  • Add tests covering real Dependabot metadata, range classification, constrained installation commands, and required lint workflow behavior.

…lve constraints in CI

Root cause: dependabot/fetch-metadata reads previous-version from a
requirement range ("10.0.0,>=9.1.0") and reports semver-major even for a
patch (runs 40478242807 sphinx 9.1.0->9.1.1, 40478270225 virtualenv
21.14.6->21.14.13), so no pip range PR ever matched the patch condition.
Ruled out: the ruff exclusion (dependency-names had no ruff) and
permissions (approve/merge steps were skipped, not failed).

Fix: a `level` step derives the bump from the PR title's old and new
bounds (changed upper bound = major, unreadable title = skip); exact pins
and Actions bumps keep fetch-metadata's type. The title reaches the script
only through env.

Merging these unattended needs CI to test them, and it didn't:
setup-python-deps never passed -c constraints.txt (its README said it did),
and pip ignores constraints for packages an install doesn't pull in
(sphinx, tox, virtualenv, pre-commit). The action now installs with
-c constraints.txt, and the required "Code Linting and Formatting" job
resolves the whole file (pip install --dry-run -r constraints.txt, ~5 s),
so a floor that doesn't exist or conflicts fails a required check before
auto-merge can land it.

tests/test_dependabot_auto_merge_workflow.py runs the workflow's and the
action's own scripts (bash 3.2 and 5) against the metadata of the real
runs; 22 of its 23 tests fail against main's files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR fixes Dependabot patch auto-merge for constraints.txt range updates by deriving semver severity from PR-title bounds, safely routing that result through workflow gates, and making CI actually apply and fully resolve constraints so invalid ranges fail required checks before merging.

Sequence diagram for constraints range validation and auto-merge

sequenceDiagram
    participant Dependabot
    participant Workflow as GitHub Actions
    participant Level as Derive bump level
    participant Lint as Required lint check
    participant Pip
    participant Merge as Auto-merge

    Dependabot->>Workflow: Open constraints.txt update PR
    Workflow->>Level: Read PR title and fetch-metadata
    Level-->>Workflow: update-type
    Workflow->>Lint: Run Check constraints.txt resolves
    Lint->>Pip: install --dry-run --ignore-installed -r constraints.txt
    Pip-->>Lint: Resolution result
    alt patch-level lower-bound update and checks pass
        Workflow->>Merge: Approve and enable auto-merge
    else changed upper bound, invalid title, or failed resolution
        Workflow-->>Dependabot: Skip auto-merge
    end
Loading

Flow diagram for deriving the Dependabot bump level

flowchart TD
    A[Read fetch-metadata and PR title] --> B{Previous version is exact}
    B -->|Yes| C[Keep fetch-metadata update type]
    B -->|No| D[Parse old and new requirement bounds]
    D --> E{Title and bounds are readable}
    E -->|No| F[Skip auto-merge]
    E -->|Yes| G{Upper bound changed}
    G -->|Yes| H[Classify as semver-major]
    G -->|No| I[Compare lower-bound versions]
    I --> J[Classify patch, minor, or major]
    C --> K{Update type is patch}
    H --> K
    J --> K
    K -->|Yes| L[Approve and enable auto-merge]
    K -->|No| F
Loading

File-Level Changes

Change Details Files
Derive Dependabot update severity from requirement-range bounds while preserving metadata classification for exact-version and Actions updates.
  • Added a bash level step that parses old/new ranges from the PR title, treating upper-bound changes as major and unclassifiable titles as skip.
  • Changed approval, auto-merge, and skip reporting to gate on the derived level while keeping the ruff exclusion.
  • Passed the title through environment variables to prevent shell injection.
.github/workflows/dependabot-auto-merge.yml
Apply constraints consistently during dependency installation and add an explicit required resolvability check.
  • Conditionally pass -c constraints.txt to requirements and editable package installs when the file exists.
  • Resolve the complete constraints file with pip dry-run in the required lint job so orphaned or conflicting constraints fail CI.
.github/actions/setup-python-deps/action.yml
.github/workflows/lint.yml
Add regression coverage for range classification, shell-safety, workflow gating, constrained installs, and constraints resolution.
  • Test real Dependabot metadata/title scenarios across patch, minor, major, exact, malformed, and upper-bound changes under configurable bash versions.
  • Verify action command construction with and without constraints and inspect the required lint check configuration.
tests/test_dependabot_auto_merge_workflow.py
Document the corrected auto-merge and CI constraint-resolution behavior.
  • Added an Unreleased/Fixed changelog entry covering classification, constrained installation, and full-file resolution.
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T00:56:58.594492Z 206c2c1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

🔒 Security Analysis Report

Security Analysis Report

Generated: Wed Oct 28 00:56:27 UTC 2026
Repository: docdyhr/versiontracker
Commit: 04c51c6

Bandit Security Scan

�[?25l
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  0%�[0m �[36m-:--:--�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  0%�[0m �[36m-:--:--�[0m
�[2KWorking... �[91m━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  8%�[0m �[36m-:--:--�[0m
�[2KWorking... �[91m━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 17%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 31%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 41%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━�[0m �[35m 66%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━━━━�[0m �[35m 77%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━�[0m �[35m 84%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━�[0m �[35m 91%�[0m �[36m0:00:01�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m100%�[0m �[33m0:00:00�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m100%�[0m �[33m0:00:00�[0m
�[?25hRun started:2026-10-28 00:56:28.476853+00:00

Test results:
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Low
   CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b608_hardcoded_sql_expressions.html
   Location: versiontracker/advanced_cache.py:693:24
692	                # Use f-string for better readability
693	                msg = f"Failed to delete from cache {key}: {e}"
694	                raise CacheError(msg) from e

--------------------------------------------------
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: 'homebrew_token'
   Severity: Low   Confidence: Medium
   CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b105_hardcoded_password_string.html
   Location: versiontracker/audit/models.py:161:21
160	    BUNDLE_ID = "bundle_id"
161	    HOMEBREW_TOKEN = "homebrew_token"
162	    DISPLAY_NAME = "display_name"

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/config.py:174:16
173	                cmd = f"{path} --version"
174	                import subprocess
175	

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/config.py:177:29
176	                try:
177	                    result = subprocess.run(cmd.split(), capture_output=True, timeout=2, check=False)
178	                    returncode = result.returncode

--------------------------------------------------
>> Issue: [B110:try_except_pass] Try, Except, Pass detected.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b110_try_except_pass.html
   Location: versiontracker/experimental/analytics.py:645:16
644	                    self.peak_cpu = max(self.peak_cpu, cpu_percent)
645	                except Exception:
646	                    pass
647	                time.sleep(0.05)

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:518:8
517	        # Type assertion: apps and brews cannot be None here due to exit_code checks above
518	        assert apps is not None
519	        assert brews is not None

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:519:8
518	        assert apps is not None
519	        assert brews is not None
520	        apps = _filter_applications(apps, brews, include_brews)

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:539:8
538	        # Type assertion: outdated_info cannot be None here due to exit_code check
539	        assert outdated_info is not None
540	        # Type cast: outdated_info cannot be None here due to exit_code check above

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/homebrew.py:12:0
11	import re
12	import subprocess
13	import time

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/macos_integration.py:12:0
11	import plistlib
12	import subprocess
13	from pathlib import Path

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/menubar_app.py:8:0
7	import logging
8	import subprocess
9	import sys

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/plugins/example_plugins.py:322:16
321	            try:
322	                import subprocess
323	

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/plugins/example_plugins.py:324:25
323	
324	                result = subprocess.run(["brew", "--version"], capture_output=True, text=True, timeout=5)
325	                if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/plugins/example_plugins.py:324:25
323	
324	                result = subprocess.run(["brew", "--version"], capture_output=True, text=True, timeout=5)
325	                if result.returncode == 0:

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/utils.py:16:0
15	import shutil
16	import subprocess
17	import sys

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/utils.py:804:17
803	    try:
804	        result = subprocess.run(["which", "brew"], capture_output=True, text=True, timeout=5)
805	        return result.returncode == 0

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:804:17
803	    try:
804	        result = subprocess.run(["which", "brew"], capture_output=True, text=True, timeout=5)
805	        return result.returncode == 0

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/utils.py:820:17
819	    try:
820	        result = subprocess.run(["brew", "--prefix"], capture_output=True, text=True, timeout=5)
821	        if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:820:17
819	    try:
820	        result = subprocess.run(["brew", "--prefix"], capture_output=True, text=True, timeout=5)
821	        if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:853:15
852	    try:
853	        return subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=check)
854	    except subprocess.TimeoutExpired as e:

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/version/batch.py:11:0
10	import logging
11	import subprocess
12	from concurrent.futures import ThreadPoolExecutor

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/version/homebrew.py:18:0
17	import logging
18	import subprocess
19	

--------------------------------------------------

Code scanned:
	Total lines of code: 17221
	Total lines skipped (#nosec): 0
	Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 30

Run metrics:
	Total issues (by severity):
		Undefined: 0
		Low: 21
		Medium: 1
		High: 0
	Total issues (by confidence):
		Undefined: 0
		Low: 1
		Medium: 1
		High: 20
Files skipped (0):

Pip-Audit Results

�[?25l�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting aiohappyeyeballs (2.7.1)
�[2K�[32m\�[0m Auditing aiohappyeyeballs (2.7.1)
�[2K�[32m\�[0m Collecting aiohttp (3.14.5)
�[2K�[32m\�[0m Auditing aiohttp (3.14.5)
�[2K�[32m\�[0m Collecting aiosignal (1.4.0)
�[2K�[32m\�[0m Auditing aiosignal (1.4.0)
�[2K�[32m\�[0m Collecting ast_serialize (0.13.1)
�[2K�[32m\�[0m Auditing ast_serialize (0.13.1)
�[2K�[32m\�[0m Collecting attrs (26.1.0)
�[2K�[32m\�[0m Auditing attrs (26.1.0)
�[2K�[32m\�[0m Collecting bandit (1.9.4)
�[2K�[32m\�[0m Auditing bandit (1.9.4)
�[2K�[32m\�[0m Collecting boolean.py (5.0)
�[2K�[32m\�[0m Auditing boolean.py (5.0)
�[2K�[32m\�[0m Collecting build (1.6.1)
�[2K�[32m\�[0m Auditing build (1.6.1)
�[2K�[32m\�[0m Collecting CacheControl (0.14.4)
�[2K�[32m\�[0m Auditing CacheControl (0.14.4)
�[2K�[32m\�[0m Collecting certifi (2026.10.23)
�[2K�[32m\�[0m Auditing certifi (2026.10.23)
�[2K�[32m\�[0m Collecting cffi (2.1.1)
�[2K�[32m\�[0m Auditing cffi (2.1.1)
�[2K�[32m\�[0m Collecting charset-normalizer (3.5.2)
�[2K�[32m\�[0m Auditing charset-normalizer (3.5.2)
�[2K�[32m\�[0m Collecting coverage (7.17.0)
�[2K�[32m\�[0m Auditing coverage (7.17.0)
�[2K�[32m\�[0m Collecting cryptography (50.0.2)
�[2K�[32m\�[0m Auditing cryptography (50.0.2)
�[2K�[32m\�[0m Collecting cyclonedx-python-lib (11.12.0)
�[2K�[32m\�[0m Auditing cyclonedx-python-lib (11.12.0)
�[2K�[32m\�[0m Collecting defusedxml (0.7.1)
�[2K�[32m\�[0m Auditing defusedxml (0.7.1)
�[2K�[32m\�[0m Collecting docutils (0.23)
�[2K�[32m\�[0m Auditing docutils (0.23)
�[2K�[32m\�[0m Collecting filelock (4.2.4)
�[2K�[32m\�[0m Auditing filelock (4.2.4)
�[2K�[32m\�[0m Collecting frozenlist (1.8.0)
�[2K�[32m\�[0m Auditing frozenlist (1.8.0)
�[2K�[32m\�[0m Collecting id (1.6.1)
�[2K�[32m\�[0m Auditing id (1.6.1)
�[2K�[32m\�[0m Collecting idna (3.20)
�[2K�[32m\�[0m Auditing idna (3.20)
�[2K�[32m\�[0m Collecting iniconfig (2.3.1)
�[2K�[32m\�[0m Auditing iniconfig (2.3.1)
�[2K�[32m\�[0m Collecting jaraco.classes (3.4.0)
�[2K�[32m\�[0m Collecting jaraco.classes (3.4.0)
�[2K�[32m\�[0m Auditing jaraco.classes (3.4.0)
�[2K�[32m\�[0m Collecting jaraco.context (6.1.2)
�[2K�[32m\�[0m Auditing jaraco.context (6.1.2)
�[2K�[32m\�[0m Collecting jaraco.functools (4.6.0)
�[2K�[32m\�[0m Auditing jaraco.functools (4.6.0)
�[2K�[32m\�[0m Collecting jeepney (0.9.0)
�[2K�[32m\�[0m Auditing jeepney (0.9.0)
�[2K�[32m\�[0m Collecting keyring (25.7.0)
�[2K�[32m\�[0m Auditing keyring (25.7.0)
�[2K�[32m\�[0m Collecting librt (0.16.0)
�[2K�[32m\�[0m Auditing librt (0.16.0)
�[2K�[32m\�[0m Collecting license-expression (30.4.4)
�[2K�[32m\�[0m Auditing license-expression (30.4.4)
�[2K�[32m\�[0m Collecting macversiontracker (1.2.0)
�[2K�[32m\�[0m Auditing macversiontracker (1.2.0)
�[2K�[32m\�[0m Collecting markdown-it-py (4.2.0)
�[2K�[32m\�[0m Auditing markdown-it-py (4.2.0)
�[2K�[32m\�[0m Collecting mdurl (0.1.2)
�[2K�[32m\�[0m Auditing mdurl (0.1.2)
�[2K�[32m\�[0m Collecting more-itertools (11.2.1)
�[2K�[32m\�[0m Auditing more-itertools (11.2.1)
�[2K�[32m\�[0m Collecting msgpack (1.2.4)
�[2K�[32m\�[0m Auditing msgpack (1.2.4)
�[2K�[32m\�[0m Collecting multidict (7.2.1)
�[2K�[32m\�[0m Auditing multidict (7.2.1)
�[2K�[32m\�[0m Collecting mypy (2.4.0)
�[2K�[32m\�[0m Auditing mypy (2.4.0)
�[2K�[32m\�[0m Collecting mypy_extensions (1.1.0)
�[2K�[32m\�[0m Auditing mypy_extensions (1.1.0)
�[2K�[32m\�[0m Collecting nh3 (0.3.7)
�[2K�[32m\�[0m Auditing nh3 (0.3.7)
�[2K�[32m\�[0m Collecting packageurl-python (0.17.6)
�[2K�[32m\�[0m Auditing packageurl-python (0.17.6)
�[2K�[32m\�[0m Collecting packaging (26.3)
�[2K�[32m\�[0m Auditing packaging (26.3)
�[2K�[32m\�[0m Collecting pathspec (1.1.1)
�[2K�[32m\�[0m Auditing pathspec (1.1.1)
�[2K�[32m\�[0m Collecting pip (26.2.2)
�[2K�[32m\�[0m Auditing pip (26.2.2)
�[2K�[32m\�[0m Collecting pip_api (0.0.35)
�[2K�[32m\�[0m Auditing pip_api (0.0.35)
�[2K�[32m\�[0m Collecting pip_audit (2.10.1)
�[2K�[32m\�[0m Auditing pip_audit (2.10.1)
�[2K�[32m\�[0m Collecting pip-requirements-parser (32.0.1)
�[2K�[32m\�[0m Auditing pip-requirements-parser (32.0.1)
�[2K�[32m\�[0m Collecting platformdirs (4.13.6)
�[2K�[32m\�[0m Auditing platformdirs (4.13.6)
�[2K�[32m\�[0m Collecting pluggy (1.7.0)
�[2K�[32m\�[0m Auditing pluggy (1.7.0)
�[2K�[32m\�[0m Collecting propcache (0.5.4)
�[2K�[32m\�[0m Auditing propcache (0.5.4)
�[2K�[32m\�[0m Collecting psutil (7.2.2)
�[2K�[32m\�[0m Auditing psutil (7.2.2)
�[2K�[32m\�[0m Collecting py-serializable (2.1.0)
�[2K�[32m\�[0m Auditing py-serializable (2.1.0)
�[2K�[32m\�[0m Collecting pycparser (3.12)
�[2K�[32m\�[0m Auditing pycparser (3.12)
�[2K�[32m\�[0m Collecting Pygments (2.21.0)
�[2K�[32m\�[0m Auditing Pygments (2.21.0)
�[2K�[32m\�[0m Collecting pyparsing (3.3.3)
�[2K�[32m\�[0m Auditing pyparsing (3.3.3)
�[2K�[32m\�[0m Collecting pyproject_hooks (1.3.3)
�[2K�[32m\�[0m Auditing pyproject_hooks (1.3.3)
�[2K�[32m\�[0m Collecting pytest (9.2.0)
�[2K�[32m\�[0m Auditing pytest (9.2.0)
�[2K�[32m\�[0m Collecting pytest-asyncio (1.4.1)
�[2K�[32m\�[0m Auditing pytest-asyncio (1.4.1)
�[2K�[32m\�[0m Collecting pytest-cov (7.1.0)
�[2K�[32m\�[0m Auditing pytest-cov (7.1.0)
�[2K�[32m\�[0m Collecting pytest-mock (3.17.0)
�[2K�[32m\�[0m Collecting pytest-mock (3.17.0)
�[2K�[32m\�[0m Auditing pytest-mock (3.17.0)
�[2K�[32m\�[0m Collecting pytest-timeout (2.4.0)
�[2K�[32m\�[0m Auditing pytest-timeout (2.4.0)
�[2K�[32m\�[0m Collecting PyYAML (6.0.3)
�[2K�[32m\�[0m Auditing PyYAML (6.0.3)
�[2K�[32m\�[0m Collecting readme_renderer (46.0)
�[2K�[32m\�[0m Auditing readme_renderer (46.0)
�[2K�[32m\�[0m Collecting requests (2.34.2)
�[2K�[32m\�[0m Auditing requests (2.34.2)
�[2K�[32m\�[0m Collecting requests-toolbelt (1.0.0)
�[2K�[32m\�[0m Auditing requests-toolbelt (1.0.0)
�[2K�[32m\�[0m Collecting rfc3986 (2.0.0)
�[2K�[32m\�[0m Auditing rfc3986 (2.0.0)
�[2K�[32m\�[0m Collecting rich (15.0.0)
�[2K�[32m\�[0m Auditing rich (15.0.0)
�[2K�[32m\�[0m Collecting ruff (0.17.0)
�[2K�[32m\�[0m Auditing ruff (0.17.0)
�[2K�[32m\�[0m Collecting SecretStorage (3.5.0)
�[2K�[32m\�[0m Auditing SecretStorage (3.5.0)
�[2K�[32m\�[0m Collecting setuptools (84.2.1)
�[2K�[32m\�[0m Auditing setuptools (84.2.1)
�[2K�[32m\�[0m Collecting sortedcontainers (2.4.0)
�[2K�[32m\�[0m Auditing sortedcontainers (2.4.0)
�[2K�[32m\�[0m Collecting stevedore (5.9.1)
�[2K�[32m\�[0m Auditing stevedore (5.9.1)
�[2K�[32m\�[0m Collecting tabulate (0.10.0)
�[2K�[32m\�[0m Auditing tabulate (0.10.0)
�[2K�[32m\�[0m Collecting termcolor (3.3.0)
�[2K�[32m\�[0m Auditing termcolor (3.3.0)
�[2K�[32m\�[0m Collecting tomli (2.5.0)
�[2K�[32m\�[0m Auditing tomli (2.5.0)
�[2K�[32m\�[0m Collecting tomli_w (1.2.0)
�[2K�[32m\�[0m Auditing tomli_w (1.2.0)
�[2K�[32m\�[0m Collecting tqdm (4.70.1)
�[2K�[32m\�[0m Auditing tqdm (4.70.1)
�[2K�[32m\�[0m Collecting twine (7.0.0)
�[2K�[32m\�[0m Auditing twine (7.0.0)
�[2K�[32m\�[0m Collecting types-PyYAML (6.0.12.20260906)
�[2K�[32m\�[0m Auditing types-PyYAML (6.0.12.20260906)
�[2K�[32m\�[0m Collecting typing_extensions (4.16.0)
�[2K�[32m\�[0m Auditing typing_extensions (4.16.0)
�[2K�[32m\�[0m Collecting urllib3 (2.8.0)
�[2K�[32m\�[0m Auditing urllib3 (2.8.0)
�[2K�[32m\�[0m Collecting wheel (0.48.0)
�[2K�[32m\�[0m Auditing wheel (0.48.0)
�[2K�[32m\�[0m Collecting yarl (1.25.1)
�[2K�[32m\�[0m Auditing yarl (1.25.1)
�[2K�[32m\�[0m Auditing yarl (1.25.1)
�[?25h
�[1A�[2K```

@codecov

codecov Bot commented Oct 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path=".github/workflows/dependabot-auto-merge.yml" line_range="86" />
<code_context>
+        env:
+          META_TYPE: ${{ steps.meta.outputs.update-type }}
+          PREV: ${{ steps.meta.outputs.previous-version }}
+          TITLE: ${{ github.event.pull_request.title }}
+
       - name: Approve patch bump
</code_context>
<issue_to_address>
**Major updates get auto-merged**

When a patch-classified run is delayed while Dependabot updates the PR to a non-patch bump, the run keeps the patch classification from its event while `gh pr merge --auto` targets the PR's current state, so the newer non-patch update gets auto-merge enabled and lands once its checks pass.

Before enabling auto-merge, verify that the PR's current head still matches the head and update classified by the run.

Also at `.github/workflows/dependabot-auto-merge.yml:87`, `.github/workflows/dependabot-auto-merge.yml:97`, `.github/workflows/dependabot-auto-merge.yml:108`.
</issue_to_address>

### Comment 2
<location path=".github/workflows/dependabot-auto-merge.yml" line_range="58-59" />
<code_context>
+          if ! printf '%s' "$PREV" | grep -Eq '^[0-9]+(\.[0-9A-Za-z]+)*$'; then
+            # Range update: compare the bounds in "update X requirement from <old> to <new>"
+            level=""
+            old=$(printf '%s' "$TITLE" | sed -nE 's/.* requirement from ([^ ]+) to ([^ ]+)( in .*)?$/\1/p')
+            new=$(printf '%s' "$TITLE" | sed -nE 's/.* requirement from ([^ ]+) to ([^ ]+)( in .*)?$/\2/p')
+            old_lo=$(printf '%s\n' "$old" | tr ',' '\n' | sed -n 's/^>=//p')
+            new_lo=$(printf '%s\n' "$new" | tr ',' '\n' | sed -n 's/^>=//p')
</code_context>
<issue_to_address>
**Edited titles can trigger major auto-merges**

When a Dependabot PR title is changed to describe a patch while its constraints change is major, `level` classifies the bounds in `TITLE` without checking them against the changed constraints file, so a Dependabot PR retitled with patch-level bounds is approved and auto-merged even when its actual change is major; callers see a major dependency update merge without the intended review.

Derive the level from the actual old and new requirement bounds in the PR diff, or verify the title’s bounds against those values before allowing auto-merge.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and the derived level controls whether a constraints-only dependency update is approved and auto-merged; if the title parsing classifies a non-patch change as a patch, the new dependency version can be installed and its code can run in CI without review. Reverting restores the constraints, but cannot undo any secrets or data exposed by code that already ran.

Blocking findings: .github/workflows/dependabot-auto-merge.yml:86, .github/workflows/dependabot-auto-merge.yml:59


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

env:
META_TYPE: ${{ steps.meta.outputs.update-type }}
PREV: ${{ steps.meta.outputs.previous-version }}
TITLE: ${{ github.event.pull_request.title }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium · Major updates get auto-merged

When a patch-classified run is delayed while Dependabot updates the PR to a non-patch bump, the run keeps the patch classification from its event while gh pr merge --auto targets the PR's current state, so the newer non-patch update gets auto-merge enabled and lands once its checks pass.

Before enabling auto-merge, verify that the PR's current head still matches the head and update classified by the run.

Also at .github/workflows/dependabot-auto-merge.yml:87, .github/workflows/dependabot-auto-merge.yml:97, .github/workflows/dependabot-auto-merge.yml:108.

Prompt for AI agents
In `.github/workflows/dependabot-auto-merge.yml` at line 86:

**Major updates get auto-merged**

When a patch-classified run is delayed while Dependabot updates the PR to a non-patch bump, the run keeps the patch classification from its event while `gh pr merge --auto` targets the PR's current state, so the newer non-patch update gets auto-merge enabled and lands once its checks pass.

Before enabling auto-merge, verify that the PR's current head still matches the head and update classified by the run.

Also at `.github/workflows/dependabot-auto-merge.yml:87`, `.github/workflows/dependabot-auto-merge.yml:97`, `.github/workflows/dependabot-auto-merge.yml:108`.

Comment on lines +58 to +59
old=$(printf '%s' "$TITLE" | sed -nE 's/.* requirement from ([^ ]+) to ([^ ]+)( in .*)?$/\1/p')
new=$(printf '%s' "$TITLE" | sed -nE 's/.* requirement from ([^ ]+) to ([^ ]+)( in .*)?$/\2/p')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High · Edited titles can trigger major auto-merges

When a Dependabot PR title is changed to describe a patch while its constraints change is major, level classifies the bounds in TITLE without checking them against the changed constraints file, so a Dependabot PR retitled with patch-level bounds is approved and auto-merged even when its actual change is major; callers see a major dependency update merge without the intended review.

Derive the level from the actual old and new requirement bounds in the PR diff, or verify the title’s bounds against those values before allowing auto-merge.

Prompt for AI agents
In `.github/workflows/dependabot-auto-merge.yml` at lines 58-59:

**Edited titles can trigger major auto-merges**

When a Dependabot PR title is changed to describe a patch while its constraints change is major, `level` classifies the bounds in `TITLE` without checking them against the changed constraints file, so a Dependabot PR retitled with patch-level bounds is approved and auto-merged even when its actual change is major; callers see a major dependency update merge without the intended review.

Derive the level from the actual old and new requirement bounds in the PR diff, or verify the title’s bounds against those values before allowing auto-merge.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant