kadet-hub logokadet-hub

Renovate automerge with required reviews on GitHub

Renovate’s automerge stops at a branch that requires an approving review. Its documentation says so: “If you have mandatory Pull Request reviews then it means Renovate can’t automerge its own PR until such a review has happened” (Renovate docs). The main ruleset of kadet-hub/registry requires one approving review and a code-owner review. Since 2 November 2026 Renovate merges its own minor, patch and digest updates there without either. The Renovate GitHub App has a ruleset bypass that works on pull requests only. GitHub’s auto-merge stays off, so Renovate merges once every check on the pull request has passed, including the checks the ruleset does not require.

The change is kadet-hub/registry#441. The first pull request merged this way was #439, a digest update of the distroless debug image that the gate’s sandbox image copies its busybox from. The GitHub docs quoted below were read on 2 November 2026, the Renovate docs and source at Renovate 44.201.0.

Why an approval bot does not help

Renovate’s docs name two ways past a required review on GitHub. One is a bypass of the review requirement. The other is the helper apps renovate-approve and renovate-approve-2, which “will mark all automerging Pull Requests by Renovate as approved” (Renovate docs).

An approval from an app does not satisfy a code-owner review. With code-owner review required, “any pull request that modifies content with a code owner must be approved by that code owner” (GitHub docs). A CODEOWNERS file lists owners as “GitHub usernames or team names” (GitHub docs). A workflow that approves with its GITHUB_TOKEN approves as an app too, because “The GITHUB_TOKEN secret is a GitHub App installation access token” (GitHub docs). Renovate’s docs put it this way: “having a CODEOWNERS file could block automerging, because it means a code owner must review the PR” (Renovate docs).

kadet-hub/registry has a code owner for .github/, gate/, policy/, scan/, sandbox/ and .clusterfuzzlite/. Every file Renovate updates in that repository sits under one of them, apart from the registry entries. #439 changed sandbox/Dockerfile.

GitHub’s auto-merge is the other half of that setup, and Renovate uses it by default. “Auto-merge merges a pull request automatically after all required reviews and status checks pass” (GitHub docs). It does not wait for a check the branch does not require. The registry’s ruleset requires two checks, gate and renders.

Several checks that exercise a dependency update in this repository come from path-filtered workflows. selftest runs the gate’s self tests when sandbox/, gate/, scan/, policy/ or a few other paths change. sandbox-advisories fails on an advisory that has a fix for the sandbox image. GitHub’s troubleshooting page says that the checks of a workflow skipped by path filtering “stay in a “Pending” state and block merging”, and advises: “Avoid requiring workflows that can be skipped” (GitHub docs). So these checks are not required, and auto-merge would not wait for them.

A bypass for pull requests only

#441 added one entry to the bypass list of the main ruleset. The repository keeps a copy of the ruleset in .github/ruleset.json:

-  "bypass_actors": [{"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"}],
+  "bypass_actors": [
+    {"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"},
+    {"actor_id": 2740, "actor_type": "Integration", "bypass_mode": "pull_request"}
+  ],

Integration is the actor type of a GitHub App. 2740 is the ID that gh api /apps/renovate returns for the Renovate app. The live ruleset carries the same entry:

$ gh api repos/kadet-hub/registry/rulesets/24720369 --jq '.bypass_actors[] | select(.actor_type == "Integration")'
{"actor_id":2740,"actor_type":"Integration","bypass_mode":"pull_request"}

The REST API accepts always, pull_request and exempt for bypass_mode, and its reference says: “pull_request means that an actor can only bypass rules on pull requests” (GitHub REST docs). The default is always. An entry that leaves bypass_mode out lets the app push to main directly.

In the ruleset editor, pull_request is the option “For pull requests only”, which GitHub offers “to grant bypass to an actor without allowing them to push directly to a repository”. The same page goes on:

The selected actor is now required to open a pull request to make changes to a repository, creating a clear trail of their changes in the pull request and audit log. The actor can then choose to bypass any branch protections and merge that pull request.

(GitHub docs)

The bypass covers the rules of the whole ruleset, the required status checks among them. Once the app is on the list, GitHub no longer stops its merge on a failed check. That job moves to Renovate.

What Renovate waits for

The top of renovate.json leaves automerge off by default and switches GitHub’s auto-merge off:

  "automerge": false,
  "platformAutomerge": false,
  "minimumReleaseAge": "3 days",
  "minimumReleaseAgeBehaviour": "timestamp-required",

With platformAutomerge at its default true, Renovate hands the merge to the platform, which merges “after all (if any) branch policies have been met” (Renovate docs). With false, Renovate merges with its own API call. For the default automergeType of pr, Renovate “automerges the PR on a subsequent run once it detects the PR’s status checks are ‘green’”, and “Renovate only automerges branches which are up-to-date and green” (Renovate docs). The entry for ignoreTests states the default: “Renovate’s default behavior is to only automerge if every status check has succeeded” (Renovate docs).

What green means on GitHub is decided in getBranchStatus (Renovate 44.201.0 source). It reads the commit statuses and the check runs of the branch’s head commit. One check run with the conclusion failure makes the branch red. The branch is green only when every check run has the conclusion success, neutral or skipped and the remaining commit statuses are successful. A check run that is queued or still running has no conclusion yet, so the branch stays yellow and Renovate does not merge. Successful statuses that Renovate sets itself, such as renovate/stability-days, are filtered out first, so they cannot turn a branch green on their own. GitHub counts the same three conclusions as a passing required check (GitHub docs).

Renovate sees only the checks that exist on the commit when it runs. A workflow that has not created its check run yet cannot hold the merge.

