Dependabot PRs: Semver Is Not a Risk Model

A version number says what the library author promises. It does not say what the update will change in your project. In this guide Jev rates Dependabot pull requests from the release notes and the project policy. A Buddy pipeline merges the safe updates on its own and leaves the rest for review.

Where semver gets it wrong

Semver (semantic versioning) is a convention for numbering releases as major.minor.patch:

  • patch - a fix without API changes, for example 3.9.8 → 3.9.9,
  • minor - a new feature that stays backward compatible, for example 0.26 → 0.27,
  • major - a change that may break something, for example 9 → 10.

The simplest auto-merge rule is built on this convention: merge patch and minor, wait on major. Three cases where this rule gets it wrong:

  • A minor in 0.x in production code. @floating-ui/react 0.26 → 0.27 is a minor for the rule, so merge. In 0.x, however, a minor may change the API.
  • A major of a development tool. ESLint 9 → 10 is a major, so wait. ESLint never reaches the browser. A major may change the lint result, but then the checks fail and the pipeline merges nothing. If lint, tests and build pass, users will not notice anything.
  • A major of a build tool. Vite 7 → 8 is also in devDependencies, but it is the tool that builds the bundle. Unit tests do not check whether the built page works in the browser.

GitHub has the dependabot/fetch-metadata action for this. It returns the bump type: semver-patch, semver-minor or semver-major. It is still the same version number. The context is missing: what the package does in the project and what the release notes say.

Project

FrogePC is a small store built with Vite and React. The cart, discount and currency logic lives in src/ and has tests. All dependencies are pinned to exact versions, so Dependabot proposes every new release.

.github/dependabot.yml checks npm once a week and groups the ESLint packages into one PR:

yaml
version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly open-pull-requests-limit: 10 groups: eslint: patterns: - eslint - "@eslint/*"

Policy in the repository

Jev judges only what it is given. That is why the rules are written down in .buddy/dependency-policy.md and not only agreed within the team:

  • everything in dependencies ends up in the browser,
  • any change to a 0.x package in runtime is high risk, a minor included,
  • a Vite major is high risk, because the tests do not run the built page,
  • a major of a tool that does not reach the bundle (ESLint, Prettier) is low risk if the checks pass,
  • breaking changes in release notes raise the risk regardless of the version number.

The policy also says what the tests do not cover: the React components in web/.

Pipeline

The pipeline starts on a PR to main. A condition on BUDDY_RUN_PR_HEAD_BRANCH lets through only dependabot/ branches. The actions in order: release notes, checks, Jev's rating and then one of three branches: merge, a request for review or Fail the run.

Image loading...Workflow tab of the Dependency review pipeline in Buddy: Git pull request trigger on main, then Fetch release notes, Run checks, Rate update risk, Merge pull request, Ask for review and Fail the run, every action after Run checks carries an IF badge

Release notes and checks

Dependabot pastes the release notes and the changelog into the PR description, so this is the most important context for Jev. A GitHub CLI action fetches them. It signs in through the GitHub integration in Buddy, with no separate token in variables:

yaml
- action: Fetch release notes type: GIT_HUB_CLI integration: GitHub commands: gh pr view "$BUDDY_RUN_PR_NO" --json title,body --template '# {{.title}}{{"\n\n"}}{{.body}}' > pr-notes.md

BUDDY_RUN_PR_NO is the PR number. The GH_REPO pipeline variable points to the repository. The file lands in the pipeline filesystem, so the next actions can see it. Then the checks:

yaml
- action: Run checks type: BUILD docker_image_name: node docker_image_tag: "22" commands: |- npm ci --ignore-scripts --no-audit --no-fund if { npm run lint && npm run format:check && npm test && npm run build; } > checks.log 2>&1; then export OUTPUT_CHECKS=passed; else export OUTPUT_CHECKS=failed; fi

The action stays green and the result of the checks goes to OUTPUT_CHECKS.

Jev rates the risk

Image loading...Rate update risk action in Buddy: typesafe integration, question How risky is it to merge this dependency update without a human review?, question type Score, criteria none, low and high, state with the pull request title, .buddy/dependency-policy.md, package.json and pr-notes.md, and the generated variables BUDDY_ACTION_JEV_RESULT, SCORE, CONFIDENCE and PROBABILITY per level

