Repository: aws/amazon-ecs-cli-v2
Pull request #530 (2019-12-10 17:48:12)

Merge dev into master
<!-- Provide summary of changes -->
Let's get rid of the temporary `dev` branch.

<!-- Issue number, if available. E.g. "Fixes #31", "Addresses #42, 77" -->

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/Makefile b/Makefile
index 6c8e7c90..98ca698a 100644
--- a/Makefile
+++ b/Makefile
@@ -138,3 +138,5 @@ gen-mocks: tools
 	${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/cli/mocks/mock_rg.go github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface ResourceGroupsTaggingAPIAPI
 	${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/cli/mocks/mock_iam.go github.com/aws/aws-sdk-go/service/iam/iamiface IAMAPI
 	${GOBIN}/mockgen -source=./internal/pkg/describe/describe.go -package=mocks -destination=./internal/pkg/describe/mocks/mock_describe.go
+	${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/mocks/mock_ecr.go github.com/aws/aws-sdk-go/service/ecr/ecriface ECRAPI
+	${GOBIN}/mockgen -source=./internal/pkg/build/docker/docker.go -package=mocks -destination=./internal/pkg/build/docker/mocks/mock_docker.go
\ No newline at end of file
diff --git a/internal/pkg/archer/manifest.go b/internal/pkg/archer/manifest.go
index 15e978a2..9d58dfbe 100644
--- a/internal/pkg/archer/manifest.go
+++ b/internal/pkg/archer/manifest.go
@@ -8,4 +8,5 @@ type Manifest interface {
 	Marshal() ([]byte, error)
 	DockerfilePath() string
 	AppName() string
+	IntegTestBuildspecPath() string
 }
diff --git a/internal/pkg/aws/ecr/ecr.go b/internal/pkg/aws/ecr/ecr.go
index a493a877..200d8ca7 100644
--- a/internal/pkg/aws/ecr/ecr.go
+++ b/internal/pkg/aws/ecr/ecr.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/arn"
 	"github.com/aws/aws-sdk-go/aws/session"
@@ -20,6 +21,7 @@ import (
 const (
 	urlFmtString      = "%s.dkr.ecr.%s.amazonaws.com/%s"
 	arnResourcePrefix = "repository/"
+	batchDeleteLimit  = 100
 )
 
 // Service wraps an AWS ECR client.
@@ -97,22 +99,32 @@ func (i Image) imageIdentifier() *ecr.ImageIdentifier {
 // ListImages calls the ECR DescribeImages API and returns a list of
 // Image metadata for images in the input ECR repository name.
 func (s Service) ListImages(repoName string) ([]Image, error) {
-	// TODO: handle paginated responses
+	var images []Image
 	resp, err := s.ecr.DescribeImages(&ecr.DescribeImagesInput{
 		RepositoryName: aws.String(repoName),
 	})
-
 	if err != nil {
 		return nil, fmt.Errorf("ecr repo %s describe images: %w", repoName, err)
 	}
-
-	var images []Image
 	for _, imageDetails := range resp.ImageDetails {
 		images = append(images, Image{
 			Digest: *imageDetails.ImageDigest,
 		})
 	}
-
+	for resp.NextToken != nil {
+		resp, err = s.ecr.DescribeImages(&ecr.DescribeImagesInput{
+			RepositoryName: aws.String(repoName),
+			NextToken:      resp.NextToken,
+		})
+		if err != nil {
+			return nil, fmt.Errorf("ecr repo %s describe images: %w", repoName, err)
+		}
+		for _, imageDetails := range resp.ImageDetails {
+			images = append(images, Image{
+				Digest: *imageDetails.ImageDigest,
+			})
+		}
+	}
 	return images, nil
 }
 
@@ -126,15 +138,24 @@ func (s Service) DeleteImages(images []Image, repoName string) error {
 	for _, image := range images {
 		imageIdentifiers = append(imageIdentifiers, image.imageIdentifier())
 	}
-
-	// TODO: handle response ecr.ImageFailures
-	_, err := s.ecr.BatchDeleteImage(&ecr.BatchDeleteImageInput{
-		RepositoryName: aws.String(repoName),
-		ImageIds:       imageIdentifiers,
-	})
-
-	if err != nil {
-		return fmt.Errorf("ecr repo %s batch delete image: %w", repoName, err)
+	var imageIdentifiersBatch [][]*ecr.ImageIdentifier
+	for batchDeleteLimit < len(imageIdentifiers) {
+		imageIdentifiers, imageIdentifiersBatch = imageIdentifiers[batchDeleteLimit:], append(imageIdentifiersBatch, imageIdentifiers[0:batchDeleteLimit])
+	}
+	imageIdentifiersBatch = append(imageIdentifiersBatch, imageIdentifiers)
+	for _, identifiers := range imageIdentifiersBatch {
+		resp, err := s.ecr.BatchDeleteImage(&ecr.BatchDeleteImageInput{
+			RepositoryName: aws.String(repoName),
+			ImageIds:       identifiers,
+		})
+		if resp != nil {
+			for _, failure := range resp.Failures {
+				log.Warningf("failed to delete %s:%s : %s %s\n", failure.ImageId.ImageDigest, failure.ImageId.ImageTag, failure.FailureCode, failure.FailureReason)
+			}
+		}
+		if err != nil {
+			return fmt.Errorf("ecr repo %s batch delete image: %w", repoName, err)
+		}
 	}
 
 	return nil
diff --git a/internal/pkg/aws/mocks/mock_ecr.go b/internal/pkg/aws/mocks/mock_ecr.go
new file mode 100644
index 00000000..295665ce
--- /dev/null
+++ b/internal/pkg/aws/mocks/mock_ecr.go
@@ -0,0 +1,1651 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/aws/aws-sdk-go/service/ecr/ecriface (interfaces: ECRAPI)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	request "github.com/aws/aws-sdk-go/aws/request"
+	ecr "github.com/aws/aws-sdk-go/service/ecr"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+)
+
+// MockECRAPI is a mock of ECRAPI interface
+type MockECRAPI struct {
+	ctrl     *gomock.Controller
+	recorder *MockECRAPIMockRecorder
+}
+
+// MockECRAPIMockRecorder is the mock recorder for MockECRAPI
+type MockECRAPIMockRecorder struct {
+	mock *MockECRAPI
+}
+
+// NewMockECRAPI creates a new mock instance
+func NewMockECRAPI(ctrl *gomock.Controller) *MockECRAPI {
+	mock := &MockECRAPI{ctrl: ctrl}
+	mock.recorder = &MockECRAPIMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockECRAPI) EXPECT() *MockECRAPIMockRecorder {
+	return m.recorder
+}
+
+// BatchCheckLayerAvailability mocks base method
+func (m *MockECRAPI) BatchCheckLayerAvailability(arg0 *ecr.BatchCheckLayerAvailabilityInput) (*ecr.BatchCheckLayerAvailabilityOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchCheckLayerAvailability", arg0)
+	ret0, _ := ret[0].(*ecr.BatchCheckLayerAvailabilityOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchCheckLayerAvailability indicates an expected call of BatchCheckLayerAvailability
+func (mr *MockECRAPIMockRecorder) BatchCheckLayerAvailability(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchCheckLayerAvailability", reflect.TypeOf((*MockECRAPI)(nil).BatchCheckLayerAvailability), arg0)
+}
+
+// BatchCheckLayerAvailabilityRequest mocks base method
+func (m *MockECRAPI) BatchCheckLayerAvailabilityRequest(arg0 *ecr.BatchCheckLayerAvailabilityInput) (*request.Request, *ecr.BatchCheckLayerAvailabilityOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchCheckLayerAvailabilityRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.BatchCheckLayerAvailabilityOutput)
+	return ret0, ret1
+}
+
+// BatchCheckLayerAvailabilityRequest indicates an expected call of BatchCheckLayerAvailabilityRequest
+func (mr *MockECRAPIMockRecorder) BatchCheckLayerAvailabilityRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchCheckLayerAvailabilityRequest", reflect.TypeOf((*MockECRAPI)(nil).BatchCheckLayerAvailabilityRequest), arg0)
+}
+
+// BatchCheckLayerAvailabilityWithContext mocks base method
+func (m *MockECRAPI) BatchCheckLayerAvailabilityWithContext(arg0 context.Context, arg1 *ecr.BatchCheckLayerAvailabilityInput, arg2 ...request.Option) (*ecr.BatchCheckLayerAvailabilityOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "BatchCheckLayerAvailabilityWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.BatchCheckLayerAvailabilityOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchCheckLayerAvailabilityWithContext indicates an expected call of BatchCheckLayerAvailabilityWithContext
+func (mr *MockECRAPIMockRecorder) BatchCheckLayerAvailabilityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchCheckLayerAvailabilityWithContext", reflect.TypeOf((*MockECRAPI)(nil).BatchCheckLayerAvailabilityWithContext), varargs...)
+}
+
+// BatchDeleteImage mocks base method
+func (m *MockECRAPI) BatchDeleteImage(arg0 *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchDeleteImage", arg0)
+	ret0, _ := ret[0].(*ecr.BatchDeleteImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchDeleteImage indicates an expected call of BatchDeleteImage
+func (mr *MockECRAPIMockRecorder) BatchDeleteImage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchDeleteImage", reflect.TypeOf((*MockECRAPI)(nil).BatchDeleteImage), arg0)
+}
+
+// BatchDeleteImageRequest mocks base method
+func (m *MockECRAPI) BatchDeleteImageRequest(arg0 *ecr.BatchDeleteImageInput) (*request.Request, *ecr.BatchDeleteImageOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchDeleteImageRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.BatchDeleteImageOutput)
+	return ret0, ret1
+}
+
+// BatchDeleteImageRequest indicates an expected call of BatchDeleteImageRequest
+func (mr *MockECRAPIMockRecorder) BatchDeleteImageRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchDeleteImageRequest", reflect.TypeOf((*MockECRAPI)(nil).BatchDeleteImageRequest), arg0)
+}
+
+// BatchDeleteImageWithContext mocks base method
+func (m *MockECRAPI) BatchDeleteImageWithContext(arg0 context.Context, arg1 *ecr.BatchDeleteImageInput, arg2 ...request.Option) (*ecr.BatchDeleteImageOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "BatchDeleteImageWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.BatchDeleteImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchDeleteImageWithContext indicates an expected call of BatchDeleteImageWithContext
+func (mr *MockECRAPIMockRecorder) BatchDeleteImageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchDeleteImageWithContext", reflect.TypeOf((*MockECRAPI)(nil).BatchDeleteImageWithContext), varargs...)
+}
+
+// BatchGetImage mocks base method
+func (m *MockECRAPI) BatchGetImage(arg0 *ecr.BatchGetImageInput) (*ecr.BatchGetImageOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchGetImage", arg0)
+	ret0, _ := ret[0].(*ecr.BatchGetImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchGetImage indicates an expected call of BatchGetImage
+func (mr *MockECRAPIMockRecorder) BatchGetImage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetImage", reflect.TypeOf((*MockECRAPI)(nil).BatchGetImage), arg0)
+}
+
+// BatchGetImageRequest mocks base method
+func (m *MockECRAPI) BatchGetImageRequest(arg0 *ecr.BatchGetImageInput) (*request.Request, *ecr.BatchGetImageOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BatchGetImageRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.BatchGetImageOutput)
+	return ret0, ret1
+}
+
+// BatchGetImageRequest indicates an expected call of BatchGetImageRequest
+func (mr *MockECRAPIMockRecorder) BatchGetImageRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetImageRequest", reflect.TypeOf((*MockECRAPI)(nil).BatchGetImageRequest), arg0)
+}
+
+// BatchGetImageWithContext mocks base method
+func (m *MockECRAPI) BatchGetImageWithContext(arg0 context.Context, arg1 *ecr.BatchGetImageInput, arg2 ...request.Option) (*ecr.BatchGetImageOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "BatchGetImageWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.BatchGetImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// BatchGetImageWithContext indicates an expected call of BatchGetImageWithContext
+func (mr *MockECRAPIMockRecorder) BatchGetImageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetImageWithContext", reflect.TypeOf((*MockECRAPI)(nil).BatchGetImageWithContext), varargs...)
+}
+
+// CompleteLayerUpload mocks base method
+func (m *MockECRAPI) CompleteLayerUpload(arg0 *ecr.CompleteLayerUploadInput) (*ecr.CompleteLayerUploadOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CompleteLayerUpload", arg0)
+	ret0, _ := ret[0].(*ecr.CompleteLayerUploadOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CompleteLayerUpload indicates an expected call of CompleteLayerUpload
+func (mr *MockECRAPIMockRecorder) CompleteLayerUpload(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteLayerUpload", reflect.TypeOf((*MockECRAPI)(nil).CompleteLayerUpload), arg0)
+}
+
+// CompleteLayerUploadRequest mocks base method
+func (m *MockECRAPI) CompleteLayerUploadRequest(arg0 *ecr.CompleteLayerUploadInput) (*request.Request, *ecr.CompleteLayerUploadOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CompleteLayerUploadRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.CompleteLayerUploadOutput)
+	return ret0, ret1
+}
+
+// CompleteLayerUploadRequest indicates an expected call of CompleteLayerUploadRequest
+func (mr *MockECRAPIMockRecorder) CompleteLayerUploadRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteLayerUploadRequest", reflect.TypeOf((*MockECRAPI)(nil).CompleteLayerUploadRequest), arg0)
+}
+
+// CompleteLayerUploadWithContext mocks base method
+func (m *MockECRAPI) CompleteLayerUploadWithContext(arg0 context.Context, arg1 *ecr.CompleteLayerUploadInput, arg2 ...request.Option) (*ecr.CompleteLayerUploadOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "CompleteLayerUploadWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.CompleteLayerUploadOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CompleteLayerUploadWithContext indicates an expected call of CompleteLayerUploadWithContext
+func (mr *MockECRAPIMockRecorder) CompleteLayerUploadWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteLayerUploadWithContext", reflect.TypeOf((*MockECRAPI)(nil).CompleteLayerUploadWithContext), varargs...)
+}
+
+// CreateRepository mocks base method
+func (m *MockECRAPI) CreateRepository(arg0 *ecr.CreateRepositoryInput) (*ecr.CreateRepositoryOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateRepository", arg0)
+	ret0, _ := ret[0].(*ecr.CreateRepositoryOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateRepository indicates an expected call of CreateRepository
+func (mr *MockECRAPIMockRecorder) CreateRepository(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepository", reflect.TypeOf((*MockECRAPI)(nil).CreateRepository), arg0)
+}
+
+// CreateRepositoryRequest mocks base method
+func (m *MockECRAPI) CreateRepositoryRequest(arg0 *ecr.CreateRepositoryInput) (*request.Request, *ecr.CreateRepositoryOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateRepositoryRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.CreateRepositoryOutput)
+	return ret0, ret1
+}
+
+// CreateRepositoryRequest indicates an expected call of CreateRepositoryRequest
+func (mr *MockECRAPIMockRecorder) CreateRepositoryRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryRequest", reflect.TypeOf((*MockECRAPI)(nil).CreateRepositoryRequest), arg0)
+}
+
+// CreateRepositoryWithContext mocks base method
+func (m *MockECRAPI) CreateRepositoryWithContext(arg0 context.Context, arg1 *ecr.CreateRepositoryInput, arg2 ...request.Option) (*ecr.CreateRepositoryOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "CreateRepositoryWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.CreateRepositoryOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateRepositoryWithContext indicates an expected call of CreateRepositoryWithContext
+func (mr *MockECRAPIMockRecorder) CreateRepositoryWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryWithContext", reflect.TypeOf((*MockECRAPI)(nil).CreateRepositoryWithContext), varargs...)
+}
+
+// DeleteLifecyclePolicy mocks base method
+func (m *MockECRAPI) DeleteLifecyclePolicy(arg0 *ecr.DeleteLifecyclePolicyInput) (*ecr.DeleteLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteLifecyclePolicy", arg0)
+	ret0, _ := ret[0].(*ecr.DeleteLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteLifecyclePolicy indicates an expected call of DeleteLifecyclePolicy
+func (mr *MockECRAPIMockRecorder) DeleteLifecyclePolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLifecyclePolicy", reflect.TypeOf((*MockECRAPI)(nil).DeleteLifecyclePolicy), arg0)
+}
+
+// DeleteLifecyclePolicyRequest mocks base method
+func (m *MockECRAPI) DeleteLifecyclePolicyRequest(arg0 *ecr.DeleteLifecyclePolicyInput) (*request.Request, *ecr.DeleteLifecyclePolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteLifecyclePolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DeleteLifecyclePolicyOutput)
+	return ret0, ret1
+}
+
+// DeleteLifecyclePolicyRequest indicates an expected call of DeleteLifecyclePolicyRequest
+func (mr *MockECRAPIMockRecorder) DeleteLifecyclePolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLifecyclePolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).DeleteLifecyclePolicyRequest), arg0)
+}
+
+// DeleteLifecyclePolicyWithContext mocks base method
+func (m *MockECRAPI) DeleteLifecyclePolicyWithContext(arg0 context.Context, arg1 *ecr.DeleteLifecyclePolicyInput, arg2 ...request.Option) (*ecr.DeleteLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DeleteLifecyclePolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DeleteLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteLifecyclePolicyWithContext indicates an expected call of DeleteLifecyclePolicyWithContext
+func (mr *MockECRAPIMockRecorder) DeleteLifecyclePolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLifecyclePolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).DeleteLifecyclePolicyWithContext), varargs...)
+}
+
+// DeleteRepository mocks base method
+func (m *MockECRAPI) DeleteRepository(arg0 *ecr.DeleteRepositoryInput) (*ecr.DeleteRepositoryOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteRepository", arg0)
+	ret0, _ := ret[0].(*ecr.DeleteRepositoryOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteRepository indicates an expected call of DeleteRepository
+func (mr *MockECRAPIMockRecorder) DeleteRepository(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepository), arg0)
+}
+
+// DeleteRepositoryPolicy mocks base method
+func (m *MockECRAPI) DeleteRepositoryPolicy(arg0 *ecr.DeleteRepositoryPolicyInput) (*ecr.DeleteRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteRepositoryPolicy", arg0)
+	ret0, _ := ret[0].(*ecr.DeleteRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteRepositoryPolicy indicates an expected call of DeleteRepositoryPolicy
+func (mr *MockECRAPIMockRecorder) DeleteRepositoryPolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryPolicy", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepositoryPolicy), arg0)
+}
+
+// DeleteRepositoryPolicyRequest mocks base method
+func (m *MockECRAPI) DeleteRepositoryPolicyRequest(arg0 *ecr.DeleteRepositoryPolicyInput) (*request.Request, *ecr.DeleteRepositoryPolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteRepositoryPolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DeleteRepositoryPolicyOutput)
+	return ret0, ret1
+}
+
+// DeleteRepositoryPolicyRequest indicates an expected call of DeleteRepositoryPolicyRequest
+func (mr *MockECRAPIMockRecorder) DeleteRepositoryPolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryPolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepositoryPolicyRequest), arg0)
+}
+
+// DeleteRepositoryPolicyWithContext mocks base method
+func (m *MockECRAPI) DeleteRepositoryPolicyWithContext(arg0 context.Context, arg1 *ecr.DeleteRepositoryPolicyInput, arg2 ...request.Option) (*ecr.DeleteRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DeleteRepositoryPolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DeleteRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteRepositoryPolicyWithContext indicates an expected call of DeleteRepositoryPolicyWithContext
+func (mr *MockECRAPIMockRecorder) DeleteRepositoryPolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryPolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepositoryPolicyWithContext), varargs...)
+}
+
+// DeleteRepositoryRequest mocks base method
+func (m *MockECRAPI) DeleteRepositoryRequest(arg0 *ecr.DeleteRepositoryInput) (*request.Request, *ecr.DeleteRepositoryOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteRepositoryRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DeleteRepositoryOutput)
+	return ret0, ret1
+}
+
+// DeleteRepositoryRequest indicates an expected call of DeleteRepositoryRequest
+func (mr *MockECRAPIMockRecorder) DeleteRepositoryRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryRequest", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepositoryRequest), arg0)
+}
+
+// DeleteRepositoryWithContext mocks base method
+func (m *MockECRAPI) DeleteRepositoryWithContext(arg0 context.Context, arg1 *ecr.DeleteRepositoryInput, arg2 ...request.Option) (*ecr.DeleteRepositoryOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DeleteRepositoryWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DeleteRepositoryOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteRepositoryWithContext indicates an expected call of DeleteRepositoryWithContext
+func (mr *MockECRAPIMockRecorder) DeleteRepositoryWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryWithContext", reflect.TypeOf((*MockECRAPI)(nil).DeleteRepositoryWithContext), varargs...)
+}
+
+// DescribeImageScanFindings mocks base method
+func (m *MockECRAPI) DescribeImageScanFindings(arg0 *ecr.DescribeImageScanFindingsInput) (*ecr.DescribeImageScanFindingsOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImageScanFindings", arg0)
+	ret0, _ := ret[0].(*ecr.DescribeImageScanFindingsOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeImageScanFindings indicates an expected call of DescribeImageScanFindings
+func (mr *MockECRAPIMockRecorder) DescribeImageScanFindings(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImageScanFindings", reflect.TypeOf((*MockECRAPI)(nil).DescribeImageScanFindings), arg0)
+}
+
+// DescribeImageScanFindingsPages mocks base method
+func (m *MockECRAPI) DescribeImageScanFindingsPages(arg0 *ecr.DescribeImageScanFindingsInput, arg1 func(*ecr.DescribeImageScanFindingsOutput, bool) bool) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImageScanFindingsPages", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeImageScanFindingsPages indicates an expected call of DescribeImageScanFindingsPages
+func (mr *MockECRAPIMockRecorder) DescribeImageScanFindingsPages(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImageScanFindingsPages", reflect.TypeOf((*MockECRAPI)(nil).DescribeImageScanFindingsPages), arg0, arg1)
+}
+
+// DescribeImageScanFindingsPagesWithContext mocks base method
+func (m *MockECRAPI) DescribeImageScanFindingsPagesWithContext(arg0 context.Context, arg1 *ecr.DescribeImageScanFindingsInput, arg2 func(*ecr.DescribeImageScanFindingsOutput, bool) bool, arg3 ...request.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeImageScanFindingsPagesWithContext", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeImageScanFindingsPagesWithContext indicates an expected call of DescribeImageScanFindingsPagesWithContext
+func (mr *MockECRAPIMockRecorder) DescribeImageScanFindingsPagesWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImageScanFindingsPagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeImageScanFindingsPagesWithContext), varargs...)
+}
+
+// DescribeImageScanFindingsRequest mocks base method
+func (m *MockECRAPI) DescribeImageScanFindingsRequest(arg0 *ecr.DescribeImageScanFindingsInput) (*request.Request, *ecr.DescribeImageScanFindingsOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImageScanFindingsRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DescribeImageScanFindingsOutput)
+	return ret0, ret1
+}
+
+// DescribeImageScanFindingsRequest indicates an expected call of DescribeImageScanFindingsRequest
+func (mr *MockECRAPIMockRecorder) DescribeImageScanFindingsRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImageScanFindingsRequest", reflect.TypeOf((*MockECRAPI)(nil).DescribeImageScanFindingsRequest), arg0)
+}
+
+// DescribeImageScanFindingsWithContext mocks base method
+func (m *MockECRAPI) DescribeImageScanFindingsWithContext(arg0 context.Context, arg1 *ecr.DescribeImageScanFindingsInput, arg2 ...request.Option) (*ecr.DescribeImageScanFindingsOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeImageScanFindingsWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DescribeImageScanFindingsOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeImageScanFindingsWithContext indicates an expected call of DescribeImageScanFindingsWithContext
+func (mr *MockECRAPIMockRecorder) DescribeImageScanFindingsWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImageScanFindingsWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeImageScanFindingsWithContext), varargs...)
+}
+
+// DescribeImages mocks base method
+func (m *MockECRAPI) DescribeImages(arg0 *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImages", arg0)
+	ret0, _ := ret[0].(*ecr.DescribeImagesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeImages indicates an expected call of DescribeImages
+func (mr *MockECRAPIMockRecorder) DescribeImages(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImages", reflect.TypeOf((*MockECRAPI)(nil).DescribeImages), arg0)
+}
+
+// DescribeImagesPages mocks base method
+func (m *MockECRAPI) DescribeImagesPages(arg0 *ecr.DescribeImagesInput, arg1 func(*ecr.DescribeImagesOutput, bool) bool) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImagesPages", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeImagesPages indicates an expected call of DescribeImagesPages
+func (mr *MockECRAPIMockRecorder) DescribeImagesPages(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImagesPages", reflect.TypeOf((*MockECRAPI)(nil).DescribeImagesPages), arg0, arg1)
+}
+
+// DescribeImagesPagesWithContext mocks base method
+func (m *MockECRAPI) DescribeImagesPagesWithContext(arg0 context.Context, arg1 *ecr.DescribeImagesInput, arg2 func(*ecr.DescribeImagesOutput, bool) bool, arg3 ...request.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeImagesPagesWithContext", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeImagesPagesWithContext indicates an expected call of DescribeImagesPagesWithContext
+func (mr *MockECRAPIMockRecorder) DescribeImagesPagesWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImagesPagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeImagesPagesWithContext), varargs...)
+}
+
+// DescribeImagesRequest mocks base method
+func (m *MockECRAPI) DescribeImagesRequest(arg0 *ecr.DescribeImagesInput) (*request.Request, *ecr.DescribeImagesOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeImagesRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DescribeImagesOutput)
+	return ret0, ret1
+}
+
+// DescribeImagesRequest indicates an expected call of DescribeImagesRequest
+func (mr *MockECRAPIMockRecorder) DescribeImagesRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImagesRequest", reflect.TypeOf((*MockECRAPI)(nil).DescribeImagesRequest), arg0)
+}
+
+// DescribeImagesWithContext mocks base method
+func (m *MockECRAPI) DescribeImagesWithContext(arg0 context.Context, arg1 *ecr.DescribeImagesInput, arg2 ...request.Option) (*ecr.DescribeImagesOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeImagesWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DescribeImagesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeImagesWithContext indicates an expected call of DescribeImagesWithContext
+func (mr *MockECRAPIMockRecorder) DescribeImagesWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeImagesWithContext), varargs...)
+}
+
+// DescribeRepositories mocks base method
+func (m *MockECRAPI) DescribeRepositories(arg0 *ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeRepositories", arg0)
+	ret0, _ := ret[0].(*ecr.DescribeRepositoriesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeRepositories indicates an expected call of DescribeRepositories
+func (mr *MockECRAPIMockRecorder) DescribeRepositories(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositories", reflect.TypeOf((*MockECRAPI)(nil).DescribeRepositories), arg0)
+}
+
+// DescribeRepositoriesPages mocks base method
+func (m *MockECRAPI) DescribeRepositoriesPages(arg0 *ecr.DescribeRepositoriesInput, arg1 func(*ecr.DescribeRepositoriesOutput, bool) bool) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeRepositoriesPages", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeRepositoriesPages indicates an expected call of DescribeRepositoriesPages
+func (mr *MockECRAPIMockRecorder) DescribeRepositoriesPages(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoriesPages", reflect.TypeOf((*MockECRAPI)(nil).DescribeRepositoriesPages), arg0, arg1)
+}
+
+// DescribeRepositoriesPagesWithContext mocks base method
+func (m *MockECRAPI) DescribeRepositoriesPagesWithContext(arg0 context.Context, arg1 *ecr.DescribeRepositoriesInput, arg2 func(*ecr.DescribeRepositoriesOutput, bool) bool, arg3 ...request.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeRepositoriesPagesWithContext", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DescribeRepositoriesPagesWithContext indicates an expected call of DescribeRepositoriesPagesWithContext
+func (mr *MockECRAPIMockRecorder) DescribeRepositoriesPagesWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoriesPagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeRepositoriesPagesWithContext), varargs...)
+}
+
+// DescribeRepositoriesRequest mocks base method
+func (m *MockECRAPI) DescribeRepositoriesRequest(arg0 *ecr.DescribeRepositoriesInput) (*request.Request, *ecr.DescribeRepositoriesOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeRepositoriesRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.DescribeRepositoriesOutput)
+	return ret0, ret1
+}
+
+// DescribeRepositoriesRequest indicates an expected call of DescribeRepositoriesRequest
+func (mr *MockECRAPIMockRecorder) DescribeRepositoriesRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoriesRequest", reflect.TypeOf((*MockECRAPI)(nil).DescribeRepositoriesRequest), arg0)
+}
+
+// DescribeRepositoriesWithContext mocks base method
+func (m *MockECRAPI) DescribeRepositoriesWithContext(arg0 context.Context, arg1 *ecr.DescribeRepositoriesInput, arg2 ...request.Option) (*ecr.DescribeRepositoriesOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeRepositoriesWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.DescribeRepositoriesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeRepositoriesWithContext indicates an expected call of DescribeRepositoriesWithContext
+func (mr *MockECRAPIMockRecorder) DescribeRepositoriesWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoriesWithContext", reflect.TypeOf((*MockECRAPI)(nil).DescribeRepositoriesWithContext), varargs...)
+}
+
+// GetAuthorizationToken mocks base method
+func (m *MockECRAPI) GetAuthorizationToken(arg0 *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetAuthorizationToken", arg0)
+	ret0, _ := ret[0].(*ecr.GetAuthorizationTokenOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetAuthorizationToken indicates an expected call of GetAuthorizationToken
+func (mr *MockECRAPIMockRecorder) GetAuthorizationToken(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizationToken", reflect.TypeOf((*MockECRAPI)(nil).GetAuthorizationToken), arg0)
+}
+
+// GetAuthorizationTokenRequest mocks base method
+func (m *MockECRAPI) GetAuthorizationTokenRequest(arg0 *ecr.GetAuthorizationTokenInput) (*request.Request, *ecr.GetAuthorizationTokenOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetAuthorizationTokenRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.GetAuthorizationTokenOutput)
+	return ret0, ret1
+}
+
+// GetAuthorizationTokenRequest indicates an expected call of GetAuthorizationTokenRequest
+func (mr *MockECRAPIMockRecorder) GetAuthorizationTokenRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizationTokenRequest", reflect.TypeOf((*MockECRAPI)(nil).GetAuthorizationTokenRequest), arg0)
+}
+
+// GetAuthorizationTokenWithContext mocks base method
+func (m *MockECRAPI) GetAuthorizationTokenWithContext(arg0 context.Context, arg1 *ecr.GetAuthorizationTokenInput, arg2 ...request.Option) (*ecr.GetAuthorizationTokenOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetAuthorizationTokenWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.GetAuthorizationTokenOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetAuthorizationTokenWithContext indicates an expected call of GetAuthorizationTokenWithContext
+func (mr *MockECRAPIMockRecorder) GetAuthorizationTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizationTokenWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetAuthorizationTokenWithContext), varargs...)
+}
+
+// GetDownloadUrlForLayer mocks base method
+func (m *MockECRAPI) GetDownloadUrlForLayer(arg0 *ecr.GetDownloadUrlForLayerInput) (*ecr.GetDownloadUrlForLayerOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetDownloadUrlForLayer", arg0)
+	ret0, _ := ret[0].(*ecr.GetDownloadUrlForLayerOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetDownloadUrlForLayer indicates an expected call of GetDownloadUrlForLayer
+func (mr *MockECRAPIMockRecorder) GetDownloadUrlForLayer(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownloadUrlForLayer", reflect.TypeOf((*MockECRAPI)(nil).GetDownloadUrlForLayer), arg0)
+}
+
+// GetDownloadUrlForLayerRequest mocks base method
+func (m *MockECRAPI) GetDownloadUrlForLayerRequest(arg0 *ecr.GetDownloadUrlForLayerInput) (*request.Request, *ecr.GetDownloadUrlForLayerOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetDownloadUrlForLayerRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.GetDownloadUrlForLayerOutput)
+	return ret0, ret1
+}
+
+// GetDownloadUrlForLayerRequest indicates an expected call of GetDownloadUrlForLayerRequest
+func (mr *MockECRAPIMockRecorder) GetDownloadUrlForLayerRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownloadUrlForLayerRequest", reflect.TypeOf((*MockECRAPI)(nil).GetDownloadUrlForLayerRequest), arg0)
+}
+
+// GetDownloadUrlForLayerWithContext mocks base method
+func (m *MockECRAPI) GetDownloadUrlForLayerWithContext(arg0 context.Context, arg1 *ecr.GetDownloadUrlForLayerInput, arg2 ...request.Option) (*ecr.GetDownloadUrlForLayerOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetDownloadUrlForLayerWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.GetDownloadUrlForLayerOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetDownloadUrlForLayerWithContext indicates an expected call of GetDownloadUrlForLayerWithContext
+func (mr *MockECRAPIMockRecorder) GetDownloadUrlForLayerWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownloadUrlForLayerWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetDownloadUrlForLayerWithContext), varargs...)
+}
+
+// GetLifecyclePolicy mocks base method
+func (m *MockECRAPI) GetLifecyclePolicy(arg0 *ecr.GetLifecyclePolicyInput) (*ecr.GetLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLifecyclePolicy", arg0)
+	ret0, _ := ret[0].(*ecr.GetLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicy indicates an expected call of GetLifecyclePolicy
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicy", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicy), arg0)
+}
+
+// GetLifecyclePolicyPreview mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyPreview(arg0 *ecr.GetLifecyclePolicyPreviewInput) (*ecr.GetLifecyclePolicyPreviewOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyPreview", arg0)
+	ret0, _ := ret[0].(*ecr.GetLifecyclePolicyPreviewOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicyPreview indicates an expected call of GetLifecyclePolicyPreview
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyPreview(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyPreview", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyPreview), arg0)
+}
+
+// GetLifecyclePolicyPreviewPages mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyPreviewPages(arg0 *ecr.GetLifecyclePolicyPreviewInput, arg1 func(*ecr.GetLifecyclePolicyPreviewOutput, bool) bool) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyPreviewPages", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// GetLifecyclePolicyPreviewPages indicates an expected call of GetLifecyclePolicyPreviewPages
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyPreviewPages(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyPreviewPages", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyPreviewPages), arg0, arg1)
+}
+
+// GetLifecyclePolicyPreviewPagesWithContext mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyPreviewPagesWithContext(arg0 context.Context, arg1 *ecr.GetLifecyclePolicyPreviewInput, arg2 func(*ecr.GetLifecyclePolicyPreviewOutput, bool) bool, arg3 ...request.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyPreviewPagesWithContext", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// GetLifecyclePolicyPreviewPagesWithContext indicates an expected call of GetLifecyclePolicyPreviewPagesWithContext
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyPreviewPagesWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyPreviewPagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyPreviewPagesWithContext), varargs...)
+}
+
+// GetLifecyclePolicyPreviewRequest mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyPreviewRequest(arg0 *ecr.GetLifecyclePolicyPreviewInput) (*request.Request, *ecr.GetLifecyclePolicyPreviewOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyPreviewRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.GetLifecyclePolicyPreviewOutput)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicyPreviewRequest indicates an expected call of GetLifecyclePolicyPreviewRequest
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyPreviewRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyPreviewRequest", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyPreviewRequest), arg0)
+}
+
+// GetLifecyclePolicyPreviewWithContext mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyPreviewWithContext(arg0 context.Context, arg1 *ecr.GetLifecyclePolicyPreviewInput, arg2 ...request.Option) (*ecr.GetLifecyclePolicyPreviewOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyPreviewWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.GetLifecyclePolicyPreviewOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicyPreviewWithContext indicates an expected call of GetLifecyclePolicyPreviewWithContext
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyPreviewWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyPreviewWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyPreviewWithContext), varargs...)
+}
+
+// GetLifecyclePolicyRequest mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyRequest(arg0 *ecr.GetLifecyclePolicyInput) (*request.Request, *ecr.GetLifecyclePolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.GetLifecyclePolicyOutput)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicyRequest indicates an expected call of GetLifecyclePolicyRequest
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyRequest), arg0)
+}
+
+// GetLifecyclePolicyWithContext mocks base method
+func (m *MockECRAPI) GetLifecyclePolicyWithContext(arg0 context.Context, arg1 *ecr.GetLifecyclePolicyInput, arg2 ...request.Option) (*ecr.GetLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetLifecyclePolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.GetLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLifecyclePolicyWithContext indicates an expected call of GetLifecyclePolicyWithContext
+func (mr *MockECRAPIMockRecorder) GetLifecyclePolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLifecyclePolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetLifecyclePolicyWithContext), varargs...)
+}
+
+// GetRepositoryPolicy mocks base method
+func (m *MockECRAPI) GetRepositoryPolicy(arg0 *ecr.GetRepositoryPolicyInput) (*ecr.GetRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRepositoryPolicy", arg0)
+	ret0, _ := ret[0].(*ecr.GetRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRepositoryPolicy indicates an expected call of GetRepositoryPolicy
+func (mr *MockECRAPIMockRecorder) GetRepositoryPolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryPolicy", reflect.TypeOf((*MockECRAPI)(nil).GetRepositoryPolicy), arg0)
+}
+
+// GetRepositoryPolicyRequest mocks base method
+func (m *MockECRAPI) GetRepositoryPolicyRequest(arg0 *ecr.GetRepositoryPolicyInput) (*request.Request, *ecr.GetRepositoryPolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRepositoryPolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.GetRepositoryPolicyOutput)
+	return ret0, ret1
+}
+
+// GetRepositoryPolicyRequest indicates an expected call of GetRepositoryPolicyRequest
+func (mr *MockECRAPIMockRecorder) GetRepositoryPolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryPolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).GetRepositoryPolicyRequest), arg0)
+}
+
+// GetRepositoryPolicyWithContext mocks base method
+func (m *MockECRAPI) GetRepositoryPolicyWithContext(arg0 context.Context, arg1 *ecr.GetRepositoryPolicyInput, arg2 ...request.Option) (*ecr.GetRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetRepositoryPolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.GetRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRepositoryPolicyWithContext indicates an expected call of GetRepositoryPolicyWithContext
+func (mr *MockECRAPIMockRecorder) GetRepositoryPolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryPolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).GetRepositoryPolicyWithContext), varargs...)
+}
+
+// InitiateLayerUpload mocks base method
+func (m *MockECRAPI) InitiateLayerUpload(arg0 *ecr.InitiateLayerUploadInput) (*ecr.InitiateLayerUploadOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InitiateLayerUpload", arg0)
+	ret0, _ := ret[0].(*ecr.InitiateLayerUploadOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// InitiateLayerUpload indicates an expected call of InitiateLayerUpload
+func (mr *MockECRAPIMockRecorder) InitiateLayerUpload(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitiateLayerUpload", reflect.TypeOf((*MockECRAPI)(nil).InitiateLayerUpload), arg0)
+}
+
+// InitiateLayerUploadRequest mocks base method
+func (m *MockECRAPI) InitiateLayerUploadRequest(arg0 *ecr.InitiateLayerUploadInput) (*request.Request, *ecr.InitiateLayerUploadOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InitiateLayerUploadRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.InitiateLayerUploadOutput)
+	return ret0, ret1
+}
+
+// InitiateLayerUploadRequest indicates an expected call of InitiateLayerUploadRequest
+func (mr *MockECRAPIMockRecorder) InitiateLayerUploadRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitiateLayerUploadRequest", reflect.TypeOf((*MockECRAPI)(nil).InitiateLayerUploadRequest), arg0)
+}
+
+// InitiateLayerUploadWithContext mocks base method
+func (m *MockECRAPI) InitiateLayerUploadWithContext(arg0 context.Context, arg1 *ecr.InitiateLayerUploadInput, arg2 ...request.Option) (*ecr.InitiateLayerUploadOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "InitiateLayerUploadWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.InitiateLayerUploadOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// InitiateLayerUploadWithContext indicates an expected call of InitiateLayerUploadWithContext
+func (mr *MockECRAPIMockRecorder) InitiateLayerUploadWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitiateLayerUploadWithContext", reflect.TypeOf((*MockECRAPI)(nil).InitiateLayerUploadWithContext), varargs...)
+}
+
+// ListImages mocks base method
+func (m *MockECRAPI) ListImages(arg0 *ecr.ListImagesInput) (*ecr.ListImagesOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListImages", arg0)
+	ret0, _ := ret[0].(*ecr.ListImagesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListImages indicates an expected call of ListImages
+func (mr *MockECRAPIMockRecorder) ListImages(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImages", reflect.TypeOf((*MockECRAPI)(nil).ListImages), arg0)
+}
+
+// ListImagesPages mocks base method
+func (m *MockECRAPI) ListImagesPages(arg0 *ecr.ListImagesInput, arg1 func(*ecr.ListImagesOutput, bool) bool) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListImagesPages", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// ListImagesPages indicates an expected call of ListImagesPages
+func (mr *MockECRAPIMockRecorder) ListImagesPages(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImagesPages", reflect.TypeOf((*MockECRAPI)(nil).ListImagesPages), arg0, arg1)
+}
+
+// ListImagesPagesWithContext mocks base method
+func (m *MockECRAPI) ListImagesPagesWithContext(arg0 context.Context, arg1 *ecr.ListImagesInput, arg2 func(*ecr.ListImagesOutput, bool) bool, arg3 ...request.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "ListImagesPagesWithContext", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// ListImagesPagesWithContext indicates an expected call of ListImagesPagesWithContext
+func (mr *MockECRAPIMockRecorder) ListImagesPagesWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImagesPagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).ListImagesPagesWithContext), varargs...)
+}
+
+// ListImagesRequest mocks base method
+func (m *MockECRAPI) ListImagesRequest(arg0 *ecr.ListImagesInput) (*request.Request, *ecr.ListImagesOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListImagesRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.ListImagesOutput)
+	return ret0, ret1
+}
+
+// ListImagesRequest indicates an expected call of ListImagesRequest
+func (mr *MockECRAPIMockRecorder) ListImagesRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImagesRequest", reflect.TypeOf((*MockECRAPI)(nil).ListImagesRequest), arg0)
+}
+
+// ListImagesWithContext mocks base method
+func (m *MockECRAPI) ListImagesWithContext(arg0 context.Context, arg1 *ecr.ListImagesInput, arg2 ...request.Option) (*ecr.ListImagesOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "ListImagesWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.ListImagesOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListImagesWithContext indicates an expected call of ListImagesWithContext
+func (mr *MockECRAPIMockRecorder) ListImagesWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImagesWithContext", reflect.TypeOf((*MockECRAPI)(nil).ListImagesWithContext), varargs...)
+}
+
+// ListTagsForResource mocks base method
+func (m *MockECRAPI) ListTagsForResource(arg0 *ecr.ListTagsForResourceInput) (*ecr.ListTagsForResourceOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListTagsForResource", arg0)
+	ret0, _ := ret[0].(*ecr.ListTagsForResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListTagsForResource indicates an expected call of ListTagsForResource
+func (mr *MockECRAPIMockRecorder) ListTagsForResource(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsForResource", reflect.TypeOf((*MockECRAPI)(nil).ListTagsForResource), arg0)
+}
+
+// ListTagsForResourceRequest mocks base method
+func (m *MockECRAPI) ListTagsForResourceRequest(arg0 *ecr.ListTagsForResourceInput) (*request.Request, *ecr.ListTagsForResourceOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListTagsForResourceRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.ListTagsForResourceOutput)
+	return ret0, ret1
+}
+
+// ListTagsForResourceRequest indicates an expected call of ListTagsForResourceRequest
+func (mr *MockECRAPIMockRecorder) ListTagsForResourceRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsForResourceRequest", reflect.TypeOf((*MockECRAPI)(nil).ListTagsForResourceRequest), arg0)
+}
+
+// ListTagsForResourceWithContext mocks base method
+func (m *MockECRAPI) ListTagsForResourceWithContext(arg0 context.Context, arg1 *ecr.ListTagsForResourceInput, arg2 ...request.Option) (*ecr.ListTagsForResourceOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "ListTagsForResourceWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.ListTagsForResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListTagsForResourceWithContext indicates an expected call of ListTagsForResourceWithContext
+func (mr *MockECRAPIMockRecorder) ListTagsForResourceWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsForResourceWithContext", reflect.TypeOf((*MockECRAPI)(nil).ListTagsForResourceWithContext), varargs...)
+}
+
+// PutImage mocks base method
+func (m *MockECRAPI) PutImage(arg0 *ecr.PutImageInput) (*ecr.PutImageOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImage", arg0)
+	ret0, _ := ret[0].(*ecr.PutImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImage indicates an expected call of PutImage
+func (mr *MockECRAPIMockRecorder) PutImage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImage", reflect.TypeOf((*MockECRAPI)(nil).PutImage), arg0)
+}
+
+// PutImageRequest mocks base method
+func (m *MockECRAPI) PutImageRequest(arg0 *ecr.PutImageInput) (*request.Request, *ecr.PutImageOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImageRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.PutImageOutput)
+	return ret0, ret1
+}
+
+// PutImageRequest indicates an expected call of PutImageRequest
+func (mr *MockECRAPIMockRecorder) PutImageRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageRequest", reflect.TypeOf((*MockECRAPI)(nil).PutImageRequest), arg0)
+}
+
+// PutImageScanningConfiguration mocks base method
+func (m *MockECRAPI) PutImageScanningConfiguration(arg0 *ecr.PutImageScanningConfigurationInput) (*ecr.PutImageScanningConfigurationOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImageScanningConfiguration", arg0)
+	ret0, _ := ret[0].(*ecr.PutImageScanningConfigurationOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImageScanningConfiguration indicates an expected call of PutImageScanningConfiguration
+func (mr *MockECRAPIMockRecorder) PutImageScanningConfiguration(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageScanningConfiguration", reflect.TypeOf((*MockECRAPI)(nil).PutImageScanningConfiguration), arg0)
+}
+
+// PutImageScanningConfigurationRequest mocks base method
+func (m *MockECRAPI) PutImageScanningConfigurationRequest(arg0 *ecr.PutImageScanningConfigurationInput) (*request.Request, *ecr.PutImageScanningConfigurationOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImageScanningConfigurationRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.PutImageScanningConfigurationOutput)
+	return ret0, ret1
+}
+
+// PutImageScanningConfigurationRequest indicates an expected call of PutImageScanningConfigurationRequest
+func (mr *MockECRAPIMockRecorder) PutImageScanningConfigurationRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageScanningConfigurationRequest", reflect.TypeOf((*MockECRAPI)(nil).PutImageScanningConfigurationRequest), arg0)
+}
+
+// PutImageScanningConfigurationWithContext mocks base method
+func (m *MockECRAPI) PutImageScanningConfigurationWithContext(arg0 context.Context, arg1 *ecr.PutImageScanningConfigurationInput, arg2 ...request.Option) (*ecr.PutImageScanningConfigurationOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "PutImageScanningConfigurationWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.PutImageScanningConfigurationOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImageScanningConfigurationWithContext indicates an expected call of PutImageScanningConfigurationWithContext
+func (mr *MockECRAPIMockRecorder) PutImageScanningConfigurationWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageScanningConfigurationWithContext", reflect.TypeOf((*MockECRAPI)(nil).PutImageScanningConfigurationWithContext), varargs...)
+}
+
+// PutImageTagMutability mocks base method
+func (m *MockECRAPI) PutImageTagMutability(arg0 *ecr.PutImageTagMutabilityInput) (*ecr.PutImageTagMutabilityOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImageTagMutability", arg0)
+	ret0, _ := ret[0].(*ecr.PutImageTagMutabilityOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImageTagMutability indicates an expected call of PutImageTagMutability
+func (mr *MockECRAPIMockRecorder) PutImageTagMutability(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageTagMutability", reflect.TypeOf((*MockECRAPI)(nil).PutImageTagMutability), arg0)
+}
+
+// PutImageTagMutabilityRequest mocks base method
+func (m *MockECRAPI) PutImageTagMutabilityRequest(arg0 *ecr.PutImageTagMutabilityInput) (*request.Request, *ecr.PutImageTagMutabilityOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutImageTagMutabilityRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.PutImageTagMutabilityOutput)
+	return ret0, ret1
+}
+
+// PutImageTagMutabilityRequest indicates an expected call of PutImageTagMutabilityRequest
+func (mr *MockECRAPIMockRecorder) PutImageTagMutabilityRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageTagMutabilityRequest", reflect.TypeOf((*MockECRAPI)(nil).PutImageTagMutabilityRequest), arg0)
+}
+
+// PutImageTagMutabilityWithContext mocks base method
+func (m *MockECRAPI) PutImageTagMutabilityWithContext(arg0 context.Context, arg1 *ecr.PutImageTagMutabilityInput, arg2 ...request.Option) (*ecr.PutImageTagMutabilityOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "PutImageTagMutabilityWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.PutImageTagMutabilityOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImageTagMutabilityWithContext indicates an expected call of PutImageTagMutabilityWithContext
+func (mr *MockECRAPIMockRecorder) PutImageTagMutabilityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageTagMutabilityWithContext", reflect.TypeOf((*MockECRAPI)(nil).PutImageTagMutabilityWithContext), varargs...)
+}
+
+// PutImageWithContext mocks base method
+func (m *MockECRAPI) PutImageWithContext(arg0 context.Context, arg1 *ecr.PutImageInput, arg2 ...request.Option) (*ecr.PutImageOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "PutImageWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.PutImageOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutImageWithContext indicates an expected call of PutImageWithContext
+func (mr *MockECRAPIMockRecorder) PutImageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutImageWithContext", reflect.TypeOf((*MockECRAPI)(nil).PutImageWithContext), varargs...)
+}
+
+// PutLifecyclePolicy mocks base method
+func (m *MockECRAPI) PutLifecyclePolicy(arg0 *ecr.PutLifecyclePolicyInput) (*ecr.PutLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutLifecyclePolicy", arg0)
+	ret0, _ := ret[0].(*ecr.PutLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutLifecyclePolicy indicates an expected call of PutLifecyclePolicy
+func (mr *MockECRAPIMockRecorder) PutLifecyclePolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutLifecyclePolicy", reflect.TypeOf((*MockECRAPI)(nil).PutLifecyclePolicy), arg0)
+}
+
+// PutLifecyclePolicyRequest mocks base method
+func (m *MockECRAPI) PutLifecyclePolicyRequest(arg0 *ecr.PutLifecyclePolicyInput) (*request.Request, *ecr.PutLifecyclePolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutLifecyclePolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.PutLifecyclePolicyOutput)
+	return ret0, ret1
+}
+
+// PutLifecyclePolicyRequest indicates an expected call of PutLifecyclePolicyRequest
+func (mr *MockECRAPIMockRecorder) PutLifecyclePolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutLifecyclePolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).PutLifecyclePolicyRequest), arg0)
+}
+
+// PutLifecyclePolicyWithContext mocks base method
+func (m *MockECRAPI) PutLifecyclePolicyWithContext(arg0 context.Context, arg1 *ecr.PutLifecyclePolicyInput, arg2 ...request.Option) (*ecr.PutLifecyclePolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "PutLifecyclePolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.PutLifecyclePolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// PutLifecyclePolicyWithContext indicates an expected call of PutLifecyclePolicyWithContext
+func (mr *MockECRAPIMockRecorder) PutLifecyclePolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutLifecyclePolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).PutLifecyclePolicyWithContext), varargs...)
+}
+
+// SetRepositoryPolicy mocks base method
+func (m *MockECRAPI) SetRepositoryPolicy(arg0 *ecr.SetRepositoryPolicyInput) (*ecr.SetRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetRepositoryPolicy", arg0)
+	ret0, _ := ret[0].(*ecr.SetRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// SetRepositoryPolicy indicates an expected call of SetRepositoryPolicy
+func (mr *MockECRAPIMockRecorder) SetRepositoryPolicy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRepositoryPolicy", reflect.TypeOf((*MockECRAPI)(nil).SetRepositoryPolicy), arg0)
+}
+
+// SetRepositoryPolicyRequest mocks base method
+func (m *MockECRAPI) SetRepositoryPolicyRequest(arg0 *ecr.SetRepositoryPolicyInput) (*request.Request, *ecr.SetRepositoryPolicyOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetRepositoryPolicyRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.SetRepositoryPolicyOutput)
+	return ret0, ret1
+}
+
+// SetRepositoryPolicyRequest indicates an expected call of SetRepositoryPolicyRequest
+func (mr *MockECRAPIMockRecorder) SetRepositoryPolicyRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRepositoryPolicyRequest", reflect.TypeOf((*MockECRAPI)(nil).SetRepositoryPolicyRequest), arg0)
+}
+
+// SetRepositoryPolicyWithContext mocks base method
+func (m *MockECRAPI) SetRepositoryPolicyWithContext(arg0 context.Context, arg1 *ecr.SetRepositoryPolicyInput, arg2 ...request.Option) (*ecr.SetRepositoryPolicyOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "SetRepositoryPolicyWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.SetRepositoryPolicyOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// SetRepositoryPolicyWithContext indicates an expected call of SetRepositoryPolicyWithContext
+func (mr *MockECRAPIMockRecorder) SetRepositoryPolicyWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRepositoryPolicyWithContext", reflect.TypeOf((*MockECRAPI)(nil).SetRepositoryPolicyWithContext), varargs...)
+}
+
+// StartImageScan mocks base method
+func (m *MockECRAPI) StartImageScan(arg0 *ecr.StartImageScanInput) (*ecr.StartImageScanOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StartImageScan", arg0)
+	ret0, _ := ret[0].(*ecr.StartImageScanOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StartImageScan indicates an expected call of StartImageScan
+func (mr *MockECRAPIMockRecorder) StartImageScan(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartImageScan", reflect.TypeOf((*MockECRAPI)(nil).StartImageScan), arg0)
+}
+
+// StartImageScanRequest mocks base method
+func (m *MockECRAPI) StartImageScanRequest(arg0 *ecr.StartImageScanInput) (*request.Request, *ecr.StartImageScanOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StartImageScanRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.StartImageScanOutput)
+	return ret0, ret1
+}
+
+// StartImageScanRequest indicates an expected call of StartImageScanRequest
+func (mr *MockECRAPIMockRecorder) StartImageScanRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartImageScanRequest", reflect.TypeOf((*MockECRAPI)(nil).StartImageScanRequest), arg0)
+}
+
+// StartImageScanWithContext mocks base method
+func (m *MockECRAPI) StartImageScanWithContext(arg0 context.Context, arg1 *ecr.StartImageScanInput, arg2 ...request.Option) (*ecr.StartImageScanOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "StartImageScanWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.StartImageScanOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StartImageScanWithContext indicates an expected call of StartImageScanWithContext
+func (mr *MockECRAPIMockRecorder) StartImageScanWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartImageScanWithContext", reflect.TypeOf((*MockECRAPI)(nil).StartImageScanWithContext), varargs...)
+}
+
+// StartLifecyclePolicyPreview mocks base method
+func (m *MockECRAPI) StartLifecyclePolicyPreview(arg0 *ecr.StartLifecyclePolicyPreviewInput) (*ecr.StartLifecyclePolicyPreviewOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StartLifecyclePolicyPreview", arg0)
+	ret0, _ := ret[0].(*ecr.StartLifecyclePolicyPreviewOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StartLifecyclePolicyPreview indicates an expected call of StartLifecyclePolicyPreview
+func (mr *MockECRAPIMockRecorder) StartLifecyclePolicyPreview(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartLifecyclePolicyPreview", reflect.TypeOf((*MockECRAPI)(nil).StartLifecyclePolicyPreview), arg0)
+}
+
+// StartLifecyclePolicyPreviewRequest mocks base method
+func (m *MockECRAPI) StartLifecyclePolicyPreviewRequest(arg0 *ecr.StartLifecyclePolicyPreviewInput) (*request.Request, *ecr.StartLifecyclePolicyPreviewOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StartLifecyclePolicyPreviewRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.StartLifecyclePolicyPreviewOutput)
+	return ret0, ret1
+}
+
+// StartLifecyclePolicyPreviewRequest indicates an expected call of StartLifecyclePolicyPreviewRequest
+func (mr *MockECRAPIMockRecorder) StartLifecyclePolicyPreviewRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartLifecyclePolicyPreviewRequest", reflect.TypeOf((*MockECRAPI)(nil).StartLifecyclePolicyPreviewRequest), arg0)
+}
+
+// StartLifecyclePolicyPreviewWithContext mocks base method
+func (m *MockECRAPI) StartLifecyclePolicyPreviewWithContext(arg0 context.Context, arg1 *ecr.StartLifecyclePolicyPreviewInput, arg2 ...request.Option) (*ecr.StartLifecyclePolicyPreviewOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "StartLifecyclePolicyPreviewWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.StartLifecyclePolicyPreviewOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StartLifecyclePolicyPreviewWithContext indicates an expected call of StartLifecyclePolicyPreviewWithContext
+func (mr *MockECRAPIMockRecorder) StartLifecyclePolicyPreviewWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartLifecyclePolicyPreviewWithContext", reflect.TypeOf((*MockECRAPI)(nil).StartLifecyclePolicyPreviewWithContext), varargs...)
+}
+
+// TagResource mocks base method
+func (m *MockECRAPI) TagResource(arg0 *ecr.TagResourceInput) (*ecr.TagResourceOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "TagResource", arg0)
+	ret0, _ := ret[0].(*ecr.TagResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// TagResource indicates an expected call of TagResource
+func (mr *MockECRAPIMockRecorder) TagResource(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResource", reflect.TypeOf((*MockECRAPI)(nil).TagResource), arg0)
+}
+
+// TagResourceRequest mocks base method
+func (m *MockECRAPI) TagResourceRequest(arg0 *ecr.TagResourceInput) (*request.Request, *ecr.TagResourceOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "TagResourceRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.TagResourceOutput)
+	return ret0, ret1
+}
+
+// TagResourceRequest indicates an expected call of TagResourceRequest
+func (mr *MockECRAPIMockRecorder) TagResourceRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResourceRequest", reflect.TypeOf((*MockECRAPI)(nil).TagResourceRequest), arg0)
+}
+
+// TagResourceWithContext mocks base method
+func (m *MockECRAPI) TagResourceWithContext(arg0 context.Context, arg1 *ecr.TagResourceInput, arg2 ...request.Option) (*ecr.TagResourceOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "TagResourceWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.TagResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// TagResourceWithContext indicates an expected call of TagResourceWithContext
+func (mr *MockECRAPIMockRecorder) TagResourceWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResourceWithContext", reflect.TypeOf((*MockECRAPI)(nil).TagResourceWithContext), varargs...)
+}
+
+// UntagResource mocks base method
+func (m *MockECRAPI) UntagResource(arg0 *ecr.UntagResourceInput) (*ecr.UntagResourceOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UntagResource", arg0)
+	ret0, _ := ret[0].(*ecr.UntagResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UntagResource indicates an expected call of UntagResource
+func (mr *MockECRAPIMockRecorder) UntagResource(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResource", reflect.TypeOf((*MockECRAPI)(nil).UntagResource), arg0)
+}
+
+// UntagResourceRequest mocks base method
+func (m *MockECRAPI) UntagResourceRequest(arg0 *ecr.UntagResourceInput) (*request.Request, *ecr.UntagResourceOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UntagResourceRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.UntagResourceOutput)
+	return ret0, ret1
+}
+
+// UntagResourceRequest indicates an expected call of UntagResourceRequest
+func (mr *MockECRAPIMockRecorder) UntagResourceRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResourceRequest", reflect.TypeOf((*MockECRAPI)(nil).UntagResourceRequest), arg0)
+}
+
+// UntagResourceWithContext mocks base method
+func (m *MockECRAPI) UntagResourceWithContext(arg0 context.Context, arg1 *ecr.UntagResourceInput, arg2 ...request.Option) (*ecr.UntagResourceOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "UntagResourceWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.UntagResourceOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UntagResourceWithContext indicates an expected call of UntagResourceWithContext
+func (mr *MockECRAPIMockRecorder) UntagResourceWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResourceWithContext", reflect.TypeOf((*MockECRAPI)(nil).UntagResourceWithContext), varargs...)
+}
+
+// UploadLayerPart mocks base method
+func (m *MockECRAPI) UploadLayerPart(arg0 *ecr.UploadLayerPartInput) (*ecr.UploadLayerPartOutput, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UploadLayerPart", arg0)
+	ret0, _ := ret[0].(*ecr.UploadLayerPartOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UploadLayerPart indicates an expected call of UploadLayerPart
+func (mr *MockECRAPIMockRecorder) UploadLayerPart(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadLayerPart", reflect.TypeOf((*MockECRAPI)(nil).UploadLayerPart), arg0)
+}
+
+// UploadLayerPartRequest mocks base method
+func (m *MockECRAPI) UploadLayerPartRequest(arg0 *ecr.UploadLayerPartInput) (*request.Request, *ecr.UploadLayerPartOutput) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UploadLayerPartRequest", arg0)
+	ret0, _ := ret[0].(*request.Request)
+	ret1, _ := ret[1].(*ecr.UploadLayerPartOutput)
+	return ret0, ret1
+}
+
+// UploadLayerPartRequest indicates an expected call of UploadLayerPartRequest
+func (mr *MockECRAPIMockRecorder) UploadLayerPartRequest(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadLayerPartRequest", reflect.TypeOf((*MockECRAPI)(nil).UploadLayerPartRequest), arg0)
+}
+
+// UploadLayerPartWithContext mocks base method
+func (m *MockECRAPI) UploadLayerPartWithContext(arg0 context.Context, arg1 *ecr.UploadLayerPartInput, arg2 ...request.Option) (*ecr.UploadLayerPartOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "UploadLayerPartWithContext", varargs...)
+	ret0, _ := ret[0].(*ecr.UploadLayerPartOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UploadLayerPartWithContext indicates an expected call of UploadLayerPartWithContext
+func (mr *MockECRAPIMockRecorder) UploadLayerPartWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadLayerPartWithContext", reflect.TypeOf((*MockECRAPI)(nil).UploadLayerPartWithContext), varargs...)
+}
diff --git a/internal/pkg/build/docker/docker.go b/internal/pkg/build/docker/docker.go
index c778f5b2..11214f4c 100644
--- a/internal/pkg/build/docker/docker.go
+++ b/internal/pkg/build/docker/docker.go
@@ -5,75 +5,61 @@ package docker
 
 import (
 	"fmt"
-	"os"
-	"os/exec"
 	"strings"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/ecr"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 )
 
-type command struct {
-	cmd *exec.Cmd
-}
-
-func (c command) run() error {
-	return c.cmd.Run()
-}
-
-func (c command) standardInput(input string) {
-	c.cmd.Stdin = strings.NewReader(input)
-}
-
-type runnable interface {
-	run() error
-	standardInput(string)
+// Service wraps a runner.
+type Service struct {
+	runner runner
 }
 
-// Service exists for mockability.
-type Service struct {
-	createCommand func(name string, args ...string) runnable
+type runner interface {
+	Run(name string, args []string, options ...command.Option) error
 }
 
 // New returns a Service.
 func New() Service {
 	return Service{
-		createCommand: newCommand,
+		runner: command.New(),
 	}
 }
 
-// Build will `os/exec` a `docker build` command with the input uri, tag, and Dockerfile image path.
+// Build will run a `docker build` command with the input uri, tag, and Dockerfile image path.
 func (s Service) Build(uri, imageTag, path string) error {
 	imageName := imageName(uri, imageTag)
 
-	cmd := s.createCommand("docker", "build", "-t", imageName, path)
+	err := s.runner.Run("docker", []string{"build", "-t", imageName, path})
 
-	if err := cmd.run(); err != nil {
+	if err != nil {
 		return fmt.Errorf("building image: %w", err)
 	}
 
 	return nil
 }
 
-// Login will `os/exec` a `docker login` command against the Service repository URI with the input uri and auth data.
-func (s Service) Login(uri string, auth ecr.Auth) error {
-	cmd := s.createCommand("docker", "login", "-u", auth.Username, "--password-stdin", uri)
-	cmd.standardInput(auth.Password)
+// Login will run a `docker login` command against the Service repository URI with the input uri and auth data.
+func (s Service) Login(uri, username, password string) error {
+	err := s.runner.Run("docker",
+		[]string{"login", "-u", username, "--password-stdin", uri},
+		command.Stdin(strings.NewReader(password)))
 
-	if err := cmd.run(); err != nil {
+	if err != nil {
 		return fmt.Errorf("authenticate to ECR: %w", err)
 	}
 
 	return nil
 }
 
-// Push will `os/exec` a `docker push` command against the Service repository URI with the input uri and image tag.
+// Push will run `docker push` command against the Service repository URI with the input uri and image tag.
 func (s Service) Push(uri, imageTag string) error {
 	path := imageName(uri, imageTag)
 
-	cmd := s.createCommand("docker", "push", path)
+	err := s.runner.Run("docker", []string{"push", path})
 
-	if err := cmd.run(); err != nil {
+	if err != nil {
 		// TODO: improve the error handling here.
 		// if you try to push an *existing* image that has Digest A and tag T then no error (also no image push).
 		// if you try to push an *existing* image that has Digest B and tag T (that belongs to another image Digest A) then docker spits out an unclear error.
@@ -88,14 +74,3 @@ func (s Service) Push(uri, imageTag string) error {
 func imageName(uri, tag string) string {
 	return fmt.Sprintf("%s:%s", uri, tag)
 }
-
-func newCommand(name string, args ...string) runnable {
-	cmd := exec.Command(name, args...)
-	// NOTE: Stdout and Stderr must both be set otherwise command output pipes to os.DevNull
-	cmd.Stdout = os.Stderr
-	cmd.Stderr = os.Stderr
-
-	return command{
-		cmd: cmd,
-	}
-}
diff --git a/internal/pkg/build/docker/mocks/mock_docker.go b/internal/pkg/build/docker/mocks/mock_docker.go
new file mode 100644
index 00000000..3e50c5bb
--- /dev/null
+++ b/internal/pkg/build/docker/mocks/mock_docker.go
@@ -0,0 +1,53 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./internal/pkg/build/docker/docker.go
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	command "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+)
+
+// Mockrunner is a mock of runner interface
+type Mockrunner struct {
+	ctrl     *gomock.Controller
+	recorder *MockrunnerMockRecorder
+}
+
+// MockrunnerMockRecorder is the mock recorder for Mockrunner
+type MockrunnerMockRecorder struct {
+	mock *Mockrunner
+}
+
+// NewMockrunner creates a new mock instance
+func NewMockrunner(ctrl *gomock.Controller) *Mockrunner {
+	mock := &Mockrunner{ctrl: ctrl}
+	mock.recorder = &MockrunnerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *Mockrunner) EXPECT() *MockrunnerMockRecorder {
+	return m.recorder
+}
+
+// Run mocks base method
+func (m *Mockrunner) Run(name string, args []string, options ...command.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{name, args}
+	for _, a := range options {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Run", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Run indicates an expected call of Run
+func (mr *MockrunnerMockRecorder) Run(name, args interface{}, options ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{name, args}, options...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*Mockrunner)(nil).Run), varargs...)
+}
diff --git a/internal/pkg/cli/app_delete.go b/internal/pkg/cli/app_delete.go
index e4646a41..5d28d30d 100644
--- a/internal/pkg/cli/app_delete.go
+++ b/internal/pkg/cli/app_delete.go
@@ -66,6 +66,13 @@ func BuildAppDeleteCmd() *cobra.Command {
 		RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
 			return opts.deleteApp()
 		}),
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			log.Infoln("Recommended follow-up actions:")
+			for _, followup := range opts.RecommendedActions() {
+				log.Infof("- %s\n", followup)
+			}
+			return nil
+		},
 	}
 
 	cmd.Flags().BoolVar(&opts.skipConfirmation, yesFlag, false, yesFlagDescription)
@@ -305,3 +312,12 @@ func (opts deleteAppOpts) deleteWorkspaceFile() error {
 
 	return nil
 }
+
+// RecommendedActions returns follow-up actions the user can take after successfully executing the command.
+func (opts *deleteAppOpts) RecommendedActions() []string {
+	// TODO: Add recommendation to do `pipeline delete` when it is available
+	return []string{
+		fmt.Sprintf("Run %s to update the corresponding pipeline if it exists.",
+			color.HighlightCode(fmt.Sprintf("ecs-preview pipeline update"))),
+	}
+}
diff --git a/internal/pkg/cli/app_deploy.go b/internal/pkg/cli/app_deploy.go
index b5109115..b968c441 100644
--- a/internal/pkg/cli/app_deploy.go
+++ b/internal/pkg/cli/app_deploy.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"os/exec"
 	"strings"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
@@ -21,6 +20,7 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 	termprogress "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/progress"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
@@ -28,12 +28,21 @@ import (
 	"github.com/spf13/cobra"
 )
 
+const (
+	inputImageTagPrompt = "Input an image tag value:"
+)
+
+var (
+	errNoLocalManifestsFound = errors.New("no manifest files found")
+)
+
 // BuildAppDeployCommand builds the `app deploy` subcommand.
 func BuildAppDeployCommand() *cobra.Command {
 	input := &appDeployOpts{
 		GlobalOpts:    NewGlobalOpts(),
 		spinner:       termprogress.NewSpinner(),
 		dockerService: docker.New(),
+		runner:        command.New(),
 	}
 
 	cmd := &cobra.Command{
@@ -80,6 +89,7 @@ type appDeployOpts struct {
 	workspaceService   archer.Workspace
 	ecrService         ecrService
 	dockerService      dockerService
+	runner             runner
 	appPackageCfClient projectResourcesGetter
 	appDeployCfClient  cloudformation.CloudFormation
 
@@ -108,10 +118,14 @@ type ecrService interface {
 
 type dockerService interface {
 	Build(uri, tag, path string) error
-	Login(uri string, auth ecr.Auth) error
+	Login(uri, username, password string) error
 	Push(uri, tag string) error
 }
 
+type runner interface {
+	Run(name string, args []string, options ...command.Option) error
+}
+
 func (opts *appDeployOpts) init() error {
 	projectService, err := store.New()
 	if err != nil {
@@ -307,16 +321,24 @@ func (opts *appDeployOpts) sourceImageTag() error {
 		return nil
 	}
 
-	cmd := exec.Command("git", "describe", "--always")
+	var buf bytes.Buffer
+	err := opts.runner.Run("git", []string{"describe", "--always"}, command.Stdout(&buf))
+
+	if err == nil {
+		// NOTE: `git describe` output bytes includes a `\n` character, so we trim it out.
+		opts.imageTag = strings.TrimSpace(buf.String())
+
+		return nil
+	}
 
-	bytes, err := cmd.Output()
+	log.Warningln("Failed to default tag, are you in a git repository?")
 
+	tag, err := opts.prompt.Get(inputImageTagPrompt, "", nil /*no validation*/)
 	if err != nil {
-		return fmt.Errorf("defaulting tag: %w", err)
+		return fmt.Errorf("prompt for image tag: %w", err)
 	}
 
-	// NOTE: `git describe` output bytes includes a `\n` character, so we trim it out.
-	opts.imageTag = strings.TrimSpace(string(bytes))
+	opts.imageTag = tag
 
 	return nil
 }
@@ -344,7 +366,7 @@ func (opts appDeployOpts) deployApp() error {
 		return fmt.Errorf("get ECR auth data: %w", err)
 	}
 
-	opts.dockerService.Login(uri, auth)
+	opts.dockerService.Login(uri, auth.Username, auth.Password)
 
 	if err != nil {
 		return err
@@ -426,10 +448,10 @@ func (opts appDeployOpts) applyAppDeployTemplate(template, stackName, changeSetN
 func (opts appDeployOpts) getAppDockerfilePath() (string, error) {
 	manifestFileNames, err := opts.workspaceService.ListManifestFiles()
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("list local manifest files: %w", err)
 	}
 	if len(manifestFileNames) == 0 {
-		return "", errors.New("no manifest files found")
+		return "", errNoLocalManifestsFound
 	}
 
 	var targetManifestFile string
@@ -440,18 +462,18 @@ func (opts appDeployOpts) getAppDockerfilePath() (string, error) {
 		}
 	}
 	if targetManifestFile == "" {
-		return "", errors.New("couldn't match manifest file name")
+		return "", fmt.Errorf("couldn't find local manifest %s", opts.app)
 	}
 
 	manifestBytes, err := opts.workspaceService.ReadFile(targetManifestFile)
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("read manifest file %s: %w", targetManifestFile, err)
 	}
 
 	mf, err := manifest.UnmarshalApp(manifestBytes)
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("unmarshal app manifest: %w", err)
 	}
 
-	return mf.DockerfilePath(), nil
+	return strings.TrimSuffix(mf.DockerfilePath(), "/Dockerfile"), nil
 }
diff --git a/internal/pkg/cli/app_init.go b/internal/pkg/cli/app_init.go
index 39137bbe..2dc5bf7b 100644
--- a/internal/pkg/cli/app_init.go
+++ b/internal/pkg/cli/app_init.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"sort"
 	"strings"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
@@ -84,7 +83,7 @@ func (opts *InitAppOpts) Validate() error {
 		}
 	}
 	if opts.DockerfilePath != "" {
-		if _, err := opts.fs.Stat(opts.DockerfilePath); err != nil {
+		if _, err := listDockerfiles(opts.fs, opts.DockerfilePath); err != nil {
 			return err
 		}
 	}
@@ -192,7 +191,7 @@ Deployed resources (such as your service, logs) will contain this app's name and
 // If the user chooses to enter a custom path, then we prompt them for the path.
 func (opts *InitAppOpts) askDockerfile() error {
 	// TODO https://github.com/aws/amazon-ecs-cli-v2/issues/206
-	dockerfiles, err := opts.listDockerfileSelections()
+	dockerfiles, err := listDockerfiles(opts.fs, ".")
 	if err != nil {
 		return err
 	}
@@ -212,58 +211,6 @@ func (opts *InitAppOpts) askDockerfile() error {
 	return nil
 }
 
-// listDockerfileDirs returns the list of directories containing Dockerfiles within
-// the current working directory and a sub-directory level below.
-// If an error occurs while reading directories, returns the error.
-func (opts *InitAppOpts) listDockerfileDirs() ([]string, error) {
-	wdFiles, err := afero.ReadDir(opts.fs, ".")
-	if err != nil {
-		return nil, fmt.Errorf("read directory: %w", err)
-	}
-	var directories []string
-
-	for _, wdFile := range wdFiles {
-		// Add current directory if a Dockerfile exists, otherwise continue.
-		if !wdFile.IsDir() {
-			if wdFile.Name() == "Dockerfile" {
-				directories = append(directories, filepath.Dir(wdFile.Name()))
-			}
-			continue
-		}
-
-		// Add sub-directories containing a Dockerfile one level below current directory.
-		subFiles, err := afero.ReadDir(opts.fs, wdFile.Name())
-		if err != nil {
-			return nil, fmt.Errorf("read directory: %w", err)
-		}
-		for _, f := range subFiles {
-			// NOTE: ignore directories in sub-directories.
-			if f.IsDir() {
-				continue
-			}
-
-			if f.Name() == "Dockerfile" {
-				directories = append(directories, wdFile.Name())
-			}
-		}
-	}
-	sort.Strings(directories)
-	return directories, nil
-}
-
-func (opts *InitAppOpts) listDockerfileSelections() ([]string, error) {
-	dockerfileDirs, err := opts.listDockerfileDirs()
-	if err != nil {
-		return nil, err
-	}
-	var dockerfiles []string
-	for _, dir := range dockerfileDirs {
-		file := dir + "/Dockerfile"
-		dockerfiles = append(dockerfiles, file)
-	}
-	return dockerfiles, nil
-}
-
 func (opts *InitAppOpts) ensureNoExistingApp(projectName, appName string) error {
 	_, err := opts.appStore.GetApplication(projectName, opts.AppName)
 	// If the app doesn't exist - that's perfect, return no error.
@@ -291,7 +238,9 @@ func (opts *InitAppOpts) RecommendedActions() []string {
 
 // BuildAppInitCmd build the command for creating a new application.
 func BuildAppInitCmd() *cobra.Command {
-	opts := &InitAppOpts{}
+	opts := &InitAppOpts{
+		GlobalOpts: NewGlobalOpts(),
+	}
 	cmd := &cobra.Command{
 		Use:   "init",
 		Short: "Creates a new application in a project.",
@@ -323,7 +272,6 @@ This command is also run as part of "ecs-preview init".`,
 			opts.projDeployer = cloudformation.New(sess)
 
 			opts.prog = termprogress.NewSpinner()
-			opts.GlobalOpts = NewGlobalOpts()
 			return opts.Validate()
 		}),
 		RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
diff --git a/internal/pkg/cli/app_package.go b/internal/pkg/cli/app_package.go
index 01df9c90..52302668 100644
--- a/internal/pkg/cli/app_package.go
+++ b/internal/pkg/cli/app_package.go
@@ -4,12 +4,12 @@
 package cli
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 
@@ -21,6 +21,7 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
 	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
@@ -46,6 +47,7 @@ type PackageAppOpts struct {
 	stackWriter  io.Writer
 	paramsWriter io.Writer
 	fs           afero.Fs
+	runner       runner
 
 	*GlobalOpts // Embed global options.
 }
@@ -54,22 +56,22 @@ type PackageAppOpts struct {
 // The CloudFormation template is written to stdout and the parameters are discarded by default.
 // If an error occurred while running git, we leave the image tag empty "".
 func NewPackageAppOpts() *PackageAppOpts {
-	commitID, err := exec.Command("git", "rev-parse", "--short", "HEAD").CombinedOutput()
-	if err != nil {
-		return &PackageAppOpts{
-			stackWriter:  os.Stdout,
-			paramsWriter: ioutil.Discard,
-			fs:           &afero.Afero{Fs: afero.NewOsFs()},
-			GlobalOpts:   NewGlobalOpts(),
-		}
-	}
-	return &PackageAppOpts{
-		Tag:          fmt.Sprintf("manual-%s", strings.TrimSpace(string(commitID))),
+	opts := &PackageAppOpts{
+		runner:       command.New(),
 		stackWriter:  os.Stdout,
 		paramsWriter: ioutil.Discard,
 		fs:           &afero.Afero{Fs: afero.NewOsFs()},
 		GlobalOpts:   NewGlobalOpts(),
 	}
+
+	var buf bytes.Buffer
+	if err := opts.runner.Run("git", []string{"rev-parse", "--short", "HEAD"}, command.Stdout(&buf)); err != nil {
+		return opts
+	}
+
+	opts.Tag = fmt.Sprintf("manual-%s", strings.TrimSpace(buf.String()))
+
+	return opts
 }
 
 // Ask prompts the user for any missing required fields.
diff --git a/internal/pkg/cli/cli.go b/internal/pkg/cli/cli.go
index 1b77a677..b06ee258 100644
--- a/internal/pkg/cli/cli.go
+++ b/internal/pkg/cli/cli.go
@@ -15,11 +15,6 @@ import (
 	"github.com/spf13/viper"
 )
 
-func init() {
-	// Store the local project's name in viper.
-	bindProjectName()
-}
-
 // GlobalOpts holds fields that are used across multiple commands.
 type GlobalOpts struct {
 	projectName string
@@ -28,6 +23,8 @@ type GlobalOpts struct {
 
 // NewGlobalOpts returns a GlobalOpts with the project name retrieved from viper.
 func NewGlobalOpts() *GlobalOpts {
+	bindProjectName()
+
 	return &GlobalOpts{
 		projectName: viper.GetString(projectFlag),
 		prompt:      prompt.New(),
diff --git a/internal/pkg/cli/dockerfile.go b/internal/pkg/cli/dockerfile.go
new file mode 100644
index 00000000..519d8eda
--- /dev/null
+++ b/internal/pkg/cli/dockerfile.go
@@ -0,0 +1,63 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"fmt"
+	"path/filepath"
+	"sort"
+
+	"github.com/spf13/afero"
+)
+
+const (
+	dockerfileName = "Dockerfile"
+)
+
+// listDockerfiles returns the list of Dockerfiles within the current
+// working directory and a sub-directory level below. If an error occurs while
+// reading directories, or no Dockerfiles found returns the error.
+func listDockerfiles(fs afero.Fs, dir string) ([]string, error) {
+	wdFiles, err := afero.ReadDir(fs, dir)
+	if err != nil {
+		return nil, fmt.Errorf("read directory: %w", err)
+	}
+	var directories []string
+
+	for _, wdFile := range wdFiles {
+		// Add current directory if a Dockerfile exists, otherwise continue.
+		if !wdFile.IsDir() {
+			if wdFile.Name() == dockerfileName {
+				directories = append(directories, filepath.Dir(wdFile.Name()))
+			}
+			continue
+		}
+
+		// Add sub-directories containing a Dockerfile one level below current directory.
+		subFiles, err := afero.ReadDir(fs, wdFile.Name())
+		if err != nil {
+			return nil, fmt.Errorf("read directory: %w", err)
+		}
+		for _, f := range subFiles {
+			// NOTE: ignore directories in sub-directories.
+			if f.IsDir() {
+				continue
+			}
+
+			if f.Name() == dockerfileName {
+				directories = append(directories, wdFile.Name())
+			}
+		}
+	}
+	if len(directories) == 0 {
+		return nil, fmt.Errorf("no Dockerfiles found within %s or a sub-directory level below", dir)
+	}
+	sort.Strings(directories)
+	dockerfiles := make([]string, 0, len(directories))
+	for _, dir := range directories {
+		file := dir + "/" + dockerfileName
+		dockerfiles = append(dockerfiles, file)
+	}
+	return dockerfiles, nil
+}
diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go
index 028628cd..6194c781 100644
--- a/internal/pkg/cli/flag.go
+++ b/internal/pkg/cli/flag.go
@@ -23,7 +23,7 @@ const (
 	deployFlag            = "deploy"
 	githubRepoFlag        = "github-repo"
 	githubAccessTokenFlag = "github-access-token"
-	enableCDFlag          = "enable-cd"
+	githubBranchFlag      = "github-branch"
 	envsFlag              = "environments"
 	domainNameFlag        = "domain"
 	pipelineFileFlag      = "file"
@@ -42,6 +42,7 @@ const (
 	dockerFileFlagShort        = "d"
 	githubRepoFlagShort        = "r"
 	githubAccessTokenFlagShort = "t"
+	githubBranchFlagShort      = "b"
 	envsFlagShort              = "e"
 	pipelineFileFlagShort      = "f"
 )
@@ -56,18 +57,17 @@ const (
 	yesFlagDescription     = "Skips confirmation prompt."
 	jsonFlagDescription    = "Output in JSON format."
 
-	dockerFileFlagDescription        = "Path to the Dockerfile."
+	dockerFileFlagDescription        = "Path to the directory with Dockerfile."
 	imageTagFlagDescription          = `Optional. The application's image tag.`
 	stackOutputDirFlagDescription    = "Optional. Writes the stack template and template configuration to a directory."
 	prodEnvFlagDescription           = "If the environment contains production services."
 	deployTestFlagDescription        = `Deploy your application to a "test" environment.`
 	githubRepoFlagDescription        = "GitHub repository for your application."
 	githubAccessTokenFlagDescription = "GitHub personal access token for your repository."
+	githubBranchFlagDescription      = "Branch name of your Github repository"
 	deployPipelineFlagDescription    = "Deploys the pipeline."
-	enableCDFlagDescription          = "Enables automatic deployment to production environment."
 	pipelineEnvsFlagDescription      = "Environments to add to the pipeline."
 	domainNameFlagDescription        = "Optional. Your existing custom domain name."
 	pipelineFileFlagDescription      = "Name of YAML file used to update the pipeline."
-	deployFlagDescription            = "Trigger a deployment of your application(s) to any new stage in your pipeline."
 	appLocalFlagDescription          = "Only show applications in the current directory."
 )
diff --git a/internal/pkg/cli/init.go b/internal/pkg/cli/init.go
index 27207b1d..b1e3c494 100644
--- a/internal/pkg/cli/init.go
+++ b/internal/pkg/cli/init.go
@@ -15,6 +15,7 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 	termprogress "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/progress"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/prompt"
@@ -108,6 +109,7 @@ func NewInitOpts() (*InitOpts, error) {
 
 		spinner:       spin,
 		dockerService: docker.New(),
+		runner:        command.New(),
 
 		GlobalOpts: NewGlobalOpts(),
 	}
@@ -229,6 +231,9 @@ func BuildInitCmd() *cobra.Command {
 		Use:   "init",
 		Short: "Create a new ECS application.",
 		PreRunE: runCmdE(func(cmd *cobra.Command, args []string) error {
+			if *opts.dockerfilePath == "" {
+				_, err = listDockerfiles(&afero.Afero{Fs: afero.NewOsFs()}, ".")
+			}
 			return err
 		}),
 		RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
diff --git a/internal/pkg/cli/mocks/mock_projectservice.go b/internal/pkg/cli/mocks/mock_projectservice.go
index 350848d2..cd309eb8 100644
--- a/internal/pkg/cli/mocks/mock_projectservice.go
+++ b/internal/pkg/cli/mocks/mock_projectservice.go
@@ -7,6 +7,7 @@ package mocks
 import (
 	archer "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	ecr "github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/ecr"
+	command "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/command"
 	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
 )
@@ -285,17 +286,17 @@ func (mr *MockdockerServiceMockRecorder) Build(uri, tag, path interface{}) *gomo
 }
 
 // Login mocks base method
-func (m *MockdockerService) Login(uri string, auth ecr.Auth) error {
+func (m *MockdockerService) Login(uri, username, password string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Login", uri, auth)
+	ret := m.ctrl.Call(m, "Login", uri, username, password)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // Login indicates an expected call of Login
-func (mr *MockdockerServiceMockRecorder) Login(uri, auth interface{}) *gomock.Call {
+func (mr *MockdockerServiceMockRecorder) Login(uri, username, password interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockdockerService)(nil).Login), uri, auth)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockdockerService)(nil).Login), uri, username, password)
 }
 
 // Push mocks base method
@@ -311,3 +312,45 @@ func (mr *MockdockerServiceMockRecorder) Push(uri, tag interface{}) *gomock.Call
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockdockerService)(nil).Push), uri, tag)
 }
+
+// Mockrunner is a mock of runner interface
+type Mockrunner struct {
+	ctrl     *gomock.Controller
+	recorder *MockrunnerMockRecorder
+}
+
+// MockrunnerMockRecorder is the mock recorder for Mockrunner
+type MockrunnerMockRecorder struct {
+	mock *Mockrunner
+}
+
+// NewMockrunner creates a new mock instance
+func NewMockrunner(ctrl *gomock.Controller) *Mockrunner {
+	mock := &Mockrunner{ctrl: ctrl}
+	mock.recorder = &MockrunnerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *Mockrunner) EXPECT() *MockrunnerMockRecorder {
+	return m.recorder
+}
+
+// Run mocks base method
+func (m *Mockrunner) Run(name string, args []string, options ...command.Option) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{name, args}
+	for _, a := range options {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Run", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Run indicates an expected call of Run
+func (mr *MockrunnerMockRecorder) Run(name, args interface{}, options ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{name, args}, options...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*Mockrunner)(nil).Run), varargs...)
+}
diff --git a/internal/pkg/cli/pipeline_init.go b/internal/pkg/cli/pipeline_init.go
index 3559fa7e..c4cc976d 100644
--- a/internal/pkg/cli/pipeline_init.go
+++ b/internal/pkg/cli/pipeline_init.go
@@ -16,24 +16,28 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store/secretsmanager"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/prompt"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
 	"github.com/aws/amazon-ecs-cli-v2/templates"
 	"github.com/gobuffalo/packd"
 
+	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
 )
 
 const (
-	pipelineAddEnvPrompt          = "Would you like to add an environment to your pipeline?"
-	pipelineAddMoreEnvPrompt      = "Would you like to add another environment to your pipeline?"
-	pipelineAddEnvHelpPrompt      = "Adds an environment that corresponds to a deployment stage in your pipeline. Environments are added sequentially."
-	pipelineAddMoreEnvHelpPrompt  = "Adds another environment that corresponds to a deployment stage in your pipeline. Environments are added sequentially."
-	pipelineSelectEnvPrompt       = "Which environment would you like to add to your pipeline?"
-	pipelineEnterGitHubRepoPrompt = "What is your application's GitHub repository?" // TODO allow just <user>/<repo>?
+	pipelineAddEnvPrompt            = "Would you like to add an environment to your pipeline?"
+	pipelineAddMoreEnvPrompt        = "Would you like to add another environment to your pipeline?"
+	pipelineAddEnvHelpPrompt        = "Adds an environment that corresponds to a deployment stage in your pipeline. Environments are added sequentially."
+	pipelineAddMoreEnvHelpPrompt    = "Adds another environment that corresponds to a deployment stage in your pipeline. Environments are added sequentially."
+	pipelineSelectEnvPrompt         = "Which environment would you like to add to your pipeline?"
+	pipelineEnterGitHubRepoPrompt   = "What is your application's GitHub repository?" // TODO allow just <user>/<repo>?
+	pipelineEnterGitHubBranchPrompt = "What is the branch name of your GitHub repository?"
 )
 
 const (
-	buildspecTemplatePath = "cicd/buildspec.yml"
+	buildspecTemplatePath          = "cicd/buildspec.yml"
+	integTestBuildspecTemplatePath = "cicd/" + manifest.IntegTestBuildspecFileName
 )
 
 var errNoEnvsInProject = errors.New("there were no more environments found that can be added to your pipeline. Please run `ecs-preview env init` to create a new environment")
@@ -44,24 +48,24 @@ type InitPipelineOpts struct {
 	Environments      []string
 	GitHubRepo        string
 	GitHubAccessToken string
-	EnableCD          bool
-	Deploy            bool
+	GitHubBranch      string
 	PipelineFilename  string
-	// TODO add git branch
 	// TODO add pipeline file (to write to different file than pipeline.yml?)
 
 	// Interfaces to interact with dependencies.
-	workspace      archer.ManifestIO
+	workspace      archer.Workspace
 	secretsmanager archer.SecretsManager
 	box            packd.Box
 
 	// Outputs stored on successful actions.
-	manifestPath  string
-	buildspecPath string
-	secretName    string
+	manifestPath            string
+	buildspecPath           string
+	integTestBuildspecPaths []string
+	secretName              string
 
 	// Caches environments
 	projectEnvs []string
+	fsUtils     *afero.Afero
 
 	*GlobalOpts
 }
@@ -69,6 +73,7 @@ type InitPipelineOpts struct {
 // NewInitPipelineOpts returns a new InitPipelineOpts struct.
 func NewInitPipelineOpts() *InitPipelineOpts {
 	return &InitPipelineOpts{
+		fsUtils:    &afero.Afero{Fs: afero.NewOsFs()},
 		GlobalOpts: NewGlobalOpts(),
 	}
 }
@@ -93,14 +98,11 @@ func (opts *InitPipelineOpts) Ask() error {
 		}
 	}
 
-	// if err := opts.askEnableCD(); err != nil {
-	// 	return err
-	// }
-
-	// TODO ask this after pipeline.yml is written
-	// if err := opts.askDeploy(); err != nil {
-	// 	return err
-	// }
+	if opts.GitHubBranch == "" {
+		if err := opts.getGitHubBranch(); err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
@@ -133,12 +135,16 @@ func (opts *InitPipelineOpts) Execute() error {
 	}
 	opts.secretName = secretName
 
+	apps, err := opts.workspace.Apps()
+	if err != nil {
+		return fmt.Errorf("could not retrieve apps in this workspace: %w", err)
+	}
+
 	// write pipeline.yml file, populate with:
 	//   - github repo as source
 	//   - stage names (environments)
 	//   - enable/disable transition to prod envs
-
-	manifestPath, err := opts.createPipelineManifest()
+	manifestPath, err := opts.createPipelineManifest(apps)
 	if err != nil {
 		return err
 	}
@@ -149,9 +155,15 @@ func (opts *InitPipelineOpts) Execute() error {
 		return err
 	}
 	opts.buildspecPath = buildspecPath
+	integTestBuildspecPaths, err := opts.createIntegTestBuildspecPerApp(apps)
+	if err != nil {
+		return err
+	}
+	opts.integTestBuildspecPaths = integTestBuildspecPaths
 
 	log.Successf("Wrote the pipeline manifest for %s at '%s'\n", color.HighlightUserInput(opts.GitHubRepo), color.HighlightResource(relPath(opts.manifestPath)))
 	log.Successf("Wrote the buildspec for the pipeline's build stage at '%s'\n", color.HighlightResource(relPath(opts.buildspecPath)))
+	log.Successf("Wrote the buildspecs for each of the app in the pipeline: '%s'\n", color.HighlightResource(fmt.Sprintf("%v", opts.integTestBuildspecPaths)))
 	log.Infoln("The manifest contains configurations for your CodePipeline resources, such as your pipeline stages and build steps.")
 	log.Infoln("The buildspec contains the commands to build and push your container images to your ECR repositories.")
 
@@ -198,14 +210,14 @@ func (opts *InitPipelineOpts) getRepoName() string {
 func (opts *InitPipelineOpts) createPipelineProvider() (manifest.Provider, error) {
 	config := &manifest.GitHubProperties{
 		OwnerAndRepository:    opts.GitHubRepo,
-		Branch:                "master", // todo - fix
+		Branch:                opts.GitHubBranch,
 		GithubSecretIdKeyName: opts.secretName,
 	}
 
 	return manifest.NewProvider(config)
 }
 
-func (opts *InitPipelineOpts) createPipelineManifest() (string, error) {
+func (opts *InitPipelineOpts) createPipelineManifest(apps []archer.Manifest) (string, error) {
 	// TODO change this to flag
 	pipelineName := opts.createPipelineName()
 	provider, err := opts.createPipelineProvider()
@@ -213,7 +225,8 @@ func (opts *InitPipelineOpts) createPipelineManifest() (string, error) {
 		return "", fmt.Errorf("could not create pipeline: %w", err)
 	}
 
-	manifest, err := manifest.CreatePipeline(pipelineName, provider, opts.Environments)
+	manifest, err := manifest.CreatePipeline(
+		pipelineName, provider, opts.Environments, apps)
 	if err != nil {
 		return "", fmt.Errorf("generate a manifest: %w", err)
 	}
@@ -242,6 +255,25 @@ func (opts *InitPipelineOpts) createBuildspec() (string, error) {
 	return path, nil
 }
 
+// write the sample integration test buildspec to each app's folder
+func (opts *InitPipelineOpts) createIntegTestBuildspecPerApp(apps []archer.Manifest) ([]string, error) {
+	content, err := opts.box.FindString(integTestBuildspecTemplatePath)
+	if err != nil {
+		return nil, fmt.Errorf("find integration test template for %s: %w", integTestBuildspecTemplatePath, err)
+	}
+
+	paths := make([]string, 0, len(apps))
+	for _, app := range apps {
+		err := opts.fsUtils.WriteFile(app.IntegTestBuildspecPath(), []byte(content), 0644)
+		if err != nil {
+			return nil, fmt.Errorf("write integration test buildspec %s to app %s: %w",
+				app.IntegTestBuildspecPath(), app.AppName(), err)
+		}
+		paths = append(paths, app.IntegTestBuildspecPath())
+	}
+	return paths, nil
+}
+
 func (opts *InitPipelineOpts) selectEnvironments() error {
 	for {
 		promptMsg := pipelineAddEnvPrompt
@@ -258,7 +290,7 @@ func (opts *InitPipelineOpts) selectEnvironments() error {
 			break
 		}
 		if err := opts.selectEnvironment(); err != nil {
-			return err
+			return fmt.Errorf("failed to add environment: %w", err)
 		}
 		if len(opts.listAvailableEnvironments()) == 0 {
 			break
@@ -305,7 +337,7 @@ func (opts *InitPipelineOpts) selectEnvironment() error {
 	)
 
 	if err != nil {
-		return fmt.Errorf("failed to add environment: %w", err)
+		return err
 	}
 
 	opts.Environments = append(opts.Environments, env)
@@ -365,32 +397,17 @@ func (opts *InitPipelineOpts) getGitHubAccessToken() error {
 	return nil
 }
 
-func (opts *InitPipelineOpts) askEnableCD() error {
-	enable, err := opts.prompt.Confirm(
-		"Would you like to automatically enable deploying to production?",
-		"Enables the transition to your production environment automatically through your pipeline.",
-	)
-
-	if err != nil {
-		return fmt.Errorf("failed to confirm enabling CD: %w", err)
-	}
-
-	opts.EnableCD = enable
-
-	return nil
-}
-
-func (opts *InitPipelineOpts) askDeploy() error {
-	deploy, err := opts.prompt.Confirm(
-		"Would you like to deploy your pipeline?",
-		"Deploys your pipeline through CloudFormation.",
-	)
+// TODO: nice to have a remote GitHub repo's branch list to offer a select menu.
+func (opts *InitPipelineOpts) getGitHubBranch() error {
+	branch, err := opts.prompt.Get(pipelineEnterGitHubBranchPrompt,
+		"Name of the branch that you wish to use in your GitHub repository. By default the branch name is \"master\".",
+		nil, prompt.WithDefaultInput("master"))
 
 	if err != nil {
-		return fmt.Errorf("failed to confirm deploying pipeline: %w", err)
+		return fmt.Errorf("failed to get GitHub branch name: %w", err)
 	}
 
-	opts.Deploy = deploy
+	opts.GitHubBranch = branch
 
 	return nil
 }
@@ -458,9 +475,6 @@ func BuildPipelineInitCmd() *cobra.Command {
 			if err := opts.Ask(); err != nil {
 				return err
 			}
-			if err := opts.Validate(); err != nil { // validate flags
-				return err
-			}
 			return opts.Execute()
 		}),
 		PostRunE: func(cmd *cobra.Command, args []string) error {
@@ -474,8 +488,7 @@ func BuildPipelineInitCmd() *cobra.Command {
 	}
 	cmd.Flags().StringVarP(&opts.GitHubRepo, githubRepoFlag, githubRepoFlagShort, "", githubRepoFlagDescription)
 	cmd.Flags().StringVarP(&opts.GitHubAccessToken, githubAccessTokenFlag, githubAccessTokenFlagShort, "", githubAccessTokenFlagDescription)
-	cmd.Flags().BoolVar(&opts.Deploy, deployFlag, false, deployPipelineFlagDescription)
-	cmd.Flags().BoolVar(&opts.EnableCD, enableCDFlag, false, enableCDFlagDescription)
+	cmd.Flags().StringVarP(&opts.GitHubBranch, githubBranchFlag, githubBranchFlagShort, "", githubBranchFlagDescription)
 	cmd.Flags().StringSliceVarP(&opts.Environments, envsFlag, envsFlagShort, []string{}, pipelineEnvsFlagDescription)
 
 	return cmd
diff --git a/internal/pkg/cli/pipeline_update.go b/internal/pkg/cli/pipeline_update.go
index b3f1edb8..0a2a2d68 100644
--- a/internal/pkg/cli/pipeline_update.go
+++ b/internal/pkg/cli/pipeline_update.go
@@ -76,16 +76,11 @@ func (opts *UpdatePipelineOpts) Validate() error {
 }
 
 func (opts *UpdatePipelineOpts) convertStages(manifestStages []manifest.PipelineStage) ([]deploy.PipelineStage, error) {
-	var stages []deploy.PipelineStage
-	apps, err := opts.ws.Apps()
+	stages := make([]deploy.PipelineStage, 0, len(manifestStages))
+	allLocalApps, err := opts.ws.Apps()
 	if err != nil {
 		return nil, err
 	}
-	// TODO: Will fast follow with another PR to actually support #443
-	appNames := make([]string, 0, len(apps))
-	for _, app := range apps {
-		appNames = append(appNames, app.AppName())
-	}
 
 	for _, stage := range manifestStages {
 		env, err := opts.envStore.GetEnvironment(opts.ProjectName(), stage.Name)
@@ -93,8 +88,21 @@ func (opts *UpdatePipelineOpts) convertStages(manifestStages []manifest.Pipeline
 			return nil, err
 		}
 
+		// customers may not want integration test for each single app, so
+		// we will provision an integration test action only if
+		// an integration test buildspec is provided for a specific app in a
+		// specific stage in the pipeline
+		appsToDeploy := make([]deploy.AppInStage, 0, len(allLocalApps))
+		for _, app := range allLocalApps {
+			appConfig := stage.Apps[app.AppName()]
+			appsToDeploy = append(appsToDeploy, deploy.AppInStage{
+				Name:                   app.AppName(),
+				IntegTestBuildspecPath: appConfig.IntegTestBuildspecPath,
+			})
+		}
+
 		pipelineStage := deploy.PipelineStage{
-			LocalApplications: appNames,
+			LocalApplications: appsToDeploy,
 			AssociatedEnvironment: &deploy.AssociatedEnvironment{
 				Name:      stage.Name,
 				Region:    env.Region,
diff --git a/internal/pkg/deploy/cloudformation/stack/pipeline.go b/internal/pkg/deploy/cloudformation/stack/pipeline.go
index d7c9bf3c..45ee5721 100644
--- a/internal/pkg/deploy/cloudformation/stack/pipeline.go
+++ b/internal/pkg/deploy/cloudformation/stack/pipeline.go
@@ -21,6 +21,19 @@ type pipelineStackConfig struct {
 	*deploy.CreatePipelineInput
 }
 
+// NeedsIntegrationTests indicates whether the pipeline created contains
+// any integration tests
+func (p *pipelineStackConfig) NeedsIntegrationTests() bool {
+	for _, stage := range p.Stages {
+		for _, app := range stage.LocalApplications {
+			if app.IntegTestBuildspecPath != "" {
+				return true
+			}
+		}
+	}
+	return false
+}
+
 func NewPipelineStackConfig(in *deploy.CreatePipelineInput) *pipelineStackConfig {
 	return &pipelineStackConfig{
 		CreatePipelineInput: in,
diff --git a/internal/pkg/deploy/pipeline.go b/internal/pkg/deploy/pipeline.go
index 1836d8d7..cb41cceb 100644
--- a/internal/pkg/deploy/pipeline.go
+++ b/internal/pkg/deploy/pipeline.go
@@ -159,7 +159,14 @@ func (s *Source) Owner() (string, error) {
 // is deloying to and the containerized applications that will be deployed.
 type PipelineStage struct {
 	*AssociatedEnvironment
-	LocalApplications []string
+	LocalApplications []AppInStage
+}
+
+// AppInStage represents configurations for an app in a pipeline stage
+type AppInStage struct {
+	Name string
+	// the path to the integration test buildspec for this action
+	IntegTestBuildspecPath string
 }
 
 // AppTemplatePath returns the full path to the application CFN template
diff --git a/internal/pkg/describe/mocks/mock_describe.go b/internal/pkg/describe/mocks/mock_describe.go
index 3bcbf03b..04e6abf4 100644
--- a/internal/pkg/describe/mocks/mock_describe.go
+++ b/internal/pkg/describe/mocks/mock_describe.go
@@ -5,63 +5,47 @@
 package mocks
 
 import (
-	archer "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	cloudformation "github.com/aws/aws-sdk-go/service/cloudformation"
 	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
 )
 
-// MockenvAppGetter is a mock of envAppGetter interface
-type MockenvAppGetter struct {
+// MockResourceIdentifier is a mock of ResourceIdentifier interface
+type MockResourceIdentifier struct {
 	ctrl     *gomock.Controller
-	recorder *MockenvAppGetterMockRecorder
+	recorder *MockResourceIdentifierMockRecorder
 }
 
-// MockenvAppGetterMockRecorder is the mock recorder for MockenvAppGetter
-type MockenvAppGetterMockRecorder struct {
-	mock *MockenvAppGetter
+// MockResourceIdentifierMockRecorder is the mock recorder for MockResourceIdentifier
+type MockResourceIdentifierMockRecorder struct {
+	mock *MockResourceIdentifier
 }
 
-// NewMockenvAppGetter creates a new mock instance
-func NewMockenvAppGetter(ctrl *gomock.Controller) *MockenvAppGetter {
-	mock := &MockenvAppGetter{ctrl: ctrl}
-	mock.recorder = &MockenvAppGetterMockRecorder{mock}
+// NewMockResourceIdentifier creates a new mock instance
+func NewMockResourceIdentifier(ctrl *gomock.Controller) *MockResourceIdentifier {
+	mock := &MockResourceIdentifier{ctrl: ctrl}
+	mock.recorder = &MockResourceIdentifierMockRecorder{mock}
 	return mock
 }
 
 // EXPECT returns an object that allows the caller to indicate expected use
-func (m *MockenvAppGetter) EXPECT() *MockenvAppGetterMockRecorder {
+func (m *MockResourceIdentifier) EXPECT() *MockResourceIdentifierMockRecorder {
 	return m.recorder
 }
 
-// GetEnvironment mocks base method
-func (m *MockenvAppGetter) GetEnvironment(projectName, environmentName string) (*archer.Environment, error) {
+// URI mocks base method
+func (m *MockResourceIdentifier) URI(envName string) (string, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetEnvironment", projectName, environmentName)
-	ret0, _ := ret[0].(*archer.Environment)
+	ret := m.ctrl.Call(m, "URI", envName)
+	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// GetEnvironment indicates an expected call of GetEnvironment
-func (mr *MockenvAppGetterMockRecorder) GetEnvironment(projectName, environmentName interface{}) *gomock.Call {
+// URI indicates an expected call of URI
+func (mr *MockResourceIdentifierMockRecorder) URI(envName interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironment", reflect.TypeOf((*MockenvAppGetter)(nil).GetEnvironment), projectName, environmentName)
-}
-
-// GetApplication mocks base method
-func (m *MockenvAppGetter) GetApplication(projectName, applicationName string) (*archer.Application, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetApplication", projectName, applicationName)
-	ret0, _ := ret[0].(*archer.Application)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetApplication indicates an expected call of GetApplication
-func (mr *MockenvAppGetterMockRecorder) GetApplication(projectName, applicationName interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplication", reflect.TypeOf((*MockenvAppGetter)(nil).GetApplication), projectName, applicationName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "URI", reflect.TypeOf((*MockResourceIdentifier)(nil).URI), envName)
 }
 
 // MockstackDescriber is a mock of stackDescriber interface
diff --git a/internal/pkg/manifest/app_manifest.go b/internal/pkg/manifest/app_manifest.go
index 438b1953..ef10029f 100644
--- a/internal/pkg/manifest/app_manifest.go
+++ b/internal/pkg/manifest/app_manifest.go
@@ -12,6 +12,10 @@ import (
 const (
 	// LoadBalancedWebApplication is a web application with a load balancer and Fargate as compute.
 	LoadBalancedWebApplication = "Load Balanced Web App"
+
+	// IntegTestBuildspecFileName is the name of the CodeBuild build
+	// specification for any integration test actions in the archer pipeline
+	IntegTestBuildspecFileName = "buildspec_integ.yml"
 )
 
 // AppTypes are the supported manifest types.
diff --git a/internal/pkg/manifest/lb_fargate_manifest.go b/internal/pkg/manifest/lb_fargate_manifest.go
index c9ff42a6..f1c36e0b 100644
--- a/internal/pkg/manifest/lb_fargate_manifest.go
+++ b/internal/pkg/manifest/lb_fargate_manifest.go
@@ -5,6 +5,7 @@ package manifest
 
 import (
 	"bytes"
+	"path/filepath"
 	"text/template"
 
 	"github.com/aws/amazon-ecs-cli-v2/templates"
@@ -105,6 +106,11 @@ func (m LBFargateManifest) DockerfilePath() string {
 	return m.Image.Build
 }
 
+// IntegTestBuildspecPath returns the path to the
+func (m LBFargateManifest) IntegTestBuildspecPath() string {
+	return filepath.Join(m.Image.Build, IntegTestBuildspecFileName)
+}
+
 // EnvConf returns the application configuration with environment overrides.
 // If the environment passed in does not have any overrides then we return the default values.
 func (m *LBFargateManifest) EnvConf(envName string) LBFargateConfig {
diff --git a/internal/pkg/manifest/pipeline.go b/internal/pkg/manifest/pipeline.go
index 86738573..9afde174 100644
--- a/internal/pkg/manifest/pipeline.go
+++ b/internal/pkg/manifest/pipeline.go
@@ -1,4 +1,3 @@
-
 // SPDX-License-Identifier: Apache-2.0
 
 package manifest
@@ -12,6 +11,7 @@ import (
 	"github.com/fatih/structs"
 	"gopkg.in/yaml.v3"
 
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/templates"
 )
 
@@ -94,22 +94,54 @@ type Source struct {
 	Properties   map[string]interface{} `yaml:"properties"`
 }
 
+// App defines configurations related to how an app should be deployed, verified
+// in the pipeline
+type App struct {
+	IntegTestBuildspecPath string `structs:"integrationTestBuildspec" yaml:"integrationTestBuildspec"`
+}
+
+// Properties return the configurations as a map
+func (a App) Properties() map[string]interface{} {
+	return structs.Map(a)
+}
+
 // PipelineStage represents a stage in the pipeline manifest
 type PipelineStage struct {
-	Name string `yaml:"name`
+	Name string         `yaml:"name`
+	Apps map[string]App `yaml:"apps"`
 }
 
 // CreatePipeline returns a pipeline manifest object.
-func CreatePipeline(pipelineName string, provider Provider, stageNames []string) (*PipelineManifest, error) {
+func CreatePipeline(
+	pipelineName string,
+	provider Provider,
+	stageNames []string,
+	apps []archer.Manifest) (*PipelineManifest, error) {
+
 	// TODO: #221 Do more validations
 	if len(stageNames) == 0 {
 		return nil, fmt.Errorf("a pipeline %s can not be created without a deployment stage",
 			pipelineName)
 	}
 
+	if len(apps) == 0 {
+		return nil, fmt.Errorf("a pipeline %s can not be created without any app to deploy",
+			pipelineName)
+	}
+
+	appsInPipeline := make(map[string]App)
+	for _, app := range apps {
+		appsInPipeline[app.AppName()] = App{
+			IntegTestBuildspecPath: app.IntegTestBuildspecPath(),
+		}
+	}
+
 	stages := make([]PipelineStage, 0, len(stageNames))
 	for _, name := range stageNames {
-		stages = append(stages, PipelineStage{Name: name})
+		stages = append(stages, PipelineStage{
+			Name: name,
+			Apps: appsInPipeline,
+		})
 	}
 
 	return &PipelineManifest{
diff --git a/internal/pkg/term/command/command.go b/internal/pkg/term/command/command.go
new file mode 100644
index 00000000..5940ef0b
--- /dev/null
+++ b/internal/pkg/term/command/command.go
@@ -0,0 +1,50 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package command
+
+import (
+	"io"
+	"os"
+	"os/exec"
+)
+
+// Service exists for mockability.
+type Service struct{}
+
+// New returns a Service.
+func New() Service {
+	return Service{}
+}
+
+// Option is the function signature for customizing the internal *exec.Cmd.
+type Option func(cmd *exec.Cmd)
+
+// Stdin sets the internal *exec.Cmd's Stdin field.
+func Stdin(r io.Reader) Option {
+	return func(c *exec.Cmd) {
+		c.Stdin = r
+	}
+}
+
+// Stdout sets the internal *exec.Cmd's Stdout field.
+func Stdout(writer io.Writer) Option {
+	return func(c *exec.Cmd) {
+		c.Stdout = writer
+	}
+}
+
+// Run runs the input command with input args with Stdout and Stderr defaulted to os.Stderr.
+// Input options will override these defaults.
+func (s Service) Run(name string, args []string, options ...Option) error {
+	cmd := exec.Command(name, args...)
+
+	cmd.Stdout = os.Stderr
+	cmd.Stderr = os.Stderr
+
+	for _, opt := range options {
+		opt(cmd)
+	}
+
+	return cmd.Run()
+}
diff --git a/templates/cicd/buildspec_integ.yml b/templates/cicd/buildspec_integ.yml
new file mode 100644
index 00000000..c51519a7
--- /dev/null
+++ b/templates/cicd/buildspec_integ.yml
@@ -0,0 +1,12 @@
+# Buildspec that runs the integration tests against each app in each
+# pipeline stage
+version: 0.2
+phases:
+  install:
+    runtime-versions:
+      docker: 18
+    commands:
+      - echo $PIPELINE_STAGE
+  build:
+    commands:
+      - echo "Please add your integration tests" && exit 1
\ No newline at end of file
diff --git a/templates/cicd/pipeline.yml b/templates/cicd/pipeline.yml
index 35f5dea5..fb28bfad 100644
--- a/templates/cicd/pipeline.yml
+++ b/templates/cicd/pipeline.yml
@@ -19,6 +19,8 @@ source:
 # The deployment section defines the order the pipeline will deploy
 # to your environments.
 stages:{{range .Stages}}
-    - # The name of the environment to deploy to.
-      name: {{.Name}}{{end}}
-{{end}}
\ No newline at end of file
+  - # The name of the environment to deploy to.
+    name: {{.Name}}
+    apps:{{range $appName, $appConfig := .Apps}}
+      {{$appName}}:{{range $field, $value := $appConfig.Properties}}
+        {{$field}}: {{$value}}{{end}}{{end}}{{end}}{{end}}
\ No newline at end of file
diff --git a/templates/cicd/pipeline_cfn.yml b/templates/cicd/pipeline_cfn.yml
index 5573b1a5..c39f72dc 100644
--- a/templates/cicd/pipeline_cfn.yml
+++ b/templates/cicd/pipeline_cfn.yml
@@ -112,7 +112,82 @@ Resources:
       Source:
         Type: CODEPIPELINE
         BuildSpec: ecs-project/buildspec.yml
-      TimeoutInMinutes: 60
+      TimeoutInMinutes: 60{{if $.NeedsIntegrationTests}}
+  IntegTestRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - codebuild.amazonaws.com
+            Action:
+              - sts:AssumeRole
+      Path: /
+  IntegTestPolicy:
+    Type: AWS::IAM::Policy
+    DependsOn: IntegTestRole
+    Properties:
+      PolicyName: !Sub ${AWS::StackName}-IntegTestPolicy
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Action:
+              - logs:CreateLogGroup
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+            Resource: arn:aws:logs:*:*:*
+          - Effect: Allow
+            Action:
+              - s3:PutObject
+              - s3:GetObject
+              - s3:GetObjectVersion
+            # TODO: This might not be necessary. We may only need the bucket
+            # that is in the same region as the pipeline.
+            # Loop through all the artifact buckets created in the stackset
+            Resource:{{range .ArtifactBuckets}}
+              - !Join ['', ['arn:aws:s3:::', '{{.BucketName}}']]
+              - !Join ['', ['arn:aws:s3:::', '{{.BucketName}}', '/*']]{{end}}
+          - Effect: Allow
+            Action:
+              # TODO: scope this down if possible
+              - kms:*
+            # TODO: This might not be necessary. We may only need the KMS key
+            # that is in the same region as the pipeline.
+            # Loop through all the KMS keys used to en/decrypt artifacts
+            # across (cross-regional) pipeline stages, with each stage
+            # backed by a (regional) S3 bucket.
+            Resource:{{range .ArtifactBuckets}}
+              - {{.KeyArn}}{{end}}
+      Roles:
+        - !Ref IntegTestRole{{end}}{{$stageLength := len .Stages}}{{if gt $stageLength 0}}{{range $stage := .Stages}}{{$appLength := len $stage.LocalApplications}}{{if gt $appLength 0}}{{range $app := $stage.LocalApplications}}{{if $app.IntegTestBuildspecPath}}
+  IntegTestProject{{logicalIDSafe $app.Name}}{{logicalIDSafe $stage.Name}}:
+    Type: AWS::CodeBuild::Project
+    Properties:
+      Name: Approve-{{$app.Name}}-{{$stage.Name}}
+      Description: Integration tests for {{$app.Name}}-{{$stage.Name}}
+      ServiceRole: !GetAtt IntegTestRole.Arn
+      Artifacts:
+        Type: CODEPIPELINE
+      Environment:
+        Type: LINUX_CONTAINER
+        ComputeType: BUILD_GENERAL1_SMALL
+        PrivilegedMode: true
+        Image: aws/codebuild/amazonlinux2-x86_64-standard:1.0
+        EnvironmentVariables:
+          - Name: PIPELINE_STAGE
+            Type: PLAINTEXT
+            Value: {{$stage.Name}}
+          - Name: APP_NAME
+            Type: PLAINTEXT
+            Value: {{$app.Name}}
+      Source:
+        Type: CODEPIPELINE
+        BuildSpec: {{$app.IntegTestBuildspecPath}}
+      TimeoutInMinutes: 480{{end}}{{end}}{{end}}{{end}}{{end}}
   PipelineRole:
     Type: AWS::IAM::Role
     Properties:
@@ -233,8 +308,7 @@ Resources:
             InputArtifacts:
               - Name: SCCheckoutArtifact
             OutputArtifacts:
-              - Name: BuildOutput
-{{$length := len .Stages}}{{if gt $length 0}}{{range $stage := .Stages}}
+              - Name: BuildOutput{{$length := len .Stages}}{{if gt $length 0}}{{range $stage := .Stages}}{{$appNum := len $stage.LocalApplications}}{{if gt $appNum 0}}
         - Name: DeployTo-{{$stage.Name}}
           Actions:{{if $stage.Prod}}
             - Name: ApprovePromotionTo-{{$stage.Name}}
@@ -244,7 +318,7 @@ Resources:
                 Provider: Manual
                 Version: 1
               RunOrder: 1{{end}}{{range $app := $stage.LocalApplications}}
-            - Name: CreateOrUpdate-{{$app}}-{{$stage.Name}}
+            - Name: CreateOrUpdate-{{$app.Name}}-{{$stage.Name}}
               Region: {{$stage.Region}}
               ActionTypeId:
                 Category: Deploy
@@ -253,12 +327,12 @@ Resources:
                 Provider: CloudFormation
               Configuration:
                 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-action-reference.html
-                ChangeSetName: {{$.ProjectName}}-{{$stage.Name}}-{{$app}}
+                ChangeSetName: {{$.ProjectName}}-{{$stage.Name}}-{{$app.Name}}
                 ActionMode: CREATE_UPDATE
-                StackName: {{$.ProjectName}}-{{$stage.Name}}-{{$app}}
+                StackName: {{$.ProjectName}}-{{$stage.Name}}-{{$app.Name}}
                 Capabilities: CAPABILITY_NAMED_IAM
-                TemplatePath: BuildOutput::infrastructure/{{$stage.AppTemplatePath $app}}
-                TemplateConfiguration: BuildOutput::infrastructure/{{$stage.AppTemplateConfigurationPath $app}}
+                TemplatePath: BuildOutput::infrastructure/{{$stage.AppTemplatePath $app.Name}}
+                TemplateConfiguration: BuildOutput::infrastructure/{{$stage.AppTemplateConfigurationPath $app.Name}}
                 # The ARN of the IAM role (in the env accountn) that
                 # AWS CloudFormation assumes when it operates on resources
                 # in a stack in an environment account.
@@ -269,4 +343,15 @@ Resources:
               # The ARN of the environment manager IAM role (in the env
               # account) that performs the declared action. This is assumed
               # through the roleArn for the pipeline.
-              RoleArn: arn:aws:iam::{{$stage.AccountID}}:role/{{$.ProjectName}}-{{$stage.Name}}-EnvManagerRole{{end}}{{end}}{{end}}
\ No newline at end of file
+              RoleArn: arn:aws:iam::{{$stage.AccountID}}:role/{{$.ProjectName}}-{{$stage.Name}}-EnvManagerRole{{if $app.IntegTestBuildspecPath}}
+            - Name: Approve-{{$app.Name}}-{{$stage.Name}}
+              ActionTypeId:
+                Category: Build
+                Owner: AWS
+                Version: 1
+                Provider: CodeBuild
+              Configuration:
+                ProjectName: !Ref IntegTestProject{{logicalIDSafe $app.Name}}{{logicalIDSafe $stage.Name}}
+              RunOrder: 3
+              InputArtifacts:
+                - Name: SCCheckoutArtifact{{end}}{{end}}{{end}}{{end}}{{end}}
\ No newline at end of file


Test patch:
diff --git a/internal/pkg/aws/ecr/ecr_test.go b/internal/pkg/aws/ecr/ecr_test.go
new file mode 100644
index 00000000..74bf24c6
--- /dev/null
+++ b/internal/pkg/aws/ecr/ecr_test.go
@@ -0,0 +1,368 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package ecr
+
+import (
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"testing"
+
+	awsmocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/mocks"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetECRAuth(t *testing.T) {
+	mockError := errors.New("error")
+
+	mockUsername := "mockUsername"
+	mockPassword := "mockPassword"
+
+	encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", mockUsername, mockPassword)))
+
+	testCases := map[string]struct {
+		mockECRAPI func(m *awsmocks.MockECRAPI)
+
+		wantAuth Auth
+		wantErr  error
+	}{
+		"should return wrapped error given error returned from GetAuthorizationToken": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().GetAuthorizationToken(gomock.Any()).Return(nil, mockError)
+			},
+			wantErr: fmt.Errorf("get ECR auth: %w", mockError),
+		},
+		"should return Auth data": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().GetAuthorizationToken(gomock.Any()).Return(&ecr.GetAuthorizationTokenOutput{
+					AuthorizationData: []*ecr.AuthorizationData{
+						&ecr.AuthorizationData{
+							AuthorizationToken: aws.String(encoded),
+						},
+					},
+				}, nil)
+			},
+			wantAuth: Auth{
+				Username: mockUsername,
+				Password: mockPassword,
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// GIVEN
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			mockECRAPI := awsmocks.NewMockECRAPI(ctrl)
+			tc.mockECRAPI(mockECRAPI)
+
+			service := Service{
+				ecr: mockECRAPI,
+			}
+
+			gotAuth, gotErr := service.GetECRAuth()
+
+			require.Equal(t, tc.wantAuth, gotAuth)
+			require.Equal(t, tc.wantErr, gotErr)
+		})
+
+	}
+}
+
+func TestGetRepository(t *testing.T) {
+	mockError := errors.New("error")
+
+	mockRepoName := "mockRepoName"
+	mockRepoURI := "mockRepoURI"
+
+	testCases := map[string]struct {
+		mockECRAPI func(m *awsmocks.MockECRAPI)
+
+		wantURI string
+		wantErr error
+	}{
+		"should return wrapped error given error returned from DescribeRepositories": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeRepositories(gomock.Any()).Return(nil, mockError)
+			},
+			wantErr: fmt.Errorf("ecr describe repository %s: %w", mockRepoName, mockError),
+		},
+		"should return error given no repositories returned in list": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeRepositories(&ecr.DescribeRepositoriesInput{
+					RepositoryNames: aws.StringSlice([]string{mockRepoName}),
+				}).Return(&ecr.DescribeRepositoriesOutput{
+					Repositories: []*ecr.Repository{},
+				}, nil)
+			},
+			wantErr: errors.New("no repositories found"),
+		},
+		"should return repository URI": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeRepositories(&ecr.DescribeRepositoriesInput{
+					RepositoryNames: aws.StringSlice([]string{mockRepoName}),
+				}).Return(&ecr.DescribeRepositoriesOutput{
+					Repositories: []*ecr.Repository{
+						&ecr.Repository{
+							RepositoryUri: aws.String(mockRepoURI),
+						},
+					},
+				}, nil)
+			},
+			wantURI: mockRepoURI,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// GIVEN
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			mockECRAPI := awsmocks.NewMockECRAPI(ctrl)
+			tc.mockECRAPI(mockECRAPI)
+
+			service := Service{
+				mockECRAPI,
+			}
+
+			gotURI, gotErr := service.GetRepository(mockRepoName)
+
+			require.Equal(t, tc.wantURI, gotURI)
+			require.Equal(t, tc.wantErr, gotErr)
+		})
+	}
+}
+
+func TestURIFromARN(t *testing.T) {
+
+	testCases := map[string]struct {
+		givenARN  string
+		wantedURI string
+		wantErr   error
+	}{
+		"valid arn": {
+			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myrepo",
+			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myrepo",
+		},
+		"valid arn with namespace": {
+			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myproject/myapp",
+			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myproject/myapp",
+		},
+		"separate region": {
+			givenARN:  "arn:aws:ecr:us-east-1:0123456789:repository/myproject/myapp",
+			wantedURI: "0123456789.dkr.ecr.us-east-1.amazonaws.com/myproject/myapp",
+		},
+		"invalid ARN": {
+			givenARN: "myproject/myapp",
+			wantErr:  fmt.Errorf("parsing repository ARN myproject/myapp: arn: invalid prefix"),
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			uri, err := URIFromARN(tc.givenARN)
+			if tc.wantErr != nil {
+				require.Error(t, err)
+				require.EqualError(t, err, tc.wantErr.Error())
+			} else {
+				require.Equal(t, tc.wantedURI, uri)
+			}
+		})
+	}
+}
+
+func TestListImages(t *testing.T) {
+	mockRepoName := "mockRepoName"
+	mockError := errors.New("mockError")
+	mockDigest := "mockDigest"
+	mockNextToken := "next"
+
+	tests := map[string]struct {
+		mockECRAPI func(m *awsmocks.MockECRAPI)
+
+		wantImages []Image
+		wantError  error
+	}{
+		"should wrap error returned by ECR DescribeImages": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeImages(gomock.Any()).Return(nil, mockError)
+			},
+			wantImages: nil,
+			wantError:  fmt.Errorf("ecr repo %s describe images: %w", mockRepoName, mockError),
+		},
+		"should return Image list": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeImages(gomock.Any()).Return(&ecr.DescribeImagesOutput{
+					ImageDetails: []*ecr.ImageDetail{
+						&ecr.ImageDetail{
+							ImageDigest: aws.String(mockDigest),
+						},
+					},
+				}, nil)
+			},
+			wantImages: []Image{Image{Digest: mockDigest}},
+			wantError:  nil,
+		},
+		"should return all images when paginated": {
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
+					RepositoryName: aws.String(mockRepoName),
+				}).Return(&ecr.DescribeImagesOutput{
+					ImageDetails: []*ecr.ImageDetail{
+						&ecr.ImageDetail{
+							ImageDigest: aws.String(mockDigest),
+						},
+					},
+					NextToken: &mockNextToken,
+				}, nil)
+				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
+					RepositoryName: aws.String(mockRepoName),
+					NextToken:      &mockNextToken,
+				}).Return(&ecr.DescribeImagesOutput{
+					ImageDetails: []*ecr.ImageDetail{
+						&ecr.ImageDetail{
+							ImageDigest: aws.String(mockDigest),
+						},
+					},
+				}, nil)
+			},
+			wantImages: []Image{Image{Digest: mockDigest}, Image{Digest: mockDigest}},
+			wantError:  nil,
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			// GIVEN
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			mockECRAPI := awsmocks.NewMockECRAPI(ctrl)
+			tc.mockECRAPI(mockECRAPI)
+
+			service := Service{
+				mockECRAPI,
+			}
+
+			gotImages, gotError := service.ListImages(mockRepoName)
+
+			require.ElementsMatch(t, tc.wantImages, gotImages)
+			require.Equal(t, tc.wantError, gotError)
+		})
+	}
+}
+
+func TestDeleteImages(t *testing.T) {
+	mockRepoName := "mockRepoName"
+	mockError := errors.New("mockError")
+	mockDigest := "mockDigest"
+	mockImages := []Image{
+		Image{
+			Digest: mockDigest,
+		},
+	}
+	mockFailCode := "400"
+	mockFailReason := "some reason"
+	// with only one image identifier
+	var imageIdentifiers []*ecr.ImageIdentifier
+	for _, image := range mockImages {
+		imageIdentifiers = append(imageIdentifiers, image.imageIdentifier())
+	}
+	var mockBatchImages []Image
+	for ii := 0; ii < batchDeleteLimit+1; ii++ {
+		mockBatchImages = append(mockBatchImages, mockImages[0])
+	}
+	// with a batch limit number of image identifiers
+	var batchImageIdentifiers []*ecr.ImageIdentifier
+	for ii := 0; ii < batchDeleteLimit; ii++ {
+		batchImageIdentifiers = append(batchImageIdentifiers, mockImages[0].imageIdentifier())
+	}
+
+	tests := map[string]struct {
+		images     []Image
+		mockECRAPI func(m *awsmocks.MockECRAPI)
+
+		wantError error
+	}{
+		"should not return error if input Image list is empty": {
+			images:     nil,
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {},
+			wantError:  nil,
+		},
+		"should wrap error return from BatchDeleteImage": {
+			images: mockImages,
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().BatchDeleteImage(gomock.Any()).Return(nil, mockError)
+			},
+			wantError: fmt.Errorf("ecr repo %s batch delete image: %w", mockRepoName, mockError),
+		},
+		"should return nil if call to BatchDeleteImage successful": {
+			images: mockImages,
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
+					RepositoryName: aws.String(mockRepoName),
+					ImageIds:       imageIdentifiers,
+				}).Return(&ecr.BatchDeleteImageOutput{}, nil)
+			},
+			wantError: nil,
+		},
+		fmt.Sprintf("should be able to batch delete more than %d images", batchDeleteLimit): {
+			images: mockBatchImages,
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
+					RepositoryName: aws.String(mockRepoName),
+					ImageIds:       batchImageIdentifiers,
+				}).Return(&ecr.BatchDeleteImageOutput{}, nil).Times(1)
+				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
+					RepositoryName: aws.String(mockRepoName),
+					ImageIds:       imageIdentifiers,
+				}).Return(&ecr.BatchDeleteImageOutput{}, nil).Times(1)
+			},
+			wantError: nil,
+		},
+		"warns if fail to delete some images": {
+			images: mockImages,
+			mockECRAPI: func(m *awsmocks.MockECRAPI) {
+				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
+					RepositoryName: aws.String(mockRepoName),
+					ImageIds:       imageIdentifiers,
+				}).Return(&ecr.BatchDeleteImageOutput{
+					Failures: []*ecr.ImageFailure{
+						&ecr.ImageFailure{
+							FailureCode:   &mockFailCode,
+							FailureReason: &mockFailReason,
+							ImageId:       imageIdentifiers[0],
+						},
+					},
+				}, nil)
+			},
+			wantError: nil,
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			// GIVEN
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			mockECRAPI := awsmocks.NewMockECRAPI(ctrl)
+			tc.mockECRAPI(mockECRAPI)
+
+			service := Service{
+				mockECRAPI,
+			}
+
+			got := service.DeleteImages(tc.images, mockRepoName)
+
+			require.Equal(t, tc.wantError, got)
+		})
+	}
+}
diff --git a/internal/pkg/aws/ecr/erc_test.go b/internal/pkg/aws/ecr/erc_test.go
deleted file mode 100644
index c722f860..00000000
--- a/internal/pkg/aws/ecr/erc_test.go
+++ /dev/null
@@ -1,316 +0,0 @@
-// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package ecr
-
-import (
-	"encoding/base64"
-	"errors"
-	"fmt"
-	"testing"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/ecr"
-	"github.com/aws/aws-sdk-go/service/ecr/ecriface"
-	"github.com/stretchr/testify/require"
-)
-
-type mockECR struct {
-	ecriface.ECRAPI
-
-	t *testing.T
-
-	mockGetAuthorizationToken func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
-	mockCreateRepository      func(*ecr.CreateRepositoryInput) (*ecr.CreateRepositoryOutput, error)
-	mockDescribeRepositories  func(*ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error)
-	mockDescribeImages        func(*testing.T, *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error)
-	mockBatchDeleteImage      func(*testing.T, *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error)
-}
-
-func (m mockECR) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
-	return m.mockGetAuthorizationToken(input)
-}
-
-func (m mockECR) CreateRepository(input *ecr.CreateRepositoryInput) (*ecr.CreateRepositoryOutput, error) {
-	return m.mockCreateRepository(input)
-}
-
-func (m mockECR) DescribeRepositories(input *ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error) {
-	return m.mockDescribeRepositories(input)
-}
-
-func (m mockECR) DescribeImages(input *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error) {
-	return m.mockDescribeImages(m.t, input)
-}
-
-func (m mockECR) BatchDeleteImage(input *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error) {
-	return m.mockBatchDeleteImage(m.t, input)
-}
-
-func TestGetECRAuth(t *testing.T) {
-	mockError := errors.New("error")
-
-	mockUsername := "mockUsername"
-	mockPassword := "mockPassword"
-
-	encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", mockUsername, mockPassword)))
-
-	testCases := map[string]struct {
-		mockGetAuthorizationToken func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
-
-		wantAuth Auth
-		wantErr  error
-	}{
-		"should return wrapped error given error returned from GetAuthorizationToken": {
-			mockGetAuthorizationToken: func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
-				return nil, mockError
-			},
-			wantErr: fmt.Errorf("get ECR auth: %w", mockError),
-		},
-		"should return Auth data": {
-			mockGetAuthorizationToken: func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
-				return &ecr.GetAuthorizationTokenOutput{
-					AuthorizationData: []*ecr.AuthorizationData{
-						&ecr.AuthorizationData{
-							AuthorizationToken: aws.String(encoded),
-						},
-					},
-				}, nil
-			},
-			wantAuth: Auth{
-				Username: mockUsername,
-				Password: mockPassword,
-			},
-		},
-	}
-
-	for name, tc := range testCases {
-		t.Run(name, func(t *testing.T) {
-			service := Service{
-				mockECR{
-					mockGetAuthorizationToken: tc.mockGetAuthorizationToken,
-				},
-			}
-
-			gotAuth, gotErr := service.GetECRAuth()
-
-			require.Equal(t, tc.wantAuth, gotAuth)
-			require.Equal(t, tc.wantErr, gotErr)
-		})
-	}
-}
-
-func TestGetRepository(t *testing.T) {
-	mockError := errors.New("error")
-
-	mockRepoName := "mockRepoName"
-	mockRepoURI := "mockRepoURI"
-
-	testCases := map[string]struct {
-		mockDescribeRepositories func(*ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error)
-
-		wantURI string
-		wantErr error
-	}{
-		"should return wrapped error given error returned from DescribeRepositories": {
-			mockDescribeRepositories: func(*ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error) {
-				return nil, mockError
-			},
-			wantErr: fmt.Errorf("ecr describe repository %s: %w", mockRepoName, mockError),
-		},
-		"should return error given no repositories returned in list": {
-			mockDescribeRepositories: func(input *ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error) {
-				return &ecr.DescribeRepositoriesOutput{
-					Repositories: []*ecr.Repository{},
-				}, nil
-			},
-			wantErr: errors.New("no repositories found"),
-		},
-		"should return repository URI": {
-			mockDescribeRepositories: func(input *ecr.DescribeRepositoriesInput) (*ecr.DescribeRepositoriesOutput, error) {
-				require.Equal(t, mockRepoName, *input.RepositoryNames[0])
-
-				return &ecr.DescribeRepositoriesOutput{
-					Repositories: []*ecr.Repository{
-						&ecr.Repository{
-							RepositoryUri: aws.String(mockRepoURI),
-						},
-					},
-				}, nil
-			},
-			wantURI: mockRepoURI,
-		},
-	}
-
-	for name, tc := range testCases {
-		t.Run(name, func(t *testing.T) {
-			service := Service{
-				mockECR{
-					mockDescribeRepositories: tc.mockDescribeRepositories,
-				},
-			}
-
-			gotURI, gotErr := service.GetRepository(mockRepoName)
-
-			require.Equal(t, tc.wantURI, gotURI)
-			require.Equal(t, tc.wantErr, gotErr)
-		})
-	}
-}
-
-func TestListImages(t *testing.T) {
-	mockRepoName := "mockRepoName"
-	mockError := errors.New("mockError")
-	mockDigest := "mockDigest"
-
-	tests := map[string]struct {
-		mockDescribeImages func(*testing.T, *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error)
-
-		wantImages []Image
-		wantError  error
-	}{
-		"should wrap error returned by ECR DescribeImages": {
-			mockDescribeImages: func(t *testing.T, in *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error) {
-				return nil, mockError
-			},
-			wantImages: nil,
-			wantError:  fmt.Errorf("ecr repo %s describe images: %w", mockRepoName, mockError),
-		},
-		"should return Image list": {
-			mockDescribeImages: func(t *testing.T, in *ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error) {
-				t.Helper()
-
-				require.Equal(t, mockRepoName, *in.RepositoryName)
-
-				return &ecr.DescribeImagesOutput{
-					ImageDetails: []*ecr.ImageDetail{
-						&ecr.ImageDetail{
-							ImageDigest: aws.String(mockDigest),
-						},
-					},
-				}, nil
-			},
-			wantImages: []Image{Image{Digest: mockDigest}},
-			wantError:  nil,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			s := Service{
-				ecr: mockECR{
-					t: t,
-
-					mockDescribeImages: test.mockDescribeImages,
-				},
-			}
-
-			gotImages, gotError := s.ListImages(mockRepoName)
-
-			require.ElementsMatch(t, test.wantImages, gotImages)
-			require.Equal(t, test.wantError, gotError)
-		})
-	}
-}
-
-func TestDeleteImages(t *testing.T) {
-	mockRepoName := "mockRepoName"
-	mockError := errors.New("mockError")
-	mockDigest := "mockDigest"
-	mockImages := []Image{
-		Image{
-			Digest: mockDigest,
-		},
-	}
-
-	tests := map[string]struct {
-		images []Image
-
-		mockBatchDeleteImage func(*testing.T, *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error)
-
-		want error
-	}{
-		"should return an error if input Image list is empty": {
-			images: nil,
-			want:   nil,
-		},
-		"should wrap error return from BatchDeleteImage": {
-			images: mockImages,
-			mockBatchDeleteImage: func(t *testing.T, in *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error) {
-				return nil, mockError
-			},
-			want: fmt.Errorf("ecr repo %s batch delete image: %w", mockRepoName, mockError),
-		},
-		"should return nil if call to BatchDeleteImage successful": {
-			images: mockImages,
-			mockBatchDeleteImage: func(t *testing.T, in *ecr.BatchDeleteImageInput) (*ecr.BatchDeleteImageOutput, error) {
-				t.Helper()
-
-				require.Equal(t, mockRepoName, *in.RepositoryName)
-
-				var imageIdentifiers []*ecr.ImageIdentifier
-				for _, image := range mockImages {
-					imageIdentifiers = append(imageIdentifiers, image.imageIdentifier())
-				}
-				require.ElementsMatch(t, imageIdentifiers, in.ImageIds)
-
-				return &ecr.BatchDeleteImageOutput{}, nil
-			},
-			want: nil,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			s := Service{
-				ecr: mockECR{
-					t: t,
-
-					mockBatchDeleteImage: test.mockBatchDeleteImage,
-				},
-			}
-
-			got := s.DeleteImages(test.images, mockRepoName)
-
-			require.Equal(t, test.want, got)
-		})
-	}
-}
-
-func TestURIFromARN(t *testing.T) {
-
-	testCases := map[string]struct {
-		givenARN  string
-		wantedURI string
-		wantErr   error
-	}{
-		"valid arn": {
-			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myrepo",
-			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myrepo",
-		},
-		"valid arn with namespace": {
-			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myproject/myapp",
-			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myproject/myapp",
-		},
-		"separate region": {
-			givenARN:  "arn:aws:ecr:us-east-1:0123456789:repository/myproject/myapp",
-			wantedURI: "0123456789.dkr.ecr.us-east-1.amazonaws.com/myproject/myapp",
-		},
-		"invalid ARN": {
-			givenARN: "myproject/myapp",
-			wantErr:  fmt.Errorf("parsing repository ARN myproject/myapp: arn: invalid prefix"),
-		},
-	}
-
-	for name, tc := range testCases {
-		t.Run(name, func(t *testing.T) {
-			uri, err := URIFromARN(tc.givenARN)
-			if tc.wantErr != nil {
-				require.Error(t, err)
-				require.EqualError(t, err, tc.wantErr.Error())
-			} else {
-				require.Equal(t, tc.wantedURI, uri)
-			}
-		})
-	}
-}
diff --git a/internal/pkg/build/docker/docker_test.go b/internal/pkg/build/docker/docker_test.go
index 5ea15b64..10fa4ea5 100644
--- a/internal/pkg/build/docker/docker_test.go
+++ b/internal/pkg/build/docker/docker_test.go
@@ -1,3 +1,6 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
 package docker
 
 import (
@@ -5,25 +8,11 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/ecr"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/build/docker/mocks"
+	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 )
 
