Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1cf2033
feat(output): complete logging and status UX migration
skevetter Sep 11, 2026
4d5b027
fix: address CodeFactor findings
skevetter Sep 11, 2026
ccf705f
fix: satisfy Go lint complexity checks
skevetter Sep 11, 2026
1adc46f
fix: clear remaining lint findings
skevetter Sep 11, 2026
f47afce
refactor: resolve lint findings without suppressions
skevetter Sep 11, 2026
5d76b02
refactor: remove remaining lint suppressions
skevetter Sep 11, 2026
b0ac2ff
fix: correct status reporter state reference
skevetter Sep 11, 2026
8206528
fix: resolve remaining lint findings
skevetter Sep 11, 2026
66c2305
fix: satisfy remaining CI lint checks
skevetter Sep 11, 2026
876024d
fix: format provider status calls
skevetter Sep 11, 2026
c4f0c09
fix: apply remaining lint formatting
skevetter Sep 12, 2026
ff63e26
fix: apply remaining golines formatting
skevetter Sep 12, 2026
c3f2bf4
fix: apply remaining agent formatting
skevetter Sep 12, 2026
d7b29b1
fix: apply devcontainer lint formatting
skevetter Sep 12, 2026
58aa21e
fix: apply remaining devcontainer formatting
skevetter Sep 12, 2026
0ca4193
fix: apply remaining lint formatting
skevetter Sep 12, 2026
aced171
fix: apply remaining log formatting
skevetter Sep 12, 2026
53f8615
fix: apply remaining status formatting
skevetter Sep 12, 2026
25bb1d0
fix: apply remaining subprocess and task formatting
skevetter Sep 12, 2026
8fe197f
style: format log test constants
skevetter Sep 12, 2026
832843f
fix(git): preserve credential helper output
skevetter Sep 12, 2026
db4adc6
style(git): format runner output setup
skevetter Sep 12, 2026
18d02da
test(desktop): accept streaming start state
skevetter Sep 12, 2026
a8b3b53
test(e2e): assert current failure status envelopes
skevetter Sep 12, 2026
f40540b
fix(task): preserve detached SSH fallback success
skevetter Sep 12, 2026
f8a1a15
test(desktop): match workspace status polling
skevetter Sep 17, 2026
90f8b24
fix(status): deliver terminal failures before shutdown
skevetter Sep 17, 2026
2d393fe
Merge branch 'main' into faded-lion
skevetter Sep 17, 2026
b3e533c
fix(lint): sync with golangci-lint 2.13
skevetter Sep 17, 2026
755fdba
fix(desktop): guard release channel switches
skevetter Sep 18, 2026
7604289
refactor(output): simplify updater state
skevetter Sep 18, 2026
d8b64a2
fix(desktop): defer updater initialization
skevetter Sep 18, 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
6 changes: 6 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ formatters:
- gofumpt
- goimports
- golines
exclusions:
paths:
- ".*\\.pb\\.go$"
linters:
enable:
- cyclop
Expand All @@ -33,6 +36,9 @@ linters:
- whitespace
exclusions:
rules:
- path: ".*\\.pb\\.go$"
linters:
- lll
- linters:
- gosec
text: "G101"
Expand Down
16 changes: 14 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,20 @@ tasks:

cli:test:e2e:build:
desc: build devsy for e2e tests
status:
- test -f e2e/bin/devsy-linux-amd64
cmds:
- task: cli:build:dev
- mkdir -p e2e/bin
- cp dist/devsy-dev_linux_amd64_v1/devsy-linux-amd64 e2e/bin/devsy-linux-amd64
- |
host_os="$(go env GOOS)"
host_arch="$(go env GOARCH)"
host_binary="devsy-${host_os}-${host_arch}"
host_dir="dist/devsy-dev_${host_os}_${host_arch}_v1"
if [ "$host_os" = "windows" ]; then
host_binary="${host_binary}.exe"
fi
test -f "${host_dir}/${host_binary}"
cp "${host_dir}/${host_binary}" "e2e/bin/${host_binary}"

