Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49ed6e2
fix(run): build recipe images for local container runtimes
antoinetoussaint-byte Oct 8, 2026
b04d4fc
Merge current main into PR934 and hand local runtimes immutable image…
antoinetoussaint-byte Oct 13, 2026
17b3a0c
fix(run): hand agents a verified canonical local image digest
antoinetoussaint-byte Oct 13, 2026
b4aa8e2
fix(composition): route foreign methods to their selected service
gael-obin Oct 27, 2026
55a80a1
merge: integrate main fixture and hop configuration improvements
gael-obin Oct 27, 2026
59cd78e
build(deps): select Core's merged foreign-method service selector
gael-obin Oct 27, 2026
9e9e36a
style(composition): lift the foreign-method selector check out of the…
gael-obin Oct 27, 2026
917b398
Merge origin/main into issue-932: the local runtime image rides the p…
antoinetoussaint-byte Oct 27, 2026
bb77dc3
Merge origin/main (ad957aee) into issue-932
antoinetoussaint-byte Oct 27, 2026
a035440
fix(run): say which image the local runtime was handed
antoinetoussaint-byte Oct 27, 2026
88206da
Merge origin/main (8056b9da) into issue-932
antoinetoussaint-byte Oct 28, 2026
c1ee0e8
feat(recover): consume Core exact-generation process recovery receipts
antoinetoussaint-byte Oct 28, 2026
846f821
fix(recover): share Core receipt bound and verify encoded round trips
antoinetoussaint-byte Oct 28, 2026
78be6b4
Merge current main including CLI1210 into existing PR934
antoinetoussaint-byte Oct 28, 2026
b28ebb0
Merge existing CLI1196 selector implementation into PR934
antoinetoussaint-byte Oct 28, 2026
4513cfe
docs(generate): name the runtime owner artifact renderer
antoinetoussaint-byte Oct 28, 2026
c16938e
feat(recover): stop an explicit container through Core receipt checks
antoinetoussaint-byte Oct 28, 2026
a92a10b
build: adopt released Core v0.19.9 recovery APIs
antoinetoussaint-byte Oct 28, 2026
a7ea869
feat(recover): force an explicitly selected process through Core iden…
antoinetoussaint-byte Oct 28, 2026
6a2d69e
fix(deps): adopt published Core v0.19.10 process inspection
antoinetoussaint-byte Oct 28, 2026
af90f9d
fix(ci): isolate disposable recipe image outputs
antoinetoussaint-byte Oct 28, 2026
3f92451
fix(ci): admit replay and scheduled image prerequisites before dispatch
antoinetoussaint-byte Oct 28, 2026
8d0ef78
test(ci): retain disposable command evidence before assertions
antoinetoussaint-byte Oct 28, 2026
48a517c
fix(generate): derive runnable audiences from owner publications
antoinetoussaint-byte Oct 28, 2026
dbc000a
test(doctor): declare runnable owner in binding fixture
antoinetoussaint-byte Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/ci/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ func initBuildService(ctx context.Context, workspace *resources.Workspace, modul
flow.WithBuildCache(cache)
flow.WithRebuild(buildCacheFlags.Rebuild())
flow.WithImageSBOM(ciImageSBOM)
if disposableRuntime {
flow.WithDisposableImages()
}
flow.WithOutputSink(cli.NewOutputSink())
flow.WithLoadOnly(loadOnly)
flow.WithInitOnly(initOnly)
Expand Down
6 changes: 6 additions & 0 deletions cmd/ci/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type ScheduleOptions struct {
Jobs int
FailFast bool
LockDependencyClosure bool
DisposableImages bool
Phase string
Suite string
RuntimeContext string
Expand Down Expand Up @@ -624,6 +625,11 @@ func scheduleStage(options ScheduleOptions) resources.Stage {
}

func buildScheduledTasks(ctx context.Context, workspace *resources.Workspace, plan *Plan, options ScheduleOptions) ([]ciScheduledTask, error) {
if options.DisposableImages {
if err := checkDisposablePlan(ctx, workspace, plan, options.Phase); err != nil {
return nil, err
}
}
if plan.replay == nil {
return resolveScheduledTasks(ctx, workspace, plan, options)
}
Expand Down
66 changes: 66 additions & 0 deletions cmd/ci/disposable_images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ci

import (
"context"
"slices"

"github.com/codefly-dev/core/architecture"

"github.com/codefly-dev/cli/pkg/orchestration"
"github.com/codefly-dev/core/resources"
)

// Admit the entire selected request before dispatching any phase or job. Use
// normal flow selection for configuration-reference participants and environment
// exclusions; this constructs no agent managers. Each actual flow checks again.
func checkDisposablePlan(ctx context.Context, workspace *resources.Workspace, plan *Plan, phases ...string) error {
// Only build scheduling expands prerequisites into additional tasks. Its
// StageBuild closure is actual dispatched work, even if a Flow excludes a
// bridge. Do not traverse raw runtime branches that no Flow would start.
if slices.Contains(phases, string(resources.PhaseBuild)) {
dependencies, err := architecture.NewServiceDependencies(ctx, workspace)
if err != nil {
return err
}
roots := make([]string, 0, len(plan.Services))
for _, selected := range plan.Services {
roots = append(roots, selected.Service)
}
if err := orchestration.CheckDisposableImageBuildPrerequisites(ctx, dependencies, roots); err != nil {
return err
}
}

cache, err := buildCacheFlags.Policy()
if err != nil {
return err
}
for _, selected := range plan.Services {
ref, err := resources.ParseServiceWithOptionalModule(selected.Service)
if err != nil {
return err
}
module, err := workspace.LoadModuleFromName(ctx, ref.Module)
if err != nil {
return err
}
service, err := module.LoadServiceFromName(ctx, ref.Name)
if err != nil {
return err
}
env, err := orchestration.SelectEnvironment(workspace, orchestration.LocalEnvironmentName)
if err != nil {
return err
}
flow, err := orchestration.NewFlow(ctx, workspace, module, service, env, orchestration.TestMode)
if err != nil {
return err
}
flow.WithDisposableImages()
flow.WithBuildCache(cache)
if err := flow.CheckDisposableImageInputs(ctx); err != nil {
return err
}
}
return nil
}
155 changes: 155 additions & 0 deletions cmd/ci/disposable_images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package ci

import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/codefly-dev/core/resources"
"github.com/stretchr/testify/require"
)

func TestDisposableCIRefusesPrerequisitesBeforeAnyScheduledAction(t *testing.T) {
root, workspace := loadSchedulerFixture(t)
path := filepath.Join(root, "modules/web/services/frontend/service.codefly.yaml")
raw, err := os.ReadFile(path)
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, []byte(strings.Replace(string(raw), "- name: gateway", "- name: organization\n module: management\n kind: build", 1)), 0600))
plan := &Plan{Services: []PlannedService{{Service: "management/organization"}, {Service: "web/frontend"}}}
for _, phase := range []string{"lint", "compile", "test", "build"} {
t.Run(phase, func(t *testing.T) {
calls := 0
err := WithPlanOptions(t.Context(), workspace, plan, func(context.Context, *resources.Workspace, *resources.Module, *resources.Service) error {
calls++
return nil
}, ScheduleOptions{Jobs: 2, Phase: phase, DisposableImages: true})
require.ErrorContains(t, err, "disposable image input rebinding is unsupported")
require.Zero(t, calls, "even the producer must not run before rejection")
})
}
}

