@@ -7,12 +7,14 @@ use crate::core::project::Project;
77use crate :: core:: release:: version;
88
99use super :: execution:: {
10- execute_preflighted_component_deploy, prepare_component_deploy, PreparedComponentDeploy ,
10+ execute_preflighted_component_deploy, prepare_component_deploy, release_artifact_plan,
11+ resolve_planned_release_artifact, PreparedComponentDeploy , ReleaseArtifactPlan ,
1112} ;
1213use super :: orchestration_ref_checkout:: { ExactRefCheckout , ExactRefIdentity } ;
1314use super :: orchestration_tag_checkout:: { checkout_deploy_tags, restore_branches} ;
1415use super :: path_roots:: { project_with_detected_path_roots, resolve_effective_remote_path} ;
1516use super :: planning:: { load_project_components, plan_components} ;
17+ use super :: release_download:: { ReleaseArtifact , ReleaseArtifactStore } ;
1618use super :: types:: { ComponentDeployResult , DeployConfig , DeployOrchestrationResult , DeploySummary } ;
1719use super :: version_overrides:: fetch_remote_versions_for_project;
1820
@@ -34,6 +36,7 @@ pub(super) fn deploy_components(
3436 project : & Project ,
3537 ctx : & RemoteProjectContext ,
3638 base_path : & str ,
39+ release_artifacts : & mut ReleaseArtifactStore ,
3740) -> Result < DeployOrchestrationResult > {
3841 let loaded = load_project_components ( project, & config. component_ids , config. check ) ?;
3942 validate_supported_build_configs ( & loaded. deployable ) ?;
@@ -105,6 +108,31 @@ pub(super) fn deploy_components(
105108
106109 validate_effective_remote_paths ( & components, & project, base_path) ?;
107110
111+ // Release assets are immutable remote inputs. Resolve and verify them before
112+ // touching any configured checkout, then reuse the same run-scoped bytes for
113+ // every target/project that requests this component.
114+ let mut resolved_release_artifacts: HashMap < String , ReleaseArtifact > = HashMap :: new ( ) ;
115+ for component in & components {
116+ if let ReleaseArtifactPlan :: Reuse { tag, .. } =
117+ release_artifact_plan ( component, config, false , false )
118+ {
119+ let artifact = resolve_planned_release_artifact ( component, & tag, release_artifacts)
120+ . map_err ( |error| {
121+ Error :: validation_invalid_argument ( "releaseArtifact" , error, None , None )
122+ } ) ?;
123+ log_status ! (
124+ "deploy" ,
125+ "Verified release asset: tag={} name={} size={} sha256={} source={}" ,
126+ artifact. tag,
127+ artifact. name,
128+ artifact. size,
129+ artifact. sha256,
130+ artifact. url
131+ ) ;
132+ resolved_release_artifacts. insert ( component. id . clone ( ) , artifact) ;
133+ }
134+ }
135+
108136 // Resolve first, then materialize immutable detached worktrees for real deploys.
109137 // Dry-run resolves in `run_dry_run_mode` and never creates a worktree.
110138 let exact_ref_checkouts = if !config. dry_run {
@@ -172,35 +200,43 @@ pub(super) fn deploy_components(
172200 ) ?) ;
173201 }
174202
203+ // Only local builds require mutable checkout safety checks. Release assets are
204+ // resolved and verified above and must not read or alter a source checkout.
205+ let local_build_components: Vec < Component > = components
206+ . iter ( )
207+ . filter ( |component| !resolved_release_artifacts. contains_key ( & component. id ) )
208+ . cloned ( )
209+ . collect ( ) ;
210+
175211 // Sync: pull latest changes before deploying (unless --no-pull or --skip-build)
176212 if config. requested_ref . is_none ( ) && !config. no_pull && !config. skip_build {
177- sync_components ( & components ) ?;
213+ sync_components ( & local_build_components ) ?;
178214 }
179215
180216 // Warn when --head deploys from a non-default branch (safety guardrail)
181217 if config. head && !config. skip_build {
182- warn_non_default_branch ( & components , config) ?;
218+ warn_non_default_branch ( & local_build_components , config) ?;
183219 }
184220
185221 if config. requested_ref . is_none ( ) && !config. force {
186- check_uncommitted_changes ( & components ) ?;
222+ check_uncommitted_changes ( & local_build_components ) ?;
187223 }
188224
189225 // Check for HEAD-vs-tag gap before the tag checkout.
190226 if config. requested_ref . is_none ( ) && !config. head && !config. skip_build {
191- check_unreleased_commits ( & components , config) ?;
227+ check_unreleased_commits ( & local_build_components , config) ?;
192228 }
193229
194230 // Checkout the deploy tag for each component (unless --head or --skip-build).
195231 let tag_checkouts = if config. requested_ref . is_none ( ) && !config. head && !config. skip_build {
196- checkout_deploy_tags ( & components , config. expected_version . as_deref ( ) ) ?
232+ checkout_deploy_tags ( & local_build_components , config. expected_version . as_deref ( ) ) ?
197233 } else {
198234 Vec :: new ( )
199235 } ;
200236
201237 // Verify expected version if --version was specified
202238 if let Some ( ref expected) = config. expected_version {
203- if let Err ( err) = verify_expected_version ( & components , expected) {
239+ if let Err ( err) = verify_expected_version ( & local_build_components , expected) {
204240 if !tag_checkouts. is_empty ( ) {
205241 restore_branches ( & tag_checkouts) ;
206242 }
@@ -221,6 +257,7 @@ pub(super) fn deploy_components(
221257 base_path,
222258 & local_versions,
223259 & remote_versions,
260+ & resolved_release_artifacts,
224261 ) {
225262 Ok ( prepared) => prepared,
226263 Err ( failures) => {
@@ -255,6 +292,11 @@ pub(super) fn deploy_components(
255292 let exact_ref_identity = exact_ref_identities. get ( & component. id ) ;
256293 let deployed_ref = if let Some ( identity) = exact_ref_identity {
257294 Some ( identity. requested_ref . clone ( ) )
295+ } else if let Some ( artifact) = resolved_release_artifacts. get ( & component. id ) {
296+ Some ( match artifact. commit . as_deref ( ) {
297+ Some ( commit) => format ! ( "{} ({commit})" , artifact. tag) ,
298+ None => artifact. tag . clone ( ) ,
299+ } )
258300 } else if let Some ( checkout) = tag_checkouts
259301 . iter ( )
260302 . find ( |c| c. component_id == component. id )
@@ -289,6 +331,8 @@ pub(super) fn deploy_components(
289331 build_provenance. built_from_ref = deployed_ref;
290332 if let Some ( identity) = exact_ref_identity {
291333 build_provenance. built_from_commit = Some ( identity. resolved_sha . clone ( ) ) ;
334+ } else if let Some ( artifact) = resolved_release_artifacts. get ( & component. id ) {
335+ build_provenance. built_from_commit = artifact. commit . clone ( ) ;
292336 }
293337 result = result. with_build_provenance ( build_provenance) ;
294338
@@ -353,6 +397,7 @@ fn prepare_component_deployments(
353397 base_path : & str ,
354398 local_versions : & HashMap < String , String > ,
355399 remote_versions : & HashMap < String , String > ,
400+ release_artifacts : & HashMap < String , ReleaseArtifact > ,
356401) -> std:: result:: Result < Vec < PreparedComponentDeploy > , Vec < ComponentDeployResult > > {
357402 let mut prepared_deployments = Vec :: new ( ) ;
358403 let mut failures = Vec :: new ( ) ;
@@ -374,6 +419,7 @@ fn prepare_component_deployments(
374419 project,
375420 local_versions. get ( & component. id ) . cloned ( ) ,
376421 remote_versions. get ( & component. id ) . cloned ( ) ,
422+ release_artifacts. get ( & component. id ) . cloned ( ) ,
377423 ) {
378424 Ok ( prepared) => prepared_deployments. push ( prepared) ,
379425 Err ( result) => failures. push ( result) ,
@@ -1206,6 +1252,7 @@ mod tests {
12061252 "/srv/site" ,
12071253 & HashMap :: new ( ) ,
12081254 & HashMap :: new ( ) ,
1255+ & HashMap :: new ( ) ,
12091256 ) {
12101257 Ok ( _) => panic ! ( "a later missing artifact must abort the whole deploy batch" ) ,
12111258 Err ( failures) => failures,
@@ -1340,6 +1387,7 @@ mod tests {
13401387 "/srv/site" ,
13411388 & HashMap :: new ( ) ,
13421389 & HashMap :: new ( ) ,
1390+ & HashMap :: new ( ) ,
13431391 )
13441392 . expect ( "prepare exact-ref artifact" ) ;
13451393
@@ -1478,6 +1526,7 @@ mod tests {
14781526 "/srv/site" ,
14791527 & HashMap :: new ( ) ,
14801528 & HashMap :: new ( ) ,
1529+ & HashMap :: new ( ) ,
14811530 ) {
14821531 Ok ( _) => panic ! ( "failed build should abort preflight" ) ,
14831532 Err ( failures) => failures,
0 commit comments