Skip to content
Merged
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
141 changes: 141 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobs:
needs: [build]
permissions:
packages: write
outputs:
image-tag: ${{ steps.primary-tag.outputs.tag }}
steps:
- uses: actions/checkout@v3
- name: Set up JDK
Expand Down Expand Up @@ -114,6 +116,10 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'push' }}
type=raw,value=beta-${{ steps.sha.outputs.short }},enable=${{ github.event_name == 'pull_request' }}

- name: Set primary tag output
id: primary-tag
run: echo "tag=$(echo "${{ steps.meta.outputs.tags }}" | head -1)" >> "$GITHUB_OUTPUT"

- name: Build application image
run: |
PRIMARY_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -1)
Expand All @@ -132,3 +138,138 @@ jobs:
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
build-native-image:
runs-on: ubuntu-latest
needs: [build]
permissions:
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: cache gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Set lowercase owner name
run: echo "OWNER_LC=${OWNER,,}" >> "${GITHUB_ENV}"
env:
OWNER: ${{ github.repository_owner }}
- name: Compute CalVer tag
if: github.event_name == 'push'
id: calver
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TODAY=$(date -u +%Y.%m.%d)

TAGS=$(gh api /users/${{ env.OWNER_LC }}/packages/container/retro-api/versions \
--paginate --jq '.[].metadata.container.tags[]' 2>/dev/null || echo "")

TODAYS_TAGS=$(echo "$TAGS" | grep "^${TODAY}" || true)

if [ -z "$TODAYS_TAGS" ]; then
echo "version=${TODAY}" >> "$GITHUB_OUTPUT"
else
MAX_SEQ=0
for tag in $TODAYS_TAGS; do
if [ "$tag" = "$TODAY" ]; then
MAX_SEQ=$((MAX_SEQ > 0 ? MAX_SEQ : 0))
else
SEQ=$(echo "$tag" | sed "s/^${TODAY}\.//")
if [ "$SEQ" -gt "$MAX_SEQ" ] 2>/dev/null; then
MAX_SEQ=$SEQ
fi
fi
done
NEXT_SEQ=$((MAX_SEQ + 1))
echo "version=${TODAY}.${NEXT_SEQ}" >> "$GITHUB_OUTPUT"
fi

- name: Compute short SHA
id: sha
run: echo "short=$(echo '${{ github.event.pull_request.head.sha || github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ env.OWNER_LC }}/retro-api
tags: |
type=raw,value=${{ steps.calver.outputs.version || 'unused' }}-native,enable=${{ github.event_name == 'push' }}
type=raw,value=native,enable=${{ github.event_name == 'push' }}
type=raw,value=beta-${{ steps.sha.outputs.short }}-native,enable=${{ github.event_name == 'pull_request' }}

- name: Build native application image
run: |
PRIMARY_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -1)
./gradlew bootBuildImage -PbuildNative --imageName "$PRIMARY_TAG"

- name: Tag and push image
run: |
PRIMARY_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -1)
echo "${{ steps.meta.outputs.tags }}" | while read -r TAG; do
if [ "$TAG" != "$PRIMARY_TAG" ]; then
docker tag "$PRIMARY_TAG" "$TAG"
fi
docker push "$TAG"
done
- name: cleanup gradle cache
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
e2e:
runs-on: ubuntu-latest
needs: [build-image]
steps:
- name: Checkout E2E repo
uses: actions/checkout@v4
with:
repository: LowBudgetMan/retro-tests

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

- name: Start E2E stack
run: |
RETRO_API_IMAGE=${{ needs.build-image.outputs.image-tag }} \
docker compose up -d --wait --wait-timeout 120

- name: Run E2E tests
run: |
docker run --network host \
-v ${{ github.workspace }}/playwright-report:/tests/playwright-report \
ghcr.io/lowbudgetman/retro-e2e:latest

- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-report
path: playwright-report/

- name: Dump logs on failure
if: failure()
run: docker compose logs

- name: Tear down
if: always()
run: docker compose down -v
2 changes: 2 additions & 0 deletions KeycloakDockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM quay.io/keycloak/keycloak:26.6.0
COPY keycloak-realm-data/realm-export.json /opt/keycloak/data/import/
14 changes: 12 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'org.springframework.boot' version '3.5.8'
id 'io.spring.dependency-management' version '1.1.7'
// id 'org.graalvm.buildtools.native' version '0.11.3'
id 'org.graalvm.buildtools.native' version '0.11.3' apply false
}

