@@ -40,6 +40,7 @@ import { command, flag, number, option, optional, positional, string } from 'cmd
4040import {
4141 DEFAULT_CATEGORY ,
4242 type EvaluationResult ,
43+ ResultsRepoRunExistsError ,
4344 addProject ,
4445 getProject ,
4546 loadConfig ,
@@ -72,12 +73,16 @@ import {
7273 resolveResultSourcePath ,
7374} from './manifest.js' ;
7475import {
76+ LocalRunPublishError ,
77+ type LocalRunPublishPreview ,
7578 type SourcedResultFileMeta ,
7679 clearRemoteRunTags ,
7780 ensureRemoteRunAvailable ,
7881 findRunById ,
7982 getRemoteResultsStatus ,
8083 listMergedResultFiles ,
84+ previewLocalRunPublish ,
85+ publishLocalRun ,
8186 readRemoteRunTagState ,
8287 setRemoteRunTags ,
8388 syncRemoteResults ,
@@ -362,6 +367,34 @@ function remoteMetadataErrorStatus(error: unknown): 400 | 409 {
362367 return 400 ;
363368}
364369
370+ function localRunPublishToWire (
371+ preview : LocalRunPublishPreview & { published ?: boolean ; replaced ?: boolean } ,
372+ ) {
373+ return {
374+ source_run_id : preview . sourceRunId ,
375+ target_repo : preview . targetRepo ,
376+ target_path : preview . targetPath ,
377+ target_run_id : preview . targetRunId ,
378+ remote_exists : preview . remoteExists ,
379+ replace_required : preview . replaceRequired ,
380+ can_publish : preview . canPublish ,
381+ ...( preview . blockReason && { block_reason : preview . blockReason } ) ,
382+ remote_status : preview . remoteStatus ,
383+ ...( preview . published !== undefined && { published : preview . published } ) ,
384+ ...( preview . replaced !== undefined && { replaced : preview . replaced } ) ,
385+ } ;
386+ }
387+
388+ function localRunPublishErrorStatus ( error : unknown ) : 400 | 409 | 500 {
389+ if ( error instanceof LocalRunPublishError ) {
390+ return error . status ;
391+ }
392+ if ( error instanceof ResultsRepoRunExistsError ) {
393+ return 409 ;
394+ }
395+ return 500 ;
396+ }
397+
365398async function ensureRunReadable (
366399 searchDir : string ,
367400 meta : SourcedResultFileMeta ,
@@ -1182,9 +1215,10 @@ function getLocalRunsRoot(searchDir: string): string {
11821215function validateLocalCompletedRun (
11831216 searchDir : string ,
11841217 meta : SourcedResultFileMeta ,
1218+ actionName = 'Run combine' ,
11851219) : { ok : true } | { error : string ; status : 400 | 409 } {
11861220 if ( meta . source === 'remote' ) {
1187- return { error : 'Run combine is only available for local runs' , status : 400 } ;
1221+ return { error : ` ${ actionName } is only available for local runs` , status : 400 } ;
11881222 }
11891223 if ( getActiveRunStatus ( meta . path ) === 'starting' || getActiveRunStatus ( meta . path ) === 'running' ) {
11901224 return { error : 'Run is still active' , status : 409 } ;
@@ -1203,6 +1237,52 @@ function validateLocalCompletedRun(
12031237 return { error : 'Run workspace is outside the local results directory' , status : 400 } ;
12041238}
12051239
1240+ async function handleRunPublishPreview ( c : C , { searchDir, projectId } : DataContext ) {
1241+ const filename = c . req . param ( 'filename' ) ?? '' ;
1242+ const meta = await findRunById ( searchDir , filename , projectId ) ;
1243+ if ( ! meta ) return c . json ( { error : 'Run not found' } , 404 ) ;
1244+ const safe = validateLocalCompletedRun ( searchDir , meta , 'Selected run publish' ) ;
1245+ if ( 'error' in safe ) return c . json ( { error : safe . error } , safe . status ) ;
1246+
1247+ try {
1248+ const preview = await previewLocalRunPublish ( searchDir , meta , projectId ) ;
1249+ return c . json ( localRunPublishToWire ( preview ) ) ;
1250+ } catch ( err ) {
1251+ return c . json ( { error : ( err as Error ) . message } , localRunPublishErrorStatus ( err ) ) ;
1252+ }
1253+ }
1254+
1255+ async function handleRunPublish ( c : C , { searchDir, projectId } : DataContext ) {
1256+ const filename = c . req . param ( 'filename' ) ?? '' ;
1257+ const meta = await findRunById ( searchDir , filename , projectId ) ;
1258+ if ( ! meta ) return c . json ( { error : 'Run not found' } , 404 ) ;
1259+ const safe = validateLocalCompletedRun ( searchDir , meta , 'Selected run publish' ) ;
1260+ if ( 'error' in safe ) return c . json ( { error : safe . error } , safe . status ) ;
1261+
1262+ let replace = false ;
1263+ try {
1264+ const body = ( await c . req . json ( ) ) as unknown ;
1265+ if ( body && typeof body === 'object' && ! Array . isArray ( body ) ) {
1266+ const value = ( body as Record < string , unknown > ) . replace ;
1267+ if ( value !== undefined && typeof value !== 'boolean' ) {
1268+ return c . json ( { error : 'replace must be a boolean' } , 400 ) ;
1269+ }
1270+ replace = value === true ;
1271+ } else if ( body !== undefined ) {
1272+ return c . json ( { error : 'Invalid payload' } , 400 ) ;
1273+ }
1274+ } catch {
1275+ return c . json ( { error : 'Invalid JSON' } , 400 ) ;
1276+ }
1277+
1278+ try {
1279+ const result = await publishLocalRun ( { cwd : searchDir , meta, projectId, replace } ) ;
1280+ return c . json ( localRunPublishToWire ( result ) ) ;
1281+ } catch ( err ) {
1282+ return c . json ( { error : ( err as Error ) . message } , localRunPublishErrorStatus ( err ) ) ;
1283+ }
1284+ }
1285+
12061286async function handleRunsCombine ( c : C , { searchDir, projectId } : DataContext ) {
12071287 let body : unknown ;
12081288 try {
@@ -1527,6 +1607,13 @@ export function createApp(
15271607 }
15281608 return handleRunsCombine ( c , defaultCtx ) ;
15291609 } ) ;
1610+ app . get ( '/api/runs/:filename/publish' , ( c ) => handleRunPublishPreview ( c , defaultCtx ) ) ;
1611+ app . post ( '/api/runs/:filename/publish' , ( c ) => {
1612+ if ( readOnly ) {
1613+ return c . json ( { error : 'Dashboard is running in read-only mode' } , 403 ) ;
1614+ }
1615+ return handleRunPublish ( c , defaultCtx ) ;
1616+ } ) ;
15301617 app . put ( '/api/runs/:filename/tags' , ( c ) => {
15311618 if ( readOnly ) {
15321619 return c . json ( { error : 'Dashboard is running in read-only mode' } , 403 ) ;
@@ -1668,6 +1755,15 @@ export function createApp(
16681755 }
16691756 return withProject ( c , handleRunsCombine ) ;
16701757 } ) ;
1758+ app . get ( '/api/projects/:projectId/runs/:filename/publish' , ( c ) =>
1759+ withProject ( c , handleRunPublishPreview ) ,
1760+ ) ;
1761+ app . post ( '/api/projects/:projectId/runs/:filename/publish' , ( c ) => {
1762+ if ( readOnly ) {
1763+ return c . json ( { error : 'Dashboard is running in read-only mode' } , 403 ) ;
1764+ }
1765+ return withProject ( c , handleRunPublish ) ;
1766+ } ) ;
16711767 app . put ( '/api/projects/:projectId/runs/:filename/tags' , ( c ) => {
16721768 if ( readOnly ) {
16731769 return c . json ( { error : 'Dashboard is running in read-only mode' } , 403 ) ;
0 commit comments