func TestDisposableCISchedulesIndependentServiceWithoutUnrelatedRefusal(t *testing.T) {
_, workspace := loadSchedulerFixture(t)
plan := &Plan{Services: []PlannedService{{Service: "management/worker"}}}
calls := 0
err := WithPlanOptions(t.Context(), workspace, plan, func(context.Context, *resources.Workspace, *resources.Module, *resources.Service) error {
calls++
return nil
}, ScheduleOptions{Phase: "build", DisposableImages: true})
require.NoError(t, err)
require.Equal(t, 1, calls)
}

// This fixture has no exported endpoints, so the withdrawal is independently
// valid. Its raw scheduler graph and selected Flow graph deliberately differ.
func disposableAdmissionWorkspace(t *testing.T, edge string, excluded bool) (string, *resources.Workspace) {
t.Helper()
root := t.TempDir()
workspace := "name: disposable-admission\nlayout: modules\nmodules:\n - name: app\nenvironments:\n - name: local\n"
if excluded {
workspace += " exclude-services:\n - service: app/intermediate\n withdrawn-from: [app/origin]\n"
}
files := map[string]string{
"workspace.codefly.yaml": workspace,
"modules/app/module.codefly.yaml": "name: app\nservices:\n - name: independent\n - name: origin\n - name: intermediate\n - name: producer\n",
}
for _, name := range []string{"independent", "origin", "intermediate", "producer"} {
declaration := "name: " + name + "\nversion: 0.0.0\nagent:\n kind: codefly:service\n publisher: example.test\n name: missing-admission-peer\n version: 0.0.1\n"
if name == "origin" {
declaration += "service-dependencies:\n - name: intermediate\n kind: " + edge + "\n"
}
if name == "intermediate" {
declaration += "service-dependencies:\n - name: producer\n kind: build\n"
}
files["modules/app/services/"+name+"/service.codefly.yaml"] = declaration
}
for name, value := range files {
path := filepath.Join(root, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700))
require.NoError(t, os.WriteFile(path, []byte(value), 0600))
}
ws, err := resources.LoadWorkspaceFromDir(t.Context(), root)
require.NoError(t, err)
return root, ws
}

