├── .gitignore ├── src └── main │ ├── webapp │ └── images │ │ └── logo-96.png │ ├── resources │ ├── org │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── appetize │ │ │ ├── AppetizeCredentials │ │ │ ├── help-description.html │ │ │ ├── help-apiToken.html │ │ │ ├── credentials.jelly │ │ │ └── help-id.html │ │ │ ├── AppetizeRecorder │ │ │ ├── help-platform.html │ │ │ ├── help-apiTokenId.html │ │ │ ├── help-appPath.html │ │ │ └── config.jelly │ │ │ ├── AppetizeBuildAction │ │ │ └── summary.jelly │ │ │ └── AppetizeProjectAction │ │ │ ├── floatingBox.jelly │ │ │ └── jobMain.jelly │ └── index.jelly │ └── java │ └── org │ └── jenkinsci │ └── plugins │ └── appetize │ ├── AppetizeProjectAction.java │ ├── AppetizeBuildAction.java │ ├── AppetizeCredentials.java │ ├── AppetizeApp.java │ ├── AppetizeApiService.java │ └── AppetizeRecorder.java ├── README.md ├── LICENSE.txt └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | work/ 3 | .idea/ 4 | *.iml 5 | -------------------------------------------------------------------------------- /src/main/webapp/images/logo-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/appetize-plugin/master/src/main/webapp/images/logo-96.png -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeCredentials/help-description.html: -------------------------------------------------------------------------------- 1 |
2 | An optional description to help tell similar credentials apart. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeRecorder/help-platform.html: -------------------------------------------------------------------------------- 1 |
2 |

Appetize.io currently supports streaming iOS and Android apps.

3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeCredentials/help-apiToken.html: -------------------------------------------------------------------------------- 1 |
2 | API Key obtained from https://appetize.io/api 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Stream iOS & Android builds directly within Jenkins via 4 | Appetize.io's 5 | cloud-based iOS Simulators & Android Emulators. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeCredentials/credentials.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeCredentials/help-id.html: -------------------------------------------------------------------------------- 1 |
2 | An internal unique ID by which these credentials are identified from jobs and other configuration. 3 | Normally left blank, in which case an ID will be generated, which is fine for jobs created using visual forms. 4 | Useful to specify explicitly when using credentials from scripted configuration. 5 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | ## This plugin is deprecated and no longer supported 3 | 4 | # appetize-plugin 5 | 6 | Stream iOS & Android builds directly within Jenkins via Appetize.io's cloud-based iOS Simulators & Android Emulators. 7 | 8 | Demos: [iPhone](https://appetize.io/demo?device=iphone), [iPad](https://appetize.io/demo?device=ipad), [Nexus 5](https://appetize.io/demo?device=nexus5) 9 | 10 | ![Plugin Screenshot](https://appetize.io/images/jenkins-plugin-screenshot.png) 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeRecorder/help-apiTokenId.html: -------------------------------------------------------------------------------- 1 |
2 |

The API Token to use when connecting to Appetize.io. Note that all apps, including those uploaded 3 | with the placeholder token, are private and secure.

4 | 5 |

The placeholder token is intended for temporary use only. Request your own at 6 | https://appetize.io/api and enter it at 7 | Jenkins → Credentials → Global credentials. 8 |

9 |
10 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeBuildAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | View on Appetize.io 6 |
7 | Manage App Settings 8 |
9 |
10 | ${it.getEmbedHtml()} 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeProjectAction/floatingBox.jelly: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 |
7 | ${action.getLastAppetizeBuild().getEmbedHtml()} 8 |

