1
0

ci(notification-service): add CI/CD pipeline with Docker, SSH deploy and API credentials

Add GitHub Actions workflow: Maven test on every push, Docker build & push to GHCR
on main, SSH deploy with runtime injection of TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
AIR_QUALITY_API_URL, API_USERNAME and API_PASSWORD secrets.
Add Basic Auth support in AirQualityService using API_USERNAME/API_PASSWORD env vars.

Closes #19
This commit is contained in:
khalil-bot
2026-05-28 15:56:28 +02:00
parent 1621465e95
commit 1e8d7db8e6
4 changed files with 151 additions and 7 deletions

View File

@@ -0,0 +1,122 @@
name: Notification Service CI/CD
on:
workflow_dispatch:
push:
paths:
- notification_service/**
- .github/workflows/notification-service.yml
pull_request:
branches: [main]
paths:
- notification_service/**
- .github/workflows/notification-service.yml
jobs:
# ── 1. Build & test ──────────────────────────────────────────────────────────
ci:
name: Build & test
runs-on: ubuntu-latest
defaults:
run:
working-directory: notification_service
steps:
- uses: actions/checkout@v4
- name: Setup Java 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
cache: maven
- name: Run tests
run: mvn verify -q
# ── 2. Docker build & push ───────────────────────────────────────────────────
docker:
name: Docker build & push
runs-on: ubuntu-latest
needs: ci
if: github.ref == 'refs/heads/main'
outputs:
sha_tag: ${{ steps.tag.outputs.sha }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Compute image tags
id: tag
run: |
OWNER="${{ github.repository_owner }}"
IMAGE_LC="ghcr.io/${OWNER,,}/notification-service"
echo "image=${IMAGE_LC}" >> $GITHUB_OUTPUT
echo "sha=${IMAGE_LC}:${{ github.sha }}" >> $GITHUB_OUTPUT
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "extra=${IMAGE_LC}:latest" >> $GITHUB_OUTPUT
else
BRANCH="${{ github.head_ref || github.ref_name }}"
echo "extra=${IMAGE_LC}:branch-${BRANCH//\//-}" >> $GITHUB_OUTPUT
fi
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push
uses: docker/build-push-action@v5
with:
context: notification_service
push: true
tags: |
${{ steps.tag.outputs.sha }}
${{ steps.tag.outputs.extra }}
# ── 3. Deploy to physical server (SSH) ───────────────────────────────────────
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: docker
if: github.ref == 'refs/heads/main'
steps:
- name: SSH deploy
env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_CERT: ${{ secrets.SSH_CERTIFICATE }}
SHA_TAG: ${{ needs.docker.outputs.sha_tag }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
AIR_QUALITY_API_URL: ${{ secrets.AIR_QUALITY_API_URL }}
API_USERNAME: ${{ secrets.API_USERNAME }}
API_PASSWORD: ${{ secrets.API_PASSWORD }}
run: |
echo "$SSH_KEY" > /tmp/deploy_key
echo "$SSH_CERT" > /tmp/deploy_key-cert.pub
chmod 600 /tmp/deploy_key
ssh -i /tmp/deploy_key \
-p ${{ secrets.SSH_PORT }} \
-o StrictHostKeyChecking=no \
-o IdentitiesOnly=yes \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
"sudo -iu e << 'EOF'
docker pull $SHA_TAG
docker stop notification-service 2>/dev/null || true
docker rm notification-service 2>/dev/null || true
docker run -d --name notification-service --restart unless-stopped \
-e TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN \
-e TELEGRAM_CHAT_ID=$TELEGRAM_CHAT_ID \
-e AIR_QUALITY_API_URL=$AIR_QUALITY_API_URL \
-e API_USERNAME=$API_USERNAME \
-e API_PASSWORD=$API_PASSWORD \
$SHA_TAG
EOF"
rm /tmp/deploy_key /tmp/deploy_key-cert.pub

View File

@@ -28,15 +28,23 @@ public class NotificationProperties {
}
public static class AirQuality {
private String apiUrl = "http://localhost:8080";
private long pollIntervalMs = 60_000;
private Thresholds thresholds = new Thresholds();
private String apiUrl = "http://localhost:8080";
private String apiUsername;
private String apiPassword;
private long pollIntervalMs = 60_000;
private Thresholds thresholds = new Thresholds();
private String alertFromLevel = "poor";
private boolean mockMode = false;
public String getApiUrl() { return apiUrl; }
public String getApiUrl() { return apiUrl; }
public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; }
public String getApiUsername() { return apiUsername; }
public void setApiUsername(String apiUsername) { this.apiUsername = apiUsername; }
public String getApiPassword() { return apiPassword; }
public void setApiPassword(String apiPassword) { this.apiPassword = apiPassword; }
public long getPollIntervalMs() { return pollIntervalMs; }
public void setPollIntervalMs(long pollIntervalMs) { this.pollIntervalMs = pollIntervalMs; }

View File

@@ -6,10 +6,13 @@ import ch.hesso.pi.notification.model.SensorReading;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import java.util.Base64;
import java.util.List;
@Service
@@ -30,10 +33,19 @@ public class AirQualityService {
* Returns an empty list on error so the scheduler can continue safely.
*/
public List<SensorReading> fetchLatestReadings() {
String url = props.getAirQuality().getApiUrl() + "/sensors/latest";
String url = props.getAirQuality().getApiUrl() + "/sensors/latest";
String username = props.getAirQuality().getApiUsername();
String password = props.getAirQuality().getApiPassword();
try {
List<SensorReading> readings = restClient.get()
.uri(url)
var request = restClient.get().uri(url);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
String encoded = Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
request = request.header(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
}
List<SensorReading> readings = request
.retrieve()
.body(new ParameterizedTypeReference<>() {});
return readings != null ? readings : List.of();

View File

@@ -17,6 +17,8 @@ notification:
chat-id: ${TELEGRAM_CHAT_ID}
air-quality:
api-url: ${AIR_QUALITY_API_URL:http://localhost:8080}
api-username: ${API_USERNAME:}
api-password: ${API_PASSWORD:}
poll-interval-ms: ${POLL_INTERVAL_MS:60000}
# Levels matching the UI co2-levels.config.ts thresholds
thresholds: