Skip to content
Open
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
113 changes: 86 additions & 27 deletions .github/workflows/ci-cd-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,115 @@ name: CI/CD Pipeline

on:
push:
branches: [ main ]
branches:
- main
- master
pull_request:
branches: [ main ]
branches:
- main
- master
workflow_dispatch: {}

permissions:
contents: read
packages: write
id-token: write
statuses: write

env:
APP_IMAGE: ghcr.io/hackandhack/ai-talk-model

jobs:
build:
runs-on: ubuntu-latest

outputs:
sha: ${{ steps.get_sha.outputs.sha }}
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
distribution: 'temurin'
cache: maven

- name: Build with Maven
run: mvn clean verify --batch-mode
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Get short SHA
id: get_sha
run: echo "::set-output name=sha::$(git rev-parse --short HEAD)"

- name: Build JAR
run: mvn package -DskipTests --batch-mode
- name: Build (Maven)
run: mvn -B -DskipTests clean package

docker:
test:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Run tests
run: mvn test

docker-build-and-push:
needs: [build, test]
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Hugging Face
run: echo ${{ secrets.HF_TOKEN }} | huggingface-cli login --token-stdin || echo 'HF_TOKEN not set, skipping login'
- name: Build and push Docker image
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
run: |
docker buildx build \
--build-arg HF_TOKEN="$HF_TOKEN" \
--tag $APP_IMAGE:latest \
--tag $APP_IMAGE:${{ github.sha }} \
--push .

- name: Build and Push Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Output image
run: echo "Image pushed: $APP_IMAGE:latest and $APP_IMAGE:${{ github.sha }}"
59 changes: 59 additions & 0 deletions LOCAL_DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
LOCAL DEVELOPMENT

This file describes how to run the application and the monitoring stack locally using docker compose.

Preparations
- Create a file named `.env` in the repo root (do NOT commit it). Add:

HF_TOKEN=your_huggingface_token
OPENAI_API_KEY=your_openai_api_key (optional, only if using openai provider)

- If the image is private on GHCR, login locally:

echo <PERSONAL_ACCESS_TOKEN> | docker login ghcr.io -u <github-username> --password-stdin

Running locally
- Build and run using Maven wrapper:

./mvnw spring-boot:run

- Or build jar and run:

./mvnw -DskipTests package
java -jar target/*.jar

- Start the stack (compose) if you want metrics and alerting:

docker compose up -d

- Services:
- App (Spring Boot): http://localhost:8080 (Actuator: /actuator/health, /actuator/prometheus)
- Assistant endpoint: POST http://localhost:8080/api/assistant
- Alias endpoint: POST http://localhost:8080/api/ai/chat
- Prometheus: http://localhost:9090
- Alertmanager: http://localhost:9093

Example POST to assistant (Hugging Face Grok 4-3 is default):

curl -s -X POST http://localhost:8080/api/assistant \
-H "Content-Type: application/json" \
-d '{"prompt":"Say hello","provider":"hf"}'

Or to the alias path expected by your tests:

curl -s -X POST http://localhost:8080/api/ai/chat \
-H "Content-Type: application/json" \
-d '{"prompt":"Say hello","provider":"hf"}'

Repository secrets (add these in GitHub: Settings → Secrets and variables → Actions)
- HF_TOKEN — Hugging Face token
- OPENAI_API_KEY — OpenAI API key (optional)
- SMTP_USER — SMTP username (e.g. your Gmail address)
- SMTP_PASSWORD — Gmail App Password or other SMTP password
- SMTP_HOST — smtp.gmail.com
- SMTP_PORT — 587
- EMAIL_FROM — no-reply@yourdomain

Notes
- The CI workflow references these secrets but does NOT store values. Add them in the repo before merging.
- The Docker build in CI uses the repository GITHUB_TOKEN to authenticate to ghcr.io. Ensure the repository allows packages: write for workflows if your organization blocks default tokens.
3 changes: 3 additions & 0 deletions TRIGGER.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
chore(ci): trigger workflow - small README note

This commit was created by the assistant to trigger the CI pipeline and validate the recent changes.
15 changes: 15 additions & 0 deletions alertmanager/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
global:
resolve_timeout: 5m

route:
receiver: 'email-notifications'

receivers:
- name: 'email-notifications'
email_configs:
- to: 'ops@example.com' # <-- Replace with real address
from: 'no-reply@example.com'
smarthost: 'smtp.gmail.com:587'
auth_username: 'your-smtp-user'
auth_password: 'your-smtp-password'
require_tls: true
31 changes: 31 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "3.8"

services:
app:
image: ghcr.io/hackandhack/ai-talk-model:latest
restart: unless-stopped
env_file: .env
environment:
SPRING_PROFILES_ACTIVE: prod
HF_TOKEN: ${HF_TOKEN}
ports:
- "8080:8080"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5

prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"

alertmanager:
image: prom/alertmanager:latest
volumes:
- ./alertmanager/config.yml:/etc/alertmanager/config.yml:ro
ports:
- "9093:9093"
8 changes: 8 additions & 0 deletions prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global:
scrape_interval: 15s

scrape_configs:
- job_name: 'ai-talk-model'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
30 changes: 30 additions & 0 deletions src/main/java/no/hackandhack/assistant/AiChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package no.hackandhack.assistant;

import no.hackandhack.assistant.dto.AssistantRequest;
import no.hackandhack.assistant.dto.AssistantResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/ai")
public class AiChatController {
private final AssistantService service;

public AiChatController(AssistantService service) {
this.service = service;
}

@PostMapping(value = "/chat", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResponseEntity<AssistantResponse>> chat(@RequestBody AssistantRequest request) {
return service.generate(request)
.map(resp -> ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(resp));
}
}