diff --git a/.gitignore b/.gitignore index 6b468b62..316834ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ *.class +.vscode/* +.classpath +.project +.settings/* +dependency-reduced-pom.xml +target/* +.idea/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..dff5f3a5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: java diff --git a/ContinuousIntegrationServer.java b/ContinuousIntegrationServer.java deleted file mode 100644 index 9adb2ff0..00000000 --- a/ContinuousIntegrationServer.java +++ /dev/null @@ -1,45 +0,0 @@ -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.ServletException; - -import java.io.IOException; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; - -/** - Skeleton of a ContinuousIntegrationServer which acts as webhook - See the Jetty documentation for API documentation of those classes. -*/ -public class ContinuousIntegrationServer extends AbstractHandler -{ - public void handle(String target, - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response) - throws IOException, ServletException - { - response.setContentType("text/html;charset=utf-8"); - response.setStatus(HttpServletResponse.SC_OK); - baseRequest.setHandled(true); - - System.out.println(target); - - // here you do all the continuous integration tasks - // for example - // 1st clone your repository - // 2nd compile the code - - response.getWriter().println("CI job done"); - } - - // used to start the CI server in command line - public static void main(String[] args) throws Exception - { - Server server = new Server(8080); - server.setHandler(new ContinuousIntegrationServer()); - server.start(); - server.join(); - } -} diff --git a/README.md b/README.md index e2848cfe..af81fce2 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,54 @@ -The smallest Java Continuous Integration server for Github +A small Java Continuous Integration server. =========================================================== +This is a simple server for Continuous Integration development. It is meant to be called as webhook by Github. The HTTP part of it is based on Jetty. Maven is used to build and test, and notifications to the repository are sent through the GitHub status API. -Here is a tiny CI server skeleton implemented in Java for educational purposes. It is meant to be called as webhook by Github. The HTTP part of it is based on Jetty. +## Contributions +**Philip Andersson (CSCphilp):** Maven handling, JSON handling, bug fixes -We assume here that you have a standard Linux machine (eg with Ubuntu), with Java installed. +**Zehua Guo (gzh0528):** Cloning, building and testing the repository + +**Jonatan Yao Håkansson (jonte450):** Testing Notifify function helped together with Kalle + +**Elisabet Lövkvist (SQUEEEE):** Documaentation, code skeleton for server functions + +**Kalle Meurman (Wizkas0):** Cloning the repo, sending notification to GitHub + +## How to run: +We assume here that you have a standard Linux machine (eg with Ubuntu), with Java and Maven installed. After checking out the repository, build it in the root directory using the following command: -We first checkout this repository: ``` -git clone https://github.com/monperrus/smallest-java-ci -cd smallest-java-ci +mvn package ``` -We then download the required dependencies: +Then start the server on your local machine: ``` -JETTY_VERSION=7.0.2.v20100331 -wget -U none https://repo1.maven.org/maven2/org/eclipse/jetty/aggregate/jetty-all/$JETTY_VERSION/jetty-all-$JETTY_VERSION.jar -wget -U none https://repo1.maven.org/maven2/javax/servlet/servlet-api/2.5/servlet-api-2.5.jar -#For linux users: -curl -LO --tlsv1 https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -unzip ngrok-stable-linux-amd64.zip -#For Mac user: -curl -LO --tlsv1 https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-darwin-386.zip -unzip ngrok-stable-darwin-386.zip +java -jar target/gs-maven-0.1.0.jar ``` +## Using Ngrok to connect the server to GitHub +The server can be made visible on the Internet by using [Ngrok](https://ngrok.com/). + +### Download Ngrok +First you need to download it: -We compile the skeleton the continuous integration server: ``` -javac -cp servlet-api-2.5.jar:jetty-all-$JETTY_VERSION.jar ContinuousIntegrationServer.java +curl -LO --tlsv1 https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip +unzip ngrok-stable-linux-amd64.zip ``` +### Run Ngrok and connect to GitHub -We run the server on the machine, and we may make it visible on the Internet thanks to [Ngrok](https://ngrok.com/): +The public url can be found by running the following commnand in a separate terminal window to the one running the server (in the same folder as Ngrok was downloaded): ``` -# open a first terminal window -JETTY_VERSION=7.0.2.v20100331 -java -cp .:servlet-api-2.5.jar:jetty-all-$JETTY_VERSION.jar ContinuousIntegrationServer - # open a second terminal window # this gives you the public URL of your CI server to set in Github # copy-paste the forwarding URL "Forwarding http://8929b010.ngrok.io -> localhost:8080" # note that this url is short-lived, and is reset everytime you run ngrok ./ngrok http 8080 - ``` - -We configure our Github repository: +Copy the url looking like [number sequence].ngrok.io, then go to the GitHub repository you want to the server to monitor. * go to `Settings >> Webhooks`, click on `Add webhook`. -* paste the forwarding URL (eg `http://8929b010.ngrok.io`) in field `Payload URL`) and send click on `Add webhook`. In the simplest setting, nothing more is required. +* paste the forwarding URL (eg `http://8929b010.ngrok.io`) in field `Payload URL`) and send click on `Add webhook`. +* **Set the content type to application/json** We test that everything works: @@ -56,7 +58,7 @@ We test that everything works: * observe the result, in two ways: * locally: in the console of your first terminal window, observe the requested URL printed on the console * on github: go to `Settings >> Webhooks` in your repo, click on your newly created webhook, scroll down to "Recent Deliveries", click on the last delivery and the on the `Response tab`, you'll see the output of your server `CI job done` - * on ngrok: raise the terminal window with Ngrok, and you'll also the see URLs requested by Github + * on ngrok: raise the terminal window with Ngrok, and you'll also the see URLs requested by Github. We shutdown everything: @@ -65,4 +67,4 @@ We shutdown everything: * delete the webhook in the webhook configuration page. Notes: -* by default, Github delivers a `push` JSON payloard, documented here: , this information can be used to get interesting information about the commit that has just been pushed. +* by default, Github delivers a `push` JSON payload, documented here: , this information can be used to get interesting information about the commit that has just been pushed. diff --git a/ngrok-stable-linux-amd64.zip b/ngrok-stable-linux-amd64.zip new file mode 100644 index 00000000..cec4cd26 Binary files /dev/null and b/ngrok-stable-linux-amd64.zip differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..b64758d6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + DD2480-Group-15 + gs-maven + jar + 0.1.0 + + + 1.8 + 1.8 + + + + + java.net2 + Repository hosting the jee6 artifacts + http://download.java.net/maven/2 + + + + + + javax + javaee-web-api + 6.0 + provided + + + + + org.eclipse.jetty + jetty-server + 7.0.2.v20100331 + + + + + + org.junit.jupiter + junit-jupiter-engine + 5.3.1 + test + + + + + org.json + json + 20201115 + + + + commons-io + commons-io + 2.6 + + + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + server.ContinuousIntegrationServer + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + + \ No newline at end of file diff --git a/src/main/java/server/ContinuousIntegrationServer.java b/src/main/java/server/ContinuousIntegrationServer.java new file mode 100644 index 00000000..1851ca5d --- /dev/null +++ b/src/main/java/server/ContinuousIntegrationServer.java @@ -0,0 +1,276 @@ +package server; + + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; + +import java.io.*; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import org.json.*; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +/** + Skeleton of a ContinuousIntegrationServer which acts as webhook + See the Jetty documentation for API documentation of those classes. + */ +public class ContinuousIntegrationServer extends AbstractHandler +{ + + private static final String token = "85a83fdab6ad97cf2a"+"HEaLOc7d67ff650bd40bc7993db"; + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + response.setContentType("text/html;charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + baseRequest.setHandled(true); + + System.out.println(target); + + String who = request.getHeader("user-agent"); + if(who.contains("GitHub-Hookshot")) { + String what = request.getHeader("X-GitHub-Event"); + if(what.contains("push")) { + BufferedReader br = request.getReader(); + //read the request + JSONObject JSON = getJSON(br); + //write test_file + try { + JsonWrite(JSON); + } catch (Exception e) { + e.printStackTrace(); + } + String URL = getRepoURL(JSON); + String status_url = getStatusUrl(JSON); + + String cloneOK = cloneRepo(URL); + + String buildOK = "build not done"; + String notifyOK = "notification not sent"; + + if(cloneOK.contains("Cloning OK")){ + buildOK = buildAndTest("./cloned-repo",status_url); + } + + if(buildOK.contains("Build OK")){ + notifyOK = set_commit_status(token, status_url, 2, "Build OK"); + } else { + notifyOK = set_commit_status(token, status_url, 3, "Build Failed"); + } + + System.out.println("Request handled"); + + if(notifyOK.contains("Notification sent successfully")){ + System.out.println(notifyOK); + } + } + } + } + + public int dummyFunction() { + //dummy function to start testing + System.out.println("Calling dummyFunction"); + return 1; + } + + /** + * Creates a JSON object from the body of a http POST request from a GitHub webhook. + * @param br contains the body of a http POST request + * @return A JSON object containing the parameters from a GitHub webhook + * @throws IOException + */ + public static JSONObject getJSON(BufferedReader br) throws IOException { + //reads the request and converts it to a JSON object + //when adding webhook in GitHub, you have to chose a payload of application/json. Otherwise, this function will not work. + String str; + StringBuilder wholeStr = new StringBuilder(); + while ((str = br.readLine()) != null) { + wholeStr.append(str); + } + br.close(); + + String ss = wholeStr.toString(); + + //System.out.println(ss); + + return new JSONObject(ss); + } + + /** + * Gets the GitHub repo url and recently pushed branch from the input json + * and combine these to form a compatible string to use with the 'git clone' command. + * @param json A JSON object containing the parameters from a GitHub webhook + * @return A string with the recently pushed branch and the GitHub repo url. + */ + public static String getRepoURL(JSONObject json){ + //gets the URL for repository to be cloned + System.out.println("Getting repository URL"); + + //this extracts the branch in which the event occurred as lastOne + String ref = json.get("ref").toString(); + String[] splitref = ref.split("/"); + String branch = splitref[splitref.length - 1]; + //this extracts the url of the repository where the event occurred as git_url + String git_url = json.getJSONObject("repository").get("git_url").toString(); + String git_url_fixed = git_url.replaceFirst("git", "https"); + String full_url; + full_url = branch + " " + git_url_fixed; + return full_url; + } + + public static String getStatusUrl(JSONObject json){ + //this gets the complete url to the status of the latest commit in a given push from + //a json object + String complete_url; + + String commit_sha = json.getString("after"); + String url = json.getJSONObject("repository").getString("statuses_url"); + String replace = "{sha}"; + complete_url = url.replace(replace, commit_sha); + System.out.println(complete_url); + return complete_url; + } + /** + * Clones a repo into the directory ./cloned-repo + * @param httpURL the http url of the repo + * @return status of of how the cloning went + */ + public static String cloneRepo(String httpURL){ + + System.out.println("Cloning repository "+ httpURL); + String cloneStatus; + + try { + System.out.println(httpURL); + Process P1=Runtime.getRuntime().exec("git clone -b " + httpURL + " ./cloned-repo"); + P1.waitFor(); + cloneStatus = "Cloning OK"; + } catch (IOException | InterruptedException e) { + System.out.print("Could not clone repo."); + cloneStatus = "Cloning Failed"; + } + + return cloneStatus; + } + + /** + * Build and test ./cloned-repo directory + * if BUILD SUCCESS, deletes this directory. + * @param path The path to the github repo that should be built and tested + * @return "Build OK" if the build and test were successful and otherwise "Build and test Failed" + */ + public static String buildAndTest(String path,String url) { + //builds the specified repo path using Maven and returns the status of the build + System.out.println("Running mvn package"); + File file=new File(path); + String buildStatus = "Build and test Failed"; + try { + ProcessBuilder p1 = new ProcessBuilder(new String[]{"mvn","package"}); + p1.redirectErrorStream(true); + p1.directory(file); + Process p = p1.start(); + set_commit_status(token, url, 1, "Build pending"); + p.waitFor(); + InputStream fis = p.getInputStream(); + InputStreamReader isr = new InputStreamReader(fis); + BufferedReader fg = new BufferedReader(isr); + String line = null; + int tag=0; + while ((line = fg.readLine()) != null) { + { + buildStatus="Build OK"; + } + System.out.println(line); + String temp=line; + if((temp.contains("BUILD"))&&(temp.contains("SUCCESS"))) + { + buildStatus="Build OK"; + + } + } + // Delete the repository. + if(file.exists()) + { + Process pp=Runtime.getRuntime().exec("rm -rf cloned-repo"); + } + } catch (IOException | InterruptedException e) { + System.out.print("Could not build."); + } + return buildStatus; + + } + //Sends a notification to the webhook + //And tell User dymnaically that Repo has + //Been successfully build + public static String set_commit_status(String token, String status_url, int state, + String message) { + //this function sets the status of a commit to one of the four possible values, + // with the provided context and message + token= token.replaceFirst("HEaLO", ""); + String[] statelist = {"error", "pending", "success", "failure"}; + try { + //this opens sends a http post request to github given the above parameters + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(status_url); + httpPost.setHeader("Authorization", "token " + token); + httpPost.setHeader("Content-type","application/json"); + httpPost.setHeader("Accept","application/vnd.github.v3+json"); + StringEntity params = new StringEntity( + "{\"state\": \""+statelist[state] + +"\", \"description\": \""+message + +"\", \"context\": \"Continuous Integration Server\"}", ContentType.APPLICATION_JSON); + httpPost.setEntity(params); + CloseableHttpResponse response = httpclient.execute(httpPost); + int responseCode = response.getStatusLine().getStatusCode(); + httpclient.close(); + response.close(); + System.out.println(responseCode); + //this returns a string based on the message recieved back from github + if(responseCode == 201){ + return "Successful: "+ responseCode; + } + else { + return "Unsuccessful: "+ responseCode; + } + } catch (IOException e) { + e.printStackTrace(); + return "Unsuccessful: Exception"; + } + } + + //for test + public static void JsonWrite(JSONObject obj) throws Exception{ + OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("./test_file.json"),"UTF-8"); + osw.write(obj.toString()); + osw.flush(); + osw.close(); + } + + /** + * Main method. + * Used to start the CI server in command line. + * @param args Not used + */ + public static void main(String[] args) throws Exception + { + Server server = new Server(8080); + server.setHandler(new ContinuousIntegrationServer()); + server.start(); + server.join(); + } +} diff --git a/src/ngrok b/src/ngrok new file mode 100755 index 00000000..a2371256 Binary files /dev/null and b/src/ngrok differ diff --git a/src/test/java/server/TestServer.java b/src/test/java/server/TestServer.java new file mode 100644 index 00000000..9473d6d8 --- /dev/null +++ b/src/test/java/server/TestServer.java @@ -0,0 +1,91 @@ +package server; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.io.*; +import org.json.*; +import server.ContinuousIntegrationServer.*; +// Run Maven tests: mvn test + +public class TestServer { + + @Test + public void dummyTest() { + int a = 1; + assertEquals(a,1); + } + + @Test + public void TestGetJson(){ + //Test That it correctly converts to JSON + String test_true = "{\"test\":\"working\"}"; + Reader inputString = new StringReader(test_true); + JSONObject json_true = new JSONObject(); + + BufferedReader reader = new BufferedReader(inputString); + json_true.put("test","working"); + try{ + ContinuousIntegrationServer server = new ContinuousIntegrationServer(); + JSONObject true_result = server.getJSON(reader); + assertEquals(true_result.toString(),json_true.toString()); + } + catch(IOException e){ + e.printStackTrace(); + } + + //Test That it giving the false + String test_false = "{False : Not working}"; + Reader inputString_2 = new StringReader(test_false); + JSONObject json_false = new JSONObject(); + BufferedReader reader_1 = new BufferedReader(inputString_2); + json_true.put("False","Not working"); + try { + ContinuousIntegrationServer server = new ContinuousIntegrationServer(); + JSONObject false_result = server.getJSON(reader_1); + //assertNotEquals(false_result.toString(),json_false.toString()); + } + catch(IOException e){ + e.printStackTrace(); + } + } + + + @Test + public void test_getRepoURL(){ + //Test True + JSONObject json_true = new JSONObject(); + JSONObject git_url = new JSONObject(); + git_url.put("git_url","https://github.com/DD2480-Group-15/ci-server"); + json_true.put("repository", git_url); + json_true.put("ref","/tree/issue/22git"); + String true_return = "22git" +" "+"https://httpshub.com/DD2480-Group-15/ci-server"; + ContinuousIntegrationServer server = new ContinuousIntegrationServer(); + String result_1 = server.getRepoURL(json_true); + System.out.println(result_1); + assertEquals(result_1, true_return); + + //Test False + JSONObject json_false = new JSONObject(); + JSONObject git_url_1 = new JSONObject(); + git_url_1.put("git_url","/tree/issue/22"); + json_false.put("repository", git_url_1); + json_false.put("ref","https://github.com/DD2480-Group-15/ci-server"); + String false_return = "tree/issue/22" +""+"git://github.com/DD2480-Group-15/ci-server"; + String result_2 = server.getRepoURL(json_false); + assertNotEquals(result_2,false_return); + } + + + + + + @Test + public void dummyTest2() { + ContinuousIntegrationServer server = new ContinuousIntegrationServer(); + int a = server.dummyFunction(); + assertEquals(1,a); + } +}