func validDisposableReplay(t *testing.T, root string, ws *resources.Workspace, phase string, changed []string) *Plan {
t.Helper()
runCacheTestGit(t, root, "init")
runCacheTestGit(t, root, "add", ".")
runCacheTestGit(t, root, "-c", "user.name=CI Test", "-c", "user.email=ci@example.com", "commit", "-m", "fixture")
options := PlanOptions{ChangedFiles: changed}
plan, err := BuildPlan(t.Context(), ws, &options)
require.NoError(t, err)
invocation := ReplayInvocation{Phases: []string{phase}, Suites: []string{"unit"}, RuntimeContext: "free"}
saved, err := buildReplayPlan(t.Context(), ws, plan, invocation)
require.NoError(t, err)
data, err := json.Marshal(saved)
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "plan.json")
require.NoError(t, os.WriteFile(path, data, 0600))
// Use the same selection entrypoint as ci test --plan, not a forged replay.
flags := SelectionFlags{changedFiles: changed, planFile: path}
actual, err := flags.BuildPlan(t.Context(), ws, invocation)
require.NoError(t, err)
require.NotNil(t, actual.replay)
return actual
}

func TestDisposableReplayAdmissionBeforeAnyAction(t *testing.T) {
root, ws := disposableAdmissionWorkspace(t, "build", false)
plan := validDisposableReplay(t, root, ws, "test", []string{"modules/app/services/independent/code/input", "modules/app/services/origin/code/input"})
for _, disposable := range []bool{false, true} {
calls := 0
err := WithPlanOptions(t.Context(), ws, plan, func(context.Context, *resources.Workspace, *resources.Module, *resources.Service) error {
calls++
return nil
}, ScheduleOptions{Phase: "test", Suite: "unit", Jobs: 1, DisposableImages: disposable})
if disposable {
require.ErrorContains(t, err, "disposable image input rebinding is unsupported")
require.Zero(t, calls)
} else {
require.NoError(t, err)
require.GreaterOrEqual(t, calls, 2, "ordinary replay remains supported")
}
}
}

func TestDisposableAdmissionIncludesScheduledExcludedBridge(t *testing.T) {
for _, replay := range []bool{false, true} {
root, ws := disposableAdmissionWorkspace(t, "build", true)
plan := &Plan{Services: []PlannedService{{Service: "app/origin"}}}
if replay {
plan = validDisposableReplay(t, root, ws, "build", []string{"modules/app/services/origin/code/input"})
}
// The Flow-only check admits this valid withdrawal; the build scheduler
// still dispatches producer/intermediate, so whole-request refusal wins.
require.NoError(t, checkDisposablePlan(t.Context(), ws, plan, "test"))
require.ErrorContains(t, checkDisposablePlan(t.Context(), ws, plan, "lint", "build"), "build prerequisite")
calls := 0
err := WithPlanOptions(t.Context(), ws, plan, func(context.Context, *resources.Workspace, *resources.Module, *resources.Service) error {
calls++
return nil
}, ScheduleOptions{Phase: "build", Jobs: 2, DisposableImages: true})
require.ErrorContains(t, err, "build prerequisite")
require.Zero(t, calls)
}
}

