Skip to content

ci: add a scheduled Dependabot auto-merge workflow driven by OpenCode - #2702

Merged
dyoshikawa merged 2 commits into
mainfrom
resolve-issue-2686-dependabot-auto-merge
Aug 25, 2026
Merged

ci: add a scheduled Dependabot auto-merge workflow driven by OpenCode#2702
dyoshikawa merged 2 commits into
mainfrom
resolve-issue-2686-dependabot-auto-merge

Conversation

@dyoshikawa

@dyoshikawa dyoshikawa commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Important

Please review and merge this one by hand. It adds a GitHub Actions workflow, which the autonomous batch flow does not auto-merge, and it has one open question (below) that only you can settle.

Background

Dependabot opens grouped bump PRs weekly for npm, GitHub Actions and Docker, and each one is babysat by hand today. The babysit-dependabot-pr skill already encodes that whole procedure, and draft-release.yml already runs OpenCode inside Actions, so this wires the two together.

Changes

Adds .github/workflows/dependabot-auto-merge.yml:

  • schedule (daily, 03:00 UTC) + workflow_dispatch. Deliberately not the pull_request event: a Dependabot-originated run gets the Dependabot secrets store, so OPENROUTER_API_KEY would be unavailable, and pull_request_target would widen the injection surface for event data without reducing the real risk (CI executes the bumped dependencies either way). On a schedule the workflow definition always runs from main, regular secrets are available, and the prompt stays fully static — the agent discovers the PRs itself with gh pr list --author "app/dependabot", so no untrusted event data is interpolated, per .github/workflows/AGENTS.md.
  • A cheap gh pr list pre-check gates every later step, so an empty run burns no model tokens.
  • Setup steps mirror draft-release.yml (checkout fetch-depth: 0, git-config, mise, Takumi Guard npm registry, pnpm install --ignore-scripts, pnpm generate), all SHA-pinned to the same versions.
  • concurrency group and timeout-minutes: 60 so overlapping runs cannot push to the same branches.
  • Model hard-coded to openrouter/deepseek/deepseek-v4-flash-0731 — verified present on models.dev under the openrouter provider. ./.github/actions/select-opencode-model cannot be reused as-is, since it maps the deepseek keyword to openrouter/deepseek/deepseek-v4-pro.
  • Job permissions are contents: write, pull-requests: write, issues: write, with the workflow-level default kept at contents: read.