-type mockRunnable struct {
-	t *testing.T
-
-	mockRun           func() error
-	mockStandardInput func(t *testing.T, input string)
-}
-
-func (mr mockRunnable) run() error {
-	return mr.mockRun()
-}
-
-func (mr mockRunnable) standardInput(input string) {
-	mr.mockStandardInput(mr.t, input)
-}
-
 func TestBuild(t *testing.T) {
 	mockError := errors.New("mockError")
 
@@ -31,43 +20,36 @@ func TestBuild(t *testing.T) {
 	mockImageTag := "mockImageTag"
 	mockPath := "mockPath"
 
-	tests := map[string]struct {
-		commandName string
-		args        []string
+	var mockRunner *mocks.Mockrunner
 
-		mockRun func() error
+	tests := map[string]struct {
+		setupMocks func(controller *gomock.Controller)
 
 		want error
 	}{
 		"wrap error returned from Run()": {
-			commandName: "docker",
-			args:        []string{"build", "-t", imageName(mockURI, mockImageTag), mockPath},
-			mockRun: func() error {
-				return mockError
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
+
+				mockRunner.EXPECT().Run("docker", []string{"build", "-t", imageName(mockURI, mockImageTag), mockPath}).Return(mockError)
 			},
 			want: fmt.Errorf("building image: %w", mockError),
 		},
 		"happy path": {
-			commandName: "docker",
-			args:        []string{"build", "-t", imageName(mockURI, mockImageTag), mockPath},
-			mockRun: func() error {
-				return nil
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
+
+				mockRunner.EXPECT().Run("docker", []string{"build", "-t", imageName(mockURI, mockImageTag), mockPath}).Return(nil)
 			},
 		},
 	}
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			test.setupMocks(controller)
 			s := Service{
-				createCommand: func(name string, args ...string) runnable {
-					require.Equal(t, test.commandName, name)
-					require.ElementsMatch(t, test.args, args)
-
-					return mockRunnable{
-						t:       t,
-						mockRun: test.mockRun,
-					}
-				},
+				runner: mockRunner,
 			}
 
 			got := s.Build(mockURI, mockImageTag, mockPath)
@@ -84,63 +66,40 @@ func TestLogin(t *testing.T) {
 	mockUsername := "mockUsername"
 	mockPassword := "mockPassword"
 
-	mockAuth := ecr.Auth{
-		Username: mockUsername,
-		Password: mockPassword,
-	}
+	var mockRunner *mocks.Mockrunner
 
 	tests := map[string]struct {
-		commandName string
-		args        []string
-
-		mockStandardInput func(t *testing.T, input string)
-		mockRun           func() error
+		setupMocks func(controller *gomock.Controller)
 
 		want error
 	}{
 		"wrap error returned from Run()": {
-			commandName: "docker",
-			args:        []string{"login", "-u", mockUsername, "--password-stdin", mockURI},
-			mockStandardInput: func(t *testing.T, input string) {
-				t.Helper()
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
 
-				require.Equal(t, mockPassword, input)
-			},
-			mockRun: func() error {
-				return mockError
+				mockRunner.EXPECT().Run("docker", []string{"login", "-u", mockUsername, "--password-stdin", mockURI}, gomock.Any()).Return(mockError)
 			},
 			want: fmt.Errorf("authenticate to ECR: %w", mockError),
 		},
 		"happy path": {
-			commandName: "docker",
-			args:        []string{"login", "-u", mockUsername, "--password-stdin", mockURI},
-			mockStandardInput: func(t *testing.T, input string) {
-				t.Helper()
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
 
-				require.Equal(t, mockPassword, input)
-			},
-			mockRun: func() error {
-				return nil
+				mockRunner.EXPECT().Run("docker", []string{"login", "-u", mockUsername, "--password-stdin", mockURI}, gomock.Any()).Return(nil)
 			},
+			want: nil,
 		},
 	}
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			test.setupMocks(controller)
 			s := Service{
-				createCommand: func(name string, args ...string) runnable {
-					require.Equal(t, test.commandName, name)
-					require.ElementsMatch(t, test.args, args)
-
-					return mockRunnable{
-						t:                 t,
-						mockRun:           test.mockRun,
-						mockStandardInput: test.mockStandardInput,
-					}
-				},
+				runner: mockRunner,
 			}
 
-			got := s.Login(mockURI, mockAuth)
+			got := s.Login(mockURI, mockUsername, mockPassword)
 
 			require.Equal(t, test.want, got)
 		})
@@ -153,43 +112,37 @@ func TestPush(t *testing.T) {
 	mockURI := "mockURI"
 	mockImageTag := "mockImageTag"
 
-	tests := map[string]struct {
-		commandName string
-		args        []string
+	var mockRunner *mocks.Mockrunner
 
-		mockRun func() error
+	tests := map[string]struct {
+		setupMocks func(controller *gomock.Controller)
 
 		want error
 	}{
 		"wrap error returned from Run()": {
-			commandName: "docker",
-			args:        []string{"push", imageName(mockURI, mockImageTag)},
-			mockRun: func() error {
-				return mockError
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
+
+				mockRunner.EXPECT().Run("docker", []string{"push", imageName(mockURI, mockImageTag)}).Return(mockError)
 			},
 			want: fmt.Errorf("docker push: %w", mockError),
 		},
 		"happy path": {
-			commandName: "docker",
-			args:        []string{"push", imageName(mockURI, mockImageTag)},
-			mockRun: func() error {
-				return nil
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = mocks.NewMockrunner(controller)
+
+				mockRunner.EXPECT().Run("docker", []string{"push", imageName(mockURI, mockImageTag)}).Return(nil)
 			},
+			want: nil,
 		},
 	}
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			test.setupMocks(controller)
 			s := Service{
-				createCommand: func(name string, args ...string) runnable {
-					require.Equal(t, test.commandName, name)
-					require.ElementsMatch(t, test.args, args)
-
-					return mockRunnable{
-						t:       t,
-						mockRun: test.mockRun,
-					}
-				},
+				runner: mockRunner,
 			}
 
 			got := s.Push(mockURI, mockImageTag)
diff --git a/internal/pkg/cli/app_deploy_test.go b/internal/pkg/cli/app_deploy_test.go
index afc8312a..5e7071ce 100644
--- a/internal/pkg/cli/app_deploy_test.go
+++ b/internal/pkg/cli/app_deploy_test.go
@@ -343,3 +343,168 @@ func TestDeployApp(t *testing.T) {
 		})
 	}
 }
+
+func TestGetAppDockerfilePath(t *testing.T) {
+	var mockWorkspace *mocks.MockWorkspace
+
+	mockError := errors.New("mockError")
+	mockManifestList := []string{
+		"appA",
+		"appB",
+	}
+	mockManifest := []byte(`name: appA
+type: 'Load Balanced Web App'
+image:
+  build: appA/Dockerfile
+`)
+
+	tests := map[string]struct {
+		inputApp   string
+		setupMocks func(controller *gomock.Controller)
+
+		wantPath string
+		wantErr  error
+	}{
+		"should wrap error returned from workspaceService ListManifestFiles()": {
+			setupMocks: func(controller *gomock.Controller) {
+				mockWorkspace = mocks.NewMockWorkspace(controller)
+
+				mockWorkspace.EXPECT().ListManifestFiles().Times(1).Return(nil, mockError)
+			},
+			wantPath: "",
+			wantErr:  fmt.Errorf("list local manifest files: %w", mockError),
+		},
+		"should return error if list of manifest files returned from workspaceService is empty": {
+			setupMocks: func(controller *gomock.Controller) {
+				mockWorkspace = mocks.NewMockWorkspace(controller)
+
+				mockWorkspace.EXPECT().ListManifestFiles().Times(1).Return([]string{}, nil)
+			},
+			wantPath: "",
+			wantErr:  errNoLocalManifestsFound,
+		},
+		"should return error if unable to match input app with local manifests": {
+			inputApp: "appC",
+			setupMocks: func(controller *gomock.Controller) {
+				mockWorkspace = mocks.NewMockWorkspace(controller)
+
+				mockWorkspace.EXPECT().ListManifestFiles().Times(1).Return(mockManifestList, nil)
+			},
+			wantPath: "",
+			wantErr:  fmt.Errorf("couldn't find local manifest %s", "appC"),
+		},
+		"should return error if workspaceService ReadFile returns error": {
+			inputApp: "appA",
+			setupMocks: func(controller *gomock.Controller) {
+				mockWorkspace = mocks.NewMockWorkspace(controller)
+
+				gomock.InOrder(
+					mockWorkspace.EXPECT().ListManifestFiles().Times(1).Return(mockManifestList, nil),
+					mockWorkspace.EXPECT().ReadFile("appA").Times(1).Return(nil, mockError),
+				)
+			},
+			wantPath: "",
+			wantErr:  fmt.Errorf("read manifest file %s: %w", "appA", mockError),
+		},
+		"should trim the manifest DockerfilePath if it contains /Dockerfile": {
+			inputApp: "appA",
+			setupMocks: func(controller *gomock.Controller) {
+				mockWorkspace = mocks.NewMockWorkspace(controller)
+
+				gomock.InOrder(
+					mockWorkspace.EXPECT().ListManifestFiles().Times(1).Return(mockManifestList, nil),
+					mockWorkspace.EXPECT().ReadFile("appA").Times(1).Return(mockManifest, nil),
+				)
+			},
+			wantPath: "appA",
+			wantErr:  nil,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+			test.setupMocks(ctrl)
+			opts := appDeployOpts{
+				app:              test.inputApp,
+				workspaceService: mockWorkspace,
+			}
+
+			gotPath, gotErr := opts.getAppDockerfilePath()
+
+			require.Equal(t, test.wantPath, gotPath)
+			require.Equal(t, test.wantErr, gotErr)
+		})
+	}
+}
+
+func TestSourceImageTag(t *testing.T) {
+	var mockRunner *climocks.Mockrunner
+	var mockPrompter *climocks.Mockprompter
+
+	mockError := errors.New("mockError")
+
+	tests := map[string]struct {
+		inputImageTag string
+
+		setupMocks func(controller *gomock.Controller)
+
+		wantErr      error
+		wantImageTag string
+	}{
+		"should return nil if input image tag is not empty": {
+			inputImageTag: "anythingreally",
+			setupMocks:    func(controller *gomock.Controller) {},
+			wantErr:       nil,
+			wantImageTag:  "anythingreally",
+		},
+		"should wrap error from prompting": {
+			inputImageTag: "",
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = climocks.NewMockrunner(controller)
+				mockPrompter = climocks.NewMockprompter(controller)
+
+				gomock.InOrder(
+					mockRunner.EXPECT().Run("git", []string{"describe", "--always"}, gomock.Any()).Times(1).Return(mockError),
+					mockPrompter.EXPECT().Get(inputImageTagPrompt, "", nil).Times(1).Return("", mockError),
+				)
+			},
+			wantErr:      fmt.Errorf("prompt for image tag: %w", mockError),
+			wantImageTag: "",
+		},
+		"should set opts imageTag to user input value": {
+			inputImageTag: "",
+			setupMocks: func(controller *gomock.Controller) {
+				mockRunner = climocks.NewMockrunner(controller)
+				mockPrompter = climocks.NewMockprompter(controller)
+
+				gomock.InOrder(
+					mockRunner.EXPECT().Run("git", []string{"describe", "--always"}, gomock.Any()).Times(1).Return(mockError),
+					mockPrompter.EXPECT().Get(inputImageTagPrompt, "", nil).Times(1).Return("youwotm8", nil),
+				)
+			},
+			wantErr:      nil,
+			wantImageTag: "youwotm8",
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			test.setupMocks(ctrl)
+			opts := &appDeployOpts{
+				GlobalOpts: &GlobalOpts{
+					prompt: mockPrompter,
+				},
+				imageTag: test.inputImageTag,
+				runner:   mockRunner,
+			}
+
+			got := opts.sourceImageTag()
+
+			require.Equal(t, test.wantErr, got)
+			require.Equal(t, test.wantImageTag, opts.imageTag)
+		})
+	}
+}
diff --git a/internal/pkg/cli/app_init_test.go b/internal/pkg/cli/app_init_test.go
index 6f197a98..94b7eaf1 100644
--- a/internal/pkg/cli/app_init_test.go
+++ b/internal/pkg/cli/app_init_test.go
@@ -23,7 +23,7 @@ func TestAppInitOpts_Ask(t *testing.T) {
 	const (
 		wantedAppType        = manifest.LoadBalancedWebApplication
 		wantedAppName        = "frontend"
-		wantedDockerfilePath = "./frontend"
+		wantedDockerfilePath = "frontend"
 	)
 	testCases := map[string]struct {
 		inAppType        string
@@ -32,6 +32,8 @@ func TestAppInitOpts_Ask(t *testing.T) {
 
 		mockFileSystem func(mockFS afero.Fs)
 		mockPrompt     func(m *climocks.Mockprompter)
+
+		wantedErr error
 	}{
 		"prompt for app type": {
 			inAppType:        "",
@@ -43,6 +45,19 @@ func TestAppInitOpts_Ask(t *testing.T) {
 				m.EXPECT().SelectOne(gomock.Eq("Which type of infrastructure pattern best represents your application?"), gomock.Any(), gomock.Eq(manifest.AppTypes)).
 					Return(wantedAppType, nil)
 			},
+			wantedErr: nil,
+		},
+		"return an error if fail to get app type": {
+			inAppType:        "",
+			inAppName:        wantedAppName,
+			inDockerfilePath: wantedDockerfilePath,
+
+			mockFileSystem: func(mockFS afero.Fs) {},
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().SelectOne(gomock.Eq("Which type of infrastructure pattern best represents your application?"), gomock.Any(), gomock.Eq(manifest.AppTypes)).
+					Return("", errors.New("some error"))
+			},
+			wantedErr: fmt.Errorf("failed to get type selection: some error"),
 		},
 		"prompt for app name": {
 			inAppType:        wantedAppType,
@@ -54,6 +69,19 @@ func TestAppInitOpts_Ask(t *testing.T) {
 				m.EXPECT().Get(gomock.Eq("What do you want to call this Load Balanced Web App?"), gomock.Any(), gomock.Any()).
 					Return(wantedAppName, nil)
 			},
+			wantedErr: nil,
+		},
+		"returns an error if fail to get application name": {
+			inAppType:        wantedAppType,
+			inAppName:        "",
+			inDockerfilePath: wantedDockerfilePath,
+
+			mockFileSystem: func(mockFS afero.Fs) {},
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Get(gomock.Eq("What do you want to call this Load Balanced Web App?"), gomock.Any(), gomock.Any()).
+					Return("", errors.New("some error"))
+			},
+			wantedErr: fmt.Errorf("failed to get application name: some error"),
 		},
 		"choose an existing Dockerfile": {
 			inAppType:        wantedAppType,
@@ -75,8 +103,44 @@ func TestAppInitOpts_Ask(t *testing.T) {
 						"backend/Dockerfile",
 						"frontend/Dockerfile",
 					})).
