55import click
66
77from sp_cli .client import ApiError
8+ from sp_cli .constants import (CANCEL_REASON_MIN_LENGTH , PLATFORMS ,
9+ RUN_STATUSES , SAMPLE_STATUSES )
810from sp_cli .output import render , render_error
911from sp_cli .runner import clean_params , fetch_and_render
1012from sp_cli .triage import classify_sample , is_failure
@@ -16,18 +18,34 @@ def run() -> None:
1618
1719
1820@run .command ('ls' )
19- @click .option ('--status' , default = None , help = 'queued|running|pass|fail|canceled|error|incomplete' )
20- @click .option ('--platform' , default = None , help = 'linux|windows' )
21+ @click .option ('--status' , type = click .Choice (RUN_STATUSES ), default = None ,
22+ help = 'Filter by lifecycle status. The API only tracks these three.' )
23+ @click .option ('--platform' , type = click .Choice (PLATFORMS ), default = None , help = 'Test platform.' )
2124@click .option ('--branch' , default = None , help = 'Filter by branch name.' )
2225@click .option ('--commit' , 'commit_sha' , default = None , help = 'Full 40-char commit SHA.' )
26+ @click .option ('--repository' , default = None , help = 'Filter by fork, as owner/repo.' )
27+ @click .option ('--sort' , default = None , help = 'Sort key, e.g. -created_at (default) or run_id.' )
28+ @click .option ('--created-after' , 'created_after' , default = None ,
29+ help = 'Only runs first seen at/after this time (ISO 8601).' )
30+ @click .option ('--created-before' , 'created_before' , default = None ,
31+ help = 'Only runs first seen at/before this time (ISO 8601).' )
2332@click .option ('--limit' , type = int , default = None , help = 'Page size (max 100).' )
2433@click .option ('--offset' , type = int , default = None , help = 'Pagination offset.' )
2534@click .pass_context
2635def run_ls (ctx : click .Context , status : Optional [str ], platform : Optional [str ], branch : Optional [str ],
27- commit_sha : Optional [str ], limit : Optional [int ], offset : Optional [int ]) -> None :
28- """List CI runs (newest first)."""
36+ commit_sha : Optional [str ], repository : Optional [str ], sort : Optional [str ],
37+ created_after : Optional [str ], created_before : Optional [str ],
38+ limit : Optional [int ], offset : Optional [int ]) -> None :
39+ """List CI runs (newest first).
40+
41+ --status only accepts queued, running, and canceled: the API derives them
42+ from the latest TestProgress row, and pass/fail are per-sample outcomes
43+ rather than run states. Use `sp run summary` for a run's pass/fail split.
44+ """
2945 params = clean_params ({'status' : status , 'platform' : platform , 'branch' : branch ,
30- 'commit_sha' : commit_sha , 'limit' : limit , 'offset' : offset })
46+ 'commit_sha' : commit_sha , 'repository' : repository , 'sort' : sort ,
47+ 'created_after' : created_after , 'created_before' : created_before ,
48+ 'limit' : limit , 'offset' : offset })
3149 fetch_and_render (ctx , '/runs' , params )
3250
3351
@@ -66,14 +84,20 @@ def run_failures(ctx: click.Context, run_id: int) -> None:
6684
6785@run .command ('results' )
6886@click .argument ('run_id' , type = int )
69- @click .option ('--status' , default = None , help = 'pass|fail|skipped|missing_output|running|not_started' )
87+ @click .option ('--status' , type = click .Choice (SAMPLE_STATUSES ), default = None ,
88+ help = 'Filter by per-sample outcome.' )
89+ @click .option ('--name' , default = None , help = 'Substring match on the sample name.' )
90+ @click .option ('--tag' , default = None , help = 'Filter by sample tag.' )
91+ @click .option ('--category' , default = None , help = 'Filter by regression-test category.' )
7092@click .option ('--limit' , type = int , default = None , help = 'Page size (max 100).' )
7193@click .option ('--offset' , type = int , default = None , help = 'Pagination offset.' )
7294@click .pass_context
73- def run_results (ctx : click .Context , run_id : int , status : Optional [str ],
95+ def run_results (ctx : click .Context , run_id : int , status : Optional [str ], name : Optional [str ],
96+ tag : Optional [str ], category : Optional [str ],
7497 limit : Optional [int ], offset : Optional [int ]) -> None :
7598 """List all regression-test results in a run."""
76- params = clean_params ({'status' : status , 'limit' : limit , 'offset' : offset })
99+ params = clean_params ({'status' : status , 'name' : name , 'tag' : tag , 'category' : category ,
100+ 'limit' : limit , 'offset' : offset })
77101 fetch_and_render (ctx , f'/runs/{ run_id } /samples' , params )
78102
79103
@@ -167,6 +191,90 @@ def run_approve_baseline(ctx: click.Context, run_id: int, sample_id: int,
167191 render (result , output )
168192
169193
194+ @run .command ('progress' )
195+ @click .argument ('run_id' , type = int )
196+ @click .option ('--limit' , type = int , default = None , help = 'Page size (max 100).' )
197+ @click .option ('--offset' , type = int , default = None , help = 'Pagination offset.' )
198+ @click .pass_context
199+ def run_progress (ctx : click .Context , run_id : int , limit : Optional [int ],
200+ offset : Optional [int ]) -> None :
201+ """Show the timeline of progress events the CI worker recorded for a run."""
202+ params = clean_params ({'limit' : limit , 'offset' : offset })
203+ fetch_and_render (ctx , f'/runs/{ run_id } /progress' , params )
204+
205+
206+ @run .command ('config' )
207+ @click .argument ('run_id' , type = int )
208+ @click .pass_context
209+ def run_config (ctx : click .Context , run_id : int ) -> None :
210+ """Show the platform, branch, commit, and regression tests a run was launched with."""
211+ fetch_and_render (ctx , f'/runs/{ run_id } /config' )
212+
213+
214+ @run .command ('cancel' )
215+ @click .argument ('run_id' , type = int )
216+ @click .option ('--reason' , default = None ,
217+ help = f'Why the run is being canceled (min { CANCEL_REASON_MIN_LENGTH } characters).' )
218+ @click .pass_context
219+ def run_cancel (ctx : click .Context , run_id : int , reason : Optional [str ]) -> None :
220+ """Cancel a queued or running test.
221+
222+ Requires the ``runs:write`` scope. Idempotent: canceling a run that has
223+ already finished succeeds and reports ``status=no_op`` rather than failing.
224+ """
225+ client = ctx .obj ['client' ]
226+ output = ctx .obj ['output' ]
227+ if reason is not None and len (reason .strip ()) < CANCEL_REASON_MIN_LENGTH :
228+ raise click .BadParameter (
229+ f'must be at least { CANCEL_REASON_MIN_LENGTH } characters' , param_hint = '--reason' )
230+ body = {'reason' : reason } if reason else None
231+ try :
232+ result = client .request ('POST' , f'/runs/{ run_id } /cancel' , json_body = body )
233+ except ApiError as error :
234+ render_error (error , output )
235+ raise SystemExit (error .exit_code )
236+ render (result , output )
237+
238+
239+ @run .command ('output' )
240+ @click .argument ('run_id' , type = int )
241+ @click .argument ('sample_id' , type = int )
242+ @click .option ('--regression' , 'regression_id' , type = int , default = None ,
243+ help = 'Regression test id (auto-resolved if omitted).' )
244+ @click .option ('--output' , 'output_id' , type = int , default = None ,
245+ help = 'Output file id (auto-resolved if omitted).' )
246+ @click .option ('--side' , type = click .Choice (('expected' , 'actual' )), default = 'actual' ,
247+ show_default = True , help = 'Which side of the comparison to fetch.' )
248+ @click .option ('--format' , 'fmt' , default = None , help = 'Response format accepted by the API.' )
249+ @click .pass_context
250+ def run_output (ctx : click .Context , run_id : int , sample_id : int , regression_id : Optional [int ],
251+ output_id : Optional [int ], side : str , fmt : Optional [str ]) -> None :
252+ """Fetch one side of a result's output file.
253+
254+ Resolves the (media sample, regression, output) ids the same way `sp run
255+ diff` does, so the hidden ids the web UI needs are not required here.
256+
257+ Note: for an output that matched, the API answers ``actual`` with a 303
258+ redirect to ``expected`` -- requests follows it, so the expected content is
259+ what comes back.
260+ """
261+ client = ctx .obj ['client' ]
262+ output = ctx .obj ['output' ]
263+ try :
264+ targets = _resolve_diff_targets (client , run_id , sample_id , regression_id , output_id )
265+ if not targets :
266+ raise ApiError ('not_found' , 'No output to fetch for this result' , 404 )
267+ media_sample_id , reg_id , out_id = targets [0 ]
268+ payload = client .get (
269+ f'/runs/{ run_id } /samples/{ media_sample_id } '
270+ f'/regression-tests/{ reg_id } /outputs/{ out_id } /{ side } ' ,
271+ params = clean_params ({'format' : fmt }))
272+ except ApiError as error :
273+ render_error (error , output )
274+ raise SystemExit (error .exit_code )
275+ render (payload , output )
276+
277+
170278@run .command ('artifacts' )
171279@click .argument ('run_id' , type = int )
172280@click .pass_context
0 commit comments