mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
feat(skills): add cloudflare-temporary-deploy optional skill (#50849)
* chore: re-trigger CI (workflows did not dispatch on prior head) * feat(skills): add cloudflare-temporary-deploy optional skill Optional web-development skill teaching the agent to deploy a Worker to a live workers.dev URL with no Cloudflare account via 'wrangler deploy --temporary' (Wrangler 4.102.0+). Cloudflare provisions a throwaway, claimable account valid for 60 minutes — ideal for an autonomous write->deploy->verify loop with no OAuth/signup hard stop. - SKILL.md: when/when-not, prereqs (unauth requirement, version floor), step-by-step deploy + verify flow, product limits table, pitfalls (hidden flag, stale global wrangler, auth-present error, rate limits, workers.dev edge cache), verification. - scripts/parse_deploy_output.py: stdlib-only parser extracting live URL, claim URL, account name/state, expiry, deploy status from wrangler output. - tests/skills/test_cloudflare_temporary_deploy_skill.py: 16 tests incl. a real-output regression case. Verified live end-to-end: temporary account created with no creds, deployed to a live URL, curl confirmed body, redeploy reused the account.
This commit is contained in:
parent
7dece1d933
commit
ff08e60c63
3 changed files with 413 additions and 0 deletions
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Parse `wrangler deploy --temporary` output into structured JSON.
|
||||
|
||||
Reads wrangler's stdout/stderr from STDIN and extracts the live workers.dev
|
||||
URL, the claim URL, the temporary account name/state, the claim window, and
|
||||
whether a deploy actually happened. Stdlib only — no dependencies.
|
||||
|
||||
Usage:
|
||||
npx wrangler@latest deploy --temporary 2>&1 | python3 parse_deploy_output.py
|
||||
python3 parse_deploy_output.py --selftest
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Match the live workers.dev URL (subdomain.subdomain.workers.dev).
|
||||
_LIVE_URL = re.compile(r"https://[A-Za-z0-9._-]+\.workers\.dev\S*")
|
||||
# Match the claim URL. Cloudflare uses dash.cloudflare.com/claim-preview?claimToken=...
|
||||
# Keep it broad enough to survive minor path changes while still requiring a claim token.
|
||||
_CLAIM_URL = re.compile(r"https://\S*claim\S*claimToken=\S+", re.IGNORECASE)
|
||||
# "Account: Serene Temple (created)" / "Account: example-name (reused)"
|
||||
# Account names can contain spaces (e.g. "Serene Temple"), so capture everything
|
||||
# up to the trailing "(state)" marker rather than a single token.
|
||||
_ACCOUNT = re.compile(
|
||||
r"Account:\s*(?P<name>.+?)\s*\((?P<state>created|reused)\)", re.IGNORECASE
|
||||
)
|
||||
# "Claim within: 60 minutes"
|
||||
_CLAIM_WITHIN = re.compile(r"Claim within:\s*(?P<minutes>\d+)\s*minutes?", re.IGNORECASE)
|
||||
# A successful deploy prints a "Deployed" / "Uploaded" line.
|
||||
_DEPLOYED = re.compile(r"^\s*(Deployed|Uploaded)\b", re.IGNORECASE | re.MULTILINE)
|
||||
|
||||
|
||||
def _first(pattern: re.Pattern, text: str) -> str | None:
|
||||
m = pattern.search(text)
|
||||
if not m:
|
||||
return None
|
||||
# Strip trailing punctuation that often clings to a URL in log lines.
|
||||
return m.group(0).rstrip(".,);]")
|
||||
|
||||
|
||||
def parse(text: str) -> dict:
|
||||
"""Extract deploy facts from wrangler output text."""
|
||||
account = _ACCOUNT.search(text)
|
||||
claim_within = _CLAIM_WITHIN.search(text)
|
||||
return {
|
||||
"live_url": _first(_LIVE_URL, text),
|
||||
"claim_url": _first(_CLAIM_URL, text),
|
||||
"account": account.group("name") if account else None,
|
||||
"account_state": account.group("state").lower() if account else None,
|
||||
"expires_minutes": int(claim_within.group("minutes")) if claim_within else None,
|
||||
"deployed": bool(_DEPLOYED.search(text)),
|
||||
}
|
||||
|
||||
|
||||
_SAMPLE = """\
|
||||
Continuing means you accept Cloudflare's Terms of Service and Privacy Policy.
|
||||
|
||||
Temporary account ready:
|
||||
Account: example-name (created)
|
||||
Claim within: 60 minutes
|
||||
Claim URL: https://dash.cloudflare.com/claim-preview?claimToken=abc123XYZ
|
||||
|
||||
Uploaded example-worker
|
||||
Deployed example-worker triggers
|
||||
https://example-worker.example-name.workers.dev
|
||||
"""
|
||||
|
||||
_SAMPLE_REUSED = """\
|
||||
Temporary account ready:
|
||||
Account: example-name (reused)
|
||||
Claim within: 42 minutes
|
||||
Claim URL: https://dash.cloudflare.com/claim-preview?claimToken=def456
|
||||
Deployed example-worker triggers
|
||||
https://example-worker.example-name.workers.dev
|
||||
"""
|
||||
|
||||
_SAMPLE_NO_TEMP = """\
|
||||
✘ [ERROR] You are not logged in.
|
||||
|
||||
To continue without logging in, rerun this command with `--temporary`.
|
||||
"""
|
||||
|
||||
|
||||
def _selftest() -> int:
|
||||
r = parse(_SAMPLE)
|
||||
assert r["live_url"] == "https://example-worker.example-name.workers.dev", r
|
||||
assert r["claim_url"] == "https://dash.cloudflare.com/claim-preview?claimToken=abc123XYZ", r
|
||||
assert r["account"] == "example-name", r
|
||||
assert r["account_state"] == "created", r
|
||||
assert r["expires_minutes"] == 60, r
|
||||
assert r["deployed"] is True, r
|
||||
|
||||
r2 = parse(_SAMPLE_REUSED)
|
||||
assert r2["account_state"] == "reused", r2
|
||||
assert r2["expires_minutes"] == 42, r2
|
||||
assert r2["deployed"] is True, r2
|
||||
|
||||
r3 = parse(_SAMPLE_NO_TEMP)
|
||||
assert r3["live_url"] is None, r3
|
||||
assert r3["claim_url"] is None, r3
|
||||
assert r3["account"] is None, r3
|
||||
assert r3["deployed"] is False, r3
|
||||
|
||||
print("selftest: OK")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if "--selftest" in argv:
|
||||
return _selftest()
|
||||
text = sys.stdin.read()
|
||||
result = parse(text)
|
||||
print(json.dumps(result, indent=2))
|
||||
# Non-zero exit if no live URL was found, so callers can branch on it.
|
||||
return 0 if result["live_url"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Loading…
Add table
Add a link
Reference in a new issue