Repository: serverless/serverless-azure-functions
Pull request #242 (2019-08-16 12:29:58)

fix: Crash deployment if function app zip file doesn't exist
With `package.individually` the expected function app zip file is not generated in form supported by a plugin.

In such case currently plugin just logs some error message as follows:

```
Function app zip file '/Users/medikoo/Projects/tests/serverless-azure/.serverless/test-azure-0x.zip' does not exist
```

but doesn't exit with an error, which gives a confusing impression.

This PR ensures that such scenario is reported with plugin error


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/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/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") {