Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 3 additions & 35 deletions cmd/agents/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (

"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/pkg/cli"
"github.com/codefly-dev/cli/pkg/dependencypins"
"github.com/codefly-dev/core/shared"
"github.com/spf13/cobra"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
)

// DepsCmd manages an agent's build dependencies: the local↔published core
Expand Down Expand Up @@ -414,44 +414,12 @@ func pinModule(ctx context.Context, dir string, env []string, version, label str
// Select pins before any mutation so a typo cannot produce a partial update or
// add a dependency to modules that never used it.
func dependencyPins(dirs, dependencies []string) (map[string][]string, error) {
pins := make(map[string][]string)
selected := make(map[string]string)
for _, dependency := range dependencies {
path, version, found := strings.Cut(dependency, "@")
if !found || version == "" || strings.ContainsAny(version, "@ \t\r\n") || module.CheckPath(path) != nil {
return nil, fmt.Errorf("dependency %q must be module@version", dependency)
}
if path == coreModule {
if strings.HasPrefix(dependency, coreModule+"@") {
return nil, fmt.Errorf("select Core through --pin, not --dependency")
}
if _, exists := selected[path]; exists {
return nil, fmt.Errorf("dependency %q is selected more than once", path)
}
selected[path] = dependency
}
matched := make(map[string]bool)
for _, dir := range dirs {
data, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
return nil, err
}
file, err := modfile.Parse("go.mod", data, nil)
if err != nil {
return nil, err
}
for _, requirement := range file.Require {
if dependency, exists := selected[requirement.Mod.Path]; exists {
pins[dir] = append(pins[dir], dependency)
matched[requirement.Mod.Path] = true
}
}
}
for path := range selected {
if !matched[path] {
return nil, fmt.Errorf("dependency %q is not required by the agent or its base fixtures", path)
}
}
return pins, nil
return dependencypins.Plan(dirs, dependencies)
}

// baseFixtureDirs returns every base-fixture Go module under the agent
Expand Down
57 changes: 42 additions & 15 deletions cmd/update/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/cmd/companion"
"github.com/codefly-dev/cli/pkg/cli"
"github.com/codefly-dev/cli/pkg/dependencypins"
coreaudit "github.com/codefly-dev/core/agents/services/audit"
"github.com/spf13/cobra"
)
Expand All @@ -30,6 +31,7 @@ var (
depsAudit bool
depsStaleDays int
depsGo string
depsSelections []string
)

// DepsCmd implements `codefly update deps` — the "update all packages"
Expand All @@ -53,13 +55,18 @@ For each Go module under --dir (default: current directory), update deps:
- go mod tidy
- re-audit with govulncheck (unless --audit=false)

With --dependency module@version (repeatable), pin only those existing external
requirements, including indirect dependencies, and leave other selections alone.
All selections are validated before changing any module.

With --companions, also rebuilds every companion image with 'docker build
--pull' so floating base tags (golang:1.26-alpine, …) pick up upstream
patch releases — clearing base-image CVEs like the Go stdlib advisories.

Examples:
codefly update deps # update Go deps under cwd + audit
codefly update deps --dir . # same, explicit
codefly update deps --dependency golang.org/x/net@v0.62.0
codefly update deps --companions # also rebuild companion images (--pull)
codefly update deps --audit=false # skip the post-update audit`,
Args: cobra.NoArgs,
Expand Down Expand Up @@ -89,6 +96,15 @@ Examples:
if len(modDirs) == 0 {
return fmt.Errorf("no go.mod found under %s", root)
}
for _, selection := range depsSelections {
if strings.HasPrefix(selection, internalModulePrefix) {
return fmt.Errorf("first-party dependency %q is not managed by update deps", selection)
}
}
pins, err := dependencypins.Plan(modDirs, depsSelections)
if err != nil {
return err
}
cli.Header(1, "Updating dependencies across %d module(s)", len(modDirs))

