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
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 }}
110 changes: 110 additions & 0 deletions .github/workflows/on-hermes-release.yml
Original file line number Diff line number Diff line change
@@ -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-<tag>
# (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.
5 changes: 3 additions & 2 deletions plugins.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
]
Expand Down
Loading