func TestDisposableAdmissionDoesNotTraverseUndispatchedRuntimeBranch(t *testing.T) {
_, ws := disposableAdmissionWorkspace(t, "runtime", true)
for _, selected := range []string{"app/origin", "app/independent"} {
plan := &Plan{Services: []PlannedService{{Service: selected}}}
calls := 0
err := WithPlanOptions(t.Context(), ws, plan, func(context.Context, *resources.Workspace, *resources.Module, *resources.Service) error {
calls++
return nil
}, ScheduleOptions{Phase: "build", DisposableImages: true})
require.NoError(t, err)
require.Equal(t, 1, calls, "no scheduler or Flow dispatches the withdrawn runtime branch")
}
}
5 changes: 5 additions & 0 deletions cmd/ci/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ var RunCmd = &cobra.Command{
return err
}
return runWithCIReport(ctx, workspace, plan, "codefly ci run", func(reporter *Reporter) error {
if disposableRuntime {
if err := checkDisposablePlan(ctx, workspace, plan, phases...); err != nil {
return err
}
}
if err := validateAgentVersions(ctx, workspace, plan); err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions cmd/ci/scheduler_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ func bindSchedulingFlags(command *cobra.Command) {

func commandScheduleOptions(lockDependencyClosure bool, phase, suite string, reporter *Reporter) ScheduleOptions {
return ScheduleOptions{
DisposableImages: disposableRuntime,
Jobs: ciJobs,
FailFast: ciFailFast,
LockDependencyClosure: lockDependencyClosure,
Expand Down
5 changes: 4 additions & 1 deletion cmd/ci/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var testSelection SelectionFlags
// Ordinary CI keeps the normal Stop semantics of stateful service agents.
var disposableRuntime bool

const disposableRuntimeUsage = "Give each validation/test flow a fresh resource scope and destroy its owned runtime resources afterward (for disposable fixtures only)"
const disposableRuntimeUsage = "Isolate runtime resources and host recipe outputs; destroy owned runtime resources but retain images (independent single-recipe fixtures only)"

type disposableTestFlow struct{ *orchestration.Flow }

Expand Down Expand Up @@ -122,6 +122,9 @@ func initTestService(ctx context.Context, workspace *resources.Workspace, module
if err != nil {
return nil, w.Wrap(err)
}
if disposableRuntime {
flow.WithDisposableImages()
}
flow.WithOutputSink(cli.NewOutputSink())
flow.WithLoadOnly(loadOnly)
flow.WithInitOnly(initOnly)
Expand Down
12 changes: 12 additions & 0 deletions cmd/doctor_workspace_binding_values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ func TestDoctorUsesEffectiveRunnableValueAndPreservesAuthoredFile(t *testing.T)
- service: api
endpoint: http
visibility: public
`,
"modules/backend/module.contract.codefly.yaml": `schema: codefly/module-contract/v1
principal: releases
namespaces: []
queues: []
scope_ceilings: []
bindings: []
destinations:
- id: http
service: api
endpoint: http
kind: module
`,
"modules/backend/services/api/openapi/api.swagger.json": string(artifact),
})
Expand Down
4 changes: 4 additions & 0 deletions cmd/generate/contract_surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/codefly-dev/core/composition"
modulecontract "github.com/codefly-dev/core/contracts/module"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/resources"
"github.com/stretchr/testify/require"
Expand All @@ -23,6 +24,9 @@ func TestContractSurfaceExportAndDerivation(t *testing.T) {
Endpoints: []*resources.Endpoint{{Name: "invoke", API: "connect"}, {Name: "receipts", API: "grpc"}},
}
root, moduleDir := saveFixtureWorkspace(t, ctx, "documents", nil, service)
saveRunnablePublication(t, moduleDir, "documents.ingestion",
modulecontract.Destination{ID: "invoke", Service: service.Name, Endpoint: "invoke", Kind: "module"},
modulecontract.Destination{ID: "receipts", Service: service.Name, Endpoint: "receipts", Kind: "module"})
service, err := resources.LoadServiceFromDir(ctx, service.Dir())
require.NoError(t, err)
writeFixtureFile(t, filepath.Join(service.Dir(), "generated/surfaces.json"), []byte(`{
Expand Down
9 changes: 5 additions & 4 deletions cmd/generate/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ By default each Go service gets its own table in its own Go module:
<service>/<source-dir>/pkg/gen/permissions, the tree the go-grpc layout
generates its bindings into (source-dir is the Go agent's setting, "code"
unless the service declares another). A service's table holds the methods its
published contracts serve plus the module contract's foreign methods, which
name no service. What an endpoint serves is what the contract catalog records
published contracts serve plus foreign methods selected for that service.
Unselected foreign methods appear in every Go service's table for compatibility.
What an endpoint serves is what the contract catalog records
for it (the declared surface, or every service of its package); a method a
published descriptor declares that no endpoint serves, such as a protocol other
modules serve, is left out with a note and needs no policy. The TypeScript file
Expand Down Expand Up @@ -119,8 +120,8 @@ type permissionDestination struct {
// permissionDestinations places the module's method policies, and names the
// published methods no endpoint serves, which no table holds. An explicit
// output holds the whole module's table. Otherwise each Go service gets the
// methods its own published contracts serve, plus the foreign methods the
// module contract declares without naming a service, in the generated tree
// methods its own published contracts serve and foreign methods selected for it,
// plus legacy foreign methods with no service selector, in the generated tree
// of its own Go module, so the service imports them with no flag. A Python
// service that publishes policies is skipped and named in skipped: nothing in
// it reads a table, because the hop in front of it decides every call.
Expand Down
Loading
Loading