-					Return(wantedDockerfilePath, nil)
+					Return("frontend/Dockerfile", nil)
+			},
+			wantedErr: nil,
+		},
+		"returns an error if fail to find Dockerfiles": {
+			inAppType:        wantedAppType,
+			inAppName:        wantedAppName,
+			inDockerfilePath: "",
+
+			mockFileSystem: func(mockFS afero.Fs) {},
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().SelectOne(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
 			},
+			wantedErr: fmt.Errorf("no Dockerfiles found within . or a sub-directory level below"),
+		},
+		"returns an error if fail to select Dockerfile": {
+			inAppType:        wantedAppType,
+			inAppName:        wantedAppName,
+			inDockerfilePath: "",
+
+			mockFileSystem: func(mockFS afero.Fs) {
+				mockFS.MkdirAll("frontend", 0755)
+				mockFS.MkdirAll("backend", 0755)
+
+				afero.WriteFile(mockFS, "Dockerfile", []byte("FROM nginx"), 0644)
+				afero.WriteFile(mockFS, "frontend/Dockerfile", []byte("FROM nginx"), 0644)
+				afero.WriteFile(mockFS, "backend/Dockerfile", []byte("FROM nginx"), 0644)
+			},
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().SelectOne(gomock.Eq("Which Dockerfile would you like to use for frontend app?"), gomock.Any(), gomock.Eq(
+					[]string{
+						"./Dockerfile",
+						"backend/Dockerfile",
+						"frontend/Dockerfile",
+					})).
+					Return("", errors.New("some error"))
+			},
+			wantedErr: fmt.Errorf("failed to select Dockerfile: some error"),
 		},
 	}
 
