You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/01/24 01:57:45 UTC

[GitHub] paulcastro closed pull request #1: Swift 4 support

paulcastro closed pull request #1: Swift 4 support
URL: https://github.com/apache/incubator-openwhisk-runtime-swift/pull/1
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/.gitignore b/.gitignore
index f65f024..6d89211 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,7 @@ bin/
 # Gradle
 .gradle
 build/
+.build/
 !/tools/build/
 
 # Python
@@ -52,6 +53,11 @@ node_modules
 *.iml
 out/
 
+# VSCode
+.vscode/
+
+*.xcodeproj/
+
 # Ansible
 ansible/environments/docker-machine/hosts
 ansible/db_local.ini*
@@ -61,12 +67,7 @@ ansible/roles/nginx/files/*.csr
 ansible/roles/nginx/files/*cert.pem
 
 # .zip files must be explicited whitelisted
-*.zip
-!tests/dat/actions/blackbox.zip
-!tests/dat/actions/helloSwift.zip
-!tests/dat/actions/python.zip
-!tests/dat/actions/python2_virtualenv.zip
-!tests/dat/actions/python3_virtualenv.zip
-!tests/dat/actions/python_virtualenv_dir.zip
-!tests/dat/actions/python_virtualenv_name.zip
-!tests/dat/actions/zippedaction.zip
+!tests/dat/build/swift311/HelloSwift3.zip
+!tests/dat/build/swift4/HelloSwift4.zip
+!tests/dat/build/swift4/SwiftyRequest.zip
+
diff --git a/README.md b/README.md
index 77b4407..26b44e0 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,158 @@
 [![Build Status](https://travis-ci.org/apache/incubator-openwhisk-runtime-swift.svg?branch=master)](https://travis-ci.org/apache/incubator-openwhisk-runtime-swift)
 
 
-### Give it a try today
+### Simple swift action hello.swift
+The traditional support for dictionary still works:
+```swift
+func main(args: [String:Any]) -> [String:Any] {
+    if let name = args["name"] as? String {
+        return [ "greeting" : "Hello \(name)!" ]
+    } else {
+        return [ "greeting" : "Hello swif4!" ]
+    }
+}
+```
+
+### Packaging an action as a Swift executable using Swift 4
+
+When you create an OpenWhisk Swift action with a Swift source file, it has to be compiled into a binary before the action is run. Once done, subsequent calls to the action are much faster until the container holding your action is purged. This delay is known as the cold-start delay.
+
+To avoid the cold-start delay, you can compile your Swift file into a binary and then upload to OpenWhisk in a zip file. As you need the OpenWhisk scaffolding, the easiest way to create the binary is to build it within the same environment as it will be run in. These are the steps:
+
+- Run an interactive Swift action container.
+  ```
+  docker run --rm -it -v "$(pwd):/owexec" openwhisk/action-swift-v4 bash
+  ```
+  This puts you in a bash shell within the Docker container.
+
+- Copy the source code and prepare to build it.
+  ```
+  cp /owexec/hello.swift /swift4Action/spm-build/Sources/Action/main.swift
+  ```
+  ```
+  cat /swift4Action/epilogue.swift >> /swift4Action/spm-build/Sources/Action/main.swift
+  ```
+  ```
+  echo '_run_main(mainFunction:main)' >> /swift4Action/spm-build/Sources/Action/main.swift
+  ```
+  Copy any additional source files to `/swift4Action/spm-build/Sources/Action/`
+
+
+- (Optional) Create the `Package.swift` file to add dependencies.
+```swift
+// swift-tools-version:4.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+    products: [
+      .executable(
+        name: "Action",
+        targets:  ["Action"]
+      )
+    ],
+    dependencies: [
+      .package(url: "https://github.com/IBM-Swift/SwiftyRequest.git", .upToNextMajor(from: "1.0.0"))
+    ],
+    targets: [
+      .target(
+        name: "Action",
+        dependencies: ["SwiftyRequest"],
+        path: "."
+      )
+ 
+```
+  As you can see this example adds `SwiftyRequest` dependencies.
+  
+  Notice that now with swift:4 is no longer required to include `CCurl`, `Kitura-net` and `SwiftyJSON` in your own `Package.swift`.
+  You are free now to use no dependencies, or add the combination that you want with the versions you want.
+
+- Copy Package.swift to spm-build directory
+  ```
+  cp /owexec/Package.swift /swift4Action/spm-build/Package.swift
+  ```
+
+- Change to the spm-build directory.
+  ```
+  cd /swift4Action/spm-build
+  ```
+
+- Compile your Swift Action.
+  ```
+  swift build -c release
+  ```
+
+- Create the zip archive.
+  ```
+  zip /owexec/hello.zip .build/release/Action
+  ```
+
+- Exit the Docker container.
+  ```
+  exit
+  ```
+
+  This has created hello.zip in the same directory as hello.swift.
+
+- Upload it to OpenWhisk with the action name helloSwifty:
+  ```
+  wsk action update helloSwiftly hello.zip openwhisk/action-swift-v4
+  ```
+
+- To check how much faster it is, run
+  ```
+  wsk action invoke helloSwiftly --blocking
+  ```
+
+### Migrating from Swift 3 to Swift 4
+
+### Helper compile.sh helper script
+When compiling and packaging your swift 4 now there are a couple of differences
+All your source code needs to be copy to `/swift4Action/spm-build/Sources/Action/` instead of `/swift3Action/spm-build/`
+You Package.swift needs to have the first line with a comment indicating swift4 tooling and format
+```
+// swift-tools-version:4.0
+```
+For swift 4 you need specify additional information in Package.swift such as `products` with executable name `Action` and `targets`
+
+You can take a look at the helper script [tools/build/compile.sh](tools/build/compile.sh) to compile and zip your Actions.
+Having a project directory `Hello` under a directory `actions` like the following:
+```
+actions/Hello/Package.swift
+actions/Hello/Sources/main.swift
+```
+Change to the parent directory then run the compile script specify the project directory, the kind `swift:3.1.1` or `swift:4` and any swiftc build flags like the following:
+```
+cd actions/
+incubator-runtime-swift/tools/build/compile.sh Hello swift:4 -v
+```
+This will produce a zip `build/swift4/Hello.zip`
+
+### SwiftyJSON using single source action file
+If you have a swift:3.1.1 action not compile, just as source using the `SwiftyJSON` package, you need to precompile your action and specify the version of SwiftyJSON you wan to use for swift:4 kind action.
+Take into account that tarting with Swift 4 there is better support to manage JSON data natively.
+
+Note: This is only applicable to the base image provided for the Swift 4 runtime, other downstream such as IBM Cloud Functions extending this image might provide additional SDK and packages including `SwiftyJSON` and IBM Watson SDK, check the vendor documentation for more specific information about packages and versions.
+
+### Building the Swift4 Image
+```
+./gradlew core:swift4Action:distDocker
+```
+This will produce the image `whisk/action-swift-v4`
+
+Build and Push image
+```
+docker login
+./gradlew core:swift4Action:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io 
+```
+
+
+### Using Swift 3.1.1
 To use as a docker action
 ```
-wsk action update myAction myAction.jar --docker openwhisk/action-swift-v3.1.1:1.0.0
+wsk action update myAction myAction.swift --docker openwhisk/action-swift-v3.1.1:1.0.0
 ```
 This works on any deployment of Apache OpenWhisk
 
diff --git a/ansible/environments/local/group_vars/all b/ansible/environments/local/group_vars/all
index 5d6a325..1ba2818 100755
--- a/ansible/environments/local/group_vars/all
+++ b/ansible/environments/local/group_vars/all
@@ -40,9 +40,14 @@ runtimes_manifest:
         name: "swift3action"
       deprecated: true
     - kind: "swift:3.1.1"
-      default: true
+      default: false
       image:
         name: "action-swift-v3.1.1"
       deprecated: false
+    - kind: "swift:4"
+      default: true
+      image:
+        name: "action-swift-v4"
+      deprecated: false
   blackboxes:
     - name: "dockerskeleton"
diff --git a/core/actionProxy/actionproxy.py b/core/actionProxy/actionproxy.py
index c68231f..10ee2a9 100644
--- a/core/actionProxy/actionproxy.py
+++ b/core/actionProxy/actionproxy.py
@@ -47,10 +47,11 @@ class ActionRunner:
     # @param source the path where the source code will be located (if any)
     # @param binary the path where the binary will be located (may be the
     # same as source code path)
-    def __init__(self, source=None, binary=None):
+    def __init__(self, source=None, binary=None, zipdest=None):
         defaultBinary = '/action/exec'
         self.source = source if source else defaultBinary
         self.binary = binary if binary else defaultBinary
+        self.zipdest = zipdest if zipdest else os.path.dirname(self.source)
 
     def preinit(self):
         return
@@ -182,7 +183,7 @@ def initCodeFromZip(self, message):
             bytes = base64.b64decode(message['code'])
             bytes = io.BytesIO(bytes)
             archive = zipfile.ZipFile(bytes)
-            archive.extractall(os.path.dirname(self.source))
+            archive.extractall(self.zipdest)
             archive.close()
             return True
         except Exception as e:
diff --git a/core/swift4Action/CHANGELOG.md b/core/swift4Action/CHANGELOG.md
new file mode 100644
index 0000000..74cb32a
--- /dev/null
+++ b/core/swift4Action/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Apache OpenWhisk Swift 4 Runtime Container
+
+## 1.0.0
+Initial swift 4 image
+
+Swift 4 runtime version:
+  - [4.0.3](https://hub.docker.com/r/ibmcom/swift-ubuntu/tags/4.0.3/)
+
+Packages included:
+  - No packages included, use Package.swift and pre-compile action.
diff --git a/core/swift4Action/Dockerfile b/core/swift4Action/Dockerfile
new file mode 100755
index 0000000..bc8b7ee
--- /dev/null
+++ b/core/swift4Action/Dockerfile
@@ -0,0 +1,31 @@
+# Dockerfile for swift actions, overrides and extends ActionRunner from actionProxy
+# This Dockerfile is partially based on: https://github.com/IBM-Swift/swift-ubuntu-docker/blob/master/swift-development/Dockerfile
+FROM ibmcom/swift-ubuntu:4.0
+
+# Set WORKDIR
+WORKDIR /
+
+# Upgrade and install basic Python dependencies
+RUN apt-get -y update \
+ && apt-get -y install --fix-missing python2.7 python-gevent python-flask zip
+
+# Add the action proxy
+RUN mkdir -p /actionProxy
+ADD actionproxy.py /actionProxy
+
+# Add files needed to build and run action
+RUN mkdir -p /swift4Action/spm-build/Sources/Action
+ADD epilogue.swift /swift4Action
+ADD buildandrecord.py /swift4Action
+ADD swift4runner.py /swift4Action
+ADD spm-build/Package.swift /swift4Action/spm-build
+ADD spm-build/_Whisk.swift /swift4Action/spm-build/Sources/Action
+
+# Build
+RUN touch /swift4Action/spm-build/Sources/Action/main.swift
+RUN python /swift4Action/buildandrecord.py && rm /swift4Action/spm-build/.build/release/Action
+#RUN cd /swift4Action/spm-build; swift build -v -c release; rm /swift4Action/spm-build/.build/release/Action
+ENV FLASK_PROXY_PORT 8080
+EXPOSE 8080
+
+CMD ["/bin/bash", "-c", "cd /swift4Action && PYTHONIOENCODING='utf-8' python -u swift4runner.py"]
\ No newline at end of file
diff --git a/core/swift4Action/build.gradle b/core/swift4Action/build.gradle
new file mode 100755
index 0000000..9ad1f58
--- /dev/null
+++ b/core/swift4Action/build.gradle
@@ -0,0 +1,13 @@
+ext.dockerImageName = 'action-swift-v4'
+apply from: '../../gradle/docker.gradle'
+distDocker.dependsOn 'copyProxy'
+distDocker.finalizedBy('cleanup')
+
+task copyProxy(type: Copy) {
+    from '../actionProxy/actionproxy.py'
+    into '.'
+}
+
+task cleanup(type: Delete) {
+    delete 'actionproxy.py'
+}
diff --git a/core/swift4Action/buildandrecord.py b/core/swift4Action/buildandrecord.py
new file mode 100755
index 0000000..346ecef
--- /dev/null
+++ b/core/swift4Action/buildandrecord.py
@@ -0,0 +1,77 @@
+"""Python to generate build script.
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+"""
+from __future__ import print_function
+import os
+import sys
+from subprocess import check_output
+
+# Settings
+COMPILE_PREFIX = "/usr/bin/swiftc -module-name Action "
+LINKER_PREFIX =  "/usr/bin/swiftc -target 'x86_64-unknown-linux' -sdk / -L '/swift4Action/spm-build/.build/x86_64-unknown-linux/release' -o '/swift4Action/spm-build/.build/x86_64-unknown-linux/release/Action' -module-name Action -emit-executable -Xlinker '-rpath=$ORIGIN'"
+GENERATED_BUILD_SCRIPT = "/swift4Action/spm-build/swiftbuildandlink.sh"
+SPM_DIRECTORY = "/swift4Action/spm-build"
+BUILD_COMMAND = ["swift", "build", "-v", "-c", "release"]
+
+# Build Swift package and capture step trace
+print("Building action")
+out = check_output(BUILD_COMMAND, cwd=SPM_DIRECTORY)
+print("action built. Decoding compile and link commands")
+
+# Look for compile and link commands in step trace
+compileCommand = None
+linkCommand = None
+
+buildInstructions = out.decode("utf-8").splitlines()
+
+for instruction in buildInstructions:
+    if instruction.startswith(COMPILE_PREFIX):
+        compileCommand = instruction
+
+        # add flag to quiet warnings
+        compileCommand += " -suppress-warnings"
+
+    elif instruction.startswith(LINKER_PREFIX):
+        linkCommand = instruction
+
+# if found, create build script, otherwise exit with error
+if compileCommand is not None and linkCommand is not None:
+    print("Generated OpenWhisk Compile command: %s" % compileCommand)
+    print("=========")
+    print("Generated OpenWhisk Link command: %s" % linkCommand)
+
+    with open(GENERATED_BUILD_SCRIPT, "a") as buildScript:
+        buildScript.write("#!/bin/bash\n")
+        buildScript.write("echo \"Compiling\"\n")
+        buildScript.write("%s\n" % compileCommand)
+        buildScript.write("swiftStatus=$?\n")
+        buildScript.write("echo swiftc status is $swiftStatus\n")
+        buildScript.write("if [[ \"$swiftStatus\" -eq \"0\" ]]; then\n")
+        buildScript.write("echo \"Linking\"\n")
+        buildScript.write("%s\n" % linkCommand)
+        buildScript.write("else\n")
+        buildScript.write(">2& echo \"Action did not compile\"\n")
+        buildScript.write("exit 1\n")
+        buildScript.write("fi")
+
+    os.chmod(GENERATED_BUILD_SCRIPT, 0o777)
+    sys.exit(0)
+else:
+    print("Cannot generate build script: compile or link command not found")
+    sys.exit(1)
diff --git a/core/swift4Action/epilogue.swift b/core/swift4Action/epilogue.swift
new file mode 100644
index 0000000..e6a9b15
--- /dev/null
+++ b/core/swift4Action/epilogue.swift
@@ -0,0 +1,28 @@
+// Imports
+import Foundation
+
+let env = ProcessInfo.processInfo.environment
+let inputStr: String = env["WHISK_INPUT"] ?? "{}"
+let json = inputStr.data(using: .utf8, allowLossyConversion: true)!
+
+
+// snippet of code "injected" (wrapper code for invoking traditional main)
+func _run_main(mainFunction: ([String: Any]) -> [String: Any]) -> Void {
+    let parsed = try! JSONSerialization.jsonObject(with: json, options: []) as! [String: Any]
+    let result = mainFunction(parsed)
+    if JSONSerialization.isValidJSONObject(result) {
+        do {
+            let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
+            if let jsonStr = String(data: jsonData, encoding: String.Encoding.utf8) {
+                print("\(jsonStr)")
+            } else {
+                print("Error serializing data to JSON, data conversion returns nil string")
+            }
+        } catch {
+            print(("\(error)"))
+        }
+    } else {
+        print("Error serializing JSON, data does not appear to be valid JSON")
+    }
+}
+
diff --git a/core/swift4Action/spm-build/Package.swift b/core/swift4Action/spm-build/Package.swift
new file mode 100755
index 0000000..397e15e
--- /dev/null
+++ b/core/swift4Action/spm-build/Package.swift
@@ -0,0 +1,35 @@
+// swift-tools-version:4.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+/*
+ * Copyright 2017 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+    products: [
+      .executable(
+        name: "Action",
+        targets:  ["Action"]
+      )
+    ],
+    targets: [
+      .target(
+        name: "Action"
+      )
+    ]
+)
diff --git a/core/swift4Action/spm-build/_Whisk.swift b/core/swift4Action/spm-build/_Whisk.swift
new file mode 100644
index 0000000..ede2a9f
--- /dev/null
+++ b/core/swift4Action/spm-build/_Whisk.swift
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import Dispatch
+
+class Whisk {
+    
+    static var baseUrl = ProcessInfo.processInfo.environment["__OW_API_HOST"]
+    static var apiKey = ProcessInfo.processInfo.environment["__OW_API_KEY"]
+    
+    class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {
+        let parsedAction = parseQualifiedName(name: action)
+        let strBlocking = blocking ? "true" : "false"
+        let path = "/api/v1/namespaces/\(parsedAction.namespace)/actions/\(parsedAction.name)?blocking=\(strBlocking)"
+        
+        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "POST")
+    }
+    
+    class func trigger(eventNamed event : String, withParameters params : [String:Any]) -> [String:Any] {
+        let parsedEvent = parseQualifiedName(name: event)
+        let path = "/api/v1/namespaces/\(parsedEvent.namespace)/triggers/\(parsedEvent.name)?blocking=true"
+        
+        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "POST")
+    }
+    
+    class func createTrigger(triggerNamed trigger: String, withParameters params : [String:Any]) -> [String:Any] {
+        let parsedTrigger = parseQualifiedName(name: trigger)
+        let path = "/api/v1/namespaces/\(parsedTrigger.namespace)/triggers/\(parsedTrigger.name)"
+        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "PUT")
+    }
+    
+    class func createRule(ruleNamed ruleName: String, withTrigger triggerName: String, andAction actionName: String) -> [String:Any] {
+        let parsedRule = parseQualifiedName(name: ruleName)
+        let path = "/api/v1/namespaces/\(parsedRule.namespace)/rules/\(parsedRule.name)"
+        let params = ["trigger":triggerName, "action":actionName]
+        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "PUT")
+    }
+    
+    // handle the GCD dance to make the post async, but then obtain/return
+    // the result from this function sync
+    private class func sendWhiskRequestSyncronish(uriPath path: String, params : [String:Any], method: String) -> [String:Any] {
+        var response : [String:Any]!
+        
+        let queue = DispatchQueue.global()
+        let invokeGroup = DispatchGroup()
+        
+        invokeGroup.enter()
+        queue.async {
+            postUrlSession(uriPath: path, params: params, method: method, group: invokeGroup) { result in
+                response = result
+            }
+        }
+        
+        // On one hand, FOREVER seems like an awfully long time...
+        // But on the other hand, I think we can rely on the system to kill this
+        // if it exceeds a reasonable execution time.
+        switch invokeGroup.wait(timeout: DispatchTime.distantFuture) {
+        case DispatchTimeoutResult.success:
+            break
+        case DispatchTimeoutResult.timedOut:
+            break
+        }
+        
+        return response
+    }
+    
+    
+    /**
+     * Using new UrlSession
+     */
+    private class func postUrlSession(uriPath: String, params : [String:Any], method: String,group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
+        
+        guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
+            callback(["error": "Error encoding uri path to make openwhisk REST call."])
+            return
+        }
+        
+        let urlStr = "\(baseUrl!)\(encodedPath)"
+        if let url = URL(string: urlStr) {
+            var request = URLRequest(url: url)
+            request.httpMethod = method
+            
+            do {
+                request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+                request.httpBody = try JSONSerialization.data(withJSONObject: params)
+                
+                let loginData: Data = apiKey!.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+                let base64EncodedAuthKey  = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
+                request.addValue("Basic \(base64EncodedAuthKey)", forHTTPHeaderField: "Authorization")
+                let session = URLSession(configuration: URLSessionConfiguration.default)
+                
+                let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
+                    
+                    // exit group after we are done
+                    defer {
+                        group.leave()
+                    }
+                    
+                    if let error = error {
+                        callback(["error":error.localizedDescription])
+                    } else {
+                        
+                        if let data = data {
+                            do {
+                                //let outputStr  = String(data: data, encoding: String.Encoding.utf8) as String!
+                                //print(outputStr)
+                                let respJson = try JSONSerialization.jsonObject(with: data)
+                                if respJson is [String:Any] {
+                                    callback(respJson as! [String:Any])
+                                } else {
+                                    callback(["error":" response from server is not a dictionary"])
+                                }
+                            } catch {
+                                callback(["error":"Error creating json from response: \(error)"])
+                            }
+                        }
+                    }
+                })
+                
+                task.resume()
+            } catch {
+                callback(["error":"Got error creating params body: \(error)"])
+            }
+        }
+    }
+    
+    // separate an OpenWhisk qualified name (e.g. "/whisk.system/samples/date")
+    // into namespace and name components
+    private class func parseQualifiedName(name qualifiedName : String) -> (namespace : String, name : String) {
+        let defaultNamespace = "_"
+        let delimiter = "/"
+        
+        let segments :[String] = qualifiedName.components(separatedBy: delimiter)
+        
+        if segments.count > 2 {
+            return (segments[1], Array(segments[2..<segments.count]).joined(separator: delimiter))
+        } else if segments.count == 2 {
+            // case "/action" or "package/action"
+            let name = qualifiedName.hasPrefix(delimiter) ? segments[1] : segments.joined(separator: delimiter)
+            return (defaultNamespace, name)
+        } else {
+            return (defaultNamespace, segments[0])
+        }
+    }
+    
+}
diff --git a/core/swift4Action/swift4runner.py b/core/swift4Action/swift4runner.py
new file mode 100644
index 0000000..d74a748
--- /dev/null
+++ b/core/swift4Action/swift4runner.py
@@ -0,0 +1,114 @@
+"""Python proxy to run Swift action.
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+"""
+import os
+import glob
+import sys
+import subprocess
+import codecs
+import json
+sys.path.append('../actionProxy')
+from actionproxy import ActionRunner, main, setRunner  # noqa
+
+SRC_EPILOGUE_FILE = '/swift4Action/epilogue.swift'
+DEST_SCRIPT_FILE = '/swift4Action/spm-build/Sources/Action/main.swift'
+DEST_SCRIPT_DIR = '/swift4Action/spm-build'
+DEST_BIN_FILE = '/swift4Action/spm-build/.build/release/Action'
+
+BUILD_PROCESS = ['./swiftbuildandlink.sh']
+
+
+class Swift4Runner(ActionRunner):
+
+    def __init__(self):
+        ActionRunner.__init__(self, DEST_SCRIPT_FILE, DEST_BIN_FILE, DEST_SCRIPT_DIR)
+
+    # remove pre-existing binary before receiving a new binary
+    def preinit(self):
+        try:
+            os.remove(self.binary)
+        except: pass
+
+    def epilogue(self, init_message):
+        # skip if executable already exists (was unzipped)
+        if os.path.isfile(self.binary):
+            return
+
+        if 'main' in init_message:
+            main_function = init_message['main']
+        else:
+            main_function = 'main'
+        # make sure there is a main.swift file
+        open(DEST_SCRIPT_FILE, 'a').close()
+
+        with codecs.open(DEST_SCRIPT_FILE, 'a', 'utf-8') as fp:
+            os.chdir(DEST_SCRIPT_DIR)
+            for file in glob.glob("*.swift"):
+                if file not in ["Package.swift", "main.swift", "_WhiskJSONUtils.swift", "_Whisk.swift"]:
+                    with codecs.open(file, 'r', 'utf-8') as f:
+                        fp.write(f.read())
+            with codecs.open(SRC_EPILOGUE_FILE, 'r', 'utf-8') as ep:
+                fp.write(ep.read())
+
+            fp.write('_run_main(mainFunction: %s)\n' % main_function)
+
+    def build(self, init_message):
+        # short circuit the build, if there already exists a binary
+        # from the zip file
+        if os.path.isfile(self.binary):
+            # file may not have executable permission, set it
+            os.chmod(self.binary, 0o555)
+            return
+
+        p = subprocess.Popen(
+            BUILD_PROCESS,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            cwd=DEST_SCRIPT_DIR)
+
+        # run the process and wait until it completes.
+        # stdout/stderr will not be None because we passed PIPEs to Popen
+        (o, e) = p.communicate()
+
+        # stdout/stderr may be either text or bytes, depending on Python
+        # version, so if bytes, decode to text. Note that in Python 2
+        # a string will match both types; so also skip decoding in that case
+        if isinstance(o, bytes) and not isinstance(o, str):
+            o = o.decode('utf-8')
+        if isinstance(e, bytes) and not isinstance(e, str):
+            e = e.decode('utf-8')
+
+        if o:
+            sys.stdout.write(o)
+            sys.stdout.flush()
+
+        if e:
+            sys.stderr.write(e)
+            sys.stderr.flush()
+
+    def env(self, message):
+        env = ActionRunner.env(self, message)
+        args = message.get('value', {}) if message else {}
+        env['WHISK_INPUT'] = json.dumps(args)
+        return env
+
+
+if __name__ == '__main__':
+    setRunner(Swift4Runner())
+    main()
diff --git a/settings.gradle b/settings.gradle
index a0f56d4..2b5078a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,6 +10,8 @@ include 'tests'
 
 include 'core:swift3.1.1Action'
 
