detached c062834

test: add CLI-level E2E tests for native push pipeline

commit
c06283480f7288bcb637a6d4269869a826a811b8
author
brice · · 2026-03-14 14:10 UTC
parent
c8966e7
signature
unsigned
2 files · +148 -7
M internal/adapters/in/cli/push.go +13 -7
@@ -22,6 +22,12 @@ import (
2222 "github.com/bnema/gordon/pkg/validation"
2323 )
2424
25+var (
26+ dockerTagFn = dockerTag
27+ dockerPushFn = dockerPush
28+ dockerImageExistsFn = dockerImageExists
29+)
30+
2531 func newPushCmd() *cobra.Command {
2632 var (
2733 noDeploy bool
@@ -486,11 +492,11 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
486492 }
487493
488494 fmt.Println("Pushing...")
489- if err := dockerPush(ctx, latestRef); err != nil {
495+ if err := dockerPushFn(ctx, latestRef); err != nil {
490496 return fmt.Errorf("failed to push %s: %w", latestRef, err)
491497 }
492498 if version != "latest" {
493- if err := dockerPush(ctx, versionRef); err != nil {
499+ if err := dockerPushFn(ctx, versionRef); err != nil {
494500 return fmt.Errorf("failed to push %s: %w", versionRef, err)
495501 }
496502 }
@@ -552,26 +558,26 @@ func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, l
552558 localImage := fmt.Sprintf("%s/%s", registry, imageName)
553559
554560 fmt.Println("\nChecking local image...")
555- if !dockerImageExists(ctx, localImage) {
561+ if !dockerImageExistsFn(ctx, localImage) {
556562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
557563 }
558564
559565 fmt.Println("Tagging...")
560- if err := dockerTag(ctx, localImage, versionRef); err != nil {
566+ if err := dockerTagFn(ctx, localImage, versionRef); err != nil {
561567 return fmt.Errorf("failed to tag %s: %w", versionRef, err)
562568 }
563569 if version != "latest" {
564- if err := dockerTag(ctx, localImage, latestRef); err != nil {
570+ if err := dockerTagFn(ctx, localImage, latestRef); err != nil {
565571 return fmt.Errorf("failed to tag %s: %w", latestRef, err)
566572 }
567573 }
568574
569575 fmt.Println("Pushing...")
570- if err := dockerPush(ctx, versionRef); err != nil {
576+ if err := dockerPushFn(ctx, versionRef); err != nil {
571577 return fmt.Errorf("failed to push %s: %w", versionRef, err)
572578 }
573579 if version != "latest" {
574- if err := dockerPush(ctx, latestRef); err != nil {
580+ if err := dockerPushFn(ctx, latestRef); err != nil {
575581 return fmt.Errorf("failed to push %s: %w", latestRef, err)
576582 }
577583 }
@@ -22,6 +22,12 @@ import (
22 "github.com/bnema/gordon/pkg/validation"22 "github.com/bnema/gordon/pkg/validation"
23 )23 )
24 24
25+var (
26+ dockerTagFn = dockerTag
27+ dockerPushFn = dockerPush
28+ dockerImageExistsFn = dockerImageExists
29+)
30+
25 func newPushCmd() *cobra.Command {31 func newPushCmd() *cobra.Command {
26 var (32 var (
27 noDeploy bool33 noDeploy bool
@@ -486,11 +492,11 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
486 }492 }
487 493
488 fmt.Println("Pushing...")494 fmt.Println("Pushing...")
489- if err := dockerPush(ctx, latestRef); err != nil {495+ if err := dockerPushFn(ctx, latestRef); err != nil {
490 return fmt.Errorf("failed to push %s: %w", latestRef, err)496 return fmt.Errorf("failed to push %s: %w", latestRef, err)
491 }497 }
492 if version != "latest" {498 if version != "latest" {
493- if err := dockerPush(ctx, versionRef); err != nil {499+ if err := dockerPushFn(ctx, versionRef); err != nil {
494 return fmt.Errorf("failed to push %s: %w", versionRef, err)500 return fmt.Errorf("failed to push %s: %w", versionRef, err)
495 }501 }
496 }502 }
@@ -552,26 +558,26 @@ func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, l
552 localImage := fmt.Sprintf("%s/%s", registry, imageName)558 localImage := fmt.Sprintf("%s/%s", registry, imageName)
553 559
554 fmt.Println("\nChecking local image...")560 fmt.Println("\nChecking local image...")
555- if !dockerImageExists(ctx, localImage) {561+ if !dockerImageExistsFn(ctx, localImage) {
556 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
557 }563 }
558 564
559 fmt.Println("Tagging...")565 fmt.Println("Tagging...")
560- if err := dockerTag(ctx, localImage, versionRef); err != nil {566+ if err := dockerTagFn(ctx, localImage, versionRef); err != nil {
561 return fmt.Errorf("failed to tag %s: %w", versionRef, err)567 return fmt.Errorf("failed to tag %s: %w", versionRef, err)
562 }568 }
563 if version != "latest" {569 if version != "latest" {
564- if err := dockerTag(ctx, localImage, latestRef); err != nil {570+ if err := dockerTagFn(ctx, localImage, latestRef); err != nil {
565 return fmt.Errorf("failed to tag %s: %w", latestRef, err)571 return fmt.Errorf("failed to tag %s: %w", latestRef, err)
566 }572 }
567 }573 }
568 574
569 fmt.Println("Pushing...")575 fmt.Println("Pushing...")
570- if err := dockerPush(ctx, versionRef); err != nil {576+ if err := dockerPushFn(ctx, versionRef); err != nil {
571 return fmt.Errorf("failed to push %s: %w", versionRef, err)577 return fmt.Errorf("failed to push %s: %w", versionRef, err)
572 }578 }
573 if version != "latest" {579 if version != "latest" {
574- if err := dockerPush(ctx, latestRef); err != nil {580+ if err := dockerPushFn(ctx, latestRef); err != nil {
575 return fmt.Errorf("failed to push %s: %w", latestRef, err)581 return fmt.Errorf("failed to push %s: %w", latestRef, err)
576 }582 }
577 }583 }
A internal/adapters/in/cli/push_integration_test.go +135 -0
@@ -0,0 +1,135 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+
11+ "github.com/bnema/zerowrap"
12+ v1 "github.com/google/go-containerregistry/pkg/v1"
13+ "github.com/google/go-containerregistry/pkg/v1/random"
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/require"
16+
17+ registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"
18+ "github.com/bnema/gordon/internal/adapters/out/filesystem"
19+ "github.com/bnema/gordon/internal/domain"
20+ registrysvc "github.com/bnema/gordon/internal/usecase/registry"
21+ "github.com/bnema/gordon/pkg/registrypush"
22+)
23+
24+func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
25+ t.Parallel()
26+
27+ ctx := context.Background()
28+ harness := newCLITestRegistry(t, 95*1024*1024)
29+ defer harness.Close()
30+
31+ img, err := random.Image(512, 1)
32+ require.NoError(t, err)
33+
34+ originalPush := dockerPushFn
35+ originalTag := dockerTagFn
36+ originalImageExists := dockerImageExistsFn
37+ t.Cleanup(func() {
38+ dockerPushFn = originalPush
39+ dockerTagFn = originalTag
40+ dockerImageExistsFn = originalImageExists
41+ })
42+
43+ dockerPushFn = func(ctx context.Context, ref string) error {
44+ pusher := registrypush.New(
45+ registrypush.WithChunkSize(256),
46+ registrypush.WithTransport(harness.server.Client().Transport),
47+ registrypush.WithImageSource(func(context.Context, string) (v1.Image, error) {
48+ return img, nil
49+ }),
50+ )
51+ return pusher.Push(ctx, ref)
52+ }
53+ dockerTagFn = func(context.Context, string, string) error {
54+ return nil
55+ }
56+ dockerImageExistsFn = func(context.Context, string) bool {
57+ return true
58+ }
59+
60+ registry := strings.TrimPrefix(harness.server.URL, "http://")
61+ versionRef := registry + "/testapp:v1.0.0"
62+ latestRef := registry + "/testapp:latest"
63+
64+ err = tagAndPush(ctx, registry, "testapp", "v1.0.0", versionRef, latestRef)
65+ require.NoError(t, err)
66+
67+ harness.RequireManifest(t, "v1.0.0")
68+ harness.RequireManifest(t, "latest")
69+ harness.RequireBlobs(ctx, t, img)
70+}
71+
72+type cliTestRegistry struct {
73+ server *httptest.Server
74+}
75+
76+func newCLITestRegistry(t *testing.T, maxBlobChunkSize int64) *cliTestRegistry {
77+ t.Helper()
78+
79+ log := zerowrap.Default()
80+ rootDir := t.TempDir()
81+
82+ blobStore, err := filesystem.NewBlobStorage(rootDir, log)
83+ require.NoError(t, err)
84+
85+ manifestStore, err := filesystem.NewManifestStorage(rootDir, log)
86+ require.NoError(t, err)
87+
88+ svc := registrysvc.NewService(blobStore, manifestStore, cliNoopEventPublisher{})
89+ handler := registryhttp.NewHandler(svc, log, maxBlobChunkSize)
90+
91+ return &cliTestRegistry{server: httptest.NewServer(handler)}
92+}
93+
94+func (h *cliTestRegistry) Close() {
95+ h.server.Close()
96+}
97+
98+func (h *cliTestRegistry) RequireManifest(t *testing.T, tag string) {
99+ t.Helper()
100+
101+ req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/testapp/manifests/%s", h.server.URL, tag), nil)
102+ require.NoError(t, err)
103+
104+ resp, err := h.server.Client().Do(req)
105+ require.NoError(t, err)
106+ defer resp.Body.Close()
107+
108+ assert.Equal(t, http.StatusOK, resp.StatusCode)
109+}
110+
111+func (h *cliTestRegistry) RequireBlobs(ctx context.Context, t *testing.T, img v1.Image) {
112+ t.Helper()
113+
114+ layers, err := img.Layers()
115+ require.NoError(t, err)
116+
117+ for _, layer := range layers {
118+ digest, digestErr := layer.Digest()
119+ require.NoError(t, digestErr)
120+
121+ req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v2/testapp/blobs/%s", h.server.URL, digest.String()), nil)
122+ require.NoError(t, reqErr)
123+
124+ resp, respErr := h.server.Client().Do(req)
125+ require.NoError(t, respErr)
126+ assert.Equal(t, http.StatusOK, resp.StatusCode)
127+ require.NoError(t, resp.Body.Close())
128+ }
129+}
130+
131+type cliNoopEventPublisher struct{}
132+
133+func (cliNoopEventPublisher) Publish(domain.EventType, any) error {
134+ return nil
135+}
@@ -0,0 +1,135 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+
11+ "github.com/bnema/zerowrap"
12+ v1 "github.com/google/go-containerregistry/pkg/v1"
13+ "github.com/google/go-containerregistry/pkg/v1/random"
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/require"
16+
17+ registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"
18+ "github.com/bnema/gordon/internal/adapters/out/filesystem"
19+ "github.com/bnema/gordon/internal/domain"
20+ registrysvc "github.com/bnema/gordon/internal/usecase/registry"
21+ "github.com/bnema/gordon/pkg/registrypush"
22+)
23+
24+func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
25+ t.Parallel()
26+
27+ ctx := context.Background()
28+ harness := newCLITestRegistry(t, 95*1024*1024)
29+ defer harness.Close()
30+
31+ img, err := random.Image(512, 1)
32+ require.NoError(t, err)
33+
34+ originalPush := dockerPushFn
35+ originalTag := dockerTagFn
36+ originalImageExists := dockerImageExistsFn
37+ t.Cleanup(func() {
38+ dockerPushFn = originalPush
39+ dockerTagFn = originalTag
40+ dockerImageExistsFn = originalImageExists
41+ })
42+
43+ dockerPushFn = func(ctx context.Context, ref string) error {
44+ pusher := registrypush.New(
45+ registrypush.WithChunkSize(256),
46+ registrypush.WithTransport(harness.server.Client().Transport),
47+ registrypush.WithImageSource(func(context.Context, string) (v1.Image, error) {
48+ return img, nil
49+ }),
50+ )
51+ return pusher.Push(ctx, ref)
52+ }
53+ dockerTagFn = func(context.Context, string, string) error {
54+ return nil
55+ }
56+ dockerImageExistsFn = func(context.Context, string) bool {
57+ return true
58+ }
59+
60+ registry := strings.TrimPrefix(harness.server.URL, "http://")
61+ versionRef := registry + "/testapp:v1.0.0"
62+ latestRef := registry + "/testapp:latest"
63+
64+ err = tagAndPush(ctx, registry, "testapp", "v1.0.0", versionRef, latestRef)
65+ require.NoError(t, err)
66+
67+ harness.RequireManifest(t, "v1.0.0")
68+ harness.RequireManifest(t, "latest")
69+ harness.RequireBlobs(ctx, t, img)
70+}
71+
72+type cliTestRegistry struct {
73+ server *httptest.Server
74+}
75+
76+func newCLITestRegistry(t *testing.T, maxBlobChunkSize int64) *cliTestRegistry {
77+ t.Helper()
78+
79+ log := zerowrap.Default()
80+ rootDir := t.TempDir()
81+
82+ blobStore, err := filesystem.NewBlobStorage(rootDir, log)
83+ require.NoError(t, err)
84+
85+ manifestStore, err := filesystem.NewManifestStorage(rootDir, log)
86+ require.NoError(t, err)
87+
88+ svc := registrysvc.NewService(blobStore, manifestStore, cliNoopEventPublisher{})
89+ handler := registryhttp.NewHandler(svc, log, maxBlobChunkSize)
90+
91+ return &cliTestRegistry{server: httptest.NewServer(handler)}
92+}
93+
94+func (h *cliTestRegistry) Close() {
95+ h.server.Close()
96+}
97+
98+func (h *cliTestRegistry) RequireManifest(t *testing.T, tag string) {
99+ t.Helper()
100+
101+ req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/testapp/manifests/%s", h.server.URL, tag), nil)
102+ require.NoError(t, err)
103+
104+ resp, err := h.server.Client().Do(req)
105+ require.NoError(t, err)
106+ defer resp.Body.Close()
107+
108+ assert.Equal(t, http.StatusOK, resp.StatusCode)
109+}
110+
111+func (h *cliTestRegistry) RequireBlobs(ctx context.Context, t *testing.T, img v1.Image) {
112+ t.Helper()
113+
114+ layers, err := img.Layers()
115+ require.NoError(t, err)
116+
117+ for _, layer := range layers {
118+ digest, digestErr := layer.Digest()
119+ require.NoError(t, digestErr)
120+
121+ req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v2/testapp/blobs/%s", h.server.URL, digest.String()), nil)
122+ require.NoError(t, reqErr)
123+
124+ resp, respErr := h.server.Client().Do(req)
125+ require.NoError(t, respErr)
126+ assert.Equal(t, http.StatusOK, resp.StatusCode)
127+ require.NoError(t, resp.Body.Close())
128+ }
129+}
130+
131+type cliNoopEventPublisher struct{}
132+
133+func (cliNoopEventPublisher) Publish(domain.EventType, any) error {
134+ return nil
135+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close