Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(github): Add github client config & Githubsync service #30

Merged
merged 3 commits into from
Nov 17, 2024
Merged
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
3 changes: 2 additions & 1 deletion server/application-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ DATASOURCE_PASSWORD=helios
NATS_SERVER=localhost:4222
NATS_AUTH_TOKEN='5760e8ae09adfb2756f9f8cd5cb2caa704cd3f549eaa9298be843ceb165185d815b81f90c680fa7f626b7cd63abf6ac9'
REPOSITORY_NAME=<repository_name e.g. ls1intum/Helios>

ORGANIZATION_NAME=<organization_name e.g. ls1intum>
GITHUB_AUTH_TOKEN=
2 changes: 2 additions & 0 deletions server/application-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ dependencies {
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'io.nats:jnats:2.20.4'
implementation 'org.kohsuke:github-api:1.326'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
runtimeOnly 'org.postgresql:postgresql'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package de.tum.cit.aet.helios.config;

import lombok.Getter;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Configuration
public class GitHubConfig {

private static final Logger logger = LoggerFactory.getLogger(GitHubConfig.class);

@Getter
@Value("${github.organizationName}")
private String organizationName;

@Value("${github.authToken}")
private String ghAuthToken;

@Value("${github.cache.enabled}")
private boolean cacheEnabled;

@Value("${github.cache.ttl}")
private int cacheTtl;

@Value("${github.cache.size}")
private int cacheSize;

private final Environment environment;

@Autowired
public GitHubConfig(Environment environment) {
this.environment = environment;
}

@Bean
public GitHub createGitHubClientWithCache() {
if (ghAuthToken == null || ghAuthToken.isEmpty()) {
logger.error("GitHub auth token is not provided! GitHub client will be disabled.");
return GitHub.offline();
}

// Set up a logging interceptor for debugging
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
if (environment.matchesProfiles("debug")) {
logger.warn("The requests to GitHub will be logged with the full body. This exposes sensitive data such as OAuth tokens. Use only for debugging!");
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
} else {
logger.info("The requests to GitHub will be logged with the basic information.");
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
}

OkHttpClient.Builder builder = new OkHttpClient().newBuilder();

if (cacheEnabled) {
File cacheDir = new File("./build/github-cache");
Cache cache = new Cache(cacheDir, cacheSize * 1024L * 1024L);
builder.cache(cache);
logger.info("Cache is enabled with TTL {} seconds and size {} MB", cacheTtl, cacheSize);
} else {
logger.info("Cache is disabled");
}

// Configure OkHttpClient with the cache and logging
OkHttpClient okHttpClient = builder
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build();

try {
// Initialize GitHub client with OAuth token and custom OkHttpClient
GitHub github = new GitHubBuilder()
.withConnector(new OkHttpGitHubConnector(okHttpClient, cacheTtl))
.withOAuthToken(ghAuthToken)
.build();
if (!github.isCredentialValid()) {
logger.error("Invalid GitHub credentials!");
throw new IllegalStateException("Invalid GitHub credentials");
}
logger.info("GitHub client initialized successfully");
return github;
} catch (IOException e) {
logger.error("Failed to initialize GitHub client: {}", e.getMessage());
throw new RuntimeException("GitHub client initialization failed", e);
} catch (Exception e) {
logger.error("An unexpected error occurred during GitHub client initialization: {}", e.getMessage());
throw new RuntimeException("Unexpected error during GitHub client initialization", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package de.tum.cit.aet.helios.gitprovider;

import de.tum.cit.aet.helios.config.GitHubConfig;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHWorkflow;
import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@Service
public class GitHubService {

private static final Logger logger = LoggerFactory.getLogger(GitHubService.class);

private final GitHub github;

private final GitHubConfig gitHubConfig;

private GHOrganization gitHubOrganization;

@Autowired
public GitHubService(GitHub github, GitHubConfig gitHubConfig) {
this.github = github;
this.gitHubConfig = gitHubConfig;
}

/**
* Retrieves the GitHub organization client.
*
* @return the GitHub organization client
* @throws IOException if an I/O error occurs
*/
public GHOrganization getOrganizationClient() throws IOException {
if (gitHubOrganization == null) {
final String organizationName = gitHubConfig.getOrganizationName();
if (organizationName == null || organizationName.isEmpty()) {
logger.error("No organization name provided in the configuration. GitHub organization client will not be initialized.");
throw new RuntimeException("No organization name provided in the configuration.");
}
gitHubOrganization = github.getOrganization(organizationName);
}
return gitHubOrganization;
}

/**
* Retrieves the GitHub repository.
*
* @param repoNameWithOwners the repository name with owners
* @return the GitHub repository
* @throws IOException if an I/O error occurs
*/
public GHRepository getRepository(String repoNameWithOwners) throws IOException {
return github.getRepository(repoNameWithOwners);
}

/**
* Retrieves the list of workflows for a given repository.
*
* @param repoNameWithOwners the repository name with owners
* @return the list of workflows
* @throws IOException if an I/O error occurs
*/
public List<GHWorkflow> getWorkflows(String repoNameWithOwners) throws IOException {
return getRepository(repoNameWithOwners).listWorkflows().toList();
}

/**
* Retrieves a specific workflow for a given repository.
*
* @param repoNameWithOwners the repository name with owners
* @param workflowFileNameOrId the workflow file name or ID
* @return the GitHub workflow
* @throws IOException if an I/O error occurs
*/
public GHWorkflow getWorkflow(String repoNameWithOwners, String workflowFileNameOrId) throws IOException {
return getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId);
}

/**
* Dispatches a workflow for a given repository.
*
* @param repoNameWithOwners the repository name with owners
* @param workflowFileNameOrId the workflow file name or ID
* @param ref the reference (branch or tag) to run the workflow on
* @param inputs the inputs for the workflow
* @throws IOException if an I/O error occurs
*/
public void dispatchWorkflow(String repoNameWithOwners, String workflowFileNameOrId, String ref, Map<String, Object> inputs) throws IOException {
getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId).dispatch(ref, inputs);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.tum.cit.aet.helios.gitprovider.common.github;

import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import lombok.*;

import java.time.OffsetDateTime;

@MappedSuperclass
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public abstract class BaseGitServiceEntity {
@Id
protected Long id;

protected OffsetDateTime createdAt;

protected OffsetDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.tum.cit.aet.helios.gitprovider.common.github;

import org.kohsuke.github.GHObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.lang.NonNull;

import java.io.IOException;

@ReadingConverter
public abstract class BaseGitServiceEntityConverter<S extends GHObject, T extends BaseGitServiceEntity>
implements Converter<S, T> {

private static final Logger logger = LoggerFactory.getLogger(BaseGitServiceEntityConverter.class);

abstract public T update(@NonNull S source, @NonNull T target);

protected void convertBaseFields(S source, T target) {
if (source == null || target == null) {
throw new IllegalArgumentException("Source and target must not be null");
}

// Map common fields
target.setId(source.getId());

try {
target.setCreatedAt(DateUtil.convertToOffsetDateTime(source.getCreatedAt()));
} catch (IOException e) {
logger.error("Failed to convert createdAt field for source {}: {}", source.getId(), e.getMessage());
target.setCreatedAt(null);
}

try {
target.setUpdatedAt(DateUtil.convertToOffsetDateTime(source.getUpdatedAt()));
} catch (IOException e) {
logger.error("Failed to convert updatedAt field for source {}: {}", source.getId(), e.getMessage());
target.setUpdatedAt(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package de.tum.cit.aet.helios.gitprovider.common.github;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class DateUtil {
public static OffsetDateTime convertToOffsetDateTime(Date date) {
return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package de.tum.cit.aet.helios.gitprovider.issue;

import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntity;
import de.tum.cit.aet.helios.gitprovider.repository.Repository;
import de.tum.cit.aet.helios.gitprovider.user.User;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.lang.NonNull;

import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "issue")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "issue_type", discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue(value = "ISSUE")
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
public class Issue extends BaseGitServiceEntity {

private int number;

@NonNull
@Enumerated(EnumType.STRING)
private State state;

@NonNull
private String title;

@Lob
private String body;

@NonNull
private String htmlUrl;

private boolean isLocked;

private OffsetDateTime closedAt;

private int commentsCount;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
@ToString.Exclude
private User author;

@ManyToMany
@JoinTable(name = "issue_assignee", joinColumns = @JoinColumn(name = "issue_id"), inverseJoinColumns = @JoinColumn(name = "user_id"))
@ToString.Exclude
private Set<User> assignees = new HashSet<>();

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "repository_id")
private Repository repository;

public enum State {
OPEN, CLOSED
}

public boolean isPullRequest() {
return false;
}

// Missing properties
// - milestone
// - issue_comments
// - labels

// Ignored GitHub properties:
// - closed_by seems not to be used by webhooks
// - author_association (not provided by our GitHub API client)
// - state_reason
// - reactions
// - active_lock_reason
// - [remaining urls]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.tum.cit.aet.helios.gitprovider.issue;

import org.springframework.data.jpa.repository.JpaRepository;

public interface IssueRepository extends JpaRepository<Issue, Long> {

}
Loading