diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/README.md b/README.md index deb741c..4320b0b 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,35 @@ At line:1 char:1 +### Compilation (P1) + +#### Implementation + +When the CI server receives a GitHub push webhook on `/webhook`, it triggers compilation of the pushed branch. The `Compiler` class in `se.ciserver.build` performs the following steps: + +1. Creates a temporary directory +2. Clones the specific branch using `git clone --branch --single-branch ` +3. Checks out the exact commit SHA with `git checkout ` +4. Runs `mvn clean compile` in the cloned project +5. Captures and prints the build output to the server console +6. Cleans up the temporary directory + +The compilation result (success/failure) is returned in the HTTP response and printed to the server console. + +#### Unit testing + +Compilation is unit-tested in `src/test/java/MainTest.java` with the following tests: + +- `compilationResultStoresSuccess()` — verifies that a successful `CompilationResult` stores `success=true` and the build output. +- `compilationResultStoresFailure()` — verifies that a failed `CompilationResult` stores `success=false` and the build output. +- `compilerHandlesCloneFailure()` — subclasses `Compiler` to override `createProcessBuilder()` with a failing command, verifying that a clone failure returns `success=false` without throwing an exception. +- `compilerReturnsSuccessWhenAllStepsPass()` — subclasses `Compiler` to override `createProcessBuilder()` with a succeeding command, verifying the full pipeline returns `success=true`. +- `ciServerHandleCompilationOnPush()` — starts a local Jetty server, sends a valid push payload to `/webhook`, and verifies the response is `200` and contains the compilation result with the commit SHA. + +To run the tests, see [Perform unit tests](#perform-unit-tests). + +--- + ## Perform unit tests 1. Build the project with **Maven** @@ -233,6 +262,33 @@ mvn clean compile ```bash mvn test ``` + +3. Verify all tests pass in the output + +```console +Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 + +BUILD SUCCESS +``` + +### Connect the server to a webhook with ngrok + +1. Run the server, see [Run the server](#run-the-server). + +2. In a separate terminal, start ngrok to expose port `8080`: + +```bash +ngrok http 8080 +``` + +3. Copy the forwarding URL (e.g. `https://xxxx.ngrok-free.app`) from the ngrok output. + +4. In your GitHub repository, go to **Settings > Webhooks > Add webhook** and set: + - **Payload URL**: `https://xxxx.ngrok-free.app/webhook` + - **Content type**: `application/json` + - **Events**: Select "Just the push event" + +5. Push a commit to the repository and observe the compilation output in the server console. --- ## Automated Test Execution via Github Push Events @@ -284,5 +340,7 @@ To avoid using real Git and Maven commands during unit testing, the `TestRunner` | Name | Contribution | |----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Josefine "joss2002" Nyholm |
  • [x] Added initial **Maven** file-tree structure, including a basic `pom.xml` with **JDK 17** support.
  • [x] Added skeleton for CI-server in `src/main/java/se/ciserver/ContinuousIntegrationServer.java`.
    • Added related dependencies, plugins; `jetty-server`, `exec-maven-plugin`
    • [x] Added additional documentation.
  • [x] Added **GitHub** push event `JSON` payload component.
    • [x] Added required Webhook payload object parameters and classes for `push` within `src/main/java/se/ciserver/github` including files; `PushParser.java`, `Push.java`, `Pusher.java`, `Commit.java`, `Author.java` and `Repository.java`.
    • [x] Added additional `Exception` extension in `InvalidPayloadException.java` specified for invalid payloads.
    • [x] Integrated the push parsing `JSON` payload functionality in the `ContinuousIntegrationServer.java` `handler()` to allow the CI-server to receive `JSON` payloads and present the relevant variables; `ref`, `after`, `repository.clone_url`, `pusher.name`, `head_commit.message`.
    • [x] Added unit tests in `src/test/java/MainTest.java` for `PushParser.java` push parsing `JSON` payload functionality and local testing of the `ContinuousIntegrationServer.java` CI-server handling of push parsing `JSON` payloads. This, as well as an additional file `src/main/java/se/ciserver/TestUtils.java` including test utilities such as reading filed. Supporting the unit tests a test `JSON` file `src/main/test/resources/githubPush.java` was added to represent a typical push event payload.
    • [x] Added additional dependencies in `pom.xml` for `jackson-databind` and `junit`
    • [x] Added additional documentation in `README.md`.
| +| Avid "HotFazz" Fayaz |
  • [x] Added webhook-triggered compilation (P1) in `src/main/java/se/ciserver/build/Compiler.java`.
    • [x] Clones the pushed branch, checks out the exact commit, and runs `mvn clean compile`.
    • [x] Added `CompilationResult.java` to hold build outcome and output.
    • [x] Integrated compilation into `ContinuousIntegrationServer.java` webhook handler.
    • [x] Added unit tests in `MainTest.java` for `CompilationResult` and `Compiler` failure handling.
    • [x] Added documentation in `README.md` for compilation implementation and unit testing.
| +| Albin "zzimbaa" Blomqvist |
  • [x] Implemented automated test execution triggered by GitHub push webhooks.
  • [x] Added `TestRunner.java` in `src/main/java/se/ciserver/` to handle CI test execution.
    • [x] Dynamically checks out the pushed branch using git checkout and updates it with git pull.
    • [x] Executes Maven test suite using `mvn test`.
    • [x] Captures test output via `ProcessBuilder` and write logs to both `terminal` and `HTTP response` indicating test success or failure.
    • [x] Determines build success/failure based on process exit code.
    • [x] Added unit test for `runTests` method to verify correct correct branch checkout and pull commands using command hook.
  • [x] Integrated the test execution logic into `ContinuousIntegrationServer.java` so that tests are triggered automatically upon receiving a GitHub push webhook event.
  • [x] Verified webhook-based test execution using `ngrok` for local tunneling and GitHub webhook deliveries.
  • [x] Added documentation in `README.md` describing how to trigger automated tests via GitHub push events.
| | Erik Olsson "erik-ol" |
  • [x] Added **GitHub** commit status setter component.
    • [x] Added `ContinuousIntegrationServer()` `startHttpClient()` and `setCommitStatus()` to `ContinuousIntegrationServer.java`
    • [x] Extended `ContinuousIntegrationServer.java` `handler()` to set commit status of recieved pushes.
    • [x] Added unit tests in `src/test/java/MainTest.java` for `setCommitStatus()` sending post request functionality and failing due to invalid url.
    • [x] Implemented github access token handling
    • [x] Added additional dependencies in `pom.xml` for `jetty-client`
    • [x] Added documentation for commit status implementation and testing in `README.md`.
| | Albin "zzimbaa" Blomqvist |
  • [x] Implemented automated test execution triggered by GitHub push webhooks.
  • [x] Added `TestRunner.java` in `src/main/java/se/ciserver/` to handle CI test execution.
    • [x] Dynamically checks out the pushed branch using git checkout and updates it with git pull.
    • [x] Executes Maven test suite using `mvn test`.
    • [x] Captures test output via `ProcessBuilder` and write logs to both `terminal` and `HTTP response` indicating test success or failure.
    • [x] Determines build success/failure based on process exit code.
    • [x] Added unit test for `runTests` method to verify correct correct branch checkout and pull commands using command hook.
  • [x] Integrated the test execution logic into `ContinuousIntegrationServer.java` so that tests are triggered automatically upon receiving a GitHub push webhook event.
  • [x] Verified webhook-based test execution using `ngrok` for local tunneling and GitHub webhook deliveries.
  • [x] Added documentation in `README.md` describing how to trigger automated tests through GitHub push events.