var failures []error
Expand Down Expand Up @@ -123,7 +139,10 @@ Examples:
failures = append(failures, err)
break
}
if err := updateModule(ctx, md); err != nil {
if len(depsSelections) > 0 && len(pins[md]) == 0 {
continue
}
if err := updateModule(ctx, md, pins[md]...); err != nil {
cli.Error("update %s: %v", rel(root, md), err)
failures = append(failures, fmt.Errorf("update %s: %w", rel(root, md), err))
}
Expand Down Expand Up @@ -172,24 +191,31 @@ Examples:
// updateModule bumps every outdated external dependency in the module at
// dir to its latest safe version, then tidies. Internal codefly-dev
// modules are skipped (see internalModulePrefix).
func updateModule(ctx context.Context, dir string) error {
res, err := coreaudit.Golang(ctx, dir, true)
if err != nil {
return fmt.Errorf("list outdated: %w", err)
}
func updateModule(ctx context.Context, dir string, selections ...string) error {
var bumped int
for _, o := range res.Outdated {
if strings.HasPrefix(o.Package, internalModulePrefix) {
continue
if len(selections) > 0 {
if err := runGo(ctx, dir, append([]string{"get"}, selections...)...); err != nil {
return err
}
if o.LatestSafe == "" || o.LatestSafe == o.Current {
continue
bumped = len(selections)
} else {
res, err := coreaudit.Golang(ctx, dir, true)
if err != nil {
return fmt.Errorf("list outdated: %w", err)
}
if err := runGo(ctx, dir, "get", o.Package+"@"+o.LatestSafe); err != nil {
return fmt.Errorf("go get %s@%s: %w", o.Package, o.LatestSafe, err)
for _, o := range res.Outdated {
if strings.HasPrefix(o.Package, internalModulePrefix) {
continue
}
if o.LatestSafe == "" || o.LatestSafe == o.Current {
continue
}
if err := runGo(ctx, dir, "get", o.Package+"@"+o.LatestSafe); err != nil {
return fmt.Errorf("go get %s@%s: %w", o.Package, o.LatestSafe, err)
}
cli.Info(" %s %s → %s", o.Package, o.Current, o.LatestSafe)
bumped++
}
cli.Info(" %s %s → %s", o.Package, o.Current, o.LatestSafe)
bumped++
}
// `go mod tidy` ignores go.work: it resolves every import from the
// module graph (cache/network), not from workspace-local sibling
Expand Down Expand Up @@ -314,6 +340,7 @@ func rel(root, p string) string {
}

func init() {
DepsCmd.Flags().StringArrayVar(&depsSelections, "dependency", nil, "Pin only selected existing external module@version requirements, including indirect dependencies (repeatable)")
DepsCmd.Flags().StringVar(&depsDir, "dir", "", "Root to update (default: current directory; recurses into every go.mod)")
DepsCmd.Flags().BoolVar(&depsCompanions, "companions", false, "Also rebuild companion Docker images with fresh base layers (docker build --pull)")
DepsCmd.Flags().BoolVar(&depsAudit, "audit", true, "Run the govulncheck audit after updating")
Expand Down
61 changes: 61 additions & 0 deletions cmd/update/deps_selected_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package update

import (
"os"
"path/filepath"
"testing"

"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/pkg/dependencypins"
"github.com/stretchr/testify/require"
"golang.org/x/mod/modfile"
)

func TestSelectedDependencyUpdatesIndirectWithoutMovingOthers(t *testing.T) {
t.Setenv("GOWORK", "off")
t.Setenv("GOPROXY", "off")
root := t.TempDir()
files := map[string]string{
"go.mod": "module example.test/app\ngo 1.23\nrequire (\nexample.test/direct v1.0.0\nexample.test/indirect v1.0.0 // indirect\n)\nreplace example.test/direct => ./direct\nreplace example.test/indirect => ./indirect\n",
"app.go": "package app\nimport \"example.test/direct\"\nvar Value = direct.Value\n",
"direct/go.mod": "module example.test/direct\ngo 1.23\nrequire example.test/indirect v1.0.0\nreplace example.test/indirect => ../indirect\n",
"direct/direct.go": "package direct\nimport \"example.test/indirect\"\nvar Value = indirect.Value\n",
"indirect/go.mod": "module example.test/indirect\ngo 1.23\n",
"indirect/indirect.go": "package indirect\nconst Value = 42\n",
}
for name, data := range files {
path := filepath.Join(root, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(data), 0o644))
}
// The existing module resolver is real; local replacements keep the fixture offline.
pins, err := dependencypins.Plan([]string{root, filepath.Join(root, "indirect")}, []string{"example.test/indirect@v1.0.1"})
require.NoError(t, err)
require.Empty(t, pins[filepath.Join(root, "indirect")], "do not inject a requirement into a module that never used it")
oldDir, oldAudit, oldSelections := depsDir, depsAudit, depsSelections
t.Cleanup(func() { depsDir, depsAudit, depsSelections = oldDir, oldAudit, oldSelections })
depsDir, depsAudit = root, false
depsSelections = []string{"example.test/indirect@v1.0.1"}
ctx, done := common.NewContext()
defer done()
require.NoError(t, DepsCmd.RunE(DepsCmd, nil))
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
require.NoError(t, err)
parsed, err := modfile.Parse("go.mod", data, nil)
require.NoError(t, err)
versions := map[string]string{}
for _, r := range parsed.Require {
versions[r.Mod.Path] = r.Mod.Version
}
require.Equal(t, "v1.0.1", versions["example.test/indirect"])
require.Equal(t, "v1.0.0", versions["example.test/direct"])
require.NoError(t, runGo(ctx, root, "build", "./..."))
require.NoError(t, runGo(ctx, root, "mod", "tidy", "-diff"))
before := string(data)
depsSelections = []string{"example.test/indirect@v1.0.2", "example.test/missing@v1.0.1"}
err = DepsCmd.RunE(DepsCmd, nil)
require.Error(t, err)
data, err = os.ReadFile(filepath.Join(root, "go.mod"))
require.NoError(t, err)
require.Equal(t, before, string(data), "all selections must validate before mutation")
}
16 changes: 11 additions & 5 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -5281,13 +5281,18 @@ For each Go module under --dir (default: current directory), update deps:
- go mod tidy
- re-audit with govulncheck (unless --audit=false)

With --dependency module@version (repeatable), pin only those existing external
requirements, including indirect dependencies, and leave other selections alone.
All selections are validated before changing any module.

With --companions, also rebuilds every companion image with 'docker build
--pull' so floating base tags (golang:1.26-alpine, …) pick up upstream
patch releases — clearing base-image CVEs like the Go stdlib advisories.

Examples:
codefly update deps # update Go deps under cwd + audit
codefly update deps --dir . # same, explicit
codefly update deps --dependency golang.org/x/net@v0.62.0
codefly update deps --companions # also rebuild companion images (--pull)
codefly update deps --audit=false # skip the post-update audit
```
Expand All @@ -5299,11 +5304,12 @@ codefly update deps [flags]
Flags:

```
--audit Run the govulncheck audit after updating (default true)
--companions Also rebuild companion Docker images with fresh base layers (docker build --pull)
--dir string Root to update (default: current directory; recurses into every go.mod)
--go string Pin the Go toolchain to this version across every go.mod + go.work (e.g. 1.26.4) — clears stdlib CVEs in standalone/CI builds
--stale-days int Audit: fail when a suppression's reviewed date is older than this many days (0 disables) (default 45)
--audit Run the govulncheck audit after updating (default true)
--companions Also rebuild companion Docker images with fresh base layers (docker build --pull)
--dependency stringArray Pin only selected existing external module@version requirements, including indirect dependencies (repeatable)
--dir string Root to update (default: current directory; recurses into every go.mod)
--go string Pin the Go toolchain to this version across every go.mod + go.work (e.g. 1.26.4) — clears stdlib CVEs in standalone/CI builds
--stale-days int Audit: fail when a suppression's reviewed date is older than this many days (0 disables) (default 45)
```

## `codefly update service`
Expand Down
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2431,6 +2431,7 @@ codefly update workspace --agent-override codefly.dev/go-grpc=0.1.47-dev.abc123d
codefly update service api # one service to its latest compatible release
codefly update service api --agent-version 0.1.47-dev.abc123def456 # pin one service exactly
codefly update deps # Go dependencies under the current directory
codefly update deps --dependency golang.org/x/net@v0.62.0 # only this existing external requirement, including indirect pins
```

Without flags, `update workspace` and `update service` move to the agent's
Expand Down
21 changes: 21 additions & 0 deletions docs/runbooks/cut-a-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,24 @@ codefly self check-update # sees the new sta
- [ ] `release.yaml` green end to end
- [ ] `codefly version --json` reports the tag
- [ ] `brew` install/upgrade works; `self check-update` sees it

## Dependency fixes in companion images

The October 2026 refresh selects `golang.org/x/net` 0.62.0 for
GO-2026-6860 and GO-2026-6866, and gRPC 1.85.0 supersedes the old 1.84.0
hold. The dependency update command's automatic outdated list contains direct
dependencies; verify indirect dependencies such as `x/net` explicitly too.
Use `codefly update deps --dependency golang.org/x/net@v0.62.0` to select an
existing direct or indirect external requirement without moving unrelated
versions. The repeatable flag validates every selection across the selected
modules before mutation, then uses the same Go resolver, tidy and audit path as
the automatic update. Modules without the selected requirement are not changed;
unknown or duplicate selections and first-party selections are refused. Exact
selections also retain a version required by a compatibility gate without
silently undoing that hold during an unrelated security update.

Companion publication compiles this checkout into the images. Merge the CLI
fix before dispatching `companions.yaml` against Core's exact merged commit
with new companion versions. A CLI merge does not refresh existing images:
inspect and scan the published executable, record each index digest, and
rebuild the services consuming it. Do not overwrite previously published tags.
20 changes: 6 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,15 @@ require (
go.uber.org/goleak v1.3.0
golang.org/x/crypto v0.58.0
golang.org/x/mod v0.42.0
golang.org/x/oauth2 v0.36.0
golang.org/x/oauth2 v0.37.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.org/x/term v0.47.0
google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b
// HELD at v1.83.2: v1.84.0 is affected by GO-2026-6443 (server panic via a
// missing :authority/Host header) and the advisory's only fix for the 1.84
// line is an unreleased v1.85.0-dev pseudo-version, so there is nothing to
// bump to. v1.83.2 is itself the patched release for the 1.83 line. The
// `bootstrap-audit` gate fails on this as an actionable finding, which is
// correct -- do NOT add a .govulncheck.yaml suppression to get past it.
// Raise this to v1.85.0 (or a patched v1.84.x) once one ships; see
// .github/dependabot.yml, which ignores only the known-bad v1.84.0.
google.golang.org/grpc v1.84.0
google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e
google.golang.org/grpc v1.85.0
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.2
oras.land/oras-go/v2 v2.7.0
sigs.k8s.io/kustomize/api v0.21.1
sigs.k8s.io/kustomize/kyaml v0.21.1
)
Expand Down Expand Up @@ -242,10 +234,10 @@ require (
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
golang.org/x/net v0.61.0 // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/time v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20261019180522-ce908d29299b // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
Expand Down
Loading
Loading