From 05aa056c9c14d1c95edcbd0a08e63ae1677c63c1 Mon Sep 17 00:00:00 2001 From: Spikel Date: Sat, 18 Jul 2026 00:17:56 -0400 Subject: [PATCH] ci: add per-PR build/test matrix and Hermes release watcher - ci.yml: on PR/push, run gen-check + each plugin's install/build/test in isolation; matrix built from plugins.manifest.json, toolchain keyed on language/packageManager. - on-hermes-release.yml: weekly watcher for NousResearch/hermes-agent releases (Hermes' harness isn't a package dep, so no Dependabot). Runs the compat agent scoped to hermes-agent/ and opens one PR; idempotent via harness.lastProcessedRelease (baseline v2026.7.7.2) + existing-PR guard. - manifest: add hermes lastProcessedRelease baseline; correct versionScheme to calver. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 70 +++++++++++++++ .github/workflows/on-hermes-release.yml | 110 ++++++++++++++++++++++++ plugins.manifest.json | 5 +- 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/on-hermes-release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..092e258 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +# Per-PR gate. Two things: +# 1. gen-check — generated constants are in sync with shared/bitrouter.json +# 2. plugin matrix — each island package installs/builds/tests in isolation, using the +# exact install/build/test commands from plugins.manifest.json (so "what CI runs" == +# "what publishes"). Toolchain setup is keyed on the plugin's language/packageManager. + +on: + pull_request: + push: + branches: [main] + +jobs: + gen-check: + name: generated files fresh + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - run: node scripts/gen.mjs --check + + setup: + name: build plugin matrix + runs-on: ubuntu-latest + outputs: + plugins: ${{ steps.m.outputs.plugins }} + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + - id: m + run: echo "plugins=$(jq -c '.plugins' plugins.manifest.json)" >> "$GITHUB_OUTPUT" + + plugin: + name: ${{ matrix.plugin.dir }} + needs: setup + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + plugin: ${{ fromJson(needs.setup.outputs.plugins) }} + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + + - if: ${{ matrix.plugin.packageManager == 'pnpm' }} + uses: pnpm/action-setup@v4 + with: { version: 10 } + + - if: ${{ matrix.plugin.language == 'ts' }} + uses: actions/setup-node@v4 + with: { node-version: "20" } + + - if: ${{ matrix.plugin.language == 'python' }} + uses: actions/setup-python@v5 + with: { python-version: "3.11" } + + - name: install + working-directory: ${{ matrix.plugin.dir }} + run: ${{ matrix.plugin.install }} + + - name: build + working-directory: ${{ matrix.plugin.dir }} + run: ${{ matrix.plugin.build }} + + - name: test + working-directory: ${{ matrix.plugin.dir }} + run: ${{ matrix.plugin.test }} diff --git a/.github/workflows/on-hermes-release.yml b/.github/workflows/on-hermes-release.yml new file mode 100644 index 0000000..1848cb2 --- /dev/null +++ b/.github/workflows/on-hermes-release.yml @@ -0,0 +1,110 @@ +name: On Hermes Release + +# hermes-agent's harness (NousResearch/hermes-agent) is not a package dependency, so it +# can't be tracked by Dependabot like openclaw/pi. This watches its releases on a schedule +# and, when a new one appears, runs the compat agent scoped to hermes-agent/ and opens one PR. +# +# "Already handled" is guarded two ways: +# - harness.lastProcessedRelease in plugins.manifest.json (advanced on merge), and +# - an existing PR (any state) for the deterministic branch chore/hermes- +# (covers the window while a PR is open, and releases a human deliberately closed). + +on: + schedule: + - cron: "0 6 * * 1" # weekly, Monday 06:00 UTC + workflow_dispatch: + inputs: + tag: + description: "Force a specific hermes tag (default: latest release)" + required: false + +jobs: + check: + name: Check for a new Hermes release + runs-on: ubuntu-latest + outputs: + repo: ${{ steps.detect.outputs.repo }} + tag: ${{ steps.detect.outputs.tag }} + fresh: ${{ steps.detect.outputs.fresh }} + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + - id: detect + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FORCE_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + REPO=$(jq -r '.plugins[] | select(.dir=="hermes-agent") | .harness.repo' plugins.manifest.json) + LAST=$(jq -r '.plugins[] | select(.dir=="hermes-agent") | .harness.lastProcessedRelease' plugins.manifest.json) + echo "repo=$REPO" >> "$GITHUB_OUTPUT" + + TAG="$FORCE_TAG" + if [ -z "$TAG" ]; then + TAG=$(gh api "repos/$REPO/releases/latest" --jq '.tag_name' 2>/dev/null || true) + fi + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + TAG=$(gh api "repos/$REPO/tags" --jq '.[0].name' 2>/dev/null || true) + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + echo "fresh=false" >> "$GITHUB_OUTPUT"; echo "No release/tag found for $REPO"; exit 0 + fi + if [ "$TAG" = "$LAST" ]; then + echo "fresh=false" >> "$GITHUB_OUTPUT"; echo "Up to date ($TAG == lastProcessedRelease)"; exit 0 + fi + COUNT=$(gh pr list --state all --head "chore/hermes-$TAG" --json number --jq 'length') + if [ "$COUNT" = "0" ]; then + echo "fresh=true" >> "$GITHUB_OUTPUT"; echo "New Hermes release: $TAG (last: $LAST)" + else + echo "fresh=false" >> "$GITHUB_OUTPUT"; echo "PR for chore/hermes-$TAG already exists" + fi + + update: + name: Update hermes-agent for ${{ needs.check.outputs.tag }} + needs: check + if: needs.check.outputs.fresh == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: { persist-credentials: false } + - uses: actions/setup-python@v5 + with: { python-version: "3.11" } + + - uses: anomalyco/opencode/github@latest + env: + BITROUTER_API_KEY: ${{ secrets.BITROUTER_API_KEY }} + with: + model: bitrouter/kimi-k2.5 + prompt: | + Hermes Agent released ${{ needs.check.outputs.tag }} (repo: + ${{ needs.check.outputs.repo }}). Work ONLY inside `hermes-agent/` and the + hermes-agent entry of `plugins.manifest.json`. + + Hermes is NOT a Python dependency — the plugin is loaded by Hermes via + hermes-agent/hermes_bitrouter/plugin.yaml. Verify the plugin still matches + Hermes's current plugin contract: + + 1. Read the release notes and provider/plugin interface: + gh release view ${{ needs.check.outputs.tag }} --repo ${{ needs.check.outputs.repo }} --json body --jq '.body' + Check for changes to the plugin manifest schema (plugin.yaml: provides, + config_schema, integration) and the model-provider methods Hermes calls on + the provider (see hermes_bitrouter/provider.py: chat_completion, + stream_chat_completion, list_models, get_model_info, health_check). + 2. If the contract changed, update hermes-agent/ to match. If a fix is uncertain, + add a TODO and flag it in the PR body. + 3. From inside hermes-agent/, run and fix only failures your changes cause: + python -m venv .venv && .venv/bin/pip install -e . -r requirements.txt + .venv/bin/python -m compileall hermes_bitrouter + .venv/bin/pytest + 4. ALWAYS set plugins.manifest.json -> hermes-agent.harness.lastProcessedRelease + to "${{ needs.check.outputs.tag }}" (records the release as handled and + guarantees a non-empty PR even when no adapter change was needed). + + Open a PR, branch chore/hermes-${{ needs.check.outputs.tag }}, touching only + hermes-agent/** and the manifest field above. Do NOT auto-merge — a human reviews. diff --git a/plugins.manifest.json b/plugins.manifest.json index 8f5c2fc..5caa53b 100644 --- a/plugins.manifest.json +++ b/plugins.manifest.json @@ -53,9 +53,10 @@ "harness": { "name": "Hermes Agent", "repo": "NousResearch/hermes-agent", - "versionScheme": "semver", + "versionScheme": "calver", "updateVia": "repo-release", - "$note": "Hermes is NOT a package dependency, so it is not tracked by Dependabot. Its update signal is a new release on NousResearch/hermes-agent, watched on a schedule." + "lastProcessedRelease": "v2026.7.7.2", + "$note": "Hermes is NOT a package dependency (not Dependabot-tracked). on-hermes-release.yml watches NousResearch/hermes-agent releases on a schedule; lastProcessedRelease is the baseline it compares against and bumps in each PR." } } ]