Repository: argoproj/argo (go)

Issue:
Workflow parameter `creationTimestamp` is not RFC3339 compliant
Checklist:

 * [x] I've included the version.
 * [x] I've included reproduction steps.
 * [x] I've included the workflow YAML.
 * [x] I've included the logs.
 
**What happened**:

Workflow parameter `workflow.creationTimestamp` did not output a timestamp in RFC3339 compliant format.

**What you expected to happen**:

[Documentation states it should output a timestamp compliant with RFC3339](https://github.com/argoproj/argo/blob/master/docs/variables.md)

**How to reproduce it (as minimally and precisely as possible)**:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
spec:
  entrypoint: whalesay
  templates:
    - name: whalesay
      container:
        image: docker/whalesay:latest
        command: [cowsay]
        args: ["{{workflow.creationTimestamp}}"]
```

Output:
```
 _______________________________ 
< 2020-05-07 16:51:21 +0000 UTC >
 ------------------------------- 
    \
     \
      \     
                    ##        .            
              ## ## ##       ==            
           ## ## ## ##      ===            
       /""""""""""""""""___/ ===        
  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~   
       \______ o          __/            
        \    \        __/             
          \____\______/   
```

**Anything else we need to know?**:

According to my reading of the spec and the [output](https://repl.it/repls/CrazyIgnorantDatum) of Python's [rfc3339-validator](https://pypi.org/project/rfc3339-validator/) package, "2020-05-07 16:51:21 +0000 UTC" is not valid - there should be a 'T' between date and time, there should not be a space before offset, the offset should be formatted hh:mm and there should not be a timezone on the end.

I think equivalent compliant timestamps would be `2020-05-07T16:51:21+00:00` and `2020-05-07T16:51:21Z`

**Environment**:

- Argo version:
```
$ argo version
argo: vv2.7.4+50b209c.dirty
  BuildDate: 2020-04-16T16:27:43Z
  GitCommit: 50b209ca14c056fb470ebb8329e255304dd5be90
  GitTreeState: dirty
  GitTag: v2.7.4
  GoVersion: go1.13
  Compiler: gc
  Platform: linux/amd64
```
- Kubernetes version :
```
$ kubectl version -o yaml
clientVersion:
  buildDate: "2019-06-19T16:40:16Z"
  compiler: gc
  gitCommit: e8462b5b5dc2584fdcd18e6bcfe9f1e4d970a529
  gitTreeState: clean
  gitVersion: v1.15.0
  goVersion: go1.12.5
  major: "1"
  minor: "15"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-03-31T02:49:49Z"
  compiler: gc
  gitCommit: a5bf731ea129336a3cf32c3375317b3a626919d7
  gitTreeState: clean
  gitVersion: v1.15.11-gke.5
  goVersion: go1.12.17b4
  major: "1"
  minor: 15+
  platform: linux/amd64
```

**Other debugging information (if applicable)**:

- workflow result:

```
argo --loglevel DEBUG get <workflowname>
Name:                hello-world-q925x
Namespace:           default
ServiceAccount:      default
Status:              Succeeded
Created:             Thu May 07 17:51:21 +0100 (14 minutes ago)
Started:             Thu May 07 17:51:21 +0100 (14 minutes ago)
Finished:            Thu May 07 17:51:33 +0100 (14 minutes ago)
Duration:            12 seconds

STEP                  TEMPLATE  PODNAME            DURATION  MESSAGE
 ✔ hello-world-q925x  whalesay  hello-world-q925x  10s       
```

- executor logs:

```
kubectl logs <failedpodname> -c init
kubectl logs <failedpodname> -c wait
( no failed pods )
```

- workflow-controller logs:

```
kubectl logs -n argo $(kubectl get pods -l app=workflow-controller -n argo -o name)

time="2020-05-07T16:51:21Z" level=info msg="Pod node {hello-world-q925x hello-world-q925x hello-world-q925x Pod whalesay nil    Pending   2020-05-07 16:51:21.821953905 +0000 UTC 0001-01-01 00:00:00 +0000 UTC   <nil> nil nil [] []} initialized Pending" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:21Z" level=info msg="Created pod: hello-world-q925x (hello-world-q925x)" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:21Z" level=info msg="Workflow update successful" namespace=default phase=Running resourceVersion=7777759 workflow=hello-world-q925x
time="2020-05-07T16:51:22Z" level=info msg="Processing workflow" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:22Z" level=info msg="Updating node &NodeStatus{ID:hello-world-q925x,Name:hello-world-q925x,DisplayName:hello-world-q925x,Type:Pod,TemplateName:whalesay,TemplateRef:nil,Phase:Pending,BoundaryID:,Message:,StartedAt:2020-05-07 16:51:21 +0000 UTC,FinishedAt:0001-01-01 00:00:00 +0000 UTC,PodIP:,Daemoned:nil,Inputs:nil,Outputs:nil,Children:[],OutboundNodes:[],StoredTemplateID:,WorkflowTemplateName:,TemplateScope:,ResourcesDuration:ResourcesDuration{},} message: ContainerCreating"
time="2020-05-07T16:51:22Z" level=info msg="Workflow update successful" namespace=default phase=Running resourceVersion=7777768 workflow=hello-world-q925x
time="2020-05-07T16:51:23Z" level=info msg="Processing workflow" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:31Z" level=info msg="Processing workflow" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:31Z" level=info msg="Updating node &NodeStatus{ID:hello-world-q925x,Name:hello-world-q925x,DisplayName:hello-world-q925x,Type:Pod,TemplateName:whalesay,TemplateRef:nil,Phase:Pending,BoundaryID:,Message:ContainerCreating,StartedAt:2020-05-07 16:51:21 +0000 UTC,FinishedAt:0001-01-01 00:00:00 +0000 UTC,PodIP:,Daemoned:nil,Inputs:nil,Outputs:nil,Children:[],OutboundNodes:[],StoredTemplateID:,WorkflowTemplateName:,TemplateScope:,ResourcesDuration:ResourcesDuration{},} status Pending -> Running"
time="2020-05-07T16:51:31Z" level=info msg="Workflow update successful" namespace=default phase=Running resourceVersion=7777806 workflow=hello-world-q925x
time="2020-05-07T16:51:32Z" level=info msg="Processing workflow" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:33Z" level=info msg="Processing workflow" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:33Z" level=info msg="Updating node &NodeStatus{ID:hello-world-q925x,Name:hello-world-q925x,DisplayName:hello-world-q925x,Type:Pod,TemplateName:whalesay,TemplateRef:nil,Phase:Running,BoundaryID:,Message:,StartedAt:2020-05-07 16:51:21 +0000 UTC,FinishedAt:0001-01-01 00:00:00 +0000 UTC,PodIP:,Daemoned:nil,Inputs:nil,Outputs:nil,Children:[],OutboundNodes:[],StoredTemplateID:,WorkflowTemplateName:,TemplateScope:,ResourcesDuration:ResourcesDuration{},} status Running -> Succeeded"
time="2020-05-07T16:51:33Z" level=info msg="Updated phase Running -> Succeeded" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:33Z" level=info msg="Marking workflow completed" namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:33Z" level=info msg="Checking daemoned children of " namespace=default workflow=hello-world-q925x
time="2020-05-07T16:51:33Z" level=info msg="Workflow update successful" namespace=default phase=Succeeded resourceVersion=7777814 workflow=hello-world-q925x
time="2020-05-07T16:51:34Z" level=info msg="Labeled pod default/hello-world-q925x completed"

```

**Logs**

```
argo get <workflowname>
kubectl logs <failedpodname> -c init
kubectl logs <failedpodname> -c wait
kubectl logs -n argo $(kubectl get pods -l app=workflow-controller -n argo -o name)
(where relevant supplied above)
```

---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

If you are impacted by this bug please add a 👍 reaction to this issue! We often sort issues this way to know what to prioritize.


Pull request:
fix(controller)!: Correctly format workflow.creationTimestamp as RFC3339. Fixes #2974
Checklist:

* [x] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this is a chore.
* [x] The title of the PR is (a) [conventional](https://www.conventionalcommits.org/en/v1.0.0/), (b) states what changed, and (c) suffixes the related issues number. E.g. `"fix(controller): Updates such and such. Fixes #1234"`.  
* [x] I've signed the CLA.
* [x] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
* [x] My builds are green. Try syncing with master if they are not. 
* [x] My organization is added to USERS.md.

Fixes #2974

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3b17e30ad..2ff911aff 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -233,7 +233,7 @@ jobs:
             - yarn-packages-v3-{{ checksum "ui/yarn.lock" }}
       - run:
           name: Download dependencies
-          command: yarn --cwd ui install --frozen-lockfile --ignore-optional --non-interactive
+          command: yarn --cwd ui install
       # - run: yarn test
       - run: yarn --cwd ui build
       - run: yarn --cwd ui lint
diff --git a/Dockerfile b/Dockerfile
index 57d8d4236..5aa9d8551 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -75,7 +75,7 @@ FROM node:14.0.0 as argo-ui
 
 ADD ["ui", "."]
 
-RUN yarn install --frozen-lockfile --ignore-optional --non-interactive
+RUN yarn install
 RUN yarn build
 
 ####################################################################################################
@@ -91,20 +91,26 @@ WORKDIR /go/src/github.com/argoproj/argo
 COPY . .
 # check we can use Git
 RUN git rev-parse HEAD
-COPY --from=argo-ui node_modules ui/node_modules
 RUN mkdir -p ui/dist
 COPY --from=argo-ui dist/app ui/dist/app
 # stop make from trying to re-build this without yarn installed
 RUN touch ui/dist/node_modules.marker
 RUN touch ui/dist/app/index.html
-# fail the build if we are "dirty"
+# fail the build if we are "dirty" prior to build
 RUN git diff --exit-code
-RUN make argo-server.crt argo-server.key
-RUN if [ "${IMAGE_OS}" = "linux" -a "${IMAGE_ARCH}" = "amd64" ]; then \
-	make dist/argo-linux-amd64 dist/workflow-controller-linux-amd64 dist/argoexec-linux-amd64; \
-    elif [ "${IMAGE_OS}" = "linux" -a "${IMAGE_ARCH}" = "arm64" ]; then \
-	make dist/argo-linux-arm64 dist/workflow-controller-linux-arm64 dist/argoexec-linux-arm64; \
-    fi 
+# order is important, must build the CLI first, as building can make the build dirty
+RUN make \
+    argo-server.crt \
+    argo-server.key \
+    dist/argo-linux-${IMAGE_ARCH} \
+    dist/workflow-controller-linux-${IMAGE_ARCH} \
+    dist/argoexec-linux-${IMAGE_ARCH}
+# double check "dirty"
+RUN git diff --exit-code
+# triple check "dirty"
+RUN ["sh", "-c", "./dist/workflow-controller-linux-${IMAGE_ARCH} version | grep clean"]
+# we can't check the argo cli, it must have a Kubernetes cluster to work
+RUN ["sh", "-c", "./dist/argoexec-linux-${IMAGE_ARCH} version | grep clean"]
 
 ####################################################################################################
 # argoexec
diff --git a/Makefile b/Makefile
index 23062d514..023d24365 100644
--- a/Makefile
+++ b/Makefile
@@ -35,14 +35,7 @@ VERSION := latest
 endif
 
 ifneq ($(findstring release,$(GIT_BRANCH)),)
-# this will be something like "v2.5" or "v3.7"
-MAJOR_MINOR := v$(word 2,$(subst -, ,$(GIT_BRANCH)))
-# if GIT_TAG is on HEAD, then this will be the same
-GIT_LATEST_TAG := $(shell git tag --merged | tail -n1)
-# only use the latest tag if it matches the correct major/minor version
-ifneq ($(findstring $(MAJOR_MINOR),$(GIT_LATEST_TAG)),)
-VERSION := $(GIT_LATEST_TAG)
-endif
+VERSION := $(shell git tag --points-at=HEAD|grep ^v|head -n1)
 endif
 
 # MANIFESTS_VERSION is the version to be used for files in manifests and should always be latests unles we are releasing
@@ -78,7 +71,7 @@ CONTROLLER_IMAGE_FILE  := dist/controller-image.$(VERSION)
 STATIC_BUILD          ?= true
 CI                    ?= false
 DB                    ?= postgres
-K3D                   := $(shell if [ "`kubectl config current-context`" = "k3s-default" ]; then echo true; else echo false; fi)
+K3D                   := $(shell if [ `which kubectl` <> '' ] && [ "`kubectl config current-context`" = "k3s-default" ]; then echo true; else echo false; fi)
 # which components to start, useful if you want to disable them to debug
 COMPONENTS            := controller,argo-server
 LOG_LEVEL             := debug
@@ -127,7 +120,7 @@ endef
 define docker_build
 	# If we're making a dev build, we build this locally (this will be faster due to existing Go build caches).
 	if [ $(DEV_IMAGE) = true ]; then $(MAKE) dist/$(2)-$(OUTPUT_IMAGE_OS)-$(OUTPUT_IMAGE_ARCH) && mv dist/$(2)-$(OUTPUT_IMAGE_OS)-$(OUTPUT_IMAGE_ARCH) $(2); fi
-	docker build $(DOCKER_BUILD_OPTS) -t $(IMAGE_NAMESPACE)/$(1):$(VERSION) --target $(1) -f $(DOCKERFILE) --build-arg IMAGE_OS=$(OUTPUT_IMAGE_OS) --build-arg IMAGE_ARCH=$(OUTPUT_IMAGE_ARCH) .
+	docker build --progress plain $(DOCKER_BUILD_OPTS) -t $(IMAGE_NAMESPACE)/$(1):$(VERSION) --target $(1) -f $(DOCKERFILE) --build-arg IMAGE_OS=$(OUTPUT_IMAGE_OS) --build-arg IMAGE_ARCH=$(OUTPUT_IMAGE_ARCH) .
 	if [ $(DEV_IMAGE) = true ]; then mv $(2) dist/$(2)-$(OUTPUT_IMAGE_OS)-$(OUTPUT_IMAGE_ARCH); fi
 	if [ $(K3D) = true ]; then k3d import-images $(IMAGE_NAMESPACE)/$(1):$(VERSION); fi
 	touch $(3)
@@ -152,7 +145,7 @@ ui/dist/node_modules.marker: ui/package.json ui/yarn.lock
 	# Get UI dependencies
 	@mkdir -p ui/node_modules
 ifeq ($(CI),false)
-	yarn --cwd ui install --frozen-lockfile --ignore-optional --non-interactive
+	yarn --cwd ui install
 endif
 	@mkdir -p ui/dist
 	touch ui/dist/node_modules.marker
@@ -167,8 +160,9 @@ else
 endif
 
 $(GOPATH)/bin/staticfiles:
-	# Install the "staticfiles" tool
+	$(call backup_go_mod)
 	go get bou.ke/staticfiles
+	$(call restore_go_mod)
 
 server/static/files.go: $(GOPATH)/bin/staticfiles ui/dist/app/index.html
 	# Pack UI into a Go file.
diff --git a/cmd/argoexec/commands/wait.go b/cmd/argoexec/commands/wait.go
index 1f25ba7bc..9a39b0788 100644
--- a/cmd/argoexec/commands/wait.go
+++ b/cmd/argoexec/commands/wait.go
@@ -43,32 +43,32 @@ func waitContainer() error {
 		// do not return here so we can still try to kill sidecars & save outputs
 	}
 
-	// Saving logs
-	logArt, err := wfExecutor.SaveLogs()
+	// Capture output script result
+	err := wfExecutor.CaptureScriptResult()
 	if err != nil {
 		wfExecutor.AddError(err)
 		return err
 	}
-	// Saving output parameters
-	err = wfExecutor.SaveParameters()
+	// Capture output script exit code
+	err = wfExecutor.CaptureScriptExitCode()
 	if err != nil {
 		wfExecutor.AddError(err)
 		return err
 	}
-	// Saving output artifacts
-	err = wfExecutor.SaveArtifacts()
+	// Saving logs
+	logArt, err := wfExecutor.SaveLogs()
 	if err != nil {
 		wfExecutor.AddError(err)
 		return err
 	}
-	// Capture output script result
-	err = wfExecutor.CaptureScriptResult()
+	// Saving output parameters
+	err = wfExecutor.SaveParameters()
 	if err != nil {
 		wfExecutor.AddError(err)
 		return err
 	}
-	// Capture output script exit code
-	err = wfExecutor.CaptureScriptExitCode()
+	// Saving output artifacts
+	err = wfExecutor.SaveArtifacts()
 	if err != nil {
 		wfExecutor.AddError(err)
 		return err
@@ -85,4 +85,4 @@ func waitContainer() error {
 	}
 
 	return nil
-}
+}
\ No newline at end of file
diff --git a/docs/configure-artifact-repository.md b/docs/configure-artifact-repository.md
index 15f763cc3..08d32561f 100644
--- a/docs/configure-artifact-repository.md
+++ b/docs/configure-artifact-repository.md
@@ -5,6 +5,17 @@ repository. Argo supports any S3 compatible artifact repository such as AWS, GCS
 and Minio. This section shows how to configure the artifact repository.
 Subsequent sections will show how to use it.
 
+| Name | Inputs | Outputs | Usage (Feb 2020) |
+|---|---|---|---|
+| Artifactory | Yes | Yes | 11% |
+| GCS | Yes | Yes | - |
+| Git | Yes | No | - |
+| HDFS | Yes | Yes | 3% |
+| HTTP | Yes | No | 2% |
+| OSS | Yes | Yes | - |
+| Raw | Yes | No | 5% |
+| S3 | Yes | Yes | 86% | 
+
 ## Configuring Minio
 
 ```
diff --git a/docs/releasing.md b/docs/releasing.md
index 8320dce9c..70622b34a 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -16,7 +16,27 @@ To generate new manifests and perform basic checks:
 
 Publish the images and local Git changes (disabling K3D as this is faster and more reliable for releases):
 
-    make publish-release K3D=false
+    make publish-release K3D=false VERSION=v2.7.2
+
+* [ ] Check the images were pushed successfully.
+
+```
+docker pull argoproj/workflow-controller:v2.7.2
+docker pull argoproj/argoexec:v2.7.2
+docker pull argoproj/argocli:v2.7.2
+```
+
+* [ ] Check the correct versions are printed:
+
+```
+docker run argoproj/workflow-controller:v2.7.2 version
+docker run argoproj/argoexec:v2.7.2 version
+docker run argoproj/argocli:v2.7.2 version
+```
+
+* [ ] Check the manifests contain the correct tags: https://raw.githubusercontent.com/argoproj/argo/v2.7.2/manifests/install.yaml
+
+### Release Notes
 
 Create [the release](https://github.com/argoproj/argo/releases) in Github. You can get some text for this using [Github Toolkit](https://github.com/alexec/github-toolkit):
 
@@ -27,8 +47,28 @@ Release notes checklist:
 * [ ] All breaking changes are listed with migration steps
 * [ ] The release notes identify every publicly known vulnerability with a CVE assignment 
 
+### Update Stable Tag
+
 If this is GA:
 
 * [ ] Update the `stable` tag
+
+```
+git tag -f stable
+git push -f origin stable
+```
+
+* [ ] Check the manifests contain the correct tags: https://raw.githubusercontent.com/argoproj/argo/stable/manifests/install.yaml
+
+### Update Homebrew
+
+If this is GA:
+
 * [ ] Update the [Homebrew tap](https://github.com/argoproj/homebrew-tap).
+* [ ] Check Homebrew was successfully updated:
  
+ ```
+ brew upgrade argoproj/tap/argo
+ argo version
+ ```
+
diff --git a/pkg/apis/workflow/v1alpha1/workflow_types.go b/pkg/apis/workflow/v1alpha1/workflow_types.go
index 06b644e05..2429afe0c 100644
--- a/pkg/apis/workflow/v1alpha1/workflow_types.go
+++ b/pkg/apis/workflow/v1alpha1/workflow_types.go
@@ -697,6 +697,18 @@ type ArtifactLocation struct {
 	GCS *GCSArtifact `json:"gcs,omitempty" protobuf:"bytes,9,opt,name=gcs"`
 }
 
+// HasLocation whether or not an artifact has a location defined
+func (a *ArtifactLocation) HasLocation() bool {
+	return a.S3.HasLocation() ||
+		a.Git.HasLocation() ||
+		a.HTTP.HasLocation() ||
+		a.Artifactory.HasLocation() ||
+		a.Raw.HasLocation() ||
+		a.HDFS.HasLocation() ||
+		a.OSS.HasLocation() ||
+		a.GCS.HasLocation()
+}
+
 type ArtifactRepositoryRef struct {
 	ConfigMap string `json:"configMap,omitempty" protobuf:"bytes,1,opt,name=configMap"`
 	Key       string `json:"key,omitempty" protobuf:"bytes,2,opt,name=key"`
@@ -1295,7 +1307,7 @@ type S3Artifact struct {
 }
 
 func (s *S3Artifact) HasLocation() bool {
-	return s != nil && s.Bucket != ""
+	return s != nil && s.Endpoint != "" && s.Bucket != "" && s.Key != ""
 }
 
 // GitArtifact is the location of an git artifact
@@ -1708,18 +1720,6 @@ func (args *Arguments) GetParameterByName(name string) *Parameter {
 	return nil
 }
 
-// HasLocation whether or not an artifact has a location defined
-func (a *Artifact) HasLocation() bool {
-	return a.S3.HasLocation() ||
-		a.Git.HasLocation() ||
-		a.HTTP.HasLocation() ||
-		a.Artifactory.HasLocation() ||
-		a.Raw.HasLocation() ||
-		a.HDFS.HasLocation() ||
-		a.OSS.HasLocation() ||
-		a.GCS.HasLocation()
-}
-
 // GetTemplateByName retrieves a defined template by its name
 func (wf *Workflow) GetTemplateByName(name string) *Template {
 	for _, t := range wf.Spec.Templates {
diff --git a/ui/.yarnrc b/ui/.yarnrc
index 7be79daa7..79ed41f3a 100644
--- a/ui/.yarnrc
+++ b/ui/.yarnrc
@@ -1,2 +1,4 @@
 --add.ignore-optional true
 --install.ignore-optional true
+--install.frozen-lockfile true
+--install.non-interactive true
diff --git a/workflow/artifacts/artifactory/artifactory.go b/workflow/artifacts/artifactory/artifactory.go
index e5c626f63..f18fb2c9d 100644
--- a/workflow/artifacts/artifactory/artifactory.go
+++ b/workflow/artifacts/artifactory/artifactory.go
@@ -37,6 +37,9 @@ func (a *ArtifactoryArtifactDriver) Load(artifact *wfv1.Artifact, path string) e
 	defer func() {
 		_ = res.Body.Close()
 	}()
+	if res.StatusCode == 404 {
+		return errors.New(errors.CodeNotFound, res.Status)
+	}
 	if res.StatusCode < 200 || res.StatusCode >= 300 {
 		return errors.InternalErrorf("loading file from artifactory failed with reason:%s", res.Status)
 	}
diff --git a/workflow/artifacts/gcs/gcs.go b/workflow/artifacts/gcs/gcs.go
index 89702c513..f310f3659 100644
--- a/workflow/artifacts/gcs/gcs.go
+++ b/workflow/artifacts/gcs/gcs.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/argoproj/pkg/file"
 
+	"github.com/argoproj/argo/errors"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 )
 
@@ -65,13 +66,13 @@ func (g *ArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) error {
 			gcsClient, err := g.newGCSClient()
 			if err != nil {
 				log.Warnf("Failed to create new GCS client: %v", err)
-				return false, nil
+				return false, err
 			}
 			defer gcsClient.Close()
 			err = downloadObjects(gcsClient, inputArtifact.GCS.Bucket, inputArtifact.GCS.Key, path)
 			if err != nil {
 				log.Warnf("Failed to download objects from GCS: %v", err)
-				return false, nil
+				return false, err
 			}
 			return true, nil
 		})
@@ -87,7 +88,7 @@ func downloadObjects(client *storage.Client, bucket, key, path string) error {
 	for _, objName := range objNames {
 		err = downloadObject(client, bucket, key, objName, path)
 		if err != nil {
-			return fmt.Errorf("download object: %v", err)
+			return err
 		}
 	}
 	return nil
@@ -107,6 +108,9 @@ func downloadObject(client *storage.Client, bucket, key, objName, path string) e
 	ctx := context.Background()
 	rc, err := client.Bucket(bucket).Object(objName).NewReader(ctx)
 	if err != nil {
+		if err == storage.ErrObjectNotExist {
+			return errors.New(errors.CodeNotFound, err.Error())
+		}
 		return fmt.Errorf("new bucket reader: %v", err)
 	}
 	defer rc.Close()
@@ -152,13 +156,12 @@ func (g *ArtifactDriver) Save(path string, outputArtifact *wfv1.Artifact) error
 			log.Infof("GCS Save path: %s, key: %s", path, outputArtifact.GCS.Key)
 			client, err := g.newGCSClient()
 			if err != nil {
-				log.Warnf("Failed to create new GCS client: %v", err)
-				return false, nil
+				return false, err
 			}
 			defer client.Close()
 			err = uploadObjects(client, outputArtifact.GCS.Bucket, outputArtifact.GCS.Key, path)
 			if err != nil {
-				log.Warnf("Failed to upload objects: %v", err)
+				return false, err
 			}
 			return true, nil
 		})
diff --git a/workflow/artifacts/git/git.go b/workflow/artifacts/git/git.go
index ae7554ebd..98c31e43c 100644
--- a/workflow/artifacts/git/git.go
+++ b/workflow/artifacts/git/git.go
@@ -49,7 +49,7 @@ func (g *GitArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) erro
 }
 
 // Save is unsupported for git output artifacts
-func (g *GitArtifactDriver) Save(path string, outputArtifact *wfv1.Artifact) error {
+func (g *GitArtifactDriver) Save(string, *wfv1.Artifact) error {
 	return errors.Errorf(errors.CodeBadRequest, "Git output artifacts unsupported")
 }
 
diff --git a/workflow/artifacts/hdfs/hdfs.go b/workflow/artifacts/hdfs/hdfs.go
index 59c17765c..73ce77af0 100644
--- a/workflow/artifacts/hdfs/hdfs.go
+++ b/workflow/artifacts/hdfs/hdfs.go
@@ -131,7 +131,7 @@ func CreateDriver(ci resource.Interface, art *wfv1.HDFSArtifact) (*ArtifactDrive
 }
 
 // Load downloads artifacts from HDFS compliant storage
-func (driver *ArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) error {
+func (driver *ArtifactDriver) Load(_ *wfv1.Artifact, path string) error {
 	hdfscli, err := createHDFSClient(driver.Addresses, driver.HDFSUser, driver.KrbOptions)
 	if err != nil {
 		return err
@@ -169,7 +169,14 @@ func (driver *ArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) er
 		}
 	}
 
-	return hdfscli.CopyToLocal(driver.Path, path)
+	err = hdfscli.CopyToLocal(driver.Path, path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return errors.New(errors.CodeNotFound, err.Error())
+		}
+		return err
+	}
+	return nil
 }
 
 // Save saves an artifact to HDFS compliant storage
diff --git a/workflow/artifacts/http/http.go b/workflow/artifacts/http/http.go
index 10f12ff4a..5547ad453 100644
--- a/workflow/artifacts/http/http.go
+++ b/workflow/artifacts/http/http.go
@@ -1,9 +1,13 @@
 package http
 
 import (
+	"os/exec"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+
 	"github.com/argoproj/argo/errors"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
-	"github.com/argoproj/argo/workflow/common"
 )
 
 // HTTPArtifactDriver is the artifact driver for a HTTP URL
@@ -12,9 +16,25 @@ type HTTPArtifactDriver struct{}
 // Load download artifacts from an HTTP URL
 func (h *HTTPArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) error {
 	// Download the file to a local file path
-	return common.RunCommand("curl", "-sS", "-L", "-o", path, inputArtifact.HTTP.URL)
+	args := []string{"-fsS", "-L", "-o", path, inputArtifact.HTTP.URL}
+	log.Info(strings.Join(append([]string{"curl"}, args...), " "))
+	cmd := exec.Command("curl", args...)
+	output, err := cmd.CombinedOutput()
+	log.Info(string(output))
+	if err != nil {
+		log.WithError(err).Error()
+		if exitErr, ok := err.(*exec.ExitError); ok {
+			// https://ec.haxx.se/usingcurl/usingcurl-returns
+			// 22 - HTTP page not retrieved.
+			if exitErr.ExitCode() == 22 {
+				return errors.New(errors.CodeNotFound, exitErr.Error())
+			}
+		}
+		return err
+	}
+	return nil
 }
 
-func (h *HTTPArtifactDriver) Save(path string, outputArtifact *wfv1.Artifact) error {
+func (h *HTTPArtifactDriver) Save(string, *wfv1.Artifact) error {
 	return errors.Errorf(errors.CodeBadRequest, "HTTP output artifacts unsupported")
 }
diff --git a/workflow/artifacts/oss/oss.go b/workflow/artifacts/oss/oss.go
index 50802691c..497f956c7 100644
--- a/workflow/artifacts/oss/oss.go
+++ b/workflow/artifacts/oss/oss.go
@@ -34,8 +34,7 @@ func (ossDriver *OSSArtifactDriver) Load(inputArtifact *wfv1.Artifact, path stri
 			log.Infof("OSS Load path: %s, key: %s", path, inputArtifact.OSS.Key)
 			osscli, err := ossDriver.newOSSClient()
 			if err != nil {
-				log.Warnf("Failed to create new OSS client: %v", err)
-				return false, nil
+				return false, err
 			}
 			bucketName := inputArtifact.OSS.Bucket
 			bucket, err := osscli.Bucket(bucketName)
diff --git a/workflow/artifacts/raw/raw.go b/workflow/artifacts/raw/raw.go
index 3742701d0..9cbadbbe0 100644
--- a/workflow/artifacts/raw/raw.go
+++ b/workflow/artifacts/raw/raw.go
@@ -25,6 +25,6 @@ func (a *RawArtifactDriver) Load(artifact *wfv1.Artifact, path string) error {
 }
 
 // Save is unsupported for raw output artifacts
-func (g *RawArtifactDriver) Save(path string, outputArtifact *wfv1.Artifact) error {
+func (g *RawArtifactDriver) Save(string, *wfv1.Artifact) error {
 	return errors.Errorf(errors.CodeBadRequest, "Raw output artifacts unsupported")
 }
diff --git a/workflow/artifacts/s3/s3.go b/workflow/artifacts/s3/s3.go
index a43f7dba1..ac9630a77 100644
--- a/workflow/artifacts/s3/s3.go
+++ b/workflow/artifacts/s3/s3.go
@@ -10,6 +10,7 @@ import (
 	"github.com/argoproj/pkg/file"
 	argos3 "github.com/argoproj/pkg/s3"
 
+	"github.com/argoproj/argo/errors"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/workflow/common"
 )
@@ -66,7 +67,7 @@ func (s3Driver *S3ArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string
 			}
 			if !isDir {
 				// It's neither a file, nor a directory. Return the original NoSuchKey error
-				return false, origErr
+				return false, errors.New(errors.CodeNotFound, origErr.Error())
 			}
 
 			if err = s3cli.GetDirectory(inputArtifact.S3.Bucket, inputArtifact.S3.Key, path); err != nil {
diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go
index 4789e1b4a..e446b1335 100644
--- a/workflow/controller/operator.go
+++ b/workflow/controller/operator.go
@@ -415,7 +415,7 @@ func (woc *wfOperationCtx) setGlobalParameters() {
 	woc.globalParams[common.GlobalVarWorkflowNamespace] = woc.wf.ObjectMeta.Namespace
 	woc.globalParams[common.GlobalVarWorkflowServiceAccountName] = woc.wf.Spec.ServiceAccountName
 	woc.globalParams[common.GlobalVarWorkflowUID] = string(woc.wf.ObjectMeta.UID)
-	woc.globalParams[common.GlobalVarWorkflowCreationTimestamp] = woc.wf.ObjectMeta.CreationTimestamp.String()
+	woc.globalParams[common.GlobalVarWorkflowCreationTimestamp] = woc.wf.ObjectMeta.CreationTimestamp.Format(time.RFC3339)
 	if woc.wf.Spec.Priority != nil {
 		woc.globalParams[common.GlobalVarWorkflowPriority] = strconv.Itoa(int(*woc.wf.Spec.Priority))
 	}
@@ -1529,10 +1529,16 @@ func (woc *wfOperationCtx) executeTemplate(nodeName string, orgTmpl wfv1.Templat
 			woc.computeMetrics(resolvedTmpl.Metrics.Prometheus, localScope, realTimeScope, true)
 		}
 		// Check if the node completed during this execution, if it did emit metrics
+		//
 		// This check is necessary because sometimes a node will be marked completed during the current execution and will
 		// not be considered again. The best example of this is the entrypoint steps/dag template (once completed, the
 		// workflow ends and it's not reconsidered). This checks makes sure that its metrics also get emitted.
-		if prevNodeStatus, ok := woc.preExecutionNodePhases[node.ID]; ok && !prevNodeStatus.Completed() && node.Completed() {
+		//
+		// In this check, a completed node may or may not have existed prior to this execution. If it did exist, ensure that it wasn't
+		// completed before this execution. If it did not exist prior, then we can infer that it was completed during this execution.
+		// The statement "(!ok || prevNodeStatus.Completed())" checks for this behavior and represents the material conditional
+		// "ok -> prevNodeStatus.Completed()" (https://en.wikipedia.org/wiki/Material_conditional)
+		if prevNodeStatus, ok := woc.preExecutionNodePhases[node.ID]; (!ok || prevNodeStatus.Completed()) && node.Completed() {
 			localScope, realTimeScope := woc.prepareMetricScope(node)
 			woc.computeMetrics(resolvedTmpl.Metrics.Prometheus, localScope, realTimeScope, false)
 		}
diff --git a/workflow/controller/steps.go b/workflow/controller/steps.go
index 64c6d2d63..b705a5839 100644
--- a/workflow/controller/steps.go
+++ b/workflow/controller/steps.go
@@ -99,8 +99,7 @@ func (woc *wfOperationCtx) executeSteps(nodeName string, tmplCtx *templateresolu
 			failMessage := fmt.Sprintf("step group %s was unsuccessful: %s", sgNode.ID, sgNode.Message)
 			woc.log.Info(failMessage)
 			woc.updateOutboundNodes(nodeName, tmpl)
-			_ = woc.markNodePhase(nodeName, wfv1.NodeFailed, sgNode.Message)
-			return node, nil
+			return woc.markNodePhase(nodeName, wfv1.NodeFailed, sgNode.Message), nil
 		}
 
 		// Add all outputs of each step in the group to the scope
diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go
index 7e958ec72..f8a10079e 100644
--- a/workflow/executor/executor.go
+++ b/workflow/executor/executor.go
@@ -164,6 +164,10 @@ func (we *WorkflowExecutor) LoadArtifacts() error {
 		tempArtPath := artPath + ".tmp"
 		err = artDriver.Load(&art, tempArtPath)
 		if err != nil {
+			if art.Optional && errors.IsCode(errors.CodeNotFound, err) {
+				log.Infof("Skipping optional input artifact that was not found: %s", art.Name)
+				continue
+			}
 			return err
 		}
 
@@ -414,6 +418,12 @@ func (we *WorkflowExecutor) isBaseImagePath(path string) bool {
 	// next check if path overlaps with a shared input-artifact emptyDir mounted by argo
 	for _, inArt := range we.Template.Inputs.Artifacts {
 		if path == inArt.Path {
+			// The input artifact may have been optional and not supplied. If this is the case, the file won't exist on
+			// the input artifact volume. Since this function was called, we know that we want to use this path as an
+			// ourput artifact, so we should look for it in the base image path.
+			if inArt.Optional && !inArt.HasLocation() {
+				return true
+			}
 			return false
 		}
 		if strings.HasPrefix(path, inArt.Path+"/") {


Test patch:
diff --git a/pkg/apis/workflow/v1alpha1/workflow_types_test.go b/pkg/apis/workflow/v1alpha1/workflow_types_test.go
index 6ed418b77..1b7409fca 100644
--- a/pkg/apis/workflow/v1alpha1/workflow_types_test.go
+++ b/pkg/apis/workflow/v1alpha1/workflow_types_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
 )
 
 func TestWorkflows(t *testing.T) {
@@ -24,6 +25,19 @@ func TestWorkflows(t *testing.T) {
 	assert.Equal(t, "3", wfs[3].Name)
 }
 
+func TestArtifactLocation_HasLocation(t *testing.T) {
+	assert.False(t, (&ArtifactLocation{}).HasLocation())
+	assert.False(t, (&ArtifactLocation{ArchiveLogs: pointer.BoolPtr(true)}).HasLocation())
+	assert.True(t, (&ArtifactLocation{S3: &S3Artifact{Key: "my-key", S3Bucket: S3Bucket{Endpoint: "my-endpoint", Bucket: "my-bucket"}}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{Git: &GitArtifact{Repo: "my-repo"}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{HTTP: &HTTPArtifact{URL: "my-url"}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{Artifactory: &ArtifactoryArtifact{URL: "my-url"}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{Raw: &RawArtifact{Data: "my-data"}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{HDFS: &HDFSArtifact{HDFSConfig: HDFSConfig{Addresses: []string{"my-address"}}}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{OSS: &OSSArtifact{Key: "my-key", OSSBucket: OSSBucket{Endpoint: "my-endpoint", Bucket: "my-bucket"}}}).HasLocation())
+	assert.True(t, (&ArtifactLocation{GCS: &GCSArtifact{Key: "my-key", GCSBucket: GCSBucket{Bucket: "my-bucket"}}}).HasLocation())
+}
+
 func TestNodes_FindByDisplayName(t *testing.T) {
 	assert.Nil(t, Nodes{}.FindByDisplayName(""))
 	assert.NotNil(t, Nodes{"": NodeStatus{DisplayName: "foo"}}.FindByDisplayName("foo"))
diff --git a/test/e2e/functional_test.go b/test/e2e/functional_test.go
index 07e0c2bad..311853abc 100644
--- a/test/e2e/functional_test.go
+++ b/test/e2e/functional_test.go
@@ -404,7 +404,7 @@ func (s *FunctionalSuite) TestStopBehavior() {
 			assert.NoError(t, err)
 			assert.Contains(t, output, "workflow stop-terminate stopped")
 		}).
-		WaitForWorkflow(30 * time.Second).
+		WaitForWorkflow(45 * time.Second).
 		Then().
 		ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
 			assert.Equal(t, wfv1.NodeFailed, status.Phase)
@@ -495,6 +495,30 @@ spec:
 		})
 }
 
+func (s *FunctionalSuite) TestSameInputOutputPathOptionalArtifact() {
+	s.Given().
+		Workflow("@testdata/same-input-output-path-optional.yaml").
+		When().
+		SubmitWorkflow().
+		WaitForWorkflow(30 * time.Second).
+		Then().
+		ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
+			assert.Equal(t, wfv1.NodeSucceeded, status.Phase)
+		})
+}
+
+func (s *FunctionalSuite) TestOptionalInputArtifacts() {
+	s.Given().
+		Workflow("@testdata/input-artifacts.yaml").
+		When().
+		SubmitWorkflow().
+		WaitForWorkflow(30 * time.Second).
+		Then().
+		ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
+			assert.Equal(t, wfv1.NodeSucceeded, status.Phase)
+		})
+}
+
 func TestFunctionalSuite(t *testing.T) {
 	suite.Run(t, new(FunctionalSuite))
 }
diff --git a/test/e2e/testdata/input-artifacts.yaml b/test/e2e/testdata/input-artifacts.yaml
new file mode 100644
index 000000000..160639613
--- /dev/null
+++ b/test/e2e/testdata/input-artifacts.yaml
@@ -0,0 +1,33 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  name: input-artifacts
+  labels:
+    argo-e2e: true
+spec:
+  entrypoint: main
+  templates:
+    - name: main
+      container:
+        image: argoproj/argosay:v2
+      inputs:
+        artifacts:
+          - name: my-http-artifact
+            path: /my-http-artifact
+            optional: true
+            http:
+              url: https://github.com/argoproj/argo/not-found
+          - name: my-s3-artifact
+            path: /my-s3-artifact
+            optional: true
+            s3:
+              bucket: my-bucket
+              key: my-key
+              endpoint: minio:9000
+              insecure: true
+              accessKeySecret:
+                name: my-minio-cred
+                key: accesskey
+              secretKeySecret:
+                name: my-minio-cred
+                key: secretkey
\ No newline at end of file
diff --git a/test/e2e/testdata/same-input-output-path-optional.yaml b/test/e2e/testdata/same-input-output-path-optional.yaml
new file mode 100644
index 000000000..47fdb5b24
--- /dev/null
+++ b/test/e2e/testdata/same-input-output-path-optional.yaml
@@ -0,0 +1,33 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  generateName: optional-input-output-artifact-same-location-
+  labels:
+    argo-e2e: true
+spec:
+  entrypoint: plan
+  templates:
+    - name: plan
+      steps:
+        - - name: write
+            template: file
+        - - name: read
+            template: file
+            arguments:
+              artifacts:
+                - name: file
+                  from: "{{steps.write.outputs.artifacts.file}}"
+    - name: file
+      inputs:
+        artifacts:
+          - name: file
+            path: /tmp/file1.txt
+            optional: true
+      container:
+        image: argoproj/argosay:v2
+        args: ["echo", "test", "/tmp/file1.txt"]
+      outputs:
+        artifacts:
+          - name: file
+            path: /tmp/file1.txt
+
diff --git a/workflow/artifacts/artifactory/artifactory_test.go b/workflow/artifacts/artifactory/artifactory_test.go
index fba6926ae..5d43a860b 100644
--- a/workflow/artifacts/artifactory/artifactory_test.go
+++ b/workflow/artifacts/artifactory/artifactory_test.go
@@ -1,61 +1,39 @@
-package artifactory_test
+package artifactory
 
 import (
-	"io/ioutil"
 	"os"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/argoproj/argo/errors"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
-	art "github.com/argoproj/argo/workflow/artifacts/artifactory"
 )
 
-const (
-	LoadFileName string = "argo_artifactory_test_load.txt"
-	SaveFileName string = "argo_artifactory_test_save.txt"
-	RepoName     string = "generic-local"
-	URL          string = "http://localhost:8081/artifactory/" + RepoName + "/" + LoadFileName
-	Username     string = "admin"
-	Password     string = "password"
-)
-
-func TestSaveAndLoad(t *testing.T) {
-
-	t.Skip("This test is skipped since it depends on external service")
-	fileContent := "time: " + string(time.Now().UnixNano())
-
-	// create file to test save
-	lf, err := ioutil.TempFile("", LoadFileName)
-	assert.NoError(t, err)
-	defer os.Remove(lf.Name())
-	// load file with test content
-	content := []byte(fileContent)
-	_, err = lf.Write(content)
-	assert.NoError(t, err)
-	err = lf.Close()
-	assert.NoError(t, err)
-
-	// create file to test load
-	sf, err := ioutil.TempFile("", SaveFileName)
-	assert.NoError(t, err)
-	defer os.Remove(sf.Name())
-
-	artL := &wfv1.Artifact{}
-	artL.Artifactory = &wfv1.ArtifactoryArtifact{
-		URL: URL,
-	}
-	driver := &art.ArtifactoryArtifactDriver{
-		Username: Username,
-		Password: Password,
-	}
-	err = driver.Save(lf.Name(), artL)
-	assert.NoError(t, err)
-	err = driver.Load(artL, sf.Name())
-	assert.NoError(t, err)
-
-	dat, err := ioutil.ReadFile(sf.Name())
-	assert.NoError(t, err)
-	assert.Equal(t, fileContent, string(dat))
+func TestArtifactoryArtifactDriver_Load(t *testing.T) {
+	driver := &ArtifactoryArtifactDriver{}
+	t.Run("NotFound", func(t *testing.T) {
+		err := driver.Load(&wfv1.Artifact{
+			ArtifactLocation: wfv1.ArtifactLocation{
+				Artifactory: &wfv1.ArtifactoryArtifact{URL: "https://github.com/argoproj/argo/not-found"},
+			},
+		}, "/tmp/not-found")
+		if assert.Error(t, err) {
+			argoError, ok := err.(errors.ArgoError)
+			if assert.True(t, ok) {
+				assert.Equal(t, errors.CodeNotFound, argoError.Code())
+			}
+		}
+	})
+	t.Run("Found", func(t *testing.T) {
+		err := driver.Load(&wfv1.Artifact{
+			ArtifactLocation: wfv1.ArtifactLocation{
+				Artifactory: &wfv1.ArtifactoryArtifact{URL: "https://github.com/argoproj/argo"},
+			},
+		}, "/tmp/found")
+		if assert.NoError(t, err) {
+			_, err := os.Stat("/tmp/found")
+			assert.NoError(t, err)
+		}
+	})
 }
diff --git a/workflow/artifacts/git/git_test.go b/workflow/artifacts/git/git_test.go
new file mode 100644
index 000000000..9b5d04e40
--- /dev/null
+++ b/workflow/artifacts/git/git_test.go
@@ -0,0 +1,35 @@
+package git
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
+)
+
+func TestGitArtifactDriver_Load(t *testing.T) {
+	driver := &GitArtifactDriver{}
+	t.Run("Found", func(t *testing.T) {
+		path := "/tmp/git-found"
+		assert.NoError(t, os.RemoveAll(path))
+		assert.NoError(t, os.MkdirAll(path, 0777))
+		d := uint64(1)
+		err := driver.Load(&wfv1.Artifact{
+			ArtifactLocation: wfv1.ArtifactLocation{
+				Git: &wfv1.GitArtifact{Repo: "https://github.com/argoproj/argoproj.git", Revision: "master", Depth: &d},
+			},
+		}, path)
+		if assert.NoError(t, err) {
+			_, err := os.Stat(path)
+			assert.NoError(t, err)
+		}
+	})
+}
+
+func TestGitArtifactDriver_Save(t *testing.T) {
+	driver := &GitArtifactDriver{}
+	err := driver.Save("", nil)
+	assert.Error(t, err)
+}
diff --git a/workflow/artifacts/http/http_test.go b/workflow/artifacts/http/http_test.go
new file mode 100644
index 000000000..fc2a7923c
--- /dev/null
+++ b/workflow/artifacts/http/http_test.go
@@ -0,0 +1,44 @@
+package http
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/argoproj/argo/errors"
+	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
+)
+
+func TestHTTPArtifactDriver_Load(t *testing.T) {
+	driver := &HTTPArtifactDriver{}
+	t.Run("NotFound", func(t *testing.T) {
+		err := driver.Load(&wfv1.Artifact{
+			ArtifactLocation: wfv1.ArtifactLocation{
+				HTTP: &wfv1.HTTPArtifact{URL: "https://github.com/argoproj/argo/not-found"},
+			},
+		}, "/tmp/not-found")
+		if assert.Error(t, err) {
+			argoError, ok := err.(errors.ArgoError)
+			if assert.True(t, ok) {
+				assert.Equal(t, errors.CodeNotFound, argoError.Code())
+			}
+		}
+	})
+	t.Run("Found", func(t *testing.T) {
+		err := driver.Load(&wfv1.Artifact{
+			ArtifactLocation: wfv1.ArtifactLocation{
+				HTTP: &wfv1.HTTPArtifact{URL: "https://github.com/argoproj/argo"},
+			},
+		}, "/tmp/found")
+		if assert.NoError(t, err) {
+			_, err := os.Stat("/tmp/found")
+			assert.NoError(t, err)
+		}
+	})
+}
+
+func TestHTTPArtifactDriver_Save(t *testing.T) {
+	driver := &HTTPArtifactDriver{}
+	assert.Error(t, driver.Save("", nil))
+}
diff --git a/workflow/controller/operator_metrics_test.go b/workflow/controller/operator_metrics_test.go
index 20efb06bb..dcd864959 100644
--- a/workflow/controller/operator_metrics_test.go
+++ b/workflow/controller/operator_metrics_test.go
@@ -139,3 +139,81 @@ func getMetricStringValue(metric prometheus.Metric) (string, error) {
 	}
 	return proto.CompactTextString(metricString), nil
 }
+
+var testMetricEmissionSameOperationCreationAndFailure = `
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  creationTimestamp: "2020-05-14T14:30:31Z"
+  name: steps-s5rz4
+spec:
+  arguments: {}
+  entrypoint: steps-1
+  onExit: whalesay
+  templates:
+  - arguments: {}
+    inputs: {}
+    metadata: {}
+    name: steps-1
+    outputs: {}
+    steps:
+    - - arguments: {}
+        name: hello2a
+        template: steps-2
+  - arguments: {}
+    inputs: {}
+    metadata: {}
+    metrics:
+      prometheus:
+      - counter:
+          value: "1"
+        gauge: null
+        help: Failure
+        histogram: null
+        labels: null
+        name: failure
+        when: '{{status}} == Failed'
+    name: steps-2
+    outputs: {}
+    steps:
+    - - arguments: {}
+        name: hello1
+        template: whalesay
+        withParam: mary had a little lamb
+  - arguments: {}
+    container:
+      args:
+      - hello
+      command:
+      - cowsay
+      image: docker/whalesay
+      name: ""
+      resources: {}
+    inputs: {}
+    metadata: {}
+    name: whalesay
+    outputs: {}
+status:
+  phase: Running
+  startedAt: "2020-05-14T14:30:31Z"
+`
+
+func TestMetricEmissionSameOperationCreationAndFailure(t *testing.T) {
+	cancel, controller := newController()
+	defer cancel()
+	wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("")
+	wf := unmarshalWF(testMetricEmissionSameOperationCreationAndFailure)
+	_, err := wfcset.Create(wf)
+	assert.NoError(t, err)
+	woc := newWorkflowOperationCtx(wf, controller)
+
+	woc.operate()
+
+	metricErrorDesc := wf.Spec.Templates[1].Metrics.Prometheus[0].GetDesc()
+	assert.NotNil(t, controller.metrics.GetCustomMetric(metricErrorDesc))
+
+	metricErrorCounter := controller.metrics.GetCustomMetric(metricErrorDesc).(prometheus.Counter)
+	metricErrorCounterString, err := getMetricStringValue(metricErrorCounter)
+	assert.NoError(t, err)
+	assert.Contains(t, metricErrorCounterString, `counter:<value:1 > `)
+}
diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go
index df75844d4..b49a07ba0 100644
--- a/workflow/controller/operator_test.go
+++ b/workflow/controller/operator_test.go
@@ -79,6 +79,24 @@ spec:
         mountPath: /mnt/existing-vol
 `
 
+func TestGlobalParams(t *testing.T) {
+	wf := unmarshalWF(helloWorldWf)
+	cancel, controller := newController(wf)
+	defer cancel()
+	woc := newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	if assert.Contains(t, woc.globalParams, "workflow.creationTimestamp") {
+		assert.NotContains(t, woc.globalParams["workflow.creationTimestamp"], "UTC")
+	}
+	assert.Contains(t, woc.globalParams, "workflow.duration")
+	assert.Contains(t, woc.globalParams, "workflow.labels.workflows.argoproj.io/phase")
+	assert.Contains(t, woc.globalParams, "workflow.name")
+	assert.Contains(t, woc.globalParams, "workflow.namespace")
+	assert.Contains(t, woc.globalParams, "workflow.parameters")
+	assert.Contains(t, woc.globalParams, "workflow.serviceAccountName")
+	assert.Contains(t, woc.globalParams, "workflow.uid")
+}
+
 // TestSidecarWithVolume verifies ia sidecar can have a volumeMount reference to both existing or volumeClaimTemplate volumes
 func TestSidecarWithVolume(t *testing.T) {
 	cancel, controller := newController()