diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e9a652..935a28c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,42 +1,37 @@ name: Java CI with Maven on: - pull_request: - types: [opened, synchronize] push: - branches: [main] + branches: [ "main", "feature/*" ] + pull_request: + branches: [ "main" ] jobs: build: + runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - - name: Build and Test with Maven - run: mvn clean install - - - name: Notify on PR creation - if: ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }} - run: | - curl -X POST ${{ secrets.CI_BOT }} \ - -H 'Content-type: application/json' \ - --data "{ - \"text\": \"📣 *New Pull Request Created*\n*Repo:* ${{ github.repository }}\n*Author:* ${{ github.actor }}\n*Link:* ${{ github.event.pull_request.html_url }}\" - }" - - - name: Notify on Build Failure - if: failure() - run: | - curl -X POST ${{ secrets.CI_BOT }} \ - -H 'Content-type: application/json' \ - --data "{ - \"text\": \"❌ *Build Failed*\n*Repo:* ${{ github.repository }}\n*Actor:* ${{ github.actor }}\n*Workflow:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\" - }" + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Check Formatting + run: mvn spotless:check + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Run Tests + run: mvn -B test --file pom.xml + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: '**/target/surefire-reports/TEST-*.xml' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/README.md b/README.md index c5670ef..e58046d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,20 @@ -# 🚀 Java-based CI/CD Notification Bot +# 🚀 AI Developer Assistant Platform -A **centralized, extensible notification microservice** that integrates with GitHub Actions, Jenkins, and other CI/CD systems to keep your team informed proactively and intelligently. +[![Java CI with Maven](https://github.com/Agaba-derrick/devops-notifier/actions/workflows/ci.yml/badge.svg)](https://github.com/Agaba-derrick/devops-notifier/actions/workflows/ci.yml) +An **intelligent, extensible platform** that integrates with GitHub Actions, Jenkins, and other CI/CD systems to assist developers proactively. Powered by Spring AI, it analyzes Pull Requests, explains build failures, and automates workflows. --- ## ✨ **Features** +✅ **AI PR Summarization** +Automatically analyzes Pull Requests using LLMs to provide structured summaries, risk assessments, and key file changes. + ✅ **Centralized Notification Engine** Receive events from multiple CI/CD pipelines and process them through a single service. ✅ **Multi-Channel Support** -Send notifications to Slack, Telegram, Email, Microsoft Teams, and more via pluggable `NotificationSender` classes. - -✅ **Configurable Rules Engine** -Define who gets notified about what events, with support for conditions, channels, and teams. - -✅ **Scheduled Proactive Notifications** -Run daily checks for stale pull requests, failing builds, or other conditions, and notify responsible developers automatically. - -✅ **Notification History and Auditing** -Log all notifications sent, including timestamps, status, and target channel, for audit and debugging. +Send AI-enhanced notifications to Slack, Telegram, Email, Microsoft Teams, and more via pluggable `NotificationSender` classes. ✅ **Extensible Event Processing Architecture** Add new event processors for Jira, SonarQube, deployment tools, and any custom event source seamlessly. diff --git a/pom.xml b/pom.xml index becc257..e549e32 100644 --- a/pom.xml +++ b/pom.xml @@ -13,18 +13,29 @@ - com.derick - ci-alert-bot + com.devassistant + dev-assistant-platform 1.0.0 jar - CI Alert Bot - Java-based CI/CD Notification Bot + Developer Assistant Platform + AI-powered Developer Assistant Platform 21 + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + @@ -38,6 +49,19 @@ spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + 1.0.0-M1 + + org.projectlombok @@ -82,6 +106,16 @@ false + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + + diff --git a/src/main/java/com/devassistant/Notificationbot/NotificationBotApplication.java b/src/main/java/com/devassistant/Notificationbot/NotificationBotApplication.java new file mode 100644 index 0000000..bd1bfd8 --- /dev/null +++ b/src/main/java/com/devassistant/Notificationbot/NotificationBotApplication.java @@ -0,0 +1,21 @@ +package com.devassistant.Notificationbot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * The main entry point for the AI Developer Assistant Platform. + * + *