+include 'core:swift4Action'
+
 rootProject.name = 'runtime-swift'
 
 gradle.ext.scala = [
diff --git a/tests/dat/actions/HelloSwift3/Package.swift b/tests/dat/actions/HelloSwift3/Package.swift
new file mode 100644
index 0000000..8a40c7d
--- /dev/null
+++ b/tests/dat/actions/HelloSwift3/Package.swift
@@ -0,0 +1,28 @@
+// swift-tools-version:3.1.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+        dependencies: [
+            .Package(url: "https://github.com/IBM-Swift/CCurl.git", "0.2.3"),
+            .Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.7.10"),
+            .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "15.0.1")
+        ]
+)
diff --git a/tests/dat/actions/HelloSwift3/Sources/main.swift b/tests/dat/actions/HelloSwift3/Sources/main.swift
new file mode 100644
index 0000000..6da92f8
--- /dev/null
+++ b/tests/dat/actions/HelloSwift3/Sources/main.swift
@@ -0,0 +1,7 @@
+func main(args: [String:Any]) -> [String:Any] {
+    if let name = args["name"] as? String {
+        return [ "greeting" : "Hello \(name)!" ]
+    } else {
+        return [ "greeting" : "Hello stranger!" ]
+    }
+}
\ No newline at end of file
diff --git a/tests/dat/actions/HelloSwift4/Package.swift b/tests/dat/actions/HelloSwift4/Package.swift
new file mode 100644
index 0000000..b9f14f9
--- /dev/null
+++ b/tests/dat/actions/HelloSwift4/Package.swift
@@ -0,0 +1,34 @@
+// swift-tools-version:4.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+    products: [
+      .executable(
+        name: "Action",
+        targets:  ["Action"]
+      )
+    ],
+    targets: [
+      .target(
+        name: "Action"
+      )
+    ]
+)
diff --git a/tests/dat/actions/HelloSwift4/Sources/main.swift b/tests/dat/actions/HelloSwift4/Sources/main.swift
new file mode 100644
index 0000000..6da92f8
--- /dev/null
+++ b/tests/dat/actions/HelloSwift4/Sources/main.swift
@@ -0,0 +1,7 @@
+func main(args: [String:Any]) -> [String:Any] {
+    if let name = args["name"] as? String {
+        return [ "greeting" : "Hello \(name)!" ]
+    } else {
+        return [ "greeting" : "Hello stranger!" ]
+    }
+}
\ No newline at end of file
diff --git a/tests/dat/actions/SwiftyRequest/Package.resolved b/tests/dat/actions/SwiftyRequest/Package.resolved
new file mode 100644
index 0000000..11daaa4
--- /dev/null
+++ b/tests/dat/actions/SwiftyRequest/Package.resolved
@@ -0,0 +1,34 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "CircuitBreaker",
+        "repositoryURL": "https://github.com/IBM-Swift/CircuitBreaker.git",
+        "state": {
+          "branch": null,
+          "revision": "252f7bccb70dc34283f7a05ece624075967d1bb2",
+          "version": "3.0.0"
+        }
+      },
+      {
+        "package": "LoggerAPI",
+        "repositoryURL": "https://github.com/IBM-Swift/LoggerAPI.git",
+        "state": {
+          "branch": null,
+          "revision": "6b62fe66da9b8794d6581e5c341710eea3173cd4",
+          "version": "1.7.1"
+        }
+      },
+      {
+        "package": "SwiftyRequest",
+        "repositoryURL": "https://github.com/IBM-Swift/SwiftyRequest.git",
+        "state": {
+          "branch": null,
+          "revision": "bb6f8b3615bf4b6c0ab0f78df6da093f7583d98b",
+          "version": "1.0.1"
+        }
+      }
+    ]
+  },
+  "version": 1
+}
diff --git a/tests/dat/actions/SwiftyRequest/Package.swift b/tests/dat/actions/SwiftyRequest/Package.swift
new file mode 100644
index 0000000..032948d
--- /dev/null
+++ b/tests/dat/actions/SwiftyRequest/Package.swift
@@ -0,0 +1,39 @@
+// swift-tools-version:4.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import PackageDescription
+
+let package = Package(
+    name: "Action",
+    products: [
+      .executable(
+        name: "Action",
+        targets:  ["Action"]
+      )
+    ],
+    dependencies: [
+      .package(url: "https://github.com/IBM-Swift/SwiftyRequest.git", .upToNextMajor(from: "1.0.0"))
+    ],
+    targets: [
+      .target(
+        name: "Action",
+        dependencies: ["SwiftyRequest"],
+        path: "."
+      )
+    ]
+)
diff --git a/tests/dat/actions/SwiftyRequest/Sources/main.swift b/tests/dat/actions/SwiftyRequest/Sources/main.swift
new file mode 100644
index 0000000..7947eac
--- /dev/null
+++ b/tests/dat/actions/SwiftyRequest/Sources/main.swift
@@ -0,0 +1,36 @@
+import SwiftyRequest
+import Dispatch
+import Foundation
+
+func main(args: [String:Any]) -> [String:Any] {
+    var resp :[String:Any] = ["error":"Action failed"]
+    let echoURL = "http://httpbin.org/post"
+    
+    // setting body data to {"Data":"string"}
+    let origJson: [String: Any] = args
+    guard let data = try? JSONSerialization.data(withJSONObject: origJson, options: []) else {
+        return ["error": "Could not encode json"]
+    }
+    let request = RestRequest(method: .post, url: echoURL)
+    request.messageBody = data
+    let semaphore = DispatchSemaphore(value: 0)
+    //sending with query ?hour=9
+    request.responseData(queryItems: [URLQueryItem(name: "hour", value: "9")]) { response in
+        switch response.result {
+        case .success(let retval):
+            if let json = try? JSONSerialization.jsonObject(with: retval, options: []) as! [String:Any]  {
+                resp = json
+            } else {
+                resp = ["error":"Response from server is not a dictionary like"]
+            }
+        case .failure(let error):
+            resp = ["error":"Failed to get data response: \(error)"]
+        }
+        semaphore.signal()
+    }
+    _ = semaphore.wait(timeout: .distantFuture)
+    return resp
+}
+//let r = main(args:["message":"serverless"])
+//print(r)
+
diff --git a/tests/dat/actions/sdk/swift3/createRule.swift b/tests/dat/actions/sdk/swift3/createRule.swift
new file mode 100644
index 0000000..67fa062
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/createRule.swift
@@ -0,0 +1,13 @@
+func main(args: [String:Any]) -> [String:Any] {
+  guard let triggerName = args["triggerName"] as? String else {
+      return ["error": "You must specify a triggerName parameter!"]
+  }
+  guard let actionName = args["actionName"] as? String else {
+      return ["error": "You must specify a actionName parameter!"]
+  }
+  guard let ruleName = args["ruleName"] as? String else {
+      return ["error": "You must specify a ruleName parameter!"]
+  }
+  print("Rule Name: \(ruleName), Trigger Name: \(triggerName), actionName: \(actionName)")
+  return Whisk.createRule(ruleNamed: ruleName, withTrigger: triggerName, andAction: actionName)
+}
diff --git a/tests/dat/actions/sdk/swift3/createTrigger.swift b/tests/dat/actions/sdk/swift3/createTrigger.swift
new file mode 100644
index 0000000..7039ac4
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/createTrigger.swift
@@ -0,0 +1,7 @@
+func main(args: [String:Any]) -> [String:Any] {
+ guard let triggerName = args["triggerName"] as? String else {
+    return ["error": "You must specify a triggerName parameter!"]
+  }
+  print("Trigger Name: \(triggerName)")
+  return Whisk.createTrigger(triggerNamed: triggerName, withParameters: [:])
+}
diff --git a/tests/dat/actions/sdk/swift3/hello.swift b/tests/dat/actions/sdk/swift3/hello.swift
new file mode 100644
index 0000000..7c903a5
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/hello.swift
@@ -0,0 +1,10 @@
+/**
+ * Hello world as a Swift Whisk action.
+ */
+func main(args: [String:Any]) -> [String:Any] {
+    if let name = args["name"] as? String {
+        return [ "greeting" : "Hello \(name)!" ]
+    } else {
+        return [ "greeting" : "Hello stranger!" ]
+    }
+}
diff --git a/tests/dat/actions/sdk/swift3/invoke.swift b/tests/dat/actions/sdk/swift3/invoke.swift
new file mode 100644
index 0000000..fd32501
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/invoke.swift
@@ -0,0 +1,16 @@
+import SwiftyJSON
+
+func main(args: [String:Any]) -> [String:Any] {
+  let invokeResult = Whisk.invoke(actionNamed: "/whisk.system/utils/date", withParameters: [:])
+  let dateActivation = JSON(invokeResult)
+
+  // the date we are looking for is the result inside the date activation
+  if let dateString = dateActivation["response"]["result"]["date"].string {
+    print("It is now \(dateString)")
+  } else {
+    print("Could not parse date of of the response.")
+  }
+
+  // return the entire invokeResult
+  return invokeResult
+}
diff --git a/tests/dat/actions/sdk/swift3/invokeNonBlocking.swift b/tests/dat/actions/sdk/swift3/invokeNonBlocking.swift
new file mode 100644
index 0000000..a12c165
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/invokeNonBlocking.swift
@@ -0,0 +1,16 @@
+import SwiftyJSON
+
+func main(args: [String:Any]) -> [String:Any] {
+  let invokeResult = Whisk.invoke(actionNamed: "/whisk.system/utils/date", withParameters: [:], blocking: false)
+  let dateActivation = JSON(invokeResult)
+
+  // the date we are looking for is the result inside the date activation
+  if let activationId = dateActivation["activationId"].string {
+    print("Invoked.")
+  } else {
+    print("Failed to invoke.")
+  }
+
+  // return the entire invokeResult
+  return invokeResult
+}
diff --git a/tests/dat/actions/sdk/swift3/trigger.swift b/tests/dat/actions/sdk/swift3/trigger.swift
new file mode 100644
index 0000000..2acf54e
--- /dev/null
+++ b/tests/dat/actions/sdk/swift3/trigger.swift
@@ -0,0 +1,8 @@
+func main(args: [String:Any]) -> [String:Any] {
+  if let triggerName = args["triggerName"] as? String {
+    print("Trigger Name: \(triggerName)")
+    return Whisk.trigger(eventNamed: triggerName, withParameters: [:])
+  } else {
+    return ["error": "You must specify a triggerName parameter!"]
+  }
+}
diff --git a/tests/dat/actions/sdk/swift4/createRule.swift b/tests/dat/actions/sdk/swift4/createRule.swift
new file mode 100644
index 0000000..5b9ae8e
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/createRule.swift
@@ -0,0 +1,17 @@
+func main(args: [String:Any]) -> [String:Any] {
+  if let baseUrl = args["baseUrl"] as? String {
+    //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
+    Whisk.baseUrl = baseUrl
+  }
+  guard let triggerName = args["triggerName"] as? String else {
+      return ["error": "You must specify a triggerName parameter!"]
+  }
+  guard let actionName = args["actionName"] as? String else {
+      return ["error": "You must specify a actionName parameter!"]
+  }
+  guard let ruleName = args["ruleName"] as? String else {
+      return ["error": "You must specify a ruleName parameter!"]
+  }
+  print("Rule Name: \(ruleName), Trigger Name: \(triggerName), actionName: \(actionName)")
+  return Whisk.createRule(ruleNamed: ruleName, withTrigger: triggerName, andAction: actionName)
+}
\ No newline at end of file
diff --git a/tests/dat/actions/sdk/swift4/createTrigger.swift b/tests/dat/actions/sdk/swift4/createTrigger.swift
new file mode 100644
index 0000000..6886abc
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/createTrigger.swift
@@ -0,0 +1,11 @@
+func main(args: [String:Any]) -> [String:Any] {
+  if let baseUrl = args["baseUrl"] as? String {
+    //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
+    Whisk.baseUrl = baseUrl
+  }
+  guard let triggerName = args["triggerName"] as? String else {
+    return ["error": "You must specify a triggerName parameter!"]
+  }
+  print("Trigger Name: \(triggerName)")
+  return Whisk.createTrigger(triggerNamed: triggerName, withParameters: [:])
+}
\ No newline at end of file
diff --git a/tests/dat/actions/sdk/swift4/hello.swift b/tests/dat/actions/sdk/swift4/hello.swift
new file mode 100644
index 0000000..6da92f8
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/hello.swift
@@ -0,0 +1,7 @@
+func main(args: [String:Any]) -> [String:Any] {
+    if let name = args["name"] as? String {
+        return [ "greeting" : "Hello \(name)!" ]
+    } else {
+        return [ "greeting" : "Hello stranger!" ]
+    }
+}
\ No newline at end of file
diff --git a/tests/dat/actions/sdk/swift4/invoke.swift b/tests/dat/actions/sdk/swift4/invoke.swift
new file mode 100644
index 0000000..a23dd69
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/invoke.swift
@@ -0,0 +1,28 @@
+struct Result: Decodable {
+  let date: String
+}
+struct Response: Decodable {
+  let result:Result
+}
+struct Activation: Decodable {
+  let response: Response
+  let activationId: String
+}
+
+func main(args: [String:Any]) -> [String:Any] {
+  if let baseUrl = args["baseUrl"] as? String {
+    //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
+    Whisk.baseUrl = baseUrl
+  }
+  let invokeResult = Whisk.invoke(actionNamed: "/whisk.system/utils/date", withParameters: [:])
+  let jsonData = try! JSONSerialization.data(withJSONObject: invokeResult)
+  let dateActivation = try! JSONDecoder().decode(Activation.self, from: jsonData)
+  let dateString = dateActivation.response.result.date
+  if dateString.isEmpty{
+      print("Could not parse date of of the response.")
+  } else {
+      print("It is now \(dateString)")
+  }
+  // return the entire invokeResult
+  return invokeResult
+}
\ No newline at end of file
diff --git a/tests/dat/actions/sdk/swift4/invokeNonBlocking.swift b/tests/dat/actions/sdk/swift4/invokeNonBlocking.swift
new file mode 100644
index 0000000..f22c9ff
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/invokeNonBlocking.swift
@@ -0,0 +1,23 @@
+struct Activation: Decodable {
+  let activationId: String
+}
+
+func main(args: [String:Any]) -> [String:Any] {
+  if let baseUrl = args["baseUrl"] as? String {
+    //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
+    Whisk.baseUrl = baseUrl
+  }
+  let invokeResult = Whisk.invoke(actionNamed: "/whisk.system/utils/date", withParameters: [:], blocking: false)
+  let jsonData = try! JSONSerialization.data(withJSONObject: invokeResult)
+  let dateActivation = try! JSONDecoder().decode(Activation.self, from: jsonData)
+  // the date we are looking for is the result inside the date activation
+  let activationId = dateActivation.activationId
+  if activationId.isEmpty{
+      print("Failed to invoke.")
+  } else {
+      print("Invoked.")
+  }
+
+  // return the entire invokeResult
+  return invokeResult
+}
diff --git a/tests/dat/actions/sdk/swift4/trigger.swift b/tests/dat/actions/sdk/swift4/trigger.swift
new file mode 100644
index 0000000..79b9a76
--- /dev/null
+++ b/tests/dat/actions/sdk/swift4/trigger.swift
@@ -0,0 +1,12 @@
+func main(args: [String:Any]) -> [String:Any] {
+  if let baseUrl = args["baseUrl"] as? String {
+    //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
+    Whisk.baseUrl = baseUrl
+  }
+  if let triggerName = args["triggerName"] as? String {
+    print("Trigger Name: \(triggerName)")
+    return Whisk.trigger(eventNamed: triggerName, withParameters: [:])
+  } else {
+    return ["error": "You must specify a triggerName parameter!"]
+  }
+}
\ No newline at end of file
diff --git a/tests/dat/build.sh b/tests/dat/build.sh
new file mode 100755
index 0000000..4ddad8f
--- /dev/null
+++ b/tests/dat/build.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+
+../../tools/build/compile.sh  HelloSwift3 swift:3.1.1 "-v"
+
+../../tools/build/compile.sh  HelloSwift4 swift:4 "-v"
+
+
diff --git a/tests/dat/build/swift311/HelloSwift3.zip b/tests/dat/build/swift311/HelloSwift3.zip
new file mode 100644
index 0000000..4a8588a
Binary files /dev/null and b/tests/dat/build/swift311/HelloSwift3.zip differ
diff --git a/tests/dat/build/swift4/HelloSwift4.zip b/tests/dat/build/swift4/HelloSwift4.zip
new file mode 100644
index 0000000..0b919c0
Binary files /dev/null and b/tests/dat/build/swift4/HelloSwift4.zip differ
diff --git a/tests/dat/build/swift4/SwiftyRequest.zip b/tests/dat/build/swift4/SwiftyRequest.zip
new file mode 100644
index 0000000..a43fab6
Binary files /dev/null and b/tests/dat/build/swift4/SwiftyRequest.zip differ
diff --git a/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
index 058ef75..bae51b1 100644
--- a/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/Swift311ActionContainerTests.scala
@@ -17,315 +17,123 @@
 
 package runtime.actionContainers
 
