Repository: openfaas/faas-cli
Pull request #841 (2020-11-16 19:00:41)

Add publish command to publish multi-arch images
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>

<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->

Add publish command to publish multi-arch images

## Motivation and Context

The publish command can be used to build and push a multi-arc image with
Docker buildx and buildkit. A multi-arch template must be used which
a Dockerfile that accepts TARGET/BUILDPLATFORM.

Read more: https://docs.docker.com/docker-for-mac/multi-arch/

Why `publish` instead of updating the existing code?

See details on these issues: #836 #662
Fixes:  #836 #662 

Adding a new command with a single purpose reduces the risk of regression in the core openfaas workflow and commands.

For local dev/test, use faas-cli up.

For CI use `faas-cli publish` followed by `faas-cli deploy`

## How Has This Been Tested?

Tested with the golang-middleware template.

The resulting image worked fine on RPi and Docker Desktop.

```
faas-cli template store pull golang-middleware
faas-cli new --lang golang-middleware --prefix alexellis2 go1

# Add a specific version instead of "latest"
sed s/latest/0.1.0/g go1.yml

# Publish image tags 0.1.0 plus "latest"
/faas-cli publish -f go1.yml --platforms linux/arm/7,linux/amd64 --extra-tag latest
```

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I've read the [CONTRIBUTION](https://github.com/openfaas/faas/blob/master/CONTRIBUTING.md) guide
- [x] I have signed-off my commits with `git commit -s`
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.



Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 00000000..c52c5374
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,72 @@
+name: build
+
+on:
+  push:
+    branches:
+      - "*"
+  pull_request:
+    branches:
+      - "*"
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - 
+        uses: actions/checkout@master
+        with:
+            fetch-depth: 1
+      -
+        name: Set Username/Repo as ENV vars
+        run: echo "USER_REPO"=$(echo "$GITHUB_REPOSITORY" | awk '{print tolower($1)}' | sed -e "s/:refs//") >> $GITHUB_ENV
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v1
+      - 
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v1
+      - 
+        name: Local docker build (non-root image)
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          file: ./Dockerfile
+          push: false
+          platforms: linux/amd64
+          target: release
+          build-args: |
+            VERSION=latest-dev
+            GIT_COMMIT=${{ github.sha }}
+            REPO_URL=https://github.com/${{ env.USER_REPO }}
+          load: true
+          tags: openfaas/faas-cli:${{ github.sha }}
+      - 
+        name: Local docker build (root image)
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          file: ./Dockerfile
+          push: false
+          platforms: linux/amd64
+          target: release
+          build-args: |
+            VERSION=latest-dev
+            GIT_COMMIT=${{ github.sha }}
+            REPO_URL=https://github.com/${{ env.USER_REPO }}
+          tags: openfaas/faas-cli:${{ github.sha }}-root
+      - 
+        name: Copy binary to host
+        run: |
+          docker create --name faas-cli openfaas/faas-cli:${{ github.sha }} && \
+          mkdir -p ./bin && \
+          docker cp faas-cli:/usr/bin/faas-cli ./bin && \
+          docker rm -f faas-cli
+      - 
+        name: Build samples
+        run: ./build_samples.sh
+      - 
+        name: Test version
+        run: ./test/version.sh ./bin/faas-cli
+      - 
+        name: Test the cli and the function behaviour
+        run: make test-templating
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 00000000..fc6bbe20
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,98 @@
+name: publish
+
+on:
+  push:
+    tags:
+      - '*'
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    steps:
+      - 
+        uses: actions/checkout@master
+        with:
+          fetch-depth: 1
+      - 
+        name: Get tags
+        id: get_tag
+        run: echo ::set-output name=TAG::${GITHUB_REF#refs/tags/}
+      -
+        name: Set Username/Repo and ImagePrefix as ENV vars
+        run: |
+          echo "USER_REPO"=$(echo "$GITHUB_REPOSITORY" | awk '{print tolower($1)}' | sed -e "s/:refs//") >> $GITHUB_ENV && \
+          echo "IMAGE_PREFIX"=$(echo "ghcr.io/$GITHUB_REPOSITORY" | awk '{print tolower($1)}' | sed -e "s/:refs//") >> $GITHUB_ENV
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v1
+      - 
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v1
+      - 
+        name: Login to Github Container Registry
+        uses: docker/login-action@v1
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.DOCKER_PASSWORD }}
+      -
+        name: Build and Push container images (non-root)
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          file: ./Dockerfile
+          platforms: linux/amd64,linux/arm/v7,linux/arm64
+          target: release
+          build-args: |
+            VERSION=${{ steps.get_tag.outputs.TAG }}
+            GIT_COMMIT=${{ github.sha }}
+            REPO_URL=https://github.com/${{ env.USER_REPO }}
+          push: true
+          tags: |
+            ${{ env.IMAGE_PREFIX }}:${{ github.sha }}
+            ${{ env.IMAGE_PREFIX }}:${{ steps.get_tag.outputs.TAG }}
+            ${{ env.IMAGE_PREFIX }}:latest
+      - 
+        name: Build and Push container images (root)
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          file: ./Dockerfile
+          platforms: linux/amd64,linux/arm/v7,linux/arm64
+          target: root
+          build-args: |
+            VERSION=${{ steps.get_tag.outputs.TAG }}
+            GIT_COMMIT=${{ github.sha }}
+            REPO_URL=https://github.com/${{ env.USER_REPO }}
+          push: true
+          tags: |
+            ${{ env.IMAGE_PREFIX }}:${{ github.sha }}-root
+            ${{ env.IMAGE_PREFIX }}:${{ steps.get_tag.outputs.TAG }}-root
+            ${{ env.IMAGE_PREFIX }}:latest-root
+      -
+        name: Build binaries for multiple environments
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          file: ./Dockerfile.redist
+          platforms: linux/amd64
+          build-args: |
+            VERSION=${{ steps.get_tag.outputs.TAG }}
+            GIT_COMMIT=${{ github.sha }}
+          load: true
+          push: false
+          tags: |
+           openfaas/faas-cli:${{ steps.get_tag.outputs.TAG }}
+      - 
+        name: Copy binaries to host
+        run: ./build_redist.sh ${{ steps.get_tag.outputs.TAG }}
+      - 
+        name: Create SHA of binaries
+        run: ./ci/hashgen.sh
+      - 
+        name: Upload binaries and their SHA to Github Release
+        uses: alexellis/upload-assets@0.2.2
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          asset_paths: '["./bin/faas-cli*"]'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a4d606a7..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-sudo: required
-language: generic
-
-services:
-- docker
-
-before_script:
-- curl -sSLf https://get.docker.com | sed s/sleep\ 20/sleep\ 0/g | sudo -E sh
-
-script:
-  - make build
-  - make build_samples
-  - ./test/version.sh ./faas-cli
-  - make build_redist
-
-after_success:
-    - if [ ! -z "$TRAVIS_TAG" ] ; then
-        if [ -z "$DOCKER_NS" ] ; then
-          export DOCKER_NS=openfaas;
-        fi;
-
-        docker tag $DOCKER_NS/faas-cli:latest-dev $DOCKER_NS/faas-cli:$TRAVIS_TAG;
-        docker tag $DOCKER_NS/faas-cli:latest-dev $DOCKER_NS/faas-cli:latest;
-        docker tag $DOCKER_NS/faas-cli:latest-dev-root $DOCKER_NS/faas-cli:${TRAVIS_TAG}-root;
-        docker tag $DOCKER_NS/faas-cli:latest-dev-root $DOCKER_NS/faas-cli:latest-root;
-        echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin;
-        docker push $DOCKER_NS/faas-cli:$TRAVIS_TAG;
-        docker push $DOCKER_NS/faas-cli:latest;
-        docker push $DOCKER_NS/faas-cli:${TRAVIS_TAG}-root;
-        docker push $DOCKER_NS/faas-cli:latest-root;
-
-      fi;
-
-before_deploy:
-  - make test-templating
-  - ./ci/hashgen.sh
-
-deploy:
-  provider: releases
-  api_key:
-      secure: "XSwuWxddw6Stz/ZtrfQJBovTL7ZyQM8kf7zCtzS6Pl1gatgpa/E/ZflVICgEArGZu2Ipaj8TfOJjQ0cgZlA3MSt+DxNXpZjHJbeids+GFQd+/xuxvyUDI9mCabE2hLM0Drqy4Psro2Rz8S995cIoCmAZM5FpGQ2xgjy6c2cIpeO1F3wnLExTe+TLBBYOP2yEdkLNem12EOnomsCnmG0Cq15nJKq0zOZ1dzIIOhKTY/9poH0s3W3lpbXYYVLGhRJ6GENwX1+HX7yM71AOAYPdXF4AV5p1jVCI74c1tnUGgysYzo9CHIgB4RME6ElJrYzOA9bBBX5mHPmwvTkfUmt2Dp/Oq4ZhqWKfAp4dFjEV2iZpKg4V8+neeiLlgHnTHur/tCxBJLywH+FuoLMghKjs10wT6gAeKa098cEJzXrnRxwRa/jHOIMJLi5BflbfS1QbjHaQMrdHgQCwQlj2ZpBcCM/MWrOgeWT72o6gAfeg4r4Gzj0MHlKr82LCY27QVUeit1iodUL2eODd6DURgt8jfRVOjEgATrV0xewsTp2ENHWccxpqfceGAjyUow6HLhjNvBHwcaUgMjTp2oAUPIDCqVp7oXyOEoHy27hyNEqxHCh0MjPY/HDoW1I+1vhBH47xewbLgNrxeT7Znm+SO6q5Ak0OIWcf/5vfLOOq3BOImno="
-  file: 
-  - faas-cli
-  - faas-cli-darwin
-  - faas-cli-armhf
-  - faas-cli.exe
-  - faas-cli-arm64
-  - faas-cli.sha256
-  - faas-cli-darwin.sha256
-  - faas-cli-armhf.sha256
-  - faas-cli.exe.sha256
-  - faas-cli-arm64.sha256
-  skip_cleanup: true
-  on:
-    tags: true
-
-env:
-  - GO111MODULE=off
diff --git a/Dockerfile b/Dockerfile
index 3984fe13..4580d152 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,15 @@
-FROM teamserverless/license-check:0.3.6 as license-check
+FROM teamserverless/license-check:0.3.9 as license-check
 
 # Build stage
