@@ -784,16 +784,57 @@ def __init__(self, project: GitProject, path: Path, **kwargs: Any) -> None:
784784 self .project = project
785785 self .path = path
786786
787+ def join_traversalsafe (self , root : Path , joined : Path ) -> Path :
788+ root = root .resolve ()
789+
790+ for part in joined .parts :
791+ new_root = (root / part ).resolve ()
792+
793+ if not new_root .is_relative_to (root ):
794+ msg = f"Joined path attempted to traverse upwards when processing { root } against { part } (gave { new_root } )"
795+ raise ValueError (msg )
796+
797+ root = new_root
798+
799+ return root
800+
801+ def join_all_traversalsafe (self , root : Path , * paths : Path ) -> Path :
802+ for path in paths :
803+ root = self .join_traversalsafe (root , path )
804+
805+ return root
806+
807+ @defer .inlineCallbacks
787808 def run (self ) -> Generator [Any , object , Any ]:
788809 props = self .build .getProperties ()
789- if props .getProperty ("branch" ) != self .project .default_branch :
810+
811+ pr = props .getProperty ("pr_number" )
812+
813+ if not pr and props .getProperty ("branch" ) != self .project .default_branch :
790814 return util .SKIPPED
791815
792- attr = Path (props .getProperty ("attr" )).name
793816 out_path = props .getProperty ("out_path" )
794- # XXX don't hardcode this
795- self .path .mkdir (parents = True , exist_ok = True )
796- (self .path / attr ).write_text (out_path )
817+
818+ if not out_path : # if, e.g., the build fails and doesn't produce an output
819+ return util .SKIPPED
820+
821+ project_name = Path (props .getProperty ("projectname" ))
822+
823+ target = Path (f"pulls/{ pr } " ) if pr else Path (props .getProperty ("branch" ))
824+
825+ attr = Path (props .getProperty ("attr" ))
826+
827+ try :
828+ file = self .join_all_traversalsafe (self .path , project_name , target , attr )
829+ except ValueError as e :
830+ error_log : StreamLog = yield self .addLog ("path_error" )
831+ error_log .addStderr (f"Path traversal prevented ... skipping update: { e } " )
832+ return util .FAILURE
833+
834+ file .parent .mkdir (parents = True , exist_ok = True )
835+
836+ file .write_text (out_path )
837+
797838 return util .SUCCESS
798839
799840
@@ -1104,6 +1145,7 @@ def nix_cached_failure_config(
11041145def nix_skipped_build_config (
11051146 project : GitProject ,
11061147 worker_names : list [str ],
1148+ outputs_path : Path | None = None ,
11071149) -> BuilderConfig :
11081150 """Dummy builder that is triggered when a build is skipped."""
11091151 factory = util .BuildFactory ()
@@ -1132,6 +1174,14 @@ def nix_skipped_build_config(
11321174 set_properties = {"report_status" : False },
11331175 ),
11341176 )
1177+ if outputs_path is not None :
1178+ factory .addStep (
1179+ UpdateBuildOutput (
1180+ project = project ,
1181+ name = "Update build output" ,
1182+ path = outputs_path ,
1183+ ),
1184+ )
11351185 return util .BuilderConfig (
11361186 name = f"{ project .name } /nix-skipped-build" ,
11371187 project = project .name ,
@@ -1282,7 +1332,7 @@ def config_for_project(
12821332 outputs_path = outputs_path ,
12831333 post_build_steps = post_build_steps ,
12841334 ),
1285- nix_skipped_build_config (project , [SKIPPED_BUILDER_NAME ]),
1335+ nix_skipped_build_config (project , [SKIPPED_BUILDER_NAME ], outputs_path ),
12861336 nix_failed_eval_config (project , [SKIPPED_BUILDER_NAME ]),
12871337 nix_dependency_failed_config (project , [SKIPPED_BUILDER_NAME ]),
12881338 nix_cached_failure_config (project , [SKIPPED_BUILDER_NAME ]),
0 commit comments