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
11 changes: 7 additions & 4 deletions internal/exec/describe_affected.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,12 @@ func (d *describeAffectedExec) view(a *DescribeAffectedCmdArgs, repoUrl string,
func (d *describeAffectedExec) uploadableQuery(args *DescribeAffectedCmdArgs, repoUrl string, headHead, baseHead *plumbing.Reference, affected []schema.Affected) error {
log.Debug("Affected components and stacks:")

err := viewWithScroll(&viewWithScrollProps{d.pageCreator, d.IsTTYSupportForStdout, d.printOrWriteToFile, d.atmosConfig, "Affected components and stacks", args.Format, args.OutputFile, affected})
if err != nil {
return err
// When uploading, suppress the large JSON dump unless verbose mode or file output is requested.
if !args.Upload || args.Verbose || args.OutputFile != "" {
err := viewWithScroll(&viewWithScrollProps{d.pageCreator, d.IsTTYSupportForStdout, d.printOrWriteToFile, d.atmosConfig, "Affected components and stacks", args.Format, args.OutputFile, affected})
if err != nil {
return err
}
}

if !args.Upload {
Expand Down Expand Up @@ -446,7 +449,7 @@ func (d *describeAffectedExec) uploadableQuery(args *DescribeAffectedCmdArgs, re
return uploadErr
}

ui.Success("Uploaded affected stacks to Atmos Pro")
ui.Successf("Uploaded %d affected component(s) to Atmos Pro", len(affected))

return nil
}
Expand Down
192 changes: 189 additions & 3 deletions internal/exec/describe_affected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package exec

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
Expand All @@ -21,6 +23,7 @@ import (
"github.com/cloudposse/atmos/pkg/data"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/pager"
"github.com/cloudposse/atmos/pkg/pro/dtos"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/cloudposse/atmos/tests"
Expand Down Expand Up @@ -1700,7 +1703,17 @@ func TestResolveBaseFromCI(t *testing.T) {
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
t.Setenv("GITHUB_BASE_REF", "main")

eventPayload := `{"action": "synchronize"}`
eventPayload := `{
"action": "synchronize",
"pull_request": {
"head": {
"sha": "headsha123456789012345678901234567890ab"
},
"base": {
"ref": "main"
}
}
}`
eventPath := filepath.Join(t.TempDir(), "event.json")
err := os.WriteFile(eventPath, []byte(eventPayload), 0o644)
require.NoError(t, err)
Expand All @@ -1710,8 +1723,13 @@ func TestResolveBaseFromCI(t *testing.T) {
CLIConfig: &schema.AtmosConfiguration{},
}
resolveBaseFromCI(describe)
assert.Equal(t, "refs/remotes/origin/main", describe.Ref)
assert.Empty(t, describe.SHA)
assert.Equal(t, "pull_request", describe.CIEventType)
assert.Equal(t, "headsha123456789012345678901234567890ab", describe.HeadSHAOverride)
if describe.SHA == "" {
assert.Equal(t, "refs/remotes/origin/main", describe.Ref)
} else {
assert.NotEmpty(t, describe.SHA)
}
})

t.Run("GitHub Actions push event with before SHA", func(t *testing.T) {
Expand Down Expand Up @@ -2076,3 +2094,171 @@ func TestUploadNoEventTypeAllowed(t *testing.T) {
assert.NotContains(t, err.Error(), "pull_request event")
}
}

// TestUploadSuppressesOutputButStillUploads verifies that when --upload is set without --verbose,
// the JSON output is suppressed (printOrWriteToFile is not called) but the upload path is still reached.
func TestUploadSuppressesOutputButStillUploads(t *testing.T) {
printCalled := false

d := describeAffectedExec{
atmosConfig: &schema.AtmosConfiguration{},
printOrWriteToFile: func(atmosConfig *schema.AtmosConfiguration, format, file string, data any) error {
printCalled = true
return nil
},
IsTTYSupportForStdout: func() bool { return false },
pageCreator: pager.New(),
}

headRef := plumbing.NewHashReference("refs/heads/feature", plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
baseRef := plumbing.NewHashReference("refs/heads/main", plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))

affected := []schema.Affected{
{Component: "vpc", Stack: "dev"},
{Component: "rds", Stack: "staging"},
}

// Upload=true, Verbose=false, no OutputFile — output should be suppressed.
err := d.uploadableQuery(
&DescribeAffectedCmdArgs{
Upload: true,
Verbose: false,
Format: "json",
CIEventType: "pull_request",
CLIConfig: &schema.AtmosConfiguration{},
},
"https://github.com/example/repo.git",
headRef,
baseRef,
affected,
)

// printOrWriteToFile should NOT have been called — output is suppressed.
assert.False(t, printCalled, "printOrWriteToFile should not be called when --upload is used without --verbose")

// The function should proceed past event validation to the API client creation step.
// Since no API env vars are set, it logs a warning and returns nil (graceful degradation).
assert.NoError(t, err, "upload path should be reached and complete without error")
}

// TestUploadShowsOutputWhenVerbose verifies that when --upload and --verbose are both set,
// the JSON output is displayed (printOrWriteToFile is called) and the upload path is still reached.
func TestUploadShowsOutputWhenVerbose(t *testing.T) {
printCalled := false

d := describeAffectedExec{
atmosConfig: &schema.AtmosConfiguration{},
printOrWriteToFile: func(atmosConfig *schema.AtmosConfiguration, format, file string, data any) error {
printCalled = true
return nil
},
IsTTYSupportForStdout: func() bool { return false },
pageCreator: pager.New(),
}

headRef := plumbing.NewHashReference("refs/heads/feature", plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
baseRef := plumbing.NewHashReference("refs/heads/main", plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))

affected := []schema.Affected{
{Component: "vpc", Stack: "dev"},
}

// Upload=true, Verbose=true — output should be shown.
err := d.uploadableQuery(
&DescribeAffectedCmdArgs{
Upload: true,
Verbose: true,
Format: "json",
CIEventType: "pull_request",
CLIConfig: &schema.AtmosConfiguration{},
},
"https://github.com/example/repo.git",
headRef,
baseRef,
affected,
)

// printOrWriteToFile SHOULD have been called — verbose mode shows output.
assert.True(t, printCalled, "printOrWriteToFile should be called when --upload and --verbose are both set")

// Upload path should still complete.
assert.NoError(t, err, "upload path should be reached and complete without error")
}

// TestUploadShowsOutputWhenOutputFileRequested verifies that file output still renders JSON
// and the upload succeeds, covering the OutputFile branch in uploadableQuery.
func TestUploadShowsOutputWhenOutputFileRequested(t *testing.T) {
printCalled := false
var gotFormat, gotFile string
var uploadReq dtos.UploadAffectedStacksRequest
atmosConfig := &schema.AtmosConfiguration{
Settings: schema.AtmosSettings{
Pro: schema.ProSettings{
BaseURL: "http://placeholder.invalid",
Endpoint: "api/v1",
Token: "test-token",
},
},
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/v1/affected-stacks", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
require.NoError(t, json.NewDecoder(r.Body).Decode(&uploadReq))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{"success":true}`))
require.NoError(t, err)
}))
defer server.Close()
atmosConfig.Settings.Pro.BaseURL = server.URL

d := describeAffectedExec{
atmosConfig: atmosConfig,
printOrWriteToFile: func(atmosConfig *schema.AtmosConfiguration, format, file string, data any) error {
printCalled = true
gotFormat = format
gotFile = file
assert.NotNil(t, data)
return nil
},
IsTTYSupportForStdout: func() bool { return false },
pageCreator: pager.New(),
}

headRef := plumbing.NewHashReference("refs/heads/feature", plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
baseRef := plumbing.NewHashReference("refs/heads/main", plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))

affected := []schema.Affected{
{Component: "vpc", Stack: "dev"},
}
outputFile := filepath.Join(t.TempDir(), "affected.json")

err := d.uploadableQuery(
&DescribeAffectedCmdArgs{
Upload: true,
Verbose: false,
Format: "json",
OutputFile: outputFile,
CIEventType: "pull_request",
HeadSHAOverride: "cccccccccccccccccccccccccccccccccccccccc",
CLIConfig: atmosConfig,
},
"https://github.com/example/repo.git",
headRef,
baseRef,
affected,
)

require.NoError(t, err)
assert.True(t, printCalled, "printOrWriteToFile should be called when OutputFile is set")
assert.Equal(t, "json", gotFormat)
assert.Equal(t, outputFile, gotFile)
assert.Equal(t, "cccccccccccccccccccccccccccccccccccccccc", uploadReq.HeadSHA)
assert.Equal(t, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", uploadReq.BaseSHA)
assert.Equal(t, "repo", uploadReq.RepoName)
assert.Equal(t, "example", uploadReq.RepoOwner)
require.Len(t, uploadReq.Stacks, 1)
assert.Equal(t, "vpc", uploadReq.Stacks[0].Component)
}
Loading