From 71c1c453d64f99436d40e8aab78af3a59526501f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 18 Apr 2026 17:08:46 -0700 Subject: [PATCH] Fetch job logs via SSH to indri instead of Forgejo web endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/changelog.d/+runner-logs-auth.feature.md | 2 +- mise-tasks/runner-logs | 45 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/docs/changelog.d/+runner-logs-auth.feature.md b/docs/changelog.d/+runner-logs-auth.feature.md index 7329bc6..9ee6fa1 100644 --- a/docs/changelog.d/+runner-logs-auth.feature.md +++ b/docs/changelog.d/+runner-logs-auth.feature.md @@ -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. diff --git a/mise-tasks/runner-logs b/mise-tasks/runner-logs index dddeaa1..579a5fd 100755 --- a/mise-tasks/runner-logs +++ b/mise-tasks/runner-logs @@ -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: - """Fetch logs for a specific job via the Forgejo web endpoint.""" - 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) - if resp.status_code == 404: + """Fetch logs for a specific job via SSH to indri. + + Forgejo stores action logs as zstd-compressed files on disk at + ~/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( - 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, ) - typer.echo(f"URL: {url}", err=True) 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()