@@ -2,6 +2,7 @@ package app
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "path/filepath"
78 "strings"
@@ -22,18 +23,21 @@ type ModulePinOptions struct {
2223 ProjectionOperationOptions
2324 ModulePath string
2425 GitmodulesName string
26+ Version string
27+ RuntimeConfig string
2528}
2629
2730type ModulePinAssessment struct {
28- ConsumerID string `json:"consumer_id"`
29- ModuleID string `json:"module_id"`
30- ConsumerRoot string `json:"consumer_root"`
31- ModuleRoot string `json:"module_root"`
32- GitmodulesName string `json:"gitmodules_name"`
33- GitlinkPath string `json:"gitlink_path"`
34- ExpectedOldOID string `json:"expected_old_oid"`
35- TargetOID string `json:"target_oid"`
36- TargetRef string `json:"target_ref"`
31+ ConsumerID string `json:"consumer_id"`
32+ ModuleID string `json:"module_id"`
33+ ConsumerRoot string `json:"consumer_root"`
34+ ModuleRoot string `json:"module_root"`
35+ GitmodulesName string `json:"gitmodules_name"`
36+ GitlinkPath string `json:"gitlink_path"`
37+ ExpectedOldOID string `json:"expected_old_oid"`
38+ TargetOID string `json:"target_oid"`
39+ TargetRef string `json:"target_ref"`
40+ Artifact * ModulePinArtifact `json:"artifact,omitempty"`
3741}
3842
3943type ModulePinPlanData struct {
@@ -48,18 +52,20 @@ type modulePinContext struct {
4852}
4953
5054type modulePinObserver struct {
51- services * Services
52- consumer string
53- module string
54- name string
55+ services * Services
56+ consumer string
57+ module string
58+ name string
59+ version string
60+ runtimeConfig string
5561}
5662
5763func (observer modulePinObserver ) Observe (
5864 ctx context.Context ,
5965 repositoryID string ,
6066) (operations.Observation , error ) {
6167 current , findings := observer .services .modulePinContext (
62- ctx , observer .consumer , observer .module , observer .name ,
68+ ctx , observer .consumer , observer .module , observer .name , observer . version , observer . runtimeConfig ,
6369 )
6470 if len (findings ) != 0 || current .assessment .ConsumerID != repositoryID {
6571 return operations.Observation {}, errors .New ("module pin precondition is no longer proven" )
@@ -75,7 +81,7 @@ func (services *Services) PlanModuleUpdatePin(
7581 if finding := validateLocalOperationIdentity (options .ProjectionOperationOptions ); finding != nil {
7682 return domain .NewEnvelope ("gds module update-pin plan" , domain .ExitInput , nil , * finding )
7783 }
78- current , findings := services .modulePinContext (ctx , path , options .ModulePath , options .GitmodulesName )
84+ current , findings := services .modulePinContext (ctx , path , options .ModulePath , options .GitmodulesName , options . Version , options . RuntimeConfig )
7985 if len (findings ) != 0 {
8086 return domain .NewEnvelope (
8187 "gds module update-pin plan" , classifyFindings (findings ), nil , findings ... ,
@@ -111,15 +117,7 @@ func (services *Services) PlanModuleUpdatePin(
111117 // this repository: provider lifecycle, rulesets, releases and anchors.
112118 Action : gitops .UpdateGitlinkAction , RequiresApproval : false ,
113119 Compensation : operations.Compensation {Mode : "explicit-plan" , Action : gitops .UpdateGitlinkAction },
114- Parameters : map [string ]any {"gitlink_pin" : map [string ]any {
115- "consumer_root" : current .assessment .ConsumerRoot ,
116- "module_root" : current .assessment .ModuleRoot ,
117- "module_id" : current .assessment .ModuleID ,
118- "gitmodules_name" : current .assessment .GitmodulesName ,
119- "expected_old_oid" : current .assessment .ExpectedOldOID ,
120- "target_oid" : current .assessment .TargetOID ,
121- "target_ref" : current .assessment .TargetRef ,
122- }},
120+ Parameters : map [string ]any {"gitlink_pin" : modulePinParameters (current .assessment )},
123121 }},
124122 ApprovalClass : "update-module-gitlink-pin" ,
125123 })
@@ -128,7 +126,7 @@ func (services *Services) PlanModuleUpdatePin(
128126 }
129127 engine := operations .NewDefaultEngine (
130128 store , services .Schemas ,
131- modulePinObserver {services : services , consumer : current .assessment .ConsumerRoot , module : current .assessment .ModuleRoot , name : current .assessment .GitmodulesName },
129+ modulePinObserver {services : services , consumer : current .assessment .ConsumerRoot , module : current .assessment .ModuleRoot , name : current .assessment .GitmodulesName , version : options . Version , runtimeConfig : options . RuntimeConfig },
132130 nil , options .DeviceID , options .SessionID ,
133131 )
134132 engine .Now = services .Now
@@ -173,8 +171,8 @@ func (services *Services) ApplyModuleUpdatePin(
173171 }
174172 engine := operations .NewDefaultEngine (
175173 store , services .Schemas ,
176- modulePinObserver {services : services , consumer : assessment .ConsumerRoot , module : assessment .ModuleRoot , name : assessment .GitmodulesName },
177- map [string ]operations.ActionHandler {gitops .UpdateGitlinkAction : handler },
174+ modulePinObserver {services : services , consumer : assessment .ConsumerRoot , module : assessment .ModuleRoot , name : assessment .GitmodulesName , version : modulePinVersion ( assessment ), runtimeConfig : options . RuntimeConfig },
175+ map [string ]operations.ActionHandler {gitops .UpdateGitlinkAction : services . modulePinHandler ( handler , assessment , plan , options . RuntimeConfig ) },
178176 options .DeviceID , options .SessionID ,
179177 )
180178 engine .Now = services .Now
@@ -225,7 +223,7 @@ func (services *Services) VerifyModuleUpdatePin(
225223 }
226224 engine := operations .NewDefaultEngine (
227225 store , services .Schemas , modulePinObserver {},
228- map [string ]operations.ActionHandler {gitops .UpdateGitlinkAction : handler },
226+ map [string ]operations.ActionHandler {gitops .UpdateGitlinkAction : services . modulePinHandler ( handler , assessment , plan , options . RuntimeConfig ) },
229227 options .DeviceID , options .SessionID ,
230228 )
231229 engine .Now = services .Now
@@ -247,6 +245,8 @@ func (services *Services) modulePinContext(
247245 consumerPath string ,
248246 modulePath string ,
249247 gitmodulesName string ,
248+ version string ,
249+ runtimeConfig string ,
250250) (modulePinContext , []domain.Finding ) {
251251 if strings .TrimSpace (modulePath ) == "" || strings .TrimSpace (gitmodulesName ) == "" {
252252 return modulePinContext {}, []domain.Finding {modulePinFinding (
@@ -306,37 +306,39 @@ func (services *Services) modulePinContext(
306306 "GDS_MODULE_PIN_IDENTITY_MISMATCH" , "Selected module boundary does not match the typed consumer relationship." ,
307307 )}
308308 }
309- if moduleAnchor .Module .PinPolicy != "default-branch-commit" {
310- return modulePinContext {}, []domain.Finding {modulePinFinding (
311- "GDS_MODULE_PIN_RELEASE_REQUIRED" , "This module pin policy requires a verified versioned release before consumer update." ,
312- )}
309+ if moduleAnchor .Module .PinPolicy != "default-branch-commit" && moduleAnchor .Module .PinPolicy != "version-tag" {
310+ return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_RELEASE_REQUIRED" , "Module pin policy requires an unsupported publication provider." )}
311+ }
312+ if (moduleAnchor .Module .PinPolicy == "version-tag" ) != (strings .TrimSpace (version ) != "" ) {
313+ return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_VERSION_REQUIRED" , "Select --version exactly when the module pin policy is version-tag." )}
313314 }
314315 moduleInfo , err := services .Git .RepositoryInfo (ctx , modulePath )
315316 if err != nil {
316317 return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_SOURCE_NOT_PROVEN" , err .Error ())}
317318 }
318319 moduleRoot := moduleInfo .WorktreeRoot
319320 moduleStatus , err := services .Git .InspectStatus (ctx , moduleRoot )
320- if err != nil || moduleStatus .Head .Mode != "branch" ||
321- moduleStatus .Branch .Name != moduleAnchor .Git .DefaultBranch || ! checkoutStatusIsClean (moduleStatus ) {
322- return modulePinContext {}, []domain.Finding {modulePinFinding (
323- "GDS_MODULE_PIN_SOURCE_STATE_UNSAFE" , "Module source must be clean on its default branch." ,
324- )}
321+ if err != nil || ! checkoutStatusIsClean (moduleStatus ) || moduleStatus .Head .OID == "" {
322+ return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_SOURCE_STATE_UNSAFE" , "Module source must be clean at the selected commit." )}
325323 }
326- // The module's origin is observed, never written. `LocalPushSupported` used
327- // to guard this line, which asks whether the module's remote accepts a push
328- // from this device -- a question this operation never needs, since the only
329- // mutation is a gitlink rewrite in the consumer. It refuses every remote that
330- // is not a local path, so on a real estate it refused every module, and the
331- // pin could not advance for that reason alone. `ObserveRemoteBranchOptional`
332- // is an `ls-remote`, and proving the target commit is published is exactly
333- // what this step is for.
334324 targetRef := "refs/heads/" + moduleAnchor .Git .DefaultBranch
335- targetOID , found , err := services .GitMutations .ObserveRemoteBranchOptional (ctx , moduleRoot , "origin" , targetRef )
336- if err != nil || ! found || targetOID != moduleStatus .Head .OID {
337- return modulePinContext {}, []domain.Finding {modulePinFinding (
338- "GDS_MODULE_PIN_TARGET_NOT_PUBLISHED" , "Module default commit is not exactly published on its configured origin." ,
339- )}
325+ targetOID := ""
326+ var artifact * ModulePinArtifact
327+ if moduleAnchor .Module .PinPolicy == "version-tag" {
328+ artifact , err = services .observeModulePinArtifact (ctx , moduleRoot , version , runtimeConfig )
329+ if err != nil {
330+ return modulePinContext {}, []domain.Finding {modulePinArtifactFinding (err )}
331+ }
332+ targetRef , targetOID = artifact .Tag .TagRef , artifact .Tag .CommitOID
333+ } else {
334+ if moduleStatus .Head .Mode != "branch" || moduleStatus .Branch .Name != moduleAnchor .Git .DefaultBranch {
335+ return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_SOURCE_STATE_UNSAFE" , "Module source must be clean on its default branch." )}
336+ }
337+ var found bool
338+ targetOID , found , err = services .GitMutations .ObserveRemoteBranchOptional (ctx , moduleRoot , "origin" , targetRef )
339+ if err != nil || ! found || targetOID != moduleStatus .Head .OID {
340+ return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_TARGET_NOT_PUBLISHED" , "Module default commit is not exactly published on its configured origin." )}
341+ }
340342 }
341343 // Resolve cheap eligibility and policy failures before materializing a
342344 // throwaway checkout or invoking any module command. Rejected pins must
@@ -401,8 +403,9 @@ func (services *Services) modulePinContext(
401403 // Verification joins the fingerprint so the plan is bound to the evidence
402404 // that justified it. A plan approved while a lane passed must not stay
403405 // applicable after that lane stops passing.
404- Verification string `json:"verification"`
405- }{consumerStatus , * submodule , moduleAnchor .Repository .ID , moduleStatus .Head .OID , moduleManifestDigest , targetRef , targetOID , verificationDigest })
406+ Verification string `json:"verification"`
407+ Artifact * ModulePinArtifact `json:"artifact,omitempty"`
408+ }{consumerStatus , * submodule , moduleAnchor .Repository .ID , moduleStatus .Head .OID , moduleManifestDigest , targetRef , targetOID , verificationDigest , artifact })
406409 if err != nil {
407410 return modulePinContext {}, []domain.Finding {modulePinFinding ("GDS_MODULE_PIN_FINGERPRINT_FAILED" , err .Error ())}
408411 }
@@ -411,7 +414,7 @@ func (services *Services) modulePinContext(
411414 ConsumerID : consumer .Repository .ID , ModuleID : moduleAnchor .Repository .ID ,
412415 ConsumerRoot : consumerInfo .WorktreeRoot , ModuleRoot : moduleRoot ,
413416 GitmodulesName : gitmodulesName , GitlinkPath : submodule .Path ,
414- ExpectedOldOID : submodule .GitlinkOID , TargetOID : targetOID , TargetRef : targetRef ,
417+ ExpectedOldOID : submodule .GitlinkOID , TargetOID : targetOID , TargetRef : targetRef , Artifact : artifact ,
415418 },
416419 observation : operations.Observation {
417420 RepositoryID : consumer .Repository .ID , HeadOID : consumerStatus .Head .OID ,
@@ -468,6 +471,19 @@ func loadModulePinPlan(
468471 assessment .ExpectedOldOID , _ = raw ["expected_old_oid" ].(string )
469472 assessment .TargetOID , _ = raw ["target_oid" ].(string )
470473 assessment .TargetRef , _ = raw ["target_ref" ].(string )
474+ if raw ["artifact" ] != nil {
475+ encoded , marshalErr := json .Marshal (raw ["artifact" ])
476+ if marshalErr != nil || json .Unmarshal (encoded , & assessment .Artifact ) != nil ||
477+ assessment .Artifact == nil || assessment .Artifact .Version == "" ||
478+ assessment .Artifact .Tag .TagRef != assessment .TargetRef ||
479+ assessment .Artifact .Tag .CommitOID != assessment .TargetOID ||
480+ assessment .Artifact .Tag .TagOID == "" || assessment .Artifact .ManifestDigest == "" {
481+ return operations.Plan {}, ModulePinAssessment {}, errors .New ("module artifact parameters are invalid" )
482+ }
483+ }
484+ if strings .HasPrefix (assessment .TargetRef , "refs/tags/" ) && assessment .Artifact == nil {
485+ return operations.Plan {}, ModulePinAssessment {}, errors .New ("version pin plan lacks artifact evidence" )
486+ }
471487 if assessment .ConsumerRoot == "" || assessment .ModuleRoot == "" || assessment .ModuleID == "" ||
472488 assessment .GitmodulesName == "" || assessment .ExpectedOldOID == "" ||
473489 assessment .TargetOID == "" || assessment .TargetRef == "" {
@@ -535,3 +551,16 @@ func pinWorktreeStateIsEligible(submodule gitprovider.Submodule, targetOID strin
535551 return false
536552 }
537553}
554+
555+ func modulePinParameters (assessment ModulePinAssessment ) map [string ]any {
556+ result := map [string ]any {
557+ "consumer_root" : assessment .ConsumerRoot , "module_root" : assessment .ModuleRoot ,
558+ "module_id" : assessment .ModuleID , "gitmodules_name" : assessment .GitmodulesName ,
559+ "expected_old_oid" : assessment .ExpectedOldOID , "target_oid" : assessment .TargetOID ,
560+ "target_ref" : assessment .TargetRef ,
561+ }
562+ if assessment .Artifact != nil {
563+ result ["artifact" ] = assessment .Artifact
564+ }
565+ return result
566+ }
0 commit comments