From 5844d0c0007380f4ff633f00524596ceedc61687 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 29 Jun 2026 08:33:01 +0200 Subject: [PATCH 1/4] feat(automation): scaffold autonomous deps + idea-to-PR pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three automation lanes, all opt-in via .github/AUTOMATION.md: Lane 1 — dependency hygiene - Group dependabot patch+minor into weekly PRs (keep majors separate) - auto-merge-deps.yml: approve + enable auto-merge for non-major bumps; label majors needs-review - dependabot-triage.yml: Claude summarises changelog risk on major bumps - dependabot-stuck-sweep.yml: weekly cron, Claude attempts fixes on Dependabot PRs stuck >7 days Lane 2 — idea-to-PR - ideas/ directory with README convention + example spec - idea-pr-opened.yml: Claude critiques spec PRs - idea-implement.yml: on push-to-main of a 'status: ready' idea, Claude implements on a branch, runs mvn verify, opens a PR for human review (never auto-merges) Lane 3 — maintenance - Weekly cron: README drift, snippet check, stale TODOs, dependency:analyze. Findings filed as draft idea PRs, feeding back into Lane 2. Hardening - .github/CODEOWNERS gates ideas/ and workflow changes - .github/AUTOMATION.md documents required GitHub UI toggles (branch protection, fork-PR approval gate, code-owner reviews, ANTHROPIC_API_KEY secret) --- .github/AUTOMATION.md | 98 ++++++++++ .github/CODEOWNERS | 13 ++ .github/dependabot.yml | 17 +- .github/workflows/auto-merge-deps.yml | 50 +++++ .github/workflows/dependabot-stuck-sweep.yml | 77 ++++++++ .github/workflows/dependabot-triage.yml | 73 +++++++ .github/workflows/idea-implement.yml | 193 +++++++++++++++++++ .github/workflows/idea-pr-opened.yml | 97 ++++++++++ .github/workflows/maintenance.yml | 111 +++++++++++ ideas/README.md | 75 +++++++ ideas/_example.md | 60 ++++++ 11 files changed, 862 insertions(+), 2 deletions(-) create mode 100644 .github/AUTOMATION.md create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/auto-merge-deps.yml create mode 100644 .github/workflows/dependabot-stuck-sweep.yml create mode 100644 .github/workflows/dependabot-triage.yml create mode 100644 .github/workflows/idea-implement.yml create mode 100644 .github/workflows/idea-pr-opened.yml create mode 100644 .github/workflows/maintenance.yml create mode 100644 ideas/README.md create mode 100644 ideas/_example.md diff --git a/.github/AUTOMATION.md b/.github/AUTOMATION.md new file mode 100644 index 0000000..450f75f --- /dev/null +++ b/.github/AUTOMATION.md @@ -0,0 +1,98 @@ +# 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. + +--- + +## 1. Secrets + +Add these to **Settings → Secrets and variables → Actions**: + +| Secret | Used by | Notes | +|-----------------------|--------------------------------------|-------| +| `ANTHROPIC_API_KEY` | dependabot-triage, idea-*, maintenance | Claude API key. Bedrock/Vertex creds work too — see the [Claude Code Action docs](https://github.com/anthropics/claude-code-action). | + +`GITHUB_TOKEN` is provisioned automatically by Actions; no setup needed. + +## 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 a Claude-powered workflow +- `idea` — PRs touching `ideas/**` + +## 5. CODEOWNERS + +`.github/CODEOWNERS` is committed but uses `@cap-java/maintainers` as a +placeholder. Before merging this scaffolding, replace it with the real +team handle or a list of user handles. Code-owner enforcement only kicks +in once branch protection has "Require review from Code Owners" enabled +(see §2). + +## 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` | Claude fetches changelog, posts a risk summary as PR comment. | +| `dependabot-stuck-sweep.yml` | Weekly cron (Mon 09:17 UTC) | Claude attempts to fix Dependabot PRs stuck >7 days. | +| `idea-pr-opened.yml` | PR opened touching `ideas/**` | Claude critiques the spec as PR comment. | +| `idea-implement.yml` | Push to `main` touching `ideas/**` | Claude implements `status: ready` ideas, opens PR. | +| `maintenance.yml` | Weekly cron (Mon 10:23 UTC) | README drift, snippet compile, stale TODO, `mvn dependency:analyze`. | + +## 7. Killswitch + +To disable a workflow temporarily, **Settings → Actions → Workflows → +[name] → ⋯ → Disable workflow**. The workflow file stays in the repo. + +To disable Claude across the board, revoke `ANTHROPIC_API_KEY`. All Claude +workflows will fail loudly on next run; nothing destructive happens. + +## 8. Cost notes + +- `auto-merge-deps`: free (no Claude calls). +- `dependabot-triage`: only fires on major bumps (~once a month). Cheap. +- `dependabot-stuck-sweep`: weekly, only if there are stuck PRs. Bounded + by `--max-turns 200` across all stuck PRs combined. +- `idea-pr-opened`: bounded critique (~25 turns). +- `idea-implement`: highest-cost workflow. Bounded by `--max-turns 300` + and a 60-minute job timeout. Diff-size cap stops runaway PRs. +- `maintenance`: weekly. Bounded by `--max-turns 100`. + +If you want a hard monthly cap, set it on the Anthropic console (Usage +limits) rather than in workflows — it's the only place that enforces it +across all runs. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0a08a9d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# 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. +# +# Replace the handles below with the actual maintainer set before enabling. +/ideas/ @cap-java/maintainers + +# Automation surfaces — keep tight, since these run with elevated permissions. +/.github/workflows/ @cap-java/maintainers +/.github/dependabot.yml @cap-java/maintainers +/.github/CODEOWNERS @cap-java/maintainers diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eea7bf5..ea9a355 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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" diff --git a/.github/workflows/auto-merge-deps.yml b/.github/workflows/auto-merge-deps.yml new file mode 100644 index 0000000..0a25441 --- /dev/null +++ b/.github/workflows/auto-merge-deps.yml @@ -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 }} diff --git a/.github/workflows/dependabot-stuck-sweep.yml b/.github/workflows/dependabot-stuck-sweep.yml new file mode 100644 index 0000000..bdd3c45 --- /dev/null +++ b/.github/workflows/dependabot-stuck-sweep.yml @@ -0,0 +1,77 @@ +name: Stuck Dependabot PR sweep + +# Once a week, find Dependabot PRs that have been open >7 days with failing +# checks. For each, run Claude headless to attempt a fix — typically a rebase, +# a test adjustment, or a follow-up bump. If Claude can fix it, it pushes a +# commit; if not, it leaves a comment explaining what's blocking the PR. + +on: + schedule: + # Mondays 09:17 UTC — off the :00 stampede + - cron: '17 9 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + sweep: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Find stuck Dependabot PRs + id: find + run: | + # PRs older than 7 days, by dependabot, failing checks + 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<> "$GITHUB_OUTPUT" + echo "$STUCK" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "count=$(echo "$STUCK" | jq 'length')" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Claude to attempt fixes + if: steps.find.outputs.count != '0' + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + The following Dependabot PRs have been open >7 days with failing + checks: + + ${{ steps.find.outputs.stuck }} + + For each PR: + + 1. `gh pr checkout ` into a local branch. + 2. Look at the failing check logs via + `gh run view --log-failed `. + 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. + claude_args: | + --allowed-tools "Bash,Read,Write,Edit,Glob,Grep" + --max-turns 200 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dependabot-triage.yml b/.github/workflows/dependabot-triage.yml new file mode 100644 index 0000000..ca20e09 --- /dev/null +++ b/.github/workflows/dependabot-triage.yml @@ -0,0 +1,73 @@ +name: Dependabot changelog triage + +# For major Dependabot PRs (which auto-merge-deps.yml flags as 'needs-review'), +# this job uses Claude Code to fetch the upstream changelog, scan for breaking +# changes, and post a one-paragraph risk summary as a PR comment. +# +# This does NOT auto-merge — it only enriches the PR with context so the +# human reviewer can decide quickly. + +on: + pull_request_target: + types: [labeled] + +permissions: + contents: read + +jobs: + triage: + if: > + github.actor == 'dependabot[bot]' && + github.event.label.name == 'major-update' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout PR + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch Dependabot metadata + id: meta + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Claude changelog triage + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + A Dependabot PR is proposing a major version bump: + + dependency: ${{ steps.meta.outputs.dependency-names }} + from: ${{ steps.meta.outputs.previous-version }} + to: ${{ steps.meta.outputs.new-version }} + ecosystem: ${{ steps.meta.outputs.package-ecosystem }} + + Steps: + 1. Use WebFetch / WebSearch to find the upstream changelog or + release notes for this version range. + 2. Identify breaking changes that could affect this repo. + Cross-check against actual usages in the codebase. + 3. Classify risk as LOW / MEDIUM / HIGH and explain in 2-3 + sentences. Be concrete — name the API or behavior change, + don't just say 'there are breaking changes'. + 4. Post the result as a PR comment using `gh pr comment`. + Format: + + ## 🤖 Changelog triage — risk: + + <2-3 sentence summary> + + + + PR URL: ${{ github.event.pull_request.html_url }} + claude_args: | + --allowed-tools "Bash(gh pr comment:*),Bash(gh pr view:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Read,Glob,Grep,WebFetch,WebSearch" + --max-turns 25 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/idea-implement.yml b/.github/workflows/idea-implement.yml new file mode 100644 index 0000000..7920d30 --- /dev/null +++ b/.github/workflows/idea-implement.yml @@ -0,0 +1,193 @@ +name: Idea — implement + +# When an idea file with `status: ready` lands on main, Claude implements +# it on a feature branch, runs `mvn verify`, and opens a PR for human review. +# +# Trigger is `push` to main — forks cannot push to main, so this workflow +# is contributor-gated by construction. Human authorization is the merge +# of the idea spec PR. + +on: + push: + branches: [main] + paths: + - 'ideas/**.md' + workflow_dispatch: + inputs: + idea_file: + description: 'Path to idea file (e.g. ideas/foo.md)' + required: true + +permissions: + contents: read + +env: + MAVEN_VERSION: '3.6.3' + JAVA_VERSION: '17' + # Hard caps on the implementer + MAX_TURNS: '300' + DIFF_LOC_CAP: '1500' + +jobs: + identify: + runs-on: ubuntu-latest + outputs: + files: ${{ steps.scan.outputs.files }} + count: ${{ steps.scan.outputs.count }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Identify ready idea files + id: scan + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + CANDIDATES="${{ inputs.idea_file }}" + else + # Files under ideas/ touched by this push (added or modified) + CANDIDATES=$(git diff --name-only \ + "${{ github.event.before }}" "${{ github.sha }}" \ + -- 'ideas/*.md' \ + | grep -v '^ideas/README\.md$' \ + | grep -v '^ideas/_example\.md$' || true) + fi + + # Filter to status: ready + READY="" + for f in $CANDIDATES; do + [ -f "$f" ] || continue + if head -n 10 "$f" | grep -qE '^status:\s*ready\s*$'; then + READY="$READY $f" + fi + done + READY=$(echo "$READY" | xargs) + + echo "files=$READY" >> "$GITHUB_OUTPUT" + if [ -z "$READY" ]; then + echo "count=0" >> "$GITHUB_OUTPUT" + else + echo "count=$(echo "$READY" | wc -w | xargs)" >> "$GITHUB_OUTPUT" + fi + + implement: + needs: identify + if: needs.identify.outputs.count != '0' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + matrix: + # One job per ready idea — parallel, isolated branches + idea: ${{ fromJSON(format('[{0}]', needs.identify.outputs.files)) }} + max-parallel: 2 + steps: + - name: Checkout main + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Java ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: sapmachine + cache: maven + + - name: Setup Maven ${{ env.MAVEN_VERSION }} + uses: stCarolas/setup-maven@v5 + with: + maven-version: ${{ env.MAVEN_VERSION }} + + - name: Derive branch + slug + id: br + run: | + SLUG=$(basename "${{ matrix.idea }}" .md | tr -c 'a-z0-9-' '-' | sed 's/--*/-/g; s/^-//; s/-$//') + echo "slug=$SLUG" >> "$GITHUB_OUTPUT" + echo "branch=idea/$SLUG" >> "$GITHUB_OUTPUT" + + - name: Create implementation branch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Reset if it already exists from a previous run + git push origin --delete "${{ steps.br.outputs.branch }}" 2>/dev/null || true + git checkout -b "${{ steps.br.outputs.branch }}" + + - name: Run Claude implementer + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Implement the idea specified in `${{ matrix.idea }}`. + + ## Process + + 1. Read `${{ matrix.idea }}` carefully. Read `ideas/README.md` + for the convention. + 2. Explore the codebase enough to know where this fits — + `cds-feature-advanced-event-mesh/src/main/java/**`, + existing tests under `src/test/`, README sections it touches. + 3. Implement the change. Match the surrounding code style + (naming, comment density, idiom). Prefer the smallest viable + change. + 4. Add or extend tests so the acceptance criteria are covered. + Tests live in `cds-feature-advanced-event-mesh/src/test/java/`. + 5. Update the README/docs if the public API changed. + 6. Run `mvn -B -ntp verify` from the repo root. It MUST pass. + If it fails, iterate. + 7. Update the frontmatter of `${{ matrix.idea }}`: change + `status: ready` to `status: in-progress`. + 8. Commit everything with a clear message referencing the idea + file. + + ## Hard limits + + - Diff size: at most ${{ env.DIFF_LOC_CAP }} LOC (added + removed, + excluding generated code and test fixtures). + Check after each commit with: + git diff --stat main..HEAD | tail -1 + If you exceed it, ABORT — do NOT push. Instead, leave the + branch as-is and write a file `.implementation-aborted.md` + at the repo root explaining what to split out. The wrapper + will pick that up. + - Wall clock: 60 minutes (the job will be killed at the + timeout — pace yourself). + + ## On success + + When `mvn verify` is green and the diff is within the cap, + push the branch: + + git push origin "${{ steps.br.outputs.branch }}" + + Then open a PR with `gh pr create`: + + - Title: `feat: ` + - Body: link to the idea file, list what changed, paste + the `git diff --stat` summary, and explicitly call out + anything from the acceptance criteria you couldn't cover. + - Labels: `automation,idea` + - Do NOT mark as ready-to-merge — leave for human review. + + ## On failure + + If after multiple attempts `mvn verify` won't pass, or the + diff cap is exceeded: + + - Don't push a half-broken implementation. + - Open a draft PR with the work-in-progress branch. + - In the PR body, explain what's blocking. + - Add labels `automation,idea,needs-review`. + claude_args: | + --allowed-tools "Bash,Read,Write,Edit,Glob,Grep,WebFetch" + --max-turns ${{ env.MAX_TURNS }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IDEA_FILE: ${{ matrix.idea }} + IDEA_BRANCH: ${{ steps.br.outputs.branch }} diff --git a/.github/workflows/idea-pr-opened.yml b/.github/workflows/idea-pr-opened.yml new file mode 100644 index 0000000..60277ea --- /dev/null +++ b/.github/workflows/idea-pr-opened.yml @@ -0,0 +1,97 @@ +name: Idea PR — critique + +# When a PR adds or modifies an idea file (ideas/**), Claude reads the spec +# and posts a critique as a PR comment: missing acceptance criteria, +# ambiguous scope, conflicts with existing code, suggested API shape. +# +# This workflow runs on `pull_request` (not `pull_request_target`), so it +# can't access secrets when triggered by a fork. Fork PRs are gated by the +# repo's "Require approval for fork workflows" setting — a maintainer must +# click approve before this runs. + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'ideas/**' + +permissions: + contents: read + pull-requests: write + +jobs: + critique: + runs-on: ubuntu-latest + steps: + - name: Checkout PR + uses: actions/checkout@v4 + + - name: Identify changed idea files + id: changed + run: | + # All ideas/*.md files touched by this PR (added or modified) + FILES=$(gh pr view "${{ github.event.pull_request.number }}" \ + --json files --jq '.files[].path' \ + | grep '^ideas/.*\.md$' \ + | grep -v '^ideas/README\.md$' \ + | grep -v '^ideas/_example\.md$' || true) + echo "files<> "$GITHUB_OUTPUT" + echo "$FILES" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + if [ -z "$FILES" ]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Claude critique + if: steps.changed.outputs.has_changes == 'true' + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + A pull request is proposing changes to one or more idea specs + under `ideas/`. Your job is to critique each spec and post one + consolidated comment on the PR. + + Changed files: + ${{ steps.changed.outputs.files }} + + For each file: + 1. Read it. + 2. Read `ideas/README.md` for the convention. + 3. Scan the relevant parts of the codebase (cds-feature-advanced-event-mesh/src/**) + to check feasibility and flag conflicts with existing APIs. + 4. Produce a critique covering: + - Missing sections (problem / proposed behavior / API sketch / + acceptance criteria / out of scope) + - Ambiguity — anywhere the implementer would have to guess + - Concrete conflicts with existing code (cite file:line) + - Suggested refinements to API shape + - Estimated implementation size (S / M / L) and whether + it fits the ~1500 LOC implementer cap + + Post the result as ONE PR comment using `gh pr comment`. Use this + structure, with one section per changed file: + + # 🤖 Idea critique + + ## `ideas/` + + **Readiness:** ✅ ready to mark `status: ready` | ⚠️ needs work | ❌ blocked + + + + **Implementation size:** S/M/L (~N LOC) + + Be specific. "Add acceptance criteria" is useless; "AC 3 says + 'fast' — define a latency budget in ms" is useful. + + PR URL: ${{ github.event.pull_request.html_url }} + claude_args: | + --allowed-tools "Bash(gh pr:*),Read,Glob,Grep" + --max-turns 25 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml new file mode 100644 index 0000000..6aa3126 --- /dev/null +++ b/.github/workflows/maintenance.yml @@ -0,0 +1,111 @@ +name: Weekly maintenance + +# Weekly sweep that delegates four hygiene checks to Claude. Anything +# actionable is filed as an idea draft (status: draft) under ideas/ — +# the same pipeline you use for human-authored ideas. Maintainer reviews, +# flips to `ready`, idea-implement.yml takes over. +# +# No code changes happen directly from this workflow. It only proposes. + +on: + schedule: + # Mondays 10:23 UTC — staggered after the stuck-PR sweep + - cron: '23 10 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + maintenance: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: sapmachine + cache: maven + + - name: Setup Maven 3.6.3 + uses: stCarolas/setup-maven@v5 + with: + maven-version: '3.6.3' + + - name: Run dependency:analyze (capture report) + run: | + mvn -B -ntp dependency:analyze > /tmp/dep-analyze.txt 2>&1 || true + echo "==== dep-analyze.txt head ====" + head -200 /tmp/dep-analyze.txt + + - name: Run Claude maintenance sweep + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Weekly maintenance pass. Perform the four checks below. For + anything actionable, create a draft idea file under `ideas/`, + commit it on a new branch `maintenance/weekly-YYYYMMDD-`, + and open a PR with label `automation,maintenance`. Don't open + more than 4 PRs in one run — pick the most valuable findings. + + ## Check 1: README drift + + Compare `README.md` against `cds-feature-advanced-event-mesh/src/main/java/**`: + public API mentioned in README that no longer exists, config + keys renamed, code snippets that wouldn't compile. + + ## Check 2: README snippet compile + + For each fenced ```java block in README.md, check that the + types and methods it references actually exist in the codebase + (grep for class/method names). Don't try to literally compile + them — most are partial. + + ## Check 3: Stale TODOs + + Find TODO/FIXME/XXX comments in the codebase. Use git blame to + get their age. List anything >90 days old. One idea file per + cluster of related TODOs, not one per comment. + + ## Check 4: Dependency hygiene + + Read `/tmp/dep-analyze.txt`. Flag: + - "Used undeclared dependencies" — transitive deps the code + relies on. Worth promoting to explicit. + - "Unused declared dependencies" — verify with a grep before + trusting (the analyzer has false positives for compile-time + annotations and SPI). + + ## Output format + + For each finding worth filing, create a file: + ideas/maintenance-YYYYMMDD-.md + + Frontmatter: + --- + status: draft + priority: low + created: + source: maintenance-sweep + --- + + Body: standard idea structure (Problem / Proposed behavior / + Acceptance criteria). Keep it short — these are starter specs, + not novels. + + Commit each idea file on its own branch and open a PR. + claude_args: | + --allowed-tools "Bash,Read,Write,Edit,Glob,Grep" + --max-turns 100 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ideas/README.md b/ideas/README.md new file mode 100644 index 0000000..8941410 --- /dev/null +++ b/ideas/README.md @@ -0,0 +1,75 @@ +# Ideas + +This directory is the entry point for requirements and feature ideas in +this repo. Drop a markdown file describing what you want, open a PR, and +the automation will help shape and (once ready) implement it. + +## Convention + +One file per idea. Filename is a kebab-case slug: `support-cross-subaccount-queues.md`. + +Each file starts with frontmatter: + +```yaml +--- +status: draft # draft | ready | in-progress | done +priority: med # low | med | high +created: 2026-06-29 # ISO date — manual, since CI can't read git dates reliably +--- +``` + +Followed by free-form markdown. The structure isn't enforced — it's a spec, +not a form — but the critique bot will nudge you toward including: + +- **Problem.** What's broken or missing today. +- **Proposed behavior.** What the user-visible change looks like. +- **API sketch.** Method signatures, config keys, or YAML changes — even + a rough draft. Reduces ambiguity for the implementer. +- **Acceptance criteria.** Bullet list of testable conditions. +- **Out of scope.** What you're explicitly *not* asking for. + +## Lifecycle + +``` +draft → (you iterate the spec, critique bot comments) + ↓ +ready → (you merge the idea PR to main) + ↓ +in-progress → (idea-implement.yml runs, opens a code PR) + ↓ +done → (you merge the code PR; flip the status manually or let the + implementation PR do it) +``` + +### Status meanings + +- **`draft`** — spec is being shaped. The critique bot will comment on + PRs touching this file. `idea-implement.yml` ignores drafts. +- **`ready`** — spec is approved. Once merged to main, `idea-implement.yml` + will pick it up and try to implement it. +- **`in-progress`** — implementation has started or there's an open code + PR. The implementer sets this automatically. +- **`done`** — implemented and merged. Kept for history. + +## Who can submit ideas + +Anyone can open a PR, but per `.github/CODEOWNERS` ideas need code-owner +review to merge. Fork PRs are inert until a maintainer approves them via +GitHub's "Approve and run" gate. + +## What gets implemented automatically + +`idea-implement.yml` only runs on `push` to `main` for `ideas/**` files +with `status: ready`. That means: + +1. A human (you) merges the idea PR. +2. The implementation job picks it up and opens a follow-up code PR. +3. The code PR goes through the normal review process — it is **not** + auto-merged. + +The implementation agent runs with a diff-size cap (~1500 LOC). If your +idea is bigger than that, split it before marking `ready`. + +## Examples + +See `ideas/_example.md` for a worked example. diff --git a/ideas/_example.md b/ideas/_example.md new file mode 100644 index 0000000..c66fdee --- /dev/null +++ b/ideas/_example.md @@ -0,0 +1,60 @@ +--- +status: draft +priority: low +created: 2026-06-29 +--- + +# Example: surface broker reconnection events as CAP events + +This file is an example of the convention. Copy it to start a real idea +and edit accordingly. + +## Problem + +When the underlying SAP Advanced Event Mesh connection drops and +reconnects, applications using this plugin have no programmatic way to +react — no warm cache to flush, no metric to bump, no log to emit. They +only see it indirectly via failed publish attempts. + +## Proposed behavior + +Emit a CAP event on the `MessagingService` whenever the broker connection +state changes: `connection.up`, `connection.down`, `connection.reconnect`. +Applications subscribe with `@On(event = "connection.down", service = "messaging")`. + +## API sketch + +```java +// New constants in MessagingEvents +public static final String CONNECTION_UP = "connection.up"; +public static final String CONNECTION_DOWN = "connection.down"; +public static final String CONNECTION_RECONNECT = "connection.reconnect"; + +// Event payload +public record ConnectionStateEvent( + String reason, // human-readable cause, e.g. "heartbeat timeout" + Instant timestamp, + int attemptNumber // 0 for first connect; n for nth reconnect attempt +) {} +``` + +## Acceptance criteria + +- [ ] `ConnectionStateEvent` payload class exists with those three fields. +- [ ] On JMS `ConnectionListener.onException`, plugin emits `connection.down`. +- [ ] On successful reconnect, plugin emits `connection.up` (initial) or + `connection.reconnect` (subsequent). +- [ ] Unit test verifies an `@On(connection.down)` handler is invoked. +- [ ] README has a 10-line "Reacting to connection events" section. + +## Out of scope + +- Programmatic reconnection control (`forceReconnect()`). Connection + state is observable, not steerable. +- Per-queue connection events. This is broker-level only. + +## References + +- JMS spec on `ExceptionListener` +- Existing `MessagingService` event constants in + `cds-feature-advanced-event-mesh/src/main/java/.../MessagingEvents.java` From 1bf068866421de359c0e38d258eaca1a51661397 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 29 Jun 2026 08:42:17 +0200 Subject: [PATCH 2/4] chore(automation): use real team handle in CODEOWNERS Replace placeholder @cap-java/maintainers with the actual team @cap-java/cds-feature-advanced-event-mesh-team. Update AUTOMATION.md note accordingly. --- .github/AUTOMATION.md | 9 ++++----- .github/CODEOWNERS | 10 ++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/AUTOMATION.md b/.github/AUTOMATION.md index 450f75f..2575566 100644 --- a/.github/AUTOMATION.md +++ b/.github/AUTOMATION.md @@ -57,11 +57,10 @@ Create these labels (Issues → Labels → New label) so the workflows can apply ## 5. CODEOWNERS -`.github/CODEOWNERS` is committed but uses `@cap-java/maintainers` as a -placeholder. Before merging this scaffolding, replace it with the real -team handle or a list of user handles. Code-owner enforcement only kicks -in once branch protection has "Require review from Code Owners" enabled -(see §2). +`.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 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0a08a9d..955b862 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,11 +3,9 @@ # 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. -# -# Replace the handles below with the actual maintainer set before enabling. -/ideas/ @cap-java/maintainers +/ideas/ @cap-java/cds-feature-advanced-event-mesh-team # Automation surfaces — keep tight, since these run with elevated permissions. -/.github/workflows/ @cap-java/maintainers -/.github/dependabot.yml @cap-java/maintainers -/.github/CODEOWNERS @cap-java/maintainers +/.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 From 1d061eb3793acda9a94866c5d2cf29ad47490892 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 29 Jun 2026 09:38:12 +0200 Subject: [PATCH 3/4] feat(automation): daily upstream-sync workflow for fork drift When this fork runs Dependabot / idea-implement / maintenance, changes land on the fork's main while upstream's main moves independently. Without sync, drift compounds and reconciliation gets painful. upstream-sync.yml runs daily: - in-sync / ahead: no-op - behind: fast-forward main, push - diverged: rebase, force-push with --force-with-lease - conflicts: leave main alone, file an issue with the conflicting files and a suggested resolution recipe Guarded with 'if: github.repository \!= cap-java/...' so it never runs on the upstream repo even if accidentally merged there. --- .github/AUTOMATION.md | 1 + .github/workflows/upstream-sync.yml | 161 ++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 .github/workflows/upstream-sync.yml diff --git a/.github/AUTOMATION.md b/.github/AUTOMATION.md index 2575566..5349b5f 100644 --- a/.github/AUTOMATION.md +++ b/.github/AUTOMATION.md @@ -72,6 +72,7 @@ enabled (see §2), and the team must have write access to the repo. | `idea-pr-opened.yml` | PR opened touching `ideas/**` | Claude critiques the spec as PR comment. | | `idea-implement.yml` | Push to `main` touching `ideas/**` | Claude 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. | ## 7. Killswitch diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml new file mode 100644 index 0000000..a80824d --- /dev/null +++ b/.github/workflows/upstream-sync.yml @@ -0,0 +1,161 @@ +name: Upstream sync + +# Daily rebase of this fork's main onto cap-java/cds-feature-advanced-event-mesh:main. +# +# Purpose: when this fork runs the automation pipelines (Dependabot, +# idea-implement, maintenance), changes accumulate on the fork's main +# while upstream's main continues to advance independently. Without +# rebasing, the fork drifts further from upstream every day and the +# divergence becomes painful to reconcile. +# +# This workflow: +# - Fetches cap-java/cds-feature-advanced-event-mesh:main +# - If main is already ahead of (or equal to) upstream: no-op +# - If main is behind: rebases the fork's main onto upstream/main and +# force-pushes +# - If the rebase has conflicts: leaves main alone, files an issue +# describing the conflict and a suggested resolution path +# +# Force-pushing main is normally aggressive, but on a fork that's running +# automation pipelines it's the right move — open Dependabot PRs will +# auto-rebase against the new base; idea-implement branches are short-lived. + +on: + schedule: + # Daily 06:43 UTC — early enough to be done before working hours, + # off the :00 stampede + - cron: '43 6 * * *' + workflow_dispatch: + +permissions: + contents: read + +env: + UPSTREAM_REPO: cap-java/cds-feature-advanced-event-mesh + UPSTREAM_BRANCH: main + +jobs: + sync: + # Only run on forks — never on the upstream repo itself + if: github.repository != 'cap-java/cds-feature-advanced-event-mesh' + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + steps: + - name: Checkout fork main + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: | + git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" + git fetch upstream "${UPSTREAM_BRANCH}" + + - name: Detect divergence + id: check + run: | + UPSTREAM_SHA=$(git rev-parse "upstream/${UPSTREAM_BRANCH}") + FORK_SHA=$(git rev-parse HEAD) + BASE=$(git merge-base HEAD "upstream/${UPSTREAM_BRANCH}") + + echo "upstream=${UPSTREAM_SHA}" >> "$GITHUB_OUTPUT" + echo "fork=${FORK_SHA}" >> "$GITHUB_OUTPUT" + + if [ "$UPSTREAM_SHA" = "$FORK_SHA" ]; then + echo "state=in-sync" >> "$GITHUB_OUTPUT" + elif [ "$BASE" = "$UPSTREAM_SHA" ]; then + # Fork is strictly ahead of upstream — nothing to pull + echo "state=ahead" >> "$GITHUB_OUTPUT" + elif [ "$BASE" = "$FORK_SHA" ]; then + # Fork is strictly behind upstream — fast-forward possible + echo "state=behind" >> "$GITHUB_OUTPUT" + else + # Diverged — fork has commits upstream doesn't + echo "state=diverged" >> "$GITHUB_OUTPUT" + fi + + - name: Fast-forward (fork is strictly behind) + if: steps.check.outputs.state == 'behind' + run: | + git merge --ff-only "upstream/${UPSTREAM_BRANCH}" + git push origin HEAD:main + + - name: Attempt rebase (fork has diverged) + if: steps.check.outputs.state == 'diverged' + id: rebase + run: | + set +e + git rebase "upstream/${UPSTREAM_BRANCH}" + STATUS=$? + set -e + if [ $STATUS -ne 0 ]; then + # Capture the conflicting files for the issue body + CONFLICTS=$(git diff --name-only --diff-filter=U | sort -u) + git rebase --abort + echo "result=conflict" >> "$GITHUB_OUTPUT" + { + echo "conflicts<> "$GITHUB_OUTPUT" + else + echo "result=ok" >> "$GITHUB_OUTPUT" + fi + + - name: Force-push rebased main + if: steps.check.outputs.state == 'diverged' && steps.rebase.outputs.result == 'ok' + run: git push --force-with-lease origin HEAD:main + + - name: File issue on conflict + if: steps.check.outputs.state == 'diverged' && steps.rebase.outputs.result == 'conflict' + run: | + BODY=$(cat <> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- State: \`${{ steps.check.outputs.state }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Upstream HEAD: \`${{ steps.check.outputs.upstream }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Fork HEAD: \`${{ steps.check.outputs.fork }}\`" >> "$GITHUB_STEP_SUMMARY" From d7cf6367ac313288860b656285a42e8aa5ff62b3 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 29 Jun 2026 11:26:40 +0200 Subject: [PATCH 4/4] refactor(automation): use opencode + SAP AI Core instead of anthropics/claude-code-action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don't call Anthropic directly — LLM access is routed through SAP AI Core. Adopting the pattern from cap/sherlock: - opencode CLI (headless, 'opencode run') replaces claude-code-action. Provider is opencode's built-in sap-ai-core; auth via XSUAA OAuth2 client-credentials, handled internally by the provider. No proxy process needed. - AICORE_SERVICE_KEY (JSON service key) replaces ANTHROPIC_API_KEY as the single secret. - Model is configured in .github/bot-config/opencode.json — defaults to sap-ai-core/anthropic--claude-4.6-opus, swappable to sonnet/gpt-5/ gemini-2.5-pro without touching workflows. - .github/actions/setup-opencode is a reusable composite action that installs opencode at a pinned version with SHA256 verification. Five workflows updated to the new pattern: dependabot-triage, dependabot-stuck-sweep, idea-pr-opened, idea-implement, maintenance. AUTOMATION.md rewritten to document the new auth model and credit the sherlock pattern. --- .github/AUTOMATION.md | 58 ++++--- .github/actions/setup-opencode/action.yml | 38 +++++ .github/bot-config/opencode.json | 45 ++++++ .github/workflows/dependabot-stuck-sweep.yml | 68 ++++----- .github/workflows/dependabot-triage.yml | 70 ++++----- .github/workflows/idea-implement.yml | 151 +++++++++---------- .github/workflows/idea-pr-opened.yml | 115 +++++++------- .github/workflows/maintenance.yml | 133 ++++++++-------- 8 files changed, 384 insertions(+), 294 deletions(-) create mode 100644 .github/actions/setup-opencode/action.yml create mode 100644 .github/bot-config/opencode.json diff --git a/.github/AUTOMATION.md b/.github/AUTOMATION.md index 5349b5f..6424081 100644 --- a/.github/AUTOMATION.md +++ b/.github/AUTOMATION.md @@ -6,6 +6,12 @@ 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 @@ -14,10 +20,18 @@ Add these to **Settings → Secrets and variables → Actions**: | Secret | Used by | Notes | |-----------------------|--------------------------------------|-------| -| `ANTHROPIC_API_KEY` | dependabot-triage, idea-*, maintenance | Claude API key. Bedrock/Vertex creds work too — see the [Claude Code Action docs](https://github.com/anthropics/claude-code-action). | +| `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`:** @@ -52,8 +66,10 @@ Create these labels (Issues → Labels → New label) so the workflows can apply - `needs-review` — Dependabot major bumps; `idea-implement` failures - `major-update` — Dependabot major bumps (triggers `dependabot-triage`) -- `automation` — anything opened by a Claude-powered workflow +- `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 @@ -67,32 +83,36 @@ enabled (see §2), and the team must have write access to the repo. | 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` | Claude fetches changelog, posts a risk summary as PR comment. | -| `dependabot-stuck-sweep.yml` | Weekly cron (Mon 09:17 UTC) | Claude attempts to fix Dependabot PRs stuck >7 days. | -| `idea-pr-opened.yml` | PR opened touching `ideas/**` | Claude critiques the spec as PR comment. | -| `idea-implement.yml` | Push to `main` touching `ideas/**` | Claude implements `status: ready` ideas, opens PR. | +| `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 Claude across the board, revoke `ANTHROPIC_API_KEY`. All Claude -workflows will fail loudly on next run; nothing destructive happens. +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 Claude calls). +- `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. Bounded - by `--max-turns 200` across all stuck PRs combined. -- `idea-pr-opened`: bounded critique (~25 turns). -- `idea-implement`: highest-cost workflow. Bounded by `--max-turns 300` - and a 60-minute job timeout. Diff-size cap stops runaway PRs. -- `maintenance`: weekly. Bounded by `--max-turns 100`. - -If you want a hard monthly cap, set it on the Anthropic console (Usage -limits) rather than in workflows — it's the only place that enforces it -across all runs. +- `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. diff --git a/.github/actions/setup-opencode/action.yml b/.github/actions/setup-opencode/action.yml new file mode 100644 index 0000000..01431b3 --- /dev/null +++ b/.github/actions/setup-opencode/action.yml @@ -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 diff --git a/.github/bot-config/opencode.json b/.github/bot-config/opencode.json new file mode 100644 index 0000000..73e975e --- /dev/null +++ b/.github/bot-config/opencode.json @@ -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" + } + } +} diff --git a/.github/workflows/dependabot-stuck-sweep.yml b/.github/workflows/dependabot-stuck-sweep.yml index bdd3c45..f7c726b 100644 --- a/.github/workflows/dependabot-stuck-sweep.yml +++ b/.github/workflows/dependabot-stuck-sweep.yml @@ -1,13 +1,11 @@ name: Stuck Dependabot PR sweep -# Once a week, find Dependabot PRs that have been open >7 days with failing -# checks. For each, run Claude headless to attempt a fix — typically a rebase, -# a test adjustment, or a follow-up bump. If Claude can fix it, it pushes a -# commit; if not, it leaves a comment explaining what's blocking the PR. +# 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: - # Mondays 09:17 UTC — off the :00 stampede - cron: '17 9 * * 1' workflow_dispatch: @@ -20,6 +18,10 @@ jobs: 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 @@ -27,7 +29,6 @@ jobs: - name: Find stuck Dependabot PRs id: find run: | - # PRs older than 7 days, by dependabot, failing checks STUCK=$(gh pr list \ --author "app/dependabot" \ --state open \ @@ -40,38 +41,35 @@ jobs: echo "$STUCK" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" echo "count=$(echo "$STUCK" | jq 'length')" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run Claude to attempt fixes + - name: Set up opencode if: steps.find.outputs.count != '0' - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - The following Dependabot PRs have been open >7 days with failing - checks: + uses: ./.github/actions/setup-opencode - ${{ steps.find.outputs.stuck }} + - name: Run fix attempts + if: steps.find.outputs.count != '0' + run: | + cat < /tmp/prompt.md + The following Dependabot PRs have been open >7 days with failing + checks: + + ${{ steps.find.outputs.stuck }} - For each PR: + For each PR: - 1. `gh pr checkout ` into a local branch. - 2. Look at the failing check logs via - `gh run view --log-failed `. - 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. + 1. \`gh pr checkout \` into a local branch. + 2. Look at the failing check logs via + \`gh run view --log-failed \`. + 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. - claude_args: | - --allowed-tools "Bash,Read,Write,Edit,Glob,Grep" - --max-turns 200 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + Budget: one fix attempt per PR. Don't loop on the same PR. + EOF + opencode run --print-logs < /tmp/prompt.md diff --git a/.github/workflows/dependabot-triage.yml b/.github/workflows/dependabot-triage.yml index ca20e09..15caaf0 100644 --- a/.github/workflows/dependabot-triage.yml +++ b/.github/workflows/dependabot-triage.yml @@ -1,11 +1,11 @@ name: Dependabot changelog triage # For major Dependabot PRs (which auto-merge-deps.yml flags as 'needs-review'), -# this job uses Claude Code to fetch the upstream changelog, scan for breaking -# changes, and post a one-paragraph risk summary as a PR comment. +# this job uses opencode (routed via SAP AI Core) to fetch the upstream +# changelog, scan for breaking changes, and post a one-paragraph risk +# summary as a PR comment. # -# This does NOT auto-merge — it only enriches the PR with context so the -# human reviewer can decide quickly. +# Does NOT auto-merge — only enriches the PR. on: pull_request_target: @@ -23,6 +23,10 @@ jobs: permissions: contents: read 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 PR uses: actions/checkout@v4 @@ -35,39 +39,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run Claude changelog triage - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - A Dependabot PR is proposing a major version bump: + - name: Set up opencode + uses: ./.github/actions/setup-opencode + + - name: Run changelog triage + run: | + cat < /tmp/prompt.md + A Dependabot PR is proposing a major version bump: - dependency: ${{ steps.meta.outputs.dependency-names }} - from: ${{ steps.meta.outputs.previous-version }} - to: ${{ steps.meta.outputs.new-version }} - ecosystem: ${{ steps.meta.outputs.package-ecosystem }} + dependency: ${{ steps.meta.outputs.dependency-names }} + from: ${{ steps.meta.outputs.previous-version }} + to: ${{ steps.meta.outputs.new-version }} + ecosystem: ${{ steps.meta.outputs.package-ecosystem }} - Steps: - 1. Use WebFetch / WebSearch to find the upstream changelog or - release notes for this version range. - 2. Identify breaking changes that could affect this repo. - Cross-check against actual usages in the codebase. - 3. Classify risk as LOW / MEDIUM / HIGH and explain in 2-3 - sentences. Be concrete — name the API or behavior change, - don't just say 'there are breaking changes'. - 4. Post the result as a PR comment using `gh pr comment`. - Format: + Steps: + 1. Use WebFetch / WebSearch to find the upstream changelog or + release notes for this version range. + 2. Identify breaking changes that could affect this repo. + Cross-check against actual usages in the codebase. + 3. Classify risk as LOW / MEDIUM / HIGH and explain in 2-3 + sentences. Be concrete — name the API or behavior change, + don't say 'there are breaking changes'. + 4. Post the result as a PR comment using \`gh pr comment\`. + Format: - ## 🤖 Changelog triage — risk: + ## 🤖 Changelog triage — risk: - <2-3 sentence summary> + <2-3 sentence summary> - + - PR URL: ${{ github.event.pull_request.html_url }} - claude_args: | - --allowed-tools "Bash(gh pr comment:*),Bash(gh pr view:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Read,Glob,Grep,WebFetch,WebSearch" - --max-turns 25 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR URL: ${{ github.event.pull_request.html_url }} + EOF + opencode run --print-logs < /tmp/prompt.md diff --git a/.github/workflows/idea-implement.yml b/.github/workflows/idea-implement.yml index 7920d30..17b32ef 100644 --- a/.github/workflows/idea-implement.yml +++ b/.github/workflows/idea-implement.yml @@ -1,7 +1,8 @@ name: Idea — implement -# When an idea file with `status: ready` lands on main, Claude implements -# it on a feature branch, runs `mvn verify`, and opens a PR for human review. +# When an idea file with `status: ready` lands on main, opencode (via +# SAP AI Core) implements it on a feature branch, runs mvn verify, and +# opens a PR for human review. # # Trigger is `push` to main — forks cannot push to main, so this workflow # is contributor-gated by construction. Human authorization is the merge @@ -24,8 +25,6 @@ permissions: env: MAVEN_VERSION: '3.6.3' JAVA_VERSION: '17' - # Hard caps on the implementer - MAX_TURNS: '300' DIFF_LOC_CAP: '1500' jobs: @@ -46,7 +45,6 @@ jobs: if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then CANDIDATES="${{ inputs.idea_file }}" else - # Files under ideas/ touched by this push (added or modified) CANDIDATES=$(git diff --name-only \ "${{ github.event.before }}" "${{ github.sha }}" \ -- 'ideas/*.md' \ @@ -54,7 +52,6 @@ jobs: | grep -v '^ideas/_example\.md$' || true) fi - # Filter to status: ready READY="" for f in $CANDIDATES; do [ -f "$f" ] || continue @@ -82,9 +79,12 @@ jobs: strategy: fail-fast: false matrix: - # One job per ready idea — parallel, isolated branches idea: ${{ fromJSON(format('[{0}]', needs.identify.outputs.files)) }} max-parallel: 2 + 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 main uses: actions/checkout@v4 @@ -104,6 +104,9 @@ jobs: with: maven-version: ${{ env.MAVEN_VERSION }} + - name: Set up opencode + uses: ./.github/actions/setup-opencode + - name: Derive branch + slug id: br run: | @@ -115,79 +118,67 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - # Reset if it already exists from a previous run git push origin --delete "${{ steps.br.outputs.branch }}" 2>/dev/null || true git checkout -b "${{ steps.br.outputs.branch }}" - - name: Run Claude implementer - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - Implement the idea specified in `${{ matrix.idea }}`. - - ## Process - - 1. Read `${{ matrix.idea }}` carefully. Read `ideas/README.md` - for the convention. - 2. Explore the codebase enough to know where this fits — - `cds-feature-advanced-event-mesh/src/main/java/**`, - existing tests under `src/test/`, README sections it touches. - 3. Implement the change. Match the surrounding code style - (naming, comment density, idiom). Prefer the smallest viable - change. - 4. Add or extend tests so the acceptance criteria are covered. - Tests live in `cds-feature-advanced-event-mesh/src/test/java/`. - 5. Update the README/docs if the public API changed. - 6. Run `mvn -B -ntp verify` from the repo root. It MUST pass. - If it fails, iterate. - 7. Update the frontmatter of `${{ matrix.idea }}`: change - `status: ready` to `status: in-progress`. - 8. Commit everything with a clear message referencing the idea - file. - - ## Hard limits - - - Diff size: at most ${{ env.DIFF_LOC_CAP }} LOC (added + removed, - excluding generated code and test fixtures). - Check after each commit with: - git diff --stat main..HEAD | tail -1 - If you exceed it, ABORT — do NOT push. Instead, leave the - branch as-is and write a file `.implementation-aborted.md` - at the repo root explaining what to split out. The wrapper - will pick that up. - - Wall clock: 60 minutes (the job will be killed at the - timeout — pace yourself). - - ## On success - - When `mvn verify` is green and the diff is within the cap, - push the branch: - - git push origin "${{ steps.br.outputs.branch }}" - - Then open a PR with `gh pr create`: - - - Title: `feat: ` - - Body: link to the idea file, list what changed, paste - the `git diff --stat` summary, and explicitly call out - anything from the acceptance criteria you couldn't cover. - - Labels: `automation,idea` - - Do NOT mark as ready-to-merge — leave for human review. - - ## On failure - - If after multiple attempts `mvn verify` won't pass, or the - diff cap is exceeded: - - - Don't push a half-broken implementation. - - Open a draft PR with the work-in-progress branch. - - In the PR body, explain what's blocking. - - Add labels `automation,idea,needs-review`. - claude_args: | - --allowed-tools "Bash,Read,Write,Edit,Glob,Grep,WebFetch" - --max-turns ${{ env.MAX_TURNS }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IDEA_FILE: ${{ matrix.idea }} - IDEA_BRANCH: ${{ steps.br.outputs.branch }} + - name: Run implementer + run: | + cat < /tmp/prompt.md + Implement the idea specified in \`${{ matrix.idea }}\`. + + ## Process + + 1. Read \`${{ matrix.idea }}\`. Read \`ideas/README.md\` for + the convention. + 2. Explore the codebase enough to know where this fits — + \`cds-feature-advanced-event-mesh/src/main/java/**\`, + existing tests under \`src/test/\`, README sections it touches. + 3. Implement the change. Match the surrounding code style. + Prefer the smallest viable change. + 4. Add or extend tests so the acceptance criteria are covered. + Tests live in \`cds-feature-advanced-event-mesh/src/test/java/\`. + 5. Update the README/docs if the public API changed. + 6. Run \`mvn -B -ntp verify\` from the repo root. It MUST pass. + If it fails, iterate. + 7. Update the frontmatter of \`${{ matrix.idea }}\`: change + \`status: ready\` to \`status: in-progress\`. + 8. Commit everything with a clear message referencing the idea + file. + + ## Hard limits + + - Diff size: at most ${{ env.DIFF_LOC_CAP }} LOC (added + removed, + excluding generated code and test fixtures). + Check after each commit with: + git diff --stat main..HEAD | tail -1 + If you exceed it, ABORT — do NOT push. Instead, leave the + branch as-is and write a file \`.implementation-aborted.md\` + at the repo root explaining what to split out. + - Wall clock: 60 minutes (the job will be killed at the timeout). + + ## On success + + When \`mvn verify\` is green and the diff is within the cap, + push the branch: + + git push origin "${{ steps.br.outputs.branch }}" + + Then open a PR with \`gh pr create\`: + + - Title: \`feat: \` + - Body: link to the idea file, list what changed, paste the + \`git diff --stat\` summary, and explicitly call out anything + from the acceptance criteria you couldn't cover. + - Labels: \`automation,idea\` + - Do NOT mark as ready-to-merge — leave for human review. + + ## On failure + + If \`mvn verify\` won't pass, or the diff cap is exceeded: + + - Don't push a half-broken implementation. + - Open a draft PR with the work-in-progress branch. + - In the PR body, explain what's blocking. + - Add labels \`automation,idea,needs-review\`. + EOF + opencode run --print-logs < /tmp/prompt.md diff --git a/.github/workflows/idea-pr-opened.yml b/.github/workflows/idea-pr-opened.yml index 60277ea..ac7c188 100644 --- a/.github/workflows/idea-pr-opened.yml +++ b/.github/workflows/idea-pr-opened.yml @@ -1,13 +1,11 @@ name: Idea PR — critique -# When a PR adds or modifies an idea file (ideas/**), Claude reads the spec -# and posts a critique as a PR comment: missing acceptance criteria, -# ambiguous scope, conflicts with existing code, suggested API shape. +# Runs opencode (via SAP AI Core) to critique idea specs touched by a PR. +# Posts one consolidated comment. Advisory only — doesn't approve or block. # -# This workflow runs on `pull_request` (not `pull_request_target`), so it -# can't access secrets when triggered by a fork. Fork PRs are gated by the -# repo's "Require approval for fork workflows" setting — a maintainer must -# click approve before this runs. +# Triggered on `pull_request`, not `pull_request_target`, so secrets are +# not exposed to forks. Fork PRs are gated by the repo's +# "Require approval for fork workflows" setting. on: pull_request: @@ -22,6 +20,10 @@ permissions: jobs: critique: runs-on: ubuntu-latest + 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 PR uses: actions/checkout@v4 @@ -29,7 +31,6 @@ jobs: - name: Identify changed idea files id: changed run: | - # All ideas/*.md files touched by this PR (added or modified) FILES=$(gh pr view "${{ github.event.pull_request.number }}" \ --json files --jq '.files[].path' \ | grep '^ideas/.*\.md$' \ @@ -43,55 +44,53 @@ jobs: else echo "has_changes=true" >> "$GITHUB_OUTPUT" fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run Claude critique + - name: Set up opencode if: steps.changed.outputs.has_changes == 'true' - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - A pull request is proposing changes to one or more idea specs - under `ideas/`. Your job is to critique each spec and post one - consolidated comment on the PR. - - Changed files: - ${{ steps.changed.outputs.files }} - - For each file: - 1. Read it. - 2. Read `ideas/README.md` for the convention. - 3. Scan the relevant parts of the codebase (cds-feature-advanced-event-mesh/src/**) - to check feasibility and flag conflicts with existing APIs. - 4. Produce a critique covering: - - Missing sections (problem / proposed behavior / API sketch / - acceptance criteria / out of scope) - - Ambiguity — anywhere the implementer would have to guess - - Concrete conflicts with existing code (cite file:line) - - Suggested refinements to API shape - - Estimated implementation size (S / M / L) and whether - it fits the ~1500 LOC implementer cap - - Post the result as ONE PR comment using `gh pr comment`. Use this - structure, with one section per changed file: - - # 🤖 Idea critique - - ## `ideas/` - - **Readiness:** ✅ ready to mark `status: ready` | ⚠️ needs work | ❌ blocked - - - - **Implementation size:** S/M/L (~N LOC) - - Be specific. "Add acceptance criteria" is useless; "AC 3 says - 'fast' — define a latency budget in ms" is useful. - - PR URL: ${{ github.event.pull_request.html_url }} - claude_args: | - --allowed-tools "Bash(gh pr:*),Read,Glob,Grep" - --max-turns 25 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: ./.github/actions/setup-opencode + + - name: Run critique + if: steps.changed.outputs.has_changes == 'true' + run: | + cat < /tmp/prompt.md + A pull request is proposing changes to one or more idea specs + under \`ideas/\`. Your job is to critique each spec and post + one consolidated comment on the PR. + + Changed files: + ${{ steps.changed.outputs.files }} + + For each file: + 1. Read it. + 2. Read \`ideas/README.md\` for the convention. + 3. Scan the relevant parts of the codebase + (cds-feature-advanced-event-mesh/src/**) to check + feasibility and flag conflicts with existing APIs. + 4. Produce a critique covering: + - Missing sections (problem / proposed behavior / + API sketch / acceptance criteria / out of scope) + - Ambiguity — anywhere the implementer would have to guess + - Concrete conflicts with existing code (cite file:line) + - Suggested refinements to API shape + - Estimated implementation size (S / M / L) and whether + it fits the ~1500 LOC implementer cap + + Post the result as ONE PR comment via \`gh pr comment\`. Use + this structure, with one section per changed file: + + # 🤖 Idea critique + + ## \`ideas/\` + + **Readiness:** ✅ ready to mark \`status: ready\` | ⚠️ needs work | ❌ blocked + + + + **Implementation size:** S/M/L (~N LOC) + + Be specific. "Add acceptance criteria" is useless; "AC 3 says + 'fast' — define a latency budget in ms" is useful. + + PR URL: ${{ github.event.pull_request.html_url }} + EOF + opencode run --print-logs < /tmp/prompt.md diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 6aa3126..8776908 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -1,15 +1,13 @@ name: Weekly maintenance -# Weekly sweep that delegates four hygiene checks to Claude. Anything -# actionable is filed as an idea draft (status: draft) under ideas/ — -# the same pipeline you use for human-authored ideas. Maintainer reviews, -# flips to `ready`, idea-implement.yml takes over. +# Weekly sweep that delegates four hygiene checks to opencode (via SAP AI +# Core). Anything actionable is filed as an idea draft (status: draft) +# under ideas/ — the same pipeline used for human-authored ideas. # # No code changes happen directly from this workflow. It only proposes. on: schedule: - # Mondays 10:23 UTC — staggered after the stuck-PR sweep - cron: '23 10 * * 1' workflow_dispatch: @@ -23,6 +21,10 @@ jobs: 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 @@ -44,68 +46,63 @@ jobs: - name: Run dependency:analyze (capture report) run: | mvn -B -ntp dependency:analyze > /tmp/dep-analyze.txt 2>&1 || true - echo "==== dep-analyze.txt head ====" head -200 /tmp/dep-analyze.txt - - name: Run Claude maintenance sweep - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - Weekly maintenance pass. Perform the four checks below. For - anything actionable, create a draft idea file under `ideas/`, - commit it on a new branch `maintenance/weekly-YYYYMMDD-`, - and open a PR with label `automation,maintenance`. Don't open - more than 4 PRs in one run — pick the most valuable findings. - - ## Check 1: README drift - - Compare `README.md` against `cds-feature-advanced-event-mesh/src/main/java/**`: - public API mentioned in README that no longer exists, config - keys renamed, code snippets that wouldn't compile. - - ## Check 2: README snippet compile - - For each fenced ```java block in README.md, check that the - types and methods it references actually exist in the codebase - (grep for class/method names). Don't try to literally compile - them — most are partial. - - ## Check 3: Stale TODOs - - Find TODO/FIXME/XXX comments in the codebase. Use git blame to - get their age. List anything >90 days old. One idea file per - cluster of related TODOs, not one per comment. - - ## Check 4: Dependency hygiene - - Read `/tmp/dep-analyze.txt`. Flag: - - "Used undeclared dependencies" — transitive deps the code - relies on. Worth promoting to explicit. - - "Unused declared dependencies" — verify with a grep before - trusting (the analyzer has false positives for compile-time - annotations and SPI). - - ## Output format - - For each finding worth filing, create a file: - ideas/maintenance-YYYYMMDD-.md - - Frontmatter: - --- - status: draft - priority: low - created: - source: maintenance-sweep - --- - - Body: standard idea structure (Problem / Proposed behavior / - Acceptance criteria). Keep it short — these are starter specs, - not novels. - - Commit each idea file on its own branch and open a PR. - claude_args: | - --allowed-tools "Bash,Read,Write,Edit,Glob,Grep" - --max-turns 100 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Set up opencode + uses: ./.github/actions/setup-opencode + + - name: Run maintenance sweep + run: | + cat < /tmp/prompt.md + Weekly maintenance pass. Perform the four checks below. For + anything actionable, create a draft idea file under \`ideas/\`, + commit it on a new branch \`maintenance/weekly-YYYYMMDD-\`, + and open a PR with label \`automation,maintenance\`. Don't open + more than 4 PRs in one run. + + ## Check 1: README drift + + Compare \`README.md\` against + \`cds-feature-advanced-event-mesh/src/main/java/**\`: public API + mentioned in README that no longer exists, config keys renamed, + code snippets that wouldn't compile. + + ## Check 2: README snippet compile + + For each fenced \`\`\`java block in README.md, check that the + types and methods it references actually exist (grep for + class/method names). Don't literally compile — most are partial. + + ## Check 3: Stale TODOs + + Find TODO/FIXME/XXX comments. Use git blame for age. List + anything >90 days old. One idea per cluster of related TODOs, + not one per comment. + + ## Check 4: Dependency hygiene + + Read \`/tmp/dep-analyze.txt\`. Flag: + - "Used undeclared dependencies" — transitive deps to promote. + - "Unused declared dependencies" — verify with a grep before + trusting (the analyzer has false positives for compile-time + annotations and SPI). + + ## Output format + + For each finding worth filing, create: + ideas/maintenance-YYYYMMDD-.md + + Frontmatter: + --- + status: draft + priority: low + created: + source: maintenance-sweep + --- + + Body: Problem / Proposed behavior / Acceptance criteria. + Short — these are starter specs. + + Commit each idea file on its own branch and open a PR. + EOF + opencode run --print-logs < /tmp/prompt.md