Skip to content

Add Dependabot Docker update policy with OpenCode risk gate #533

Description

@wax911

Background

local-stack is a Swarm-first infrastructure repository. Automated dependency updates are useful, but Docker image bumps can break a running host even when CI passes. The current pain point is that Dependabot can raise updates that are auto-merged without enough runtime risk context, including major image bumps.

This issue introduces a guarded Dependabot workflow where Docker and GitHub Actions dependency updates are classified before merge. The classifier must use OpenCode via anomalyco/opencode/github@main, produce a deterministic PR comment, and only approve low-risk updates. Major updates and unknown-risk updates must require human review.

The deployment branch for this repository is dev.

Goals

  • Add Dependabot configuration for Docker and GitHub Actions updates targeting dev.
  • Group patch/minor updates separately from major updates.
  • Add an OpenCode-based risk gate for Dependabot PRs.
  • Produce a deterministic PR review comment for every Dependabot PR handled by the gate.
  • Approve low-risk patch/minor updates when validation passes.
  • Request changes or block auto-merge for major, unknown-risk, stateful, or runtime-sensitive updates.
  • Prevent the OpenCode gate from deploying anything.
  • Prevent production/deployment secrets from being exposed to PR workflows.

Non-goals

  • Do not configure Doco-CD in this issue.
  • Do not deploy to the host from GitHub Actions.
  • Do not use Watchtower as the primary update mechanism.
  • Do not allow AI output to directly merge without schema validation and normal required checks.

Required Dependabot configuration

Add .github/dependabot.yml.

Expected baseline:

version: 2

updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "saturday"
      time: "03:00"
      timezone: "Africa/Johannesburg"
    target-branch: "dev"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "docker"
      - "automation"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      docker-patch-minor:
        update-types:
          - "minor"
          - "patch"
      docker-major:
        update-types:
          - "major"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "saturday"
      time: "03:30"
      timezone: "Africa/Johannesburg"
    target-branch: "dev"
    open-pull-requests-limit: 5
    labels:
      - "dependencies"
      - "github-actions"
      - "automation"
    commit-message:
      prefix: "ci"
      include: "scope"
    groups:
      github-actions-patch-minor:
        update-types:
          - "minor"
          - "patch"
      github-actions-major:
        update-types:
          - "major"

