Files
molecule-ai-workspace-templ…/.gitea/workflows/upstream-sync.yml
T
hongming-ceo-delegatedandClaude Opus 5.5 9fc8f3879f
meta-ci-advisory / meta (advisory) (push) Skipped
publish-image / Resolve runtime version (push) Skipped
publish-image / Classify change (decouple publish from promote) (push) Skipped
publish-image / Build & push workspace-template-hermes image (push) Skipped
publish-image / Promote runtime_image_pins (CP admin) (staging-api.moleculesai.app, staging) (push) Skipped
publish-image / Verify runtime_image_pins moved (fail-loud) (staging-api.moleculesai.app, staging) (push) Skipped
Secret scan / Scan diff for credential-shaped strings (push) Skipped
sync-providers-yaml / Compare synced providers.yaml against controlplane canonical (push) Skipped
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (push) Skipped
sync-providers-yaml / Compare synced providers.yaml against controlplane canonical (pull_request) Skipped
CI / Template validation (static) (push) Successful in 42s
CI / Template validation (static) (pull_request) Successful in 44s
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (pull_request) Successful in 30s
meta-ci-advisory / meta (advisory) (pull_request) Successful in 52s
CI / Shell unit tests (push) Successful in 51s
CI / Shell unit tests (pull_request) Successful in 46s
CI / Adapter-socket conformance (push) Successful in 3m45s
CI / Gateway live round-trip (pinned hermes) (pull_request) Successful in 4m50s
CI / Adapter-socket conformance (pull_request) Successful in 6m10s
CI / Gateway live round-trip (pinned hermes) (push) Successful in 8m45s
CI / Template validation (runtime) (push) Successful in 9m19s
CI / T4 tier-4 conformance (live) (push) Successful in 9m24s
CI / validate (push) Successful in 1s
CI / all-required (push) Successful in 1s
CI / Template validation (runtime) (pull_request) Successful in 10m24s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 10m30s
CI / validate (pull_request) Successful in 1s
CI / all-required (pull_request) Successful in 1s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 3s
fix(image): run the vendored hermes installer, checksum-verified, with no pipe
Every image build of the runtime 0.4.92 bump (#378) failed at

  RUN curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HERMES_COMMIT}/scripts/install.sh" \
        | bash -s -- --skip-setup --commit "${HERMES_COMMIT}" --force-commit

raw.githubusercontent.com answered HTTP 429 (curl: (22) in CI jobs 1054807,
1054808, 1054817, 1054818, on runners on robot-1 and robot-2, 20:26-20:27Z).
The RUN had no pipefail, so its status was bash's: bash ran an empty script
and exited 0. The build then failed at the next step with
"fatal: cannot change to '/home/agent/.hermes/hermes-agent'".

Changes:
- vendor/hermes-agent/install.sh: byte copy of upstream scripts/install.sh at
  HERMES_COMMIT 29112bef (git blob 6f717b01, the blob upstream's tree lists
  for that path at that commit).
- Dockerfile: COPY the copy, check it against the new
  ARG HERMES_INSTALL_SH_SHA256 with `sha256sum -c`, then run it with the same
  arguments (--skip-setup --commit "${HERMES_COMMIT}" --force-commit). No
  download, no pipe. The rev-parse pin assertion after it is unchanged.
- ci.yml gateway-live-roundtrip: new step comparing the copy's git blob id
  with scripts/install.sh at HERMES_COMMIT in the job's existing upstream
  clone. The job is required by validate and all-required.
- tests/test_vendored_hermes_installer.py (added to the conformance job):
  no RUN pipes a download without pipefail; the installer RUN verifies
  ARG HERMES_INSTALL_SH_SHA256 before running the vendored copy; installer
  arguments unchanged; digest and git blob match the copy;
  vendor/hermes-agent/README.md records the same commit as ARG HERMES_COMMIT;
  the CI comparison step exists and stays required.
- README.md, upstream-sync.yml checklist, vendor/hermes-agent/README.md: the
  copy must be refreshed with HERMES_COMMIT, and how.

What the installer installs is unchanged: same script bytes, same arguments,
same user, HOME and WORKDIR. It no longer reads itself from stdin; its only
stdin-dependent branches are `[ -t 0 ]` and /dev/tty probes, and neither a
pipe nor a build step's stdin is a tty.

Verify:
  pytest tests/test_vendored_hermes_installer.py   # 6 passed; 6 failed on origin/main
  git hash-object vendor/hermes-agent/install.sh    # 6f717b011b
  sha256sum vendor/hermes-agent/install.sh          # 85ef536d455e51ab67aa74d79272efd49fe717597dbaadfd3cca179a905f4706

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:32:28 -07:00

217 lines
11 KiB
YAML