The type is Score, because risk levels have an order. The criteria, from the lowest:

  • none: only development tools change, or a stable runtime package gets a patch that does not change behaviour,
  • low: a major of a development tool, or a minor of a stable runtime package with no breaking changes and no changed defaults,
  • high: any change to a 0.x runtime package, a major of a runtime package or of Vite, or breaking changes in the release notes.

As context Jev gets the PR title, the policy, package.json and the release notes. It runs only after green checks. There is no point in rating an update that does not pass the build.

Merge or review

Merge pull request runs when the checks passed, the result is not high and confidence is at least 0.7. Merge first, comment second:

bash
RATING="Jev rated this update \`$BUDDY_ACTION_JEV_RESULT\` risk (confidence $BUDDY_ACTION_JEV_CONFIDENCE). Checks passed" if gh pr merge "$BUDDY_RUN_PR_NO" --squash --match-head-commit "$BUDDY_RUN_COMMIT"; then gh pr comment "$BUDDY_RUN_PR_NO" --body "$RATING, merged." exit 0 fi HEAD_SHA=$(gh pr view "$BUDDY_RUN_PR_NO" --json headRefOid -q .headRefOid) MERGEABLE=$(gh pr view "$BUDDY_RUN_PR_NO" --json mergeable -q .mergeable) if [ "$HEAD_SHA" != "$BUDDY_RUN_COMMIT" ]; then echo "The branch moved to $HEAD_SHA during the run, the next run reviews it." exit 0 fi if [ "$MERGEABLE" = "CONFLICTING" ]; then gh pr comment "$BUDDY_RUN_PR_NO" --body "@dependabot rebase" exit 0 fi gh pr comment "$BUDDY_RUN_PR_NO" --body "$RATING, but the merge failed ($MERGEABLE). Merge it by hand." exit 1 $$$$$$$$$$$$$$$$$
  • --match-head-commit merges only the commit that passed the checks. If Dependabot pushes a new one during the run, the next run rates it.
  • On a conflict with main the action asks Dependabot for a rebase, and the synchronize event starts the pipeline again.
  • Any other error leaves a comment in the PR and ends the run with the Failed status.

On the PR with eslint 10.12.0 Jev gives none with confidence 0.95. The action log shows the order: first the merge of the exact commit that passed the checks, then the comment.

Image loading...Run #17 of the Dependency review pipeline for Bump eslint from 10.11.0 to 10.12.0: Jev rates none at 97% with confidence 0.95, the Merge pull request log shows gh pr merge 9 --squash --match-head-commit, then gh pr comment with Checks passed, merged., Ask for review and Fail the run are skipped

Ask for review runs on high or on confidence below 0.7 and leaves Jev's rating in the PR. Fail the run runs after failed checks and prints the end of checks.log.

Five PRs, one pipeline

PR Semver Semver rule Jev Pipeline
prettier 3.9.8 → 3.9.9 patch, dev merge none (1.00) merge
eslint + @eslint/js 9 → 10 major, dev wait low (0.78) merge
vite 7.3.6 → 8.3.1 major, build wait high (0.64-0.67) review
lucide-react 0.487 → 1.48 major, runtime wait high (0.87) review
@floating-ui/react 0.26 → 0.27 minor in 0.x, runtime merge high (0.97) review

The semver rule and Jev agree in only three cases out of five. The pipeline merged two PRs on its own:

Image loading...Closed pull requests: Bump prettier from 3.9.8 to 3.9.9 and Bump the eslint group across 1 directory with 2 updates, both merged

Three wait for a human, each with a comment from the pipeline:

Image loading...Open pull requests: lucide-react 0.487.0 to 1.48.0, vite 7.3.6 to 8.3.1 and @floating-ui/react 0.26.28 to 0.27.20, all with passing checks

Prettier: a patch that changes nothing

The 3.9.9 release notes have one fix in the Markdown parser. Prettier only formats code, so Jev gives none with confidence 1.00. The pipeline leaves a comment and merges. This run used an earlier version of the action that commented before the merge. That is why the comment says "merging" and not "merged":