if (project.hasProperty('buildNative')) {
apply plugin: 'org.graalvm.buildtools.native'
}

group = 'io.nickreuter'
Expand Down Expand Up @@ -48,4 +52,10 @@ tasks.named('test') {

bootBuildImage {
imageName = 'retro-api'
buildCache {
volume { name = 'retro-api-build-cache' }
}
launchCache {
volume { name = 'retro-api-launch-cache' }
}
}
16 changes: 9 additions & 7 deletions docker-compose.full.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
db:
image: postgres:13
image: postgres:16
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: retroapi
Expand All @@ -11,12 +11,14 @@ services:
- postgres_data:/var/lib/postgresql/data

auth-server:
image: quay.io/keycloak/keycloak:22.0.0
image: quay.io/keycloak/keycloak:26.6.0
environment:
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin'
KC_BOOTSTRAP_ADMIN_USERNAME: 'admin'
KC_BOOTSTRAP_ADMIN_PASSWORD: 'admin'
KC_HOSTNAME: "http://localhost:${KEYCLOAK_PORT:-8010}"
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
ports:
- '8010:8080'
- "${KEYCLOAK_PORT:-8010}:8080"
volumes:
- ./keycloak-realm-data:/opt/keycloak/data/import
command: start-dev --import-realm
Expand All @@ -34,9 +36,9 @@ services:
- '25672:25672'

api:
image: ghcr.io/lowbudgetman/retro-api:latest
image: ${RETRO_API_IMAGE:-ghcr.io/lowbudgetman/retro-api:latest}
ports:
- "8080:8080"
- "${API_PORT:-8080}:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
depends_on:
Expand Down
12 changes: 6 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
services:
db:
image: postgres:13
image: postgres:16
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: retroapi
POSTGRES_DB: retroapi
ports:
- "5432:5432"
auth-server:
image: quay.io/keycloak/keycloak:22.0.0
image: quay.io/keycloak/keycloak:26.6.0
environment:
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin'
KC_BOOTSTRAP_ADMIN_USERNAME: 'admin'
KC_BOOTSTRAP_ADMIN_PASSWORD: 'admin'
ports:
- '8010:8080'
volumes:
- ./keycloak-realm-data:/opt/keycloak/data/import
command: start-dev --import-realm
rabbitmq:
build:
build:
context: ./
dockerfile: RabbitMqDockerfile
ports:
Expand All @@ -27,4 +27,4 @@ services:
- '61613:61613'
- '1883:1883'
- '15692:15692'
- '25672:25672'
- '25672:25672'
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.nickreuter.retroapi.configuration;

import io.nickreuter.retroapi.notification.EventType;
import io.nickreuter.retroapi.notification.event.BaseEvent;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;

import jakarta.persistence.Entity;

@Configuration
@ImportRuntimeHints(JacksonNativeHints.Registrar.class)
class JacksonNativeHints {

private static final String BASE_PACKAGE = "io.nickreuter.retroapi";
private static final MemberCategory[] JACKSON_CATEGORIES = {
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS,
MemberCategory.DECLARED_FIELDS
};

static class Registrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(BaseEvent.class));
scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));

for (BeanDefinition bd : scanner.findCandidateComponents(BASE_PACKAGE)) {
try {
hints.reflection().registerType(
Class.forName(bd.getBeanClassName()),
JACKSON_CATEGORIES
);
} catch (ClassNotFoundException ignored) {
}
}

hints.reflection().registerType(EventType.class, JACKSON_CATEGORIES);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.nickreuter.retroapi.configuration;

import liquibase.datatype.LiquibaseDataType;
import liquibase.snapshot.jvm.JdbcSnapshotGenerator;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.type.filter.AssignableTypeFilter;

@Configuration
@ImportRuntimeHints(LiquibaseNativeHints.Registrar.class)
class LiquibaseNativeHints {

private static final MemberCategory[] CATEGORIES = {
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS,
MemberCategory.DECLARED_FIELDS
};

static class Registrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("db/*");
hints.resources().registerPattern("www.liquibase.org/*");
hints.resources().registerPattern("liquibase/*");

var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(JdbcSnapshotGenerator.class));
scanner.addIncludeFilter(new AssignableTypeFilter(LiquibaseDataType.class));

for (BeanDefinition bd : scanner.findCandidateComponents("liquibase")) {
try {
hints.reflection().registerType(Class.forName(bd.getBeanClassName()), CATEGORIES);
} catch (ClassNotFoundException ignored) {
}
}
}
}
}
Loading
Loading