Skip to content

Commit 2f1850a

Browse files
tarekziadeclaude
andcommitted
webapp: OIDC-authorized task status endpoint for the triage reconciler
The transformers-ci nightly triage opens a tracking issue then dispatches one serge task per failure group, and refreshes the issue by polling open PRs. Groups that end `no_fix` open no PR, so their row never updates — the script has no way to learn a task's terminal status (it holds only a GitHub Actions OIDC bearer, and /info requires a web session). Add GET /tasks/{owner}/{repo}/{job_id}/status, authorized the same way as POST /tasks (OIDC bearer, repo claim must match the path). Returns status / result / error so the reconciler can mark no_fix (and error) groups in the tracking issue instead of waiting on a PR that never comes. Serge-side prerequisite only; the transformers-ci reconciler change to consume it is a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a94044 commit 2f1850a

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

reviewbot/webapp.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3573,6 +3573,57 @@ def task_info(request: Request, owner: str, repo: str, job_id: str) -> JSONRespo
35733573
)
35743574

35753575

3576+
@app.get("/tasks/{owner}/{repo}/{job_id}/status")
3577+
def task_status(request: Request, owner: str, repo: str, job_id: str) -> JSONResponse:
3578+
"""Machine-facing task status, authorized by GitHub Actions OIDC (same as
3579+
``POST /tasks``) rather than a web session — so the caller that dispatched a
3580+
task can poll its terminal outcome. The ``/info`` sibling requires a browser
3581+
session; this one accepts the OIDC bearer the dispatcher already holds.
3582+
3583+
Authorized on the token's ``repository`` claim: a run can only read tasks it
3584+
could have dispatched for its own repo. Returns the job ``status``
3585+
(``running``/``published``/``no_fix``/``error``/…), the ``result`` (patch/PR
3586+
details on ``published``), and ``error`` — enough for the triage reconciler
3587+
to mark a group ``no_fix`` in the tracking issue instead of waiting on a PR
3588+
that never comes."""
3589+
if not cfg.task_api_enabled:
3590+
raise HTTPException(status_code=404, detail="tasks_api_disabled")
3591+
3592+
token = _bearer_token(request)
3593+
try:
3594+
claims = verify_token(
3595+
token,
3596+
issuer=cfg.task_oidc_issuer,
3597+
audience=cfg.task_oidc_audience,
3598+
)
3599+
except OIDCError as exc:
3600+
raise HTTPException(status_code=401, detail=f"oidc_verification_failed: {exc}")
3601+
3602+
# The OIDC repository claim is authoritative — the caller may only read tasks
3603+
# for the repo its token was minted for, and it must match the path.
3604+
if claims.repository.lower() != f"{owner}/{repo}".lower():
3605+
raise HTTPException(status_code=403, detail="repo_claim_mismatch")
3606+
3607+
with _jobs_lock:
3608+
job = _jobs.get(job_id)
3609+
if job is None:
3610+
job = _load_job_from_store(job_id)
3611+
if job is None or job.kind != "task":
3612+
raise HTTPException(status_code=404, detail="task_not_found")
3613+
if job.target_owner != owner or job.target_repo != repo:
3614+
raise HTTPException(status_code=404, detail="task_target_mismatch")
3615+
3616+
return JSONResponse(
3617+
{
3618+
"id": job.id,
3619+
"status": job.status,
3620+
"target": f"{job.target_owner}/{job.target_repo}",
3621+
"result": job.task_result,
3622+
"error": job.error,
3623+
}
3624+
)
3625+
3626+
35763627
@app.get("/tasks/{owner}/{repo}/{job_id}/stream")
35773628
async def task_stream(
35783629
request: Request, owner: str, repo: str, job_id: str

tests/test_webapp_tasks.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,70 @@ def fake_submit(fn, job, worker_cfg, req):
206206
self.assertNotIn("skipped", results[1].json())
207207
self.assertTrue(results[2].json().get("skipped"))
208208

209+
def _seed_task_job(self, webapp, *, status="no_fix", repo="acme/widgets"):
210+
owner, name = repo.split("/", 1)
211+
webapp._store.insert_job(
212+
id="task-status-1",
213+
user="octocat",
214+
target_owner=owner,
215+
target_repo=name,
216+
target_number=0,
217+
trigger_comment="fix it",
218+
llm_provider="hf",
219+
llm_api_base=None,
220+
llm_model="m",
221+
created_at=1.0,
222+
status=status,
223+
source="task",
224+
kind="task",
225+
task_spec_json="{}",
226+
)
227+
228+
def test_status_endpoint_oidc_returns_no_fix(self):
229+
if TestClient is None:
230+
self.skipTest("fastapi not installed")
231+
webapp = self._import_webapp()
232+
self._seed_task_job(webapp, status="no_fix")
233+
with patch.object(webapp, "verify_token", return_value=_Claims()):
234+
client = TestClient(webapp.app)
235+
r = client.get(
236+
"/tasks/acme/widgets/task-status-1/status",
237+
headers={"Authorization": "Bearer tok"},
238+
)
239+
self.assertEqual(r.status_code, 200)
240+
body = r.json()
241+
self.assertEqual(body["id"], "task-status-1")
242+
self.assertEqual(body["status"], "no_fix")
243+
self.assertEqual(body["target"], "acme/widgets")
244+
245+
def test_status_endpoint_rejects_foreign_repo_claim(self):
246+
if TestClient is None:
247+
self.skipTest("fastapi not installed")
248+
webapp = self._import_webapp()
249+
self._seed_task_job(webapp, status="no_fix")
250+
# A token minted for a different repo must not read acme/widgets tasks.
251+
with patch.object(
252+
webapp, "verify_token", return_value=_Claims(repository="evil/repo")
253+
):
254+
client = TestClient(webapp.app)
255+
r = client.get(
256+
"/tasks/acme/widgets/task-status-1/status",
257+
headers={"Authorization": "Bearer tok"},
258+
)
259+
self.assertEqual(r.status_code, 403)
260+
261+
def test_status_endpoint_404_when_api_disabled(self):
262+
if TestClient is None:
263+
self.skipTest("fastapi not installed")
264+
webapp = self._import_webapp(task_api_enabled=False)
265+
with patch.object(webapp, "verify_token", return_value=_Claims()):
266+
client = TestClient(webapp.app)
267+
r = client.get(
268+
"/tasks/acme/widgets/task-status-1/status",
269+
headers={"Authorization": "Bearer tok"},
270+
)
271+
self.assertEqual(r.status_code, 404)
272+
209273
def test_inprocess_execution_dispatches_to_worker(self):
210274
if TestClient is None:
211275
self.skipTest("fastapi not installed")

0 commit comments

Comments
 (0)