Skip to content

Commit 2bf2473

Browse files
committed
add github client config & githubsync service
1 parent 6422d30 commit 2bf2473

32 files changed

+1732
-2
lines changed

server/application-server/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ DATASOURCE_PASSWORD=helios
44
NATS_SERVER=localhost:4222
55
NATS_AUTH_TOKEN='5760e8ae09adfb2756f9f8cd5cb2caa704cd3f549eaa9298be843ceb165185d815b81f90c680fa7f626b7cd63abf6ac9'
66
REPOSITORY_NAME=<repository_name e.g. ls1intum/Helios>
7-
7+
GITHUB_AUTH_TOKEN=

server/application-server/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ dependencies {
5454
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
5555
implementation 'io.nats:jnats:2.20.4'
5656
implementation 'org.kohsuke:github-api:1.326'
57+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
58+
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
5759
runtimeOnly 'org.postgresql:postgresql'
5860
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
5961
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package de.tum.cit.aet.helios.config;
2+
3+
import lombok.Getter;
4+
import okhttp3.Cache;
5+
import okhttp3.OkHttpClient;
6+
import okhttp3.logging.HttpLoggingInterceptor;
7+
import org.kohsuke.github.GitHub;
8+
import org.kohsuke.github.GitHubBuilder;
9+
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.core.env.Environment;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.util.concurrent.TimeUnit;
21+
22+
@Configuration
23+
public class GitHubConfig {
24+
25+
private static final Logger logger = LoggerFactory.getLogger(GitHubConfig.class);
26+
27+
@Getter
28+
@Value("${github.organizationName}")
29+
private String organizationName;
30+
31+
@Value("${github.authToken}")
32+
private String ghAuthToken;
33+
34+
@Value("${github.cache.enabled}")
35+
private boolean cacheEnabled;
36+
37+
@Value("${github.cache.ttl}")
38+
private int cacheTtl;
39+
40+
@Value("${github.cache.size}")
41+
private int cacheSize;
42+
43+
private final Environment environment;
44+
45+
@Autowired
46+
public GitHubConfig(Environment environment) {
47+
this.environment = environment;
48+
}
49+
50+
@Bean
51+
public GitHub createGitHubClientWithCache() {
52+
if (ghAuthToken == null || ghAuthToken.isEmpty()) {
53+
logger.error("GitHub auth token is not provided! GitHub client will be disabled.");
54+
return GitHub.offline();
55+
}
56+
57+
// Set up a logging interceptor for debugging
58+
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
59+
if (environment.matchesProfiles("debug")) {
60+
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!");
61+
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
62+
} else {
63+
logger.info("The requests to GitHub will be logged with the basic information.");
64+
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
65+
}
66+
67+
OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
68+
69+
if (cacheEnabled) {
70+
File cacheDir = new File("./build/github-cache");
71+
Cache cache = new Cache(cacheDir, cacheSize * 1024L * 1024L);
72+
builder.cache(cache);
73+
logger.info("Cache is enabled with TTL {} seconds and size {} MB", cacheTtl, cacheSize);
74+
} else {
75+
logger.info("Cache is disabled");
76+
}
77+
78+
// Configure OkHttpClient with the cache and logging
79+
OkHttpClient okHttpClient = builder
80+
.connectTimeout(15, TimeUnit.SECONDS)
81+
.writeTimeout(15, TimeUnit.SECONDS)
82+
.readTimeout(15, TimeUnit.SECONDS)
83+
.addInterceptor(loggingInterceptor)
84+
.build();
85+
86+
try {
87+
// Initialize GitHub client with OAuth token and custom OkHttpClient
88+
GitHub github = new GitHubBuilder()
89+
.withConnector(new OkHttpGitHubConnector(okHttpClient, cacheTtl))
90+
.withOAuthToken(ghAuthToken)
91+
.build();
92+
if (!github.isCredentialValid()) {
93+
logger.error("Invalid GitHub credentials!");
94+
throw new IllegalStateException("Invalid GitHub credentials");
95+
}
96+
logger.info("GitHub client initialized successfully");
97+
return github;
98+
} catch (IOException e) {
99+
logger.error("Failed to initialize GitHub client: {}", e.getMessage());
100+
throw new RuntimeException("GitHub client initialization failed", e);
101+
} catch (Exception e) {
102+
logger.error("An unexpected error occurred during GitHub client initialization: {}", e.getMessage());
103+
throw new RuntimeException("Unexpected error during GitHub client initialization", e);
104+
}
105+
}
106+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package de.tum.cit.aet.helios.gitprovider;
2+
3+
import de.tum.cit.aet.helios.config.GitHubConfig;
4+
import org.kohsuke.github.GHOrganization;
5+
import org.kohsuke.github.GHRepository;
6+
import org.kohsuke.github.GHWorkflow;
7+
import org.kohsuke.github.GitHub;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.io.IOException;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
@Service
18+
public class GitHubService {
19+
20+
private static final Logger logger = LoggerFactory.getLogger(GitHubService.class);
21+
22+
private final GitHub github;
23+
24+
private final GitHubConfig gitHubConfig;
25+
26+
private GHOrganization gitHubOrganization;
27+
28+
@Autowired
29+
public GitHubService(GitHub github, GitHubConfig gitHubConfig) {
30+
this.github = github;
31+
this.gitHubConfig = gitHubConfig;
32+
}
33+
34+
/**
35+
* Retrieves the GitHub organization client.
36+
*
37+
* @return the GitHub organization client
38+
* @throws IOException if an I/O error occurs
39+
*/
40+
public GHOrganization getOrganizationClient() throws IOException {
41+
if (gitHubOrganization == null) {
42+
final String organizationName = gitHubConfig.getOrganizationName();
43+
if (organizationName == null || organizationName.isEmpty()) {
44+
logger.error("No organization name provided in the configuration. GitHub organization client will not be initialized.");
45+
throw new RuntimeException("No organization name provided in the configuration.");
46+
}
47+
gitHubOrganization = github.getOrganization(organizationName);
48+
}
49+
return gitHubOrganization;
50+
}
51+
52+
/**
53+
* Retrieves the GitHub repository.
54+
*
55+
* @param repoNameWithOwners the repository name with owners
56+
* @return the GitHub repository
57+
* @throws IOException if an I/O error occurs
58+
*/
59+
public GHRepository getRepository(String repoNameWithOwners) throws IOException {
60+
return github.getRepository(repoNameWithOwners);
61+
}
62+
63+
/**
64+
* Retrieves the list of workflows for a given repository.
65+
*
66+
* @param repoNameWithOwners the repository name with owners
67+
* @return the list of workflows
68+
* @throws IOException if an I/O error occurs
69+
*/
70+
public List<GHWorkflow> getWorkflows(String repoNameWithOwners) throws IOException {
71+
return getRepository(repoNameWithOwners).listWorkflows().toList();
72+
}
73+
74+
/**
75+
* Retrieves a specific workflow for a given repository.
76+
*
77+
* @param repoNameWithOwners the repository name with owners
78+
* @param workflowFileNameOrId the workflow file name or ID
79+
* @return the GitHub workflow
80+
* @throws IOException if an I/O error occurs
81+
*/
82+
public GHWorkflow getWorkflow(String repoNameWithOwners, String workflowFileNameOrId) throws IOException {
83+
return getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId);
84+
}
85+
86+
/**
87+
* Dispatches a workflow for a given repository.
88+
*
89+
* @param repoNameWithOwners the repository name with owners
90+
* @param workflowFileNameOrId the workflow file name or ID
91+
* @param ref the reference (branch or tag) to run the workflow on
92+
* @param inputs the inputs for the workflow
93+
* @throws IOException if an I/O error occurs
94+
*/
95+
public void dispatchWorkflow(String repoNameWithOwners, String workflowFileNameOrId, String ref, Map<String, Object> inputs) throws IOException {
96+
getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId).dispatch(ref, inputs);
97+
}
98+
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package de.tum.cit.aet.helios.gitprovider.common.github;
2+
3+
import jakarta.persistence.Id;
4+
import jakarta.persistence.MappedSuperclass;
5+
import lombok.*;
6+
7+
import java.time.OffsetDateTime;
8+
9+
@MappedSuperclass
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@ToString
15+
public abstract class BaseGitServiceEntity {
16+
@Id
17+
protected Long id;
18+
19+
protected OffsetDateTime createdAt;
20+
21+
protected OffsetDateTime updatedAt;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package de.tum.cit.aet.helios.gitprovider.common.github;
2+
3+
import org.kohsuke.github.GHObject;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.core.convert.converter.Converter;
7+
import org.springframework.data.convert.ReadingConverter;
8+
import org.springframework.lang.NonNull;
9+
10+
import java.io.IOException;
11+
12+
@ReadingConverter
13+
public abstract class BaseGitServiceEntityConverter<S extends GHObject, T extends BaseGitServiceEntity>
14+
implements Converter<S, T> {
15+
16+
private static final Logger logger = LoggerFactory.getLogger(BaseGitServiceEntityConverter.class);
17+
18+
abstract public T update(@NonNull S source, @NonNull T target);
19+
20+
protected void convertBaseFields(S source, T target) {
21+
if (source == null || target == null) {
22+
throw new IllegalArgumentException("Source and target must not be null");
23+
}
24+
25+
// Map common fields
26+
target.setId(source.getId());
27+
28+
try {
29+
target.setCreatedAt(DateUtil.convertToOffsetDateTime(source.getCreatedAt()));
30+
} catch (IOException e) {
31+
logger.error("Failed to convert createdAt field for source {}: {}", source.getId(), e.getMessage());
32+
target.setCreatedAt(null);
33+
}
34+
35+
try {
36+
target.setUpdatedAt(DateUtil.convertToOffsetDateTime(source.getUpdatedAt()));
37+
} catch (IOException e) {
38+
logger.error("Failed to convert updatedAt field for source {}: {}", source.getId(), e.getMessage());
39+
target.setUpdatedAt(null);
40+
}
41+
}
42+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package de.tum.cit.aet.helios.gitprovider.common.github;
2+
3+
import java.time.OffsetDateTime;
4+
import java.time.ZoneOffset;
5+
import java.util.Date;
6+
7+
public class DateUtil {
8+
public static OffsetDateTime convertToOffsetDateTime(Date date) {
9+
return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null;
10+
}
11+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package de.tum.cit.aet.helios.gitprovider.issue;
2+
3+
import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntity;
4+
import de.tum.cit.aet.helios.gitprovider.repository.Repository;
5+
import de.tum.cit.aet.helios.gitprovider.user.User;
6+
import jakarta.persistence.*;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
import lombok.Setter;
10+
import lombok.ToString;
11+
import org.springframework.lang.NonNull;
12+
13+
import java.time.OffsetDateTime;
14+
import java.util.HashSet;
15+
import java.util.Set;
16+
17+
@Entity
18+
@Table(name = "issue")
19+
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
20+
@DiscriminatorColumn(name = "issue_type", discriminatorType = DiscriminatorType.STRING)
21+
@DiscriminatorValue(value = "ISSUE")
22+
@Getter
23+
@Setter
24+
@NoArgsConstructor
25+
@ToString(callSuper = true)
26+
public class Issue extends BaseGitServiceEntity {
27+
28+
private int number;
29+
30+
@NonNull
31+
@Enumerated(EnumType.STRING)
32+
private State state;
33+
34+
@NonNull
35+
private String title;
36+
37+
@Lob
38+
private String body;
39+
40+
@NonNull
41+
private String htmlUrl;
42+
43+
private boolean isLocked;
44+
45+
private OffsetDateTime closedAt;
46+
47+
private int commentsCount;
48+
49+
@ManyToOne(fetch = FetchType.LAZY)
50+
@JoinColumn(name = "author_id")
51+
@ToString.Exclude
52+
private User author;
53+
54+
@ManyToMany
55+
@JoinTable(name = "issue_assignee", joinColumns = @JoinColumn(name = "issue_id"), inverseJoinColumns = @JoinColumn(name = "user_id"))
56+
@ToString.Exclude
57+
private Set<User> assignees = new HashSet<>();
58+
59+
@ManyToOne(fetch = FetchType.LAZY)
60+
@JoinColumn(name = "repository_id")
61+
private Repository repository;
62+
63+
public enum State {
64+
OPEN, CLOSED
65+
}
66+
67+
public boolean isPullRequest() {
68+
return false;
69+
}
70+
71+
// Missing properties
72+
// - milestone
73+
// - issue_comments
74+
// - labels
75+
76+
// Ignored GitHub properties:
77+
// - closed_by seems not to be used by webhooks
78+
// - author_association (not provided by our GitHub API client)
79+
// - state_reason
80+
// - reactions
81+
// - active_lock_reason
82+
// - [remaining urls]
83+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package de.tum.cit.aet.helios.gitprovider.issue;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
5+
public interface IssueRepository extends JpaRepository<Issue, Long> {
6+
7+
}

0 commit comments

Comments
 (0)