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(); + } +}