-import java.io.File
-
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
-
-import ActionContainer.withContainer
-import common.WskActorSystem
 import spray.json.JsObject
 import spray.json.JsString
-import common.TestUtils
-
 @RunWith(classOf[JUnitRunner])
-class Swift3ActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
+class Swift311ActionContainerTests extends SwiftActionContainerTests {
 
-  // note: "out" will likely not be empty in some swift build as the compiler
-  // prints status messages and there doesn't seem to be a way to quiet them
-  val enforceEmptyOutputStream = false
-  lazy val swiftContainerImageName = "action-swift-v3.1.1"
-  lazy val envCode = makeEnvCode("ProcessInfo.processInfo")
+  override lazy val swiftContainerImageName = "action-swift-v3.1.1"
+  override lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/build/swift311/HelloSwift3.zip"
 
-  def makeEnvCode(processInfo: String) = ("""
-         |func main(args: [String: Any]) -> [String: Any] {
-         |     let env = """ + processInfo + """.environment
-         |     var a = "???"
-         |     var b = "???"
-         |     var c = "???"
-         |     var d = "???"
-         |     var e = "???"
-         |     var f = "???"
-         |     if let v : String = env["__OW_API_HOST"] {
-         |         a = "\(v)"
-         |     }
-         |     if let v : String = env["__OW_API_KEY"] {
-         |         b = "\(v)"
-         |     }
-         |     if let v : String = env["__OW_NAMESPACE"] {
-         |         c = "\(v)"
-         |     }
-         |     if let v : String = env["__OW_ACTION_NAME"] {
-         |         d = "\(v)"
-         |     }
-         |     if let v : String = env["__OW_ACTIVATION_ID"] {
-         |         e = "\(v)"
-         |     }
-         |     if let v : String = env["__OW_DEADLINE"] {
-         |         f = "\(v)"
-         |     }
-         |     return ["api_host": a, "api_key": b, "namespace": c, "action_name": d, "activation_id": e, "deadline": f]
-         |}
-         """).stripMargin
-
-  lazy val errorCode = """
-                | // You need an indirection, or swiftc detects the div/0
-                | // at compile-time. Smart.
-                | func div(x: Int, y: Int) -> Int {
-                |     return x/y
-                | }
-                | func main(args: [String: Any]) -> [String: Any] {
-                |     return [ "divBy0": div(x:5, y:0) ]
-                | }
-            """.stripMargin
   lazy val watsonCode = """
-                | import AlchemyDataNewsV1
-                | import ConversationV1
-                | import DiscoveryV1
-                | import DocumentConversionV1
-                | import NaturalLanguageClassifierV1
-                | import NaturalLanguageUnderstandingV1
-                | import PersonalityInsightsV3
-                | import RetrieveAndRankV1
-                | import ToneAnalyzerV3
-                | import TradeoffAnalyticsV1
-                | import VisualRecognitionV3
-                |
-                | func main(args: [String:Any]) -> [String:Any] {
-                |     return ["message": "I compiled and was able to import Watson SDKs"]
-                | }
-            """.stripMargin
-  lazy val swiftBinaryName = "helloSwift311.zip"
-
-  // Helpers specific to swift actions
-  override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
-    withContainer(swiftContainerImageName, env)(code)
-  }
-
-  behavior of swiftContainerImageName
-
-  // remove this test: it will not even compile under Swift 3 anymore
-  // so it should not be possible to write an action that does not return
-  // a [String:Any]
-  /*testNotReturningJson(
-        """
-        |func main(args: [String: Any]) -> String {
-        |    return "not a json object"
-        |}
-        """.stripMargin)
-   */
-
-  testEcho(Seq {
-    (
-      "swift",
-      """
+        | import AlchemyDataNewsV1
+        | import ConversationV1
+        | import DiscoveryV1
+        | import DocumentConversionV1
+        | import NaturalLanguageClassifierV1
+        | import NaturalLanguageUnderstandingV1
+        | import PersonalityInsightsV3
+        | import RetrieveAndRankV1
+        | import ToneAnalyzerV3
+        | import TradeoffAnalyticsV1
+        | import VisualRecognitionV3
+        |
+        | func main(args: [String:Any]) -> [String:Any] {
+        |     return ["message": "I compiled and was able to import Watson SDKs"]
+        | }
+    """.stripMargin
+
+  val httpCode = """
+         | import KituraNet
          | import Foundation
-         |
-         | extension FileHandle : TextOutputStream {
-         |     public func write(_ string: String) {
-         |         guard let data = string.data(using: .utf8) else { return }
-         |         self.write(data)
-         |     }
-         | }
-         |
-         | func main(args: [String: Any]) -> [String: Any] {
-         |     print("hello stdout")
-         |     var standardError = FileHandle.standardError
-         |     print("hello stderr", to: &standardError)
-         |     return args
-         | }
-        """.stripMargin)
-  })
-
-  testUnicode(Seq {
-    (
-      "swift",
-      """
-         | func main(args: [String: Any]) -> [String: Any] {
-         |     if let str = args["delimiter"] as? String {
-         |         let msg = "\(str) ? \(str)"
-         |         print(msg)
-         |         return [ "winter" : msg ]
-         |     } else {
-         |         return [ "error" : "no delimiter" ]
+         | import Dispatch
+         | func main(args:[String: Any]) -> [String:Any] {
+         |       let retries = 3
+         |       var resp = [String:Any]()
+         |       var attempts = 0
+         |       if let url = args["getUrl"] as? String {
+         |           while attempts < retries {
+         |               let group = DispatchGroup()
+         |               let queue = DispatchQueue.global(qos: .default)
+         |               group.enter()
+         |               queue.async {
+         |                   HTTP.get(url, callback: { response in
+         |                       if let response = response {
+         |                           do {
+         |                               var jsonData = Data()
+         |                               try response.readAllData(into: &jsonData)
+         |                               if let dic = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
+         |                                   resp = dic
+         |                               } else {
+         |                                   resp = ["error":"response from server is not JSON"]
+         |                               }
+         |                           } catch {
+         |                              resp["error"] = error.localizedDescription
+         |                           }
+         |                       }
+         |                       group.leave()
+         |                   })
+         |               }
+         |            switch group.wait(timeout: DispatchTime.distantFuture) {
+         |                case DispatchTimeoutResult.success:
+         |                    resp["attempts"] = attempts
+         |                    return resp
+         |                case DispatchTimeoutResult.timedOut:
+         |                    attempts = attempts + 1
+         |            }
+         |        }
          |     }
+         |     return ["status":"Exceeded \(retries) attempts, aborting."]
          | }
-         """.stripMargin.trim)
-  })
-
-  testEnv(Seq {
-    ("swift", envCode)
-  }, enforceEmptyOutputStream)
-
-  it should "support actions using non-default entry points" in {
-    withActionContainer() { c =>
-      val code = """
-                | func niam(args: [String: Any]) -> [String: Any] {
-                |     return [ "result": "it works" ]
-                | }
-                |""".stripMargin
-
-      val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
-      initCode should be(200)
-
-      val (_, runRes) = c.run(runPayload(JsObject()))
-      runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
-    }
-  }
-
-  it should "return some error on action error" in {
-    val (out, err) = withActionContainer() { c =>
-      val code = errorCode
-
-      val (initCode, _) = c.init(initPayload(code))
-      initCode should be(200)
-
-      val (runCode, runRes) = c.run(runPayload(JsObject()))
-      runCode should be(502)
-
-      runRes shouldBe defined
-      runRes.get.fields.get("error") shouldBe defined
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        if (enforceEmptyOutputStream) o shouldBe empty
-        e shouldBe empty
-    })
-  }
-
-  it should "log compilation errors" in {
-    val (out, err) = withActionContainer() { c =>
-      val code = """
-              | 10 PRINT "Hello!"
-              | 20 GOTO 10
-            """.stripMargin
-
-      val (initCode, _) = c.init(initPayload(code))
-      initCode should not be (200)
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        if (enforceEmptyOutputStream) o shouldBe empty
-        e.toLowerCase should include("error")
-    })
-  }
-
-  it should "support application errors" in {
-    val (out, err) = withActionContainer() { c =>
-      val code = """
-                | func main(args: [String: Any]) -> [String: Any] {
-                |     return [ "error": "sorry" ]
-                | }
-            """.stripMargin
-
-      val (initCode, _) = c.init(initPayload(code))
-      initCode should be(200)
-
-      val (runCode, runRes) = c.run(runPayload(JsObject()))
-      runCode should be(200) // action writer returning an error is OK
-
-      runRes shouldBe defined
-      runRes should be(Some(JsObject("error" -> JsString("sorry"))))
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        if (enforceEmptyOutputStream) o shouldBe empty
-        e shouldBe empty
-    })
-  }
-
-  it should "support support multiple files in a zip file" in {
-    val zip = new File(TestUtils.getTestActionFilename("multiSwift.zip")).toPath
-    val code = ResourceHelpers.readAsBase64(zip)
-
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, initRes) = c.init(initPayload(code))
-      initCode should be(200)
-
-      val args = JsObject()
-      val (runCode, runRes) = c.run(runPayload(args))
-
-      runCode should be(200)
-      runRes.get shouldBe JsObject("greeting" -> (JsString("Hello stranger!")))
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        if (enforceEmptyOutputStream) o shouldBe empty
-        e shouldBe empty
-    })
-  }
-
-  it should "support pre-compiled binary in a zip file" in {
-    val zip = new File(TestUtils.getTestActionFilename(swiftBinaryName)).toPath
-    val code = ResourceHelpers.readAsBase64(zip)
-
-    val (out, err) = withActionContainer() { c =>
-      val (initCode, initRes) = c.init(initPayload(code))
-      initCode should be(200)
-
-      val args = JsObject()
-      val (runCode, runRes) = c.run(runPayload(args))
-
-      runCode should be(200)
-      runRes.get shouldBe JsObject("greeting" -> (JsString("Hello stranger!")))
-    }
-
-    checkStreams(out, err, {
-      case (o, e) =>
-        if (enforceEmptyOutputStream) o shouldBe empty
-        e shouldBe empty
-    })
-  }
+       """.stripMargin
 
   it should "properly use KituraNet and Dispatch" in {
     val (out, err) = withActionContainer() { c =>
       val code = """
