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..feef9534 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,35 @@ -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. We use Maven for building and managing our project. -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. +We assume here that you have a standard Linux machine (eg with Ubuntu), with Java and Maven installed. -We assume here that you have a standard Linux machine (eg with Ubuntu), with Java installed. -We first checkout this repository: -``` -git clone https://github.com/monperrus/smallest-java-ci -cd smallest-java-ci -``` +## How to run: +After checking out the repository, build it in the root directory using the following command: -We then download the required dependencies: ``` -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 +mvn package ``` -We compile the skeleton the continuous integration server: +Then start the server on your local machine: ``` -javac -cp servlet-api-2.5.jar:jetty-all-$JETTY_VERSION.jar ContinuousIntegrationServer.java +java -jar target/gs-maven-0.1.0.jar ``` -We run the server on the machine, and we may make it visible on the Internet thanks to [Ngrok](https://ngrok.com/): +The serverr is visible on the Internet by using [Ngrok](https://ngrok.com/). The public url can be found by running the following commnand in a second terminal window: ``` -# 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 +39,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 +48,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..66aea7bb --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + + 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.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..fa28fabc --- /dev/null +++ b/src/main/java/server/ContinuousIntegrationServer.java @@ -0,0 +1,210 @@ +package server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; + +import java.io.*; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import org.json.*; + +/** + 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); + + 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); + + String URL = getRepoURL(JSON); + + String cloneOK = cloneRepo(URL); + + String buildOK = "build not done"; + String notifyOK = "notification not sent"; + + if(cloneOK.contains("Cloning OK")){ + buildOK = buildAndTest("./cloned-repo"); + } + + if(buildOK.contains("Build OK")){ + notifyOK = notify(buildOK); + } + + 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 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 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; + } + + /** + * 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 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 String buildAndTest(String path) { + //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(); + p.waitFor(); + InputStream fis = p.getInputStream(); + InputStreamReader isr = new InputStreamReader(fis); + BufferedReader fg = new BufferedReader(isr); + String line = null; + while ((line = fg.readLine()) != null) { + 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; + + } + + public String notify(String status){ + //sends notification of the build to the webhook + System.out.println("Notifying GitHub of build status"); + String notificationStatus = "Notification sent successfully"; + return notificationStatus; + } + + // public void write_payload_to_json(JSONObject input, String file_name) { + // //this function writes a JSONObject to a specified json-file + // try (FileWriter file = new FileWriter(file_name)) { + // com.alibaba.fastjson.JSONWriter WT = new JSONWriter(file); + // WT.writeObject(input); + // } catch (IOException e) { + // e.printStackTrace(); + // } + // } + + /** + * 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(); + } +} \ No newline at end of file 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..d56e5aa9 --- /dev/null +++ b/src/test/java/server/TestServer.java @@ -0,0 +1,73 @@ +package server; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.*; +import org.json.*; + +// 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"); + //assertEquals(getJSON(reader),json_true); + + + //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"); + //assertNotEquals(getJSON(reader_1),json_true); + } + + + @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/22"); + String true_return = "tree/issue/22" +""+"git://github.com/DD2480-Group-15/ci-server"; + //assertEquals(getRepoURL(json_true), 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"; + //assertNotEquals(getRepoURL(json_false), false_return); + } + + + + + + @Test + public void dummyTest2() { + ContinuousIntegrationServer server = new ContinuousIntegrationServer(); + int a = server.dummyFunction(); + assertEquals(1,a); + } +}