Image loading...Pull request #4 Bump prettier from 3.9.8 to 3.9.9: comment Jev rated this update none risk (confidence 1.00). Checks passed, merging., followed by the squash merge into main

ESLint 10: a major that can be merged

The ESLint 10 release notes are a long list of breaking changes: the end of .eslintrc support, removed SourceCode and context methods, a stricter RuleTester. For a plugin author that is a lot of work. This project uses flat config and has no custom rules, and ESLint never reaches the bundle. Lint, tests and build pass, so Jev gives low with confidence 0.78 and the pipeline merges despite the major.

The first run did not merge the PR. After the Prettier merge the ESLint branch had a conflict with main. After the @dependabot rebase comment Dependabot rebuilt the branch, synchronize started the pipeline again and that run merged the PR. Today the Merge pull request action sends that comment itself.

Image loading...Pull request #1 Bump the eslint group: Jev rated this update low risk (confidence 0.79), then @dependabot rebase and a force-push from Dependabot, a second rating low risk (confidence 0.78), and the squash merge of commit bc6503a into main

Vite 8: a major of the tool that builds the page

The Vite 8.3.1 release notes are mostly fixes, but it is still a major of the tool that builds the bundle. The policy says that the tests do not run the built page, so Jev gives high. Confidence is the lowest here (0.64-0.67), because the checks pass and there is nothing alarming in the notes. After every Dependabot rebase the pipeline rates the PR again and asks for a review each time:

Image loading...Pull request #3 Bump vite from 7.3.6 to 8.3.1: three comments after successive force-pushes, Jev rated this update high risk with confidence 0.65, 0.67 and 0.64, each one asking for a human review

Floating UI 0.27: a minor the rule would not have stopped

This is the strongest example. For the semver rule 0.26 → 0.27 is a minor, so an automatic merge. The 0.27 release notes list only "Patch Changes", but they change how useRole, useDismiss and FloatingFocusManager behave. The store uses useRole in the discount tooltip in the cart. The tests in src/ will not catch this, because the components in web/ have no tests. The package is in 0.x and in dependencies, so Jev gives high with confidence 0.97:

Image loading...Pull request #2 Bump @floating-ui/react from 0.26.28 to 0.27.20: all checks passed and the branch can be merged, but the pipeline comment says Jev rated this update high risk (confidence 0.97) and the update needs a human review

Things to watch

  • The branch name does not confirm the author. The condition on dependabot/ only limits which PRs start the pipeline. Anyone with push access can create a branch with that name. Before you turn this on in a shared repository, add a check at the start of Fetch release notes:

    bash
    AUTHOR=$(gh pr view "$BUDDY_RUN_PR_NO" --json author -q .author.login) [ "$AUTHOR" = "app/dependabot" ] || exit 1 $$
  • mergeable can be UNKNOWN right after a push. GitHub computes it in the background. If the merge fails and the answer is UNKNOWN, the action leaves a comment to merge by hand. Ask again after a few seconds before you treat the answer as final.
  • Code from the PR runs in your project. npm ci installs packages from the Dependabot branch, which is why it uses --ignore-scripts. Only the GitHub CLI action can merge, not the container that runs the checks.
  • Jev will not detect a malicious version. A hijacked package has clean release notes and passes the tests, so it gets none. Malicious versions are usually removed from npm within hours. With auto-merge, extend the cooldown in dependabot.yml, for example default-days: 7. By default Dependabot waits 3 days after a release. Security updates skip the cooldown.
  • Branch protection can block the merge. If main requires a status from Buddy, the run that is still in progress has no green status yet, so GitHub may reject the merge.
  • The policy is half of the result. Without the sentence about 0.x Jev rates the Floating UI minor the same way the semver rule does.
  • Jev does not replace tests. It rates the risk of what the checks do not verify. The weaker the tests, the more should end up in high.

What next

Jarek Dylewski

Jarek Dylewski

Customer Support

A journalist and an SEO specialist trying to find himself in the unforgiving world of coders. Gamer, a non-fiction literature fan and obsessive carnivore. Jarek uses his talents to convert the programming lingo into a cohesive and approachable narration.

Oct 8, 2026
Share