-                | import KituraNet
-                | import Foundation
-                | import Dispatch
-                | func main(args:[String: Any]) -> [String:Any] {
-                |       let retries = 3
-                |       var resp = [String:Any]()
-                |       var attempts = 0
-                |       if let url = args["getUrl"] as? String {
-                |           while attempts < retries {
-                |               let group = DispatchGroup()
-                |               let queue = DispatchQueue.global(qos: .default)
-                |               group.enter()
-                |               queue.async {
-                |                   HTTP.get(url, callback: { response in
-                |                       if let response = response {
-                |                           do {
-                |                               var jsonData = Data()
-                |                               try response.readAllData(into: &jsonData)
-                |                               if let dic = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
-                |                                   resp = dic
-                |                               } else {
-                |                                   resp = ["error":"response from server is not JSON"]
-                |                               }
-                |                           } catch {
-                |                              resp["error"] = error.localizedDescription
-                |                           }
-                |                       }
-                |                       group.leave()
-                |                   })
-                |               }
-                |            switch group.wait(timeout: DispatchTime.distantFuture) {
-                |                case DispatchTimeoutResult.success:
-                |                    resp["attempts"] = attempts
-                |                    return resp
-                |                case DispatchTimeoutResult.timedOut:
-                |                    attempts = attempts + 1
-                |            }
-                |        }
-                |     }
-                |     return ["status":"Exceeded \(retries) attempts, aborting."]
-                | }
-            """.stripMargin
+          | import KituraNet
+          | import Foundation
+          | import Dispatch
+          | func main(args:[String: Any]) -> [String:Any] {
+          |       let retries = 3
+          |       var resp = [String:Any]()
+          |       var attempts = 0
+          |       if let url = args["getUrl"] as? String {
+          |           while attempts < retries {
+          |               let group = DispatchGroup()
+          |               let queue = DispatchQueue.global(qos: .default)
+          |               group.enter()
+          |               queue.async {
+          |                   HTTP.get(url, callback: { response in
+          |                       if let response = response {
+          |                           do {
+          |                               var jsonData = Data()
+          |                               try response.readAllData(into: &jsonData)
+          |                               if let dic = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
+          |                                   resp = dic
+          |                               } else {
+          |                                   resp = ["error":"response from server is not JSON"]
+          |                               }
+          |                           } catch {
+          |                              resp["error"] = error.localizedDescription
+          |                           }
+          |                       }
+          |                       group.leave()
+          |                   })
+          |               }
+          |            switch group.wait(timeout: DispatchTime.distantFuture) {
+          |                case DispatchTimeoutResult.success:
+          |                    resp["attempts"] = attempts
+          |                    return resp
+          |                case DispatchTimeoutResult.timedOut:
+          |                    attempts = attempts + 1
+          |            }
+          |        }
+          |     }
+          |     return ["status":"Exceeded \(retries) attempts, aborting."]
+          | }
+      """.stripMargin
 
       val (initCode, _) = c.init(initPayload(code))
 
