diff --git a/.github/workflows/hermes-version-gate.yml b/.github/workflows/hermes-version-gate.yml new file mode 100644 index 0000000..bf392ae --- /dev/null +++ b/.github/workflows/hermes-version-gate.yml @@ -0,0 +1,47 @@ +--- +name: Hermes Version Gate + +on: # yamllint disable-line rule:truthy + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: [main] + +permissions: + contents: read + +jobs: + # Required check; unfiltered and unconditional so it reports on every PR. + version-bumped: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: hermes/pyproject.toml version must move + env: + BASE_REF: ${{ github.base_ref }} + NO_PLUGIN_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'no-plugin-release') }} + run: | + set -euo pipefail + if [ "$NO_PLUGIN_RELEASE" = "true" ]; then + echo "no-plugin-release label present; version bump not required" + exit 0 + fi + version() { sed -n 's/^version = "\(.*\)"$/\1/p' "$1" | head -1; } + base=$(git merge-base "origin/${BASE_REF}" HEAD) + if git diff --quiet "$base" HEAD -- hermes/; then + echo "no hermes/ changes in this PR; version bump not required" + exit 0 + fi + head_version=$(version hermes/pyproject.toml) + git show "${base}:hermes/pyproject.toml" > "$RUNNER_TEMP/base-pyproject.toml" + base_version=$(version "$RUNNER_TEMP/base-pyproject.toml") + echo "base=${base_version} head=${head_version}" + [ -n "$head_version" ] || { echo "::error file=hermes/pyproject.toml::no version found"; exit 1; } + if [ "$head_version" = "$base_version" ]; then + echo "::error file=hermes/pyproject.toml::this PR changes hermes/ but leaves version at ${base_version}; bump it, or label the PR no-plugin-release" + exit 1 + fi diff --git a/hermes/plugin.yaml b/hermes/plugin.yaml index 2b045c5..5e0a647 100644 --- a/hermes/plugin.yaml +++ b/hermes/plugin.yaml @@ -1,5 +1,5 @@ name: tinyfish -version: 0.1.0 +version: 0.1.1 description: "First-party TinyFish provider plugin for Hermes Agent: Search and Fetch over the TinyFish REST APIs with API-key auth, plus credit-gated Browser sessions." author: TinyFish kind: backend diff --git a/hermes/pyproject.toml b/hermes/pyproject.toml index 69ddf58..0088e02 100644 --- a/hermes/pyproject.toml +++ b/hermes/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "tinyfish-hermes" -version = "0.1.0" +version = "0.1.1" description = "TinyFish web provider plugin for Hermes Agent - Search and Fetch over the TinyFish REST APIs" readme = "README.md" license = { text = "MIT" } diff --git a/hermes/tests/test_setup_cli.py b/hermes/tests/test_setup_cli.py index 238cea9..7b888d9 100644 --- a/hermes/tests/test_setup_cli.py +++ b/hermes/tests/test_setup_cli.py @@ -2,6 +2,7 @@ import argparse import json +from pathlib import Path from typing import Any import pytest @@ -628,3 +629,50 @@ def test_in_session_status_command(env: dict[str, Any]) -> None: assert cli.tinyfish_status_command("bogus", provider=provider) == ( "Usage: /tinyfish-status [live]" ) + + +# Mirrors hermesPluginStatusSchema in ux-labs sdk/cli/src/lib/hermes-plugin.ts. +_TINYFISH_CLI_STATUS_KEYS = frozenset( + { + "ok", + "api_key_configured", + "api_key_env_var", + "plugin_version", + "provider_available", + "web_backend_configured", + } +) + + +def test_status_json_carries_every_key_the_tinyfish_cli_parses( + env: dict[str, Any], capsys: pytest.CaptureFixture[str] +) -> None: + """A missing key degrades `tinyfish doctor` to 'unparseable output'.""" + args = _parser().parse_args(["status", "--json"]) + + assert cli.dispatch_tinyfish_cli(args) == 0 + + payload = json.loads(capsys.readouterr().out) + assert _TINYFISH_CLI_STATUS_KEYS <= payload.keys() + + +def test_status_json_exits_zero_even_when_unhealthy( + env: dict[str, Any], capsys: pytest.CaptureFixture[str] +) -> None: + """A non-zero exit reads as 'plugin not installed' in `tinyfish doctor`.""" + env["config"] = {} + + assert cli.dispatch_tinyfish_cli(_parser().parse_args(["status", "--json"])) == 0 + assert json.loads(capsys.readouterr().out)["ok"] is False + + +def test_plugin_manifest_name_is_what_uninstall_keys_on() -> None: + """`tinyfish connect --uninstall` runs `hermes plugins uninstall tinyfish`.""" + manifest = Path(__file__).resolve().parents[1] / "plugin.yaml" + names = [ + line.partition(":")[2].strip().strip("'\"") + for line in manifest.read_text(encoding="utf-8").splitlines() + if line.startswith("name:") + ] + + assert names == ["tinyfish"] diff --git a/hermes/tests/test_version.py b/hermes/tests/test_version.py index 1fbf6e3..2807b34 100644 --- a/hermes/tests/test_version.py +++ b/hermes/tests/test_version.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +import tomllib import tinyfish_hermes as plugin @@ -53,3 +54,13 @@ def test_public_version_is_exported() -> None: assert isinstance(plugin.__version__, str) assert plugin.__version__ assert "__version__" in plugin.__all__ + + +def test_plugin_manifest_version_matches_the_distribution_version() -> None: + """Both feed `plugin_version`; drift makes one install report two versions.""" + hermes_root = Path(plugin.__file__).resolve().parents[1] + pyproject = tomllib.loads( + (hermes_root / "pyproject.toml").read_text(encoding="utf-8") + ) + + assert plugin._version_from_plugin_manifest() == pyproject["project"]["version"]