@@ -101,12 +165,17 @@ func TestAppInitOpts_Ask(t *testing.T) {
 			tc.mockPrompt(mockPrompt)
 
 			// WHEN
-			opts.Ask()
+			err := opts.Ask()
 
 			// THEN
-			require.Equal(t, wantedAppType, opts.AppType)
-			require.Equal(t, wantedAppName, opts.AppName)
-			require.Equal(t, wantedDockerfilePath, opts.DockerfilePath)
+			if tc.wantedErr != nil {
+				require.EqualError(t, err, tc.wantedErr.Error())
+			} else {
+				require.Nil(t, err)
+				require.Equal(t, wantedAppType, opts.AppType)
+				require.Equal(t, wantedAppName, opts.AppName)
+				require.Equal(t, wantedDockerfilePath, opts.DockerfilePath)
+			}
 		})
 	}
 }
@@ -129,18 +198,28 @@ func TestAppInitOpts_Validate(t *testing.T) {
 			inAppName: "1234",
 			wantedErr: fmt.Errorf("application name 1234 is invalid: %s", errValueBadFormat),
 		},
-		"invalid dockerfile path": {
-			inDockerfilePath: "./hello/Dockerfile",
-			wantedErr:        errors.New("open hello/Dockerfile: file does not exist"),
+		"invalid dockerfile directory path": {
+			inDockerfilePath: "./hello",
+			wantedErr:        errors.New("read directory: open hello: file does not exist"),
 		},
 		"invalid project name": {
 			inProjectName: "",
 			wantedErr:     errNoProjectInWorkspace,
 		},
+		"valid dockerfile directory with no dockerfile": {
+			inAppName:        "frontend",
+			inAppType:        "Load Balanced Web App",
+			inDockerfilePath: "./hello",
+			inProjectName:    "phonetool",
+			mockFileSystem: func(mockFS afero.Fs) {
+				mockFS.MkdirAll("hello", 0755)
+			},
+			wantedErr: errors.New("no Dockerfiles found within ./hello or a sub-directory level below"),
+		},
 		"valid flags": {
 			inAppName:        "frontend",
 			inAppType:        "Load Balanced Web App",
-			inDockerfilePath: "./hello/Dockerfile",
+			inDockerfilePath: "./hello",
 			inProjectName:    "phonetool",
 
 			mockFileSystem: func(mockFS afero.Fs) {
diff --git a/internal/pkg/cli/dockerfile_test.go b/internal/pkg/cli/dockerfile_test.go
new file mode 100644
index 00000000..3aa741aa
--- /dev/null
+++ b/internal/pkg/cli/dockerfile_test.go
@@ -0,0 +1,51 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidateDockerfiles(t *testing.T) {
+	wantedDockerfiles := []string{"./Dockerfile", "backend/Dockerfile", "frontend/Dockerfile"}
+	testCases := map[string]struct {
+		mockFileSystem func(mockFS afero.Fs)
+		err            error
+	}{
+		"find Dockerfiles": {
+			mockFileSystem: func(mockFS afero.Fs) {
+				mockFS.MkdirAll("frontend", 0755)
+				mockFS.MkdirAll("backend", 0755)
+
+				afero.WriteFile(mockFS, "Dockerfile", []byte("FROM nginx"), 0644)
+				afero.WriteFile(mockFS, "frontend/Dockerfile", []byte("FROM nginx"), 0644)
+				afero.WriteFile(mockFS, "backend/Dockerfile", []byte("FROM nginx"), 0644)
+			},
+			err: nil,
+		},
+		"no Dockerfiles": {
+			mockFileSystem: func(mockFS afero.Fs) {},
+			err:            fmt.Errorf("no Dockerfiles found within . or a sub-directory level below"),
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			fs := &afero.Afero{Fs: afero.NewMemMapFs()}
+			tc.mockFileSystem(fs)
+			got, err := listDockerfiles(fs, ".")
+
+			if tc.err != nil {
+				require.EqualError(t, err, tc.err.Error())
+			} else {
+				require.Nil(t, err)
+				require.Equal(t, wantedDockerfiles, got)
+			}
+		})
+	}
+}
diff --git a/internal/pkg/cli/pipeline_init_test.go b/internal/pkg/cli/pipeline_init_test.go
index 1796428c..22ba46d7 100644
--- a/internal/pkg/cli/pipeline_init_test.go
+++ b/internal/pkg/cli/pipeline_init_test.go
@@ -7,33 +7,40 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path/filepath"
 	"testing"
 
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	climocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/mocks"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store/secretsmanager"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
 	archermocks "github.com/aws/amazon-ecs-cli-v2/mocks"
 
 	"github.com/gobuffalo/packd"
 	"github.com/golang/mock/gomock"
+	"github.com/spf13/afero"
 	"github.com/stretchr/testify/require"
 )
 
 const githubRepoURL = "https://github.com/badGoose/chaOS.git"
 const githubRepoName = "https://github.com/badGoose/chaOS"
 const githubToken = "hunter2"
+const githubBranch = "dev"
 
 func TestInitPipelineOpts_Ask(t *testing.T) {
 	testCases := map[string]struct {
 		inEnvironments      []string
 		inGitHubRepo        string
 		inGitHubAccessToken string
+		inGitHubBranch      string
 		inProjectEnvs       []string
 
 		mockPrompt func(m *climocks.Mockprompter)
 
 		expectedGitHubRepo        string
 		expectedGitHubAccessToken string
+		expectedGitHubBranch      string
 		expectedEnvironments      []string
 		expectedError             error
 	}{
@@ -41,6 +48,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
 			inEnvironments:      []string{},
 			inGitHubRepo:        "",
 			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
 			inProjectEnvs:       []string{"test", "prod"},
 
 			mockPrompt: func(m *climocks.Mockprompter) {
@@ -50,14 +58,120 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
 				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"prod"}).Return("prod", nil).Times(1)
 
 				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubRepoPrompt), gomock.Any(), gomock.Any()).Return(githubRepoURL, nil).Times(1)
+				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubBranchPrompt), gomock.Any(), gomock.Any(), gomock.Any()).Return(githubBranch, nil).Times(1)
 				m.EXPECT().GetSecret(gomock.Eq("Please enter your GitHub Personal Access Token for your repository: https://github.com/badGoose/chaOS"), gomock.Any()).Return(githubToken, nil).Times(1)
 			},
 
 			expectedGitHubRepo:        githubRepoName,
 			expectedGitHubAccessToken: githubToken,
+			expectedGitHubBranch:      githubBranch,
 			expectedEnvironments:      []string{"test", "prod"},
 			expectedError:             nil,
 		},
+		"returns error if fail to confirm adding environment": {
+			inEnvironments:      []string{},
+			inGitHubRepo:        "",
+			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
+			inProjectEnvs:       []string{"test", "prod"},
+
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Confirm(pipelineAddEnvPrompt, gomock.Any()).Return(false, errors.New("some error")).Times(1)
+			},
+
+			expectedGitHubRepo:        "",
+			expectedGitHubAccessToken: "",
+			expectedGitHubBranch:      "",
+			expectedEnvironments:      []string{},
+			expectedError:             fmt.Errorf("confirm adding an environment: some error"),
+		},
+		"returns error if fail to add an environment": {
+			inEnvironments:      []string{},
+			inGitHubRepo:        "",
+			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
+			inProjectEnvs:       []string{"test", "prod"},
+
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Confirm(pipelineAddEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"test", "prod"}).Return("", errors.New("some error")).Times(1)
+			},
+
+			expectedGitHubRepo:        "",
+			expectedGitHubAccessToken: "",
+			expectedGitHubBranch:      "",
+			expectedEnvironments:      []string{},
+			expectedError:             fmt.Errorf("failed to add environment: some error"),
+		},
+		"returns error if fail to get GitHub repo": {
+			inEnvironments:      []string{},
+			inGitHubRepo:        "",
+			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
+			inProjectEnvs:       []string{"test", "prod"},
+
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Confirm(pipelineAddEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().Confirm(pipelineAddMoreEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"test", "prod"}).Return("test", nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"prod"}).Return("prod", nil).Times(1)
+
+				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubRepoPrompt), gomock.Any(), gomock.Any()).Return("", errors.New("some error")).Times(1)
+			},
+
+			expectedGitHubRepo:        "",
+			expectedGitHubAccessToken: "",
+			expectedGitHubBranch:      "",
+			expectedEnvironments:      []string{},
+			expectedError:             fmt.Errorf("failed to get GitHub repository: some error"),
+		},
+		"returns error if fail to get GitHub access token": {
+			inEnvironments:      []string{},
+			inGitHubRepo:        "",
+			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
+			inProjectEnvs:       []string{"test", "prod"},
+
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Confirm(pipelineAddEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().Confirm(pipelineAddMoreEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"test", "prod"}).Return("test", nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"prod"}).Return("prod", nil).Times(1)
+
+				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubRepoPrompt), gomock.Any(), gomock.Any()).Return(githubRepoURL, nil).Times(1)
+				m.EXPECT().GetSecret(gomock.Eq("Please enter your GitHub Personal Access Token for your repository: https://github.com/badGoose/chaOS"), gomock.Any()).Return("", errors.New("some error")).Times(1)
+			},
+
+			expectedGitHubRepo:        githubRepoName,
+			expectedGitHubAccessToken: "",
+			expectedGitHubBranch:      "",
+			expectedEnvironments:      []string{},
+			expectedError:             fmt.Errorf("failed to get GitHub access token: some error"),
+		},
+		"returns error if fail to get GitHub branch name": {
+			inEnvironments:      []string{},
+			inGitHubRepo:        "",
+			inGitHubAccessToken: "",
+			inGitHubBranch:      "",
+			inProjectEnvs:       []string{"test", "prod"},
+
+			mockPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().Confirm(pipelineAddEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().Confirm(pipelineAddMoreEnvPrompt, gomock.Any()).Return(true, nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"test", "prod"}).Return("test", nil).Times(1)
+				m.EXPECT().SelectOne(pipelineSelectEnvPrompt, gomock.Any(), []string{"prod"}).Return("prod", nil).Times(1)
+
+				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubRepoPrompt), gomock.Any(), gomock.Any()).Return(githubRepoURL, nil).Times(1)
+				m.EXPECT().Get(gomock.Eq(pipelineEnterGitHubBranchPrompt), gomock.Any(), gomock.Any(), gomock.Any()).Return("", errors.New("some error")).Times(1)
+				m.EXPECT().GetSecret(gomock.Eq("Please enter your GitHub Personal Access Token for your repository: https://github.com/badGoose/chaOS"), gomock.Any()).Return(githubToken, nil).Times(1)
+			},
+
+			expectedGitHubRepo:        githubRepoName,
+			expectedGitHubAccessToken: githubToken,
+			expectedGitHubBranch:      "",
+			expectedEnvironments:      []string{"test", "prod"},
+			expectedError:             fmt.Errorf("failed to get GitHub branch name: some error"),
+		},
 	}
 
 	for name, tc := range testCases {
@@ -72,6 +186,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
 				Environments:      tc.inEnvironments,
 				GitHubRepo:        tc.inGitHubRepo,
 				GitHubAccessToken: tc.inGitHubAccessToken,
+				GitHubBranch:      tc.inGitHubBranch,
 
 				projectEnvs: tc.inProjectEnvs,
 
@@ -87,12 +202,12 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
 
 			// THEN
 			if tc.expectedError != nil {
-				require.Equal(t, tc.expectedError, err)
+				require.EqualError(t, err, tc.expectedError.Error())
 			} else {
 				require.Nil(t, err)
 				require.Equal(t, tc.expectedGitHubRepo, opts.GitHubRepo)
-				require.Equal(t, tc.expectedGitHubRepo, opts.GitHubRepo)
 				require.Equal(t, tc.expectedGitHubAccessToken, opts.GitHubAccessToken)
+				require.Equal(t, tc.expectedGitHubBranch, opts.GitHubBranch)
 				require.ElementsMatch(t, tc.expectedEnvironments, opts.Environments)
 			}
 		})