diff --git a/tests/src/test/scala/actionContainers/Swift4ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Swift4ActionContainerTests.scala
new file mode 100644
index 0000000..9ad15ac
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/Swift4ActionContainerTests.scala
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.actionContainers
+
+import java.io.File
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import spray.json.{JsObject, JsString}
+@RunWith(classOf[JUnitRunner])
+class Swift4ActionContainerTests extends SwiftActionContainerTests {
+
+  override lazy val swiftContainerImageName = "action-swift-v4"
+  override lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/build/swift4/HelloSwift4.zip"
+  val partyCompile = System.getProperty("user.dir") + "/dat/build/swift4/SwiftyRequest.zip"
+
+  val httpCode = """
+       | import Dispatch
+       | func main(args:[String: Any]) -> [String:Any] {
+       |     var resp :[String:Any] = ["error":"getUrl failed"]
+       |     guard let urlStr = args["getUrl"] as? String else {
+       |         return ["error":"getUrl not found in action input"]
+       |     }
+       |     guard let url = URL(string: urlStr) else {
+       |         return ["error":"invalid url string \(urlStr)"]
+       |     }
+       |     let request = URLRequest(url: url)
+       |     let session = URLSession(configuration: .default)
+       |     let semaphore = DispatchSemaphore(value: 0)
+       |     let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
+       |         print("done with http request")
+       |         if let error = error {
+       |             print("There was an error \(error)")
+       |         } else if let data = data,
+       |             let response = response as? HTTPURLResponse,
+       |             response.statusCode == 200 {
+       |             do {
+       |                 let respJson = try JSONSerialization.jsonObject(with: data)
+       |                 if respJson is [String:Any] {
+       |                     resp = respJson as! [String:Any]
+       |                 } else {
+       |                     resp = ["error":"Response from server is not a dictionary"]
+       |                 }
+       |             } catch {
+       |                 resp = ["error":"Error creating json from response: \(error)"]
+       |             }
+       |         }
+       |         semaphore.signal()
+       |     })
+       |     task.resume()
+       |     _ = semaphore.wait(timeout: .distantFuture)
+       |     return resp
+       | }
+     """.stripMargin
+
+  it should "support ability to use 3rd party packages like SwiftyRequest" in {
+    val zip = new File(partyCompile).toPath
+    val code = ResourceHelpers.readAsBase64(zip)
+
+    val (out, err) = withActionContainer() { c =>
+      val (initCode, initRes) = c.init(initPayload(code))
+      initCode should be(200)
+
+      val args = JsObject("message" -> (JsString("serverless")))
+      val (runCode, runRes) = c.run(runPayload(args))
+
+      runCode should be(200)
+      val json = runRes.get.fields.get("json")
+      json shouldBe Some(args)
+    }
+
+    checkStreams(out, err, {
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
+    })
+  }
+
+}
diff --git a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala b/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
similarity index 51%
rename from tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
rename to tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
index 1471d12..f1b8752 100644
--- a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/SwiftActionContainerTests.scala
@@ -18,209 +18,238 @@
 package runtime.actionContainers
 
 import java.io.File