cli:test:e2e:
desc: run e2e tests
Expand All @@ -125,12 +133,16 @@ tasks:
cli:test:e2e:suite:
# usage: task cli:test:e2e:suite -- "test suite name"
desc: run e2e tests suite
deps:
- cli:test:e2e:build
dir: e2e
cmd: go tool ginkgo --label-filter {{ .CLI_ARGS }}

cli:test:e2e:focus:
# usage: task cli:test:e2e:focus -- "test suite pattern"
desc: run focused e2e tests
deps:
- cli:test:e2e:build
dir: e2e
cmd: go tool ginkgo --focus {{ .CLI_ARGS }}

Expand Down
42 changes: 33 additions & 9 deletions cmd/ci/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"

Expand All @@ -18,6 +19,7 @@ import (
"github.com/devsy-org/devsy/pkg/flags/names"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/provider"
"github.com/devsy-org/devsy/pkg/status"
workspace2 "github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -207,6 +209,10 @@ func (cmd *CICmd) execute(ctx context.Context, source string) (err error) {
// workspace.
ctx, cancel := up.WithSignals(ctx)
defer cancel()
reporter, err := newStatusReporter(os.Stderr, cmd.Verbosity > 0 || cmd.Debug)
if err != nil {
return err
}

devsyConfig, cfgErr := config.LoadConfig(cmd.Context, cmd.Provider)
if cfgErr != nil {
Expand All @@ -216,12 +222,24 @@ func (cmd *CICmd) execute(ctx context.Context, source string) (err error) {
cmd.StrictHostKeyChecking = true
}

workspaceClient, cleanup, resolveErr := cmd.resolveWorkspace(ctx, devsyConfig, source)
workspaceClient, cleanup, teardown, resolveErr := cmd.resolveWorkspace(ctx, devsyConfig, source)
if resolveErr != nil {
return resolveErr
}
// cleanup tears down the workspace; join error with the result.
defer func() { err = errors.Join(err, cleanup()) }()
defer func() {
var cleanupErr error
if teardown {
cleanupErr = status.Run(context.Background(), reporter, status.Operation{
Phase: status.PhaseDeletingWorkspace,
}, func(context.Context) error {
return cleanup()
})
} else {
cleanupErr = cleanup()
}
err = errors.Join(err, cleanupErr)
}()

if _, ok := workspaceClient.(client.WorkspaceClient); !ok {
return fmt.Errorf("ci is currently not supported for proxy providers")
Expand All @@ -233,23 +251,29 @@ func (cmd *CICmd) execute(ctx context.Context, source string) (err error) {
GlobalFlags: cmd.GlobalFlags,
DevsyConfig: devsyConfig,
CLIOptions: cmd.CLIOptions,
Reporter: reporter,
ProviderOptions: cmd.ProviderOptions,
SecretsFile: cmd.SecretsFile,
FeatureSecretsFile: cmd.FeatureSecretsFile,
}); err != nil {
return fmt.Errorf("start devcontainer: %w", err)
}

log.Infof("running command in devcontainer: %s", strings.Join(cmd.RunCmd, " "))
return cmd.runInContainer(ctx, workspaceClient)
log.Debugf("running command in devcontainer: %s", strings.Join(cmd.RunCmd, " "))
return status.Run(ctx, reporter, status.Operation{
Phase: status.PhaseRunningCommand,
Step: "CI command",
}, func(ctx context.Context) error {
return cmd.runInContainer(ctx, workspaceClient)
})
}

// resolveWorkspace resolves (creating if needed) the workspace to run in.
func (cmd *CICmd) resolveWorkspace(
ctx context.Context,
devsyConfig *config.Config,
source string,
) (client.BaseWorkspaceClient, func() error, error) {
) (client.BaseWorkspaceClient, func() error, bool, error) {
var args []string
if source != "" {
args = []string{source}
Expand All @@ -258,7 +282,7 @@ func (cmd *CICmd) resolveWorkspace(

sshConfigPath, cleanupSSH, err := workspacecmd.NewTempSSHConfig()
if err != nil {
return nil, nil, err
return nil, nil, false, err
}

workspaceClient, err := workspace2.Resolve(ctx, devsyConfig, workspace2.ResolveParams{
Expand All @@ -273,7 +297,7 @@ func (cmd *CICmd) resolveWorkspace(
})
if err != nil {
cleanupSSH()
return nil, nil, err
return nil, nil, false, err
}

teardown := exists == "" && !cmd.Keep
Expand All @@ -284,7 +308,7 @@ func (cmd *CICmd) resolveWorkspace(
}
return nil
}
return workspaceClient, cleanup, nil
return workspaceClient, cleanup, teardown, nil
}

// runInContainer runs the command inside the started container.
Expand All @@ -298,7 +322,7 @@ func (cmd *CICmd) runInContainer(ctx context.Context, c client.BaseWorkspaceClie
}

func (cmd *CICmd) teardown(c client.BaseWorkspaceClient) error {
log.Infof("tearing down workspace")
log.Debugf("tearing down workspace")
ctx, cancel := context.WithTimeout(context.Background(), teardownTimeout)
defer cancel()
if err := c.Delete(ctx, client.DeleteOptions{Force: true}); err != nil {
Expand Down
35 changes: 35 additions & 0 deletions cmd/ci/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ci

import (
"io"

"github.com/devsy-org/devsy/pkg/status"
)

func newStatusReporter(out io.Writer, verbose bool) (status.Reporter, error) {
reporter, err := status.NewReporter(status.ReporterOptions{
Format: "plain",
Out: out,
Prefix: "ci",
Verbose: verbose,
SuppressFailureDetails: true,
Labels: map[status.Phase]string{
status.PhaseCloningRepository: "cloning repository",
status.PhaseResolvingConfig: "resolving devcontainer config",
status.PhaseInitializeCommand: "running initializeCommand",
status.PhaseBuildingImage: "building image",
status.PhaseStartingContainer: "starting container",
status.PhaseInjectingAgent: "injecting agent",
status.PhaseRunningLifecycleHook: "running lifecycle hooks",
status.PhaseWaitingFor: "waiting for readiness",
status.PhaseReady: "ready",
status.PhaseRunningCommand: "running command",
status.PhaseDeletingWorkspace: "tearing down workspace",
},
Envelope: func(status.Event) error { return nil },
})
if err != nil {
return nil, err
}
return status.ForPipeline(reporter, status.PipelineWorkspaceUp), nil
}
37 changes: 37 additions & 0 deletions cmd/ci/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ci

import (
"bytes"
"strings"
"testing"

"github.com/devsy-org/devsy/pkg/status"
)

func TestStatusReporterUsesDeterministicASCIIOutput(t *testing.T) {
var out bytes.Buffer
reporter, err := newStatusReporter(&out, false)
if err != nil {
t.Fatalf("newStatusReporter: %v", err)
}

reporter.Report(status.Event{Phase: status.PhaseRunningCommand, State: status.StateStarted})
reporter.Report(status.Event{
Phase: status.PhaseRunningCommand,
State: status.StateSucceeded,
Duration: 1250 * 1000 * 1000,
})

got := out.String()
if !strings.Contains(got, "[RUN] ci: running command") {
t.Fatalf("started output = %q", got)
}
if !strings.Contains(got, "[OK] ci: running command (1.2s)") {
t.Fatalf("completed output = %q", got)
}
for _, r := range got {
if r > 127 {
t.Fatalf("output contains non-ASCII rune %q: %q", r, got)
}
}
}
Loading
Loading