diff --git a/.editorconfig b/.editorconfig
index c67139c..32483b0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,6 +1,3 @@
-# EditorConfig is awesome: https://EditorConfig.org
-
-# top-most EditorConfig file
root = true
[*.{css,js,json,xml,java}]
diff --git a/Jenkinsfile b/Jenkinsfile
index fccfa47..e87d830 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -24,7 +24,7 @@ pipeline {
steps {
script {
docker.build('maven-build', '-f Dockerfile.maven .').inside {
- maven cmd: "clean install -Pproduction"
+ maven cmd: "clean verify -Pit,production"
if (env.BRANCH_NAME == 'master') {
maven cmd: "org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DincludeLicenseText=true -DoutputFormat=json"
@@ -48,7 +48,7 @@ pipeline {
}
}
- junit testDataPublishers: [[$class: 'StabilityTestDataPublisher']], testResults: '**/target/surefire-reports/**/*.xml'
+ junit testDataPublishers: [[$class: 'StabilityTestDataPublisher']], testResults: '**/target/*-reports/**/*.xml'
recordIssues tools: [eclipse()], qualityGates: [[threshold: 1, type: 'TOTAL']]
recordIssues tools: [mavenConsole()]
}
diff --git a/pom.xml b/pom.xml
index b2013d9..375a720 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,187 +1,228 @@
- 4.0.0
-
- io.ivyteam.devops
- devops
- 0.0.1-SNAPSHOT
- jar
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
-
- 21
- 24.6.0
- true
-
+ io.ivyteam.devops
+ devops
+ 0.0.1-SNAPSHOT
+ jar
-
- org.springframework.boot
- spring-boot-starter-parent
- 3.4.1
-
-
-
-
- Vaadin Directory
- https://maven.vaadin.com/vaadin-addons
-
- false
-
-
-
+
+ 21
+ 24.6.0
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
-
-
-
- com.vaadin
- vaadin-bom
- ${vaadin.version}
- pom
- import
-
-
-
+
+
+ Vaadin Directory
+ https://maven.vaadin.com/vaadin-addons
+
+ false
+
+
+
+
com.vaadin
- vaadin
-
-
- com.vaadin
- vaadin-spring-boot-starter
-
-
- org.springframework.boot
- spring-boot-starter-oauth2-client
-
-
- org.springframework.boot
- spring-boot-starter-validation
-
-
- org.springframework.boot
- spring-boot-devtools
- true
+ vaadin-bom
+ ${vaadin.version}
+ pom
+ import
+
+
-
- org.kohsuke
- github-api
- 1.326
-
+
+
+ com.vaadin
+ vaadin
+
+
+ com.vaadin
+ vaadin-spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ true
+
-
- io.jsonwebtoken
- jjwt-api
- 0.12.6
-
-
- io.jsonwebtoken
- jjwt-impl
- 0.12.6
-
-
- io.jsonwebtoken
- jjwt-jackson
- 0.12.6
-
+
+ org.kohsuke
+ github-api
+ 1.326
+
-
- org.xerial
- sqlite-jdbc
- 3.47.1.0
-
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+
-
- org.junit.jupiter
- junit-jupiter-engine
- test
-
-
- org.assertj
- assertj-core
- test
-
-
+
+ org.xerial
+ sqlite-jdbc
+ 3.47.1.0
+
-
- spring-boot:run
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+ spring-boot:run
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ com.vaadin
+ vaadin-maven-plugin
+ ${vaadin.version}
+
+
+
+ prepare-frontend
+
+
+
+
+
+
+
+
+
+ native
+
-
- org.springframework.boot
- spring-boot-maven-plugin
-
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+
+ -march=x86-64-v2
+ -H:+StaticExecutableWithDynamicLibC
+
+
+
+
+
+
-
- com.vaadin
- vaadin-maven-plugin
- ${vaadin.version}
-
-
-
- prepare-frontend
-
-
-
-
+
+
+ production
+
+
+
+ com.vaadin
+ vaadin-core
+
+
+ com.vaadin
+ vaadin-dev
+
+
+
+
+
+
+
+ com.vaadin
+ vaadin-maven-plugin
+ ${vaadin.version}
+
+
+
+ build-frontend
+
+ compile
+
+
+
-
+
+
-
-
- native
-
-
-
- org.graalvm.buildtools
- native-maven-plugin
-
-
-
- -march=x86-64-v2
- -H:+StaticExecutableWithDynamicLibC
-
-
-
-
-
-
-
-
-
- production
-
-
-
- com.vaadin
- vaadin-core
-
-
- com.vaadin
- vaadin-dev
-
-
-
-
-
-
-
- com.vaadin
- vaadin-maven-plugin
- ${vaadin.version}
-
-
-
- build-frontend
-
- compile
-
-
-
-
-
-
-
+
+ it
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.5.2
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ start-spring-boot
+ pre-integration-test
+
+ start
+
+
+
+ stop-spring-boot
+ post-integration-test
+
+ stop
+
+
+
+
+
+
+
+
diff --git a/src/main/java/io/ivyteam/devops/github/webhook/GitHubWebhookController.java b/src/main/java/io/ivyteam/devops/github/webhook/GitHubWebhookController.java
index a14cebb..7045ece 100644
--- a/src/main/java/io/ivyteam/devops/github/webhook/GitHubWebhookController.java
+++ b/src/main/java/io/ivyteam/devops/github/webhook/GitHubWebhookController.java
@@ -16,9 +16,10 @@
import io.ivyteam.devops.branch.Branch;
import io.ivyteam.devops.branch.BranchRepository;
-import io.ivyteam.devops.github.GitHubProvider;
import io.ivyteam.devops.pullrequest.PullRequest;
import io.ivyteam.devops.pullrequest.PullRequestRepository;
+import io.ivyteam.devops.repo.Repo;
+import io.ivyteam.devops.repo.RepoRepository;
@RestController
@RequestMapping(GitHubWebhookController.PATH)
@@ -28,13 +29,13 @@ public class GitHubWebhookController {
public static final String PATH = "/github-webhook/";
@Autowired
- BranchRepository branches;
+ RepoRepository repos;
@Autowired
- PullRequestRepository prs;
+ BranchRepository branches;
@Autowired
- GitHubProvider gitHub;
+ PullRequestRepository prs;
@GetMapping(produces = "text/plain")
String get() {
@@ -42,56 +43,41 @@ String get() {
}
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, headers = "X-GitHub-Event=push")
- public ResponseEntity push(@RequestBody PushBean bean) {
- validateBean(bean);
+ public ResponseEntity push(@RequestBody PushBean bean) {
if (bean.deleted) {
- return ResponseEntity.noContent().build();
+ return ResponseEntity.ok().body("DELETED");
}
var branch = bean.toBranch();
+ if (!repos.exist(bean.repository.full_name)) {
+ repos.create(Repo.create().name(bean.repository.full_name).build());
+ }
branches.create(branch);
- return ResponseEntity.ok().body(branch);
+ return ResponseEntity.ok().body("CREATED");
}
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, headers = "X-GitHub-Event=delete")
- public ResponseEntity deleteBranch(@RequestBody BranchBean bean) {
- validateBean(bean);
+ public ResponseEntity delete(@RequestBody BranchBean bean) {
if ("branch".equals(bean.ref_type)) {
- branches.delete(bean.repo(), bean.name());
- return ResponseEntity.ok().body(bean);
+ branches.delete(bean.repository().full_name(), bean.ref());
+ return ResponseEntity.ok().body("DELETED");
}
throw new RuntimeException("ref type not supported: " + bean.ref_type);
}
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, headers = "X-GitHub-Event=pull_request")
- public ResponseEntity pr(@RequestBody PullRequestBean bean) {
- validateBean(bean);
+ public ResponseEntity pr(@RequestBody PullRequestBean bean) {
var pr = bean.toPullRequest();
if ("opened".equals(bean.action)) {
prs.create(pr);
- return ResponseEntity.ok().body(pr);
+ return ResponseEntity.ok().body("CREATED");
}
if ("closed".equals(bean.action)) {
prs.delete(pr);
- return ResponseEntity.ok().body(pr);
+ return ResponseEntity.ok().body("DELETED");
}
return ResponseEntity.noContent().build();
}
- private void validateBean(Object bean) {
- switch (bean) {
- case PushBean p -> validateOrg(p.organization);
- case BranchBean b -> validateOrg(b.organization);
- case PullRequestBean pr -> validateOrg(pr.organization);
- default -> throw new RuntimeException("Invalid request body provided: " + bean);
- }
- }
-
- private void validateOrg(Organization org) {
- if (org == null || org.login == null || !org.login.equals(gitHub.org())) {
- throw new RuntimeException("Invalid organization provided: " + org);
- }
- }
-
private static Date tsToDate(String timestamp) {
var instant = ZonedDateTime.parse(timestamp).toInstant();
return Date.from(instant);
@@ -101,7 +87,6 @@ record PushBean(
String ref,
Repository repository,
Commit head_commit,
- Organization organization,
boolean deleted) {
Branch toBranch() {
@@ -109,7 +94,6 @@ Branch toBranch() {
.repository(this.repository.full_name)
.name(ref.replace("refs/heads/", ""))
.lastCommitAuthor(this.head_commit.author.username)
- .protectedBranch(false)
.authoredDate(tsToDate(this.head_commit.timestamp))
.build();
}
@@ -118,25 +102,13 @@ Branch toBranch() {
record BranchBean(
String ref,
String ref_type,
- Repository repository,
- User sender,
- Organization organization,
- String updated_at) {
-
- String repo() {
- return repository.full_name;
- }
-
- String name() {
- return ref;
- }
+ Repository repository) {
}
record PullRequestBean(
String action,
PrDetail pull_request,
- Repository repository,
- Organization organization) {
+ Repository repository) {
PullRequest toPullRequest() {
return PullRequest.create()
@@ -149,15 +121,12 @@ PullRequest toPullRequest() {
}
}
- record Commit(String id, String timestamp, String url, Author author) {
+ record Commit(String timestamp, Author author) {
}
record Author(String username) {
}
- record Organization(String login) {
- }
-
record Repository(String full_name) {
}
diff --git a/src/main/java/io/ivyteam/devops/repo/RepoRepository.java b/src/main/java/io/ivyteam/devops/repo/RepoRepository.java
index d3190e8..6265ab4 100644
--- a/src/main/java/io/ivyteam/devops/repo/RepoRepository.java
+++ b/src/main/java/io/ivyteam/devops/repo/RepoRepository.java
@@ -51,6 +51,20 @@ public List all() {
}
}
+ public boolean exist(String name) {
+ try (var connection = db.connection()) {
+ try (var stmt = connection.prepareStatement("SELECT COUNT(*) FROM repository WHERE name = ?")) {
+ stmt.setString(1, name);
+ try (var result = stmt.executeQuery()) {
+ result.next();
+ return result.getInt(1) > 0;
+ }
+ }
+ } catch (SQLException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
public void create(Repo repo) {
try (var connection = db.connection()) {
try (var stmt = connection.prepareStatement("DELETE FROM repository WHERE name = ?")) {
diff --git a/src/main/java/io/ivyteam/devops/security/SecurityConfiguration.java b/src/main/java/io/ivyteam/devops/security/SecurityConfiguration.java
index 8a2abdb..39584c0 100644
--- a/src/main/java/io/ivyteam/devops/security/SecurityConfiguration.java
+++ b/src/main/java/io/ivyteam/devops/security/SecurityConfiguration.java
@@ -6,7 +6,6 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -21,30 +20,18 @@ public class SecurityConfiguration extends VaadinWebSecurity {
@Override
protected void configure(HttpSecurity http) throws Exception {
- http
- .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable())
- .authorizeHttpRequests(
- authz -> {
- authz.requestMatchers(GitHubWebhookController.PATH).anonymous();
- })
- .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());
- super.configure(http);
- http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());
+ http.authorizeHttpRequests(authz -> authz.requestMatchers(GitHubWebhookController.PATH).anonymous());
+ http.csrf(c -> c.ignoringRequestMatchers(GitHubWebhookController.PATH));
http.oauth2Login(c -> c.loginPage("/login").permitAll());
+ super.configure(http);
}
@Bean
- public OAuth2AuthorizedClientManager authorizedClientManager(
- ClientRegistrationRepository clientRegistrationRepository,
- OAuth2AuthorizedClientService authorizedClientService) {
-
- OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
- .clientCredentials().build();
-
- AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
- clientRegistrationRepository, authorizedClientService);
- authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
-
- return authorizedClientManager;
+ public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository repo,
+ OAuth2AuthorizedClientService service) {
+ var provider = OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build();
+ var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(repo, service);
+ manager.setAuthorizedClientProvider(provider);
+ return manager;
}
}
diff --git a/src/main/java/io/ivyteam/devops/settings/Settings.java b/src/main/java/io/ivyteam/devops/settings/Settings.java
index beca5c2..df8d162 100644
--- a/src/main/java/io/ivyteam/devops/settings/Settings.java
+++ b/src/main/java/io/ivyteam/devops/settings/Settings.java
@@ -9,8 +9,8 @@ public class Settings {
public static final String EXCLUDED_BRANCH_PREFIXES = "excluded.branch.prefixes";
public static final String BRANCH_PROTECTION_PREFIXES = "branch.protection.prefixes";
- private String gitHubClientId = "";
- private String gitHubClientSecret = "";
+ private String gitHubClientId = "client-id";
+ private String gitHubClientSecret = "client-secret";
private String gitHubAppId = "";
private String gitHubAppInstallationId = "";
private String excludedBranchPrefixes = "";
diff --git a/src/test/java/io/ivyteam/devops/github/webhook/GitHubWebhookControllerIT.java b/src/test/java/io/ivyteam/devops/github/webhook/GitHubWebhookControllerIT.java
new file mode 100644
index 0000000..ad76e6b
--- /dev/null
+++ b/src/test/java/io/ivyteam/devops/github/webhook/GitHubWebhookControllerIT.java
@@ -0,0 +1,114 @@
+package io.ivyteam.devops.github.webhook;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+import org.junit.jupiter.api.Test;
+
+class GitHubWebhookControllerIT {
+
+ @Test
+ void get() throws Exception {
+ try (var client = HttpClient.newHttpClient()) {
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:8080/github-webhook/"))
+ .build();
+ var response = client.send(request, BodyHandlers.ofString());
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body()).isEqualTo("OK");
+ }
+ }
+
+ @Test
+ void push_create() throws Exception {
+ var push = """
+ {
+ "ref": "refs/heads/market-install-result",
+ "repository": {
+ "full_name": "axonivy/core"
+ },
+ "deleted": false,
+ "head_commit": {
+ "timestamp": "2024-12-27T09:44:59+01:00",
+ "author": {
+ "username": "ivy-lmu"
+ }
+ }
+ }
+ """;
+
+ try (var client = HttpClient.newHttpClient()) {
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:8080/github-webhook/"))
+ .header("X-GitHub-Event", "push")
+ .header("Content-Type", "application/json")
+ .POST(BodyPublishers.ofString(push))
+ .build();
+ var response = client.send(request, BodyHandlers.ofString());
+ response.headers().map().forEach((k, v) -> System.out.println(k + ": " + v));
+ // assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body()).isEqualTo("CREATED");
+ }
+ }
+
+ @Test
+ void push_delete() throws Exception {
+ var push = """
+ {
+ "ref": "refs/heads/market-install-result",
+ "repository": {
+ "full_name": "axonivy/core"
+ },
+ "deleted": true,
+ "head_commit": {
+ "timestamp": "2024-12-27T09:44:59+01:00",
+ "author": {
+ "username": "ivy-lmu"
+ }
+ }
+ }
+ """;
+
+ try (var client = HttpClient.newHttpClient()) {
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:8080/github-webhook/"))
+ .header("X-GitHub-Event", "push")
+ .header("Content-Type", "application/json")
+ .POST(BodyPublishers.ofString(push))
+ .build();
+ var response = client.send(request, BodyHandlers.ofString());
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body()).isEqualTo("DELETED");
+ }
+ }
+
+ @Test
+ void delete() throws Exception {
+ var push = """
+ {
+ "ref": "market-install-result",
+ "repository": {
+ "full_name": "axonivy/core"
+ },
+ "ref_type": "branch"
+ }
+ """;
+
+ try (var client = HttpClient.newHttpClient()) {
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:8080/github-webhook/"))
+ .header("X-GitHub-Event", "delete")
+ .header("Content-Type", "application/json")
+ .POST(BodyPublishers.ofString(push))
+ .build();
+ var response = client.send(request, BodyHandlers.ofString());
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body()).isEqualTo("DELETED");
+ }
+ }
+}
diff --git a/src/test/java/io/ivyteam/devops/repo/TestRepoRepository.java b/src/test/java/io/ivyteam/devops/repo/TestRepoRepository.java
new file mode 100644
index 0000000..8593a1d
--- /dev/null
+++ b/src/test/java/io/ivyteam/devops/repo/TestRepoRepository.java
@@ -0,0 +1,46 @@
+package io.ivyteam.devops.repo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import io.ivyteam.devops.db.Database;
+
+class TestRepoRepository {
+
+ private static final Repo REPO = Repo.create()
+ .name("axonivy/test")
+ .build();
+
+ @TempDir
+ Path tempDir;
+
+ RepoRepository repos;
+
+ @BeforeEach
+ void beforeEach() {
+ var db = new Database(tempDir.resolve("test.db"));
+ repos = new RepoRepository(db);
+ }
+
+ @Test
+ void create() {
+ repos.create(REPO);
+ assertThat(repos.all()).containsExactly(REPO);
+ }
+
+ @Test
+ void exist() {
+ repos.create(REPO);
+ assertThat(repos.exist("axonivy/test")).isTrue();
+ }
+
+ @Test
+ void doesNotExist() {
+ assertThat(repos.exist("axonivy/test")).isFalse();
+ }
+}