Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c1bed6d
Feature/ei 35 (#26)
OhJino Oct 20, 2025
71ea450
EI-209 Schedule Table Complete (#18)
Mxrcos13 Oct 20, 2025
76ee890
EI-113: create batch card component (#22)
N-Cooper-100 Oct 25, 2025
b42f436
EI-41: stub batch controller endpoints (#17)
cGod778 Oct 25, 2025
1ab124b
feat: create job entity (EI-137) (#21)
dcarello Oct 29, 2025
896a997
EI-20 Created Login page UI (#30)
Mxrcos13 Oct 29, 2025
a67dbe0
Merge branch 'develop' of https://github.com/git-stuff-done/EndpointI…
cGod778 Dec 1, 2025
aa6be60
Merge branch 'develop' of https://github.com/git-stuff-done/EndpointI…
Cdog778 Feb 8, 2026
37b412b
t status
Cdog778 Feb 21, 2026
9a09b77
EI-255
Cdog778 Feb 21, 2026
bcc9970
Update create-job-form.ts
Cdog778 Feb 21, 2026
303bb37
EI-255
Cdog778 Feb 21, 2026
9fdef70
Ei-255
Cdog778 Feb 21, 2026
54071bd
Update JobServiceTest.java
Cdog778 Feb 21, 2026
6e06a3a
255
Cdog778 Feb 22, 2026
6470eaa
ei255
Cdog778 Feb 22, 2026
d0611a8
Update authentication.service.spec.ts
Cdog778 Feb 22, 2026
19cd126
Update authentication.service.spec.ts
Cdog778 Feb 22, 2026
a501b45
Update authentication.service.spec.ts
Cdog778 Feb 22, 2026
3e94808
Ei-255
Cdog778 Feb 22, 2026
5a7efad
Update authentication.service.spec.ts
Cdog778 Feb 22, 2026
32c3ac4
EI-255
Cdog778 Feb 22, 2026
9c7c40d
ei-255
Cdog778 Feb 22, 2026
3bd2f59
EI-255
Cdog778 Feb 22, 2026
8b816bf
Merge branch 'develop' into feature/EI-255
cGod778 Feb 23, 2026
9293375
Merge branch 'develop' into feature/EI-255
cGod778 Feb 23, 2026
26f73b6
Merge branch 'develop' into feature/EI-255
cGod778 Feb 23, 2026
76819c3
Update http-interceptor.service.spec.ts
Cdog778 Feb 23, 2026
69a63fe
Merge branch 'feature/EI-255' of https://github.com/git-stuff-done/En…
Cdog778 Feb 23, 2026
4c40608
Merge branch 'develop' into feature/EI-255
cGod778 Feb 23, 2026
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
16 changes: 16 additions & 0 deletions endpoint-insights-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,22 @@
<version>2.17.2</version>
</dependency>

<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.10.0.202406032230-r</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<version>6.10.0.202406032230-r</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>

<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.vsp.endpointinsightsapi.controller;
import com.vsp.endpointinsightsapi.dto.GitCheckoutResponse;
import com.vsp.endpointinsightsapi.model.*;

import com.vsp.endpointinsightsapi.authentication.PublicAPI;
import com.vsp.endpointinsightsapi.model.Job;
Expand Down Expand Up @@ -165,6 +167,20 @@ public ResponseEntity<JobRunHistory> getJobHistory(
return ResponseEntity.ok(new JobRunHistory(List.of(new JobRun(UUID.fromString("1"), jobId))));
}

/**
* Endpoint to checkout the job repository.
*
* @param jobId the id of the job to checkout
* @return checkout information
* */
@PostMapping("/{id}/checkout")
public ResponseEntity<GitCheckoutResponse> checkoutJobRepository(
@PathVariable("id")
@NotNull(message = ErrorMessages.JOB_ID_REQUIRED)
UUID jobId) {
return ResponseEntity.ok(new GitCheckoutResponse(jobId, jobService.checkoutJobRepository(jobId)));
}




Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.vsp.endpointinsightsapi.dto;

import java.nio.file.Path;
import java.util.UUID;

public class GitCheckoutResponse {
private final UUID jobId;
private final String checkoutPath;

public GitCheckoutResponse(UUID jobId, Path checkoutPath) {
this.jobId = jobId;
this.checkoutPath = checkoutPath == null ? null : checkoutPath.toString();
}

public UUID getJobId() {
return jobId;
}

public String getCheckoutPath() {
return checkoutPath;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package com.vsp.endpointinsightsapi.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vsp.endpointinsightsapi.model.enums.GitAuthType;
import com.vsp.endpointinsightsapi.model.enums.JobStatus;
import com.vsp.endpointinsightsapi.model.enums.TestType;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -36,6 +39,26 @@ public class Job extends AuditingEntity {
@Column(name = "git_url")
private String gitUrl;

@Enumerated(EnumType.STRING)
@Column(name = "git_auth_type", length = 20)
private GitAuthType gitAuthType = GitAuthType.NONE;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(name = "git_username")
private String gitUsername;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(name = "git_password")
private String gitPassword;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(name = "git_ssh_private_key", columnDefinition = "text")
private String gitSshPrivateKey;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(name = "git_ssh_passphrase")
private String gitSshPassphrase;

@Column(name = "run_command")
private String runCommand;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vsp.endpointinsightsapi.model.enums;

public enum GitAuthType {
NONE,
BASIC,
SSH_KEY
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package com.vsp.endpointinsightsapi.service;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.vsp.endpointinsightsapi.model.Job;
import com.vsp.endpointinsightsapi.model.enums.GitAuthType;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory;
import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.function.Supplier;

@Service
public class GitRepositoryService {

private static final Logger LOG = LoggerFactory.getLogger(GitRepositoryService.class);
private static final String DEFAULT_CHECKOUT_DIR = "./.git-checkouts";

private final Path checkoutRoot;

public GitRepositoryService(@Value("${app.git.checkout-dir:" + DEFAULT_CHECKOUT_DIR + "}") String checkoutDir) {
this.checkoutRoot = Paths.get(checkoutDir).toAbsolutePath().normalize();
try {
Files.createDirectories(this.checkoutRoot);
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to initialize checkout directory", e);
}
}

public Path checkoutJobRepository(Job job) {
if (job.getGitUrl() == null || job.getGitUrl().trim().isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Git URL is required to checkout a repository");
}

Path checkoutPath = checkoutRoot.resolve(job.getJobId().toString());

try {
if (Files.exists(checkoutPath.resolve(".git"))) {
pullRepository(job, checkoutPath);
} else {
cloneRepository(job, checkoutPath);
}
return checkoutPath;
} catch (GitAPIException | IOException e) {
LOG.error("Failed to checkout repository for job {}", job.getJobId(), e);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unable to checkout repository", e);
}
}

private void cloneRepository(Job job, Path checkoutPath) throws GitAPIException, IOException {
CredentialsProvider credentialsProvider = buildCredentialsProvider(job);
Runnable cloneAction = () -> {
CloneCommand command = Git.cloneRepository()
.setURI(job.getGitUrl())
.setDirectory(checkoutPath.toFile());
if (credentialsProvider != null) {
command.setCredentialsProvider(credentialsProvider);
}
try (Git ignored = command.call()) {
LOG.info("Repository cloned for job {}", job.getJobId());
} catch (GitAPIException e) {
throw new GitOperationException(e);
}
};

runWithSshIfNeeded(job, cloneAction);
}

private void pullRepository(Job job, Path checkoutPath) throws GitAPIException, IOException {
CredentialsProvider credentialsProvider = buildCredentialsProvider(job);
Runnable pullAction = () -> {
try (Git git = Git.open(checkoutPath.toFile())) {
PullCommand command = git.pull();
if (credentialsProvider != null) {
command.setCredentialsProvider(credentialsProvider);
}
command.call();
LOG.info("Repository updated for job {}", job.getJobId());
} catch (IOException | GitAPIException e) {
throw new GitOperationException(e);
}
};

runWithSshIfNeeded(job, pullAction);
}

private void runWithSshIfNeeded(Job job, Runnable action) throws GitAPIException, IOException {
if (job.getGitAuthType() == GitAuthType.SSH_KEY) {
String privateKey = job.getGitSshPrivateKey();
if (privateKey == null || privateKey.trim().isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "SSH private key is required for git SSH auth");
}

try {
withSshKey(privateKey, job.getGitSshPassphrase(), () -> {
action.run();
return null;
});
return;
} catch (GitOperationException e) {
if (e.getCause() instanceof GitAPIException gitException) {
throw gitException;
}
if (e.getCause() instanceof IOException ioException) {
throw ioException;
}
throw e;
}
}

try {
action.run();
} catch (GitOperationException e) {
if (e.getCause() instanceof GitAPIException gitException) {
throw gitException;
}
if (e.getCause() instanceof IOException ioException) {
throw ioException;
}
throw e;
}
}

private CredentialsProvider buildCredentialsProvider(Job job) {
if (job.getGitAuthType() != GitAuthType.BASIC) {
return null;
}

String username = job.getGitUsername();
String password = job.getGitPassword();
if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Git basic auth requires username and password");
}

return new UsernamePasswordCredentialsProvider(username, password);
}

private <T> T withSshKey(String privateKey, String passphrase, Supplier<T> action) {
Path tempDir = null;
SshSessionFactory previousFactory = SshSessionFactory.getInstance();
try {
tempDir = Files.createTempDirectory("ei-git-ssh");
Path sshDir = Files.createDirectories(tempDir.resolve(".ssh"));
Path keyPath = sshDir.resolve("id_rsa");

Files.writeString(keyPath, privateKey, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
setOwnerOnlyPermissions(keyPath);

JschConfigSessionFactory factory = new JschConfigSessionFactory() {
@Override
protected void configure(OpenSshConfig.Host host, Session session) {
session.setConfig("StrictHostKeyChecking", "no");
}

@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch jsch = super.createDefaultJSch(fs);
jsch.removeAllIdentity();
if (passphrase != null && !passphrase.trim().isEmpty()) {
jsch.addIdentity(keyPath.toString(), passphrase);
} else {
jsch.addIdentity(keyPath.toString());
}
return jsch;
}
};

SshSessionFactory.setInstance(factory);
return action.get();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (RuntimeException e) {
throw e;
} finally {
SshSessionFactory.setInstance(previousFactory);
if (tempDir != null) {
deleteDirectoryQuietly(tempDir);
}
}
}

private void setOwnerOnlyPermissions(Path keyPath) {
try {
Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(keyPath, permissions);
} catch (UnsupportedOperationException | IOException ignored) {
// Non-POSIX file system or permissions not supported.
}
}

private void deleteDirectoryQuietly(Path directory) {
try {
Files.walkFileTree(directory, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.deleteIfExists(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.deleteIfExists(dir);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException ignored) {
}
}

private static class GitOperationException extends RuntimeException {
private GitOperationException(Exception cause) {
super(cause);
}
}
}
Loading
Loading