Auto-Merge Renovate PRs #277

Closed
opened 2026-05-27 05:31:45 +00:00 by guettli · 4 comments
guettli commented 2026-05-27 05:31:45 +00:00 (Migrated from codeberg.org)

We use Renovate, which creates PRs like this:

https://codeberg.org/guettli/sharedinbox/pulls/275

Update plan.

do automerge via ci.yaml.

But Renovate should only do updates which are considered "safe".

We use Renovate, which creates PRs like this: https://codeberg.org/guettli/sharedinbox/pulls/275 Update plan. do automerge via ci.yaml. But Renovate should only do updates which are considered "safe".
guettlibot commented 2026-05-27 06:19:53 +00:00 (Migrated from codeberg.org)

Implementation Plan: Auto-Merge Renovate PRs

Background

Renovate PRs (e.g. #275) are currently created by guettlibot with branches named renovate/.... Automerge is shown as "Disabled by config" in PR descriptions because renovate.json does not set automerge: true.

The codebase already has sophisticated merge tooling (scripts/agent_loop.py uses fgj pr merge), so the plumbing exists — it just needs to be wired up for Renovate PRs.


Recommended Approach: Add an automerge job to ci.yml

Add a new dependent job at the end of .forgejo/workflows/ci.yml that runs only when the existing CI job succeeds and the PR originates from Renovate.

Why this over alternatives:

  • Zero delay — merges immediately when CI goes green, no polling wait
  • The CI workflow already knows all checks passed (via needs:)
  • No new workflow file needed; no cron/schedule involved
  • More reliable than Renovate's built-in automerge polling on Forgejo

Step 1 — Modify .forgejo/workflows/ci.yml

Add a job like this after the existing CI job (replace ci with the actual job name in the file):

  automerge-renovate:
    needs: [ci]                        # wait for the main CI job
    if: >
      github.event_name == 'pull_request' &&
      startsWith(github.head_ref, 'renovate/')
    runs-on: ubuntu-latest
    steps:
      - name: Merge Renovate PR
        env:
          FORGEJO_TOKEN: ${{ secrets.RENOVATE_FORGEJO_TOKEN }}
        run: |
          fgj pr merge ${{ github.event.pull_request.number }} \
            --repo ${{ github.repository }} \
            --squash --yes

The if: condition ensures this job only runs for Renovate branches, not normal feature branches or pushes to main.

Step 2 (optional fallback) — Enable Renovate's native automerge in renovate.json

{
  "extends": ["config:recommended"],
  "labels": ["dependencies"],
  "automerge": true,
  "automergeType": "pr",
  "automergeStrategy": "squash",
  "github-actions": {
    "fileMatch": ["^\\.forgejo/workflows/[^/]+\\.ya?ml$"]
  }
}

This provides a fallback: if the workflow-based merge ever fails, Renovate will attempt the merge itself during its next daily run (06:00 UTC).


Files to Change

File Change
.forgejo/workflows/ci.yml Add automerge-renovate job (primary implementation)
renovate.json Add automerge: true + strategy (optional fallback)

Risks & Open Questions

  1. Token permissions — The job needs a token with merge rights. RENOVATE_FORGEJO_TOKEN (already used in renovate.yml) is the natural candidate since it belongs to guettlibot. Confirm it has the write:repository (or equivalent) scope to merge PRs.

  2. fgj availability on runner — The automerge-renovate job needs fgj installed. The self-hosted runner Dockerfile installs task and dagger but fgj may not be present. If not, either install it in the step or use the Forgejo REST API directly (curl -X POST .../pulls/{id}/merge).

  3. CI path filtersci.yml currently only triggers on changes to lib/, test/, android/, linux/, etc. A Renovate PR that only touches pubspec.yaml or android/app/build.gradle may or may not match those filters depending on exact paths. Verify that ci.yml actually runs for the kinds of PRs Renovate creates (e.g. #275 updates desugar_jdk_libs in an Android Gradle file).

  4. Major version bumps — Should major dependency upgrades be auto-merged too, or only patch and minor? Renovate supports automerge scoped to update types via packageRules. If you want to be conservative, restrict automerge to patch/minor only:

    "packageRules": [
      { "matchUpdateTypes": ["patch", "minor"], "automerge": true },
      { "matchUpdateTypes": ["major"], "automerge": false }
    ]
    

    The workflow-based approach should mirror the same logic (e.g. via PR title prefix fix(deps) vs feat(deps)!).

  5. Minimum stabilisation delay — Renovate supports a minimumReleaseAge setting (e.g. "3 days") so brand-new versions aren't merged before potential security advisories surface. Worth considering for production dependencies.

  6. Branch protection — If main has branch protection rules requiring a minimum number of approvals, the merge will fail unless the token is a repository admin or the rule exempts bots. Check repo settings.


Summary

The cleanest path is a single new job in ci.yml plus optionally enabling automerge in renovate.json as a fallback. The main prerequisite to confirm before coding is whether RENOVATE_FORGEJO_TOKEN has merge permissions and whether fgj is available on the CI runner.

## Implementation Plan: Auto-Merge Renovate PRs ### Background Renovate PRs (e.g. #275) are currently created by `guettlibot` with branches named `renovate/...`. Automerge is shown as **"Disabled by config"** in PR descriptions because `renovate.json` does not set `automerge: true`. The codebase already has sophisticated merge tooling (`scripts/agent_loop.py` uses `fgj pr merge`), so the plumbing exists — it just needs to be wired up for Renovate PRs. --- ### Recommended Approach: Add an `automerge` job to `ci.yml` Add a new dependent job at the end of `.forgejo/workflows/ci.yml` that runs only when the existing CI job succeeds and the PR originates from Renovate. **Why this over alternatives:** - Zero delay — merges immediately when CI goes green, no polling wait - The CI workflow already knows all checks passed (via `needs:`) - No new workflow file needed; no cron/schedule involved - More reliable than Renovate's built-in automerge polling on Forgejo #### Step 1 — Modify `.forgejo/workflows/ci.yml` Add a job like this after the existing CI job (replace `ci` with the actual job name in the file): ```yaml automerge-renovate: needs: [ci] # wait for the main CI job if: > github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/') runs-on: ubuntu-latest steps: - name: Merge Renovate PR env: FORGEJO_TOKEN: ${{ secrets.RENOVATE_FORGEJO_TOKEN }} run: | fgj pr merge ${{ github.event.pull_request.number }} \ --repo ${{ github.repository }} \ --squash --yes ``` The `if:` condition ensures this job only runs for Renovate branches, not normal feature branches or pushes to `main`. #### Step 2 (optional fallback) — Enable Renovate's native automerge in `renovate.json` ```json { "extends": ["config:recommended"], "labels": ["dependencies"], "automerge": true, "automergeType": "pr", "automergeStrategy": "squash", "github-actions": { "fileMatch": ["^\\.forgejo/workflows/[^/]+\\.ya?ml$"] } } ``` This provides a fallback: if the workflow-based merge ever fails, Renovate will attempt the merge itself during its next daily run (06:00 UTC). --- ### Files to Change | File | Change | |---|---| | `.forgejo/workflows/ci.yml` | Add `automerge-renovate` job (primary implementation) | | `renovate.json` | Add `automerge: true` + strategy (optional fallback) | --- ### Risks & Open Questions 1. **Token permissions** — The job needs a token with merge rights. `RENOVATE_FORGEJO_TOKEN` (already used in `renovate.yml`) is the natural candidate since it belongs to `guettlibot`. Confirm it has the `write:repository` (or equivalent) scope to merge PRs. 2. **`fgj` availability on runner** — The `automerge-renovate` job needs `fgj` installed. The self-hosted runner Dockerfile installs `task` and `dagger` but `fgj` may not be present. If not, either install it in the step or use the Forgejo REST API directly (`curl -X POST .../pulls/{id}/merge`). 3. **CI path filters** — `ci.yml` currently only triggers on changes to `lib/`, `test/`, `android/`, `linux/`, etc. A Renovate PR that only touches `pubspec.yaml` or `android/app/build.gradle` may or may not match those filters depending on exact paths. Verify that `ci.yml` actually runs for the kinds of PRs Renovate creates (e.g. #275 updates `desugar_jdk_libs` in an Android Gradle file). 4. **Major version bumps** — Should major dependency upgrades be auto-merged too, or only `patch` and `minor`? Renovate supports `automerge` scoped to update types via `packageRules`. If you want to be conservative, restrict automerge to patch/minor only: ```json "packageRules": [ { "matchUpdateTypes": ["patch", "minor"], "automerge": true }, { "matchUpdateTypes": ["major"], "automerge": false } ] ``` The workflow-based approach should mirror the same logic (e.g. via PR title prefix `fix(deps)` vs `feat(deps)!`). 5. **Minimum stabilisation delay** — Renovate supports a `minimumReleaseAge` setting (e.g. `"3 days"`) so brand-new versions aren't merged before potential security advisories surface. Worth considering for production dependencies. 6. **Branch protection** — If `main` has branch protection rules requiring a minimum number of approvals, the merge will fail unless the token is a repository admin or the rule exempts bots. Check repo settings. --- ### Summary The cleanest path is **a single new job in `ci.yml`** plus optionally enabling automerge in `renovate.json` as a fallback. The main prerequisite to confirm before coding is whether `RENOVATE_FORGEJO_TOKEN` has merge permissions and whether `fgj` is available on the CI runner.
guettlibot commented 2026-05-27 06:20:02 +00:00 (Migrated from codeberg.org)

Planning complete. To resume this session:

claude --resume 4477d0e8-26c3-4f1a-b5a8-680383a2c230
Planning complete. To resume this session: ``` claude --resume 4477d0e8-26c3-4f1a-b5a8-680383a2c230 ```
guettlibot commented 2026-05-27 07:05:21 +00:00 (Migrated from codeberg.org)

Implementation Plan: Auto-Merge Renovate PRs

Overview

The goal is to automatically merge Renovate PRs that are considered "safe" once CI passes. The approach uses two layers:

  1. renovate.json — classify which update types are safe via packageRules and add an automerge label to those PRs.
  2. .forgejo/workflows/ci.yml — add a new automerge job that runs after the check job succeeds and merges the PR when it has the automerge label and was opened by Renovate.

What counts as "safe"?

For this Flutter/Dart project, safe means low-probability-of-breaking-changes:

Update type Safe? Rationale
patch (1.2.3 → 1.2.4) Semver: no API changes
minor for dev_dependencies Test/lint tools; no app-runtime impact
minor for app dependencies ⚠️ Could change public API; treat as manual
major Breaking changes by definition
lockFileMaintenance Lock-file-only; no version change
pin / digest (CI action SHA pins) Security hardening only
Flutter SDK / Dart SDK Too impactful; always manual

File 1: renovate.json

Add a packageRules section to label safe PRs with automerge. Renovate's own automerge flag is intentionally not set — the merge is delegated to CI so that CI is the single gate.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "labels": ["dependencies"],
  "github-actions": {
    "fileMatch": ["^\\.forgejo/workflows/[^/]+\\.ya?ml$"]
  },
  "packageRules": [
    {
      "description": "Safe: patch updates for all packages",
      "matchUpdateTypes": ["patch", "pin", "digest", "lockFileMaintenance"],
      "addLabels": ["automerge"]
    },
    {
      "description": "Safe: minor updates for dev_dependencies only",
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["minor"],
      "addLabels": ["automerge"]
    },
    {
      "description": "Never automerge Flutter or Dart SDK",
      "matchPackageNames": ["flutter", "dart"],
      "addLabels": []
    }
  ]
}

The addLabels field is additive; the base "labels": ["dependencies"] already applies to all PRs.


File 2: .forgejo/workflows/ci.yml

Add a new automerge job at the end of the file. It:

  • only runs on pull_request events (not pushes to main)
  • requires the check job to have succeeded
  • only proceeds if the PR actor is renovate[bot] and the PR carries the automerge label
  • calls the Forgejo API to merge (squash or merge-commit — squash keeps history clean)
  automerge:
    name: Auto-merge Renovate PR
    runs-on: ubuntu-latest
    needs: check
    if: >
      github.event_name == 'pull_request' &&
      github.actor == 'renovate[bot]'
    steps:
      - name: Check automerge label and merge
        env:
          FORGEJO_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          # Abort if the automerge label is not present
          LABELS=$(curl -sf             -H "Authorization: token $FORGEJO_TOKEN"             "https://codeberg.org/api/v1/repos/guettli/sharedinbox/issues/$PR_NUMBER/labels"             | python3 -c "import json,sys; print(' '.join(l['name'] for l in json.load(sys.stdin)))")
          echo "PR labels: $LABELS"
          if ! echo "$LABELS" | grep -qw automerge; then
            echo "No 'automerge' label — skipping."
            exit 0
          fi
          # Squash-merge
          curl -sf -X POST             -H "Authorization: token $FORGEJO_TOKEN"             -H "Content-Type: application/json"             -d '{"Do":"squash","merge_message_field":"Auto-merged by CI"}'             "https://codeberg.org/api/v1/repos/guettli/sharedinbox/pulls/$PR_NUMBER/merge"
          echo "Merged PR #$PR_NUMBER"

Why a dedicated AUTOMERGE_TOKEN secret? The existing RENOVATE_FORGEJO_TOKEN is scoped to Renovate's own operations. The merge step needs a token with repository write (merge PRs) permission. Using a separate token limits blast radius.


Required secret

Secret name Required permissions
AUTOMERGE_TOKEN Forgejo user token with repository write (merge) on guettli/sharedinbox

This can be the same Forgejo account as RENOVATE_FORGEJO_TOKEN but stored as a separate secret, or a dedicated bot account.


How the flow works end-to-end

  1. Renovate (daily at 06:00 UTC) opens a PR, e.g. "Update dependency foo to v1.2.4".
  2. Renovate evaluates packageRules → adds the automerge label to the PR if it matches a safe rule.
  3. CI runs on the PR (pull_request event), executes task check-dagger.
  4. If check passes, the automerge job runs, detects the automerge label, and squash-merges the PR.
  5. If check fails, automerge is skipped (needs: check prevents it from running).

Risks and open questions

  1. Does github.actor equal 'renovate[bot]' on Codeberg/Forgejo?
    On GitHub this is renovate[bot]; on Forgejo it may be renovate (without brackets). Check the actor name in an existing Renovate PR's workflow run logs before finalising the if: condition. Worst case, use contains(github.actor, 'renovate').

  2. Label sync timing
    The automerge label must be set before CI starts. Renovate sets labels when it creates or updates the PR, so the label should be present by the time the CI pull_request event fires. However, if Renovate fails to set labels (e.g., API timeout), CI will safely skip the merge.

  3. Minor app-dependency updates deliberately excluded
    The plan excludes minor bumps for runtime dependencies (only patch is safe). If experience shows that minor updates rarely break things in practice, the rule can be relaxed later.

  4. Squash vs. merge commit
    Squash keeps the main branch history clean (one commit per Renovate PR). If a merge commit is preferred, change "Do":"squash" to "Do":"merge".

  5. ci.yml path filter
    Currently ci.yml only triggers on PRs that touch pubspec.yaml, pubspec.lock, lib/**, etc. Renovate PRs that only update pubspec.yaml/pubspec.lock will match the path filter — but pure .forgejo/workflows/ action-version updates will also match because .forgejo/workflows/ci.yml is in the path list. This seems intentional and correct.

  6. Re-entrancy
    If the merged Renovate commit triggers the push CI run on main, that is a normal post-merge check and is not a problem.

## Implementation Plan: Auto-Merge Renovate PRs ### Overview The goal is to automatically merge Renovate PRs that are considered "safe" once CI passes. The approach uses two layers: 1. **`renovate.json`** — classify which update types are safe via `packageRules` and add an `automerge` label to those PRs. 2. **`.forgejo/workflows/ci.yml`** — add a new `automerge` job that runs after the `check` job succeeds and merges the PR when it has the `automerge` label and was opened by Renovate. --- ### What counts as "safe"? For this Flutter/Dart project, safe means low-probability-of-breaking-changes: | Update type | Safe? | Rationale | |---|---|---| | `patch` (1.2.3 → 1.2.4) | ✅ | Semver: no API changes | | `minor` for `dev_dependencies` | ✅ | Test/lint tools; no app-runtime impact | | `minor` for app `dependencies` | ⚠️ | Could change public API; treat as manual | | `major` | ❌ | Breaking changes by definition | | `lockFileMaintenance` | ✅ | Lock-file-only; no version change | | `pin` / `digest` (CI action SHA pins) | ✅ | Security hardening only | | Flutter SDK / Dart SDK | ❌ | Too impactful; always manual | --- ### File 1: `renovate.json` Add a `packageRules` section to label safe PRs with `automerge`. Renovate's own `automerge` flag is intentionally **not** set — the merge is delegated to CI so that CI is the single gate. ```json { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "labels": ["dependencies"], "github-actions": { "fileMatch": ["^\\.forgejo/workflows/[^/]+\\.ya?ml$"] }, "packageRules": [ { "description": "Safe: patch updates for all packages", "matchUpdateTypes": ["patch", "pin", "digest", "lockFileMaintenance"], "addLabels": ["automerge"] }, { "description": "Safe: minor updates for dev_dependencies only", "matchDepTypes": ["devDependencies"], "matchUpdateTypes": ["minor"], "addLabels": ["automerge"] }, { "description": "Never automerge Flutter or Dart SDK", "matchPackageNames": ["flutter", "dart"], "addLabels": [] } ] } ``` The `addLabels` field is additive; the base `"labels": ["dependencies"]` already applies to all PRs. --- ### File 2: `.forgejo/workflows/ci.yml` Add a new `automerge` job at the end of the file. It: - only runs on `pull_request` events (not pushes to main) - requires the `check` job to have succeeded - only proceeds if the PR actor is `renovate[bot]` **and** the PR carries the `automerge` label - calls the Forgejo API to merge (squash or merge-commit — squash keeps history clean) ```yaml automerge: name: Auto-merge Renovate PR runs-on: ubuntu-latest needs: check if: > github.event_name == 'pull_request' && github.actor == 'renovate[bot]' steps: - name: Check automerge label and merge env: FORGEJO_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | # Abort if the automerge label is not present LABELS=$(curl -sf -H "Authorization: token $FORGEJO_TOKEN" "https://codeberg.org/api/v1/repos/guettli/sharedinbox/issues/$PR_NUMBER/labels" | python3 -c "import json,sys; print(' '.join(l['name'] for l in json.load(sys.stdin)))") echo "PR labels: $LABELS" if ! echo "$LABELS" | grep -qw automerge; then echo "No 'automerge' label — skipping." exit 0 fi # Squash-merge curl -sf -X POST -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" -d '{"Do":"squash","merge_message_field":"Auto-merged by CI"}' "https://codeberg.org/api/v1/repos/guettli/sharedinbox/pulls/$PR_NUMBER/merge" echo "Merged PR #$PR_NUMBER" ``` **Why a dedicated `AUTOMERGE_TOKEN` secret?** The existing `RENOVATE_FORGEJO_TOKEN` is scoped to Renovate's own operations. The merge step needs a token with `repository write` (merge PRs) permission. Using a separate token limits blast radius. --- ### Required secret | Secret name | Required permissions | |---|---| | `AUTOMERGE_TOKEN` | Forgejo user token with `repository write` (merge) on `guettli/sharedinbox` | This can be the same Forgejo account as `RENOVATE_FORGEJO_TOKEN` but stored as a separate secret, or a dedicated bot account. --- ### How the flow works end-to-end 1. Renovate (daily at 06:00 UTC) opens a PR, e.g. "Update dependency foo to v1.2.4". 2. Renovate evaluates `packageRules` → adds the `automerge` label to the PR if it matches a safe rule. 3. CI runs on the PR (`pull_request` event), executes `task check-dagger`. 4. If `check` passes, the `automerge` job runs, detects the `automerge` label, and squash-merges the PR. 5. If `check` fails, `automerge` is skipped (`needs: check` prevents it from running). --- ### Risks and open questions 1. **Does `github.actor` equal `'renovate[bot]'` on Codeberg/Forgejo?** On GitHub this is `renovate[bot]`; on Forgejo it may be `renovate` (without brackets). Check the actor name in an existing Renovate PR's workflow run logs before finalising the `if:` condition. Worst case, use `contains(github.actor, 'renovate')`. 2. **Label sync timing** The `automerge` label must be set *before* CI starts. Renovate sets labels when it creates or updates the PR, so the label should be present by the time the CI `pull_request` event fires. However, if Renovate fails to set labels (e.g., API timeout), CI will safely skip the merge. 3. **Minor app-dependency updates deliberately excluded** The plan excludes minor bumps for runtime `dependencies` (only patch is safe). If experience shows that minor updates rarely break things in practice, the rule can be relaxed later. 4. **Squash vs. merge commit** Squash keeps the main branch history clean (one commit per Renovate PR). If a merge commit is preferred, change `"Do":"squash"` to `"Do":"merge"`. 5. **`ci.yml` path filter** Currently `ci.yml` only triggers on PRs that touch `pubspec.yaml`, `pubspec.lock`, `lib/**`, etc. Renovate PRs that only update `pubspec.yaml`/`pubspec.lock` **will** match the path filter — but pure `.forgejo/workflows/` action-version updates will also match because `.forgejo/workflows/ci.yml` is in the path list. This seems intentional and correct. 6. **Re-entrancy** If the merged Renovate commit triggers the `push` CI run on `main`, that is a normal post-merge check and is not a problem.
guettlibot commented 2026-05-27 07:06:02 +00:00 (Migrated from codeberg.org)

Planning complete. To resume this session:

claude --resume a93d066e-ed04-4882-a8cc-ad819790918f
Planning complete. To resume this session: ``` claude --resume a93d066e-ed04-4882-a8cc-ad819790918f ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#277