mirror of
https://github.com/0xrsydn/nix-hermes-agent.git
synced 2026-08-07 00:53:52 +00:00
feat: quarantine upstream update candidates
This commit is contained in:
parent
35817ccbf4
commit
5b0336a493
15 changed files with 660 additions and 230 deletions
160
scripts/render-update-report.py
Executable file
160
scripts/render-update-report.py
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Render a small, deterministic compatibility report for an upstream update."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CONTRACT_PATHS = (
|
||||
"hermes_cli",
|
||||
"hermes_cli/config.py",
|
||||
"hermes_constants.py",
|
||||
"skills",
|
||||
"optional-skills",
|
||||
"hermes-tui",
|
||||
"mini-swe-agent",
|
||||
)
|
||||
|
||||
|
||||
def metadata(source: Path | None) -> dict[str, object]:
|
||||
result: dict[str, object] = {
|
||||
"dependencies": set(),
|
||||
"dependency_specs": {},
|
||||
"optional_groups": set(),
|
||||
"scripts": {},
|
||||
"python": "unknown",
|
||||
"config_version": "unknown",
|
||||
"paths": set(),
|
||||
}
|
||||
if source is None or not source.is_dir():
|
||||
return result
|
||||
|
||||
pyproject = source / "pyproject.toml"
|
||||
if pyproject.is_file():
|
||||
try:
|
||||
with pyproject.open("rb") as handle:
|
||||
project = tomllib.load(handle).get("project", {})
|
||||
specs = {
|
||||
dependency_name(item): item for item in project.get("dependencies", [])
|
||||
}
|
||||
result["dependencies"] = set(specs)
|
||||
result["dependency_specs"] = specs
|
||||
result["optional_groups"] = set(project.get("optional-dependencies", {}))
|
||||
result["scripts"] = dict(project.get("scripts", {}))
|
||||
result["python"] = project.get("requires-python", "unknown")
|
||||
except (OSError, tomllib.TOMLDecodeError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
for config_path in (source / "hermes_cli/config.py", source / "hermes_constants.py"):
|
||||
if not config_path.is_file():
|
||||
continue
|
||||
contents = config_path.read_text(encoding="utf-8", errors="replace")
|
||||
matches = re.findall(
|
||||
r'(?:CURRENT_CONFIG_VERSION|["\']_config_version["\'])\s*(?::|=)\s*(\d+)',
|
||||
contents,
|
||||
)
|
||||
if matches:
|
||||
result["config_version"] = matches[-1]
|
||||
break
|
||||
|
||||
result["paths"] = {path for path in CONTRACT_PATHS if (source / path).exists()}
|
||||
return result
|
||||
|
||||
|
||||
def dependency_name(requirement: str) -> str:
|
||||
return re.split(r"[<>=!~;\[ @]", requirement, maxsplit=1)[0].strip().lower()
|
||||
|
||||
|
||||
def changes(before: set[str], after: set[str]) -> str:
|
||||
added = sorted(after - before)
|
||||
removed = sorted(before - after)
|
||||
parts = []
|
||||
if added:
|
||||
parts.append("added: " + ", ".join(f"`{item}`" for item in added))
|
||||
if removed:
|
||||
parts.append("removed: " + ", ".join(f"`{item}`" for item in removed))
|
||||
return "; ".join(parts) if parts else "none"
|
||||
|
||||
|
||||
def dependency_changes(before: dict[str, str], after: dict[str, str]) -> str:
|
||||
summary = changes(set(before), set(after))
|
||||
changed = sorted(name for name in before.keys() & after.keys() if before[name] != after[name])
|
||||
if changed:
|
||||
detail = ", ".join(
|
||||
f"`{name}` (`{before[name]}` → `{after[name]}`)" for name in changed
|
||||
)
|
||||
summary = (summary + "; " if summary != "none" else "") + "changed: " + detail
|
||||
return summary
|
||||
|
||||
|
||||
def render(args: argparse.Namespace) -> str:
|
||||
before = metadata(args.before_source)
|
||||
after = metadata(args.after_source)
|
||||
status_icon = "✅" if args.build_status == "passed" else "❌"
|
||||
lines = [
|
||||
f"## Hermes Agent {args.channel} candidate",
|
||||
"",
|
||||
"This PR is a quarantined upstream candidate. Merge it only after required checks pass.",
|
||||
"",
|
||||
"| Contract | Current | Candidate |",
|
||||
"| --- | --- | --- |",
|
||||
f"| Version | `{args.current_version}` | `{args.candidate_version}` |",
|
||||
f"| Revision | `{args.current_rev}` | `{args.candidate_rev}` |",
|
||||
f"| Python | `{before['python']}` | `{after['python']}` |",
|
||||
f"| Config schema | `{before['config_version']}` | `{after['config_version']}` |",
|
||||
f"| Build validation | — | {status_icon} {args.build_status} |",
|
||||
"",
|
||||
"### Detected interface changes",
|
||||
"",
|
||||
f"- Direct dependencies: {dependency_changes(before['dependency_specs'], after['dependency_specs'])}",
|
||||
f"- Optional dependency groups: {changes(before['optional_groups'], after['optional_groups'])}",
|
||||
f"- CLI entry points: {changes(set(before['scripts']), set(after['scripts']))}",
|
||||
f"- Package/module asset paths: {changes(before['paths'], after['paths'])}",
|
||||
"",
|
||||
]
|
||||
if args.upstream_url:
|
||||
lines.extend((f"Upstream: {args.upstream_url}", ""))
|
||||
if args.build_log and args.build_log.is_file() and args.build_status != "passed":
|
||||
tail = args.build_log.read_text(encoding="utf-8", errors="replace").splitlines()[-80:]
|
||||
lines.extend(
|
||||
(
|
||||
"<details><summary>Validation failure (last 80 lines)</summary>",
|
||||
"",
|
||||
"```text",
|
||||
"\n".join(tail).replace("```", "'''"),
|
||||
"```",
|
||||
"</details>",
|
||||
"",
|
||||
)
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--channel", required=True)
|
||||
parser.add_argument("--current-version", required=True)
|
||||
parser.add_argument("--candidate-version", required=True)
|
||||
parser.add_argument("--current-rev", required=True)
|
||||
parser.add_argument("--candidate-rev", required=True)
|
||||
parser.add_argument("--before-source", type=Path)
|
||||
parser.add_argument("--after-source", type=Path)
|
||||
parser.add_argument("--build-status", choices=("passed", "failed"), required=True)
|
||||
parser.add_argument("--build-log", type=Path)
|
||||
parser.add_argument("--upstream-url")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(render(args), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
scripts/update-common.sh
Executable file
38
scripts/update-common.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set_output() {
|
||||
local name=$1 value=$2
|
||||
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||
printf '%s=%s\n' "$name" "$value" >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_sri_hash() {
|
||||
sed -nE 's/.*got:[[:space:]]*(sha256-[A-Za-z0-9+\/=]+).*/\1/p' "$1" | head -1
|
||||
}
|
||||
|
||||
realize_flake_source() {
|
||||
nix build "$1.src" --no-link --print-out-paths 2>/dev/null | tail -1 || true
|
||||
}
|
||||
|
||||
current_nix_system() {
|
||||
nix eval --impure --raw --expr builtins.currentSystem
|
||||
}
|
||||
|
||||
publish_summary() {
|
||||
local report=$1
|
||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -f "$report" ]]; then
|
||||
cat "$report" >>"$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
}
|
||||
|
||||
# Validation failure is data for the candidate PR, not a reason to discard it.
|
||||
run_validation() {
|
||||
local log_file=$1
|
||||
shift
|
||||
if "$@" >"$log_file" 2>&1; then
|
||||
printf 'passed\n'
|
||||
else
|
||||
printf 'failed\n'
|
||||
fi
|
||||
}
|
||||
|
|
@ -1,102 +1,96 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Auto-update nightly.nix to track HEAD of NousResearch/hermes-agent main branch.
|
||||
# Designed to run in GitHub Actions (see .github/workflows/update-nightly.yml).
|
||||
# Generate a reviewable nightly candidate without pushing to main.
|
||||
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
nightly_file="$repo_root/nightly.nix"
|
||||
report=${HERMES_UPDATE_REPORT:-/tmp/hermes-nightly-update.md}
|
||||
|
||||
log() {
|
||||
printf '>> %s\n' "$*"
|
||||
}
|
||||
# shellcheck source=scripts/update-common.sh
|
||||
source "$repo_root/scripts/update-common.sh"
|
||||
|
||||
# --- Resolve HEAD of main ---
|
||||
log "Fetching HEAD of NousResearch/hermes-agent main branch"
|
||||
head_sha=$(git ls-remote https://github.com/NousResearch/hermes-agent.git refs/heads/main | awk '{print $1}')
|
||||
if [[ -z "$head_sha" ]]; then
|
||||
echo "Failed to resolve HEAD of main" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "HEAD SHA: $head_sha"
|
||||
log() { printf '>> %s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Compare with current ---
|
||||
current_rev=$(awk -F'"' '/pinRev = /{print $2}' "$nightly_file" | head -1)
|
||||
log "Current pinned rev: $current_rev"
|
||||
command -v gh >/dev/null || die "gh is required"
|
||||
command -v python3 >/dev/null || die "python3 is required"
|
||||
|
||||
head_sha=$(gh api /repos/NousResearch/hermes-agent/commits/main --jq '.sha')
|
||||
[[ -n "$head_sha" ]] || die "failed to resolve upstream main"
|
||||
current_rev=$(awk -F'"' '/pinRev = /{print $2; exit}' "$nightly_file")
|
||||
current_version=$(awk -F'"' '/pinVersion = /{print $2; exit}' "$nightly_file")
|
||||
[[ -n "$current_rev" && -n "$current_version" ]] || die "could not read current nightly pin"
|
||||
if [[ "$current_rev" == "$head_sha" ]]; then
|
||||
log "Already up to date ($current_rev). Nothing to do."
|
||||
log "Nightly already tracks $head_sha"
|
||||
set_output update_available false
|
||||
set_output compatible true
|
||||
exit 0
|
||||
fi
|
||||
log "Update available: ${current_rev:0:12} → ${head_sha:0:12}"
|
||||
|
||||
# --- Determine base version from package.nix ---
|
||||
base_version=$(awk -F'"' '/pinVersion \? "/{print $2}' "$repo_root/package.nix" | head -1)
|
||||
if [[ -z "$base_version" ]]; then
|
||||
base_version="0.0.0"
|
||||
fi
|
||||
nightly_version="${base_version}-unstable-$(date -u +%Y-%m-%d)"
|
||||
log "Nightly version: $nightly_version"
|
||||
upstream_version=$(
|
||||
gh api -H "Accept: application/vnd.github.raw+json" \
|
||||
"/repos/NousResearch/hermes-agent/contents/pyproject.toml?ref=${head_sha}" |
|
||||
python3 -c 'import sys, tomllib; print(tomllib.load(sys.stdin.buffer)["project"]["version"])'
|
||||
)
|
||||
python3 - "$upstream_version" <<'PY' || die "upstream pyproject contains an invalid version"
|
||||
import re, sys
|
||||
raise SystemExit(0 if re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", sys.argv[1]) else 1)
|
||||
PY
|
||||
nightly_version="${upstream_version}-unstable-$(date -u +%Y-%m-%d).${head_sha:0:8}"
|
||||
set_output update_available true
|
||||
|
||||
# --- Update nightly.nix with new rev and empty hash ---
|
||||
before_source=$(realize_flake_source ".#hermes-agent-nightly")
|
||||
backup=$(mktemp)
|
||||
build_log=$(mktemp)
|
||||
cp "$nightly_file" "$backup"
|
||||
candidate_ready=false
|
||||
cleanup() {
|
||||
[[ "$candidate_ready" == true ]] || cp "$backup" "$nightly_file"
|
||||
rm -f "$backup" "$build_log"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "Preparing nightly candidate ${current_rev:0:12} -> ${head_sha:0:12}"
|
||||
[[ $(grep -c 'pinVersion = "' "$nightly_file") == 1 ]] || die "unexpected nightly version layout"
|
||||
[[ $(grep -c 'pinRev = "' "$nightly_file") == 1 ]] || die "unexpected nightly revision layout"
|
||||
[[ $(grep -c 'pinHash = "' "$nightly_file") == 1 ]] || die "unexpected nightly hash layout"
|
||||
perl -0pi -e "s|pinVersion = \"[^\"]+\";|pinVersion = \"${nightly_version}\";|" "$nightly_file"
|
||||
perl -0pi -e "s|pinRev = \"[^\"]+\";|pinRev = \"${head_sha}\";|" "$nightly_file"
|
||||
perl -0pi -e 's|pinHash = "[^"]*";|pinHash = "";|' "$nightly_file"
|
||||
perl -0pi -e 's|pinHash = "[^"]*";|pinHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";|' "$nightly_file"
|
||||
grep -Fq "pinRev = \"${head_sha}\";" "$nightly_file" || die "failed to update nightly revision"
|
||||
|
||||
# --- Prefetch to get correct hash ---
|
||||
build_log=$(mktemp)
|
||||
log "Running nix build to compute source hash..."
|
||||
if nix build .#hermes-agent-nightly --accept-flake-config >"$build_log" 2>&1; then
|
||||
log "Build succeeded with empty hash?! Unexpected, but OK."
|
||||
source_hash=""
|
||||
else
|
||||
source_hash=$(grep -oP 'got: *\Ksha256-[A-Za-z0-9+/=]+' "$build_log" | head -1 || true)
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
log "Build failed but couldn't extract hash. Build log:"
|
||||
tail -50 "$build_log" >&2
|
||||
# Restore original
|
||||
git checkout -- "$nightly_file"
|
||||
rm -f "$build_log"
|
||||
exit 1
|
||||
fi
|
||||
if nix build '.#hermes-agent-nightly.src' --no-link --accept-flake-config >"$build_log" 2>&1; then
|
||||
cp "$backup" "$nightly_file"
|
||||
die "the fake source hash unexpectedly succeeded"
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
log "Source hash: $source_hash"
|
||||
|
||||
# Update with the correct hash
|
||||
if [[ -n "$source_hash" ]]; then
|
||||
perl -0pi -e "s|pinHash = \"[^\"]*\";|pinHash = \"${source_hash}\";|" "$nightly_file"
|
||||
source_hash=$(extract_sri_hash "$build_log")
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
cp "$backup" "$nightly_file"
|
||||
tail -80 "$build_log" >&2
|
||||
die "could not extract the candidate source hash"
|
||||
fi
|
||||
perl -0pi -e "s|pinHash = \"[^\"]*\";|pinHash = \"${source_hash}\";|" "$nightly_file"
|
||||
|
||||
# --- Validate build ---
|
||||
build_log=$(mktemp)
|
||||
log "Validating full nightly build..."
|
||||
if ! nix build .#hermes-agent-nightly --accept-flake-config >"$build_log" 2>&1; then
|
||||
log "Build validation FAILED. This likely means dependencies changed upstream."
|
||||
log "Build log (last 100 lines):"
|
||||
tail -100 "$build_log" >&2
|
||||
git checkout -- "$nightly_file"
|
||||
rm -f "$build_log"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
log "Build validation PASSED ✅"
|
||||
after_source=$(realize_flake_source ".#hermes-agent-nightly")
|
||||
system=$(current_nix_system)
|
||||
build_status=$(
|
||||
run_validation "$build_log" nix build ".#legacyPackages.${system}.nightlyChecks.all" --accept-flake-config
|
||||
)
|
||||
|
||||
# --- Commit and push ---
|
||||
if git diff --quiet "$nightly_file"; then
|
||||
log "No changes to commit (shouldn't happen)"
|
||||
exit 0
|
||||
fi
|
||||
source_args=()
|
||||
[[ -n "$before_source" ]] && source_args+=(--before-source "$before_source")
|
||||
[[ -n "$after_source" ]] && source_args+=(--after-source "$after_source")
|
||||
python3 "$repo_root/scripts/render-update-report.py" \
|
||||
--channel nightly \
|
||||
--current-version "$current_version" --candidate-version "$nightly_version" \
|
||||
--current-rev "$current_rev" --candidate-rev "$head_sha" \
|
||||
"${source_args[@]}" \
|
||||
--build-status "$build_status" --build-log "$build_log" \
|
||||
--upstream-url "https://github.com/NousResearch/hermes-agent/commit/${head_sha}" \
|
||||
--output "$report"
|
||||
|
||||
log "Committing nightly update"
|
||||
git add "$nightly_file"
|
||||
git commit -m "🤖 bump hermes-agent-nightly to ${head_sha:0:12} (${nightly_version})" \
|
||||
-m "Upstream HEAD: https://github.com/NousResearch/hermes-agent/commit/${head_sha}" \
|
||||
-m "Tests: nix build .#hermes-agent-nightly (passed)"
|
||||
|
||||
log "Pushing to main"
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
git push origin HEAD:main
|
||||
|
||||
log "Done! Updated hermes-agent-nightly to ${head_sha:0:12} (${nightly_version})"
|
||||
publish_summary "$report"
|
||||
set_output compatible "$([[ "$build_status" == passed ]] && echo true || echo false)"
|
||||
candidate_ready=true
|
||||
log "Candidate prepared; validation: $build_status. The workflow will open or update its PR."
|
||||
|
|
|
|||
|
|
@ -1,157 +1,120 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Auto-update nix-hermes-agent to track latest stable release from NousResearch/hermes-agent.
|
||||
# Designed to run in GitHub Actions (see .github/workflows/update-pins.yml).
|
||||
# Similar to nix-openclaw's update-pins.sh but tracks releases instead of HEAD.
|
||||
# Generate a reviewable stable candidate. The workflow owns commits and PRs;
|
||||
# this script intentionally never mutates main or any remote branch.
|
||||
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
package_file="$repo_root/package.nix"
|
||||
report=${HERMES_UPDATE_REPORT:-/tmp/hermes-stable-update.md}
|
||||
|
||||
log() {
|
||||
printf '>> %s\n' "$*"
|
||||
}
|
||||
# shellcheck source=scripts/update-common.sh
|
||||
source "$repo_root/scripts/update-common.sh"
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "jq is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
log() { printf '>> %s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Resolve latest stable release ---
|
||||
log "Fetching latest release from NousResearch/hermes-agent"
|
||||
# Use the releases endpoint to find the latest non-prerelease
|
||||
release_json=$(gh api /repos/NousResearch/hermes-agent/releases/latest 2>/dev/null || true)
|
||||
if [[ -z "$release_json" ]]; then
|
||||
echo "Failed to fetch latest release" >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v gh >/dev/null || die "gh is required"
|
||||
command -v jq >/dev/null || die "jq is required"
|
||||
command -v python3 >/dev/null || die "python3 is required"
|
||||
|
||||
release_json=$(gh api /repos/NousResearch/hermes-agent/releases/latest)
|
||||
release_tag=$(printf '%s' "$release_json" | jq -r '.tag_name // empty')
|
||||
release_name=$(printf '%s' "$release_json" | jq -r '.name // empty')
|
||||
if [[ -z "$release_tag" ]]; then
|
||||
echo "No release tag found" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Latest release: $release_tag ($release_name)"
|
||||
[[ -n "$release_tag" ]] || die "latest release has no tag"
|
||||
|
||||
# Extract version from release name or tag (e.g. "Hermes Agent v0.3.0 (v2026.3.17)" → "0.3.0")
|
||||
upstream_version=$(printf '%s' "$release_name" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
|
||||
if [[ -z "$upstream_version" ]]; then
|
||||
# Fallback: try tag itself
|
||||
upstream_version=$(printf '%s' "$release_tag" | grep -oP 'v?\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
|
||||
fi
|
||||
if [[ -z "$upstream_version" ]]; then
|
||||
echo "Could not parse version from release tag=$release_tag name=$release_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Parsed version: $upstream_version"
|
||||
log "Resolving immutable commit for $release_tag"
|
||||
tag_object=$(gh api "/repos/NousResearch/hermes-agent/git/ref/tags/${release_tag}")
|
||||
tag_sha=$(printf '%s' "$tag_object" | jq -r '.object.sha // empty')
|
||||
tag_type=$(printf '%s' "$tag_object" | jq -r '.object.type // empty')
|
||||
for _depth in 1 2 3 4 5; do
|
||||
[[ "$tag_type" == "tag" ]] || break
|
||||
tag_object=$(gh api "/repos/NousResearch/hermes-agent/git/tags/${tag_sha}")
|
||||
tag_sha=$(printf '%s' "$tag_object" | jq -r '.object.sha // empty')
|
||||
tag_type=$(printf '%s' "$tag_object" | jq -r '.object.type // empty')
|
||||
done
|
||||
[[ "$tag_type" == "commit" && -n "$tag_sha" ]] || die "release tag does not resolve to a commit"
|
||||
|
||||
# --- Compare with current ---
|
||||
current_version=$(awk -F'"' '/pinVersion \? "/{print $2}' "$package_file" | head -1)
|
||||
log "Current pinned version: $current_version"
|
||||
upstream_version=$(
|
||||
gh api -H "Accept: application/vnd.github.raw+json" \
|
||||
"/repos/NousResearch/hermes-agent/contents/pyproject.toml?ref=${tag_sha}" |
|
||||
python3 -c 'import sys, tomllib; print(tomllib.load(sys.stdin.buffer)["project"]["version"])'
|
||||
)
|
||||
current_version=$(awk -F'"' '/pinVersion \? "/{print $2; exit}' "$package_file")
|
||||
current_rev=$(awk -F'"' '/pinRev \? "/{print $2; exit}' "$package_file")
|
||||
[[ -n "$current_version" && -n "$current_rev" ]] || die "could not read current stable pin"
|
||||
|
||||
if [[ "$current_rev" == "$tag_sha" ]]; then
|
||||
log "Already tracking $release_tag at $tag_sha"
|
||||
set_output update_available false
|
||||
set_output compatible true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$current_version" == "$upstream_version" ]]; then
|
||||
log "Already up to date ($current_version). Nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
log "Update available: $current_version → $upstream_version"
|
||||
|
||||
# --- Resolve the commit SHA for the release tag ---
|
||||
# Properly handle dereferencing annotated tags
|
||||
tag_sha=$(gh api "/repos/NousResearch/hermes-agent/git/ref/tags/${release_tag}" --jq '.object.sha' 2>/dev/null || true)
|
||||
if [[ -n "$tag_sha" ]]; then
|
||||
tag_object_type=$(gh api "/repos/NousResearch/hermes-agent/git/ref/tags/${release_tag}" --jq '.object.type' 2>/dev/null || true)
|
||||
if [[ "$tag_object_type" == "tag" ]]; then
|
||||
log "Tag is annotated, dereferencing to commit SHA..."
|
||||
tag_sha=$(gh api "/repos/NousResearch/hermes-agent/git/tags/${tag_sha}" --jq '.object.sha' 2>/dev/null || true)
|
||||
fi
|
||||
die "release version $upstream_version now points at a different commit; review the retag manually"
|
||||
fi
|
||||
|
||||
if [[ -z "$tag_sha" ]]; then
|
||||
# Last resort: ls-remote
|
||||
tag_sha=$(git ls-remote https://github.com/NousResearch/hermes-agent.git "refs/tags/${release_tag}^{}" | awk '{print $1}' || true)
|
||||
fi
|
||||
python3 - "$current_version" "$upstream_version" <<'PY' || die "refusing an automatic version downgrade"
|
||||
import re, sys
|
||||
def version(value):
|
||||
match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:[-+].*)?", value)
|
||||
if not match:
|
||||
raise SystemExit(2)
|
||||
return tuple(map(int, match.groups()))
|
||||
raise SystemExit(0 if version(sys.argv[2]) >= version(sys.argv[1]) else 1)
|
||||
PY
|
||||
|
||||
if [[ -z "$tag_sha" ]]; then
|
||||
echo "Failed to resolve commit SHA for tag $release_tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Release commit SHA: $tag_sha"
|
||||
|
||||
# --- Prefetch source ---
|
||||
source_url="https://github.com/NousResearch/hermes-agent/archive/${tag_sha}.tar.gz"
|
||||
log "Prefetching source tarball (with submodules via fetchFromGitHub)..."
|
||||
|
||||
# Strategy: use nix to evaluate the hash by building with a fake hash
|
||||
log "Computing fetchFromGitHub hash (with submodules)..."
|
||||
|
||||
# Save original
|
||||
cp "$package_file" "$package_file.bak"
|
||||
|
||||
# Update pinVersion, pinRev, and set pinHash to empty
|
||||
perl -0pi -e "s|pinVersion \\? \"[^\"]+\"|pinVersion \\? \"${upstream_version}\"|" "$package_file"
|
||||
perl -0pi -e "s|pinRev \\? \"[^\"]+\"|pinRev \\? \"${tag_sha}\"|" "$package_file"
|
||||
perl -0pi -e 's|pinHash \? "sha256-[^"]+"|pinHash \? ""|' "$package_file"
|
||||
|
||||
# Build and capture the correct hash from the error
|
||||
set_output update_available true
|
||||
before_source=$(realize_flake_source ".#hermes-agent")
|
||||
backup=$(mktemp)
|
||||
build_log=$(mktemp)
|
||||
log "Running nix build to get correct hash..."
|
||||
if nix build .#hermes-agent --accept-flake-config >"$build_log" 2>&1; then
|
||||
log "Build succeeded with empty hash?! Unexpected, but OK."
|
||||
source_hash=""
|
||||
else
|
||||
# Nix 2.4+ output format
|
||||
source_hash=$(grep -oP 'got: *\Ksha256-[A-Za-z0-9+/=]+' "$build_log" | head -1 || true)
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
# Fallback to older Nix formats or different diagnostic styles
|
||||
source_hash=$(grep -oP 'specified: .*, got: *\Ksha256-[A-Za-z0-9+/=]+' "$build_log" | head -1 || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
log "Build failed but couldn't extract hash. Build log:"
|
||||
tail -50 "$build_log" >&2
|
||||
cp "$package_file.bak" "$package_file"
|
||||
rm -f "$build_log" "$package_file.bak"
|
||||
exit 1
|
||||
fi
|
||||
cp "$package_file" "$backup"
|
||||
candidate_ready=false
|
||||
cleanup() {
|
||||
[[ "$candidate_ready" == true ]] || cp "$backup" "$package_file"
|
||||
rm -f "$backup" "$build_log"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "Preparing stable candidate $current_version -> $upstream_version"
|
||||
[[ $(grep -c 'pinVersion ? "' "$package_file") == 1 ]] || die "unexpected pinVersion layout"
|
||||
[[ $(grep -c 'pinRev ? "' "$package_file") == 1 ]] || die "unexpected pinRev layout"
|
||||
[[ $(grep -c 'pinHash ? "' "$package_file") == 1 ]] || die "unexpected pinHash layout"
|
||||
perl -0pi -e "s|pinVersion \\? \"[^\"]+\"|pinVersion ? \"${upstream_version}\"|" "$package_file"
|
||||
perl -0pi -e "s|pinRev \\? \"[^\"]+\"|pinRev ? \"${tag_sha}\"|" "$package_file"
|
||||
perl -0pi -e 's|pinHash \? "[^"]*"|pinHash ? "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="|' "$package_file"
|
||||
grep -Fq "pinRev ? \"${tag_sha}\"" "$package_file" || die "failed to update stable revision"
|
||||
|
||||
if nix build '.#hermes-agent.src' --no-link --accept-flake-config >"$build_log" 2>&1; then
|
||||
cp "$backup" "$package_file"
|
||||
die "the fake source hash unexpectedly succeeded"
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
log "Source hash: $source_hash"
|
||||
|
||||
# Update with the correct hash
|
||||
if [[ -n "$source_hash" ]]; then
|
||||
perl -0pi -e "s|pinHash \\? \"[^\"]*\"|pinHash \\? \"${source_hash}\"|" "$package_file"
|
||||
source_hash=$(extract_sri_hash "$build_log")
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
cp "$backup" "$package_file"
|
||||
tail -80 "$build_log" >&2
|
||||
die "could not extract the candidate source hash"
|
||||
fi
|
||||
perl -0pi -e "s|pinHash \\? \"[^\"]*\"|pinHash ? \"${source_hash}\"|" "$package_file"
|
||||
|
||||
# --- Validate build ---
|
||||
build_log=$(mktemp)
|
||||
log "Validating full build..."
|
||||
if ! nix build .#hermes-agent --accept-flake-config >"$build_log" 2>&1; then
|
||||
log "Build validation FAILED. This likely means dependencies changed upstream."
|
||||
log "Build log (last 100 lines):"
|
||||
tail -100 "$build_log" >&2
|
||||
cp "$package_file.bak" "$package_file"
|
||||
rm -f "$build_log" "$package_file.bak"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$build_log" "$package_file.bak"
|
||||
log "Build validation PASSED ✅"
|
||||
after_source=$(realize_flake_source ".#hermes-agent")
|
||||
build_status=$(
|
||||
run_validation "$build_log" nix flake check --keep-going --no-write-lock-file --accept-flake-config
|
||||
)
|
||||
|
||||
# --- Commit and push ---
|
||||
if git diff --quiet "$package_file"; then
|
||||
log "No changes to commit (shouldn't happen)"
|
||||
exit 0
|
||||
fi
|
||||
source_args=()
|
||||
[[ -n "$before_source" ]] && source_args+=(--before-source "$before_source")
|
||||
[[ -n "$after_source" ]] && source_args+=(--after-source "$after_source")
|
||||
python3 "$repo_root/scripts/render-update-report.py" \
|
||||
--channel stable \
|
||||
--current-version "$current_version" --candidate-version "$upstream_version" \
|
||||
--current-rev "$current_rev" --candidate-rev "$tag_sha" \
|
||||
"${source_args[@]}" \
|
||||
--build-status "$build_status" --build-log "$build_log" \
|
||||
--upstream-url "https://github.com/NousResearch/hermes-agent/releases/tag/${release_tag}" \
|
||||
--output "$report"
|
||||
|
||||
log "Committing update"
|
||||
git add "$package_file"
|
||||
git commit -m "🤖 bump hermes-agent ${current_version} → ${upstream_version} (${release_tag})" \
|
||||
-m "Upstream: https://github.com/NousResearch/hermes-agent/releases/tag/${release_tag}" \
|
||||
-m "Tests: nix build .#hermes-agent (passed)"
|
||||
|
||||
log "Pushing to main"
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
git push origin HEAD:main
|
||||
|
||||
log "Done! Updated hermes-agent to ${upstream_version} (${release_tag})"
|
||||
publish_summary "$report"
|
||||
set_output compatible "$([[ "$build_status" == passed ]] && echo true || echo false)"
|
||||
candidate_ready=true
|
||||
log "Candidate prepared; validation: $build_status. The workflow will open or update its PR."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue