@@ -40,14 +40,19 @@ pub(super) fn execute_release_plan_step(
4040 Ok ( Some ( run_changelog_bootstrap_preflight ( step, context) ) )
4141 }
4242 "preflight.package" => Ok ( Some (
43- executor:: package_preflight:: run_package_preflight (
44- context. extensions ,
45- context. component ,
46- context. component_id ,
47- & context. component . local_path ,
48- context. options . skip_build_validation ,
49- )
50- . unwrap_or_else ( |err| failed_result ( "preflight.package" , "preflight.package" , err) ) ,
43+ executor:: package_preflight:: run_package_preflight ( & context. component . local_path )
44+ . map ( |_| {
45+ executor:: step_success (
46+ "preflight.package" ,
47+ "preflight.package" ,
48+ Some ( serde_json:: json!( {
49+ "component_path" : context. component. local_path,
50+ "validated_action" : "release.package.guards" ,
51+ } ) ) ,
52+ Vec :: new ( ) ,
53+ )
54+ } )
55+ . unwrap_or_else ( |err| failed_result ( "preflight.package" , "preflight.package" , err) ) ,
5156 ) ) ,
5257 "preflight.tag_availability" => {
5358 let tag_name = step
@@ -109,6 +114,14 @@ pub(super) fn execute_release_plan_step(
109114 None ,
110115 context. options . skip_build_validation ,
111116 )
117+ . and_then ( |result| {
118+ executor:: package_preflight:: validate_package_completeness (
119+ context. component ,
120+ std:: path:: Path :: new ( & context. component . local_path ) ,
121+ & context. state . artifacts ,
122+ ) ?;
123+ Ok ( result)
124+ } )
112125 . unwrap_or_else ( |err| failed_result ( "package" , "package" , err) ) ,
113126 ) ) ,
114127 "artifacts.inventory" => {
@@ -729,6 +742,7 @@ mod tests {
729742 release_step_is_show_stopper, release_step_unexpected_dirty_files, ReleaseExecutionContext ,
730743 } ;
731744 use crate :: core:: component:: { Component , ComponentScriptsConfig , VersionTarget } ;
745+ use crate :: core:: extension:: ExtensionManifest ;
732746 use crate :: core:: plan:: PlanStep ;
733747 use crate :: core:: release:: types:: {
734748 ReleaseOptions , ReleaseState , ReleaseStepResult , ReleaseStepStatus ,
@@ -774,6 +788,135 @@ mod tests {
774788 assert ! ( !release_step_is_plan_only( & plan_step( "deploy" ) ) ) ;
775789 }
776790
791+ #[ test]
792+ fn release_package_builds_once_and_validates_the_uploaded_durable_bytes ( ) {
793+ crate :: test_support:: with_isolated_home ( |_| {
794+ let repo = tempfile:: tempdir ( ) . expect ( "repo" ) ;
795+ std:: fs:: write ( repo. path ( ) . join ( "plugin.php" ) , "<?php\n " ) . expect ( "plugin" ) ;
796+ run_in ( repo. path ( ) , & [ "git" , "init" , "-q" ] ) ;
797+ configure_git_user ( repo. path ( ) ) ;
798+ run_in ( repo. path ( ) , & [ "git" , "add" , "plugin.php" ] ) ;
799+ run_in ( repo. path ( ) , & [ "git" , "commit" , "-qm" , "Initial commit" ] ) ;
800+ let counter = repo. path ( ) . join ( "package-count" ) ;
801+ let package = package_extension ( & format ! (
802+ "n=$(cat '{counter}' 2>/dev/null || echo 0); echo $((n + 1)) > '{counter}'; \
803+ mkdir -p build/stage; cp plugin.php build/stage/plugin.php; \
804+ (cd build && zip -q fixture.zip stage/plugin.php); \
805+ printf '[{{\" path\" :\" build/fixture.zip\" ,\" type\" :\" archive\" }}]'",
806+ counter = counter. display( ) ,
807+ ) ) ;
808+ crate :: core:: extension:: save_manifest ( & package) . expect ( "save package extension" ) ;
809+
810+ let component = Component {
811+ id : "fixture" . to_string ( ) ,
812+ local_path : repo. path ( ) . to_string_lossy ( ) . to_string ( ) ,
813+ ..Component :: default ( )
814+ } ;
815+ let options = ReleaseOptions :: default ( ) ;
816+ let extensions = vec ! [ package] ;
817+ let mut context = ReleaseExecutionContext {
818+ component : & component,
819+ extensions : & extensions,
820+ component_id : "fixture" ,
821+ options : & options,
822+ state : ReleaseState {
823+ version : Some ( "1.2.3" . to_string ( ) ) ,
824+ ..ReleaseState :: default ( )
825+ } ,
826+ publish_failed : false ,
827+ } ;
828+
829+ execute_release_plan_step ( & plan_step ( "preflight.package" ) , & mut context)
830+ . expect ( "preflight dispatch" )
831+ . expect ( "preflight result" ) ;
832+ assert ! ( !counter. exists( ) , "preflight must not run release.package" ) ;
833+
834+ let result = execute_release_plan_step ( & plan_step ( "package" ) , & mut context)
835+ . expect ( "package dispatch" )
836+ . expect ( "package result" ) ;
837+ assert_eq ! ( result. status, ReleaseStepStatus :: Success ) ;
838+ assert_eq ! (
839+ std:: fs:: read_to_string( & counter) . expect( "count" ) . trim( ) ,
840+ "1"
841+ ) ;
842+ let durable = context. state . artifacts [ 0 ]
843+ . durable_path
844+ . as_ref ( )
845+ . expect ( "durable artifact" ) ;
846+ assert_eq ! (
847+ std:: fs:: read( repo. path( ) . join( "build/fixture.zip" ) ) . expect( "source artifact" ) ,
848+ std:: fs:: read( durable) . expect( "uploaded artifact" ) ,
849+ ) ;
850+ } ) ;
851+ }
852+
853+ #[ test]
854+ fn final_package_completeness_failure_stops_before_publication ( ) {
855+ crate :: test_support:: with_isolated_home ( |_| {
856+ let repo = tempfile:: tempdir ( ) . expect ( "repo" ) ;
857+ std:: fs:: create_dir_all ( repo. path ( ) . join ( "agents" ) ) . expect ( "agents" ) ;
858+ std:: fs:: write ( repo. path ( ) . join ( "plugin.php" ) , "<?php\n " ) . expect ( "plugin" ) ;
859+ std:: fs:: write ( repo. path ( ) . join ( "agents/runtime.php" ) , "<?php\n " ) . expect ( "runtime" ) ;
860+ run_in ( repo. path ( ) , & [ "git" , "init" , "-q" ] ) ;
861+ configure_git_user ( repo. path ( ) ) ;
862+ run_in (
863+ repo. path ( ) ,
864+ & [ "git" , "add" , "plugin.php" , "agents/runtime.php" ] ,
865+ ) ;
866+ run_in ( repo. path ( ) , & [ "git" , "commit" , "-qm" , "Initial commit" ] ) ;
867+ let package = package_extension (
868+ "mkdir -p build/stage; cp plugin.php build/stage/plugin.php; (cd build && zip -q fixture.zip stage/plugin.php); printf '[{\" path\" :\" build/fixture.zip\" ,\" type\" :\" archive\" }]'" ,
869+ ) ;
870+ crate :: core:: extension:: save_manifest ( & package) . expect ( "save package extension" ) ;
871+
872+ let component = Component {
873+ id : "fixture" . to_string ( ) ,
874+ local_path : repo. path ( ) . to_string_lossy ( ) . to_string ( ) ,
875+ ..Component :: default ( )
876+ } ;
877+ let options = ReleaseOptions :: default ( ) ;
878+ let extensions = vec ! [ package] ;
879+ let mut context = ReleaseExecutionContext {
880+ component : & component,
881+ extensions : & extensions,
882+ component_id : "fixture" ,
883+ options : & options,
884+ state : ReleaseState {
885+ version : Some ( "1.2.3" . to_string ( ) ) ,
886+ ..ReleaseState :: default ( )
887+ } ,
888+ publish_failed : false ,
889+ } ;
890+
891+ let result = execute_release_plan_step ( & plan_step ( "package" ) , & mut context)
892+ . expect ( "package dispatch" )
893+ . expect ( "package result" ) ;
894+ assert_eq ! ( result. status, ReleaseStepStatus :: Failed ) ;
895+ assert ! ( result
896+ . error
897+ . as_deref( )
898+ . unwrap_or_default( )
899+ . contains( "agents/runtime.php" ) ) ;
900+ assert ! ( release_step_is_show_stopper( & result) ) ;
901+ } ) ;
902+ }
903+
904+ fn package_extension ( command : & str ) -> ExtensionManifest {
905+ let mut extension: ExtensionManifest = serde_json:: from_value ( serde_json:: json!( {
906+ "name" : "Fixture Packager" ,
907+ "version" : "1.0.0" ,
908+ "actions" : [ {
909+ "id" : "release.package" ,
910+ "label" : "Package release" ,
911+ "type" : "command" ,
912+ "command" : command,
913+ } ] ,
914+ } ) )
915+ . expect ( "package extension" ) ;
916+ extension. id = "fixture-packager" . to_string ( ) ;
917+ extension
918+ }
919+
777920 #[ test]
778921 fn dependency_preflight_hydrates_before_lint_self_check ( ) {
779922 let temp = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
0 commit comments