@@ -144,100 +259,201 @@ func TestInitPipelineOpts_Validate(t *testing.T) {
 	}
 }
 
+func genApps(names ...string) []archer.Manifest {
+	result := make([]archer.Manifest, 0, len(names))
+	for _, name := range names {
+		result = append(result, &manifest.LBFargateManifest{
+			AppManifest: manifest.AppManifest{
+				Name: name,
+				Type: manifest.LoadBalancedWebApplication,
+			},
+			Image: manifest.ImageWithPort{
+				AppImage: manifest.AppImage{
+					Build: name,
+				},
+			},
+		})
+	}
+	return result
+}
+
 func TestInitPipelineOpts_Execute(t *testing.T) {
+	const expectedIntegTestBuildSpecTemplate = "integrationTests"
+	mockApps := genApps("app01", "app02")
+
 	testCases := map[string]struct {
 		inEnvironments []string
 		inGitHubToken  string
 		inGitHubRepo   string
+		inGitHubBranch string
 		inProjectName  string
 
 		mockSecretsManager func(m *archermocks.MockSecretsManager)
-		mockManifestWriter func(m *archermocks.MockManifestIO)
+		mockManifestWriter func(m *archermocks.MockWorkspace)
 		mockBox            func(box *packd.MemoryBox)
+		mockFileSystem     func(mockFS afero.Fs)
 
-		expectedSecretName    string
-		expectManifestPath    string
-		expectedBuildspecPath string
-		expectedError         error
+		expectedSecretName              string
+		expectManifestPath              string
+		expectedBuildspecPath           string
+		expectedIntegTestBuildspecPaths []string
+		expectedError                   error
 	}{
-		"creates secret and writes manifests": {
+		"creates secret and writes manifest and buildspecs": {
 			inEnvironments: []string{"test"},
 			inGitHubToken:  "hunter2",
 			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inGitHubBranch: githubBranch,
 			inProjectName:  "badgoose",
 
 			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
 				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("some-arn", nil)
 			},
-			mockManifestWriter: func(m *archermocks.MockManifestIO) {
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(mockApps, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.PipelineFileName).Return(workspace.PipelineFileName, nil)
 				m.EXPECT().WriteFile([]byte("hello"), workspace.BuildspecFileName).Return(workspace.BuildspecFileName, nil)
 			},
 			mockBox: func(m *packd.MemoryBox) {
 				m.AddString(buildspecTemplatePath, "hello")
+				m.AddString(integTestBuildspecTemplatePath, expectedIntegTestBuildSpecTemplate)
+			},
+			mockFileSystem: func(mockFS afero.Fs) {
+				for _, app := range mockApps {
+					mockFS.MkdirAll(filepath.Dir(app.IntegTestBuildspecPath()), 0755)
+				}
 			},
 			expectedSecretName:    "github-token-badgoose-goose",
 			expectManifestPath:    workspace.PipelineFileName,
 			expectedBuildspecPath: workspace.BuildspecFileName,
-			expectedError:         nil,
+			expectedIntegTestBuildspecPaths: func() []string {
+				expectedPaths := make([]string, 0, len(mockApps))
+				for _, app := range mockApps {
+					expectedPaths = append(expectedPaths, app.IntegTestBuildspecPath())
+				}
+				return expectedPaths
+			}(),
+			expectedError: nil,
 		},
 		"does not return an error if secret already exists": {
 			inEnvironments: []string{"test"},
 			inGitHubToken:  "hunter2",
 			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inGitHubBranch: githubBranch,
 			inProjectName:  "badgoose",
 
 			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
 				existsErr := &secretsmanager.ErrSecretAlreadyExists{}
 				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("", existsErr)
 			},
-			mockManifestWriter: func(m *archermocks.MockManifestIO) {
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(mockApps, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.PipelineFileName).Return(workspace.PipelineFileName, nil)
 				m.EXPECT().WriteFile([]byte("hello"), workspace.BuildspecFileName).Return(workspace.BuildspecFileName, nil)
 			},
 			mockBox: func(m *packd.MemoryBox) {
 				m.AddString(buildspecTemplatePath, "hello")
+				m.AddString(integTestBuildspecTemplatePath, expectedIntegTestBuildSpecTemplate)
+			},
+			mockFileSystem: func(mockFS afero.Fs) {
+				for _, app := range mockApps {
+					mockFS.MkdirAll(filepath.Dir(app.IntegTestBuildspecPath()), 0755)
+				}
 			},
 
 			expectedSecretName:    "github-token-badgoose-goose",
 			expectManifestPath:    workspace.PipelineFileName,
 			expectedBuildspecPath: workspace.BuildspecFileName,
-			expectedError:         nil,
+			expectedIntegTestBuildspecPaths: func() []string {
+				expectedPaths := make([]string, 0, len(mockApps))
+				for _, app := range mockApps {
+					expectedPaths = append(expectedPaths, app.IntegTestBuildspecPath())
+				}
+				return expectedPaths
+			}(),
+			expectedError: nil,
 		},
 		"returns an error if buildspec template does not exist": {
 			inEnvironments: []string{"test"},
 			inGitHubToken:  "hunter2",
 			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inGitHubBranch: githubBranch,
 			inProjectName:  "badgoose",
 
 			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
 				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("some-arn", nil)
 			},
-			mockManifestWriter: func(m *archermocks.MockManifestIO) {
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(mockApps, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.PipelineFileName).Return(workspace.PipelineFileName, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.BuildspecFileName).Times(0)
 			},
 			mockBox: func(m *packd.MemoryBox) {
+				m.AddString(integTestBuildspecTemplatePath, expectedIntegTestBuildSpecTemplate)
 			},
-			expectedError: fmt.Errorf("find template for %s: %w", buildspecTemplatePath, os.ErrNotExist),
+			mockFileSystem: func(mockFS afero.Fs) {},
+			expectedError:  fmt.Errorf("find template for %s: %w", buildspecTemplatePath, os.ErrNotExist),
+		},
+		"returns an error if integ test buildspec template does not exist": {
+			inEnvironments: []string{"test"},
+			inGitHubToken:  "hunter2",
+			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inProjectName:  "badgoose",
+
+			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
+				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("some-arn", nil)
+			},
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(mockApps, nil)
+				m.EXPECT().WriteFile(gomock.Any(), workspace.PipelineFileName).Return(workspace.PipelineFileName, nil)
+				m.EXPECT().WriteFile([]byte("hello"), workspace.BuildspecFileName).Return(workspace.BuildspecFileName, nil)
+			},
+			mockBox: func(m *packd.MemoryBox) {
+				m.AddString(buildspecTemplatePath, "hello")
+				// intentionally miss out the integration test template here
+			},
+			mockFileSystem: func(mockFS afero.Fs) {},
+			expectedError:  fmt.Errorf("find integration test template for %s: %w", integTestBuildspecTemplatePath, os.ErrNotExist),
 		},
 		"returns an error if can't write buildspec": {
 			inEnvironments: []string{"test"},
 			inGitHubToken:  "hunter2",
 			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inGitHubBranch: githubBranch,
 			inProjectName:  "badgoose",
 
 			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
 				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("some-arn", nil)
 			},