Which updates merge

The last three packageRules of renovate.json decide what Renovate may merge:

    {
      "description": "REV-5: Renovate merges its own pull request once every check passed",
      "matchUpdateTypes": [
        "minor",
        "patch",
        "digest",
        "pin",
        "pinDigest"
      ],
      "automerge": true
    },
    {
      "description": "REV-5: an entry bump publishes a new version, which a maintainer approves (DEC-3)",
      "matchFileNames": [
        "generators/*.yaml",
        "recipes/*.yaml"
      ],
      "automerge": false
    },
    {
      "description": "REV-5: a digest update of an action under the same version is a moved tag",
      "matchManagers": [
        "github-actions"
      ],
      "matchUpdateTypes": [
        "digest"
      ],
      "automerge": false
    }

The descriptions cite kadet-hub’s specs. REV-5 is the automerge requirement in gate.md, and DEC-3 is the decision in marketplace.md that every new version needs a maintainer’s approval.

Renovate applies packageRules in order, and “later rules may override configuration options from earlier ones, if they both specify the same option” (Renovate docs). The first rule turns automerge on for five update types. A major update matches none of the three and keeps the top-level "automerge": false.

The second rule covers the registry entries. Each file under generators/ and recipes/ pins a generator or a recipe to a tag and commit in its author’s repository. Merging a bump of that pin publishes the new version in the catalog, so these bumps stay with a maintainer whatever their update type.

The third rule is about GitHub Actions. kadet-hub pins every action to a commit SHA with the version as a comment, through the helpers:pinGitHubActionDigests preset. A new release of an action changes the version in that comment. A digest update under the same version means that the tag now points at another commit. That is how tj-actions/changed-files was compromised in March 2025: “Attackers retroactively modified multiple version tags to reference a malicious commit” (GHSA-mrrh-fwg8-r2c3). The three days of minimumReleaseAge do not hold such an update back: “If an existing tag is force-pushed to new commits, the digest update ages against the original release date, so it may pass Minimum Release Age immediately” (Renovate docs).

Everything else still waits three days after its release when the datasource reports a release timestamp. kadet-hub’s config treats git tags and registries other than Docker Hub as reporting none and lets those updates through without one (minimumReleaseAgeBehaviour: timestamp-optional). Such an update merges as soon as its checks pass.

kadet-hub’s threat model in marketplace.md records what this gives up. A malicious minor, patch or image digest release now reaches main without a maintainer once the release age allows it and every check passes. Before #441 it needed one approval by the single maintainer.

kadet-hub/recipes#56 made the same change in kadet-hub/recipes and keeps more updates manual. Lock file maintenance is not on its automerge list, because “the updated packages have no release timestamp for Renovate to check” (Renovate docs). The pin of the next tag of kadet-hub’s Kapitan image stays manual, since that image is rebuilt up to every 15 minutes and its digest updates have no release timestamp. A recipe’s Source line names the chart or image the recipe is built from, and updates to it wait for a reviewer. The main ruleset there requires one approval without a code-owner review and has the same pull_request bypass for the Renovate app.

The first merge

#439 has no review, and GitHub still reports a review as required:

$ gh pr view 439 -R kadet-hub/registry --json mergedBy,mergedAt,reviewDecision,reviews
{"mergedAt":"2026-11-02T10:11:50Z","mergedBy":{"is_bot":true,"login":"app/renovate"},"reviewDecision":"REVIEW_REQUIRED","reviews":[]}

The check runs on its last commit, ordered by completion time in UTC:

$ gh pr view 439 -R kadet-hub/registry --json statusCheckRollup \
    --jq '.statusCheckRollup | map(select(.__typename == "CheckRun")) | sort_by(.completedAt)[] | [.completedAt, .conclusion, .name] | @tsv'
2026-11-02T09:25:20Z	SUCCESS	workflow-lint
2026-11-02T09:25:24Z	SUCCESS	checksum-pins
2026-11-02T09:25:31Z	SUCCESS	sandbox-advisories
2026-11-02T09:25:49Z	SUCCESS	gate
2026-11-02T09:25:49Z	SKIPPED	renders
2026-11-02T09:25:59Z	SUCCESS	comment
2026-11-02T09:26:06Z	SUCCESS	consumer-selftest
2026-11-02T09:26:26Z	SUCCESS	CodeQL
2026-11-02T09:26:31Z	SUCCESS	analyze
2026-11-02T10:10:36Z	SUCCESS	gate-selftest

The same rollup holds one commit status, renovate/stability-days, with the state SUCCESS.

gate and renders are the required checks. renders checks a recipe on kind and runs only when the gate compiled one, so on #439 it was skipped. A skipped job “will not prevent a pull request from merging, even if it is a required check” (GitHub docs). Both required checks were done at 09:25:49. gate-selftest finished at 10:10:36, and Renovate merged at 10:11:50.

GitHub keeps the rule evaluations of the merge as a rule suite, which the REST API returns (GitHub REST docs):

$ gh api repos/kadet-hub/registry/rulesets/rule-suites/4853993537 \
    --jq '{actor_name, ref, result, rules: [.rule_evaluations[] | {rule_type, result}]}'
{"actor_name":"renovate[bot]","ref":"refs/heads/main","result":"bypass","rules":[{"result":"fail","rule_type":"pull_request"},{"result":"pass","rule_type":"required_status_checks"},{"result":"pass","rule_type":"non_fast_forward"},{"result":"pass","rule_type":"deletion"}]}

The pull_request rule failed, and its details name the missing code-owner review. The required status checks passed on their own. The merge went through as bypass.