@@ -756,16 +756,57 @@ def __init__(self, project: GitProject, path: Path, **kwargs: Any) -> None:
756756 self .project = project
757757 self .path = path
758758
759+ def join_traversalsafe (self , root : Path , joined : Path ) -> Path :
760+ root = root .resolve ()
761+
762+ for part in joined .parts :
763+ new_root = (root / part ).resolve ()
764+
765+ if not new_root .is_relative_to (root ):
766+ msg = f"Joined path attempted to traverse upwards when processing { root } against { part } (gave { new_root } )"
767+ raise ValueError (msg )
768+
769+ root = new_root
770+
771+ return root
772+
773+ def join_all_traversalsafe (self , root : Path , * paths : Path ) -> Path :
774+ for path in paths :
775+ root = self .join_traversalsafe (root , path )
776+
777+ return root
778+
779+ @defer .inlineCallbacks
759780 def run (self ) -> Generator [Any , object , Any ]:
760781 props = self .build .getProperties ()
761- if props .getProperty ("branch" ) != self .project .default_branch :
782+
783+ pr = props .getProperty ("pr_number" )
784+
785+ if not pr and props .getProperty ("branch" ) != self .project .default_branch :
762786 return util .SKIPPED
763787
764- attr = Path (props .getProperty ("attr" )).name
765788 out_path = props .getProperty ("out_path" )
766- # XXX don't hardcode this
767- self .path .mkdir (parents = True , exist_ok = True )
768- (self .path / attr ).write_text (out_path )
789+
790+ if not out_path : # if, e.g., the build fails and doesn't produce an output
791+ return util .SKIPPED
792+
793+ project_name = Path (props .getProperty ("projectname" ))
794+
795+ target = Path (f"pulls/{ pr } " ) if pr else Path (props .getProperty ("branch" ))
796+
797+ attr = Path (props .getProperty ("attr" ))
798+
799+ try :
800+ file = self .join_all_traversalsafe (self .path , project_name , target , attr )
801+ except ValueError as e :
802+ error_log : StreamLog = yield self .addLog ("path_error" )
803+ error_log .addStderr (f"Path traversal prevented ... skipping update: { e } " )
804+ return util .FAILURE
805+
806+ file .parent .mkdir (parents = True , exist_ok = True )
807+
808+ file .write_text (out_path )
809+
769810 return util .SUCCESS
770811
771812
@@ -1074,6 +1115,7 @@ def nix_cached_failure_config(
10741115def nix_skipped_build_config (
10751116 project : GitProject ,
10761117 worker_names : list [str ],
1118+ outputs_path : Path | None = None ,
10771119) -> BuilderConfig :
10781120 """Dummy builder that is triggered when a build is skipped."""
10791121 factory = util .BuildFactory ()
@@ -1102,6 +1144,14 @@ def nix_skipped_build_config(
11021144 set_properties = {"report_status" : False },
11031145 ),
11041146 )
1147+ if outputs_path is not None :
1148+ factory .addStep (
1149+ UpdateBuildOutput (
1150+ project = project ,
1151+ name = "Update build output" ,
1152+ path = outputs_path ,
1153+ ),
1154+ )
11051155 return util .BuilderConfig (
11061156 name = f"{ project .name } /nix-skipped-build" ,
11071157 project = project .name ,
@@ -1250,7 +1300,7 @@ def config_for_project(
12501300 outputs_path = outputs_path ,
12511301 post_build_steps = post_build_steps ,
12521302 ),
1253- nix_skipped_build_config (project , [SKIPPED_BUILDER_NAME ]),
1303+ nix_skipped_build_config (project , [SKIPPED_BUILDER_NAME ], outputs_path ),
12541304 nix_failed_eval_config (project , [SKIPPED_BUILDER_NAME ]),
12551305 nix_dependency_failed_config (project , [SKIPPED_BUILDER_NAME ]),
12561306 nix_cached_failure_config (project , [SKIPPED_BUILDER_NAME ]),
0 commit comments