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
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 blob6f717b01, 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 #6f717b011bsha256sum vendor/hermes-agent/install.sh # 85ef536d455e51ab67aa74d79272efd49fe717597dbaadfd3cca179a905f4706 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
217 lines
11 KiB
YAML
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
|