-FROM golang:1.13 as builder
+FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.13-alpine as builder
+
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+ARG TARGETOS
+ARG TARGETARCH
+
+ARG GIT_COMMIT
+ARG VERSION
 
 ENV GO111MODULE=on
 ENV GOFLAGS=-mod=vendor
@@ -9,6 +17,7 @@ ENV CGO_ENABLED=0
 
 WORKDIR /usr/bin/
 
+RUN apk --no-cache add git
 COPY --from=license-check /license-check /usr/bin/
 
 WORKDIR /go/src/github.com/openfaas/faas-cli
@@ -21,31 +30,39 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))"
 # ldflags -X injects commit version into binary
 RUN /usr/bin/license-check -path ./ --verbose=false "Alex Ellis" "OpenFaaS Author(s)"
 
-RUN go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/|grep -v /sample/) -cover
+RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
+    go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/|grep -v /sample/) -cover
 
-RUN VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \
-    && GIT_COMMIT=$(git rev-list -1 HEAD) \
-    && CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w \
+RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 \
+    go build --ldflags "-s -w \
     -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} \
     -X github.com/openfaas/faas-cli/version.Version=${VERSION} \
-    -X github.com/openfaas/faas-cli/commands.Platform=x86_64" \
+    -X github.com/openfaas/faas-cli/commands.Platform=${TARGETARCH}" \
     -a -installsuffix cgo -o faas-cli
 
 # CICD stage