actionlint runs on this PR (it is path-triggered on .github/workflows/**), which covers step 3 of the issue.

The merge path (issue step 2)

I read the ruleset (repos/dyoshikawa/rulesync/rulesets/6248659) rather than leaving this open:

  • Its only bypass actor is RepositoryRole id 5 with bypass_mode: pull_request, so the workflow GITHUB_TOKEN is not a bypass actor and gh pr merge --admin cannot force a merge from Actions. The prompt now tells the agent to use gh pr merge <number> --merge and to report rather than retry with --admin if the ruleset refuses — that keeps the shared babysit-dependabot-pr skill untouched for interactive use.
  • The ruleset sets required_approving_review_count: 0 and there is no CODEOWNERS file, so require_code_owner_review has no owners to demand. A plain --merge on a green PR should therefore satisfy it, and no PAT or new bypass actor is needed today.

If reviews ever become mandatory on main, this will need either a PAT secret or an Actions bypass actor — worth revisiting then, not now.

Also in this update

  • The job is gated on github.actor for workflow_dispatch, mirroring draft-release.yml, so the step holding the model key and a write-scoped token is reachable only through the schedule (whose definition always comes from main) or a maintainer's own dispatch.
  • .rulesync/rules/github-actions-security.md now names this workflow in the anomalyco/opencode/github pinning stance, since that stance was written around draft-release.yml being the only workflow reaching https://opencode.ai/install.
  • The action pin is bumped to v1.18.19 to match draft-release.yml on main.
  • Rebased onto current main.

Verification

  • actionlint 1.7.11 (the version CI pins) reports no findings.
  • pnpm cicheck passes.
  • A manual workflow_dispatch validation run has not been performed — it needs the repository secrets and would act on live PRs, so it is left to you.

Closes #2686

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I reviewed this with a code-quality pass and a security pass. The trigger choice is right — avoiding pull_request_target and running the definition from main is the correct call, and the static prompt does satisfy the script-injection rules in .github/workflows/AGENTS.md. But there are three things I don't think we can merge past, and they're all about the gap between "the prompt is static" and "what the agent actually reads and can do at runtime".

The first is mechanical: with use_github_token: "true", the agent's pushes are made with GITHUB_TOKEN, and those pushes do not start a new workflow run. The skill's Step 3-2 pushes a fix to the Dependabot branch and Step 3-3 then waits for every check to pass — but no checks will ever appear for the new head SHA. Best case the job burns the 60-minute timeout; worst case the model reads the previous SHA's green checks and merges a commit that CI never ran against. This needs either a GitHub App / fine-grained PAT for the push, or an explicit re-trigger, plus a hard rule in the skill that zero checks on the head SHA means stop.

The second is secret placement. The workflow's own pnpm install --ignore-scripts runs against main, which is fine — but the skill then checks out the PR head, runs a plain pnpm install, and runs pnpm cicheck, which loads and executes the bumped dependency code. That happens in the same job that holds OPENROUTER_API_KEY and a contents: write token. The comment on line 9 says CI executes the bumped dependencies either way, and that isn't equivalent: ci.yml runs with contents: read, no OPENROUTER_API_KEY, and --ignore-scripts. minimumReleaseAge, allowBuilds and Takumi Guard help, but none of them stop code that runs during tests. Splitting the verification into a job without the API key, or leaving verification to the normal CI run after the push, would close this.

The third is indirect prompt injection. The prompt has no ${{ }} in it, but at runtime the agent reads Dependabot PR bodies (which embed upstream release notes written by any package maintainer) and full CI logs (which contain whatever the bumped dependency printed). It reads all of that while holding bash, merge rights, a write token and an LLM API key. The skill tells the agent to treat titles, branch names and comments as untrusted, but it doesn't name CI logs or release notes, and deepseek-v4-flash-0731 is a weak model to be making the merge call. Worth adding those two sources to the untrusted list, trimming logs to the failing lines, and using a stronger model for anything that decides to merge.

Separately, the PR description already notes that gh pr merge --admin will almost certainly be rejected because the default GITHUB_TOKEN isn't a bypass actor. I'd rather settle that before this lands than have a red run at 03:00 UTC every day. There's also no required-status-checks rule on the main ruleset today, so nothing structurally prevents merging a red PR — the skill's own judgement is the only gate. Adding required checks would make this a lot safer regardless of what happens with the token.

The rest of the line comments are smaller and can be folded in with the above.

permissions:
contents: write
pull-requests: write
issues: write

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declaring permissions here makes every unlisted scope none, so the skill's two core steps will 403: gh pr checks needs checks: read and gh run view --job <id> --log needs actions: read. ci-failure-reminder.yml already declares checks: read / statuses: read for the same purpose — worth matching it.

Also, contents: write is broader than this needs. The main ruleset stops a direct push to main, but it doesn't stop pushes or force-pushes to any other branch, and tag creation isn't restricted at all.

on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

draft-release.yml guards its workflow_dispatch with if: github.actor == 'dyoshikawa' || github.actor == 'cm-dyoshikawa'. This workflow starts an agent with a write token and merge rights, so it should have at least the same guard.

issues: write
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

93cb6efe... is v5.0.1. Nine of the ten actions/checkout uses in this repo are already on 8e8c483db84b4bee98b60c0593521ed34d9990e8 (v6.0.1) — only draft-release.yml is still on v5.0.1. Since .github/dependabot.yml ignores actions/checkout, this pin won't be updated automatically, so a new file should start on v6.0.1.

While here: no persist-credentials: false, so the GITHUB_TOKEN stays in .git/config as an http.extraheader and is readable by anything that runs later in the job. That matters given the untrusted dependency code the skill ends up executing.

echo "Open Dependabot pull requests: ${count}"

- name: Configure git
if: steps.check.outputs.count != '0'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

if: steps.check.outputs.count != '0' is repeated on six consecutive steps. Splitting the pre-check into its own job with an output and gating this job with needs: + a single job-level if would express the intent once instead of six times.

The condition is also fail-open: if gh returns an empty string, '' != '0' is true and everything runs anyway. has_prs=true/false or an explicit != '' check would be safer.

env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
count=$(gh pr list --state open --author "app/dependabot" --json number --jq 'length')

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No set -euo pipefail, and gh pr list defaults to --limit 30. The limit is fine in practice (the dependabot.yml open-PR limits add up to 4), but a failing gh call here silently produces an empty count that the downstream gate reads as "not zero".

with:
# Hard-coded rather than resolved through ./.github/actions/select-opencode-model,
# which maps the `deepseek` keyword to `openrouter/deepseek/deepseek-v4-pro`.
model: openrouter/deepseek/deepseek-v4-flash-0731

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The reason for hard-coding is real — select-opencode-model has no entry for deepseek-v4-flash-0731 — but the fix is to add the keyword to that action rather than bypass the one place models are resolved. Otherwise this file gets left behind on the next model migration, and a typo in the model id won't surface until the 03:00 UTC run.

Separately, this agent holds contents: write, rewrites lockfiles and decides what to merge, and it's on a weaker flash model than the glm-5.1 default the other OpenCode workflows use. That trade seems backwards for the workflow with the most authority.


- name: Run OpenCode to babysit Dependabot pull requests
if: steps.check.outputs.count != '0'
uses: anomalyco/opencode/github@31406ccc51b4bd2a4e1e086b2bcaa5f7f804f26d # v1.18.18

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is v1.18.18; #2716 bumps the same action to v1.18.19 in draft-release.yml, which is currently the only reference. Whichever lands second will leave one file a version behind, so it's worth rebasing one onto the other.

jobs:
dependabot-auto-merge:
runs-on: ubuntu-latest
timeout-minutes: 60

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

60 minutes is tight for "process each Dependabot PR one at a time, waiting for CI to go green on each" — a single CI run is already tens of minutes. On timeout you'd be left with a Dependabot branch that has an unmerged, unreported fix pushed to it. Handling one PR per run (gh pr list --limit 1) would make the state transitions much easier to reason about.


- name: Install dependencies
if: steps.check.outputs.count != '0'
run: pnpm install --ignore-scripts

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This installs from main's lockfile, but the skill later switches the working tree to the Dependabot branch and runs pnpm cicheck without reinstalling. That verifies the bumped code against the old node_modules. The skill should re-run the install after checking out the PR head.

cm-dyoshikawa and others added 2 commits August 24, 2026 20:46
…mand

- Gate the job on `github.actor` for `workflow_dispatch`, mirroring
  `draft-release.yml`, so the step holding the model key and a write-scoped
  token is reachable only through the schedule or a maintainer dispatch.
- Tell the agent to merge with `gh pr merge --merge`. The `main` ruleset lists
  only `RepositoryRole` id 5 as a bypass actor, so the workflow GITHUB_TOKEN
  cannot bypass it and the skill default of `--admin` would fail here.
- Bump the `anomalyco/opencode/github` pin to v1.18.19, matching
  `draft-release.yml` on `main`.
- Record the new workflow in the third-party pinning stance rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dyoshikawa
dyoshikawa force-pushed the resolve-issue-2686-dependabot-auto-merge branch from c2a9edd to 20fec46 Compare August 25, 2026 03:56
@dyoshikawa

Copy link
Copy Markdown
Owner Author

All 11 CI checks are green on the current head.

This PR is deliberately left unmerged. It adds a GitHub Actions workflow
(.github/workflows/dependabot-auto-merge.yml), which the autonomous batch flow
classifies as a high-risk change that must not be auto-merged. Please review and
merge it manually.

Summary of what is in it:

  • New dependabot-auto-merge.yml workflow, gated with the same github.actor
    check as draft-release.yml so the job that receives the model key and a
    write-scoped token is reachable only through the schedule (whose definition
    always comes from main) or a maintainer's own dispatch.
  • anomalyco/opencode/github pinned to 2b72179c663cadcb54f54d9f19221b3fb3d11fb6 (v1.18.19).
  • The agent prompt tells the job to merge with gh pr merge <number> --merge,
    not --admin: the main ruleset lists no bypass actor for the Actions app,
    so --admin cannot force a merge from there.
  • .rulesync/rules/github-actions-security.md updated so the supply-chain
    stance names the new workflow alongside draft-release.yml.

@dyoshikawa
dyoshikawa merged commit bf29dec into main Aug 25, 2026
11 checks passed
@dyoshikawa
dyoshikawa deleted the resolve-issue-2686-dependabot-auto-merge branch August 25, 2026 14:20
@dyoshikawa

Copy link
Copy Markdown
Owner Author

@dyoshikawa Thank you!

dyoshikawa added a commit that referenced this pull request Aug 26, 2026
…to-merge

revert: remove the scheduled Dependabot auto-merge workflow (#2702)
rudironsoni pushed a commit to rudironsoni/rulesync that referenced this pull request Aug 27, 2026
Reverts dyoshikawa#2702 (merge commit bf29dec) at the maintainer's request. Removes .github/workflows/dependabot-auto-merge.yml and restores the github-actions-security rule to describing draft-release.yml as the only workflow that reaches https://opencode.ai/install.
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.

Auto-fix and merge Dependabot PRs with the OpenCode GitHub agent

2 participants