-import java.util.Base64
-
-import org.apache.commons.io.FileUtils
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-
-import ActionContainer.withContainer
-import common.TestUtils
 import common.WskActorSystem
+import ActionContainer.withContainer
 import spray.json.DefaultJsonProtocol._
 import spray.json._
 
-@RunWith(classOf[JUnitRunner])
-class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSystem {
-
-  override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
-    withContainer("dockerskeleton", env)(code)
-  }
+abstract class SwiftActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
 
-  val codeNotReturningJson = """
-           |#!/bin/sh
-           |echo not a json object
-        """.stripMargin.trim
-
-  /** Standard code samples, should print 'hello' to stdout and echo the input args. */
-  val stdCodeSamples = {
-    val bash = """
-                |#!/bin/bash
-                |echo 'hello stdout'
-                |echo 'hello stderr' 1>&2
-                |if [[ -z $1 || $1 == '{}' ]]; then
-                |   echo '{ "msg": "Hello from bash script!" }'
-                |else
-                |   echo $1 # echo the arguments back as the result
-                |fi
-            """.stripMargin.trim
-
-    val python = """
-                |#!/usr/bin/env python
-                |from __future__ import print_function
-                |import sys
-                |print('hello stdout')
-                |print('hello stderr', file=sys.stderr)
-                |print(sys.argv[1])
-            """.stripMargin.trim
-
-    val perl = """
-                |#!/usr/bin/env perl
-                |print STDOUT "hello stdout\n";
-                |print STDERR "hello stderr\n";
-                |print $ARGV[0];
-            """.stripMargin.trim
-
-    // excluding perl as it not installed in alpine based image
-    Seq(("bash", bash), ("python", python))
-  }
+  // note: "out" will likely not be empty in some swift build as the compiler
+  // prints status messages and there doesn't seem to be a way to quiet them
+  val enforceEmptyOutputStream = false
+  lazy val swiftContainerImageName = "action-swift-v4"
+  lazy val swiftBinaryName = System.getProperty("user.dir") + "/dat/actions/swift4zip/build/Hello.zip"
+  val httpCode: String
 
-  val stdUnicodeSamples = {
-    // python 3 in base image
-    val python = """
-                |#!/usr/bin/env python
-                |import json, sys
-                |j = json.loads(sys.argv[1])
-                |sep = j["delimiter"]
-                |s = sep + " ? " + sep
-                |print(s)
-                |print(json.dumps({"winter": s}))
-            """.stripMargin.trim
-
-    Seq(("python", python))
-  }
+  behavior of swiftContainerImageName
 