-FROM alpine:3.12 as root
+FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.12 as root
+
+ARG REPO_URL
+
+LABEL org.opencontainers.image.source $REPO_URL
 
 RUN apk --no-cache add ca-certificates git
 
 WORKDIR /home/app
 
-COPY --from=builder /go/src/github.com/openfaas/faas-cli/faas-cli               /usr/bin/
+COPY --from=builder /go/src/github.com/openfaas/faas-cli/faas-cli /usr/bin/
 
 ENV PATH=$PATH:/usr/bin/
 
 ENTRYPOINT [ "faas-cli" ]
 
 # Release stage
-FROM alpine:3.12 as release
+FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.12 as release
+
+ARG REPO_URL
+
+LABEL org.opencontainers.image.source $REPO_URL
 
 RUN apk --no-cache add ca-certificates git
 
@@ -55,11 +72,11 @@ RUN addgroup -S app \
 
 WORKDIR /home/app
 
-COPY --from=builder /go/src/github.com/openfaas/faas-cli/faas-cli               /usr/bin/
+COPY --from=builder /go/src/github.com/openfaas/faas-cli/faas-cli /usr/bin/
 RUN chown -R app:app ./
 
 USER app
 
 ENV PATH=$PATH:/usr/bin/
 
-ENTRYPOINT ["faas-cli"]
+ENTRYPOINT ["faas-cli"]
\ No newline at end of file
diff --git a/Dockerfile.redist b/Dockerfile.redist
index 1c394001..986b8b50 100644
--- a/Dockerfile.redist
+++ b/Dockerfile.redist
@@ -3,6 +3,9 @@ FROM teamserverless/license-check:0.3.6 as license-check
 # Build stage
 FROM golang:1.13 as builder
 
+ARG GIT_COMMIT
+ARG VERSION
+
 ENV GO111MODULE=on
 ENV GOFLAGS=-mod=vendor
 ENV CGO_ENABLED=0
