ship grants, a2a_client, discovery, sandbox SDK + tests
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -204,16 +205,6 @@ def init(
|
||||
"a2a.yaml.tmpl", name=name, class_name=class_name
|
||||
),
|
||||
"requirements.txt": _render_template("requirements.txt.tmpl"),
|
||||
"Dockerfile": _render_template(
|
||||
"Dockerfile.tmpl", entrypoint=f"agent:{class_name}"
|
||||
),
|
||||
".dockerignore": _render_template("dockerignore.tmpl"),
|
||||
".gitea/workflows/build.yml": _render_template(
|
||||
"workflow.yml.tmpl", name=name
|
||||
),
|
||||
"deploy/20-deployment.yaml": _render_template(
|
||||
"deployment.yaml.tmpl", name=name
|
||||
),
|
||||
}
|
||||
for relpath, content in files.items():
|
||||
target_path = project / relpath
|
||||
@@ -323,60 +314,71 @@ def build(
|
||||
console.print(f"[green]pushed[/] [cyan]{image}[/]")
|
||||
|
||||
|
||||
def _git_push_source(project: Path, push_url: str) -> None:
|
||||
"""Initialize the project as a git repo (if needed) and push to ``push_url``.
|
||||
def _make_tarball(project: Path) -> bytes:
|
||||
"""Tar.gz the user's project directory.
|
||||
|
||||
Idempotent: re-running on an existing repo just commits any changes
|
||||
and pushes.
|
||||
Excludes platform/dev artifacts so build images stay small. The
|
||||
platform stamps in Dockerfile/workflow/manifests on the server side.
|
||||
"""
|
||||
git_dir = project / ".git"
|
||||
if not git_dir.exists():
|
||||
_run(["git", "-C", str(project), "init", "-q", "-b", "main"])
|
||||
_run(["git", "-C", str(project), "config", "user.email", "agent@a2a.local"])
|
||||
_run(["git", "-C", str(project), "config", "user.name", "agent"])
|
||||
# Ensure remote
|
||||
have_remote = subprocess.run(
|
||||
["git", "-C", str(project), "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if have_remote:
|
||||
_run(["git", "-C", str(project), "remote", "set-url", "origin", push_url])
|
||||
else:
|
||||
_run(["git", "-C", str(project), "remote", "add", "origin", push_url])
|
||||
_run(["git", "-C", str(project), "add", "-A"])
|
||||
status = subprocess.run(
|
||||
["git", "-C", str(project), "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
if status:
|
||||
_run(["git", "-C", str(project), "commit", "-q", "-m", "deploy"])
|
||||
# Pull --rebase first to integrate any auto-bump commits the runner pushed back.
|
||||
subprocess.run(
|
||||
["git", "-C", str(project), "pull", "--rebase", "origin", "main"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
_run(["git", "-C", str(project), "push", "-u", "origin", "main"])
|
||||
import io
|
||||
import tarfile
|
||||
|
||||
excluded_dirs = {
|
||||
"__pycache__",
|
||||
".venv",
|
||||
".git",
|
||||
".pytest_cache",
|
||||
".mypy_cache",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
".gitea",
|
||||
"deploy",
|
||||
}
|
||||
excluded_files = {"Dockerfile", ".dockerignore"}
|
||||
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
for root, dirs, files in os.walk(project):
|
||||
dirs[:] = [d for d in dirs if d not in excluded_dirs]
|
||||
for fname in files:
|
||||
if fname in excluded_files or fname.endswith(".pyc"):
|
||||
continue
|
||||
fpath = Path(root) / fname
|
||||
arcname = fpath.relative_to(project)
|
||||
tar.add(fpath, arcname=str(arcname))
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _wait_for_url(url: str, timeout: int = 180) -> bool:
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with httpx.Client(timeout=3.0) as c:
|
||||
if c.get(f"{url}/healthz").status_code == 200:
|
||||
return True
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
time.sleep(4)
|
||||
return False
|
||||
|
||||
|
||||
@app.command()
|
||||
def deploy(
|
||||
project: Path = typer.Option(Path("."), "--project", "-p"),
|
||||
public: bool | None = typer.Option(None, "--public/--private"),
|
||||
wait: bool = typer.Option(True, "--wait/--no-wait", help="Poll until URL is live"),
|
||||
api: str | None = typer.Option(None, "--api", help="Override control plane URL"),
|
||||
) -> None:
|
||||
"""Push source to gitea; the platform builds + deploys.
|
||||
"""Ship the agent.
|
||||
|
||||
No local docker. The CLI:
|
||||
1. asks the control plane to provision a gitea repo + ArgoCD app
|
||||
2. ``git push``es the project source to that repo
|
||||
The Gitea Actions runner builds the image and ArgoCD reconciles
|
||||
``deploy/`` onto the cluster. Returns the public URL once registered.
|
||||
Tarballs your source, uploads to the control plane, and prints the
|
||||
URL when it's live. No local docker. No git. No knowledge of how the
|
||||
platform builds or deploys.
|
||||
"""
|
||||
creds = credentials.load()
|
||||
if creds is None:
|
||||
@@ -389,33 +391,29 @@ def deploy(
|
||||
)
|
||||
description = cfg.get("description", cls.description or "")
|
||||
|
||||
if not (project / "deploy").exists() or not (project / ".gitea/workflows/build.yml").exists():
|
||||
_fail(
|
||||
"project missing deploy/ or .gitea/workflows/. "
|
||||
"Re-run `a2a init` or upgrade the scaffold."
|
||||
)
|
||||
console.print("[dim]packaging source...[/]")
|
||||
tarball = _make_tarball(project)
|
||||
size_kb = len(tarball) / 1024
|
||||
console.print(f"[dim]uploading {size_kb:.1f}KB to {credentials.resolve_api_url(api)}...[/]")
|
||||
|
||||
client = _client(api)
|
||||
console.print(f"[dim]asking control plane to provision repo + argo app...[/]")
|
||||
try:
|
||||
prov = client.from_source(
|
||||
out = client.from_tarball(
|
||||
name=cls.name,
|
||||
description=description,
|
||||
version=cfg["version"],
|
||||
entrypoint=cfg["entrypoint"],
|
||||
description=description,
|
||||
public=is_public,
|
||||
tarball=tarball,
|
||||
)
|
||||
except ApiError as exc:
|
||||
_fail(str(exc))
|
||||
|
||||
console.print(f"[dim]pushing source → {prov['repo_url']}[/]")
|
||||
_git_push_source(project, prov["push_url"])
|
||||
|
||||
summary = {
|
||||
"agent": prov["name"],
|
||||
"repo": prov["repo_url"],
|
||||
"url": prov.get("expected_url"),
|
||||
"card": f"{prov['expected_url']}/.well-known/agent-card" if prov.get("expected_url") else None,
|
||||
"next": "the runner is building. `a2a agents` to check status, or curl the url in ~30s",
|
||||
"agent": out["name"],
|
||||
"version": out["version"],
|
||||
"status": out["status"],
|
||||
"url": out.get("url"),
|
||||
}
|
||||
console.print(
|
||||
Panel.fit(
|
||||
@@ -424,6 +422,17 @@ def deploy(
|
||||
)
|
||||
)
|
||||
|
||||
url = out.get("url")
|
||||
if wait and url:
|
||||
console.print(f"[dim]waiting for {url} ...[/]")
|
||||
ready = _wait_for_url(url)
|
||||
if ready:
|
||||
console.print(f"[green]live[/]: {url}")
|
||||
else:
|
||||
console.print(
|
||||
f"[yellow]still building[/]; check `a2a agents` or curl {url} in a bit"
|
||||
)
|
||||
|
||||
|
||||
# Used as `python -m a2a_pack.cli.main`
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
Reference in New Issue
Block a user