Last Successful Build (#${action.getLastAppetizeBuild().getBuildNumber()})

9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeRecorder/help-appPath.html: -------------------------------------------------------------------------------- 1 |
2 |

The output location of your app, relative to the project workspace.

3 | 4 |

iOS

5 |

Specify the ".app" folder that contains the simulator build, e.g. 6 | build/Release-iphonesimulator/example.app

7 | 8 |

Note that iOS apps require a simulator build. To create a build with a script, use the command:

9 | 10 |

xcodebuild -sdk iphonesimulator

11 | 12 |

13 | If you are using the Jenkins Xcode plugin, specify iphonesimulator for SDK 14 | under "Advanced Xcode build options". 15 |

16 | 17 |

Android

18 |

Specify the location of your apk, e.g. app/build/outputs/apk/app-release.apk

19 | 20 |

Android apps that use the NDK (very few do) must include the x86 instruction set.

21 |
22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Appetize.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeProjectAction/jobMain.jelly: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 18 | 19 |
9 | 10 | 12 | View on Appetize.io 13 |
14 | Manage App Settings 15 |
16 | Last Successful Build (#${it.getLastAppetizeBuild().getBuildNumber()}) 17 |
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/appetize/AppetizeRecorder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeProjectAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.appetize; 26 | 27 | import hudson.model.AbstractBuild; 28 | import hudson.model.AbstractProject; 29 | import hudson.model.Action; 30 | 31 | /** 32 | * Developers: Weiyin He and John Snyder 33 | */ 34 | public class AppetizeProjectAction implements Action { 35 | private final AbstractProject project; 36 | 37 | public AppetizeProjectAction(AbstractProject project) { 38 | this.project = project; 39 | } 40 | 41 | public AppetizeBuildAction getLastAppetizeBuild() { 42 | AbstractBuild build = project.getLastSuccessfulBuild(); 43 | return build == null ? null : build.getAction(AppetizeBuildAction.class); 44 | } 45 | 46 | @Override 47 | public String getIconFileName() { 48 | return null; 49 | } 50 | 51 | @Override 52 | public String getDisplayName() { 53 | return null; 54 | } 55 | 56 | @Override 57 | public String getUrlName() { 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 1.480 9 | 10 | 11 | org.jenkins-ci.plugins 12 | appetize 13 | 1.1.1-SNAPSHOT 14 | hpi 15 | 16 | Appetize.io Plugin 17 | Stream iOS & Android builds directly within Jenkins via Appetize.io's cloud-based iOS Simulators & Android Emulators. 18 | https://wiki.jenkins-ci.org/display/JENKINS/Appetize.io+Plugin 19 | 20 | 21 | MIT License 22 | http://opensource.org/licenses/MIT 23 | 24 | 25 | 26 | 27 | weiyin 28 | Weiyin He 29 | weiyin@appetize.io 30 | 31 | 32 | jcsnyder 33 | John Snyder 34 | john@appetize.io 35 | 36 | 37 | 38 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 39 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 40 | http://github.com/jenkinsci/${project.artifactId}-plugin 41 | HEAD 42 | 43 | 44 | 45 | repo.jenkins-ci.org 46 | https://repo.jenkins-ci.org/public/ 47 | 48 | 49 | 50 | 51 | repo.jenkins-ci.org 52 | https://repo.jenkins-ci.org/public/ 53 | 54 | 55 | 56 | 57 | org.jenkins-ci.plugins 58 | credentials 59 | 1.21 60 | 61 | 62 | com.google.code.gson 63 | gson 64 | 2.3.1 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeBuildAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.appetize; 26 | 27 | import hudson.EnvVars; 28 | import hudson.model.AbstractBuild; 29 | import hudson.model.EnvironmentContributingAction; 30 | 31 | /** 32 | * Developers: Weiyin He and John Snyder 33 | */ 34 | public class AppetizeBuildAction extends AppetizeApp implements EnvironmentContributingAction { 35 | private int buildNumber; 36 | 37 | public AppetizeBuildAction(String platform, String privateKey, String publicKey, String publicUrl, String manageUrl, int buildNumber) { 38 | super(platform, privateKey, publicKey, publicUrl, manageUrl); 39 | this.buildNumber = buildNumber; 40 | } 41 | 42 | public void buildEnvVars(AbstractBuild abstractBuild, EnvVars envVars) { 43 | if (envVars == null) return; 44 | 45 | envVars.put("APPETIZEIO_PUBLIC_KEY", getPublicKey()); 46 | envVars.put("APPETIZEIO_PRIVATE_KEY", getPrivateKey()); 47 | envVars.put("APPETIZEIO_PUBLIC_URL", getPublicUrl()); 48 | envVars.put("APPETIZEIO_MANAGE_URL", getManageUrl()); 49 | } 50 | 51 | @Override 52 | public String getIconFileName() { 53 | // don't show anything on left sidebar 54 | return null; 55 | } 56 | 57 | @Override 58 | public String getDisplayName() { 59 | return null; 60 | } 61 | 62 | @Override 63 | public String getUrlName() { 64 | return null; 65 | } 66 | 67 | public int getBuildNumber() { 68 | return buildNumber; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeCredentials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package org.jenkinsci.plugins.appetize; 25 | 26 | import com.cloudbees.plugins.credentials.CredentialsNameProvider; 27 | import com.cloudbees.plugins.credentials.CredentialsScope; 28 | import com.cloudbees.plugins.credentials.NameWith; 29 | import com.cloudbees.plugins.credentials.common.StandardCredentials; 30 | import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; 31 | import edu.umd.cs.findbugs.annotations.NonNull; 32 | import hudson.Extension; 33 | import hudson.Util; 34 | import hudson.util.Secret; 35 | import org.kohsuke.stapler.DataBoundConstructor; 36 | 37 | /** 38 | * Developers: Weiyin He and John Snyder 39 | */ 40 | @NameWith(value=AppetizeCredentials.NameProvider.class) 41 | public class AppetizeCredentials extends BaseStandardCredentials { 42 | private static final long serialVersionUID = 1L; 43 | 44 | // Appetize.io API token 45 | private final Secret apiToken; 46 | 47 | @DataBoundConstructor 48 | public AppetizeCredentials(CredentialsScope scope, String id, String description, String apiToken) { 49 | super(scope, id, description); 50 | this.apiToken = Secret.fromString(apiToken); 51 | } 52 | 53 | public Secret getApiToken() { 54 | return apiToken; 55 | } 56 | 57 | public String toString() { 58 | String description = Util.fixEmptyAndTrim(getDescription()); 59 | return "API Token" + (description != null ? " (" + description + ")" : ""); 60 | } 61 | 62 | @Extension 63 | public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { 64 | @Override 65 | public String getDisplayName() { 66 | return "Appetize.io Credentials"; 67 | } 68 | } 69 | 70 | public static class NameProvider extends CredentialsNameProvider { 71 | @NonNull 72 | @Override 73 | public String getName(StandardCredentials appetizeCredentials) { 74 | return appetizeCredentials.toString(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.appetize; 26 | 27 | /** 28 | * Developers: Weiyin He and John Snyder 29 | */ 30 | public class AppetizeApp { 31 | private String platform; 32 | private String privateKey; 33 | private String publicKey; 34 | private String publicUrl; 35 | private String manageUrl; 36 | 37 | public AppetizeApp(String platform, String privateKey, String publicKey, String publicUrl, String manageUrl) { 38 | this.platform = platform; 39 | this.privateKey = privateKey; 40 | this.publicKey = publicKey; 41 | this.publicUrl = publicUrl; 42 | this.manageUrl = manageUrl; 43 | } 44 | 45 | public String getEmbedHtml() { 46 | String device = null; 47 | String width = null; 48 | String height = null; 49 | 50 | if (platform.equalsIgnoreCase("ios")) { 51 | device = "iphone"; 52 | width = "284px"; 53 | height = "600px"; 54 | } 55 | else if (platform.equalsIgnoreCase("android")) { 56 | device = "nexus5"; 57 | width = "300px"; 58 | height = "597px"; 59 | } 60 | 61 | if (device != null && privateKey != null) { 62 | String sourceUrl = "https://appetize.io/embed/" + publicKey + 63 | "?device=" + device + "&scale=75&autoplay=false&orientation=portrait&deviceColor=black"; 64 | String iframe = String.format("", 65 | sourceUrl, width, height); 66 | return iframe; 67 | } else { 68 | return ""; 69 | } 70 | } 71 | 72 | public String getPlatform() { 73 | return platform; 74 | } 75 | 76 | public String getPrivateKey() { 77 | return privateKey; 78 | } 79 | 80 | public String getPublicKey() { 81 | return publicKey; 82 | } 83 | 84 | public String getPublicUrl() { 85 | return publicUrl; 86 | } 87 | 88 | public String getManageUrl() { 89 | return manageUrl; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeApiService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.appetize; 26 | 27 | import com.google.gson.Gson; 28 | 29 | import java.io.*; 30 | import java.net.HttpURLConnection; 31 | import java.net.Proxy; 32 | import java.net.URL; 33 | import hudson.ProxyConfiguration; 34 | import jenkins.model.Jenkins; 35 | 36 | /** 37 | * Developers: Weiyin He and John Snyder 38 | */ 39 | public class AppetizeApiService { 40 | private final String PRESIGN_URL = "https://api.appetize.io/v1/jenkins/presigned"; 41 | private final String UPDATE_URL = "https://api.appetize.io/v1/app/update"; 42 | 43 | private PrintStream logger; 44 | private Gson gson; 45 | 46 | public AppetizeApiService(PrintStream logger) { 47 | this.logger = logger; 48 | this.gson = new Gson(); 49 | } 50 | 51 | public static class AppetizePresignedUrls { 52 | public String iosUrl; 53 | public String androidUrl; 54 | } 55 | 56 | public static class AppetizeUpdateParams { 57 | public String url; 58 | public String platform; 59 | public String token; 60 | public String privateKey; 61 | public String source; 62 | public String jenkinsUUID; 63 | public String jobUUID; 64 | public Integer buildNumber; 65 | } 66 | 67 | public static class AppetizeUpdateResult { 68 | public String publicKey; 69 | public String privateKey; 70 | public String publicURL; 71 | public String appURL; 72 | public String manageURL; 73 | } 74 | 75 | public AppetizePresignedUrls getPresignedUrls() { 76 | HttpURLConnection connection = null; 77 | try { 78 | URL url = new URL(PRESIGN_URL); 79 | connection = getConnection(url); 80 | connection.setRequestMethod("GET"); 81 | connection.connect(); 82 | int status = connection.getResponseCode(); 83 | 84 | if (status >= 200 && status <= 299) { 85 | InputStreamReader reader = new InputStreamReader(connection.getInputStream()); 86 | return gson.fromJson(reader, AppetizePresignedUrls.class); 87 | } else { 88 | String errorMessage = readToString(connection.getErrorStream()); 89 | throw new Exception("Status " + status + ": " + errorMessage); 90 | } 91 | } catch (Exception e) { 92 | println("Error getting Appetize.io upload URLs"); 93 | println(e.getMessage()); 94 | return null; 95 | } finally { 96 | if (connection != null) { 97 | connection.disconnect(); 98 | } 99 | } 100 | } 101 | 102 | public boolean uploadData(InputStream in, String urlString) { 103 | HttpURLConnection connection = null; 104 | try { 105 | URL url = new URL(urlString); 106 | connection = getConnection(url); 107 | connection.setDoOutput(true); 108 | connection.setRequestMethod("PUT"); 109 | 110 | OutputStream out = connection.getOutputStream(); 111 | copy(in, out); 112 | in.close(); 113 | out.close(); 114 | 115 | int status = connection.getResponseCode(); 116 | if (status >= 200 && status <= 299) { 117 | return true; 118 | } else { 119 | throw new Exception("Status " + status); 120 | } 121 | } catch (Exception e) { 122 | println("Error uploading to " + urlString); 123 | println(e.getMessage()); 124 | return false; 125 | } finally { 126 | if (connection != null) connection.disconnect(); 127 | } 128 | } 129 | 130 | public AppetizeUpdateResult updateApp(AppetizeUpdateParams params) { 131 | 132 | HttpURLConnection connection = null; 133 | try { 134 | URL url = new URL(UPDATE_URL); 135 | connection = getConnection(url); 136 | connection.setRequestMethod("POST"); 137 | connection.setRequestProperty("Content-Type", "application/json"); 138 | connection.setDoOutput(true); 139 | 140 | // upload json 141 | String json = gson.toJson(params); 142 | OutputStream out = connection.getOutputStream(); 143 | OutputStreamWriter writer = new OutputStreamWriter(out); 144 | writer.write(json); 145 | writer.flush(); 146 | writer.close(); 147 | 148 | // get response 149 | int status = connection.getResponseCode(); 150 | if (status >= 200 && status <= 299) { 151 | InputStreamReader reader = new InputStreamReader(connection.getInputStream()); 152 | return gson.fromJson(reader, AppetizeUpdateResult.class); 153 | } else { 154 | String errorMessage = readToString(connection.getErrorStream()); 155 | throw new Exception("Status " + status + ": " + errorMessage); 156 | } 157 | } catch (Exception e) { 158 | println("Error calling Appetize.io API"); 159 | println(e.getMessage()); 160 | return null; 161 | } finally { 162 | if (connection != null) { 163 | connection.disconnect(); 164 | } 165 | } 166 | } 167 | 168 | void println(String message) { 169 | if (this.logger != null) logger.println(message); 170 | } 171 | 172 | String readToString(InputStream is) { 173 | if (is == null) return null; 174 | 175 | InputStreamReader reader = new InputStreamReader(is); 176 | StringWriter writer = new StringWriter(); 177 | char[] buf = new char[1024]; 178 | try { 179 | while (reader.read(buf) > 0) { 180 | writer.write(buf); 181 | } 182 | is.close(); 183 | 184 | return writer.toString(); 185 | } catch (IOException e) { 186 | return null; 187 | } 188 | } 189 | 190 | private static void copy(InputStream in, OutputStream out) throws IOException{ 191 | byte[] buf = new byte[1024 * 10]; 192 | int len; 193 | while ((len = in.read(buf)) > 0) { 194 | out.write(buf, 0, len); 195 | } 196 | } 197 | 198 | /** 199 | * Returns an HttpURLConnection using the Jenkins global proxy if set 200 | * @param url URL to open the connection 201 | * @return Instanciated connection 202 | * @throws IOException 203 | */ 204 | private HttpURLConnection getConnection(URL url) throws IOException { 205 | 206 | HttpURLConnection connection; 207 | ProxyConfiguration proxyConfig = Jenkins.getInstance().proxy; 208 | if (proxyConfig != null) { 209 | Proxy proxy = proxyConfig.createProxy(url.getHost()); 210 | if (proxy != null && proxy.type() == Proxy.Type.HTTP) { 211 | connection = (HttpURLConnection)url.openConnection(proxy); 212 | } 213 | else { 214 | connection = (HttpURLConnection)url.openConnection(); 215 | } 216 | } 217 | else { 218 | connection = (HttpURLConnection)url.openConnection(); 219 | } 220 | 221 | return connection; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/appetize/AppetizeRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 Appetize.io 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.appetize; 26 | 27 | import com.cloudbees.plugins.credentials.CredentialsProvider; 28 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 29 | import hudson.Extension; 30 | import hudson.FilePath; 31 | import hudson.Launcher; 32 | import hudson.model.*; 33 | import hudson.security.ACL; 34 | import hudson.tasks.BuildStepDescriptor; 35 | import hudson.tasks.BuildStepMonitor; 36 | import hudson.tasks.Publisher; 37 | import hudson.tasks.Recorder; 38 | import hudson.util.ListBoxModel; 39 | import org.kohsuke.stapler.DataBoundConstructor; 40 | 41 | import java.io.File; 42 | import java.io.IOException; 43 | import java.io.PrintStream; 44 | import java.security.MessageDigest; 45 | import java.security.NoSuchAlgorithmException; 46 | import java.util.*; 47 | 48 | /** 49 | * Developers: Weiyin He and John Snyder 50 | */ 51 | public class AppetizeRecorder extends Recorder { 52 | // For production use, please request an API token. This token is for temporary use when 53 | // first setting up projects. 54 | private static final String PLACEHOLDER_API_TOKEN = "tok_7vkmr5quwwjjxy4rv1q1h0rn08"; 55 | private static final String PLACEHOLDER_ID = "placeholder"; 56 | 57 | private final String platform; 58 | private final String appPath; 59 | private final String apiTokenId; 60 | 61 | @DataBoundConstructor 62 | public AppetizeRecorder(String platform, String appPath, String apiTokenId) { 63 | this.platform = platform; 64 | this.appPath = appPath; 65 | this.apiTokenId = apiTokenId; 66 | } 67 | 68 | public String getPlatform() { 69 | return platform; 70 | } 71 | 72 | public String getApiTokenId() { 73 | return apiTokenId; 74 | } 75 | 76 | public String getAppPath() { 77 | return appPath; 78 | } 79 | 80 | @Override 81 | public DescriptorImpl getDescriptor() { 82 | return (DescriptorImpl)super.getDescriptor(); 83 | } 84 | 85 | @Override 86 | public BuildStepMonitor getRequiredMonitorService() { 87 | return BuildStepMonitor.NONE; 88 | } 89 | 90 | @Override 91 | public Collection getProjectActions(AbstractProject project) { 92 | ArrayList collection = new ArrayList(1); 93 | AppetizeProjectAction action = new AppetizeProjectAction(project); 94 | if (action != null) collection.add(action); 95 | return collection; 96 | } 97 | 98 | @Override 99 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { 100 | // only run on SUCCESS 101 | if (!build.getResult().isBetterOrEqualTo(Result.SUCCESS)) return false; 102 | 103 | PrintStream logger = listener.getLogger(); 104 | 105 | // check platform 106 | if (!platform.equalsIgnoreCase("ios") && !platform.equalsIgnoreCase("android")) { 107 | logger.println("Error: Invalid platform " + platform); 108 | return false; 109 | } 110 | 111 | // check appPath exists 112 | if (appPath == null || appPath.isEmpty()) { 113 | logger.println("Error: Empty appPath"); 114 | return false; 115 | } 116 | FilePath appLocation = new FilePath(build.getWorkspace(), appPath); 117 | if ((platform.equalsIgnoreCase("ios") && !appLocation.isDirectory()) || 118 | (platform.equalsIgnoreCase("android") && !appLocation.exists())) { 119 | logger.println("Error: could not find app in " + appLocation.getRemote()); 120 | return false; 121 | } 122 | 123 | // get api token 124 | String apiToken = null; 125 | if (apiTokenId == null || apiTokenId.isEmpty() || apiTokenId.equals(PLACEHOLDER_ID)) { 126 | apiToken = PLACEHOLDER_API_TOKEN; 127 | } else { 128 | List credentialsList = CredentialsProvider.lookupCredentials( 129 | AppetizeCredentials.class, (Item)null, 130 | ACL.SYSTEM, Collections.emptyList()); 131 | for(AppetizeCredentials credentials : credentialsList) { 132 | if (apiTokenId.equals(credentials.getId())) { 133 | apiToken = credentials.getApiToken().getPlainText(); 134 | break; 135 | } 136 | } 137 | 138 | if (apiToken == null) { 139 | logger.println("Error looking up appetize.io credentials. Please reconfigure the appetize.io post-build action"); 140 | return false; 141 | } 142 | } 143 | 144 | // get pre-signed url 145 | AppetizeApiService appetize = new AppetizeApiService(logger); 146 | AppetizeApiService.AppetizePresignedUrls urls = appetize.getPresignedUrls(); 147 | if (urls == null) { 148 | logger.println("Error getting appetize.io upload URL"); 149 | return false; 150 | } 151 | 152 | // prepare upload 153 | FilePath uploadFile; 154 | File zipFile = null; 155 | String uploadUrl = null; 156 | if (platform.equalsIgnoreCase("ios")) { 157 | try { 158 | zipFile = File.createTempFile("appetize", ".zip"); 159 | uploadFile = new FilePath(zipFile); 160 | appLocation.zip(uploadFile); 161 | } catch (Exception e) { 162 | logger.println("Error creating zip file in " + zipFile.toString()); 163 | return false; 164 | } 165 | uploadUrl = urls.iosUrl; 166 | } 167 | else { 168 | uploadFile = appLocation; 169 | uploadUrl = urls.androidUrl; 170 | } 171 | 172 | // upload file 173 | if (!appetize.uploadData(uploadFile.read(), uploadUrl)) { 174 | return false; 175 | } 176 | 177 | String jobUUID; 178 | try { 179 | String projectName = build.getProject().getName(); 180 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); 181 | String jenkinsUUID = getDescriptor().getJenkinsUUID(); 182 | if (jenkinsUUID != null) digest.update(jenkinsUUID.getBytes()); 183 | if (projectName != null) digest.update(projectName.getBytes()); 184 | if (jenkinsUUID != null) digest.update(jenkinsUUID.getBytes()); 185 | 186 | byte[] hash = digest.digest(); 187 | StringBuffer hexString = new StringBuffer(); 188 | for (int i = 0; i < hash.length; i++) { 189 | String hex = Integer.toHexString(0xff & hash[i]); 190 | if(hex.length() == 1) hexString.append('0'); 191 | hexString.append(hex); 192 | } 193 | jobUUID = hexString.toString(); 194 | } catch (NoSuchAlgorithmException e) { 195 | return false; 196 | } 197 | 198 | // hit api to create app 199 | AppetizeApiService.AppetizeUpdateParams params = new AppetizeApiService.AppetizeUpdateParams(); 200 | params.url = uploadUrl; 201 | params.platform = platform; 202 | params.token = apiToken; 203 | params.source = "appetize-jenkins-plugin"; 204 | params.jenkinsUUID = getDescriptor().getJenkinsUUID(); 205 | params.jobUUID = jobUUID; 206 | params.buildNumber = build.getNumber(); 207 | AppetizeApiService.AppetizeUpdateResult result = appetize.updateApp(params); 208 | if (result == null) { 209 | logger.println("Error calling Appetize.io API"); 210 | return false; 211 | } 212 | 213 | logger.println("Success uploading to Appetize.io"); 214 | logger.println("You can view your app at " + result.publicURL); 215 | logger.println("You can manage your app at " + result.manageURL); 216 | 217 | // add action to build 218 | build.addAction(new AppetizeBuildAction(platform, result.privateKey, result.publicKey, 219 | result.publicURL, result.manageURL, build.getNumber())); 220 | 221 | return true; 222 | } 223 | 224 | @Extension 225 | public static class DescriptorImpl extends BuildStepDescriptor { 226 | private String jenkinsUUID; 227 | 228 | public DescriptorImpl() { 229 | load(); 230 | if (jenkinsUUID == null || jenkinsUUID.isEmpty()) { 231 | jenkinsUUID = UUID.randomUUID().toString(); 232 | save(); 233 | } 234 | } 235 | 236 | @Override 237 | public boolean isApplicable(Class aClass) { 238 | return true; 239 | } 240 | 241 | @Override 242 | public String getDisplayName() { 243 | return "Upload to Appetize.io"; 244 | } 245 | 246 | public String getJenkinsUUID() { 247 | return jenkinsUUID; 248 | } 249 | 250 | public ListBoxModel doFillApiTokenIdItems() { 251 | ListBoxModel items = new ListBoxModel(); 252 | items.add("Placeholder API Token", PLACEHOLDER_ID); 253 | 254 | List credentialsList = CredentialsProvider.lookupCredentials( 255 | AppetizeCredentials.class, (Item)null, 256 | ACL.SYSTEM, Collections.emptyList()); 257 | for (AppetizeCredentials credentials : credentialsList) { 258 | items.add(credentials.toString(), credentials.getId()); 259 | } 260 | 261 | return items; 262 | } 263 | } 264 | } 265 | --------------------------------------------------------------------------------