@@ -132,6 +132,8 @@ async def _create_task_event(
132132 db : AsyncSession ,
133133 auth : SdkAuth ,
134134 error_message : str | None = None ,
135+ commit_hash : str | None = None ,
136+ extra_metadata : dict | None = None ,
135137) -> TaskEventResponse :
136138 """Create a task event and return slim response."""
137139 # Limit checks
@@ -144,11 +146,21 @@ async def _create_task_event(
144146
145147 _ , db_task = await _get_build_and_task (build_id , task_id , db , auth )
146148
149+ # Build event_metadata from commit_hash and any extra metadata
150+ event_metadata : dict | None = None
151+ if commit_hash or extra_metadata :
152+ event_metadata = {}
153+ if commit_hash :
154+ event_metadata ["commit_hash" ] = commit_hash
155+ if extra_metadata :
156+ event_metadata .update (extra_metadata )
157+
147158 event = Event (
148159 build_id = build_id ,
149160 task_id = db_task .id ,
150161 event_type = event_type ,
151162 error_message = error_message ,
163+ event_metadata = event_metadata ,
152164 )
153165 db .add (event )
154166 await db .commit ()
@@ -342,17 +354,32 @@ async def get_build(
342354 )
343355
344356
357+ def _build_event_metadata (
358+ commit_hash : str | None = None ,
359+ triggered_by_user_id : str | None = None ,
360+ ) -> dict | None :
361+ """Build event_metadata dict from optional fields."""
362+ metadata : dict = {}
363+ if commit_hash :
364+ metadata ["commit_hash" ] = commit_hash
365+ if triggered_by_user_id :
366+ metadata ["triggered_by_user_id" ] = triggered_by_user_id
367+ return metadata or None
368+
369+
345370@router .post ("/{build_id}/complete" , response_model = BuildResponse )
346371async def complete_build (
347372 build_id : UUID ,
348373 db : Annotated [AsyncSession , Depends (get_db )],
349374 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
350375 triggered_by_user_id : str | None = None ,
376+ commit_hash : str | None = None ,
351377):
352378 """Mark a build as completed.
353379
354380 Args:
355381 triggered_by_user_id: Optional user ID if this is a manual override from UI.
382+ commit_hash: Optional git commit hash of the code that ran this build.
356383 """
357384 # Limit checks
358385 _raise_if_limit_exceeded (check_rate_limit (auth .workspace_id , limits_settings ))
@@ -372,16 +399,11 @@ async def complete_build(
372399 status_code = 403 , detail = "Build does not belong to this environment"
373400 )
374401
375- # Store user ID in metadata if this was user-triggered
376- event_metadata = (
377- {"triggered_by_user_id" : triggered_by_user_id } if triggered_by_user_id else None
378- )
379-
380402 event = Event (
381403 build_id = build_id ,
382404 task_id = None ,
383405 event_type = EventType .BUILD_COMPLETED ,
384- event_metadata = event_metadata ,
406+ event_metadata = _build_event_metadata ( commit_hash , triggered_by_user_id ) ,
385407 )
386408 db .add (event )
387409 await db .commit ()
@@ -416,12 +438,14 @@ async def fail_build(
416438 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
417439 error_message : str | None = None ,
418440 triggered_by_user_id : str | None = None ,
441+ commit_hash : str | None = None ,
419442):
420443 """Mark a build as failed.
421444
422445 Args:
423446 error_message: Optional error message.
424447 triggered_by_user_id: Optional user ID if this is a manual override from UI.
448+ commit_hash: Optional git commit hash of the code that ran this build.
425449 """
426450 # Limit checks
427451 _raise_if_limit_exceeded (check_rate_limit (auth .workspace_id , limits_settings ))
@@ -441,17 +465,12 @@ async def fail_build(
441465 status_code = 403 , detail = "Build does not belong to this environment"
442466 )
443467
444- # Store user ID in metadata if this was user-triggered
445- event_metadata = (
446- {"triggered_by_user_id" : triggered_by_user_id } if triggered_by_user_id else None
447- )
448-
449468 event = Event (
450469 build_id = build_id ,
451470 task_id = None ,
452471 event_type = EventType .BUILD_FAILED ,
453472 error_message = error_message ,
454- event_metadata = event_metadata ,
473+ event_metadata = _build_event_metadata ( commit_hash , triggered_by_user_id ) ,
455474 )
456475 db .add (event )
457476 await db .commit ()
@@ -485,11 +504,13 @@ async def cancel_build(
485504 db : Annotated [AsyncSession , Depends (get_db )],
486505 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
487506 triggered_by_user_id : str | None = None ,
507+ commit_hash : str | None = None ,
488508):
489509 """Cancel a build.
490510
491511 Args:
492512 triggered_by_user_id: Optional user ID if this is a manual override from UI.
513+ commit_hash: Optional git commit hash of the code that ran this build.
493514 """
494515 # Limit checks
495516 _raise_if_limit_exceeded (check_rate_limit (auth .workspace_id , limits_settings ))
@@ -509,16 +530,11 @@ async def cancel_build(
509530 status_code = 403 , detail = "Build does not belong to this environment"
510531 )
511532
512- # Store user ID in metadata if this was user-triggered
513- event_metadata = (
514- {"triggered_by_user_id" : triggered_by_user_id } if triggered_by_user_id else None
515- )
516-
517533 event = Event (
518534 build_id = build_id ,
519535 task_id = None ,
520536 event_type = EventType .BUILD_CANCELLED ,
521- event_metadata = event_metadata ,
537+ event_metadata = _build_event_metadata ( commit_hash , triggered_by_user_id ) ,
522538 )
523539 db .add (event )
524540 await db .commit ()
@@ -552,6 +568,7 @@ async def exit_early(
552568 db : Annotated [AsyncSession , Depends (get_db )],
553569 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
554570 reason : str | None = None ,
571+ commit_hash : str | None = None ,
555572):
556573 """Mark a build as exited early (all remaining tasks running in other builds)."""
557574 # Limit checks
@@ -577,6 +594,7 @@ async def exit_early(
577594 task_id = None ,
578595 event_type = EventType .BUILD_EXIT_EARLY ,
579596 error_message = reason , # Reuse error_message field for the reason
597+ event_metadata = _build_event_metadata (commit_hash ),
580598 )
581599 db .add (event )
582600 await db .commit ()
@@ -787,9 +805,12 @@ async def start_task(
787805 task_id : str ,
788806 db : Annotated [AsyncSession , Depends (get_db )],
789807 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
808+ commit_hash : str | None = None ,
790809):
791810 """Mark a task as started within a build."""
792- return await _create_task_event (build_id , task_id , EventType .TASK_STARTED , db , auth )
811+ return await _create_task_event (
812+ build_id , task_id , EventType .TASK_STARTED , db , auth , commit_hash = commit_hash
813+ )
793814
794815
795816@router .post ("/{build_id}/tasks/{task_id}/complete" , response_model = TaskEventResponse )
@@ -798,10 +819,11 @@ async def complete_task(
798819 task_id : str ,
799820 db : Annotated [AsyncSession , Depends (get_db )],
800821 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
822+ commit_hash : str | None = None ,
801823):
802824 """Mark a task as completed within a build."""
803825 return await _create_task_event (
804- build_id , task_id , EventType .TASK_COMPLETED , db , auth
826+ build_id , task_id , EventType .TASK_COMPLETED , db , auth , commit_hash = commit_hash
805827 )
806828
807829
@@ -812,10 +834,17 @@ async def fail_task(
812834 db : Annotated [AsyncSession , Depends (get_db )],
813835 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
814836 error_message : str | None = None ,
837+ commit_hash : str | None = None ,
815838):
816839 """Mark a task as failed within a build."""
817840 return await _create_task_event (
818- build_id , task_id , EventType .TASK_FAILED , db , auth , error_message
841+ build_id ,
842+ task_id ,
843+ EventType .TASK_FAILED ,
844+ db ,
845+ auth ,
846+ error_message ,
847+ commit_hash = commit_hash ,
819848 )
820849
821850
@@ -825,10 +854,11 @@ async def suspend_task(
825854 task_id : str ,
826855 db : Annotated [AsyncSession , Depends (get_db )],
827856 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
857+ commit_hash : str | None = None ,
828858):
829859 """Mark a task as suspended (waiting for dynamic dependencies)."""
830860 return await _create_task_event (
831- build_id , task_id , EventType .TASK_SUSPENDED , db , auth
861+ build_id , task_id , EventType .TASK_SUSPENDED , db , auth , commit_hash = commit_hash
832862 )
833863
834864
@@ -838,9 +868,12 @@ async def resume_task(
838868 task_id : str ,
839869 db : Annotated [AsyncSession , Depends (get_db )],
840870 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
871+ commit_hash : str | None = None ,
841872):
842873 """Mark a task as resumed (dynamic dependencies completed)."""
843- return await _create_task_event (build_id , task_id , EventType .TASK_RESUMED , db , auth )
874+ return await _create_task_event (
875+ build_id , task_id , EventType .TASK_RESUMED , db , auth , commit_hash = commit_hash
876+ )
844877
845878
846879@router .post ("/{build_id}/tasks/{task_id}/cancel" , response_model = TaskEventResponse )
@@ -849,10 +882,11 @@ async def cancel_task(
849882 task_id : str ,
850883 db : Annotated [AsyncSession , Depends (get_db )],
851884 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
885+ commit_hash : str | None = None ,
852886):
853887 """Cancel a task within a build."""
854888 return await _create_task_event (
855- build_id , task_id , EventType .TASK_CANCELLED , db , auth
889+ build_id , task_id , EventType .TASK_CANCELLED , db , auth , commit_hash = commit_hash
856890 )
857891
858892
@@ -865,35 +899,19 @@ async def task_waiting_for_lock(
865899 db : Annotated [AsyncSession , Depends (get_db )],
866900 auth : Annotated [SdkAuth , Depends (require_sdk_auth )],
867901 lock_owner : str | None = None ,
902+ commit_hash : str | None = None ,
868903):
869904 """Record that a task is waiting for a global lock held by another build."""
870- # Limit checks
871- _raise_if_limit_exceeded (check_rate_limit (auth .workspace_id , limits_settings ))
872- _raise_if_limit_exceeded (
873- await check_entity_creation_limit (
874- db , auth .workspace_id , "events" , limits_settings
875- )
876- )
877-
878- _ , db_task = await _get_build_and_task (build_id , task_id , db , auth )
879-
880- # Store lock owner info in event_metadata if provided
881- event_metadata = {"lock_owner" : lock_owner } if lock_owner else None
882-
883- event = Event (
884- build_id = build_id ,
885- task_id = db_task .id ,
886- event_type = EventType .TASK_WAITING_FOR_LOCK ,
887- event_metadata = event_metadata ,
905+ extra_metadata = {"lock_owner" : lock_owner } if lock_owner else None
906+ return await _create_task_event (
907+ build_id ,
908+ task_id ,
909+ EventType .TASK_WAITING_FOR_LOCK ,
910+ db ,
911+ auth ,
912+ commit_hash = commit_hash ,
913+ extra_metadata = extra_metadata ,
888914 )
889- db .add (event )
890- await db .commit ()
891-
892- record_entity_created (auth .workspace_id , "events" )
893-
894- status , _ , _ , _ = await get_task_status_in_build (db , build_id , db_task .id )
895-
896- return TaskEventResponse (task_id = db_task .task_id , status = status )
897915
898916
899917@router .post (
@@ -1080,7 +1098,10 @@ async def list_tasks_in_build(
10801098 error_message ,
10811099 status_build_id ,
10821100 waiting_for_lock ,
1083- ) = statuses .get (task .id , (TaskStatus .PENDING , None , None , None , None , False ))
1101+ commit_hash ,
1102+ ) = statuses .get (
1103+ task .id , (TaskStatus .PENDING , None , None , None , None , False , None )
1104+ )
10841105 responses .append (
10851106 TaskWithStatusResponse (
10861107 id = task .id ,
@@ -1099,6 +1120,7 @@ async def list_tasks_in_build(
10991120 artifact_count = artifact_counts .get (task .id , 0 ),
11001121 waiting_for_lock = waiting_for_lock ,
11011122 status_build_id = status_build_id ,
1123+ commit_hash = commit_hash ,
11021124 )
11031125 )
11041126
@@ -1223,8 +1245,8 @@ async def get_build_graph(
12231245 if task .is_phantom :
12241246 status = TaskStatus .UNREGISTERED
12251247 else :
1226- status , _ , _ , _ , _ , _ = statuses .get (
1227- task .id , (TaskStatus .PENDING , None , None , None , None , False )
1248+ status , _ , _ , _ , _ , _ , _ = statuses .get (
1249+ task .id , (TaskStatus .PENDING , None , None , None , None , False , None )
12281250 )
12291251 nodes .append (
12301252 TaskNode (
0 commit comments