Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/AUTOMATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Automation setup — operator notes

This repo runs three lanes of automation, each backed by GitHub Actions
workflows in `.github/workflows/`. This file documents the **one-time
setup** a maintainer must do in the GitHub UI for the automation to work
safely. The workflow files themselves are self-contained; this is the
"flip these switches" companion.

The LLM-backed steps use [**opencode**](https://github.com/anomalyco/opencode)
(headless CLI mode) routed through **SAP AI Core** via opencode's
built-in `sap-ai-core` provider. No proxy process needed — the provider
does XSUAA OAuth2 client-credentials exchange internally. Pattern lifted
from [cap/sherlock](https://github.tools.sap/cap/sherlock).

---

## 1. Secrets

Add these to **Settings → Secrets and variables → Actions**:

| Secret | Used by | Notes |
|-----------------------|--------------------------------------|-------|
| `AICORE_SERVICE_KEY` | dependabot-triage, idea-*, maintenance | The SAP AI Core service key JSON, verbatim. The opencode `sap-ai-core` provider parses this and handles token exchange. |

`GITHUB_TOKEN` is provisioned automatically by Actions; no setup needed.

### Model selection

The default model is `sap-ai-core/anthropic--claude-4.6-opus`, configured
in [`.github/bot-config/opencode.json`](./bot-config/opencode.json). Swap
to `claude-4.6-sonnet`, `gpt-5`, or `gemini-2.5-pro` by editing that file
— no workflow changes needed. Which models are actually available depends
on what's deployed in your AI Core tenant.

## 2. Branch protection on `main`

**Settings → Branches → Branch protection rules → `main`:**

- ✅ Require a pull request before merging
- Require approvals: **1**
- Dismiss stale approvals on new commits: ✅
- Require review from Code Owners: ✅ ← this is what enforces `CODEOWNERS`
- ✅ Require status checks to pass before merging
- Required checks: `Pull Request Voter / build` (the matrix job), plus
`Blackduck Scan` when it's stable for Dependabot PRs
- ✅ Require conversation resolution before merging
- ✅ Allow auto-merge ← required for `auto-merge-deps.yml` to work
- ❌ Do NOT include administrators (optional, your call)

## 3. Actions permissions

**Settings → Actions → General:**

- **Fork pull request workflows from outside collaborators:**
"**Require approval for all outside collaborators**" — gates fork PRs
from triggering any workflow until a maintainer clicks "Approve and run".
- **Workflow permissions:** "Read repository contents and packages
permissions". Individual workflows opt into write permissions in their
yaml.
- **Allow GitHub Actions to create and approve pull requests:** ✅
(required for `idea-implement.yml` to open PRs).

## 4. Labels

Create these labels (Issues → Labels → New label) so the workflows can apply them:

- `needs-review` — Dependabot major bumps; `idea-implement` failures
- `major-update` — Dependabot major bumps (triggers `dependabot-triage`)
- `automation` — anything opened by an LLM-powered workflow
- `idea` — PRs touching `ideas/**`
- `maintenance` — PRs from the weekly sweep
- `upstream-sync` — issues filed by the fork's drift workflow

## 5. CODEOWNERS

`.github/CODEOWNERS` assigns ownership of `/ideas/` and automation files
to `@cap-java/cds-feature-advanced-event-mesh-team`. Code-owner enforcement
only kicks in once branch protection has "Require review from Code Owners"
enabled (see §2), and the team must have write access to the repo.

## 6. Workflow inventory

| Workflow | Trigger | What it does |
|-----------------------------------|--------------------------------------|--------------|
| `auto-merge-deps.yml` | Dependabot PR opened | Auto-approves + enables auto-merge for patch/minor; labels majors `needs-review`. |
| `dependabot-triage.yml` | PR labeled `major-update` | opencode fetches changelog, posts a risk summary as PR comment. |
| `dependabot-stuck-sweep.yml` | Weekly cron (Mon 09:17 UTC) | opencode attempts to fix Dependabot PRs stuck >7 days. |
| `idea-pr-opened.yml` | PR opened touching `ideas/**` | opencode critiques the spec as PR comment. |
| `idea-implement.yml` | Push to `main` touching `ideas/**` | opencode implements `status: ready` ideas, opens PR. |
| `maintenance.yml` | Weekly cron (Mon 10:23 UTC) | README drift, snippet compile, stale TODO, `mvn dependency:analyze`. |
| `upstream-sync.yml` | Daily cron (06:43 UTC), forks only | Rebases fork `main` onto upstream `main`; files an issue on conflict. No-op on the upstream repo. |

The `setup-opencode` composite action in `.github/actions/setup-opencode/`
installs the opencode CLI with a pinned version + SHA256 check. Bump the
`version` / `sha256` defaults in that action's `action.yml` when you want
to upgrade — verify the SHA against the official release before changing.

## 7. Killswitch

To disable a workflow temporarily, **Settings → Actions → Workflows →
[name] → ⋯ → Disable workflow**. The workflow file stays in the repo.

To disable LLM access across the board, revoke `AICORE_SERVICE_KEY` or
delete the AI Core deployment. All LLM-backed workflows will fail loudly
on the next run; nothing destructive happens.

## 8. Cost notes

- `auto-merge-deps`: free (no LLM calls).
- `dependabot-triage`: only fires on major bumps (~once a month). Cheap.
- `dependabot-stuck-sweep`: weekly, only if there are stuck PRs.
- `idea-pr-opened`: bounded by job wall-clock; one critique per spec.
- `idea-implement`: highest-cost workflow. 60-minute job timeout, plus
the diff-size cap (`DIFF_LOC_CAP=1500`) which the agent self-enforces.
- `maintenance`: weekly. Capped at 4 new PRs per run by the prompt.

For a hard monthly cap, set quotas on the AI Core deployment side — it's
the only enforcement point that survives a runaway workflow.
11 changes: 11 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# CODEOWNERS — see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-security/customizing-your-repository/about-code-owners

# Anything under ideas/ requires code-owner review. Combined with branch
# protection "Require review from Code Owners" this means even an approved
# fork PR cannot merge an idea spec without a maintainer's sign-off.
/ideas/ @cap-java/cds-feature-advanced-event-mesh-team

# Automation surfaces — keep tight, since these run with elevated permissions.
/.github/workflows/ @cap-java/cds-feature-advanced-event-mesh-team
/.github/dependabot.yml @cap-java/cds-feature-advanced-event-mesh-team
/.github/CODEOWNERS @cap-java/cds-feature-advanced-event-mesh-team
38 changes: 38 additions & 0 deletions .github/actions/setup-opencode/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Setup opencode
description: |
Installs the opencode CLI at a pinned version, verifies its SHA256, and
puts it on PATH. Used by every automation workflow that needs to run
Claude (via SAP AI Core) headlessly.

Pattern lifted from cap/sherlock — see docs/ARCHITECTURE.md there for
why this is preferred over the upstream opencode GitHub Action (the
upstream action assumes github.com / api.opencode.ai OIDC; we want a
vendor-neutral install that works anywhere).

inputs:
version:
description: opencode release tag (without leading 'v')
required: false
default: "1.14.33"
sha256:
description: SHA256 of opencode-linux-x64.tar.gz for the pinned version
required: false
default: "ab3de3d40a67573419ec74916210cf90dcf8c180dc9d8052db71ac9f55c28810"

runs:
using: composite
steps:
- name: Install opencode
shell: bash
run: |
set -euo pipefail
mkdir -p "$HOME/bin"
curl -fsSL \
"https://github.com/anomalyco/opencode/releases/download/v${{ inputs.version }}/opencode-linux-x64.tar.gz" \
-o /tmp/opencode.tar.gz
echo "${{ inputs.sha256 }} /tmp/opencode.tar.gz" | sha256sum -c -
tar -xz -C "$HOME/bin" opencode < /tmp/opencode.tar.gz
rm /tmp/opencode.tar.gz
chmod +x "$HOME/bin/opencode"
echo "$HOME/bin" >> "$GITHUB_PATH"
"$HOME/bin/opencode" --version
45 changes: 45 additions & 0 deletions .github/bot-config/opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "https://opencode.ai/config.json",

"// model": "Routed through SAP AI Core. Auth is via AICORE_SERVICE_KEY (JSON service key in env). No proxy process required — the sap-ai-core provider does XSUAA OAuth2 client-credentials exchange internally. Swap to claude-4.6-sonnet for faster/cheaper runs.",
"model": "sap-ai-core/anthropic--claude-4.6-opus",

"autoshare": false,

"permission": {
"// rationale": "Deny-by-default for bash and edit. Each workflow's prompt narrates which tools it needs and the workflow yaml sets OPENCODE_CONFIG_EXTRA to grant the specific permissions. webfetch is allowed at base level since most jobs need to read external pages (changelogs, docs).",
"edit": "allow",
"write": "allow",
"webfetch": "allow",
"bash": {
"*": "deny",
"git status": "allow",
"git diff *": "allow",
"git log *": "allow",
"git show *": "allow",
"git rev-parse *": "allow",
"git checkout -b *": "allow",
"git config *": "allow",
"git add *": "allow",
"git commit *": "allow",
"git push *": "allow",
"git rebase *": "allow",
"git merge-base *": "allow",
"ls *": "allow",
"cat *": "allow",
"head *": "allow",
"tail *": "allow",
"wc *": "allow",
"grep *": "allow",
"rg *": "allow",
"find *": "allow",
"mvn *": "allow",
"./mvnw *": "allow",
"gh pr *": "allow",
"gh issue *": "allow",
"gh api *": "allow",
"gh run *": "allow",
"gh label *": "allow"
}
}
}
17 changes: 15 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ updates:
directories:
- "/"
schedule:
interval: daily
interval: weekly
open-pull-requests-limit: 10
# Group patch + minor bumps so weekly noise stays bounded; majors land as
# individual PRs so the auto-merge workflow can hold them for human review.
groups:
maven-minor-and-patch:
update-types:
- "minor"
- "patch"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
interval: weekly
groups:
github-actions-minor-and-patch:
update-types:
- "minor"
- "patch"
50 changes: 50 additions & 0 deletions .github/workflows/auto-merge-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Auto-merge Dependabot PRs

# Auto-approves and enables auto-merge for patch + minor dependency bumps.
# Major updates get a 'needs-review' label and are left for a human.
#
# Merge still requires all branch protection checks (pull-request-build,
# Black Duck via main-build) to pass — auto-merge just clicks the button
# once they're green.

on:
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Fetch Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Enable auto-merge for patch + minor
if: >
steps.meta.outputs.update-type == 'version-update:semver-patch' ||
steps.meta.outputs.update-type == 'version-update:semver-minor'
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Flag major update for human review
if: steps.meta.outputs.update-type == 'version-update:semver-major'
run: |
gh pr edit "$PR_URL" --add-label "needs-review,major-update"
gh pr comment "$PR_URL" --body "🚧 **Major version bump** — left for human review. Auto-merge is disabled for major updates."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75 changes: 75 additions & 0 deletions .github/workflows/dependabot-stuck-sweep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Stuck Dependabot PR sweep

# Weekly cron: find Dependabot PRs open >7 days with failing checks; let
# opencode (via SAP AI Core) attempt a fix per PR. Pushes a commit on
# success, otherwise leaves a comment.

on:
schedule:
- cron: '17 9 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
sweep:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
AICORE_SERVICE_KEY: ${{ secrets.AICORE_SERVICE_KEY }}
OPENCODE_CONFIG: ${{ github.workspace }}/.github/bot-config/opencode.json
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Find stuck Dependabot PRs
id: find
run: |
STUCK=$(gh pr list \
--author "app/dependabot" \
--state open \
--json number,title,headRefName,createdAt,statusCheckRollup \
--jq '[.[] | select(
(.createdAt | fromdateiso8601) < (now - 7*86400) and
(.statusCheckRollup | map(select(.conclusion == "FAILURE")) | length > 0)
)]')
echo "stuck<<EOF" >> "$GITHUB_OUTPUT"
echo "$STUCK" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "count=$(echo "$STUCK" | jq 'length')" >> "$GITHUB_OUTPUT"

- name: Set up opencode
if: steps.find.outputs.count != '0'
uses: ./.github/actions/setup-opencode

- name: Run fix attempts
if: steps.find.outputs.count != '0'
run: |
cat <<EOF > /tmp/prompt.md
The following Dependabot PRs have been open >7 days with failing
checks:

${{ steps.find.outputs.stuck }}

For each PR:

1. \`gh pr checkout <number>\` into a local branch.
2. Look at the failing check logs via
\`gh run view --log-failed <run-id>\`.
3. Decide if it's a fixable mechanical issue (test needs
adjusting for an API change, a sibling dep also needs
bumping, a config tweak). If yes:
- Fix it locally
- Run \`mvn -B verify\` to confirm it passes
- Commit and push to the PR branch
4. If it isn't fixable mechanically, leave a comment on the PR
summarizing what's blocking it in 2-3 sentences.
5. Move to the next PR.

Budget: one fix attempt per PR. Don't loop on the same PR.
EOF
opencode run --print-logs < /tmp/prompt.md
Loading
Loading