Implementation must verify whether Dependabot updates the service-level docker-compose.yml files, generated stacks/*.yml, or both. The source of truth is the service-level Compose files plus swarm.fragment.yml; generated stacks/*.yml must not become the only modified files. If Dependabot modifies generated stack files without the corresponding source change, the PR must be treated as invalid.

Required validation workflow

Add .github/workflows/validate-stack.yml.

This workflow must run on pull_request targeting dev and push to dev.

It must:

  1. Check out the code.
  2. Install Python and tools/requirements.txt.
  3. Run ./stackctl.sh sync.
  4. Run ./stackctl.sh up --dry-run --no-logs.

This workflow must not use production secrets.

Expected baseline:

name: Validate stack

on:
  pull_request:
    branches:
      - dev
  push:
    branches:
      - dev

permissions:
  contents: read

jobs:
  stack:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.12"

      - name: Install render dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install -r tools/requirements.txt

      - name: Check generated stack drift
        run: ./stackctl.sh sync

      - name: Dry-run stack rendering
        run: ./stackctl.sh up --dry-run --no-logs

Required OpenCode gate workflow

Add .github/workflows/dependabot-opencode-gate.yml.

The gate must:

  • Run only for dependabot[bot] PRs.
  • Use pull_request_target only for metadata/comment/review operations.
  • Avoid checking out and executing PR code in pull_request_target.
  • Fetch PR metadata and diff using GitHub API/CLI.
  • Run OpenCode via anomalyco/opencode/github@main.
  • Use Context7 MCP or an equivalent documentation/release-note retrieval path where available.
  • Validate OpenCode output against a strict JSON schema before commenting or applying review decisions.
  • Post or update a deterministic PR comment.
  • Approve low-risk updates.
  • Request changes for high-risk updates.
  • Return neutral/no-auto-merge for unknown or mixed-risk updates.

Required workflow shape:

name: Dependabot OpenCode gate

on:
  pull_request_target:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review

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

jobs:
  gate:
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    runs-on: ubuntu-latest
    steps:
      - name: Fetch Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@<pin-sha>
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Collect PR changed files and patch metadata
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
        run: |
          mkdir -p .review
          gh pr diff "$PR" --repo "$REPO" --name-only > .review/files.txt
          gh pr diff "$PR" --repo "$REPO" > .review/patch.diff
          gh pr view "$PR" --repo "$REPO" --json title,body,author,baseRefName,headRefName,labels > .review/pr.json

      - name: Build deterministic review context
        run: ./tools/ci/build-dependabot-review-context.sh

      - name: Run OpenCode review
        id: opencode
        uses: anomalyco/opencode/github@main
        with:
          # Confirm exact input names against the action before implementation.
          prompt-file: .github/prompts/dependabot-docker-risk-review.md
          context-file: .review/context.json
          output-file: .review/opencode-result.json
          mcp-config-file: .github/opencode/mcp.json

      - name: Validate OpenCode output schema
        run: python tools/ci/validate-opencode-risk-output.py .review/opencode-result.json

      - name: Comment review result
        run: python tools/ci/comment-opencode-risk-review.py .review/opencode-result.json

      - name: Apply PR review decision
        run: python tools/ci/apply-opencode-review-decision.py .review/opencode-result.json

The exact anomalyco/opencode/github@main input names must be verified during implementation. Do not leave this workflow broken with guessed inputs.

Required prompt and output schema

Add:

  • .github/prompts/dependabot-docker-risk-review.md
  • .github/opencode/mcp.json
  • .github/opencode/risk-output.schema.json
  • tools/ci/build-dependabot-review-context.sh
  • tools/ci/validate-opencode-risk-output.py
  • tools/ci/comment-opencode-risk-review.py
  • tools/ci/apply-opencode-review-decision.py

The OpenCode output must validate to this shape:

{
  "schema_version": "local-stack.dependabot-risk.v1",
  "decision": "approve | changes_requested | neutral",
  "risk": "low | medium | high | unknown",
  "automerge_allowed": true,
  "dependency_ecosystem": "docker | github-actions | mixed | unknown",
  "update_type": "semver-patch | semver-minor | semver-major | digest | non-semver | unknown",
  "changed_images": [
    {
      "name": "apache/apisix",
      "from": "3.16.0-debian",
      "to": "3.17.0-debian",
      "risk_reason": "minor update; release notes checked; no breaking config migration found"
    }
  ],
  "changed_files": [
    "apisix/api-gateway/docker-compose.yml"
  ],
  "summary": "One paragraph summary.",
  "breaking_change_assessment": "No breaking changes found in checked release notes.",
  "runtime_impact": "Expected rolling service update only.",
  "required_checks": [
    "./stackctl.sh sync",
    "./stackctl.sh up --dry-run --no-logs"
  ],
  "manual_follow_up": [],
  "sources_checked": [
    "Dependabot metadata",
    "PR diff",
    "Context7 package/release notes"
  ],
  "confidence": "high | medium | low"
}

Decision policy

Approve only when all are true:

  • The PR is from dependabot[bot].
  • The update is patch/minor or digest-only.
  • Validation workflow passes.
  • OpenCode classifies risk as low.
  • Changed files are limited to expected dependency files.
  • No source/generated stack drift is present.
  • No stateful major image migration is detected.

Return neutral/no-auto-merge when:

  • The update is mixed-risk.
  • OpenCode confidence is low.
  • Release-note context is missing but no direct blocker is found.

Request changes when:

  • Any Docker image update is major.
  • Any stateful service image has migration/storage risk.
  • Any gateway/proxy image has breaking config risk.
  • The PR touches .env.example, .env.enc, .sops.yaml, stackctl.sh, tools/render_compose.py, or tools/generate_stacks.py.
  • Generated stacks/*.yml are changed without source Compose/fragment changes.
  • OpenCode cannot classify a runtime-sensitive update.

PR comment format

The comment must use this stable format:

## OpenCode Dependabot Risk Review

Schema: `local-stack.dependabot-risk.v1`

Decision: `APPROVE | CHANGES_REQUESTED | NEUTRAL`
Risk: `LOW | MEDIUM | HIGH | UNKNOWN`
Automerge allowed: `true | false`
Update type: `<type>`
Ecosystem: `<ecosystem>`

### Summary

<one paragraph>

### Changed runtime artifacts

| Image / dependency | From | To | Risk note |
|---|---:|---:|---|
| `apache/apisix` | `3.16.0-debian` | `3.17.0-debian` | Patch/minor update; no breaking config migration found. |

### Runtime impact

<short paragraph>

### Required checks

- [ ] `./stackctl.sh sync`
- [ ] `./stackctl.sh up --dry-run --no-logs`

### Manual follow-up

None or bullet list.

### Sources checked

- Dependabot metadata
- PR diff
- Context7 release/package context

Acceptance criteria

  • .github/dependabot.yml exists and targets dev.
  • Docker and GitHub Actions updates are grouped by patch/minor vs major.
  • Validate stack workflow runs ./stackctl.sh sync and ./stackctl.sh up --dry-run --no-logs.
  • OpenCode gate runs only for Dependabot PRs.
  • OpenCode output is schema-validated before any comment/review action.
  • Low-risk patch/minor PRs can be approved.
  • Major updates are not auto-approved.
  • Generated stack drift is detected and blocks approval.
  • No production deploy secrets are required by the OpenCode gate.
  • A test Dependabot-style major Docker update receives CHANGES_REQUESTED.
  • A test patch/minor Docker update receives APPROVE when validation passes.

Implementation notes

Patch/minor Docker image updates may auto-merge and auto-deploy after all required checks pass. Major updates require manual promotion and must not be auto-approved.

This issue intentionally stops at PR gating. Doco-CD deployment is covered by a separate issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions