Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,35 @@ At line:1 char:1

</details>

### 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 <branch> --single-branch <url>`
3. Checks out the exact commit SHA with `git checkout <sha>`
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**
Expand All @@ -225,10 +254,38 @@ 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.
---
## Statements of contributions
| Name | Contribution |
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Josefine "joss2002" Nyholm | <ul><li>[x] Added initial **Maven** file-tree structure, including a basic `pom.xml` with **JDK 17** support.</li></ul><ul><li>[x] Added skeleton for CI-server in `src/main/java/se/ciserver/ContinuousIntegrationServer.java`.<ul><li>Added related dependencies, plugins; `jetty-server`, `exec-maven-plugin`</li><li>[x] Added additional documentation.</li></ul></li></ul><ul><li>[x] Added **GitHub** push event `JSON` payload component.<ul><li>[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`.</li><li>[x] Added additional `Exception` extension in `InvalidPayloadException.java` specified for invalid payloads.</li><li>[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`.</li><li>[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.</li><li>[x] Added additional dependencies in `pom.xml` for `jackson-databind` and `junit`</li><li>[x] Added additional documentation in `README.md`.</li></ul></li></ul> |
| Avid "HotFazz" Fayaz | <ul><li>[x] Added webhook-triggered compilation (P1) in `src/main/java/se/ciserver/build/Compiler.java`.<ul><li>[x] Clones the pushed branch, checks out the exact commit, and runs `mvn clean compile`.</li><li>[x] Added `CompilationResult.java` to hold build outcome and output.</li><li>[x] Integrated compilation into `ContinuousIntegrationServer.java` webhook handler.</li><li>[x] Added unit tests in `MainTest.java` for `CompilationResult` and `Compiler` failure handling.</li><li>[x] Added documentation in `README.md` for compilation implementation and unit testing.</li></ul></li></ul> |
32 changes: 30 additions & 2 deletions src/main/java/se/ciserver/ContinuousIntegrationServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import java.io.IOException;
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;
Expand All @@ -20,7 +22,8 @@
*/
public class ContinuousIntegrationServer extends AbstractHandler
{
private final PushParser parser = new PushParser();
private final PushParser parser = new PushParser();
private final Compiler compiler = new Compiler();

/**
* Handles incoming HTTP requests for the CI server and presents necessary information.
Expand All @@ -41,23 +44,48 @@ 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);
response.getWriter().println(
(result.success ? "Compilation succeeded" : "Compilation failed")
+ " for commit: " + push.after);
}
catch (InvalidPayloadException e)
{
// Malformed or missing JSON fields
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().println("Invalid payload: " + e.getMessage());
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/se/ciserver/build/CompilationResult.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
190 changes: 190 additions & 0 deletions src/main/java/se/ciserver/build/Compiler.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading