ci: add a scheduled Dependabot auto-merge workflow driven by OpenCode - #2702
Conversation
dyoshikawa
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
…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>
c2a9edd to
20fec46
Compare
|
All 11 CI checks are green on the current head. This PR is deliberately left unmerged. It adds a GitHub Actions workflow Summary of what is in it:
|
|
@dyoshikawa Thank you! |
…to-merge revert: remove the scheduled Dependabot auto-merge workflow (#2702)
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.
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-prskill already encodes that whole procedure, anddraft-release.ymlalready 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 thepull_requestevent: a Dependabot-originated run gets the Dependabot secrets store, soOPENROUTER_API_KEYwould be unavailable, andpull_request_targetwould 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 frommain, regular secrets are available, and the prompt stays fully static — the agent discovers the PRs itself withgh pr list --author "app/dependabot", so no untrusted event data is interpolated, per.github/workflows/AGENTS.md.gh pr listpre-check gates every later step, so an empty run burns no model tokens.draft-release.yml(checkoutfetch-depth: 0,git-config, mise, Takumi Guard npm registry,pnpm install --ignore-scripts,pnpm generate), all SHA-pinned to the same versions.concurrencygroup andtimeout-minutes: 60so overlapping runs cannot push to the same branches.openrouter/deepseek/deepseek-v4-flash-0731— verified present on models.dev under theopenrouterprovider../.github/actions/select-opencode-modelcannot be reused as-is, since it maps thedeepseekkeyword toopenrouter/deepseek/deepseek-v4-pro.contents: write,pull-requests: write,issues: write, with the workflow-level default kept atcontents: read.actionlintruns 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:RepositoryRoleid 5 withbypass_mode: pull_request, so the workflowGITHUB_TOKENis not a bypass actor andgh pr merge --admincannot force a merge from Actions. The prompt now tells the agent to usegh pr merge <number> --mergeand to report rather than retry with--adminif the ruleset refuses — that keeps the sharedbabysit-dependabot-prskill untouched for interactive use.required_approving_review_count: 0and there is noCODEOWNERSfile, sorequire_code_owner_reviewhas no owners to demand. A plain--mergeon 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
github.actorforworkflow_dispatch, mirroringdraft-release.yml, so the step holding the model key and a write-scoped token is reachable only through the schedule (whose definition always comes frommain) or a maintainer's own dispatch..rulesync/rules/github-actions-security.mdnow names this workflow in theanomalyco/opencode/githubpinning stance, since that stance was written arounddraft-release.ymlbeing the only workflow reachinghttps://opencode.ai/install.draft-release.ymlonmain.main.Verification
actionlint1.7.11 (the version CI pins) reports no findings.pnpm cicheckpasses.workflow_dispatchvalidation run has not been performed — it needs the repository secrets and would act on live PRs, so it is left to you.Closes #2686