| diff --git a/src/main/java/se/ciserver/ContinuousIntegrationServer.java b/src/main/java/se/ciserver/ContinuousIntegrationServer.java index 4cbee9f..39606aa 100644 --- a/src/main/java/se/ciserver/ContinuousIntegrationServer.java +++ b/src/main/java/se/ciserver/ContinuousIntegrationServer.java @@ -19,6 +19,8 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import se.ciserver.build.Compiler; +import se.ciserver.build.CompilationResult; import se.ciserver.github.Push; import se.ciserver.github.PushParser; import se.ciserver.github.InvalidPayloadException; @@ -28,8 +30,12 @@ */ public class ContinuousIntegrationServer extends AbstractHandler { - private final PushParser parser = new PushParser(); + /** Flag to skip test execution during integration tests. */ public static boolean isIntegrationTest = false; + + private final PushParser parser = new PushParser(); + private final Compiler compiler = new Compiler(); + private HttpClient httpClient; private String accessToken; private String latestTestOutput = "No tests run yet."; @@ -69,18 +75,40 @@ public void handle(String target, { if ("/webhook".equals(target) && "POST".equalsIgnoreCase(request.getMethod())) { + // Read the full JSON payload from the request body String json = request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); try { + // Parse the GitHub push event payload into a Push object Push push = parser.parse(json); + // Log the push event details to the server console System.out.println("\nReceived push on branch : " + push.ref + "\nAfter SHA : " + push.after + "\nRepository URL : " + push.repository.clone_url + "\nPusher name : " + push.pusher.name + "\n\nHead commit message : " + push.head_commit.message); + // P1: Clone the pushed branch and run mvn clean compile + System.out.println("\nStarting compilation..."); + CompilationResult result = compiler.compile( + push.repository.clone_url, + push.ref, + push.after); + + // Log the compilation outcome to the server console + if (result.success) + { + System.out.println("\nCompilation SUCCEEDED"); + } + else + { + System.out.println("\nCompilation FAILED"); + } + + // Respond with 200 regardless of build outcome; + // the webhook delivery itself was successful response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println("Push received: " + push.after); @@ -112,6 +140,7 @@ public void handle(String target, } catch (InvalidPayloadException e) { + // Malformed or missing JSON fields response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().println("Invalid payload: " + e.getMessage()); } diff --git a/src/main/java/se/ciserver/build/CompilationResult.java b/src/main/java/se/ciserver/build/CompilationResult.java new file mode 100644 index 0000000..795f806 --- /dev/null +++ b/src/main/java/se/ciserver/build/CompilationResult.java @@ -0,0 +1,25 @@ +package se.ciserver.build; + +/** + * Holds the result of a compilation attempt. + */ +public class CompilationResult +{ + /** Whether the compilation succeeded (exit code 0). */ + public final boolean success; + + /** The combined stdout/stderr output from the build. */ + public final String output; + + /** + * Constructs a CompilationResult. + * + * @param success Whether the compilation succeeded + * @param output The combined stdout/stderr output from the build + */ + public CompilationResult(boolean success, String output) + { + this.success = success; + this.output = output; + } +} diff --git a/src/main/java/se/ciserver/build/Compiler.java b/src/main/java/se/ciserver/build/Compiler.java new file mode 100644 index 0000000..c8219a8 --- /dev/null +++ b/src/main/java/se/ciserver/build/Compiler.java @@ -0,0 +1,190 @@ +package se.ciserver.build; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Collectors; + +/** + * Handles cloning a repository, checking out a specific commit, + * and running Maven compilation. + */ +public class Compiler +{ + /** + * Clones the repository, checks out the specified commit, and + * runs {@code mvn clean compile}. + * + * @param cloneUrl The clone URL of the repository + * @param branch The branch name to clone + * @param commitSha The commit SHA to checkout + * + * @return A {@link CompilationResult} indicating success/failure and build output + */ + public CompilationResult compile(String cloneUrl, String branch, String commitSha) + { + Path tempDir = null; + + try + { + // Create an isolated temporary directory for this build + tempDir = Files.createTempDirectory("ci-build-"); + + // Step 1: Clone only the target branch (--single-branch avoids + // downloading the full repo history) + int cloneExit = runProcess(tempDir.getParent(), + "git", "clone", "--branch", branch, "--single-branch", + cloneUrl, tempDir.toString()); + + if (cloneExit != 0) + { + return new CompilationResult(false, + "Git clone failed with exit code " + cloneExit); + } + + // Step 2: Checkout the exact commit SHA that triggered the webhook + int checkoutExit = runProcess(tempDir, + "git", "checkout", commitSha); + + if (checkoutExit != 0) + { + return new CompilationResult(false, + "Git checkout failed with exit code " + checkoutExit); + } + + // Step 3: Run Maven compilation and return the result + return runCompilation(tempDir); + } + catch (IOException | InterruptedException e) + { + return new CompilationResult(false, + "Compilation error: " + e.getMessage()); + } + finally + { + // Always clean up the temporary directory to avoid disk bloat + if (tempDir != null) + { + cleanup(tempDir); + } + } + } + + /** + * Runs a process and returns its exit code. + * Output is printed to System.out for server console visibility. + * + * @param workDir The working directory for the process + * @param command The command and its arguments + * + * @return The process exit code + * + * @throws IOException If an I/O error occurs + * @throws InterruptedException If the process is interrupted + */ + private int runProcess(Path workDir, String... command) + throws IOException, InterruptedException + { + ProcessBuilder pb = createProcessBuilder(command); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); // Merge stderr into stdout + + Process process = pb.start(); + + // Consume output line-by-line and print to the server console + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) + { + String line; + while ((line = reader.readLine()) != null) + { + System.out.println(line); + } + } + + // Block until the process finishes and return its exit code + return process.waitFor(); + } + + /** + * Runs {@code mvn clean compile} in the given directory and captures output. + * + * @param workDir The directory containing the Maven project + * + * @return A {@link CompilationResult} with the build outcome + * + * @throws IOException If an I/O error occurs + * @throws InterruptedException If the process is interrupted + */ + private CompilationResult runCompilation(Path workDir) + throws IOException, InterruptedException + { + ProcessBuilder pb = createProcessBuilder("mvn", "clean", "compile"); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); // Merge stderr into stdout + + Process process = pb.start(); + + // Capture all build output into a single string + String output; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) + { + output = reader.lines() + .collect(Collectors.joining(System.lineSeparator())); + } + + int exitCode = process.waitFor(); + + // Print Maven output to server console so grader can observe the build + System.out.println(output); + + // Exit code 0 means compilation succeeded + return new CompilationResult(exitCode == 0, output); + } + + /** + * Creates a ProcessBuilder for the given command. + * Protected so tests can override to avoid spawning real processes. + * + * @param command The command and its arguments + * + * @return A new ProcessBuilder configured for the command + */ + protected ProcessBuilder createProcessBuilder(String... command) + { + return new ProcessBuilder(command); + } + + /** + * Recursively deletes the temporary build directory. + * + * @param directory The directory to delete + */ + private void cleanup(Path directory) + { + try + { + Files.walk(directory) + .sorted(Comparator.reverseOrder()) + .forEach(path -> + { + try + { + Files.delete(path); + } + catch (IOException e) + { + /* best effort cleanup */ + } + }); + } + catch (IOException e) + { + System.err.println("Warning: Failed to clean up " + directory); + } + } +} diff --git a/src/test/java/MainTest.java b/src/test/java/MainTest.java index 1ce74bc..5970772 100644 --- a/src/test/java/MainTest.java +++ b/src/test/java/MainTest.java @@ -4,6 +4,8 @@ import org.junit.After; import org.junit.Test; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream;