Repository: serverless/serverless-azure-functions
Pull request #241 (2019-08-13 22:14:54)

perf: Skip upload to function app if configured to run from blob
Quick fix for not uploading zipped code to Function App if configured to only run from blob, and vice versa


Patch:
diff --git a/package-lock.json b/package-lock.json
index 63f220e..3969a80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,7 +1794,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1967,7 +1966,6 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2437,8 +2435,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
-      "optional": true
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2543,8 +2540,7 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
-      "optional": true
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2573,7 +2569,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
-      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2610,7 +2605,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
-      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2707,8 +2701,7 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
-      "optional": true
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2890,7 +2883,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3268,7 +3260,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3281,7 +3272,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3521,8 +3511,7 @@
     "deep-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
-      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
-      "dev": true
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
     },
     "deep-extend": {
       "version": "0.6.0",
@@ -3740,7 +3729,6 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3802,7 +3790,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4182,7 +4169,6 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4197,7 +4183,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
-      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -5476,7 +5461,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5486,7 +5470,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5496,7 +5479,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
-      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5815,8 +5797,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "optional": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7456,8 +7437,7 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "optional": true
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7548,7 +7528,6 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7574,7 +7553,6 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
-      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7665,14 +7643,12 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "optional": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
-      "optional": true
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8279,7 +8255,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
-      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8388,7 +8363,6 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
-      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8591,8 +8565,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "optional": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8659,7 +8632,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9072,7 +9044,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9462,7 +9433,6 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9658,8 +9628,7 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "optional": true
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
     },
     "source-map": {
       "version": "0.7.3",
@@ -10216,8 +10185,7 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
-      "optional": true
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11067,7 +11035,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11076,8 +11043,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "optional": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         }
       }
     },
diff --git a/package.json b/package.json
index 1396e58..b7cb6f8 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "@azure/ms-rest-nodeauth": "^1.0.1",
     "@azure/storage-blob": "^10.3.0",
     "axios": "^0.18.0",
+    "deep-equal": "^1.0.1",
     "js-yaml": "^3.13.1",
     "jsonpath": "^1.0.1",
     "lodash": "^4.16.6",
diff --git a/pipelines/publish-prerelease.yml b/pipelines/publish-prerelease.yml
new file mode 100644
index 0000000..3634d30
--- /dev/null
+++ b/pipelines/publish-prerelease.yml
@@ -0,0 +1,43 @@
+trigger:
+  branches:
+    include:
+    - dev
+pr: none
+
+variables:
+- group: npm-release-credentials
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+steps:
+- task: NodeTool@0
+  displayName: 'Use Node 10.x'
+  inputs:
+    versionSpec: 10.x
+
+- checkout: self
+  clean: true
+  persistCredentials: true
+  fetchDepth: 1
+
+- task: Bash@3
+  name: BumpNpmVersion
+  displayName: Bump NPM Prerelease Version
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/version.sh
+    arguments: 'serverless/serverless-azure-functions'
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
+    GITHUB_ACCESS_TOKEN: $(GITHUB_ACCESS_TOKEN)
+    SOURCE_BRANCH: $(Build.SourceBranch)
+
+- task: Bash@3
+  displayName: 'Publish serverless-azure-functions to NPM'
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/publish.sh
+    arguments: 'prerelease'
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
diff --git a/pipelines/publish-release.yml b/pipelines/publish-release.yml
new file mode 100644
index 0000000..b761823
--- /dev/null
+++ b/pipelines/publish-release.yml
@@ -0,0 +1,26 @@
+trigger: none
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+variables:
+- group: npm-release-credentials
+
+steps:
+- task: NodeTool@0
+  displayName: 'Use Node 10.x'
+  inputs:
+    versionSpec: 10.x
+
+- checkout: self
+  clean: true
+  persistCredentials: true
+  fetchDepth: 1
+
+- task: Bash@3
+  displayName: 'Publish serverless-azure-functions to NPM'
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/publish.sh
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
diff --git a/scripts/publish.sh b/scripts/publish.sh
new file mode 100644
index 0000000..7a4a807
--- /dev/null
+++ b/scripts/publish.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -eo pipefail
+
+$(pwd)/scripts/build.sh
+
+# NOTE: auth is taken care of via NPM_TOKEN env variable
+if [ -z "$1" ]; then
+  echo "Publishing 'latest' to NPM...";
+  npm publish
+else
+  echo "Publishing 'prerelease' to NPM...";
+  npm publish --tag=beta
+fi
diff --git a/scripts/version.sh b/scripts/version.sh
new file mode 100644
index 0000000..d6b54eb
--- /dev/null
+++ b/scripts/version.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -euo pipefail
+
+# Environment Variables Needed:
+# SOURCE_BRANCH
+# GITHUB_ACCESS_TOKEN
+
+PACKAGE_NAME=$1
+NPM_RELEASE_TYPE=${2-"prerelease"}
+
+# Get full branch name excluding refs/head from the env var SOURCE_BRANCH
+SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/}
+
+# Configure git to commit as SLS Azure Functions Service Account
+echo "Configuring git to use service account..."
+git config --local user.email "Serverless Azure Functions"
+git config --local user.name "sls-az@microsoft.com"
+
+git pull origin ${SOURCE_BRANCH_NAME}
+git checkout ${SOURCE_BRANCH_NAME}
+echo "Checked out branch: ${SOURCE_BRANCH_NAME}"
+
+NPM_VERSION=`npm version ${NPM_RELEASE_TYPE} -m "release: Update ${NPM_RELEASE_TYPE} version to %s ***NO_CI***"`
+echo "Set NPM version to: ${NPM_VERSION}"
+
+SHA=`git rev-parse HEAD`
+
+git remote add authOrigin https://${GITHUB_ACCESS_TOKEN}@github.com/serverless/serverless-azure-functions.git
+git push authOrigin ${SOURCE_BRANCH_NAME} --tags
+
+echo "Pushed new tag: ${PACKAGE_NAME}-${NPM_VERSION} @ SHA: ${SHA:0:8}"
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 8e38560..ff9d60f 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -56,8 +56,7 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
     const functionAppService = new FunctionAppService(this.serverless, this.options);
     const zipFile = functionAppService.getFunctionZipFile();
     if (!fs.existsSync(zipFile)) {
-      this.log(`Function app zip file '${zipFile}' does not exist`);
-      return Promise.resolve();
+      throw new Error(`Function app zip file '${zipFile}' does not exist`);
     }
     await resourceService.deployResourceGroup();
     const functionApp = await functionAppService.deploy();
diff --git a/src/services/armService.ts b/src/services/armService.ts
index d021b36..3abb531 100644
--- a/src/services/armService.ts
+++ b/src/services/armService.ts
@@ -8,6 +8,8 @@ import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "..
 import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { BaseService } from "./baseService";
+import { ResourceService } from "./resourceService"
+import deepEqual from "deep-equal";
 
 export class ArmService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -110,17 +112,56 @@ export class ArmService extends BaseService {
       }
     };
 
+    const resourceService = new ResourceService(this.serverless, this.options);
+    const latest = await resourceService.getLastDeploymentTemplate();
+
+    if (latest) {
+      const templateEqual = deepEqual(latest.template, deployment.template);
+      const mergedDefaultParameters = this.mergeDefaultParams(deploymentParameters, deployment.template.parameters);
+      const paramatersEqual = deepEqual(latest.parameters, mergedDefaultParameters);
+
+      if (templateEqual && paramatersEqual) {
+        this.log("Generated template same as previous. Skipping ARM deployment");
+        return;
+      }
+    }
+
     // Deploy ARM template
     this.log("-> Deploying ARM template...");
     this.log(`---> Resource Group: ${this.resourceGroup}`)
     this.log(`---> Deployment Name: ${this.deploymentName}`)
 
-    const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment);
+    const result = await this.resourceClient.deployments.createOrUpdate(
+      this.resourceGroup,
+      this.deploymentName,
+      armDeployment
+    );
     this.log("-> ARM deployment complete");
 
     return result;
   }
 
+  /**
+   * Merge parameters and default parameters for comparison with previously deployed template
+   * @param parameters Parameters with specified values
+   * @param defaultParameters Parameters with `type` and `defaultValue`
+   */
+  private mergeDefaultParams(parameters: any, defaultParameters: any) {
+    const mergedParams = {}
+    Object.keys(defaultParameters).forEach((key) => {
+      const defaultParam = defaultParameters[key];
+      mergedParams[key] = {
+        type: defaultParam.type,
+        value: (key in parameters)
+          ?
+          parameters[key].value
+          :
+          defaultParameters[key].defaultValue
+      }
+    });
+    return mergedParams;
+  }
+
   /**
    * Applies sls yaml environment variables into the appSettings section of the function app configuration
    * @param deployment The ARM deployment
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index dc21b70..43c3808 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -191,10 +191,6 @@ export abstract class BaseService {
     (this.serverless.cli.log as any)(message, entity, options);
   }
 
-  protected prettyPrint(object: any) {
-    this.log(JSON.stringify(object, null, 2));
-  }
-
   /**
    * Get function objects
    */
@@ -210,6 +206,10 @@ export abstract class BaseService {
     return Utils.get(this.options, key, defaultValue);
   }
 
+  protected prettyPrint(object: any) {
+    this.log(JSON.stringify(object, null, 2));
+  }
+
   private setDefaultValues(): void {
     // TODO: Right now the serverless core will always default to AWS default region if the
     // region has not been set in the serverless.yml or CLI options
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index bd57a50..353a658 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -149,13 +149,11 @@ export class FunctionAppService extends BaseService {
     this.log("Deploying serverless functions...");
 
     const functionZipFile = this.getFunctionZipFile();
-    const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
-    const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
-
-    await Promise.all([uploadFunctionApp, uploadBlobStorage]);
+    const blobUpload = this.uploadZippedArtifactToBlobStorage(functionZipFile);
 
     if (this.deploymentConfig.runFromBlobUrl) {
       this.log("Updating function app setting to run from external package...");
+      await blobUpload;
       const sasUrl = await this.blobService.generateBlobSasTokenUrl(
         this.deploymentConfig.container,
         this.artifactName
@@ -165,6 +163,9 @@ export class FunctionAppService extends BaseService {
         configConstants.runFromPackageSetting,
         sasUrl
       )
+    } else {
+      const functionAppUpload = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
+      Promise.all([blobUpload, functionAppUpload]);
     }
 
     this.log("Deployed serverless functions:")
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 1248e2d..51885e6 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -3,6 +3,8 @@ import { ResourceManagementClient } from "@azure/arm-resources";
 import { BaseService } from "./baseService";
 import { Utils } from "../shared/utils";
 import { AzureNamingService } from "./namingService";
+import { ArmDeployment } from "../models/armTemplates";
+import { DeploymentExtended } from "@azure/arm-resources/esm/models";
 
 export class ResourceService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -14,11 +16,40 @@ export class ResourceService extends BaseService {
   }
 
   /**
-   * Get all deployments for resource group
+   * Get all deployments for resource group sorted by timestamp (most recent first)
    */
   public async getDeployments() {
     this.log(`Listing deployments for resource group '${this.resourceGroup}':`);
-    return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+    const deployments = await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+    return deployments.sort((a: DeploymentExtended, b: DeploymentExtended) => {
+      return (a.properties.timestamp > b.properties.timestamp) ? 1 : -1
+    });
+  }
+
+  /**
+   * Get the most recent resource group deployment
+   */
+  public async getLastDeployment() {
+    const deployments = await this.getDeployments();
+    if (deployments && deployments.length) {
+      return deployments[0];
+    }
+  }
+
+  /**
+   * Get template from last resource group deployment
+   */
+  public async getLastDeploymentTemplate(): Promise<ArmDeployment> {
+    const deployment = await this.getLastDeployment();
+    if (!deployment) {
+      return;
+    }
+    const { parameters } = deployment.properties;
+    const { template } = await this.getDeploymentTemplate(deployment.name);
+    return {
+      template,
+      parameters
+    }
   }
 
   /**


Test patch:
diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
index 7b1f3e0..c17bd4a 100644
--- a/src/plugins/deploy/azureDeployPlugin.test.ts
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -64,4 +64,10 @@ describe("Deploy plugin", () => {
     expect(ResourceService.prototype.listDeployments).toBeCalled();
     expect(sls.cli.log).lastCalledWith(deploymentString);
   });
+	
+  it("crashes deploy if zip file is not found", async () => {
+    FunctionAppService.prototype.getFunctionZipFile = jest.fn(() => "notExisting.zip");
+    await expect(invokeHook(plugin, "deploy:deploy"))
+      .rejects.toThrow(/Function app zip file '.*' does not exist/)
+  });
 });
diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts
index 825599e..71dedfe 100644
--- a/src/services/armService.test.ts
+++ b/src/services/armService.test.ts
@@ -1,12 +1,13 @@
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 import { ArmService } from "./armService";
-import { ArmResourceTemplate, ArmTemplateType } from "../models/armTemplates";
+import { ArmResourceTemplate, ArmTemplateType, ArmDeployment } from "../models/armTemplates";
 import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
 import mockFs from "mock-fs";
 import jsonpath from "jsonpath";
 import { Deployments } from "@azure/arm-resources";
 import { Deployment } from "@azure/arm-resources/esm/models";
+import { ResourceService } from "./resourceService";
 
 describe("Arm Service", () => {
   let sls: Serverless
@@ -29,6 +30,13 @@ describe("Arm Service", () => {
     };
 
     service = createService();
+    ResourceService.prototype.getDeployments = jest.fn(() => MockFactory.createTestDeployments()) as any;
+    ResourceService.prototype.getDeploymentTemplate = jest.fn(() => {
+      return {
+        template: MockFactory.createTestArmTemplate()
+      }
+    }) as any;
+
   })
 
   afterEach(() => {
@@ -151,6 +159,51 @@ describe("Arm Service", () => {
       Deployments.prototype.createOrUpdate = jest.fn(() => Promise.resolve(null));
     });
 
+    it("Does not deploy if previously deployed template is the same", async () => {
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).not.toBeCalled()
+    });
+
+    it("Calls deploy if parameters have changed from deployed template", async () => {
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      deployment.parameters.param1 = "3"
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled();
+    });
+
+    it("Calls deploy if previously deployed template is different", async () => {
+      ResourceService.prototype.getDeploymentTemplate = jest.fn(() => {
+        return {
+          template: {}
+        }
+      }) as any;
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled()
+    });
+
+    it("Calls deploy if running first deployment", async () => {
+      ResourceService.prototype.getDeployments = jest.fn(() => {
+        return []
+      }) as any;
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled()
+    });
+
     it("Appends environment variables into app settings of ARM template", async () => {
       const environmentConfig: any = {
         PARAM_1: "1",
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index bf60773..b7cc9c8 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -364,10 +364,27 @@ describe("Function App Service", () => {
       newSlsService.provider["deployment"] = {
         runFromBlobUrl: true,
       }
+      const service = createService(MockFactory.createTestServerless({
+        service: newSlsService,
+      }));
+      await service.uploadFunctions(app);
+      expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).toBeCalled();
+      expect(FunctionAppService.prototype.updateFunctionAppSetting).toBeCalledWith(
+        app,
+        "WEBSITE_RUN_FROM_PACKAGE",
+        sasUrl
+      );
+    });
 
+    it("does not upload directly to function app if configured to run from blob", async () => {
+      const newSlsService = MockFactory.createTestService();
+      newSlsService.provider["deployment"] = {
+        runFromBlobUrl: true,
+      }
       const service = createService(MockFactory.createTestServerless({
         service: newSlsService,
       }));
+      FunctionAppService.prototype.uploadZippedArfifactToFunctionApp = jest.fn();
       await service.uploadFunctions(app);
       expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).toBeCalled();
       expect(FunctionAppService.prototype.updateFunctionAppSetting).toBeCalledWith(
@@ -375,6 +392,24 @@ describe("Function App Service", () => {
         "WEBSITE_RUN_FROM_PACKAGE",
         sasUrl
       );
+      expect(FunctionAppService.prototype.uploadZippedArfifactToFunctionApp).not.toBeCalled();
+      (FunctionAppService.prototype.uploadZippedArfifactToFunctionApp as any).mockRestore();
+    });
+
+    it("uploads directly to function app if not configured to run from blob", async () => {
+      const newSlsService = MockFactory.createTestService();
+      newSlsService.provider["deployment"] = {
+        runFromBlobUrl: false,
+      }
+      const sls = MockFactory.createTestServerless({
+        service: newSlsService,
+      });
+      const service = createService(sls);
+      FunctionAppService.prototype.uploadZippedArfifactToFunctionApp = jest.fn();
+      await service.uploadFunctions(app);
+      expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).not.toBeCalled();
+      expect(FunctionAppService.prototype.uploadZippedArfifactToFunctionApp).toBeCalled();
+      (FunctionAppService.prototype.uploadZippedArfifactToFunctionApp as any).mockRestore();
     });
 
     it("does not generate SAS URL or update WEBSITE_RUN_FROM_PACKAGE if not configured to run from blob", async() => {
@@ -384,6 +419,4 @@ describe("Function App Service", () => {
       expect(FunctionAppService.prototype.updateFunctionAppSetting).not.toBeCalled();
     });
   });
-
-
 });
diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts
index 941b4fa..cfbc7bd 100644
--- a/src/services/loginService.test.ts
+++ b/src/services/loginService.test.ts
@@ -22,7 +22,7 @@ describe("Login Service", () => {
     const emptyObj = { subscriptions: [] };
     Object.defineProperty(nodeauth,
       "interactiveLoginWithAuthResponse",
-      { value: jest.fn(_obj => emptyObj) }
+      { value: jest.fn(() => emptyObj) }
     );
 
     await AzureLoginService.login();
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index fe46f31..dffaa4e 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -89,6 +89,7 @@ describe("Resource Service", () => {
     const options = MockFactory.createTestServerlessOptions();
     const service = new ResourceService(sls, options);
     const deps = await service.getDeployments();
+    // Make sure deps are in correct order
     expect(deps).toEqual(deployments);
   });
 
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index b44a7e6..9789940 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -169,25 +169,29 @@ export class MockFactory {
     const result = [];
     const originalTimestamp = +MockFactory.createTestTimestamp();
     for (let i = 0; i < count; i++) {
+      const name = (includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`;
       result.push(
-        MockFactory.createTestDeployment((includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`)
+        MockFactory.createTestDeployment(name, i)
       )
     }
     return result as DeploymentsListByResourceGroupResponse
   }
 
-  public static createTestParameters() {
-    return {
+  public static createTestParameters(wrap = true) {
+    return (wrap) ? {
       param1: { value: "1", type: "String" },
       param2: { value: "2", type: "String" },
+    } : {
+      param1: "1",
+      param2: "2",
     }
   }
 
-  public static createTestDeployment(name?: string): DeploymentExtended {
+  public static createTestDeployment(name?: string, second: number = 0): DeploymentExtended {
     return {
       name: name || `deployment1-t${MockFactory.createTestTimestamp()}`,
       properties: {
-        timestamp: new Date(),
+        timestamp: new Date(2019, 1, 1, 0, 0, second),
         parameters: MockFactory.createTestParameters(),
       }
     }
@@ -430,7 +434,7 @@ export class MockFactory {
       handler: "handler.js",
     }
   }
-  
+
   public static createTestBinding() {
     // Only supporting HTTP for now, could support others
     return MockFactory.createTestHttpBinding();
@@ -461,7 +465,7 @@ export class MockFactory {
       name: "item",
       eventhubname: "hello",
       consumerGroup: "$Default",
-      connection: "EventHubsConnection"        
+      connection: "EventHubsConnection"
     }
   }
   public static createTestBindingsObject(name: string = "index.js") {