267 lines
9.0 KiB
Bash
Executable File
267 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ── Workflow Control Helpers ───────────────────────────────────────────
|
|
# Source this in the controls pane: source ~/.hermes/bin/ops-helpers.sh
|
|
# These helpers intentionally target the current Hermes + Gitea workflow
|
|
# and do not revive deprecated bash worker loops.
|
|
# ───────────────────────────────────────────────────────────────────────
|
|
|
|
export GITEA="${GITEA:-http://143.198.27.163:3000}"
|
|
export OPS_DEFAULT_REPO="${OPS_DEFAULT_REPO:-Timmy_Foundation/timmy-home}"
|
|
export OPS_CORE_REPOS="${OPS_CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
|
|
|
|
ops-token() {
|
|
local token_file
|
|
for token_file in "$HOME/.hermes/gitea_token_vps" "$HOME/.config/gitea/token" "$HOME/.config/gitea/codex-token"; do
|
|
if [ -f "$token_file" ]; then
|
|
cat "$token_file"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
ops-help() {
|
|
echo ""
|
|
echo -e "\033[1m\033[35m ◈ WORKFLOW CONTROLS\033[0m"
|
|
echo -e "\033[2m ──────────────────────────────────────\033[0m"
|
|
echo ""
|
|
echo -e " \033[1mReview\033[0m"
|
|
echo " ops-prs [repo] List open PRs across the core repos or one repo"
|
|
echo " ops-review-queue Show PRs waiting on Timmy or Allegro"
|
|
echo " ops-merge PR REPO Squash-merge a reviewed PR"
|
|
echo ""
|
|
echo -e " \033[1mDispatch\033[0m"
|
|
echo " ops-assign ISSUE AGENT [repo] Assign an issue to an agent"
|
|
echo " ops-unassign ISSUE [repo] Remove all assignees from an issue"
|
|
echo " ops-queue AGENT [repo|all] Show an agent's queue"
|
|
echo " ops-unassigned [repo|all] Show unassigned issues"
|
|
echo ""
|
|
echo -e " \033[1mWorkflow Health\033[0m"
|
|
echo " ops-gitea-feed Render the Gitea workflow feed"
|
|
echo " ops-freshness Check Hermes session/export freshness"
|
|
echo ""
|
|
echo -e " \033[1mShortcuts\033[0m"
|
|
echo " ops-assign-allegro ISSUE [repo]"
|
|
echo " ops-assign-codex ISSUE [repo]"
|
|
echo " ops-assign-groq ISSUE [repo]"
|
|
echo " ops-assign-claude ISSUE [repo]"
|
|
echo " ops-assign-ezra ISSUE [repo]"
|
|
echo ""
|
|
}
|
|
|
|
ops-python() {
|
|
local token
|
|
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
|
|
OPS_TOKEN="$token" python3 - "$@"
|
|
}
|
|
|
|
ops-prs() {
|
|
local target="${1:-all}"
|
|
ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
base = sys.argv[1].rstrip("/")
|
|
repos = sys.argv[2].split()
|
|
target = sys.argv[3]
|
|
token = os.environ["OPS_TOKEN"]
|
|
headers = {"Authorization": f"token {token}"}
|
|
|
|
if target != "all":
|
|
repos = [target]
|
|
|
|
pulls = []
|
|
for repo in repos:
|
|
req = urllib.request.Request(
|
|
f"{base}/api/v1/repos/{repo}/pulls?state=open&limit=20",
|
|
headers=headers,
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
for pr in json.loads(resp.read().decode()):
|
|
pr["_repo"] = repo
|
|
pulls.append(pr)
|
|
|
|
if not pulls:
|
|
print(" (none)")
|
|
else:
|
|
for pr in pulls:
|
|
print(f" #{pr['number']:4d} {pr['_repo'].split('/', 1)[1]:12s} {pr['user']['login'][:12]:12s} {pr['title'][:60]}")
|
|
PY
|
|
}
|
|
|
|
ops-review-queue() {
|
|
ops-python "$GITEA" "$OPS_CORE_REPOS" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
base = sys.argv[1].rstrip("/")
|
|
repos = sys.argv[2].split()
|
|
token = os.environ["OPS_TOKEN"]
|
|
headers = {"Authorization": f"token {token}"}
|
|
|
|
items = []
|
|
for repo in repos:
|
|
req = urllib.request.Request(
|
|
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls",
|
|
headers=headers,
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
for item in json.loads(resp.read().decode()):
|
|
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
|
|
if any(name in assignees for name in ("Timmy", "allegro")):
|
|
item["_repo"] = repo
|
|
items.append(item)
|
|
|
|
if not items:
|
|
print(" (clear)")
|
|
else:
|
|
for item in items:
|
|
names = ",".join(a.get("login", "") for a in (item.get("assignees") or []))
|
|
print(f" #{item['number']:4d} {item['_repo'].split('/', 1)[1]:12s} {names[:20]:20s} {item['title'][:56]}")
|
|
PY
|
|
}
|
|
|
|
ops-assign() {
|
|
local issue="$1"
|
|
local agent="$2"
|
|
local repo="${3:-$OPS_DEFAULT_REPO}"
|
|
local token
|
|
[ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; }
|
|
[ -z "$agent" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; }
|
|
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
|
|
curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \
|
|
"$GITEA/api/v1/repos/$repo/issues/$issue" -d "{\"assignees\":[\"$agent\"]}" | python3 -c "
|
|
import json,sys
|
|
d=json.loads(sys.stdin.read())
|
|
names=','.join(a.get('login','') for a in (d.get('assignees') or []))
|
|
print(f' ✓ #{d.get(\"number\", \"?\")} assigned to {names or \"(none)\"}')
|
|
" 2>/dev/null
|
|
}
|
|
|
|
ops-unassign() {
|
|
local issue="$1"
|
|
local repo="${2:-$OPS_DEFAULT_REPO}"
|
|
local token
|
|
[ -z "$issue" ] && { echo "Usage: ops-unassign ISSUE_NUMBER [owner/repo]"; return 1; }
|
|
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
|
|
curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \
|
|
"$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":[]}' | python3 -c "
|
|
import json,sys
|
|
d=json.loads(sys.stdin.read())
|
|
print(f' ✓ #{d.get(\"number\", \"?\")} unassigned')
|
|
" 2>/dev/null
|
|
}
|
|
|
|
ops-queue() {
|
|
local agent="$1"
|
|
local target="${2:-all}"
|
|
[ -z "$agent" ] && { echo "Usage: ops-queue AGENT [repo|all]"; return 1; }
|
|
ops-python "$GITEA" "$OPS_CORE_REPOS" "$agent" "$target" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
base = sys.argv[1].rstrip("/")
|
|
repos = sys.argv[2].split()
|
|
agent = sys.argv[3]
|
|
target = sys.argv[4]
|
|
token = os.environ["OPS_TOKEN"]
|
|
headers = {"Authorization": f"token {token}"}
|
|
|
|
if target != "all":
|
|
repos = [target]
|
|
|
|
rows = []
|
|
for repo in repos:
|
|
req = urllib.request.Request(
|
|
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues",
|
|
headers=headers,
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
for issue in json.loads(resp.read().decode()):
|
|
assignees = [a.get("login", "") for a in (issue.get("assignees") or [])]
|
|
if agent in assignees:
|
|
rows.append((repo, issue["number"], issue["title"]))
|
|
|
|
if not rows:
|
|
print(" (empty)")
|
|
else:
|
|
for repo, number, title in rows:
|
|
print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}")
|
|
PY
|
|
}
|
|
|
|
ops-unassigned() {
|
|
local target="${1:-all}"
|
|
ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
base = sys.argv[1].rstrip("/")
|
|
repos = sys.argv[2].split()
|
|
target = sys.argv[3]
|
|
token = os.environ["OPS_TOKEN"]
|
|
headers = {"Authorization": f"token {token}"}
|
|
|
|
if target != "all":
|
|
repos = [target]
|
|
|
|
rows = []
|
|
for repo in repos:
|
|
req = urllib.request.Request(
|
|
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues",
|
|
headers=headers,
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
for issue in json.loads(resp.read().decode()):
|
|
if not issue.get("assignees"):
|
|
rows.append((repo, issue["number"], issue["title"]))
|
|
|
|
if not rows:
|
|
print(" (none)")
|
|
else:
|
|
for repo, number, title in rows[:20]:
|
|
print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}")
|
|
if len(rows) > 20:
|
|
print(f" ... +{len(rows) - 20} more")
|
|
PY
|
|
}
|
|
|
|
ops-merge() {
|
|
local pr="$1"
|
|
local repo="${2:-$OPS_DEFAULT_REPO}"
|
|
local token
|
|
[ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER [owner/repo]"; return 1; }
|
|
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
|
|
curl -s -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \
|
|
"$GITEA/api/v1/repos/$repo/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c "
|
|
import json,sys
|
|
d=json.loads(sys.stdin.read())
|
|
if 'sha' in d:
|
|
print(f' ✓ PR merged ({d[\"sha\"][:8]})')
|
|
else:
|
|
print(f' ✗ {d.get(\"message\", \"unknown error\")}')
|
|
" 2>/dev/null
|
|
}
|
|
|
|
ops-gitea-feed() {
|
|
bash "$HOME/.hermes/bin/ops-gitea.sh"
|
|
}
|
|
|
|
ops-freshness() {
|
|
bash "$HOME/.hermes/bin/pipeline-freshness.sh"
|
|
}
|
|
|
|
ops-assign-allegro() { ops-assign "$1" "allegro" "${2:-$OPS_DEFAULT_REPO}"; }
|
|
ops-assign-codex() { ops-assign "$1" "codex-agent" "${2:-$OPS_DEFAULT_REPO}"; }
|
|
ops-assign-groq() { ops-assign "$1" "groq" "${2:-$OPS_DEFAULT_REPO}"; }
|
|
ops-assign-claude() { ops-assign "$1" "claude" "${2:-$OPS_DEFAULT_REPO}"; }
|
|
ops-assign-ezra() { ops-assign "$1" "ezra" "${2:-$OPS_DEFAULT_REPO}"; }
|