name: upstream-sync
# Daily hermes freshness bot (2026-07-22, principal directive: "keep at least
# once a day sync so everything is up to date").
#
# SIMPLIFIED after the fork retirement (same day): the effective hermes is the
# stock upstream code, so "sync" is just: watch PyPI and tell us when to move.
#
# REPURPOSED 2026-09-03 — this bot used to rewrite `ARG HERMES_VERSION` in the
# Dockerfile to PyPI's latest. That ARG is gone. The image now pins an
# immutable upstream git commit (`ARG HERMES_COMMIT`), because the fixes the
# fleet needs exist only as git refs: PyPI's newest hermes-agent is still
# 0.19.0 (2026-07-20). Shipping an unreleased third-party ref is a deliberate,
# owner-authorised exception, and this job is the tripwire that ends it.
#
# So the comparison is now PyPI-latest vs `ARG HERMES_PYPI_FLOOR` — the lowest
# PUBLISHED release that would carry everything HERMES_COMMIT carries. While
# PyPI is below the floor there is nothing to do and the job says so. Once PyPI
# reaches the floor, the bot opens a PR that does NOT pretend to be a
# mechanical bump: returning to a published release means deleting the
# `--commit/--force-commit` pin, restoring a wheel install, and re-verifying
# the vendor-branding neutralizer against the new source. That is a human/agent
# decision, so the PR is a checklist against an unchanged tree, not a rewrite.
#
# Deliberately NOT auto-bumping HERMES_COMMIT to newer git refs: `main` moves
# ~300 commits a week, and every bump is unreviewed third-party code entering
# a customer image. The pin moves when someone decides it should.
#
# NOT a scheduled CI gate: molecule-core's zero-cron ratchet forbids
# timer-driven TEST runs; this job runs no tests and gates nothing — bump PRs
# go through the normal per-PR CI (image build + prebake self-check + adapter
# socket conformance), and humans/agents gate the merge, same contract as the
# runtime .runtime-version propagate bot.
#
# Idempotent: skips when the pin already matches, when the sync branch already
# exists, or when a bump PR is already open. PRs are opened purely via the
# Gitea contents+pulls API (no git clone, so the bot token never lands in an
# on-disk clone URL).
#
# AUTH: RUNTIME_BOT_TOKEN from the Infisical SSOT (/shared/runtime-bot).
# Absent bootstrap creds degrade to report-only; present-but-broken fetch
# fails loudly.
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
permissions:
contents: read
concurrency:
group: upstream-sync
cancel-in-progress: false
jobs:
sync:
name: Check whether PyPI has caught up to the pinned upstream ref
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Fetch RUNTIME_BOT_TOKEN from Infisical SSOT
env:
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
run: |
set -uo pipefail
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
echo "::warning::INFISICAL_CI_* bootstrap creds absent; sync degrades to report-only."
exit 0
fi
BASE="https://key.moleculesai.app"
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
| python3 -c 'import sys,json;d=json.load(sys.stdin);v=d.get("accessToken");print(v if isinstance(v,str) and v else "")')
if [ -z "${TOK}" ]; then
echo "::error::Infisical universal-auth login failed with creds present — real infra fault." >&2
exit 1
fi
BOT=$(curl -fsS "$BASE/api/v3/secrets/raw/RUNTIME_BOT_TOKEN?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=%2Fshared%2Fruntime-bot" \
-H "Authorization: Bearer $TOK" \
| python3 -c 'import sys,json;d=json.load(sys.stdin);v=(d.get("secret") or {}).get("secretValue");print(v if isinstance(v,str) and v else "")')
if [ -z "${BOT}" ]; then
echo "::error::Infisical returned empty RUNTIME_BOT_TOKEN at /shared/runtime-bot — failing loudly." >&2
exit 1
fi
echo "::add-mask::$BOT"
echo "BOT_TOKEN=$BOT" >> "$GITHUB_ENV"
- name: Compare PyPI latest against HERMES_PYPI_FLOOR; open return-to-release PR
env:
BOT_TOKEN: ${{ env.BOT_TOKEN }}
API: ${{ env.GITHUB_SERVER_URL }}/api/v1
REPO: molecule-ai/molecule-ai-workspace-template-hermes
run: |
set -euo pipefail
LATEST=$(curl -fsS --max-time 30 https://pypi.org/pypi/hermes-agent/json \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["info"]["version"])')
FLOOR=$(sed -n 's/^ARG HERMES_PYPI_FLOOR=//p' Dockerfile | head -1)
COMMIT=$(sed -n 's/^ARG HERMES_COMMIT=//p' Dockerfile | head -1)
echo "pypi latest=$LATEST floor=$FLOOR pinned-commit=$COMMIT"
if [ -z "$LATEST" ] || [ -z "$FLOOR" ] || [ -z "$COMMIT" ]; then
echo "::error::could not resolve (latest='$LATEST' floor='$FLOOR' commit='$COMMIT')"; exit 1
fi
# Compare as versions, not strings: "0.9.0" > "0.10.0" lexically.
# packaging is not guaranteed present on the runner, so use the tuple
# of leading integer components — enough for hermes' N.N.N scheme.
if ! python3 -c '
import sys
def key(v):
out = []
for part in v.split("."):
num = ""
for ch in part:
if not ch.isdigit():
break
num += ch
out.append(int(num) if num else 0)
return tuple(out)
sys.exit(0 if key(sys.argv[1]) >= key(sys.argv[2]) else 1)
' "$LATEST" "$FLOOR"; then
echo "PyPI latest $LATEST is still below the floor $FLOOR — the image"
echo "stays on the pinned upstream git commit $COMMIT. Nothing to do."
exit 0
fi
echo "::notice::PyPI has caught up: hermes-agent $LATEST >= floor $FLOOR."
if [ -z "${BOT_TOKEN:-}" ]; then
echo "::warning::report-only (no BOT_TOKEN): hermes-agent $LATEST published;"
echo "::warning::the image can return to a tagged release (pinned commit $COMMIT)."
exit 0
fi
python3 - "$LATEST" "$FLOOR" "$COMMIT" <<'PYEOF'
import json, os, sys, urllib.request
latest, floor, commit = sys.argv[1], sys.argv[2], sys.argv[3]
api = os.environ["API"]; repo = os.environ["REPO"]
tok = os.environ["BOT_TOKEN"]
title = f"hermes-agent {latest} is on PyPI — return the pin to a tagged release"
def req(method, p, body=None):
r = urllib.request.Request(
f"{api}{p}", method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": f"token {tok}",
"Content-Type": "application/json",
# Cloudflare bot-fight bans urllib's default UA
# at the edge (error 1010) — spoof the fleet's
# standard curl UA (see sync-providers-yaml.yml).
"User-Agent": "curl/8.4.0"})
try:
with urllib.request.urlopen(r) as resp:
return resp.status, json.load(resp)
except urllib.error.HTTPError as e:
return e.code, {}
# Idempotent: one open issue per published version. This is a
# NOTIFICATION, not a mechanical bump. Returning to a release is a
# multi-file change with a re-verification step, so the bot files the
# checklist and a human/agent does the work. Opening a PR would mean
# pushing a dummy commit just to have a branch, and would imply the
# change is mechanical when it is not.
st, issues = req("GET", f"/repos/{repo}/issues?state=open&type=issues")
for i in (issues if isinstance(issues, list) else []):
if i.get("title") == title:
print(f"issue #{i.get('number')} already open — skipping")
sys.exit(0)
body = "\n".join([
f"PyPI now publishes hermes-agent **{latest}**, at or above",
f"`ARG HERMES_PYPI_FLOOR={floor}` in the Dockerfile.",
"",
"The image currently pins the unreleased upstream git commit",
f"`{commit}`. See the Dockerfile comment for why: the",
"context-compaction and unattended-approval fixes the fleet needs",
"were git-only when the pin was taken. A published release at or",
"above the floor means that exception can end.",
"",
"This is NOT a one-line bump. To return to a tagged release:",
"",
f"1. Confirm {latest} actually contains the mechanisms the pin was",
" taken for — grep the published sdist for `_MAX_KEEP_TOOL_IMAGES`,",
" `_anti_thrash_recovery_deadline`, `_PRESSURE_KEEP_RECENT_MESSAGES`,",
" `proactive_prune`, and `approvals.unattended_mode`. Do not infer",
" presence from the version number.",
"2. Decide how the wheel becomes the code the gateway RUNS.",
" `~/.local/bin/hermes` execs `$HERMES_ROOT/hermes`, which puts the",
" CHECKOUT at `sys.path[0]` ahead of site-packages — so installing a",
" wheel is not by itself enough. Either keep installing from the",
" checkout (pinned to the release tag's commit) or change the",
" launcher. A wheel pin that only site-packages satisfies is the",
" exact inert pin this workflow was rewritten to stop shipping.",
"3. Re-verify `scripts/neutralize-vendor-branding.py` against the new",
" source, re-capture the fixture in",
" `tests/test_vendor_branding_neutralized.py`, and update",
" `VERIFIED_HERMES_COMMIT`.",
"4. Re-run the breaking-change review: config schema, plugin",
" entry-point classification, approval semantics, state.db schema.",
"5. Update `ARG HERMES_COMMIT`, `ARG HERMES_PYPI_FLOOR`, and the",
" `gateway-live-roundtrip` install line in `.gitea/workflows/ci.yml`.",
" A new commit also needs a refreshed `vendor/hermes-agent/install.sh`",
" and `ARG HERMES_INSTALL_SH_SHA256` (see `vendor/hermes-agent/README.md`).",
"",
"Filed automatically by `.gitea/workflows/upstream-sync.yml`.",
])
st, issue = req("POST", f"/repos/{repo}/issues", {"title": title, "body": body})
assert st in (200, 201), f"issues POST {st}"
print(f"opened issue #{issue.get('number')}: {title}")
PYEOF