diff --git a/README.md b/README.md
index 994e3e84..946989bb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 ## faas-cli
-[![Build Status](https://travis-ci.com/openfaas/faas-cli.svg?branch=master)](https://travis-ci.com/openfaas/faas-cli)
+[![Build Status](https://github.com/openfaas/faas-cli/workflows/build/badge.svg?branch=master)](https://github.com/openfaas/faas-cli/actions)
 [![Go Report Card](https://goreportcard.com/badge/github.com/openfaas/faas-cli)](https://goreportcard.com/report/github.com/openfaas/faas-cli)
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 [![OpenFaaS](https://img.shields.io/badge/openfaas-serverless-blue.svg)](https://www.openfaas.com)
@@ -77,6 +77,8 @@ The main commands supported by the CLI are:
 * `faas-cli push` - pushes Docker images into a registry
 * `faas-cli deploy` - deploys the functions into a local or remote OpenFaaS gateway
 
+* `faas-cli publish` - build and push multi-arch images for CI and release artifacts
+
 * `faas-cli remove` - removes the functions from a local or remote OpenFaaS gateway
 * `faas-cli invoke` - invokes the functions and reads from STDIN for the body of the request
 * `faas-cli store` - allows browsing and deploying OpenFaaS store functions
diff --git a/build_redist.sh b/build_redist.sh
index dcf55fe7..cccfd03a 100755
--- a/build_redist.sh
+++ b/build_redist.sh
@@ -6,11 +6,11 @@ if [ $1 ] ; then
   eTAG=$1
 fi
 
-docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t openfaas/faas-cli:$eTAG . -f Dockerfile.redist && \
- docker create --name faas-cli openfaas/faas-cli:$eTAG && \
- docker cp faas-cli:/home/app/faas-cli . && \
- docker cp faas-cli:/home/app/faas-cli-darwin . && \
- docker cp faas-cli:/home/app/faas-cli-armhf . && \
- docker cp faas-cli:/home/app/faas-cli-arm64 . && \
- docker cp faas-cli:/home/app/faas-cli.exe . && \
- docker rm -f faas-cli
+docker create --name faas-cli openfaas/faas-cli:${eTAG} && \
+mkdir -p ./bin && \
+docker cp faas-cli:/home/app/faas-cli ./bin && \
+docker cp faas-cli:/home/app/faas-cli-darwin ./bin && \
+docker cp faas-cli:/home/app/faas-cli-armhf ./bin && \
+docker cp faas-cli:/home/app/faas-cli-arm64 ./bin && \
+docker cp faas-cli:/home/app/faas-cli.exe ./bin && \
+docker rm -f faas-cli
diff --git a/build_samples.sh b/build_samples.sh
index 33a79c8e..3f41be81 100755
--- a/build_samples.sh
+++ b/build_samples.sh
@@ -1,5 +1,5 @@
 #!/bin/sh
 
-./faas-cli build # --squash=true
+./bin/faas-cli build # --squash=true
 
 docker images |head -n 4
diff --git a/builder/build.go b/builder/build.go
index 3e670b69..ca6af73b 100644
--- a/builder/build.go
+++ b/builder/build.go
@@ -23,6 +23,7 @@ import (
 const AdditionalPackageBuildArg = "ADDITIONAL_PACKAGE"
 
 // BuildImage construct Docker image from function parameters
+// TODO: refactor signature to a struct to simplify the length of the method header
 func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string, buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string) error {
 
 	if stack.IsValidTemplate(language) {
@@ -142,7 +143,8 @@ func getDockerBuildCommand(build dockerBuild) (string, []string) {
 	flagSlice := buildFlagSlice(build.NoCache, build.Squash, build.HTTPProxy, build.HTTPSProxy, build.BuildArgMap, build.BuildOptPackages, build.BuildLabelMap)
 	args := []string{"build"}
 	args = append(args, flagSlice...)
-	args = append(args, "-t", build.Image, ".")
+
+	args = append(args, "--tag", build.Image, ".")
 
 	command := "docker"
 
@@ -159,6 +161,12 @@ type dockerBuild struct {
 	BuildArgMap      map[string]string
 	BuildOptPackages []string
 	BuildLabelMap    map[string]string
+
+	// Platforms for use with buildx and publish command
+	Platforms string
+
+	// ExtraTags for published images like :latest
+	ExtraTags []string
 }
 
 var defaultDirPermissions os.FileMode = 0700
diff --git a/builder/publish.go b/builder/publish.go
new file mode 100644
index 00000000..0800dd43
--- /dev/null
+++ b/builder/publish.go
@@ -0,0 +1,133 @@
+// Copyright (c) Alex Ellis 2017. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package builder
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	v1execute "github.com/alexellis/go-execute/pkg/v1"
+	"github.com/openfaas/faas-cli/schema"
+	"github.com/openfaas/faas-cli/stack"
+)
+
+// PublishImage will publish images as multi-arch
+// TODO: refactor signature to a struct to simplify the length of the method header
+func PublishImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string,
+	buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool, copyExtraPaths []string, platforms string, extraTags []string) error {
+
+	if stack.IsValidTemplate(language) {
+		pathToTemplateYAML := fmt.Sprintf("./template/%s/template.yml", language)
+		if _, err := os.Stat(pathToTemplateYAML); os.IsNotExist(err) {
+			return err
+		}
+
+		langTemplate, err := stack.ParseYAMLForLanguageTemplate(pathToTemplateYAML)
+		if err != nil {
+			return fmt.Errorf("error reading language template: %s", err.Error())
+		}
+
+		branch, version, err := GetImageTagValues(tagMode)
+		if err != nil {
+			return err
+		}
+
+		imageName := schema.BuildImageName(tagMode, image, version, branch)
+
+		if err := ensureHandlerPath(handler); err != nil {
+			return fmt.Errorf("building %s, %s is an invalid path", imageName, handler)
+		}
+
+		tempPath, buildErr := createBuildContext(functionName, handler, language, isLanguageTemplate(language), langTemplate.HandlerFolder, copyExtraPaths)
+		fmt.Printf("Building: %s with %s template. Please wait..\n", imageName, language)
+		if buildErr != nil {
+			return buildErr
+		}
+
+		if shrinkwrap {
+			fmt.Printf("%s shrink-wrapped to %s\n", functionName, tempPath)
+			return nil
+		}
+
+		buildOptPackages, buildPackageErr := getBuildOptionPackages(buildOptions, language, langTemplate.BuildOptions)
+
+		if buildPackageErr != nil {
+			return buildPackageErr
+
+		}
+
+		dockerBuildVal := dockerBuild{
+			Image:            imageName,
+			NoCache:          nocache,
+			Squash:           squash,
+			HTTPProxy:        os.Getenv("http_proxy"),
+			HTTPSProxy:       os.Getenv("https_proxy"),
+			BuildArgMap:      buildArgMap,
+			BuildOptPackages: buildOptPackages,
+			BuildLabelMap:    buildLabelMap,
+			Platforms:        platforms,
+			ExtraTags:        extraTags,
+		}
+
+		command, args := getDockerBuildxCommand(dockerBuildVal)
+		fmt.Printf("Publishing with command: %v %v\n", command, args)
+
+		task := v1execute.ExecTask{
+			Cwd:         tempPath,
+			Command:     command,
+			Args:        args,
+			StreamStdio: !quietBuild,
+		}
+
+		res, err := task.Execute()
+
+		if err != nil {
+			return err
+		}
+
+		if res.ExitCode != 0 {
+			return fmt.Errorf("[%s] received non-zero exit code from build, error: %s", functionName, res.Stderr)
+		}
+
+		fmt.Printf("Image: %s built.\n", imageName)
+
+	} else {
+		return fmt.Errorf("language template: %s not supported, build a custom Dockerfile", language)
+	}
+
+	return nil
+}
+
+func getDockerBuildxCommand(build dockerBuild) (string, []string) {
+	flagSlice := buildFlagSlice(build.NoCache, build.Squash, build.HTTPProxy, build.HTTPSProxy, build.BuildArgMap,
+		build.BuildOptPackages, build.BuildLabelMap)
+
+	// pushOnly defined at https://github.com/docker/buildx
+	const pushOnly = "--output=type=registry,push=true"
+
+	args := []string{"buildx", "build", "--progress=plain", "--platform=" + build.Platforms, pushOnly}
+
+	args = append(args, flagSlice...)
+
+	for _, t := range build.ExtraTags {
+		var tag string
+		if i := strings.LastIndex(build.Image, ":"); i > -1 {
+			tag = applyTag(i, build.Image, t)
+		} else {
+			tag = applyTag(len(build.Image)-1, build.Image, t)
+		}
+		args = append(args, "--tag", tag)
+	}
+
+	args = append(args, "--tag", build.Image, ".")
+
+	command := "docker"
+
+	return command, args
+}
+
+func applyTag(index int, baseImage, tag string) string {
+	return fmt.Sprintf("%s:%s", baseImage[:index], tag)
+}
diff --git a/ci/hashgen.sh b/ci/hashgen.sh
index 25bab9ad..da454eff 100755
--- a/ci/hashgen.sh
+++ b/ci/hashgen.sh
@@ -1,3 +1,3 @@
 #!/bin/sh
 
-for f in faas-cli*; do shasum -a 256 $f > $f.sha256; done
\ No newline at end of file
+for f in bin/faas-cli*; do shasum -a 256 $f > $f.sha256; done
\ No newline at end of file
diff --git a/commands/build.go b/commands/build.go
index 63a035d8..16f142ae 100644
--- a/commands/build.go
+++ b/commands/build.go
@@ -73,12 +73,12 @@ var buildCmd = &cobra.Command{
                  [--lang <ruby|python|python3|node|csharp|dockerfile>]
                  [--no-cache] [--squash]
                  [--regex "REGEX"]
-				 [--filter "WILDCARD"]
-				 [--parallel PARALLEL_DEPTH]
-				 [--build-arg KEY=VALUE]
-				 [--build-option VALUE]
-				 [--copy-extra PATH]
-				 [--tag <sha|branch|describe>]`,
+                 [--filter "WILDCARD"]
+                 [--parallel PARALLEL_DEPTH]
+                 [--build-arg KEY=VALUE]
+                 [--build-option VALUE]
+                 [--copy-extra PATH]
+                 [--tag <sha|branch|describe>]`,
 	Short: "Builds OpenFaaS function containers",
 	Long: `Builds OpenFaaS function containers either via the supplied YAML config using
 the "--yaml" flag (which may contain multiple function definitions), or directly
diff --git a/commands/publish.go b/commands/publish.go
new file mode 100644
index 00000000..0aff0a0c
--- /dev/null
+++ b/commands/publish.go
@@ -0,0 +1,234 @@
+// Copyright (c) Alex Ellis 2017. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package commands
+
+import (
+	"fmt"
+	"os"
+	"sync"
+	"time"
+
+	v1execute "github.com/alexellis/go-execute/pkg/v1"
+	"github.com/morikuni/aec"
+	"github.com/openfaas/faas-cli/builder"
+	"github.com/openfaas/faas-cli/stack"
+	"github.com/spf13/cobra"
+)
+
+var (
+	platforms string
+	extraTags []string
+)
+
+func init() {
+	// Setup flags that are used by multiple commands (variables defined in faas.go)
+	publishCmd.Flags().StringVar(&image, "image", "", "Docker image name to build")
+	publishCmd.Flags().StringVar(&handler, "handler", "", "Directory with handler for function, e.g. handler.js")
+	publishCmd.Flags().StringVar(&functionName, "name", "", "Name of the deployed function")
+	publishCmd.Flags().StringVar(&language, "lang", "", "Programming language template")
+
+	// Setup flags that are used only by this command (variables defined above)
+	publishCmd.Flags().BoolVar(&nocache, "no-cache", false, "Do not use Docker's build cache")
+	publishCmd.Flags().BoolVar(&squash, "squash", false, `Use Docker's squash flag for smaller images [experimental] `)
+	publishCmd.Flags().IntVar(&parallel, "parallel", 1, "Build in parallel to depth specified.")
+	publishCmd.Flags().BoolVar(&shrinkwrap, "shrinkwrap", false, "Just write files to ./build/ folder for shrink-wrapping")
+	publishCmd.Flags().StringArrayVarP(&buildArgs, "build-arg", "b", []string{}, "Add a build-arg for Docker (KEY=VALUE)")
+	publishCmd.Flags().StringArrayVarP(&buildOptions, "build-option", "o", []string{}, "Set a build option, e.g. dev")
+	publishCmd.Flags().Var(&tagFormat, "tag", "Override latest tag on function Docker image, accepts 'latest', 'sha', 'branch', or 'describe'")
+	publishCmd.Flags().StringArrayVar(&buildLabels, "build-label", []string{}, "Add a label for Docker image (LABEL=VALUE)")
+	publishCmd.Flags().StringArrayVar(&copyExtra, "copy-extra", []string{}, "Extra paths that will be copied into the function build context")
+	publishCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yml file")
+	publishCmd.Flags().BoolVar(&quietBuild, "quiet", false, "Perform a quiet build, without showing output from Docker")
+	publishCmd.Flags().BoolVar(&disableStackPull, "disable-stack-pull", false, "Disables the template configuration in the stack.yml")
+	publishCmd.Flags().StringVar(&platforms, "platforms", "linux/amd64", "A set of platforms to publish")
+	publishCmd.Flags().StringArrayVar(&extraTags, "extra-tag", []string{""}, "Additional extra image tag")
+
+	// Set bash-completion.
+	_ = publishCmd.Flags().SetAnnotation("handler", cobra.BashCompSubdirsInDir, []string{})
+
+	faasCmd.AddCommand(publishCmd)
+}
+
+// publishCmd allows the user to build an OpenFaaS function container
+var publishCmd = &cobra.Command{
+	Use: `publish -f YAML_FILE [--no-cache] [--squash]
+  faas-cli publish --image IMAGE_NAME
+                   --handler HANDLER_DIR
+                   --name FUNCTION_NAME
+                   [--lang <ruby|python|python3|node|csharp|dockerfile>]
+                   [--no-cache] [--squash]
+                   [--regex "REGEX"]
+                   [--filter "WILDCARD"]
+                   [--parallel PARALLEL_DEPTH]
+                   [--build-arg KEY=VALUE]
+                   [--build-option VALUE]
+                   [--copy-extra PATH]
+                   [--tag <sha|branch|describe>]
+                   [--platforms linux/arm/v7]`,
+	Short: "Builds and pushes multi-arch OpenFaaS container images",
+	Long: `Builds and pushes multi-arch OpenFaaS container images using Docker buildx.
+Most users will want faas-cli build or faas-cli up for development and testing.
+This command is designed to make releasing and publishing multi-arch container 
+images easier.
+
+A stack.yaml file is required, and any images that are built will not be 
+available in the local Docker library. This is due to technical constraints in 
+Docker and buildx. You must use a multi-arch template to use this command with 
+correctly configured TARGETPLATFORM and BUILDPLATFORM arguments.
+
+See also: faas-cli build`,
+	Example: `  faas-cli publish --platforms linux/amd64,linux/arm64,linux/arm/7
+	faas-cli publish --platforms linux/arm/7 --filter webhook
+  faas-cli publish -f go.yml --no-cache --build-arg NPM_VERSION=0.2.2
+  faas-cli publish --build-option dev
+  faas-cli publish --tag sha
+  `,
+	PreRunE: preRunBuild,
+	RunE:    runPublish,
+}
+
+// preRunPublish validates args & flags
+func preRunPublish(cmd *cobra.Command, args []string) error {
+	language, _ = validateLanguageFlag(language)
+
+	mapped, err := parseBuildArgs(buildArgs)
+
+	if err == nil {
+		buildArgMap = mapped
+	}
+
+	buildLabelMap, err = parseMap(buildLabels, "build-label")
+
+	if parallel < 1 {
+		return fmt.Errorf("the --parallel flag must be great than 0")
+	}
+
+	return err
+}
+
+func runPublish(cmd *cobra.Command, args []string) error {
+
+	var services stack.Services
+	if len(yamlFile) > 0 {
+		parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst)
+		if err != nil {
+			return err
+		}
+
+		if parsedServices != nil {
+			services = *parsedServices
+		}
+	}
+
+	templateAddress := getTemplateURL("", os.Getenv(templateURLEnvironment), DefaultTemplateRepository)
+	if pullErr := PullTemplates(templateAddress); pullErr != nil {
+		return fmt.Errorf("could not pull templates for OpenFaaS: %v", pullErr)
+	}
+
+	task := v1execute.ExecTask{
+		Command:     "docker",
+		Args:        []string{"buildx", "create", "--use", "--name=multiarch", "--node=multiarch"},
+		StreamStdio: !quietBuild,
+		Env:         []string{"DOCKER_CLI_EXPERIMENTAL=enabled"},
+	}
+
+	res, err := task.Execute()
+	if err != nil {
+		return err
+	}
+	if res.ExitCode != 0 {
+		return fmt.Errorf("non-zero exit code: %d", res.ExitCode)
+	}
+
+	fmt.Printf("Created buildx node: %s\n", res.Stdout)
+
+	if len(services.StackConfiguration.TemplateConfigs) != 0 && !disableStackPull {
+		err := pullStackTemplates(services.StackConfiguration.TemplateConfigs, cmd)
+		if err != nil {
+			return fmt.Errorf("could not pull templates from function yaml file: %s", err.Error())
+		}
+	}
+
+	errors := publish(&services, parallel, shrinkwrap, quietBuild)
+	if len(errors) > 0 {
+		errorSummary := "Errors received during build:\n"
+		for _, err := range errors {
+			errorSummary = errorSummary + "- " + err.Error() + "\n"
+		}
+		return fmt.Errorf("%s", aec.Apply(errorSummary, aec.RedF))
+	}
+	return nil
+}
+
+func publish(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool) []error {
+	startOuter := time.Now()
+
+	errors := []error{}
+
+	wg := sync.WaitGroup{}
+
+	workChannel := make(chan stack.Function)
+
+	wg.Add(queueDepth)
+	for i := 0; i < queueDepth; i++ {
+		go func(index int) {
+			for function := range workChannel {
+				start := time.Now()
+
+				fmt.Printf(aec.YellowF.Apply("[%d] > Building %s.\n"), index, function.Name)
+				if len(function.Language) == 0 {
+					fmt.Println("Please provide a valid language for your function.")
+				} else {
+					combinedBuildOptions := combineBuildOpts(function.BuildOptions, buildOptions)
+					combinedBuildArgMap := mergeMap(function.BuildArgs, buildArgMap)
+					combinedExtraPaths := mergeSlice(services.StackConfiguration.CopyExtraPaths, copyExtra)
+					err := builder.PublishImage(function.Image,
+						function.Handler,
+						function.Name,
+						function.Language,
+						nocache,
+						squash,
+						shrinkwrap,
+						combinedBuildArgMap,
+						combinedBuildOptions,
+						tagFormat,
+						buildLabelMap,
+						quietBuild,
+						combinedExtraPaths,
+						platforms,
+						extraTags,
+					)
+
+					if err != nil {
+						errors = append(errors, err)
+					}
+				}
+
+				duration := time.Since(start)
+				fmt.Printf(aec.YellowF.Apply("[%d] < Building %s done in %1.2fs.\n"), index, function.Name, duration.Seconds())
+			}
+
+			fmt.Printf(aec.YellowF.Apply("[%d] Worker done.\n"), index)
+			wg.Done()
+		}(i)
+
+	}
+
+	for k, function := range services.Functions {
+		if function.SkipBuild {
+			fmt.Printf("Skipping build of: %s.\n", function.Name)
+		} else {
+			function.Name = k
+			workChannel <- function
+		}
+	}
+
+	close(workChannel)
+
+	wg.Wait()
+
+	duration := time.Since(startOuter)
+	fmt.Printf("\n%s\n", aec.Apply(fmt.Sprintf("Total build time: %1.2fs", duration.Seconds()), aec.YellowF))
+	return errors
+}
diff --git a/commands/store.go b/commands/store.go
index d10d55e8..11f0d5e8 100644
--- a/commands/store.go
+++ b/commands/store.go
@@ -20,8 +20,15 @@ var (
 	storeAddress     string
 	verbose          bool
 	storeDeployFlags DeployFlags
-	//Platform platform variable updated at build time
+	//Platform platform variable set at build time
 	Platform string
+	// if the CLI is built using buildx, then the Platform value needs to be mapped to
+	// one of the supported values used in the store.
+	shortPlatform = map[string]string{
+		"linux/arm/v6": "armhf",
+		"linux/amd64":  "x86_64",
+		"linux/arm64":  "arm64",
+	}
 )
 
 const (
@@ -131,7 +138,12 @@ func getPlatform() string {
 
 func getTargetPlatform(inputPlatform string) string {
 	if len(inputPlatform) == 0 {
-		return getPlatform()
+		currentPlatform := getPlatform()
+		target, ok := shortPlatform[currentPlatform]
+		if ok {
+			return target
+		}
+		return currentPlatform
 	}
 	return inputPlatform
 }
diff --git a/go.mod b/go.mod
index dbb103f0..272bcbec 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
 	github.com/morikuni/aec v1.0.0
 	github.com/openfaas/faas v0.0.0-20200422142642-18f6c720b50d
 	github.com/openfaas/faas-provider v0.15.1
+	github.com/openfaas/templates-sdk v0.0.0-20200723110415-a699ec277c12 // indirect
 	github.com/pkg/errors v0.8.1
 	github.com/ryanuber/go-glob v1.0.0
 	github.com/sirupsen/logrus v1.4.2 // indirect
diff --git a/go.sum b/go.sum
index e7a7d804..f466641e 100644
--- a/go.sum
+++ b/go.sum
@@ -84,6 +84,7 @@ github.com/openfaas/faas v0.0.0-20200422142642-18f6c720b50d h1:mlI37Hd4Htedk7iCd
 github.com/openfaas/faas v0.0.0-20200422142642-18f6c720b50d/go.mod h1:E0m2rLup0Vvxg53BKxGgaYAGcZa3Xl+vvL7vSi5yQ14=
 github.com/openfaas/faas-provider v0.15.1 h1:5M+DrGbuKlZxhN3/otFgLgBUnPQcYm6XosQDSbKXh30=
 github.com/openfaas/faas-provider v0.15.1/go.mod h1:ChVioeB3snmfYxBTu7SrWGYLoYN9fZkQlnuEQ86a6l0=
+github.com/openfaas/templates-sdk v0.0.0-20200723110415-a699ec277c12/go.mod h1:JWcVHdzlHcR7nLuaDL88Mz68wOqDvOn0CLO6t27OMhk=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
diff --git a/stack/schema.go b/stack/schema.go
index a2a63196..eaa473d7 100644
--- a/stack/schema.go
+++ b/stack/schema.go
@@ -63,6 +63,9 @@ type Function struct {
 
 	// BuildArgs for providing build-args
 	BuildArgs map[string]string `yaml:"build_args,omitempty"`
+
+	// Platforms for use with buildx and faas-cli publish
+	Platforms string `yaml:"platforms,omitempty"`
 }
 
 // Configuration for the stack.yml file
@@ -73,6 +76,7 @@ type Configuration struct {
 // StackConfiguration for the overall stack.yml
 type StackConfiguration struct {
 	TemplateConfigs []TemplateSource `yaml:"templates"`
+
 	// CopyExtraPaths specifies additional paths (relative to the stack file) that will be copied
 	// into the functions build context, e.g. specifying `"common"` will look for and copy the
 	// "common/" folder of file in the same root as the stack file.  All paths must be contained
@@ -109,8 +113,9 @@ type Services struct {
 
 // LanguageTemplate read from template.yml within root of a language template folder
 type LanguageTemplate struct {
-	Language     string        `yaml:"language,omitempty"`
-	FProcess     string        `yaml:"fprocess,omitempty"`
+	Language string `yaml:"language,omitempty"`
+	FProcess string `yaml:"fprocess,omitempty"`
+
 	BuildOptions []BuildOption `yaml:"build_options,omitempty"`
 	// WelcomeMessage is printed to the user after generating a function
 	WelcomeMessage string `yaml:"welcome_message,omitempty"`


Test patch:
diff --git a/build_integration_test.sh b/build_integration_test.sh
index 7b685a4b..47f86746 100755
--- a/build_integration_test.sh
+++ b/build_integration_test.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-cli="./faas-cli"
+cli="./bin/faas-cli"
 template="python3"
 
 get_package() {
diff --git a/builder/build_test.go b/builder/build_test.go
index 544afd86..a754efaf 100644
--- a/builder/build_test.go
+++ b/builder/build_test.go
@@ -42,7 +42,7 @@ func Test_getDockerBuildCommand_NoOpts(t *testing.T) {
 		BuildOptPackages: []string{},
 	}
 
-	want := "build -t imagename:latest ."
+	want := "build --tag imagename:latest ."
 	wantCommand := "docker"
 
 	command, args := getDockerBuildCommand(dockerBuildVal)
@@ -69,7 +69,7 @@ func Test_getDockerBuildCommand_WithNoCache(t *testing.T) {
 		BuildOptPackages: []string{},
 	}
 
-	want := "build --no-cache -t imagename:latest ."
+	want := "build --no-cache --tag imagename:latest ."
 
 	wantCommand := "docker"
 
@@ -97,7 +97,7 @@ func Test_getDockerBuildCommand_WithProxies(t *testing.T) {
 		BuildOptPackages: []string{},
 	}
 
-	want := "build --build-arg http_proxy=http://127.0.0.1:3128 --build-arg https_proxy=https://127.0.0.1:3128 -t imagename:latest ."
+	want := "build --build-arg http_proxy=http://127.0.0.1:3128 --build-arg https_proxy=https://127.0.0.1:3128 --tag imagename:latest ."
 
 	wantCommand := "docker"