-  /** Standard code samples, should print 'hello' to stdout and echo the input args. */
-  val stdEnvSamples = {
-    val bash = """
-                |#!/bin/bash
-                |echo "{ \
-                |\"api_host\": \"$__OW_API_HOST\", \"api_key\": \"$__OW_API_KEY\", \
-                |\"namespace\": \"$__OW_NAMESPACE\", \"action_name\": \"$__OW_ACTION_NAME\", \
-                |\"activation_id\": \"$__OW_ACTIVATION_ID\", \"deadline\": \"$__OW_DEADLINE\" }"
-            """.stripMargin.trim
-
-    val python =
+  testEcho(Seq {
+    (
+      "swift echo",
       """
-                |#!/usr/bin/env python
-                |import os
-                |
-                |print('{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", "activation_id": "%s", "deadline": "%s" }' % (
-                |  os.environ['__OW_API_HOST'], os.environ['__OW_API_KEY'],
-                |  os.environ['__OW_NAMESPACE'], os.environ['__OW_ACTION_NAME'],
-                |  os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE']))
-            """.stripMargin.trim
-
-    val perl =
+        | import Foundation
+        |
+        | extension FileHandle : TextOutputStream {
+        |     public func write(_ string: String) {
+        |         guard let data = string.data(using: .utf8) else { return }
+        |         self.write(data)
+        |     }
+        | }
+        |
+        | func main(args: [String: Any]) -> [String: Any] {
+        |     print("hello stdout")
+        |     var standardError = FileHandle.standardError
+        |     print("hello stderr", to: &standardError)
+        |     return args
+        | }
+      """.stripMargin)
+  })
+
+  testUnicode(Seq {
+    (
+      "swift unicode",
       """
-                |#!/usr/bin/env perl
-                |$a = $ENV{'__OW_API_HOST'};
-                |$b = $ENV{'__OW_API_KEY'};
-                |$c = $ENV{'__OW_NAMESPACE'};
-                |$d = $ENV{'__OW_ACTION_NAME'};
-                |$e = $ENV{'__OW_ACTIVATION_ID'};
-                |$f = $ENV{'__OW_DEADLINE'};
-                |print "{ \"api_host\": \"$a\", \"api_key\": \"$b\", \"namespace\": \"$c\", \"action_name\": \"$d\", \"activation_id\": \"$e\", \"deadline\": \"$f\" }";
-            """.stripMargin.trim
-
-    // excluding perl as it not installed in alpine based image
-    Seq(("bash", bash), ("python", python))
-  }
-
-  behavior of "openwhisk/dockerskeleton"
+        | func main(args: [String: Any]) -> [String: Any] {
+        |     if let str = args["delimiter"] as? String {
+        |         let msg = "\(str) ? \(str)"
+        |         print(msg)
+        |         return [ "winter" : msg ]
+        |     } else {
+        |         return [ "error" : "no delimiter" ]
+        |     }
+        | }
+      """.stripMargin.trim)
+  })
+
+  testEnv(
+    Seq {
+      (
+        "swift environment",
+        """
+        | func main(args: [String: Any]) -> [String: Any] {
+        |     let env = ProcessInfo.processInfo.environment
+        |     var a = "???"
+        |     var b = "???"
+        |     var c = "???"
+        |     var d = "???"
+        |     var e = "???"
+        |     var f = "???"
+        |     if let v : String = env["__OW_API_HOST"] {
+        |         a = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_API_KEY"] {
+        |         b = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_NAMESPACE"] {
+        |         c = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_ACTION_NAME"] {
+        |         d = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_ACTIVATION_ID"] {
+        |         e = "\(v)"
+        |     }
+        |     if let v : String = env["__OW_DEADLINE"] {
+        |         f = "\(v)"
+        |     }
+        |     return ["api_host": a, "api_key": b, "namespace": c, "action_name": d, "activation_id": e, "deadline": f]
+        | }
+      """.stripMargin)
+    },
+    enforceEmptyOutputStream)
+
+  it should "support actions using non-default entry points" in {
+    withActionContainer() { c =>
+      val code = """
+                   | func niam(args: [String: Any]) -> [String: Any] {
+                   |     return [ "result": "it works" ]
+                   | }
+                   |""".stripMargin
+
+      val (initCode, initRes) = c.init(initPayload(code, main = "niam"))
+      initCode should be(200)
 
-  it should "run sample without init" in {
-    val (out, err) = withActionContainer() { c =>
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
+      val (_, runRes) = c.run(runPayload(JsObject()))
+      runRes.get.fields.get("result") shouldBe Some(JsString("it works"))
     }
-
-    checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
-    })
   }
 
-  it should "run sample with 'null' init" in {
+  it should "return some error on action error" in {
     val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(initPayload(null))
+      val code = """
+                   | // You need an indirection, or swiftc detects the div/0
+                   | // at compile-time. Smart.
+                   | func div(x: Int, y: Int) -> Int {
+                   |     return x/y
+                   | }
+                   | func main(args: [String: Any]) -> [String: Any] {
+                   |     return [ "divBy0": div(x:5, y:0) ]
+                   | }
+                 """.stripMargin
+
+      val (initCode, _) = c.init(initPayload(code))
       initCode should be(200)
 
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
+      val (runCode, runRes) = c.run(runPayload(JsObject()))
+      runCode should be(502)
+
+      runRes shouldBe defined
+      runRes.get.fields.get("error") shouldBe defined
     }
 
     checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
     })
   }
 
-  it should "run sample with init that does nothing" in {
+  it should "log compilation errors" in {
     val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(JsObject())
-      initCode should be(200)
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic."))))
+      val code = """
+                   | 10 PRINT "Hello!"
+                   | 20 GOTO 10
+                 """.stripMargin
+
+      val (initCode, _) = c.init(initPayload(code))
+      initCode should not be (200)
     }
 
     checkStreams(out, err, {
-      case (o, _) => o should include("This is a stub action")
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e.toLowerCase should include("error")
     })
   }
 
-  it should "respond with 404 for bad run argument" in {
+  it should "support application errors" in {
     val (out, err) = withActionContainer() { c =>
-      val (runCode, out) = c.run(runPayload(JsString("A")))
-      runCode should be(404)
+      val code = """
+                   | func main(args: [String: Any]) -> [String: Any] {
+                   |     return [ "error": "sorry" ]
+                   | }
+                 """.stripMargin
+
+      val (initCode, _) = c.init(initPayload(code))
+      initCode should be(200)
+
+      val (runCode, runRes) = c.run(runPayload(JsObject()))
+      runCode should be(200) // action writer returning an error is OK
+
+      runRes shouldBe defined
+      runRes should be(Some(JsObject("error" -> JsString("sorry"))))
     }
 
     checkStreams(out, err, {
       case (o, e) =>
-        o shouldBe empty
+        if (enforceEmptyOutputStream) o shouldBe empty
         e shouldBe empty
     })
   }
 
-  it should "fail to run a bad script" in {
+  it should "support pre-compiled binary in a zip file" in {
+    val zip = new File(swiftBinaryName).toPath
+    val code = ResourceHelpers.readAsBase64(zip)
+
     val (out, err) = withActionContainer() { c =>
-      val (initCode, _) = c.init(initPayload(""))
+      val (initCode, initRes) = c.init(initPayload(code))
       initCode should be(200)
-      val (runCode, out) = c.run(JsNull)
-      runCode should be(502)
-      out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary."))))
+
+      val args = JsObject()
+      val (runCode, runRes) = c.run(runPayload(args))
+
+      runCode should be(200)
+      runRes.get shouldBe JsObject("greeting" -> (JsString("Hello stranger!")))
     }
 
     checkStreams(out, err, {
-      case (o, _) => o should include("error")
+      case (o, e) =>
+        if (enforceEmptyOutputStream) o shouldBe empty
+        e shouldBe empty
     })
   }
 
-  it should "extract and run a compatible zip exec" in {
-    val zip = FileUtils.readFileToByteArray(new File(TestUtils.getTestActionFilename("blackbox.zip")))
-    val contents = Base64.getEncoder.encodeToString(zip)
-
+  it should "be able to do an http request" in {
     val (out, err) = withActionContainer() { c =>
-      val (initCode, err) =
-        c.init(JsObject("value" -> JsObject("code" -> JsString(contents), "binary" -> JsBoolean(true))))
+      val (initCode, _) = c.init(initPayload(httpCode))
+
       initCode should be(200)
-      val (runCode, out) = c.run(JsObject())
-      runCode should be(200)
-      out.get should be(JsObject("msg" -> JsString("hello zip")))
+
+      val argss = List(JsObject("getUrl" -> JsString("https://openwhisk.ng.bluemix.net/api/v1")))
+
+      for (args <- argss) {
+        val (runCode, out) = c.run(runPayload(args))
+        runCode should be(200)
+      }
     }
 
+    // in side try catch finally print (out file)
+    // in catch block an error has occurred, get docker logs and print
+    // throw
+
     checkStreams(out, err, {
       case (o, e) =>
-        o shouldBe "This is an example zip used with the docker skeleton action."
+        if (enforceEmptyOutputStream) o shouldBe empty
         e shouldBe empty
     })
   }
 
-  testNotReturningJson(codeNotReturningJson, checkResultInLogs = true)
-  testEcho(stdCodeSamples)
-  testUnicode(stdUnicodeSamples)
-  testEnv(stdEnvSamples)
+  // Helpers specific to swift actions
+  override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
+    withContainer(swiftContainerImageName, env)(code)
+  }
+
 }
 
 trait BasicActionRunnerTests extends ActionProxyContainerTestUtils {
@@ -344,5 +373,6 @@ trait BasicActionRunnerTests extends ActionProxyContainerTestUtils {
         })
       }
     }
+
   }
 }