-			mockManifestWriter: func(m *archermocks.MockManifestIO) {
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(mockApps, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.PipelineFileName).Return(workspace.PipelineFileName, nil)
 				m.EXPECT().WriteFile(gomock.Any(), workspace.BuildspecFileName).Return("", errors.New("some error"))
 			},
 			mockBox: func(m *packd.MemoryBox) {
 				m.AddString(buildspecTemplatePath, "hello")
+				m.AddString(integTestBuildspecTemplatePath, expectedIntegTestBuildSpecTemplate)
 			},
-			expectedError: fmt.Errorf("write file %s to workspace: some error", workspace.BuildspecFileName),
+			mockFileSystem: func(mockFS afero.Fs) {},
+			expectedError:  fmt.Errorf("write file %s to workspace: some error", workspace.BuildspecFileName),
+		},
+		"returns an error when retrieving local apps": {
+			inEnvironments: []string{"test"},
+			inGitHubToken:  "hunter2",
+			inGitHubRepo:   "https://github.com/badgoose/goose",
+			inProjectName:  "badgoose",
+
+			mockSecretsManager: func(m *archermocks.MockSecretsManager) {
+				m.EXPECT().CreateSecret("github-token-badgoose-goose", "hunter2").Return("some-arn", nil)
+			},
+			mockManifestWriter: func(m *archermocks.MockWorkspace) {
+				m.EXPECT().Apps().Return(nil, errors.New("some error"))
+			},
+			mockBox: func(m *packd.MemoryBox) {
+				m.AddString(buildspecTemplatePath, "hello")
+			},
+			mockFileSystem: func(mockFS afero.Fs) {},
+			expectedError:  errors.New("could not retrieve apps in this workspace: some error"),
 		},
 	}
 
@@ -248,21 +464,24 @@ func TestInitPipelineOpts_Execute(t *testing.T) {
 			defer ctrl.Finish()
 
 			mockSecretsManager := archermocks.NewMockSecretsManager(ctrl)
-			mockWriter := archermocks.NewMockManifestIO(ctrl)
+			mockWriter := archermocks.NewMockWorkspace(ctrl)
 			mockBox := packd.NewMemoryBox()
 
 			tc.mockSecretsManager(mockSecretsManager)
 			tc.mockManifestWriter(mockWriter)
 			tc.mockBox(mockBox)
+			memFs := &afero.Afero{Fs: afero.NewMemMapFs()}
+			tc.mockFileSystem(memFs)
 
 			opts := &InitPipelineOpts{
 				Environments:      tc.inEnvironments,
 				GitHubRepo:        tc.inGitHubRepo,
 				GitHubAccessToken: tc.inGitHubToken,
-
-				secretsmanager: mockSecretsManager,
-				workspace:      mockWriter,
-				box:            mockBox,
+				GitHubBranch:      tc.inGitHubBranch,
+				secretsmanager:    mockSecretsManager,
+				workspace:         mockWriter,
+				box:               mockBox,
+				fsUtils:           memFs,
 
 				GlobalOpts: &GlobalOpts{projectName: tc.inProjectName},
 			}
@@ -278,6 +497,7 @@ func TestInitPipelineOpts_Execute(t *testing.T) {
 				require.Equal(t, tc.expectedSecretName, opts.secretName)
 				require.Equal(t, tc.expectManifestPath, opts.manifestPath)
 				require.Equal(t, tc.expectedBuildspecPath, opts.buildspecPath)
+				require.Equal(t, tc.expectedIntegTestBuildspecPaths, opts.integTestBuildspecPaths)
 			}
 		})
 	}
diff --git a/internal/pkg/cli/pipeline_update_test.go b/internal/pkg/cli/pipeline_update_test.go
index 9c910f4a..5ed91d16 100644
--- a/internal/pkg/cli/pipeline_update_test.go
+++ b/internal/pkg/cli/pipeline_update_test.go
@@ -6,6 +6,7 @@ package cli
 import (
 	"errors"
 	"fmt"
+	"path/filepath"
 	"testing"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
@@ -33,6 +34,19 @@ func TestUpdatePipelineOpts_convertStages(t *testing.T) {
 			stages: []manifest.PipelineStage{
 				{
 					Name: "test",
+					Apps: map[string]manifest.App{
+						"frontend": manifest.App{
+							IntegTestBuildspecPath: filepath.Join("frontend", manifest.IntegTestBuildspecFileName),
+						},
+					},
+				},
+				{
+					Name: "gamma",
+					Apps: map[string]manifest.App{
+						"backend": manifest.App{
+							IntegTestBuildspecPath: filepath.Join("backend", manifest.IntegTestBuildspecFileName),
+						},
+					},
 				},
 			},
 			inProjectName: "badgoose",
@@ -42,23 +56,38 @@ func TestUpdatePipelineOpts_convertStages(t *testing.T) {
 						AppManifest: manifest.AppManifest{
 							Name: "frontend",
 						},
+						Image: manifest.ImageWithPort{
+							AppImage: manifest.AppImage{
+								Build: "frontend",
+							},
+						},
 					},
 					&manifest.LBFargateManifest{
 						AppManifest: manifest.AppManifest{
 							Name: "backend",
 						},
+						Image: manifest.ImageWithPort{
+							AppImage: manifest.AppImage{
+								Build: "backend",
+							},
+						},
 					}}, nil).Times(1)
 			},
 			mockEnvStore: func(m *archermocks.MockEnvironmentStore) {
-				mockEnv := &archer.Environment{
+				m.EXPECT().GetEnvironment("badgoose", "test").Return(&archer.Environment{
 					Name:      "test",
 					Project:   "badgoose",
 					Region:    "us-west-2",
 					AccountID: "123456789012",
 					Prod:      false,
-				}
-
-				m.EXPECT().GetEnvironment("badgoose", "test").Return(mockEnv, nil).Times(1)
+				}, nil)
+				m.EXPECT().GetEnvironment("badgoose", "gamma").Return(&archer.Environment{
+					Name:      "gamma",
+					Project:   "badgoose",
+					Region:    "us-west-2",
+					AccountID: "123456789012",
+					Prod:      false,
+				}, nil).Times(1)
 			},
 
 			expectedStages: []deploy.PipelineStage{
@@ -69,7 +98,32 @@ func TestUpdatePipelineOpts_convertStages(t *testing.T) {
 						AccountID: "123456789012",
 						Prod:      false,
 					},
-					LocalApplications: []string{"frontend", "backend"},
+					LocalApplications: []deploy.AppInStage{
+						{
+							Name:                   "frontend",
+							IntegTestBuildspecPath: filepath.Join("frontend", manifest.IntegTestBuildspecFileName),
+						},
+						{
+							Name: "backend",
+						},
+					},
+				},
+				{
+					AssociatedEnvironment: &deploy.AssociatedEnvironment{
+						Name:      "gamma",
+						Region:    "us-west-2",
+						AccountID: "123456789012",
+						Prod:      false,
+					},
+					LocalApplications: []deploy.AppInStage{
+						{
+							Name: "frontend",
+						},
+						{
+							Name:                   "backend",
+							IntegTestBuildspecPath: filepath.Join("backend", manifest.IntegTestBuildspecFileName),
+						},
+					},
 				},
 			},
 			expectedError: nil,
diff --git a/internal/pkg/cli/validate_test.go b/internal/pkg/cli/validate_test.go
index 6b08c7ed..3d3eb65e 100644
--- a/internal/pkg/cli/validate_test.go
+++ b/internal/pkg/cli/validate_test.go
@@ -103,36 +103,36 @@ func TestValidateEnvironmentName(t *testing.T) {
 
 func TestIsCorrectFormat(t *testing.T) {
 	testCases := map[string]struct {
-		input string
+		input   string
 		isLegit bool
 	}{
 		"numbers only input": {
-			input: "1234",
-			isLegit:  false,
+			input:   "1234",
+			isLegit: false,
 		},
 		"lower-case alphabetic input only": {
-			input: "badgoose",
-			isLegit:  true,
+			input:   "badgoose",
+			isLegit: true,
 		},
 		"alphanumeric string input": {
-			input: "abc123",
-			isLegit:  true,
+			input:   "abc123",
+			isLegit: true,
 		},
 		"contains hyphen": {
-			input: "bad-goose",
-			isLegit:  true,
+			input:   "bad-goose",
+			isLegit: true,
 		},
 		"non-alphanumeric string input": {
-			input: "bad-goose!",
-			isLegit:  false,
+			input:   "bad-goose!",
+			isLegit: false,
 		},
 		"starts with non-letter": {
-			input: "1bad-goose",
-			isLegit:  false,
+			input:   "1bad-goose",
+			isLegit: false,
 		},
 		"contains capital letter": {
-			input: "badGoose",
-			isLegit:  false,
+			input:   "badGoose",
+			isLegit: false,
 		},
 	}
 
@@ -148,19 +148,19 @@ func TestIsCorrectFormat(t *testing.T) {
 func TestValidateGitHubRepo(t *testing.T) {
 	testCases := map[string]struct {
 		input string
-		err error
+		err   error
 	}{
 		"full url": {
 			input: "https://github.com/badgoose/chaos",
-			err:  nil,
+			err:   nil,
 		},
 		"owner and repo only": {
 			input: "badgoose/chaos",
-			err:  nil,
+			err:   nil,
 		},
 		"invalid repo": {
 			input: "THEGOOSE",
-			err: errInvalidGitHubRepo,
+			err:   errInvalidGitHubRepo,
 		},
 	}
 
@@ -172,4 +172,3 @@ func TestValidateGitHubRepo(t *testing.T) {
 		})
 	}
 }
-
diff --git a/internal/pkg/deploy/cloudformation/pipeline_integration_test.go b/internal/pkg/deploy/cloudformation/pipeline_integration_test.go
index df3e372b..1e3f9923 100644
--- a/internal/pkg/deploy/cloudformation/pipeline_integration_test.go
+++ b/internal/pkg/deploy/cloudformation/pipeline_integration_test.go
@@ -8,6 +8,7 @@ package cloudformation_test
 import (
 	"errors"
 	"fmt"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -15,6 +16,8 @@ import (
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/endpoints"
 	awsCF "github.com/aws/aws-sdk-go/service/cloudformation"
+	"github.com/aws/aws-sdk-go/service/secretsmanager"
+	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
 	"github.com/stretchr/testify/require"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
@@ -31,8 +34,11 @@ func TestPipelineCreation(t *testing.T) {
 	projectCallerInfo, err := projectId.Get()
 	require.NoError(t, err)
 	projectDeployer := cloudformation.New(projectSess)
+	sm := secretsmanager.New(projectSess)
+	secretId := "testGitHubSecret" + randStringBytes(10)
 
 	t.Run("creates a cross-region pipeline in a region with no environment", func(t *testing.T) {
+		createMockSecret(t, sm, secretId)
 		projCfClient := awsCF.New(projectSess)
 
 		const pipelineName = "pipepiper"
@@ -120,6 +126,8 @@ func TestPipelineCreation(t *testing.T) {
 				StackName: aws.String(envStackName),
 			})
 			require.NoError(t, err)
+
+			deleteMockSecretImmediately(t, sm, secretId)
 		}()
 
 		// Given both the project stack and env we are deploying to do not
@@ -182,18 +190,56 @@ func TestPipelineCreation(t *testing.T) {
 						AccountID: project.AccountID,
 						Prod:      true,
 					},
-					LocalApplications: []string{"frontend", "backend"},
+					LocalApplications: []deploy.AppInStage{
+						{
+							Name:                   "frontend",
+							IntegTestBuildspecPath: filepath.Join("frontend", manifest.IntegTestBuildspecFileName),
+						},
+						{
+							Name: "backend",
+						},
+					},
 				},
 			},
 			ArtifactBuckets: artifactBuckets,
 		}
-		require.NoError(t, projectDeployer.DeployPipeline(pipelineInput))
+		require.NoError(t, projectDeployer.CreatePipeline(pipelineInput))
 
 		// Ensure that the new stack exists
 		assertStackExists(t, projCfClient, pipelineStackName)
 	})
 }
 
