From 14b8a6bf16fc7f91be6f2af442aeadad66a42251 Mon Sep 17 00:00:00 2001 From: Spikel Date: Mon, 24 Aug 2026 22:25:52 +0800 Subject: [PATCH] ci: let the plugin maintain itself, up to the point of release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three triggers, one rule: a pull request that passes its checks merges itself, and the only step a person performs is cutting a release. on-bitrouter-release.yml watches the gateway this plugin talks to. The dispatch from bitrouter/bitrouter arrives within seconds of a release; the weekly schedule is the backstop that notices when one never did, because a token expiring quietly is exactly the failure a fast path does not report. Both converge on .github/upstream.json, so whichever gets there first the other does nothing — and the agent stamps that file whatever it concludes, which turns "we looked and nothing was needed" from a silence into a recorded fact. on-harness-update.yml runs only when CI has already said no. The question a harness bump raises — is this package still compatible — has a cheaper and more reliable answer than an agent's opinion: the build typechecks against the new types and the suite exercises the contracts under them. A green bump needs nobody. So the agent runs where the cheap answer runs out, and its job is repair rather than assessment. It pushes onto the Dependabot branch so the bump and its fix arrive as one reviewable thing. It matches on the bumped package name rather than on Dependabot's label, which would not have worked: npm allows one update entry per directory, so every npm bump here carries harness-update whether or not it is one. automerge.yml enables auto-merge and nothing else — GitHub still decides, when the required checks pass. It withholds for a needs-human label, and applies one to an automated pull request that edits both src/ and test/: an agent asked to repair a break can always make a suite green by weakening the suite, so green there is not evidence. The same diff from a person is ordinary work. The policy the agent works under lives in bitrouter/.github rather than here. Three copies of a prompt is three behaviours; the install and test commands, which are genuinely this repository's, stay here. Co-Authored-By: Claude Opus 5 --- .github/upstream.json | 6 + .github/workflows/automerge.yml | 24 ++++ .github/workflows/on-bitrouter-release.yml | 125 +++++++++++++++++++++ .github/workflows/on-harness-update.yml | 109 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 .github/upstream.json create mode 100644 .github/workflows/automerge.yml create mode 100644 .github/workflows/on-bitrouter-release.yml create mode 100644 .github/workflows/on-harness-update.yml diff --git a/.github/upstream.json b/.github/upstream.json new file mode 100644 index 0000000..d765348 --- /dev/null +++ b/.github/upstream.json @@ -0,0 +1,6 @@ +{ + "$comment": "What this plugin has already been checked against. The release watcher compares its findings to these and does nothing when they match, so a missed dispatch self-heals on the next scheduled run instead of being lost. Advanced by the compatibility agent, in the same pull request as any code change it made.", + "bitrouter": { + "lastProcessedRelease": "v1.0.0-alpha.27" + } +} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..90b8b3d --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,24 @@ +name: Auto-merge + +# A pull request that passes its checks merges itself. The policy — what that +# excludes, and why — lives once, in bitrouter/.github. +# +# `pull_request_target` because a pull request from a fork gets a read-only +# token on `pull_request`, and enabling auto-merge is a write. Nothing here +# checks out the pull request's code or runs anything from it: the called +# workflow only reads the diff's file names through the API and calls +# `gh pr merge --auto`. The merging itself is GitHub's, gated on the required +# checks in this repository's branch protection. + +on: + pull_request_target: + types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review] + +permissions: {} + +jobs: + enable: + uses: bitrouter/.github/.github/workflows/plugin-automerge.yml@main + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/on-bitrouter-release.yml b/.github/workflows/on-bitrouter-release.yml new file mode 100644 index 0000000..14ca8d0 --- /dev/null +++ b/.github/workflows/on-bitrouter-release.yml @@ -0,0 +1,125 @@ +name: On BitRouter release + +# A BitRouter gateway release affects this plugin through one surface: the +# wire. `test/schema.test.ts` already watches the published `/v1/models` +# contract on a daily clock, so what a release adds is everything the schema +# cannot describe — an endpoint that moved, an auth flow that changed, a +# behaviour named in the notes rather than in a type. +# +# Two ways in, deliberately. `bitrouter/bitrouter` dispatches here the moment +# it publishes, which is fast; the schedule is the backstop that notices when +# a dispatch never arrived, because a token expiring quietly is exactly the +# kind of failure that a fast path does not report. Both converge on +# `.github/upstream.json`, so whichever gets there first, the other does +# nothing. + +on: + repository_dispatch: + types: [bitrouter-release] + schedule: + - cron: "0 7 * * 1" # Mondays, after the nightly jobs have had their say + workflow_dispatch: + inputs: + tag: + description: "Force a specific BitRouter tag (default: latest release)" + required: false + +permissions: {} + +jobs: + check: + name: is there a release we have not looked at + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + tag: ${{ steps.detect.outputs.tag }} + fresh: ${{ steps.detect.outputs.fresh }} + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + - id: detect + env: + GH_TOKEN: ${{ github.token }} + DISPATCHED: ${{ github.event.client_payload.tag }} + FORCED: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + SEEN=$(jq -r '.bitrouter.lastProcessedRelease' .github/upstream.json) + + TAG="${FORCED:-${DISPATCHED:-}}" + if [ -z "$TAG" ]; then + TAG=$(gh api repos/bitrouter/bitrouter/releases/latest --jq .tag_name 2>/dev/null || true) + fi + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + echo "fresh=false" >> "$GITHUB_OUTPUT" + echo "no release found for bitrouter/bitrouter" + exit 0 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + if [ "$TAG" = "$SEEN" ]; then + echo "fresh=false" >> "$GITHUB_OUTPUT" + echo "already looked at $TAG" + exit 0 + fi + + # A pull request in any state for this tag's branch counts as + # handled: it covers the window while one is open for review, and it + # respects a release somebody deliberately closed without merging. + COUNT=$(gh pr list --state all --head "chore/bitrouter-$TAG" --json number --jq length) + if [ "$COUNT" != "0" ]; then + echo "fresh=false" >> "$GITHUB_OUTPUT" + echo "a pull request for chore/bitrouter-$TAG already exists" + exit 0 + fi + + echo "fresh=true" >> "$GITHUB_OUTPUT" + echo "new BitRouter release: $TAG (last looked at: $SEEN)" + + compat: + needs: check + if: needs.check.outputs.fresh == 'true' + uses: bitrouter/.github/.github/workflows/plugin-compat.yml@main + permissions: + contents: read + secrets: + agent_token: ${{ secrets.PLUGIN_AGENT_TOKEN }} + bitrouter_api_key: ${{ secrets.BITROUTER_API_KEY }} + with: + node_version: "20" + install: npm ci + build: npm run build + test: npm test + branch: chore/bitrouter-${{ needs.check.outputs.tag }} + title: "chore: check @bitrouter/opencode against BitRouter ${{ needs.check.outputs.tag }}" + reason: >- + BitRouter released ${{ needs.check.outputs.tag }}, and this plugin + talks to that gateway. + brief: | + Read the release notes first: + + gh release view ${{ needs.check.outputs.tag }} --repo bitrouter/bitrouter --json body --jq .body + + Most releases need no change here. This plugin is coupled to the + gateway's HTTP surface and nothing else — it ships no binary, pins no + gateway version, and downloads nothing. So act only on notes that + describe a change to something it actually uses: a `/v1/*` endpoint's + path, its response shape, its auth, or the reserved `bitrouter/auto` + slug and the preset resolution behind it. + + Do NOT act on a change to the routing policy engine, the TUI, config + file handling, telemetry, or anything else that never crosses the + wire this plugin speaks over. Those move constantly and none of them + reach this package. + + The response shape has its own watcher — `test/schema.test.ts` checks + the published `/v1/models` schema daily and fails on any field that + appears or disappears. If the notes describe a models-endpoint change, + expect that suite to have caught it already; say so rather than + duplicating the work. + + Whatever you conclude, set `.bitrouter.lastProcessedRelease` in + `.github/upstream.json` to "${{ needs.check.outputs.tag }}". Do this + even when nothing else changed — it is what records that this release + was examined, and what stops the watcher from asking again every week. diff --git a/.github/workflows/on-harness-update.yml b/.github/workflows/on-harness-update.yml new file mode 100644 index 0000000..3ebe80e --- /dev/null +++ b/.github/workflows/on-harness-update.yml @@ -0,0 +1,109 @@ +name: On harness update + +# Dependabot opens the bump; CI judges it; this runs only when CI said no. +# +# The old shape of this ran an agent on every harness bump to ask whether the +# plugin was still compatible. But that question already has a cheaper and +# more reliable answer: the build typechecks this package against the new +# harness types and the suite exercises the contracts underneath them. A green +# bump needs no opinion, and under auto-merge it merges itself. +# +# So the agent runs where the cheap answer runs out — on a bump that broke +# something — and its job is not to assess but to repair. It pushes onto the +# Dependabot branch rather than opening a second pull request, so the bump and +# the fix arrive as one reviewable thing instead of two incomplete ones. +# +# `workflow_run` rather than `pull_request_target`: it runs from the default +# branch, so the workflow definition and the secrets it sees are this +# repository's own and never the pull request's. + +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: {} + +jobs: + identify: + name: is this a harness bump that failed + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'failure' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.actor.login == 'dependabot[bot]' + permissions: + contents: read + pull-requests: read + outputs: + pr: ${{ steps.pick.outputs.pr }} + title: ${{ steps.pick.outputs.title }} + steps: + - id: pick + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD: ${{ github.event.workflow_run.head_branch }} + run: | + set -euo pipefail + PR=$(gh pr list --repo "$REPO" --head "$HEAD" --state open --json number --jq '.[0].number // empty') + if [ -z "$PR" ]; then + echo "no open pull request for $HEAD — nothing to repair" + exit 0 + fi + + TITLE=$(gh pr view "$PR" --repo "$REPO" --json title --jq .title) + + # Only the harness. A vitest or typescript bump breaking the build is + # a real signal too, but it is not what this automation is for, and + # an agent let loose on a tooling upgrade tends to rewrite more than + # it repairs. + # + # Matched on the bumped package rather than on Dependabot's label: + # the label is configured per update entry and npm allows only one + # entry per directory, so every npm bump in this repository carries + # `harness-update` whether or not it is one. The package name is the + # thing that actually distinguishes them. + case "$TITLE" in + *"@opencode-ai/"*) ;; + *) echo "#$PR does not bump the harness (\"$TITLE\") — leaving it"; exit 0 ;; + esac + + echo "pr=$PR" >> "$GITHUB_OUTPUT" + echo "title=$TITLE" >> "$GITHUB_OUTPUT" + echo "repairing #$PR" + + repair: + needs: identify + if: needs.identify.outputs.pr != '' + uses: bitrouter/.github/.github/workflows/plugin-compat.yml@main + permissions: + contents: read + secrets: + agent_token: ${{ secrets.PLUGIN_AGENT_TOKEN }} + bitrouter_api_key: ${{ secrets.BITROUTER_API_KEY }} + with: + node_version: "20" + install: npm ci + build: npm run build + test: npm test + pull_request: ${{ needs.identify.outputs.pr }} + reason: >- + Dependabot bumped the opencode harness ("${{ needs.identify.outputs.title }}") + and CI failed on the bump. + brief: | + The dependency is already bumped in this checkout — that part is + Dependabot's and is not yours to change. Do not touch `package.json` + or the lockfile, and do not pin the harness back to an older version + to make the failure go away. Reverting the bump is not a repair; if + the new version genuinely cannot be supported yet, change nothing and + say so in your report. + + Run the commands above and read the actual failure before changing + anything. The usual cause is a moved or renamed export, a changed type + signature, or a service contract this plugin implements that gained or + lost a member. Adapt this package to the new shape. + + Where the harness changed behaviour rather than types, be careful: a + change that still compiles can still be wrong. If the right adaptation + depends on intent you cannot read off the diff, leave it and flag it.