diff --git a/tests/src/test/scala/sdk/Swift311SDKTests.scala b/tests/src/test/scala/sdk/Swift311SDKTests.scala
new file mode 100644
index 0000000..7cf9b06
--- /dev/null
+++ b/tests/src/test/scala/sdk/Swift311SDKTests.scala
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.sdk
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class Swift311SDKTests extends SwiftSDKTests {
+  override lazy val actionKind = "swift:3.1.1"
+}
diff --git a/tests/src/test/scala/sdk/Swift4SDKTests.scala b/tests/src/test/scala/sdk/Swift4SDKTests.scala
new file mode 100644
index 0000000..fe9a6ea
--- /dev/null
+++ b/tests/src/test/scala/sdk/Swift4SDKTests.scala
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.sdk
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class Swift4SDKTests extends SwiftSDKTests {
+  override lazy val actionKind = "swift:4"
+}
diff --git a/tests/src/test/scala/sdk/SwiftSDKTests.scala b/tests/src/test/scala/sdk/SwiftSDKTests.scala
new file mode 100644
index 0000000..ad77386
--- /dev/null
+++ b/tests/src/test/scala/sdk/SwiftSDKTests.scala
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package runtime.sdk
+
+import java.io.File
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+import org.scalatest.Matchers
+import common.{TestHelpers, WhiskProperties, WskProps, WskTestHelpers}
+import common.rest.WskRest
+import spray.json._
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+
+abstract class SwiftSDKTests extends TestHelpers with WskTestHelpers with Matchers {
+
+  implicit val wskprops = WskProps()
+  val wsk = new WskRest
+  val expectedDuration = 45 seconds
+  val activationPollDuration = 60 seconds
+  lazy val actionKind = "swift:3.1.1"
+  lazy val lang = actionKind.split(":")(0)
+  lazy val majorVersion = actionKind.split(":")(1).split('.')(0)
+  lazy val actionDir = s"$lang$majorVersion"
+  lazy val actionTypeDir: String = System.getProperty("user.dir") + "/dat/actions/sdk/" + actionDir
+  val controllerHost = WhiskProperties.getBaseControllerHost()
+  val controllerPort = WhiskProperties.getControllerBasePort()
+  val baseUrl = s"http://$controllerHost:$controllerPort"
+  //when running tests on environment with valid ssl certs in whisk host then pass -DswiftUseSSLWhiskHost=true
+  val overridebaseURL = "true" != System.getProperty("swiftUseSSLWhiskHost")
+
+  behavior of s"Swift Whisk SDK tests using $actionKind"
+
+  it should "allow Swift actions to invoke other actions" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    val file = Some(new File(actionTypeDir, "invoke.swift").toString())
+
+    val actionName = "invokeAction"
+    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+      action.create(name = actionName, artifact = file, kind = Some(actionKind))
+    }
+    // invoke the action
+    var params = Map("dummy" -> JsString("dummy"))
+    if (overridebaseURL)
+      params = params + ("baseUrl" -> JsString(baseUrl))
+
+    val run = wsk.action.invoke(actionName, params)
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+      // should be successful
+      activation.response.success shouldBe true
+
+      // should have a field named "activationId" which is the date action's activationId
+      activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+      // check for "date" field that comes from invoking the date action
+      whisk.utils.JsHelpers.fieldPathExists(activation.response.result.get, "response", "result", "date") should be(
+        true)
+    }
+  }
+
+  it should "allow Swift actions to invoke other actions and not block" in withAssetCleaner(wskprops) {
+    (wp, assetHelper) =>
+      // use CLI to create action from dat/actions/invokeNonBlocking.swift
+      val file = Some(new File(actionTypeDir, "invokeNonBlocking.swift").toString())
+      val actionName = "invokeNonBlockingAction"
+      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+        action.create(name = actionName, file, kind = Some(actionKind))
+      }
+
+      // invoke the action
+      var params = Map("dummy" -> JsString("dummy"))
+      if (overridebaseURL)
+        params = params + ("baseUrl" -> JsString(baseUrl))
+
+      val run = wsk.action.invoke(actionName, params)
+      withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+        // should not have a "response"
+        whisk.utils.JsHelpers.fieldPathExists(activation.response.result.get, "response") shouldBe false
+
+        // should have a field named "activationId" which is the date action's activationId
+        activation.response.result.get.fields("activationId").toString.length should be >= 32
+      }
+  }
+
+  it should "allow Swift actions to trigger events" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    // create a trigger
+    val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName)
+    }
+
+    // create an action that fires the trigger
+    val file = Some(new File(actionTypeDir, "trigger.swift").toString())
+    val actionName = "ActionThatTriggers"
+    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+      action.create(name = actionName, file, kind = Some(actionKind))
+    }
+
+    // invoke the action
+    var params = Map("triggerName" -> JsString(triggerName))
+    if (overridebaseURL)
+      params = params + ("baseUrl" -> JsString(baseUrl))
+
+    val run = wsk.action.invoke(actionName, params)
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+      // should be successful
+      activation.response.success shouldBe true
+
+      // should have a field named "activationId" which is the date action's activationId
+      activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+      // should result in an activation for triggerName
+      val triggerActivations = wsk.activation.pollFor(1, Some(triggerName), retries = 20)
+      withClue(s"trigger activations for $triggerName:") {
+        triggerActivations.length should be(1)
+      }
+    }
+  }
+
+  it should "allow Swift actions to create a trigger" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    // create an action that creates the trigger
+    val file = Some(new File(actionTypeDir, "createTrigger.swift").toString())
+    val actionName = "ActionThatTriggers"
+
+    // the name of the trigger to create
+    val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+      assetHelper.withCleaner(wsk.trigger, triggerName) { (_, _) =>
+        // using an asset cleaner on the created trigger name will clean it up at the conclusion of the test
+        action.create(name = actionName, file, kind = Some(actionKind))
+      }
+    }
+
+    // invoke the action
+    var params = Map("triggerName" -> JsString(triggerName))
+    if (overridebaseURL)
+      params = params + ("baseUrl" -> JsString(baseUrl))
+
+    val run = wsk.action.invoke(actionName, params)
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+      // should be successful
+      activation.response.success shouldBe true
+
+      // should have a field named "name" which is the name of the trigger created
+      activation.response.result.get.fields("name") shouldBe JsString(triggerName)
+    }
+  }
+
+  it should "allow Swift actions to create a rule" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    val ruleTriggerName = s"TestTrigger ${System.currentTimeMillis()}"
+    val ruleActionName = s"TestAction ${System.currentTimeMillis()}"
+    val ruleName = s"TestRule ${System.currentTimeMillis()}"
+
+    // create a dummy action and trigger for the rule
+    assetHelper.withCleaner(wsk.action, ruleActionName) { (action, name) =>
+      val dummyFile = Some(new File(actionTypeDir, "hello.swift").toString())
+      action.create(name, dummyFile, kind = Some(actionKind))
+    }
+
+    assetHelper.withCleaner(wsk.trigger, ruleTriggerName) { (trigger, name) =>
+      assetHelper.withCleaner(wsk.rule, ruleName) { (_, _) =>
+        // using an asset cleaner on the created trigger name will clean it up at the conclusion of the test
+        trigger.create(name)
+      }
+    }
+
+    // create an action that creates the rule
+    val createRuleFile = Some(new File(actionTypeDir, "createRule.swift").toString())
+    assetHelper.withCleaner(wsk.action, "ActionThatCreatesRule") { (action, name) =>
+      action.create(name, createRuleFile, kind = Some(actionKind))
+    }
+
+    // invoke the create rule action
+    var params = Map(
+      "triggerName" -> s"/_/$ruleTriggerName".toJson,
+      "actionName" -> s"/_/$ruleActionName".toJson,
+      "ruleName" -> ruleName.toJson)
+
+    if (overridebaseURL)
+      params = params + ("baseUrl" -> JsString(baseUrl))
+
+    val run = wsk.action.invoke("ActionThatCreatesRule", params)
+    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) { activation =>
+      // should be successful
+      activation.response.success shouldBe true
+
+      // should have a field named "trigger" which is the name of the trigger associated with the rule
+      activation.response.result.get.fields("trigger").asJsObject.fields("name") shouldBe ruleTriggerName.toJson
+
+      // should have a field named "action" which is the name of the action associated with the rule
+      activation.response.result.get.fields("action").asJsObject.fields("name") shouldBe ruleActionName.toJson
+    }
+  }
+
+}
diff --git a/tools/build/compile.sh b/tools/build/compile.sh
new file mode 100755
index 0000000..716511d
--- /dev/null
+++ b/tools/build/compile.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+set -e
+
+if [ -z "$1" ] ; then
+    echo 'Error: Missing action name'
+    exit 1
+fi
+if [ -z "$2" ] ; then
+    echo 'Error: Missing runtime docker image name, for example openwhisk/action-swift-v4'
+    exit 2
+fi
+
+BASE_PATH="/swift3Action"
+DEST_SOURCE="$BASE_PATH/spm-build"
+RUNTIME="openwhisk/action-swift-v3.1.1"
+if [ ${2} == "swift:3.1.1" ]; then
+  OUTPUT_DIR="build/swift311"
+elif [ ${2} == "swift:4" ]; then
+  RUNTIME="action-swift-v4"
+  BASE_PATH="/swift4Action"
+  DEST_SOURCE="/$BASE_PATH/spm-build/Sources/Action"
+  OUTPUT_DIR="build/swift4"
+else
+  echo "Error: Kind $2 not recognize"
+  exit 3
+fi
+DEST_PACKAGE_SWIFT="$BASE_PATH/spm-build/Package.swift"
+
+BUILD_FLAGS=""
+if [ -n "$3" ] ; then
+    BUILD_FLAGS=${3}
+fi
+
+
+echo "Using runtime $RUNTIME to compile swift"
+docker run --rm --name=compile-ow-swift -it -v "$(pwd):/owexec" $RUNTIME bash -ex -c "
+
+if [ -f \"/owexec/$OUTPUT_DIR/$1.zip\" ] ; then
+    rm \"/owexec/$OUTPUT_DIR/$1.zip\"
+fi
+
+echo 'Setting up build...'
+cp /owexec/actions/$1/Sources/*.swift $DEST_SOURCE/
+
+# action file can be either {action name}.swift or main.swift
+if [ -f \"$DEST_SOURCE/$1.swift\" ] ; then
+    echo 'renaming $DEST_SOURCE/$1.swift $DEST_SOURCE/main.swift'
+    mv \"$DEST_SOURCE/$1.swift\" $DEST_SOURCE/main.swift
+fi
+# Add in the OW specific bits
+cat $BASE_PATH/epilogue.swift >> $DEST_SOURCE/main.swift
+echo '_run_main(mainFunction:main)' >> $DEST_SOURCE/main.swift
+
+echo \"Compiling $1...\"
+cd /$BASE_PATH/spm-build
+cp /owexec/actions/$1/Package.swift $DEST_PACKAGE_SWIFT
+# we have our own Package.swift, do a full compile
+swift build ${BUILD_FLAGS} -c release
+
+echo 'Creating archive $1.zip...'
+#.build/release/Action
+mkdir -p /owexec/$OUTPUT_DIR
+zip \"/owexec/$OUTPUT_DIR/$1.zip\" .build/release/Action
+
+"
\ No newline at end of file
diff --git a/tools/travis/build.sh b/tools/travis/build.sh
index 1ac1e5a..3a20575 100755
--- a/tools/travis/build.sh
+++ b/tools/travis/build.sh
@@ -15,6 +15,7 @@ IMAGE_PREFIX="testing"
 cd $ROOTDIR
 TERM=dumb ./gradlew \
 :core:swift3.1.1Action:distDocker \
+:core:swift4Action:distDocker \
 -PdockerImagePrefix=${IMAGE_PREFIX}
 
 
diff --git a/tools/travis/deploy.sh b/tools/travis/deploy.sh
index 1be60b3..e8c9a6b 100755
--- a/tools/travis/deploy.sh
+++ b/tools/travis/deploy.sh
@@ -20,6 +20,7 @@ $ANSIBLE_CMD couchdb.yml
 $ANSIBLE_CMD initdb.yml
 $ANSIBLE_CMD wipe.yml
 $ANSIBLE_CMD openwhisk.yml -e cli_installation_mode=remote
+$ANSIBLE_CMD postdeploy.yml
 
 docker images
 docker ps
diff --git a/tools/travis/publish.sh b/tools/travis/publish.sh
index 979089a..64ebcb9 100755
--- a/tools/travis/publish.sh
+++ b/tools/travis/publish.sh
@@ -15,6 +15,8 @@ IMAGE_TAG=$3
 
 if [ ${RUNTIME_VERSION} == "3.1.1" ]; then
   RUNTIME="swift3.1.1Action"
+elif [ ${RUNTIME_VERSION} == "4" ]; then
+  RUNTIME="swift4Action"
 fi
 
 if [[ ! -z ${DOCKER_USER} ]] && [[ ! -z ${DOCKER_PASSWORD} ]]; then
diff --git a/tools/travis/test.sh b/tools/travis/test.sh
index a3c47d6..bdcc407 100755
--- a/tools/travis/test.sh
+++ b/tools/travis/test.sh
@@ -11,7 +11,7 @@ export OPENWHISK_HOME=$WHISKDIR
 
 cd ${ROOTDIR}
 TERM=dumb ./gradlew :tests:checkScalafmtAll
-TERM=dumb ./gradlew :tests:test --tests *Swift3*Tests
+TERM=dumb ./gradlew :tests:test
 
 
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services