Commit Graph
31 Commits
Author SHA1 Message Date
37547a7bd1 Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454) (#6837)
* test: prove worklog continuity across stream settle

* fix: keep settled worklog in place on stream done

* fix: preserve settle follow ordering

* test: wire settled worklog frame proof

* fix: preserve settled worklog detail disclosure

* fix: preserve default-open settled worklogs

* fix: preserve blank-owner worklogs

* fix: keep render fallback self-contained

* ci: retrigger hosted checks

* style(worklog): grow collapsed Worklog rail tap target to 44px on touch devices (#6454 follow-up)

* Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 18:06:57 -07:00
Frank SongandFrank Song 89023eb191 Live-to-Final: prove historical hydration and fence terminal retry ownership (#6451)
* test: add historical hydration lifecycle proof

* fix: retry anchor persistence after terminal app errors

* fix: fence deferred anchor settlement ownership

* test: cover deferred anchor retry ownership

* fix: preserve terminal anchor retry across refresh

* fix: bind terminal anchor retry to turn owner

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-07-31 11:04:40 -07:00
starship-s d00c3332e4 ci: constrain MCP SDK to compatible 1.x releases (#6564)
Assisted-by: Hermes Agent:gpt-5.6-sol
2026-07-29 10:35:23 -07:00
a61880b977 Release: terminal-error lifecycle gate row (#6354, @franksong2702) (#6358)
* test: add terminal-error lifecycle matrix row

* test: reject empty terminal process rows

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-20 10:52:18 +00:00
307b7fce5f Release: terminal-error settlement timing/seal (#6323) + Docker experimental builds (#6329) (#6341)
* fix: preserve timing and seal tool rows on terminal error (#6309)

* fix: publish Docker experimental builds to ghcr (#6298)

- Add exp-v* trigger to release workflow so experimental tags build
  and push Docker images
- Add :experimental floating tag for experimental channel, keeping
  :latest scoped to stable v* tags only
- Mark GitHub Releases from exp-v* tags as pre-releases
- Document available Docker tags (:latest, :experimental, version
  pins) in docs/docker.md

Closes #6298

* docs(changelog): stamp #6323 terminal-error timing/seal + #6329 Docker exp builds

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-19 22:17:36 +00:00
deac1384fe Live Stream: add public conversation lifecycle browser gate (#6251)
* test: add conversation lifecycle browser gate

* test: wait for durable lifecycle settlement

* test: harden lifecycle gate cleanup and startup

* test: normalize gateway fixture request paths

* test: harden conversation lifecycle gate

* test: fix lifecycle request failure capture

* test: align lifecycle CI dependencies

* test: harden lifecycle gate waits

* docs: align lifecycle gate setup command

* test: harden lifecycle gate persistence wait

* ci: scope conversation-lifecycle gate to relevant code paths

Only run the playwright browser gate when the chat render/streaming surface
it exercises actually changes (static/**, api/**.py, server.py, the test,
deps, the workflow). Docs-only and unrelated PRs skip it entirely, keeping
CI lean per the docs-only fast-path philosophy.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-18 21:23:25 -07:00
nesquena-hermesandnesquena-hermes 2ea0cc6ed2 ci: docs-only fast-path + minimal docs CI (#6279)
* ci: docs-only fast path + minimal docs CI

Skip the full pytest matrix + browser smoke on docs/CHANGELOG/README-only PRs
(required checks still report green fast via a fail-safe 'changes' gate job), and
add a lightweight Docs CI: critical_markdown_check.py (rendering-breaks only, not
style) + lychee broken-link check. Detection fails safe (any uncertainty or any
non-doc path -> full suite runs).

* ci: tighten docs-only detection — extension/type wins over name

A code file whose NAME contains README/CHANGELOG (scripts/CHANGELOG_stamp.py,
static/README_renderer.js) was wrongly classified docs-only, which would SKIP the
test suite on a real code change. Now a path is docs only by doc extension, exact
doc basename, or non-code file under docs/. Verified against 16 cases incl. every
code-with-docs-name trap.

* ci: fix all Fable+Codex gate findings on docs-only fast path

- BLOCKER: drop *.txt from is_docs (requirements.txt is a dep manifest — a bump
  would have skipped the whole test matrix). docs = *.md/.markdown/.rst + bare
  doc basenames only; strict allowlist, no docs/** denylist.
- Rename hole: use 'git diff --name-only --no-renames' so a rename src/app.py ->
  docs.md reveals the code-side deletion instead of collapsing to the doc dest.
- SECURITY: docs-ci.yml no longer interpolates untrusted fork-PR filenames into
  run: via ${{ }} (which executes $() in a crafted name). File list flows through
  a file + mapfile-as-args; lychee gets a fixed glob, not the attacker list.
  Added permissions: contents: read.
- Wedge belt-and-suspenders: 'if: ${{ always() }}' on the required test +
  browser-smoke jobs so a failed 'changes' job can't skip them; step guards treat
  missing/empty docs_only as run-full.
- critical_markdown_check.py: corrected the core rule — a newline in the whitespace
  AROUND a link destination is valid CommonMark (was a false positive); only a
  newline INSIDE the destination token, or an unclosed inline link, breaks
  rendering. Verified full agreement with the markdown-it-py reference parser +
  0 false positives on all 43 repo docs. Also blank 4-space indented code + multi
  backtick spans. Reworded 'block the merge' -> 'break rendering' (non-required).

* ci: address Codex re-gate — lint always() + markdown title/unclosed cases

- Add if: always() to the lint job too (not currently required, but future-proof
  against the wedge class if it's ever promoted).
- critical_markdown_check.py: handle two more CommonMark cases Codex found —
  a newline inside a "title" string is legal (skip), and a newline-terminated
  unclosed dest ([x](url\n at EOF/EOL with no close) is broken (flag). After the
  destination token ends, valid continuations are ')' or a title opener (" ' ();
  bare text after the newline is the real break. Verified full agreement with
  markdown-it-py across 13 cases + 0 FP on all 43 repo docs.

* ci: model the CommonMark inline-dest grammar (root-cause fix for markdown checker)

Round-3 gate found the title heuristic caused sibling regressions: a parenthesized
multi-line title (url (a\nb)) was falsely flagged, and a quote glued into the URL
(exa"part) was wrongly treated as a title-start and skipped. Rather than patch more
heuristics, replace the ad-hoc newline logic with _scan_inline_dest(), which walks
the actual grammar: skip leading ws -> bare dest (balanced parens, ends at ws or the
depth-0 ')') or <angle> dest -> after ws the next char must be ')' or a real title
opener (" ' () -> else the destination is split across the line (broken). Verified
FULL agreement with markdown-it-py across 20 adversarial cases (incl. both round-3
regressions, balanced parens, angle dests, multiline titles) + 0 FP on all 43 docs.

* test: pytest suite for critical_markdown_check (42 cases)

Durable, repeatable verification for the docs-CI markdown checker: 19 verdict cases
+ 19 cross-checked against the markdown-it-py CommonMark reference (skips cleanly if
the lib is absent) + 3 code-span-safety cases + empty/no-link inputs. Covers both
round-3 regressions (parenthesized multi-line title valid; quote-glued-in-URL broken),
balanced parens, angle destinations, and multiline titles. 42 passed.

* ci: fix 2 grammar edges from Codex round-4 (escaped-> in angle dest, unbalanced bare-dest parens)

- Angle dest <...> now honors backslash escapes: [x](<foo\>bar>) renders (the \> is
  escaped), was a false positive.
- Bare dest must have BALANCED parens: [x](foo(\n)) does not render (a '(' stays open
  when whitespace ends the token) — now returns split, was a false negative.
Both verified against markdown-it-py + added as pytest cases. 46 passed, 0 FP on 43 docs.

* ci: escaped-newline in angle dest is still a raw newline (Codex round-5)

<...> escape handling skipped the char after backslash including a newline, so
[x](<foo\<nl>>) returned ok but doesn't render (blockquote on line 2). An escaped
newline inside an angle destination is still a raw newline -> split. Preserves
[x](<foo\>bar>). Added regression case. 48 pytest cases pass, 0 FP on 43 docs.

* ci: mirror escaped-newline guard to bare dest (Codex round-6, GFM-correct)

The angle branch already treated backslash-newline as split; the bare branch skipped
it, so [x](foo\<nl>bar) returned ok. GFM/CommonMark forbid line endings in bare
destinations (GitHub's cmark-gfm won't render it), though markdown-it-py permissively
does. Since these docs are GitHub-rendered we follow GFM: flag it. Added as a
SPEC_DIVERGENT test case (verdict-asserted, excluded from the permissive parser
cross-check). 49 pytest pass, escaped paren/space still valid, 0 FP on 43 docs.

* ci: delimiter-aware title scan (Codex round-7 unclosed-link false negatives)

Naive text.find(')') matched the TITLE's own ')' not the link's outer ')', so
[x](foo (title), [x](<foo> (title), [x](foo "title" all returned ok despite being
unclosed links. Now parse the title to its actual closing delimiter ("..", '..',
or a (..) that forbids nested unescaped '(' per CommonMark), then require the link's
own ')' after optional whitespace. Also catches the nested-paren-title break
[x](foo (a (b) c)). Verified vs markdown-it-py; escaped parens + single-quoted
titles containing parens still valid. 61 pytest pass, 0 FP on 43 docs.

* ci: remove unused variable (ruff F841 in critical_markdown_check)

The CI lint gate (ruff forward E9+F+B on new lines) caught a dead 'stripped =
line.lstrip()' leftover from an earlier refactor in _blank_code — the fence
detection matches on the raw line. Removed. No behavior change (61 pytest pass).

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-18 23:24:23 +00:00
Rod Boev e19eefbe59 ci(#5658): add restore-keys for graceful cache fallback on version bumps 2026-07-05 22:59:37 -04:00
Rod Boev f09e5d0b33 ci(#5658): install Playwright in pytest CI so browser tests run 2026-07-05 22:54:44 -04:00
nesquena-hermes 76d6cc7f74 #5142: office-doc preview + safe docx editing (#540), rebased on master; optional deps 2026-07-02 01:15:57 +00:00
nesquena-hermes 72f7530c45 ci: run tests across 5 shards (up from 3) + fix 2 latent test-isolation leaks
Two cross-file isolation leaks that a finer shard partition exposes are fixed at
root so every shard passes regardless of execution order:
- test_auth_session_persistence.py: bind api.auth STATE_DIR/_SESSIONS_FILE/
  _LOGIN_ATTEMPTS_FILE in setUp + restore in tearDown (was relying on a sibling
  reload test to rebind them).
- test_request_diagnostics_cache.py: pin the session-list cache source stamp via
  an autouse fixture (matches test_session_sidebar_cache.py) so a leaked state.db
  WAL sidecar can't cause a spurious cache-miss.

Verified all shards green under 3-, 4-, and 5-way partitions. CI infra + test
harness only; no application behavior change.
2026-06-29 05:55:16 +00:00
nesquena-hermesandnesquena-hermes 3ba5d4dd67 ci: add whole-tree Python syntax gate (compileall) to fail fast on unparseable .py (#4643)
The ruff E9 gate is diff-scoped, so a SyntaxError/IndentationError reported on
an untouched line can slip the filter and only surface as a mass-failure of
every test shard at the conftest server-boot fixture. compileall fails in ~2s
with a clear file:line, before the matrix suite runs. Python analog of the JS
node --check pass. Prompted by #4641.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-21 17:21:30 -07:00
nesquena-hermesandnesquena-hermes da5bf69aee fix(sidebar): hoist _sessionAttentionState to fix ReferenceError crash (#3696) + scope-undef prevention gate (#3698)
* fix(sidebar): hoist _sessionAttentionState to top-level scope (#3696)

_sessionAttentionState was declared inside renderSessionListFromCache() and
relied on function hoisting, but the top-level function _sidebarRowHasVisible
Messages (reached via renderSessionListFromCache -> _partitionSidebarSessionRows)
called it bare. Hoisting is scoped to the enclosing function, so every sidebar
cache-render threw 'ReferenceError: _sessionAttentionState is not defined' and
the session list went blank. Regressed in #3672 (v0.51.269) when _sidebarRow
HasVisibleMessages was extracted to top level.

Fix: move _sessionAttentionState to top-level scope (it is pure — only uses its
arg plus the i18n global t), so both the visibility predicate and the nested
per-row renderer can reach it.

Prevention (the durable half): add scripts/scope_undef_gate.py — models the
classic-<script> shared global scope (union of all static files' top-level
symbols) and runs ESLint no-undef per file, flagging a function defined nested
but called from a sibling scope. Wired into CI (.github/workflows/tests.yml lint
job) alongside the existing no-const-assign runtime gate, plus an in-suite test
(test_static_js_scope_undef.py) and a focused structural regression test
(test_issue3696_session_attention_scope.py). RED/GREEN-validated against the
broken tree.

* fix(streaming): thread source param into stale-stream bailout; tighten scope gate

Opus review of #3698 found the new scope_undef_gate's 'source' allowlist entry
was masking a real same-class bug: _bailOutOfTerminalEventsFromStaleStream
(declared inside attachLiveStream, params activeSid/streamId/uploaded/options)
called _closeSource(source) against a 'source' not in its lexical scope. All 5
call sites are inside _wireSSE(source), but JS scope is lexical not dynamic, so
the helper would throw ReferenceError: source is not defined on the stale-stream
terminal-event path (user back in an active session whose old stream finalizes
late).

Fix: thread source as an explicit parameter (declaration + all 5 call sites),
the same make-the-dependency-explicit fix as #3696 — and REMOVE the 'source'
allowlist entry so the gate stays gated against that name (it now passes because
the bug is fixed, not because it's allowlisted). Added the documented
false-negative classes from Opus's review to the gate docstring (name-collision
shadowing, destructuring-regex gap, exposure escape hatches, name-keyed
allowlist) and a focused regression test.

This is the prevention gate catching a real latent bug on its first outing.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-05 20:49:45 -07:00
nesquena-hermes 717f4688d3 chore: add GitHub Sponsors funding config
Adds .github/FUNDING.yml so GitHub displays a "Sponsor" button on the
repository, linking to GitHub Sponsors for @nesquena.
2026-05-31 22:26:35 +00:00
nesquena-hermesandnesquena-hermes e58fb6a201 ci: forward-looking ruff lint gate (E9+F+B, new-code-only) (#3275)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-05-31 18:26:37 +00:00
nesquena-hermesandnesquena-hermes 5b2cbe1323 ci: add headless browser smoke test (runtime brick-class gate) (#3231)
Boots the real server.py agent-free and loads the key pages in headless
Chromium, failing on any console error or uncaught JS exception. Catches the
runtime-JS brick class (const-reassign #3162, function/window collision
#2715/#2771) that node --check, ESLint, and the mocked pytest suite cannot see
because they only manifest when a real browser executes the page.

Credential-free: strips *_API_KEY from the env, drives no real model, needs no
secrets. Runs on every PR + push via .github/workflows/browser-smoke.yml.

Co-authored-by: nesquena-hermes <[email protected]>
2026-05-30 18:48:25 -07:00
nesquena-hermes f9acf464e9 ci: run test suite in 3 parallel shards + make suite shard-safe
Completes the test-sharding half of #3197 (Docker-cache half shipped v0.51.177).
Adds pytest-shard 3-way split to tests.yml (3 shards x 3 Python = 9 jobs,
fail-fast: false). pytest-shard is 0-indexed so the matrix uses [0,1,2] — the
original #3197 used [1,2,3] which would have crashed the out-of-range job and
silently skipped shard 0's tests.

Made the suite shard-safe by fixing 4 cross-test state-pollution bugs that
passed sequentially but failed when sharded:
- test_onboarding_mvp: reset onboarding_completed flag (settings.json) in the
  autouse fixture; the config-cleanup only cleared config.yaml/.env.
- test_issue693_system_health_panel: invalidate the process-wide password-hash
  cache before/after so a prior test's "no password" cache doesn't defeat the
  auth-gate assertion.
- test_auth_session_persistence: assert against auth._SESSIONS_FILE (where auth
  actually writes) instead of a local _TEST_STATE path that only matched under a
  lucky import order.
- test_profile_env_isolation (root cause of the worst leak): stop deleting +
  re-importing api.profiles under a temp HERMES_BASE_HOME — that swapped the
  module object and poisoned the cached _DEFAULT_HERMES_HOME for every later
  test (broke test_title_aux_routing's load_config). Now points the cached path
  via monkeypatch.setattr (auto-restored, no module swap).
- conftest: autouse fixture restores HERMES_HOME/HERMES_BASE_HOME after each
  test as defense-in-depth against future switch_profile leaks.

Verified: all 3 shards green (6912 passed, 0 failed); full sequential run still
green (6957 passed, 0 failed). Slowest shard ~70s vs ~180s sequential.
2026-05-30 21:09:25 +00:00
nesquena-hermesandhayriodabas f4772ff081 ci(docker): cache smoke-test image layers via GHA cache (#3197 docker half)
Build the Docker image once in a build-image job, cache layers via type=gha,
restore from cache in each smoke variant instead of rebuilding. Variant restore
uses cache-from only (build-image is the sole cache writer).

Co-authored-by: hayriodabas <hayriodabas@users.noreply.github.com>
2026-05-30 20:10:27 +00:00
Dustin ae6b6b1b72 ci(windows): make taskkill no-op when server.py already exited
The path-discovery step succeeds on the first run, but the cleanup
step exits non-zero because `taskkill /PID 5560 /T /F` returns 128
("process not found") when server.py has already exited on the mock
hermes_cli stub. That's the expected steady state for this mock-only
workflow, not a failure.

Two-line fix: reset `$global:LASTEXITCODE = 0` after the taskkill
call, and explicit `exit 0` at the end of the step so any other
external-command exit codes don't bubble up. The try/catch wrapper
didn't help because taskkill writes its diagnostic to stderr without
raising a PowerShell exception — `catch` never fired.

Run 26352805510 on this branch shows the failure shape: "OK: start.ps1
path discovery - all guards passed." in the verify step, then
"ERROR: The process '5560' not found." in the cleanup step. Path
discovery is what this workflow exists to validate; cleanup just has
to not fail the job.
2026-05-24 17:10:02 +00:00
Dustin 145a442f61 ci(windows): rework #2811 with mock hermes_cli (maintainer ask, option 1)
Per @nesquena-hermes review on #2811: hermes-agent isn't published to
PyPI, so `pip install hermes-agent` finds nothing and start.ps1's
hermes_cli guard correctly bails out — leaving the previous workflow
unable to self-validate against release/stage-batch6.

This rework adopts option 1 from the review: drop the pip install,
stub a hermes_cli/ directory with a minimal __init__.py next to the
sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and
assert that none of its own Write-Error guards (no Python, no agent
dir, bad port, missing hermes_cli, missing server.py) appeared in
stderr. /health is no longer probed — the server cannot boot on a
stub, and full-boot regressions stay covered by the Linux jobs and
docker-smoke.yml.

Scope intentionally narrower than the original: this workflow
validates start.ps1's PowerShell syntax + path discovery only. The
exact bug class PR #2805 caught (WOW64 ProgramFiles redirect) would
now light up red here pre-merge, which is the reason this gate exists.

Paths filter trimmed to `start.ps1` + the workflow itself; the broader
list (requirements.txt / bootstrap.py / server.py) was inherited from
the original full-boot scoping and isn't relevant for a path-discovery-
only run.

Verification: workflow runs on this PR via its own pull_request trigger.
The first CI run on this branch IS the verification.

CHANGELOG updated under [Unreleased] with a single bullet sized to the
surrounding density.
2026-05-24 17:10:02 +00:00
Nathan EsquenaziandClaude Opus 4.7 64590cb6b9 harden(docker-smoke): catch !!ERROR/!!Exiting + tighten egg_info test
Two non-blocking observations from the review, both addressed:

1. The bad-pattern grep listed `error_exit` as a literal token, but the
   `error_exit()` function at docker_init.bash:5-10 only echoes the
   strings `"!! ERROR: "` and `"!! Exiting script (ID: $$)"` — the
   function name itself never appears in container logs. So
   `grep -E -i "error_exit"` would only fire on stray debug prints of
   the name, not on actual failures. The other patterns
   (`Failed to set (UID|GID|...)`, `groupmod: cannot`, etc.) DO catch
   real error_exit output, so this wasn't a coverage gap — just a dead
   token.

   Add `!! ERROR` and `!! Exiting script` to the bad-pattern set so the
   grep actually matches the function's output. Keep the literal
   `error_exit` token as belt-and-suspenders for any debug/echo of the
   name.

2. `test_docker_init_excludes_egg_info_during_staging` was a single
   `assert "egg-info" in src` check. That passes if any occurrence
   appears — including the explanatory comment block above the staging
   logic. A maintainer removing the `--exclude='*.egg-info'` from
   rsync but keeping the comment would slip past the test.

   Tighten to:
   - scope to the staging block (between `_stage_src=` and the
     `uv pip install` line) so comments outside that window can't
     satisfy the assertion;
   - require the literal `--exclude='*.egg-info'` rsync flag;
   - require `*.egg-info` in the block so the cp-fallback cleanup is
     also pinned;
   - additionally require `--exclude='build'`, `--exclude='dist'`,
     `--exclude='__pycache__'` so all four setuptools-touchable
     artifact dirs stay excluded.

Verified:
- tests/test_docker_docs_and_readonly.py — 11/11 pass.
- YAML parses cleanly via `yaml.safe_load`.
- Full suite: 5770 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:34:46 -07:00
nesquena-hermes 5b6f69c884 ci(docker): runtime smoke gate for Docker init logic
Closes the source-only-test gap that let v0.51.84's :ro-mount x chown -h
{} + startup regression reach review with 5800+ green pytests. Adds a
new GitHub Actions workflow .github/workflows/docker-smoke.yml that
actually runs 'docker compose up' against each compose variant.

Triggers
--------
Path-filtered on pull_request + push to master:
  Dockerfile, docker_init.bash, docker-compose*.yml, .dockerignore,
  .env.docker.example, .github/workflows/docker-smoke.yml itself.
Also workflow_dispatch for manual runs.

Jobs
----
1. compose-config -- preflight that 'docker compose config' parses each
   of the three compose files. Cheap, fast, catches schema/interpolation
   drift in parallel before any container starts.

2. smoke (matrix: single / two-container / three-container) -- for each
   variant:
   a. Reap any leftover hermes-smoke-* containers/volumes/networks from
      prior runs (defence-in-depth on self-hosted runners; hosted runners
      are fresh).
   b. docker build -t ghcr.io/nesquena/hermes-webui:latest .
      Critical: the multi-container compose files reference the GHCR
      image. Without this retag, multi-container smoke would test the
      previously-released image, NOT the PR's docker_init.bash / Dockerfile
      changes. With the retag, Compose's default pull_policy=missing keeps
      the local build in place and the PR is genuinely exercised.
   c. mktemp -d for ephemeral HERMES_HOME + HERMES_WORKSPACE so the
      runner's host filesystem is never touched.
   d. docker compose up -d --wait --wait-timeout 120 (Dockerfile carries a
      HEALTHCHECK so --wait blocks on 'healthy', not just 'running').
   e. curl /health probe with a 30-attempt x 2s poll loop as headroom for
      the multi-container variants' Python dep install phase.
   f. grep startup logs for known-bad signatures:
        EROFS | Read-only file system | Traceback | PermissionError |
        error_exit | groupmod: cannot | usermod: cannot |
        Failed to set (UID|GID|owner|permissions|ownership)
      These are the exact patterns that would have flagged #2470 in real
      time. Failed-to-set is anchored to specific objects to avoid false
      positives on benign locale/library bootstrap warnings.
   g. trap on EXIT: docker compose down -v --remove-orphans + rm -rf the
      ephemeral host paths, regardless of how the job exited.

Safety
------
- permissions: contents: read only -- no GITHUB_TOKEN write scope.
- Fork PRs run with no secrets (standard pull_request, not
  pull_request_target).
- No host bind mounts; no ~/.hermes exposure; no network egress beyond
  what compose itself needs to pull the agent image.
- timeout-minutes: 15 on the smoke job as a hard ceiling against a
  hung docker build.
- Per-run COMPOSE_PROJECT name (hermes-smoke-VARIANT-RUNID-ATTEMPT)
  so concurrent runs or reruns can't clobber each other.

Out of scope for v1 (per design review)
---------------------------------------
- HERMES_WEBUI_SMOKE_TEST env flag in docker_init.bash -- production-code
  footgun that would let any leaked env var silently exit before
  serving traffic.
- --user 60000:60000 -- incompatible with the image's root-init phase
  and would skip the very chown branch we are guarding against.
- Local-runnable scripts/docker-smoke-test.sh -- defer until CI gating
  ships and we see what contributors actually trip over.
- Hadolint / yamllint -- separate lint workflow, follow-up PR.
- Podman runtime smoke -- defer until a podman-specific bug ships.

Pre-merge verification
----------------------
- actionlint: clean
- YAML parse: clean (3 triggers, 2 jobs, 3-variant matrix)
- bash -n on all 6 run-blocks: clean
- pytest tests/ -q --timeout=60: 5889 passed, 6 skipped (no test impact;
  workflow-only change)
- Opus design review on the brief (REVISE -> minimum scope adopted)
- Opus implementation review on this workflow (APPROVE)
2026-05-18 00:09:41 +00:00
ai-ag2026 98c9a3de72 test: tighten CI and console hygiene
(cherry picked from commit bd9e6df71c2e8a6f0902b9b7a348dc21c854141a)
2026-05-11 23:13:16 +00:00
nesquena-hermes 0590d597a3 ci: install mcp + pytest-asyncio in CI; importorskip in test_mcp_server.py
CI failed on stage-323 because:
1. mcp_server.py imports the 'mcp' package (optional runtime dep) — only
   users who actually run the MCP integration install it. CI runs with
   stdlib-only deps (pyyaml + pytest + pytest-timeout).
2. tests/test_mcp_server.py uses pytest.mark.asyncio which requires
   pytest-asyncio — not installed in CI.

Fix:
- Add pytest-asyncio to CI install line.
- Try-install mcp; if it fails (Python 3.13 wheel issues, etc.) the test
  module uses pytest.importorskip and skips cleanly without breaking the
  matrix.
- tests/test_mcp_server.py: add module-level importorskip for both 'mcp'
  and 'pytest_asyncio' as a safety net.

Local: 4947/4947 still pass after change.
2026-05-08 20:26:11 +00:00
nesquena-hermesandnesquena-hermes 38e215e8f8 fix: dynamic version badge — read from git tag, never hardcoded (#790)
* fix: dynamic version badge — read from git tag, never hardcoded

The settings panel showed v0.50.87 and the HTTP Server: header said
HermesWebUI/0.50.38 — both hardcoded strings that drift further behind
with every release because there was no mechanism to keep them in sync.

Changes:
- api/updates.py: add _run_git() (moved before _detect_webui_version),
  _detect_webui_version(), and WEBUI_VERSION module constant resolved
  once at import time via 'git describe --tags --always --dirty'.
  Fallback chain: git → api/_version.py → 'unknown'.
- api/routes.py: inject webui_version into GET /api/settings response
  so the frontend can read it without a separate API call.
- static/panels.js: loadSettingsPanel() populates .settings-version-badge
  from settings.webui_version — one line after the existing api() call.
- static/index.html: replace stale hardcoded 'v0.50.87' with '—'
  placeholder; JS overwrites it as soon as the settings panel opens.
- server.py: replace hardcoded 'HermesWebUI/0.50.38' server_version with
  'HermesWebUI/' + WEBUI_VERSION.lstrip('v') — stays in sync automatically.
- Dockerfile: add ARG HERMES_VERSION=unknown and write api/_version.py
  so Docker images (where .git is excluded) still show the correct tag.
- .github/workflows/release.yml: pass build-args: HERMES_VERSION=${{ github.ref_name }}
  to the Docker build step on tag pushes.
- .gitignore: exclude api/_version.py (generated by Docker/CI, never committed).

No manual 'update the version badge' step is required going forward.
Tagging is sufficient — the badge and HTTP header update automatically.

Tests: 18 new tests in tests/test_version_badge.py covering the full
resolution chain, /api/settings injection, HTML placeholder, JS wiring,
and server.py import. 1596 tests pass total.

* fix: address review feedback on PR #790

- api/updates.py: replace exec() with regex parse for api/_version.py
  (no supply-chain risk from build artifact; exec unnecessary for one assignment)
- api/updates.py: cap git describe timeout at 3s (was 10s — import-time
  stall on NFS/.git would block server startup unnecessarily)
- server.py: lstrip('v') → removeprefix('v') (lstrip strips chars not prefix)
- server.py: emit bare 'HermesWebUI' when version is 'unknown' rather than
  'HermesWebUI/unknown' (log aggregators expect semver-ish suffix or none)
- CHANGELOG.md: add v0.50.124 entry for this user-visible change
- tests: rename exec-error test to reflect regex behaviour; add tests for
  removeprefix usage and unknown-version header guard (1598 tests total)

---------

Co-authored-by: nesquena-hermes <hermes@nesquena.com>
2026-04-20 20:36:53 -07:00
Nathan EsquenaziandClaude Opus 4.6 b12a682121 ci: add GitHub Actions workflow to run tests on PRs (#307)
Runs pytest suite across Python 3.11, 3.12, and 3.13 on ubuntu-latest.
Agent-dependent tests auto-skip via existing conftest logic.
Triggers on PRs targeting master and pushes to master.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:38:27 -07:00
Nathan EsquenaziandClaude Opus 4.6 bba9a236c3 fix(ci): use stricter semver regex for Docker tag extraction
Use v(\d+\.\d+(?:\.\d+)?) instead of v(.*) to only match real
version numbers (v0.29, v0.29.1), not arbitrary strings.
Keep latest unconditional since all tag pushes are releases.

Based on review of PR #52 approach vs #53.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:32:50 -07:00
Nathan EsquenaziandClaude Opus 4.6 1a579ef9cf fix(ci): use match pattern for Docker tags to support 2-part versions
The semver pattern in docker/metadata-action requires 3-part versions
(e.g. v0.29.0). Our tags use 2-part (v0.29), causing the metadata
step to produce empty tags, which made build-push-action fail.

Fix: use type=match with regex to extract the version string directly,
plus type=raw for the latest tag unconditionally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:27:41 -07:00
Nathan EsquenaziandClaude Opus 4.6 2766314e81 fix(ci): use standard version tags for GitHub Actions
The SHA-pinned versions from the security hardening commit referenced
non-existent commit hashes, causing the workflow to fail with 'unable
to resolve action'. Switch to standard major version tags (v4, v3, v2,
v6, v5) which are the recommended approach for GitHub-maintained and
well-known actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:09:36 -07:00
Nathan Esquenazi 4a3b9571f1 fix(ci): pin all GitHub Actions to full commit SHAs for supply chain security
Pinned all 7 third-party actions from mutable version tags to immutable
commit SHAs. Mutable tags (e.g. @v4) can be force-pushed by the action
author (or a compromised account) to inject malicious code into the workflow,
which runs with write access to the repo and GHCR registry.

Also moved 'permissions' from workflow level to job level (best practice:
scope permissions as narrowly as possible).

Pin mapping:
  actions/checkout@v4               -> @11bd71901bbe...  (v4.2.2)
  softprops/action-gh-release@v2    -> @c062e08bd532...  (v2.2.1)
  docker/setup-qemu-action@v3       -> @49b3bc8e6bdd...  (v3.2.0)
  docker/setup-buildx-action@v3     -> @c47758b77c97...  (v3.7.1)
  docker/login-action@v3            -> @9780b0c442fb...  (v3.3.0)
  docker/metadata-action@v5         -> @369eb591f429...  (v5.6.1)
  docker/build-push-action@v6       -> @ca877d9245fe...  (v6.10.0)
2026-04-03 21:02:08 +00:00
Nathan EsquenaziandClaude Opus 4.6 6a61f36280 ci: add GitHub Actions workflow for multi-arch Docker + releases
On tag push (v*):
- Creates a GitHub Release with auto-generated release notes
- Builds multi-arch Docker image (linux/amd64, linux/arm64)
- Pushes to ghcr.io/nesquena/hermes-webui with semver tags
- Uses GitHub Actions cache for faster builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:55:41 -07:00