Educational Note: This class uses the {@code @SpringBootApplication} annotation, which is a + * convenience annotation that adds: - {@code @Configuration}: Tags the class as a source of bean + * definitions for the application context. - {@code @EnableAutoConfiguration}: Tells Spring Boot to + * start adding beans based on classpath settings, other beans, and various property settings. - + * {@code @ComponentScan}: Tells Spring to look for other components, configurations, and services + * in the 'com.devassistant' package, allowing it to find the controllers and processors. + */ +@SpringBootApplication +public class NotificationBotApplication { + public static void main(String[] args) { + SpringApplication.run(NotificationBotApplication.class, args); + } +} diff --git a/src/main/java/derrick/Notificationbot/rest/HelloController.java b/src/main/java/com/devassistant/Notificationbot/rest/HelloController.java similarity index 54% rename from src/main/java/derrick/Notificationbot/rest/HelloController.java rename to src/main/java/com/devassistant/Notificationbot/rest/HelloController.java index c756fdd..2871690 100644 --- a/src/main/java/derrick/Notificationbot/rest/HelloController.java +++ b/src/main/java/com/devassistant/Notificationbot/rest/HelloController.java @@ -1,4 +1,4 @@ -package derrick.Notificationbot.rest; +package com.devassistant.Notificationbot.rest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -6,8 +6,8 @@ @RestController public class HelloController { - @GetMapping("/") - public String hello() { - return "Notification Bot is running!"; - } -} \ No newline at end of file + @GetMapping("/") + public String hello() { + return "Notification Bot is running!"; + } +} diff --git a/src/main/java/com/devassistant/logging/Logger.java b/src/main/java/com/devassistant/logging/Logger.java new file mode 100644 index 0000000..04606b2 --- /dev/null +++ b/src/main/java/com/devassistant/logging/Logger.java @@ -0,0 +1,9 @@ +package com.devassistant.logging; + +public interface Logger { + void info(String message); + + void warn(String message); + + void error(String message, Throwable t); +} diff --git a/src/main/java/com/devassistant/logging/LoggerImpl.java b/src/main/java/com/devassistant/logging/LoggerImpl.java new file mode 100644 index 0000000..6788e9f --- /dev/null +++ b/src/main/java/com/devassistant/logging/LoggerImpl.java @@ -0,0 +1,24 @@ +package com.devassistant.logging; + +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class LoggerImpl implements Logger { + private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoggerImpl.class); + + @Override + public void info(String message) { + log.info(message); + } + + @Override + public void warn(String message) { + log.warn(message); + } + + @Override + public void error(String message, Throwable t) { + log.error(message, t); + } +} diff --git a/src/main/java/com/devassistant/processor/BuildFailureEventProcessor.java b/src/main/java/com/devassistant/processor/BuildFailureEventProcessor.java new file mode 100644 index 0000000..42a8ab0 --- /dev/null +++ b/src/main/java/com/devassistant/processor/BuildFailureEventProcessor.java @@ -0,0 +1,3 @@ +package com.devassistant.processor; + +public class BuildFailureEventProcessor {} diff --git a/src/main/java/com/devassistant/processor/EventProcessor.java b/src/main/java/com/devassistant/processor/EventProcessor.java new file mode 100644 index 0000000..b6d748b --- /dev/null +++ b/src/main/java/com/devassistant/processor/EventProcessor.java @@ -0,0 +1,29 @@ +package com.devassistant.processor; + +/** + * EventProcessor defines the contract for processing different types of CI/CD events. + * + *

Educational Note: This interface is the core of the Strategy Design Pattern used in this + * platform. By defining a common interface, the main Webhook controller doesn't need to know the + * specifics of how to handle a Pull Request versus a Build Failure. It can simply iterate through a + * list of EventProcessors, ask each one if it {@code supports()} the event, and if so, call {@code + * processEvent()}. This makes the system highly extensible: to add a new event type, you simply + * create a new class implementing this interface, without modifying the core logic. + */ +public interface EventProcessor { + + /** + * Checks if this processor supports the given event type. + * + * @param eventType The event type (e.g. pull_request, build_failure) + * @return true if supported, false otherwise + */ + boolean supports(String eventType); + + /** + * Processes the given event payload. + * + * @param payload The event payload as a String (typically JSON) + */ + void processEvent(String payload); +} diff --git a/src/main/java/com/devassistant/processor/PullRequestEventProcessor.java b/src/main/java/com/devassistant/processor/PullRequestEventProcessor.java new file mode 100644 index 0000000..5f4ce6d --- /dev/null +++ b/src/main/java/com/devassistant/processor/PullRequestEventProcessor.java @@ -0,0 +1,83 @@ +package com.devassistant.processor; + +import com.devassistant.logging.Logger; +import com.devassistant.pullrequest.PullRequestService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Component; + +/** + * Processes incoming Pull Request events and utilizes AI to summarize the changes. + * + *

Educational Note: This class implements the {@link EventProcessor} interface. We use Spring + * AI's {@link ChatClient} to interact with the configured LLM (e.g., OpenAI). By injecting {@code + * ChatClient.Builder}, we can create a client that sends the PR title and author to the AI to + * generate a risk assessment and summary. + */ +@Component +public class PullRequestEventProcessor implements EventProcessor { + + private static final String SUPPORTED_EVENT = "pull_request"; + + private final ObjectMapper objectMapper; + private final PullRequestService pullRequestService; + private final Logger logger; + private final ChatClient chatClient; + + public PullRequestEventProcessor( + PullRequestService pullRequestService, Logger logger, ChatClient.Builder chatClientBuilder) { + this.objectMapper = new ObjectMapper(); + this.pullRequestService = pullRequestService; + this.logger = logger; + this.chatClient = chatClientBuilder.build(); + } + + @Override + public boolean supports(String eventType) { + return SUPPORTED_EVENT.equalsIgnoreCase(eventType); + } + + @Override + public void processEvent(String payload) { + try { + JsonNode root = objectMapper.readTree(payload); + JsonNode actionNode = root.path("action"); + JsonNode pullRequestNode = root.path("pull_request"); + + if (actionNode.isMissingNode() || pullRequestNode.isMissingNode()) { + logger.warn("Missing pull request action or data in payload."); + return; + } + + String action = actionNode.asText(); + String prTitle = pullRequestNode.path("title").asText(); + String prUrl = pullRequestNode.path("html_url").asText(); + String author = pullRequestNode.path("user").path("login").asText(); + + // AI Integration: Generate PR Summary + String aiPrompt = + String.format( + "Analyze this PR title and provide a 2 sentence summary and risk assessment (Low/Medium/High). Title: '%s', Author: '%s'", + prTitle, author); + + String aiSummary = "AI Summary not available"; + try { + aiSummary = chatClient.prompt().user(aiPrompt).call().content(); + } catch (Exception aiException) { + logger.error("Failed to generate AI summary for PR", aiException); + } + + pullRequestService.handlePullRequest( + action, prTitle + " - AI Analysis: " + aiSummary, prUrl, author); + + logger.info( + String.format( + "Processed pull request event: action=%s, title=%s, author=%s", + action, prTitle, author)); + + } catch (Exception e) { + logger.error("Failed to process pull request event", e); + } + } +} diff --git a/src/main/java/com/devassistant/pullrequest/PullRequestService.java b/src/main/java/com/devassistant/pullrequest/PullRequestService.java new file mode 100644 index 0000000..9449471 --- /dev/null +++ b/src/main/java/com/devassistant/pullrequest/PullRequestService.java @@ -0,0 +1,13 @@ +package com.devassistant.pullrequest; + +public interface PullRequestService { + void commentOnPullRequest(String payload); + + void validatePullRequest(String payload); + + void mergePullRequest(String payload); + + String fetchPullRequestInfo(String pullRequestId); + + void handlePullRequest(String action, String prTitle, String prUrl, String author); +} diff --git a/src/main/java/com/devassistant/pullrequest/PullRequestServiceImpl.java b/src/main/java/com/devassistant/pullrequest/PullRequestServiceImpl.java new file mode 100644 index 0000000..10b22d9 --- /dev/null +++ b/src/main/java/com/devassistant/pullrequest/PullRequestServiceImpl.java @@ -0,0 +1,31 @@ +package com.devassistant.pullrequest; + +import org.springframework.stereotype.Service; + +@Service +public class PullRequestServiceImpl implements PullRequestService { + @Override + public void commentOnPullRequest(String payload) { + // Implementation for commenting on a pull request + } + + @Override + public void validatePullRequest(String payload) { + // Implementation for validating a pull request + } + + @Override + public void mergePullRequest(String payload) { + // Implementation for merging a pull request + } + + @Override + public String fetchPullRequestInfo(String pullRequestId) { + return null; + } + + @Override + public void handlePullRequest(String action, String prTitle, String prUrl, String author) { + // Implementation for handling a pull request from webhook + } +} diff --git a/src/main/java/derrick/Notificationbot/NotificationBotApplication.java b/src/main/java/derrick/Notificationbot/NotificationBotApplication.java deleted file mode 100644 index 596bc2f..0000000 --- a/src/main/java/derrick/Notificationbot/NotificationBotApplication.java +++ /dev/null @@ -1,11 +0,0 @@ -package derrick.Notificationbot; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class NotificationBotApplication { - public static void main(String[] args) { - SpringApplication.run(NotificationBotApplication.class, args); - } -} \ No newline at end of file diff --git a/src/main/java/derrick/logging/Logger b/src/main/java/derrick/logging/Logger deleted file mode 100644 index 0450f6a..0000000 --- a/src/main/java/derrick/logging/Logger +++ /dev/null @@ -1,6 +0,0 @@ -package derrick.logging; - -public interface Logger { - void info(String message); - void error(String message, Throwable t); -} diff --git a/src/main/java/derrick/logging/LoggerImpl b/src/main/java/derrick/logging/LoggerImpl deleted file mode 100644 index 8cc5c5b..0000000 --- a/src/main/java/derrick/logging/LoggerImpl +++ /dev/null @@ -1,14 +0,0 @@ -package derrick.logging.impl; - -import derrick.logging.Logger; - -public class ConsoleLogger implements Logger { - public void info(String message) { - System.out.println("[INFO] " + message); // Can replace later with structured logging - } - - public void error(String message, Throwable t) { - System.err.println("[ERROR] " + message); - t.printStackTrace(System.err); - } -} diff --git a/src/main/java/derrick/processor/BuildFailureEventProcessor.java b/src/main/java/derrick/processor/BuildFailureEventProcessor.java deleted file mode 100644 index 451e9d6..0000000 --- a/src/main/java/derrick/processor/BuildFailureEventProcessor.java +++ /dev/null @@ -1,5 +0,0 @@ -package derrick.processor; - -public class BuildFailureEventProcessor { - -} diff --git a/src/main/java/derrick/processor/EventProcessor.java b/src/main/java/derrick/processor/EventProcessor.java deleted file mode 100644 index 9735318..0000000 --- a/src/main/java/derrick/processor/EventProcessor.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.derick.notificationbot.processor; - -/** - * EventProcessor defines the contract for processing different types of CI/CD events. - */ -public interface EventProcessor { - - /** - * Checks if this processor supports the given event type. - * - * @param eventType The event type (e.g. pull_request, build_failure) - * @return true if supported, false otherwise - */ - boolean supports(String eventType); - - /** - * Processes the given event payload. - * - * @param payload The event payload as a String (typically JSON) - */ - void processEvent(String payload); -} -` diff --git a/src/main/java/derrick/processor/PullRequestEventProcessor b/src/main/java/derrick/processor/PullRequestEventProcessor deleted file mode 100644 index b94aad3..0000000 --- a/src/main/java/derrick/processor/PullRequestEventProcessor +++ /dev/null @@ -1,53 +0,0 @@ -package derrick.processor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import derrick.service.PullRequestService; -import derrick.util.Logger; - -public class PullRequestEventProcessor implements EventProcessor { - - private static final String SUPPORTED_EVENT = "pull_request"; - - private final ObjectMapper objectMapper; - private final PullRequestService pullRequestService; - private final Logger logger; - - public PullRequestEventProcessor(PullRequestService pullRequestService, Logger logger) { - this.objectMapper = new ObjectMapper(); - this.pullRequestService = pullRequestService; - this.logger = logger; - } - - @Override - public boolean supports(String eventType) { - return SUPPORTED_EVENT.equalsIgnoreCase(eventType); - } - - @Override - public void processEvent(String payload) { - try { - JsonNode root = objectMapper.readTree(payload); - JsonNode actionNode = root.path("action"); - JsonNode pullRequestNode = root.path("pull_request"); - - if (actionNode.isMissingNode() || pullRequestNode.isMissingNode()) { - logger.warn("Missing pull request action or data in payload."); - return; - } - - String action = actionNode.asText(); - String prTitle = pullRequestNode.path("title").asText(); - String prUrl = pullRequestNode.path("html_url").asText(); - String author = pullRequestNode.path("user").path("login").asText(); - - pullRequestService.handlePullRequest(action, prTitle, prUrl, author); - - logger.info(String.format("Processed pull request event: action=%s, title=%s, author=%s", - action, prTitle, author)); - - } catch (Exception e) { - logger.error("Failed to process pull request event", e); - } - } -} diff --git a/src/main/java/derrick/pullrequest/PullRequestService b/src/main/java/derrick/pullrequest/PullRequestService deleted file mode 100644 index ba8ebae..0000000 --- a/src/main/java/derrick/pullrequest/PullRequestService +++ /dev/null @@ -1,8 +0,0 @@ -package derrick.service; - -public interface PullRequestService { - void commentOnPullRequest(String payload); - void validatePullRequest(String payload); - void mergePullRequest(String payload); - String fetchPullRequestInfo(String pullRequestId); -} \ No newline at end of file diff --git a/src/main/java/derrick/pullrequest/PullRequestServiceImpl b/src/main/java/derrick/pullrequest/PullRequestServiceImpl deleted file mode 100644 index bbf7287..0000000 --- a/src/main/java/derrick/pullrequest/PullRequestServiceImpl +++ /dev/null @@ -1,34 +0,0 @@ -package derrick.service.impl; - -import derrick.service.PullRequestService; - -public class GitHubPullRequestServiceImpl implements PullRequestService { - @Override - public void commentOnPullRequest(String payload) { - // Implementation for commenting on a pull request - // This could involve using GitHub's API to post a comment - // based on the provided payload. - } - - @Override - public void validatePullRequest(String payload) { - // Implementation for validating a pull request - // This could involve checking the status of the pull request - // or ensuring it meets certain criteria. - } - - @Override - public void mergePullRequest(String payload) { - // Implementation for merging a pull request - // This could involve using GitHub's API to merge the pull request - // based on the provided payload. - } - - @Override - public String fetchPullRequestInfo(String pullRequestId) { - // Implementation for fetching pull request information - // This could involve using GitHub's API to retrieve details - // about the specified pull request. - return null; - } -}