+func createMockSecret(t *testing.T, sm secretsmanageriface.SecretsManagerAPI, secretId string) {
+	_, err := sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
+		SecretId: aws.String(secretId),
+	})
+	if err != nil {
+		if awsErr, ok := err.(awserr.Error); ok {
+			switch awsErr.Code() {
+			case secretsmanager.ErrCodeResourceNotFoundException:
+				_, err = sm.CreateSecret(&secretsmanager.CreateSecretInput{
+					Name:         aws.String(secretId),
+					SecretString: aws.String("dontCare"),
+				})
+				require.NoError(t, err, "CreateSecret should not return an error")
+				return
+			default:
+				require.Fail(t, "GetSecretValue failed: %w", awsErr)
+			}
+		}
+		require.Fail(t, "GetSecretValue failed: %w", err)
+	}
+}
+
+func deleteMockSecretImmediately(t *testing.T, sm secretsmanageriface.SecretsManagerAPI, secretId string) {
+	_, err := sm.DeleteSecret(&secretsmanager.DeleteSecretInput{
+		SecretId:                   aws.String(secretId),
+		ForceDeleteWithoutRecovery: aws.Bool(true),
+	})
+	require.NoError(t, err, "DeleteSecret should not return an error")
+}
+
 func assertStackDoesNotExist(t *testing.T, cfClient *awsCF.CloudFormation, stackName string) {
 	_, err := cfClient.DescribeStacks(&awsCF.DescribeStacksInput{
 		StackName: aws.String(stackName),
diff --git a/internal/pkg/deploy/cloudformation/stack/pipeline_test.go b/internal/pkg/deploy/cloudformation/stack/pipeline_test.go
index 0d60c034..29805601 100644
--- a/internal/pkg/deploy/cloudformation/stack/pipeline_test.go
+++ b/internal/pkg/deploy/cloudformation/stack/pipeline_test.go
@@ -6,6 +6,7 @@ package stack
 import (
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 	"testing"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -13,6 +14,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 )
 
 const (
@@ -66,6 +68,18 @@ func TestPipelineTemplateRendering(t *testing.T) {
 	require.Equal(t, string(expectedTemplate), string(tmpl), "the rendered template differs from the expected")
 }
 
+func TestPipelineTemplateRenderingWithNoApplication(t *testing.T) {
+	expectedTemplate, err := ioutil.ReadFile("testdata/rendered_pipeline_with_no_application_cfn_template.yml")
+	require.NoError(t, err, "expected template can not be read")
+
+	pipeline := NewPipelineStackConfig(
+		mockCreatePipelineInputWithNoApplication(),
+	)
+	tmpl, err := pipeline.Template()
+	require.NoError(t, err, "template serialization failed")
+	require.Equal(t, string(expectedTemplate), string(tmpl), "the rendered template differs from the expected")
+}
+
 func mockAssociatedEnv(envName, region string, isProd bool) *deploy.AssociatedEnvironment {
 	return &deploy.AssociatedEnvironment{
 		Name:      envName,
@@ -76,6 +90,64 @@ func mockAssociatedEnv(envName, region string, isProd bool) *deploy.AssociatedEn
 }
 
 func mockCreatePipelineInput() *deploy.CreatePipelineInput {
+	return &deploy.CreatePipelineInput{
+		ProjectName: projectName,
+		Name:        pipelineName,
+		Source: &deploy.Source{
+			ProviderName: "GitHub",
+			Properties: map[string]interface{}{
+				"repository":          "hencrice/amazon-ecs-cli-v2",
+				"branch":              "master",
+				"access_token_secret": "testGitHubSecret",
+			},
+		},
+		Stages: []deploy.PipelineStage{
+			{
+				// in this stage, only frontend-app has integration test
+				AssociatedEnvironment: mockAssociatedEnv("test-chicken", "us-west-2", false),
+				LocalApplications: []deploy.AppInStage{
+					{
+						Name:                   "frontend-app",
+						IntegTestBuildspecPath: filepath.Join("frontend-app", manifest.IntegTestBuildspecFileName),
+					},
+					{
+						Name: "backend-app",
+					},
+				},
+			},
+			{
+				// in this stage, only backend-app has integration test
+				AssociatedEnvironment: mockAssociatedEnv("prod-can-fly", "us-east-1", true),
+				LocalApplications: []deploy.AppInStage{
+					{
+						Name: "frontend-app",
+					},
+					{
+						Name:                   "backend-app",
+						IntegTestBuildspecPath: filepath.Join("backend-app", manifest.IntegTestBuildspecFileName),
+					},
+				},
+			},
+		},
+		ArtifactBuckets: []deploy.ArtifactBucket{
+			{
+				BucketName: "chicken-us-east-1",
+				KeyArn:     fmt.Sprintf("arn:aws:kms:us-east-1:%s:key/30131d3f-c30f-4d49-beaa-cf4bfc07f34e", toolsAccountID),
+			},
+			{
+				BucketName: "chicken-us-west-2",
+				KeyArn:     fmt.Sprintf("arn:aws:kms:us-west-2:%s:key/80de5f7f-422d-4dff-8f4d-01f6ec5715bc", toolsAccountID),
+			},
+			// assume the pipeline is hosted in a region that does not contain any archer environment
+			{
+				BucketName: "chicken-us-west-1",
+				KeyArn:     fmt.Sprintf("arn:aws:kms:us-west-1:%s:key/75668c57-ec4b-4d0c-b880-8dc3fa78f6d1", toolsAccountID),
+			},
+		},
+	}
+}
+
+func mockCreatePipelineInputWithNoApplication() *deploy.CreatePipelineInput {
 	return &deploy.CreatePipelineInput{
 		ProjectName: projectName,
 		Name:        pipelineName,
@@ -90,11 +162,9 @@ func mockCreatePipelineInput() *deploy.CreatePipelineInput {
 		Stages: []deploy.PipelineStage{
 			{
 				AssociatedEnvironment: mockAssociatedEnv("test-chicken", "us-west-2", false),
-				LocalApplications:     []string{"frontend", "backend"},
 			},
 			{
 				AssociatedEnvironment: mockAssociatedEnv("prod-can-fly", "us-east-1", true),
-				LocalApplications:     []string{"frontend", "backend"},
 			},
 		},
 		ArtifactBuckets: []deploy.ArtifactBucket{
diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_cfn_template.yml b/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_cfn_template.yml
index 89ca68da..ecf8031c 100755
--- a/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_cfn_template.yml
+++ b/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_cfn_template.yml
@@ -119,6 +119,111 @@ Resources:
         Type: CODEPIPELINE
         BuildSpec: ecs-project/buildspec.yml
       TimeoutInMinutes: 60
+  IntegTestRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - codebuild.amazonaws.com
+            Action:
+              - sts:AssumeRole
+      Path: /
+  IntegTestPolicy:
+    Type: AWS::IAM::Policy
+    DependsOn: IntegTestRole
+    Properties:
+      PolicyName: !Sub ${AWS::StackName}-IntegTestPolicy
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Action:
+              - logs:CreateLogGroup
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+            Resource: arn:aws:logs:*:*:*
+          - Effect: Allow
+            Action:
+              - s3:PutObject
+              - s3:GetObject
+              - s3:GetObjectVersion
+            # TODO: This might not be necessary. We may only need the bucket
+            # that is in the same region as the pipeline.
+            # Loop through all the artifact buckets created in the stackset
+            Resource:
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1', '/*']]
+          - Effect: Allow
+            Action:
+              # TODO: scope this down if possible
+              - kms:*
+            # TODO: This might not be necessary. We may only need the KMS key
+            # that is in the same region as the pipeline.
+            # Loop through all the KMS keys used to en/decrypt artifacts
+            # across (cross-regional) pipeline stages, with each stage
+            # backed by a (regional) S3 bucket.
+            Resource:
+              - arn:aws:kms:us-east-1:012345678910:key/30131d3f-c30f-4d49-beaa-cf4bfc07f34e
+              - arn:aws:kms:us-west-2:012345678910:key/80de5f7f-422d-4dff-8f4d-01f6ec5715bc
+              - arn:aws:kms:us-west-1:012345678910:key/75668c57-ec4b-4d0c-b880-8dc3fa78f6d1
+      Roles:
+        - !Ref IntegTestRole
+  IntegTestProjectfrontendDASHapptestDASHchicken:
+    Type: AWS::CodeBuild::Project
+    Properties:
+      Name: Approve-frontend-app-test-chicken
+      Description: Integration tests for frontend-app-test-chicken
+      ServiceRole: !GetAtt IntegTestRole.Arn
+      Artifacts:
+        Type: CODEPIPELINE
+      Environment:
+        Type: LINUX_CONTAINER
+        ComputeType: BUILD_GENERAL1_SMALL
+        PrivilegedMode: true
+        Image: aws/codebuild/amazonlinux2-x86_64-standard:1.0
+        EnvironmentVariables:
+          - Name: PIPELINE_STAGE
+            Type: PLAINTEXT
+            Value: test-chicken
+          - Name: APP_NAME
+            Type: PLAINTEXT
+            Value: frontend-app
+      Source:
+        Type: CODEPIPELINE
+        BuildSpec: frontend-app/buildspec_integ.yml
+      TimeoutInMinutes: 480
+  IntegTestProjectbackendDASHappprodDASHcanDASHfly:
+    Type: AWS::CodeBuild::Project
+    Properties:
+      Name: Approve-backend-app-prod-can-fly
+      Description: Integration tests for backend-app-prod-can-fly
+      ServiceRole: !GetAtt IntegTestRole.Arn
+      Artifacts:
+        Type: CODEPIPELINE
+      Environment:
+        Type: LINUX_CONTAINER
+        ComputeType: BUILD_GENERAL1_SMALL
+        PrivilegedMode: true
+        Image: aws/codebuild/amazonlinux2-x86_64-standard:1.0
+        EnvironmentVariables:
+          - Name: PIPELINE_STAGE
+            Type: PLAINTEXT
+            Value: prod-can-fly
+          - Name: APP_NAME
+            Type: PLAINTEXT
+            Value: backend-app
+      Source:
+        Type: CODEPIPELINE
+        BuildSpec: backend-app/buildspec_integ.yml
+      TimeoutInMinutes: 480
   PipelineRole:
     Type: AWS::IAM::Role
     Properties:
@@ -261,10 +366,9 @@ Resources:
               - Name: SCCheckoutArtifact
             OutputArtifacts:
               - Name: BuildOutput
-
         - Name: DeployTo-test-chicken
           Actions:
-            - Name: CreateOrUpdate-frontend-test-chicken
+            - Name: CreateOrUpdate-frontend-app-test-chicken
               Region: us-west-2
               ActionTypeId:
                 Category: Deploy
@@ -273,12 +377,12 @@ Resources:
                 Provider: CloudFormation
               Configuration:
                 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-action-reference.html
-                ChangeSetName: chickenProject-test-chicken-frontend
+                ChangeSetName: chickenProject-test-chicken-frontend-app
                 ActionMode: CREATE_UPDATE
-                StackName: chickenProject-test-chicken-frontend
+                StackName: chickenProject-test-chicken-frontend-app
                 Capabilities: CAPABILITY_NAMED_IAM
-                TemplatePath: BuildOutput::infrastructure/frontend.stack.yml
-                TemplateConfiguration: BuildOutput::infrastructure/frontend-test-chicken.params.json
+                TemplatePath: BuildOutput::infrastructure/frontend-app.stack.yml
+                TemplateConfiguration: BuildOutput::infrastructure/frontend-app-test-chicken.params.json
                 # The ARN of the IAM role (in the env accountn) that
                 # AWS CloudFormation assumes when it operates on resources
                 # in a stack in an environment account.
@@ -290,7 +394,18 @@ Resources:
               # account) that performs the declared action. This is assumed
               # through the roleArn for the pipeline.
               RoleArn: arn:aws:iam::109876543210:role/chickenProject-test-chicken-EnvManagerRole
-            - Name: CreateOrUpdate-backend-test-chicken
+            - Name: Approve-frontend-app-test-chicken
+              ActionTypeId:
+                Category: Build
+                Owner: AWS
+                Version: 1
+                Provider: CodeBuild
+              Configuration:
+                ProjectName: !Ref IntegTestProjectfrontendDASHapptestDASHchicken
+              RunOrder: 3
+              InputArtifacts:
+                - Name: SCCheckoutArtifact
+            - Name: CreateOrUpdate-backend-app-test-chicken
               Region: us-west-2
               ActionTypeId:
                 Category: Deploy
@@ -299,12 +414,12 @@ Resources:
                 Provider: CloudFormation
               Configuration:
                 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-action-reference.html
-                ChangeSetName: chickenProject-test-chicken-backend
+                ChangeSetName: chickenProject-test-chicken-backend-app
                 ActionMode: CREATE_UPDATE
-                StackName: chickenProject-test-chicken-backend
+                StackName: chickenProject-test-chicken-backend-app
                 Capabilities: CAPABILITY_NAMED_IAM
-                TemplatePath: BuildOutput::infrastructure/backend.stack.yml
-                TemplateConfiguration: BuildOutput::infrastructure/backend-test-chicken.params.json
+                TemplatePath: BuildOutput::infrastructure/backend-app.stack.yml
+                TemplateConfiguration: BuildOutput::infrastructure/backend-app-test-chicken.params.json
                 # The ARN of the IAM role (in the env accountn) that
                 # AWS CloudFormation assumes when it operates on resources
                 # in a stack in an environment account.
@@ -325,7 +440,7 @@ Resources:
                 Provider: Manual
                 Version: 1
               RunOrder: 1
-            - Name: CreateOrUpdate-frontend-prod-can-fly
+            - Name: CreateOrUpdate-frontend-app-prod-can-fly
               Region: us-east-1
               ActionTypeId:
                 Category: Deploy
@@ -334,12 +449,12 @@ Resources:
                 Provider: CloudFormation
               Configuration:
                 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-action-reference.html
-                ChangeSetName: chickenProject-prod-can-fly-frontend
+                ChangeSetName: chickenProject-prod-can-fly-frontend-app
                 ActionMode: CREATE_UPDATE
-                StackName: chickenProject-prod-can-fly-frontend
+                StackName: chickenProject-prod-can-fly-frontend-app
                 Capabilities: CAPABILITY_NAMED_IAM
-                TemplatePath: BuildOutput::infrastructure/frontend.stack.yml
-                TemplateConfiguration: BuildOutput::infrastructure/frontend-prod-can-fly.params.json
+                TemplatePath: BuildOutput::infrastructure/frontend-app.stack.yml
+                TemplateConfiguration: BuildOutput::infrastructure/frontend-app-prod-can-fly.params.json
                 # The ARN of the IAM role (in the env accountn) that
                 # AWS CloudFormation assumes when it operates on resources
                 # in a stack in an environment account.
@@ -351,7 +466,7 @@ Resources:
               # account) that performs the declared action. This is assumed
               # through the roleArn for the pipeline.
               RoleArn: arn:aws:iam::109876543210:role/chickenProject-prod-can-fly-EnvManagerRole
-            - Name: CreateOrUpdate-backend-prod-can-fly
+            - Name: CreateOrUpdate-backend-app-prod-can-fly
               Region: us-east-1
               ActionTypeId:
                 Category: Deploy
@@ -360,12 +475,12 @@ Resources:
                 Provider: CloudFormation
               Configuration:
                 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-action-reference.html
-                ChangeSetName: chickenProject-prod-can-fly-backend
+                ChangeSetName: chickenProject-prod-can-fly-backend-app
                 ActionMode: CREATE_UPDATE
-                StackName: chickenProject-prod-can-fly-backend
+                StackName: chickenProject-prod-can-fly-backend-app
                 Capabilities: CAPABILITY_NAMED_IAM
-                TemplatePath: BuildOutput::infrastructure/backend.stack.yml
-                TemplateConfiguration: BuildOutput::infrastructure/backend-prod-can-fly.params.json
+                TemplatePath: BuildOutput::infrastructure/backend-app.stack.yml
+                TemplateConfiguration: BuildOutput::infrastructure/backend-app-prod-can-fly.params.json
                 # The ARN of the IAM role (in the env accountn) that
                 # AWS CloudFormation assumes when it operates on resources
                 # in a stack in an environment account.
@@ -376,4 +491,15 @@ Resources:
               # The ARN of the environment manager IAM role (in the env
               # account) that performs the declared action. This is assumed
               # through the roleArn for the pipeline.
-              RoleArn: arn:aws:iam::109876543210:role/chickenProject-prod-can-fly-EnvManagerRole
\ No newline at end of file
+              RoleArn: arn:aws:iam::109876543210:role/chickenProject-prod-can-fly-EnvManagerRole
+            - Name: Approve-backend-app-prod-can-fly
+              ActionTypeId:
+                Category: Build
+                Owner: AWS
+                Version: 1
+                Provider: CodeBuild
+              Configuration:
+                ProjectName: !Ref IntegTestProjectbackendDASHappprodDASHcanDASHfly
+              RunOrder: 3
+              InputArtifacts:
+                - Name: SCCheckoutArtifact
\ No newline at end of file
diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_with_no_application_cfn_template.yml b/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_with_no_application_cfn_template.yml
new file mode 100644
index 00000000..2ae2f8c7
--- /dev/null
+++ b/internal/pkg/deploy/cloudformation/stack/testdata/rendered_pipeline_with_no_application_cfn_template.yml
@@ -0,0 +1,263 @@
+# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
+# the License. A copy of the License is located at
+#     http://aws.amazon.com/apache2.0/
+# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+# CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and
+# limitations under the License.
+AWSTemplateFormatVersion: '2010-09-09'
+Description: CodePipeline for the chickenProject
+Parameters:
+  # TODO: #273 make this source-provider-agnostic
+  GitHubAccessTokenSecretId:
+    Description: The secretId of the GitHub Personal Access token stored in the Secrets Manager
+    Type: String
+    Default: testGitHubSecret
+Resources:
+  BuildProjectRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - codebuild.amazonaws.com
+            Action:
+              - sts:AssumeRole
+      Path: /
+      ManagedPolicyArns:
+        - 'arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess' # for env ls
+        - 'arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess' # for app package
+  BuildProjectPolicy:
+    Type: AWS::IAM::Policy
+    DependsOn: BuildProjectRole
+    Properties:
+      PolicyName: !Sub ${AWS::StackName}-CodeBuildPolicy
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Action:
+              - s3:PutObject
+              - s3:GetObject
+              - s3:GetObjectVersion
+            # TODO: This might not be necessary. We may only need the bucket
+            # that is in the same region as the pipeline.
+            # Loop through all the artifact buckets created in the stackset
+            Resource:
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1', '/*']]
+          - Effect: Allow
+            Action:
+              # TODO: scope this down if possible
+              - kms:*
+            # TODO: This might not be necessary. We may only need the KMS key
+            # that is in the same region as the pipeline.
+            # Loop through all the KMS keys used to en/decrypt artifacts
+            # across (cross-regional) pipeline stages, with each stage
+            # backed by a (regional) S3 bucket.
+            Resource:
+              - arn:aws:kms:us-east-1:012345678910:key/30131d3f-c30f-4d49-beaa-cf4bfc07f34e
+              - arn:aws:kms:us-west-2:012345678910:key/80de5f7f-422d-4dff-8f4d-01f6ec5715bc
+              - arn:aws:kms:us-west-1:012345678910:key/75668c57-ec4b-4d0c-b880-8dc3fa78f6d1
+          - Effect: Allow
+            Action:
+              - logs:CreateLogGroup
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+            Resource: arn:aws:logs:*:*:*
+          - Effect: Allow
+            Action:
+              - ecr:GetAuthorizationToken
+            Resource: '*'
+          - Effect: Allow
+            Action:
+              - ecr:DescribeImageScanFindings
+              - ecr:GetLifecyclePolicyPreview
+              - ecr:GetDownloadUrlForLayer
+              - ecr:BatchGetImage
+              - ecr:DescribeImages
+              - ecr:ListTagsForResource
+              - ecr:BatchCheckLayerAvailability
+              - ecr:GetLifecyclePolicy
+              - ecr:GetRepositoryPolicy
+              - ecr:PutImage
+              - ecr:InitiateLayerUpload
+              - ecr:UploadLayerPart
+              - ecr:CompleteLayerUpload
+            Resource: '*'
+            Condition: {StringEquals: {'ecr:ResourceTag/ecs-project': chickenProject}}
+      Roles:
+        - !Ref BuildProjectRole
+  BuildProject:
+    Type: AWS::CodeBuild::Project
+    Properties:
+      Name: !Sub ${AWS::StackName}-BuildProject
+      Description: !Sub Build for ${AWS::StackName}
+      # ArtifactKey is the KMS key ID or ARN that is used with the artifact bucket
+      # created in the same region as this pipeline.
+      EncryptionKey: !ImportValue chickenProject-ArtifactKey
+      ServiceRole: !GetAtt BuildProjectRole.Arn
+      Artifacts:
+        Type: CODEPIPELINE
+      Cache:
+        Modes:
+          - LOCAL_DOCKER_LAYER_CACHE
+        Type: LOCAL
+      Environment:
+        Type: LINUX_CONTAINER
+        ComputeType: BUILD_GENERAL1_SMALL
+        PrivilegedMode: true
+        Image: aws/codebuild/amazonlinux2-x86_64-standard:1.0
+      Source:
+        Type: CODEPIPELINE
+        BuildSpec: ecs-project/buildspec.yml
+      TimeoutInMinutes: 60
+  PipelineRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - codepipeline.amazonaws.com
+            Action:
+              - sts:AssumeRole
+      Path: /
+  PipelineRolePolicy:
+    Type: AWS::IAM::Policy
+    Properties:
+      PolicyName: !Sub ${AWS::StackName}-CodepipelinePolicy
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Action:
+              - codepipeline:*
+              - iam:ListRoles
+              - cloudformation:Describe*
+              - cloudFormation:List*
+              - codebuild:BatchGetBuilds
+              - codebuild:StartBuild
+              - cloudformation:CreateStack
+              - cloudformation:DeleteStack
+              - cloudformation:DescribeStacks
+              - cloudformation:UpdateStack
+              - cloudformation:CreateChangeSet
+              - cloudformation:DeleteChangeSet
+              - cloudformation:DescribeChangeSet
+              - cloudformation:ExecuteChangeSet
+              - cloudformation:SetStackPolicy
+              - cloudformation:ValidateTemplate
+              - iam:PassRole
+              - s3:ListAllMyBuckets
+              - s3:GetBucketLocation
+            Resource:
+              - "*"
+          - Effect: Allow
+            Action:
+              - kms:Decrypt
+              - kms:Encrypt
+              - kms:GenerateDataKey
+            Resource:
+              - arn:aws:kms:us-east-1:012345678910:key/30131d3f-c30f-4d49-beaa-cf4bfc07f34e
+              - arn:aws:kms:us-west-2:012345678910:key/80de5f7f-422d-4dff-8f4d-01f6ec5715bc
+              - arn:aws:kms:us-west-1:012345678910:key/75668c57-ec4b-4d0c-b880-8dc3fa78f6d1
+          - Effect: Allow
+            Action:
+              - s3:PutObject
+              - s3:GetBucketPolicy
+              - s3:GetObject
+              - s3:ListBucket
+            Resource:
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-east-1', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-2', '/*']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1']]
+              - !Join ['', ['arn:aws:s3:::', 'chicken-us-west-1', '/*']]
+          - Effect: Allow
+            Action:
+              - sts:AssumeRole
+            Resource:
+              - arn:aws:iam::109876543210:role/chickenProject-test-chicken-EnvManagerRole
+              - arn:aws:iam::109876543210:role/chickenProject-prod-can-fly-EnvManagerRole
+      Roles:
+        - !Ref PipelineRole
+  Pipeline:
+    Type: AWS::CodePipeline::Pipeline
+    DependsOn:
+      - PipelineRole
+      - PipelineRolePolicy
+    Properties:
+      ArtifactStores:
+        - Region: us-east-1
+          ArtifactStore:
+            Type: S3
+            Location: chicken-us-east-1
+            EncryptionKey:
+              Id: arn:aws:kms:us-east-1:012345678910:key/30131d3f-c30f-4d49-beaa-cf4bfc07f34e
+              Type: KMS
+        - Region: us-west-2
+          ArtifactStore:
+            Type: S3
+            Location: chicken-us-west-2
+            EncryptionKey:
+              Id: arn:aws:kms:us-west-2:012345678910:key/80de5f7f-422d-4dff-8f4d-01f6ec5715bc
+              Type: KMS
+        - Region: us-west-1
+          ArtifactStore:
+            Type: S3
+            Location: chicken-us-west-1
+            EncryptionKey:
+              Id: arn:aws:kms:us-west-1:012345678910:key/75668c57-ec4b-4d0c-b880-8dc3fa78f6d1
+              Type: KMS
+      RoleArn: !GetAtt PipelineRole.Arn
+      Name: !Ref AWS::StackName
+      Stages:
+        - Name: Source
+          Actions:
+            - Name: SourceCodeFor-chickenProject
+              ActionTypeId:
+                Category: Source
+                # TODO: #273 For CodeCommit, this needs to be AWS. Let's do that later
+                Owner: ThirdParty
+                Version: 1
+                Provider: GitHub
+              Configuration:
+                # TODO: #273 For now this set of configurations is GitHub-specific,
+                # change it to be more generic
+                Owner: hencrice
+                Repo: amazon-ecs-cli-v2
+                Branch: master
+                # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager
+                # Use the *entire* SecretString with version AWSCURRENT
+                OAuthToken: !Sub
+                    - '{{resolve:secretsmanager:${SecretId}}}'
+                    - { SecretId: !Ref GitHubAccessTokenSecretId }
+              OutputArtifacts:
+                - Name: SCCheckoutArtifact
+              RunOrder: 1
+        - Name: Build
+          Actions:
+          - Name: Build
+            ActionTypeId:
+              Category: Build
+              Owner: AWS
+              Version: 1
+              Provider: CodeBuild
+            Configuration:
+              ProjectName: !Ref BuildProject
+            RunOrder: 1
+            InputArtifacts:
+              - Name: SCCheckoutArtifact
+            OutputArtifacts:
+              - Name: BuildOutput
\ No newline at end of file
diff --git a/internal/pkg/describe/webapp_test.go b/internal/pkg/describe/webapp_test.go
index a5a5e1a9..595961a7 100644
--- a/internal/pkg/describe/webapp_test.go
+++ b/internal/pkg/describe/webapp_test.go
@@ -9,6 +9,7 @@ import (
 	"testing"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	climocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/mocks"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/describe/mocks"
 	"github.com/aws/aws-sdk-go/aws"
@@ -61,15 +62,15 @@ func TestWebAppDescriber_URI(t *testing.T) {
 		testAppPath        = "*"
 	)
 	testCases := map[string]struct {
-		mockStore           func(ctrl *gomock.Controller) *mocks.MockenvAppGetter
+		mockStore           func(ctrl *gomock.Controller) *climocks.MockprojectService
 		mockStackDescribers func(ctrl *gomock.Controller) map[string]stackDescriber
 
 		wantedURI   *webAppURI
 		wantedError error
 	}{
 		"environment does not exist in store": {
-			mockStore: func(ctrl *gomock.Controller) *mocks.MockenvAppGetter {
-				m := mocks.NewMockenvAppGetter(ctrl)
+			mockStore: func(ctrl *gomock.Controller) *climocks.MockprojectService {
+				m := climocks.NewMockprojectService(ctrl)
 				m.EXPECT().GetEnvironment(testProject, testEnv).Return(nil, errors.New("some error"))
 				return m
 			},
@@ -79,8 +80,8 @@ func TestWebAppDescriber_URI(t *testing.T) {
 			wantedError: errors.New("some error"),
 		},
 		"cfn error": {
-			mockStore: func(ctrl *gomock.Controller) *mocks.MockenvAppGetter {
-				m := mocks.NewMockenvAppGetter(ctrl)
+			mockStore: func(ctrl *gomock.Controller) *climocks.MockprojectService {
+				m := climocks.NewMockprojectService(ctrl)
 				m.EXPECT().GetEnvironment(testProject, testEnv).Return(&archer.Environment{
 					Project:        testProject,
 					Name:           testEnv,
@@ -98,8 +99,8 @@ func TestWebAppDescriber_URI(t *testing.T) {
 			wantedError: fmt.Errorf("describe stack %s: %s", stack.NameForEnv(testProject, testEnv), "some error"),
 		},
 		"stack does not exist": {
-			mockStore: func(ctrl *gomock.Controller) *mocks.MockenvAppGetter {
-				m := mocks.NewMockenvAppGetter(ctrl)
+			mockStore: func(ctrl *gomock.Controller) *climocks.MockprojectService {
+				m := climocks.NewMockprojectService(ctrl)
 				m.EXPECT().GetEnvironment(testProject, testEnv).Return(&archer.Environment{
 					Project:        testProject,
 					Name:           testEnv,
@@ -119,8 +120,8 @@ func TestWebAppDescriber_URI(t *testing.T) {
 			wantedError: fmt.Errorf("stack %s not found", stack.NameForEnv(testProject, testEnv)),
 		},
 		"https web application": {
-			mockStore: func(ctrl *gomock.Controller) *mocks.MockenvAppGetter {
-				m := mocks.NewMockenvAppGetter(ctrl)
+			mockStore: func(ctrl *gomock.Controller) *climocks.MockprojectService {
+				m := climocks.NewMockprojectService(ctrl)
 				m.EXPECT().GetEnvironment(testProject, testEnv).Return(&archer.Environment{
 					Project:        testProject,
 					Name:           testEnv,
@@ -172,8 +173,8 @@ func TestWebAppDescriber_URI(t *testing.T) {
 			},
 		},
 		"http web application": {
-			mockStore: func(ctrl *gomock.Controller) *mocks.MockenvAppGetter {
-				m := mocks.NewMockenvAppGetter(ctrl)
+			mockStore: func(ctrl *gomock.Controller) *climocks.MockprojectService {
+				m := climocks.NewMockprojectService(ctrl)
 				m.EXPECT().GetEnvironment(testProject, testEnv).Return(&archer.Environment{
 					Project:        testProject,
 					Name:           testEnv,
diff --git a/internal/pkg/manifest/pipeline_test.go b/internal/pkg/manifest/pipeline_test.go
index 4a943ba2..92419ad2 100644
--- a/internal/pkg/manifest/pipeline_test.go
+++ b/internal/pkg/manifest/pipeline_test.go
@@ -6,10 +6,13 @@ package manifest
 import (
 	"errors"
 	"fmt"
+	"path/filepath"
 	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 )
 
 func TestNewProvider(t *testing.T) {
@@ -38,14 +41,37 @@ func TestNewProvider(t *testing.T) {
 	}
 }
 
+func genApps(names ...string) []archer.Manifest {
+	result := make([]archer.Manifest, 0, len(names))
+	for _, name := range names {
+		result = append(result, &LBFargateManifest{
+			AppManifest: AppManifest{
+				Name: name,
+				Type: LoadBalancedWebApplication,
+			},
+			Image: ImageWithPort{
+				AppImage: AppImage{
+					Build: name,
+				},
+			},
+		})
+	}
+	return result
+}
+
 func TestCreatePipeline(t *testing.T) {
 	const pipelineName = "pipepiper"
+	const (
+		app01 = "app01"
+		app02 = "app02"
+	)
 
 	testCases := map[string]struct {
 		beforeEach     func() error
 		provider       Provider
 		expectedErr    error
 		inputStages    []string
+		inputApps      []archer.Manifest
 		expectedStages []PipelineStage
 	}{
 		"errors out when no stage provided": {
@@ -60,7 +86,20 @@ func TestCreatePipeline(t *testing.T) {
 			expectedErr: fmt.Errorf("a pipeline %s can not be created without a deployment stage",
 				pipelineName),
 		},
-		"happy case with non-default stages": {
+		"errors out when no app provided": {
+			provider: func() Provider {
+				p, err := NewProvider(&GitHubProperties{
+					OwnerAndRepository: "aws/amazon-ecs-cli-v2",
+					Branch:             "master",
+				})
+				require.NoError(t, err, "failed to create provider")
+				return p
+			}(),
+			inputStages: []string{"chicken", "wings"},
+			expectedErr: fmt.Errorf("a pipeline %s can not be created without any app to deploy",
+				pipelineName),
+		},
+		"happy case": {
 			provider: func() Provider {
 				p, err := NewProvider(&GitHubProperties{
 					OwnerAndRepository: "aws/amazon-ecs-cli-v2",
@@ -69,14 +108,38 @@ func TestCreatePipeline(t *testing.T) {
 				require.NoError(t, err, "failed to create provider")
 				return p
 			}(),
-			inputStages:    []string{"chicken", "wings"},
-			expectedStages: []PipelineStage{{"chicken"}, {"wings"}},
+			inputStages: []string{"chicken", "wings"},
+			inputApps:   genApps(app01, app02),
+			expectedStages: []PipelineStage{
+				{
+					Name: "chicken",
+					Apps: map[string]App{
+						app01: App{
+							IntegTestBuildspecPath: filepath.Join(app01, IntegTestBuildspecFileName),
+						},
+						app02: App{
+							IntegTestBuildspecPath: filepath.Join(app02, IntegTestBuildspecFileName),
+						},
+					},
+				},
+				{
+					Name: "wings",
+					Apps: map[string]App{
+						app01: App{
+							IntegTestBuildspecPath: filepath.Join(app01, IntegTestBuildspecFileName),
+						},
+						app02: App{
+							IntegTestBuildspecPath: filepath.Join(app02, IntegTestBuildspecFileName),
+						},
+					},
+				},
+			},
 		},
 	}
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			m, err := CreatePipeline(pipelineName, tc.provider, tc.inputStages)
+			m, err := CreatePipeline(pipelineName, tc.provider, tc.inputStages, tc.inputApps)
 
 			if tc.expectedErr != nil {
 				require.EqualError(t, err, tc.expectedErr.Error())
@@ -89,6 +152,10 @@ func TestCreatePipeline(t *testing.T) {
 
 func TestPipelineManifestMarshal(t *testing.T) {
 	const pipelineName = "pipepiper"
+	const (
+		app01 = "app01"
+		app02 = "app02"
+	)
 	wantedContent := `# This YAML file defines the relationship and deployment ordering of your environments.
 
 # The name of the pipeline
@@ -112,11 +179,20 @@ source:
 # The deployment section defines the order the pipeline will deploy
 # to your environments.
 stages:
-    - # The name of the environment to deploy to.
-      name: chicken
-    - # The name of the environment to deploy to.
-      name: wings
-`
+  - # The name of the environment to deploy to.
+    name: chicken
+    apps:
+      app01:
+        integrationTestBuildspec: app01/buildspec_integ.yml
+      app02:
+        integrationTestBuildspec: app02/buildspec_integ.yml
+  - # The name of the environment to deploy to.
+    name: wings
+    apps:
+      app01:
+        integrationTestBuildspec: app01/buildspec_integ.yml
+      app02:
+        integrationTestBuildspec: app02/buildspec_integ.yml`
 	// reset the global map before each test case is run
 	provider, err := NewProvider(&GitHubProperties{
 		OwnerAndRepository:    "aws/amazon-ecs-cli-v2",
@@ -125,7 +201,8 @@ stages:
 	})
 	require.NoError(t, err)
 
-	m, err := CreatePipeline(pipelineName, provider, []string{"chicken", "wings"})
+	m, err := CreatePipeline(pipelineName, provider,
+		[]string{"chicken", "wings"}, genApps(app01, app02))
 	require.NoError(t, err)
 
 	b, err := m.Marshal()
@@ -134,6 +211,10 @@ stages:
 }
 
 func TestUnmarshalPipeline(t *testing.T) {
+	const (
+		app01 = "app01"
+		app02 = "app02"
+	)
 	testCases := map[string]struct {
 		inContent        string
 		expectedManifest *PipelineManifest
@@ -151,10 +232,18 @@ source:
     branch: master
 
 stages:
-    -
-      name: test
-    -
-      name: prod
+  - name: chicken
+    apps:
+      app01:
+        integrationTestBuildspec: app01/buildspec_integ.yml
+      app02:
+        integrationTestBuildspec: app02/buildspec_integ.yml
+  - name: wings
+    apps:
+      app01:
+        integrationTestBuildspec: app01/buildspec_integ.yml
+      app02:
+        integrationTestBuildspec: app02/buildspec_integ.yml
 `,
 			expectedErr: &ErrInvalidPipelineManifestVersion{
 				PipelineSchemaMajorVersion(-1),
@@ -177,10 +266,14 @@ source:
     branch: master
 
 stages:
-    -
-      name: chicken
-    -
-      name: wings
+  - name: chicken
+    apps:
+      app01:
+        integrationTestBuildspec: app01/buildspec_integ.yml
+  - name: wings
+    apps:
+      app02:
+        integrationTestBuildspec: app02/buildspec_integ.yml
 `,
 			expectedManifest: &PipelineManifest{
 				Name:    "pipepiper",
@@ -194,8 +287,23 @@ stages:
 					},
 				},
 				Stages: []PipelineStage{
-					PipelineStage{"chicken"},
-					PipelineStage{"wings"}},
+					{
+						Name: "chicken",
+						Apps: map[string]App{
+							app01: App{
+								IntegTestBuildspecPath: filepath.Join(app01, IntegTestBuildspecFileName),
+							},
+						},
+					},
+					{
+						Name: "wings",
+						Apps: map[string]App{
+							app02: App{
+								IntegTestBuildspecPath: filepath.Join(app02, IntegTestBuildspecFileName),
+							},
+						},
+					},
+				},
 			},
 		},
 	}
diff --git a/internal/pkg/workspace/workspace_test.go b/internal/pkg/workspace/workspace_test.go
index 6f1ff408..a210b658 100644
--- a/internal/pkg/workspace/workspace_test.go
+++ b/internal/pkg/workspace/workspace_test.go
@@ -80,6 +80,11 @@ func genApps(names ...string) []archer.Manifest {
 				Name: name,
 				Type: manifest.LoadBalancedWebApplication,
 			},
+			Image: manifest.ImageWithPort{
+				AppImage: manifest.AppImage{
+					Build: name,
+				},
+			},
 		})
 	}
 	return result
@@ -90,8 +95,10 @@ func renderManifest(name string) string {
 name: %s
 # The "architecture" of the application you're running.
 type: Load Balanced Web App
+image:
+    build: %s 
 `
-	return fmt.Sprintf(template, name)
+	return fmt.Sprintf(template, name, name)
 }
 
 func TestApps(t *testing.T) {