@@ -14,7 +14,10 @@ import (
1414
1515 "github.com/operatorstack/boatstack/boatstack/controlprogram"
1616 softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery"
17+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/durable"
18+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects"
1719 "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model"
20+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant"
1821 "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol"
1922 "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces"
2023)
@@ -59,6 +62,7 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions,
5962 if options .flowProgramFingerprint != "" && options .flowProgramFingerprint != compiled .Fingerprint {
6063 return commandOptions {}, fmt .Errorf ("FLOW_PROGRAM_DRIFT: run fingerprint does not match the current artifact" )
6164 }
65+ options .flowProgramFingerprint = compiled .Fingerprint
6266 objective , err := softwareflow .ObjectiveForEntry (ctx , compiled , resolver , options .entryID )
6367 if err != nil {
6468 return commandOptions {}, err
@@ -67,27 +71,35 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions,
6771 if ! ok {
6872 return commandOptions {}, fmt .Errorf ("FLOW_ENTRY_UNKNOWN: %s" , options .entryID )
6973 }
70- plan , deliveryID , err := resolveBoundPlan ( repository , entry , options )
74+ options , err = bindActiveFlowContext ( ctx , repository , options , objective )
7175 if err != nil {
7276 return commandOptions {}, err
7377 }
74- planRaw , err := os .ReadFile (plan )
75- if err != nil {
76- return commandOptions {}, fmt .Errorf ("FLOW_INPUT_REQUIRED: read selected plan: %w" , err )
77- }
78- planDigest := sha256 .Sum256 (planRaw )
79- planFingerprint := hex .EncodeToString (planDigest [:])
80- repositoryIdentity , err := flowRepositoryIdentity (repository )
78+ plan , deliveryID , err := resolveBoundPlan (repository , entry , objective , options )
8179 if err != nil {
8280 return commandOptions {}, err
8381 }
84- runID := flowRunID (repositoryIdentity , compiled .Fingerprint , options .entryID , deliveryID , planFingerprint )
85- if options .runID != "" && options .runID != runID {
86- return commandOptions {}, fmt .Errorf ("FLOW_RUN_MISMATCH: run ID does not identify the selected plan and repository" )
82+ planFingerprint := ""
83+ if plan != "" {
84+ planRaw , readErr := os .ReadFile (plan )
85+ if readErr != nil {
86+ return commandOptions {}, fmt .Errorf ("FLOW_INPUT_REQUIRED: read selected plan: %w" , readErr )
87+ }
88+ planDigest := sha256 .Sum256 (planRaw )
89+ planFingerprint = hex .EncodeToString (planDigest [:])
90+ repositoryIdentity , identityErr := flowRepositoryIdentity (repository )
91+ if identityErr != nil {
92+ return commandOptions {}, identityErr
93+ }
94+ runID := flowRunID (repositoryIdentity , compiled .Fingerprint , options .entryID , deliveryID , planFingerprint )
95+ if options .runID != "" && options .runID != runID {
96+ return commandOptions {}, fmt .Errorf ("FLOW_RUN_MISMATCH: run ID does not identify the selected plan and repository" )
97+ }
98+ options .runID = runID
99+ } else if options .runID == "" {
100+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_INVALID: active abandonment has no committed run identity" )
87101 }
88102 options .repository = repository
89- options .flowProgramFingerprint = compiled .Fingerprint
90- options .runID = runID
91103 if options .objectiveKind == "" {
92104 options .objectiveKind = string (objective )
93105 }
@@ -137,6 +149,80 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions,
137149 return options , nil
138150}
139151
152+ func bindActiveFlowContext (ctx context.Context , repository string , options commandOptions , entryObjective model.ObjectiveKind ) (commandOptions , error ) {
153+ if options .runID != "" && entryObjective != model .ObjectiveAbandoned {
154+ return options , nil
155+ }
156+ resolver , err := plant .NewResolver ("" )
157+ if err != nil {
158+ return commandOptions {}, err
159+ }
160+ host := options .host
161+ if host == "" {
162+ host = "cli"
163+ }
164+ invocation , err := resolver .ResolveInvocation (ctx , repository , host , "flow-entry-resume" )
165+ if err != nil {
166+ common , commonErr := flowRepositoryIdentity (repository )
167+ if commonErr == nil {
168+ if _ , stateErr := os .Stat (filepath .Join (common , "boatstack" , "v2" )); os .IsNotExist (stateErr ) {
169+ return options , nil
170+ }
171+ }
172+ if _ , stateErr := os .Stat (filepath .Join (repository , ".git" , "boatstack" )); stateErr != nil {
173+ return options , nil
174+ }
175+ return commandOptions {}, err
176+ }
177+ layout , _ , err := resolver .ResolveLayout (ctx , invocation )
178+ if err != nil {
179+ return commandOptions {}, err
180+ }
181+ raw , err := os .ReadFile (layout .StatePath )
182+ if os .IsNotExist (err ) {
183+ return options , nil
184+ }
185+ if err != nil {
186+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_INVALID: read durable state: %w" , err )
187+ }
188+ state , err := durable .DecodeState (raw )
189+ if err != nil {
190+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_INVALID: decode durable state: %w" , err )
191+ }
192+ active , ok := state .ActiveObjective ()
193+ if ! ok {
194+ return options , nil
195+ }
196+ prefix := "objective-" + options .programID + "-" + options .entryID + "-"
197+ receipt , found , findErr := effects .FindLatestCommittedFlowForObjective (layout , invocation , active , state .Revision )
198+ if findErr != nil {
199+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_INVALID: inspect committed flow receipts: %w" , findErr )
200+ }
201+ if ! found || ! strings .HasPrefix (receipt .FlowID , "run-" ) {
202+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_INVALID: active objective has no committed run identity" )
203+ }
204+ if active .Kind == entryObjective && strings .HasPrefix (active .ID , prefix ) {
205+ options .runID , options .deliveryID = receipt .FlowID , active .DeliveryID
206+ options .objectiveID , options .objectiveKind = active .ID , string (active .Kind )
207+ options .activeFlowBound = true
208+ return options , nil
209+ }
210+ if entryObjective == model .ObjectiveAbandoned {
211+ repositoryIdentity , identityErr := flowRepositoryIdentity (repository )
212+ if identityErr != nil {
213+ return commandOptions {}, identityErr
214+ }
215+ expectedRunID := flowRunID (repositoryIdentity , options .flowProgramFingerprint , options .entryID , active .DeliveryID , "active-run:" + receipt .FlowID )
216+ if options .runID != "" && options .runID != expectedRunID {
217+ return commandOptions {}, fmt .Errorf ("FLOW_RUN_MISMATCH: run ID does not identify the active delivery" )
218+ }
219+ options .runID = expectedRunID
220+ options .deliveryID , options .activeFlowBound = active .DeliveryID , true
221+ return options , nil
222+ }
223+ return commandOptions {}, fmt .Errorf ("FLOW_ACTIVE_RUN_CONFLICT: delivery %q is active under objective %q; abandon it before selecting another inbox plan" , active .DeliveryID , active .ID )
224+ }
225+
140226func validateResolvedParameter (parameters protocol.Parameters , name , expected string ) error {
141227 if actual , exists := parameters .Get (name ); exists && actual != expected {
142228 return fmt .Errorf ("FLOW_INPUT_MISMATCH: parameter %s conflicts with the entry-resolved value" , name )
@@ -186,8 +272,11 @@ func bindRPCFlowEntry(ctx context.Context, request surfaces.Request) (surfaces.R
186272 return request , nil
187273}
188274
189- func resolveBoundPlan (repository string , entry controlprogram.Entry , options commandOptions ) (string , string , error ) {
190- if options .runID == "" {
275+ func resolveBoundPlan (repository string , entry controlprogram.Entry , entryObjective model.ObjectiveKind , options commandOptions ) (string , string , error ) {
276+ if options .activeFlowBound && entryObjective == model .ObjectiveAbandoned {
277+ return "" , options .deliveryID , nil
278+ }
279+ if options .runID == "" && options .deliveryID == "" {
191280 return resolvePlanInput (repository , entry )
192281 }
193282 if ! flowSegment .MatchString (options .deliveryID ) {
0 commit comments