Skip to content

Commit 150a855

Browse files
committed
Add governed foreground work to repository flows
1 parent 17e89ed commit 150a855

71 files changed

Lines changed: 4190 additions & 250 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/tests/test_detached_supervision.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
435435
self.assertEqual(bound["decision"]["kind"], "PRESCRIBED")
436436
self.assertEqual(bound["decision"]["transition"]["id"], "plan.create")
437437

438-
def test_repository_authority_rematerialization_fails_closed_without_verified_config(self) -> None:
438+
def test_repository_authority_rematerialization_waits_for_verified_config(self) -> None:
439439
# control-law: repository-authority-requires-exact-verified-fingerprint
440440
root = Path(self.work.name) / "unverified"
441441
root.mkdir()
@@ -446,19 +446,18 @@ def test_repository_authority_rematerialization_fails_closed_without_verified_co
446446
self._git(root, "add", "README.md")
447447
self._git(root, "commit", "-m", "fixture")
448448
before = self.porcelain(root)
449-
result = self.run_helper(
449+
result = self.helper_json(
450450
"next", "--repo", root,
451451
"--objective-id", "unverified-authority",
452452
"--target-id", "open-or-updated-pr",
453453
"--delivery", "unverified-authority",
454454
"--run-id", "flow-unverified-authority",
455455
"--human", "contract", "--repository-authority",
456-
cwd=root, expected=1,
457-
)
458-
self.assertIn(
459-
"repository authority requires current verified configuration evidence",
460-
result.stderr,
456+
cwd=root,
461457
)
458+
self.assertEqual(result["decision"]["kind"], "CANDIDATE")
459+
self.assertEqual(result["decision"]["transition"]["id"], "installation.initialize")
460+
self.assertNotIn('"repository-policy"', json.dumps(result))
462461
self.assertEqual(self.porcelain(root), before)
463462

464463

boatstack/cmd/boatstack-helper/delegation_command.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import (
1919
"github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces"
2020
)
2121

22+
var resolveGitHubProviderAuthority = func(ctx context.Context, repository, previewFingerprint string, now time.Time) (protocol.AuthorityReceipt, error) {
23+
return effects.NewNativeBoundary().ResolveGitHubProviderAuthority(ctx, repository, previewFingerprint, now)
24+
}
25+
2226
func runFlowAuthorize(arguments []string) error {
2327
flags := flag.NewFlagSet("flow authorize", flag.ContinueOnError)
2428
flags.SetOutput(os.Stderr)
@@ -258,7 +262,13 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
258262
if err != nil {
259263
return resolved, err
260264
}
261-
rebound, changed, rebindErr := bindContinuationCandidate(ctx, bound, resolved)
265+
rebound, changed, rebindErr := bindTrustedProviderCandidate(ctx, bound, resolved)
266+
if rebindErr != nil {
267+
return surfaces.Response{}, rebindErr
268+
}
269+
if !changed {
270+
rebound, changed, rebindErr = bindContinuationCandidate(ctx, bound, resolved)
271+
}
262272
if rebindErr != nil {
263273
return surfaces.Response{}, rebindErr
264274
}
@@ -335,6 +345,21 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
335345
return applied, nil
336346
}
337347

348+
func bindTrustedProviderCandidate(ctx context.Context, bound commandOptions, response surfaces.Response) (commandOptions, bool, error) {
349+
if bound.transitionID != "" || response.Prescription != nil || response.Decision == nil ||
350+
(response.Decision.Kind != supervisor.DecisionFrontier && response.Decision.Kind != supervisor.DecisionCandidate) ||
351+
len(response.Decision.Candidates) != 1 || response.Decision.Candidates[0] != "publication.execute" {
352+
return bound, false, nil
353+
}
354+
rebound := bound
355+
rebound.transitionID = "publication.execute"
356+
rebound, err := bindFlowEntry(ctx, rebound)
357+
if err != nil {
358+
return commandOptions{}, false, err
359+
}
360+
return rebound, true, nil
361+
}
362+
338363
func bindContinuationCandidate(ctx context.Context, bound commandOptions, response surfaces.Response) (commandOptions, bool, error) {
339364
if bound.transitionID != "" || response.Prescription != nil || response.Decision == nil || response.Decision.Kind != supervisor.DecisionCandidate || response.Decision.Transition == nil || len(response.Decision.Candidates) != 1 {
340365
return bound, false, nil
@@ -381,5 +406,6 @@ func advanceContinuation(options *commandOptions, response surfaces.Response) er
381406
options.requiredCapabilities = nil
382407
options.effectiveCapabilities = nil
383408
options.idempotencyKey = ""
409+
options.trustedAuthorityReceipts = nil
384410
return nil
385411
}

boatstack/cmd/boatstack-helper/flow_command.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime"
2222
)
2323

24-
const flowCompilerVersion = "control-program.compiler.1"
24+
const flowCompilerVersion = "control-program.compiler.2"
2525

2626
type flowCommandOptions struct {
2727
repository string
@@ -33,7 +33,7 @@ type flowCommandOptions struct {
3333

3434
func runFlowCommand(arguments []string) error {
3535
if len(arguments) == 0 {
36-
return fmt.Errorf("usage: boatstack flow <compile|check|authorize|revoke|run> [flags]")
36+
return fmt.Errorf("usage: boatstack flow <compile|check|authorize|revoke|run|work> [flags]")
3737
}
3838
action := arguments[0]
3939
if action == "authorize" {
@@ -45,6 +45,9 @@ func runFlowCommand(arguments []string) error {
4545
if action == "run" {
4646
return runFlowContinuation(arguments[1:])
4747
}
48+
if action == "work" {
49+
return runFlowWork(arguments[1:])
50+
}
4851
flags := flag.NewFlagSet("flow "+action, flag.ContinueOnError)
4952
flags.SetOutput(os.Stderr)
5053
options := flowCommandOptions{}
@@ -116,7 +119,7 @@ func compileFlow(ctx context.Context, options flowCommandOptions) error {
116119
if err != nil {
117120
return err
118121
}
119-
compiled, err := controlprogram.Load(bytes.NewReader(rawIR), resolver)
122+
compiled, err := controlprogram.LoadWithAssets(bytes.NewReader(rawIR), resolver, controlprogram.RepositoryAssetResolver{Repository: options.repository})
120123
if err != nil {
121124
return err
122125
}
@@ -162,13 +165,29 @@ func compileFlow(ctx context.Context, options flowCommandOptions) error {
162165
writes = append(writes, boatstackruntime.ProjectionWrite{
163166
Path: artifactPath, Content: artifactRaw, Mode: 0o644, ExpectedPreviousSHA256: artifactPrevious, PublishLast: true,
164167
})
165-
if err := rejectProjectionInputOverlap(lockPath, writes, removals); err != nil {
166-
return err
167-
}
168168
expectations := []boatstackruntime.ProjectionExpectation{
169169
{Path: source, Exists: true, ExpectedSHA256: fileDigest(sourceRaw)},
170170
{Path: lockPath, Exists: true, ExpectedSHA256: fileDigest(lockRaw)},
171171
}
172+
compileInputs := []string{source, lockPath}
173+
assetPaths := make([]string, 0, len(artifact.Assets))
174+
for relative := range artifact.Assets {
175+
assetPaths = append(assetPaths, relative)
176+
}
177+
sort.Strings(assetPaths)
178+
for _, relative := range assetPaths {
179+
absolute, pathErr := exactRepositoryPath(options.repository, relative)
180+
if pathErr != nil {
181+
return pathErr
182+
}
183+
compileInputs = append(compileInputs, absolute)
184+
expectations = append(expectations, boatstackruntime.ProjectionExpectation{
185+
Path: absolute, Exists: true, ExpectedSHA256: artifact.Assets[relative],
186+
})
187+
}
188+
if err := rejectProjectionInputOverlap(compileInputs, writes, removals); err != nil {
189+
return err
190+
}
172191
artifactRelative, _ := filepath.Rel(options.repository, artifactPath)
173192
nextOwnership := boatstackruntime.NewFlowProjectionOwnership(filepath.ToSlash(sourceRelative), filepath.ToSlash(artifactRelative), artifactRaw, skills)
174193
if err := boatstackruntime.ApplyOwnedFlowProjection(options.repository, writes, removals, expectations, ownership, nextOwnership); err != nil {
@@ -177,16 +196,19 @@ func compileFlow(ctx context.Context, options flowCommandOptions) error {
177196
return renderFlowResult("compiled", artifactPath, artifact)
178197
}
179198

180-
func rejectProjectionInputOverlap(lockPath string, writes []boatstackruntime.ProjectionWrite, removals []boatstackruntime.ProjectionRemoval) error {
181-
lockPath = filepath.Clean(lockPath)
199+
func rejectProjectionInputOverlap(inputs []string, writes []boatstackruntime.ProjectionWrite, removals []boatstackruntime.ProjectionRemoval) error {
200+
bound := make(map[string]bool, len(inputs))
201+
for _, input := range inputs {
202+
bound[filepath.Clean(input)] = true
203+
}
182204
for _, write := range writes {
183-
if filepath.Clean(write.Path) == lockPath {
184-
return fmt.Errorf("FLOW_COMPILE_INPUT_OVERLAP: dependency lock is a projection output")
205+
if bound[filepath.Clean(write.Path)] {
206+
return fmt.Errorf("FLOW_COMPILE_INPUT_OVERLAP: compile input %s is a projection output", write.Path)
185207
}
186208
}
187209
for _, removal := range removals {
188-
if filepath.Clean(removal.Path) == lockPath {
189-
return fmt.Errorf("FLOW_COMPILE_INPUT_OVERLAP: dependency lock is a retired projection output")
210+
if bound[filepath.Clean(removal.Path)] {
211+
return fmt.Errorf("FLOW_COMPILE_INPUT_OVERLAP: compile input %s is a retired projection output", removal.Path)
190212
}
191213
}
192214
return nil

0 commit comments

Comments
 (0)