Fetch job logs via SSH to indri instead of Forgejo web endpoint

Forgejo's web action routes don't support API token auth for private
repos (only session cookies or public access). Switch log fetching to
read the zstd-compressed log files directly from indri via SSH —
Forgejo stores all runner logs on disk regardless of which runner
executed the job.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-04-18 17:08:46 -07:00
commit 71c1c453d6
2 changed files with 38 additions and 9 deletions

View file

@ -1 +1 @@
runner-logs now authenticates with Forgejo API token (works on private repos) and auto-detects the repo from git remote. runner-logs now authenticates with Forgejo API token and auto-detects the repo from git remote. Job logs are fetched via SSH to indri (reading Forgejo's on-disk zstd log files) instead of the web endpoint, which doesn't support token auth for private repos.

View file

@ -203,18 +203,47 @@ def show_jobs(run_number: int, repo: str, token: str, console: Console) -> None:
def fetch_log(run_number: int, job_index: int, repo: str, token: str) -> None: def fetch_log(run_number: int, job_index: int, repo: str, token: str) -> None:
"""Fetch logs for a specific job via the Forgejo web endpoint.""" """Fetch logs for a specific job via SSH to indri.
url = f"{FORGE_URL}/{repo}/actions/runs/{run_number}/jobs/{job_index}/attempt/1/logs"
resp = httpx.get(url, headers=auth_headers(token), timeout=30, follow_redirects=True) Forgejo stores action logs as zstd-compressed files on disk at
if resp.status_code == 404: ~/forgejo/data/actions_log/{owner}/{repo}/{hex_prefix}/{task_id}.log.zst
regardless of which runner executed the job. The web log endpoint doesn't
support API-token auth for private repos, so we read the files directly.
"""
tasks = fetch_tasks(repo, token)
jobs = sorted(
[t for t in tasks if t["run_number"] == run_number],
key=lambda x: x["id"],
)
if not jobs:
typer.echo(f"Error: No jobs found for run #{run_number}", err=True)
raise typer.Exit(1)
if job_index < 0 or job_index >= len(jobs):
typer.echo( typer.echo(
f"Error: No logs found for run #{run_number} job {job_index}", f"Error: job index {job_index} out of range (run #{run_number} has {len(jobs)} jobs)",
err=True, err=True,
) )
typer.echo(f"URL: {url}", err=True)
raise typer.Exit(1) raise typer.Exit(1)
resp.raise_for_status()
sys.stdout.write(resp.text) task_id = jobs[job_index]["id"]
hex_prefix = f"{task_id & 0xff:02x}"
log_path = f"~/forgejo/data/actions_log/{repo}/{hex_prefix}/{task_id}.log.zst"
result = subprocess.run(
["ssh", "indri", f"zstdcat {log_path}"],
capture_output=True,
text=True,
)
if result.returncode != 0:
typer.echo(
f"Error: could not read log for run #{run_number} job {job_index} (task {task_id})",
err=True,
)
typer.echo(f"Path: indri:{log_path}", err=True)
if result.stderr.strip():
typer.echo(result.stderr.strip(), err=True)
raise typer.Exit(1)
sys.stdout.write(result.stdout)
@app.command() @app.command()