Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/upstream.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
24 changes: 24 additions & 0 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -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
125 changes: 125 additions & 0 deletions .github/workflows/on-bitrouter-release.yml
Original file line number Diff line number Diff line change
@@ -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.
109 changes: 109 additions & 0 deletions .github/workflows/on-harness-update.yml
Original file line number Diff line number Diff line change
@@ -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.
Loading