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
128 changes: 98 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,74 @@ jobs:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Build production and baseline images
shell: bash
run: |
docker build -t coliee-backend:prod-ci -f ./backend/Coliee.API/Dockerfile ./backend/Coliee.API
docker build -t coliee-backend:dev-baseline -f ./backend/Coliee.API/Dockerfile.dev ./backend/Coliee.API
docker build \
-t coliee-frontend:prod-ci \
--build-arg VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }} \
-f ./frontend/Dockerfile \
./frontend
docker build -t coliee-frontend:dev-baseline -f ./frontend/Dockerfile.dev ./frontend
- name: Compare image sizes
shell: bash
run: |
backend_prod_size=$(docker image inspect coliee-backend:prod-ci --format '{{.Size}}')
backend_dev_size=$(docker image inspect coliee-backend:dev-baseline --format '{{.Size}}')
frontend_prod_size=$(docker image inspect coliee-frontend:prod-ci --format '{{.Size}}')
frontend_dev_size=$(docker image inspect coliee-frontend:dev-baseline --format '{{.Size}}')

echo "Backend dev size: ${backend_dev_size}"
echo "Backend prod size: ${backend_prod_size}"
echo "Frontend dev size: ${frontend_dev_size}"
echo "Frontend prod size: ${frontend_prod_size}"

if [ "$backend_prod_size" -ge "$backend_dev_size" ]; then
echo "Backend production image is not smaller than the dev baseline."
exit 1
fi

if [ "$frontend_prod_size" -ge "$frontend_dev_size" ]; then
echo "Frontend production image is not smaller than the dev baseline."
exit 1
fi
- name: Run Trivy on backend image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'coliee-backend:prod-ci'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL'
- name: Run Trivy on frontend image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'coliee-frontend:prod-ci'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL'
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & push backend image
uses: docker/build-push-action@v5
with:
context: ./backend/Coliee.API
file: ./backend/Coliee.API/Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:latest
${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:${{ github.sha }}
- name: Build & push frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
build-args: |
VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }}
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:latest
${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:${{ github.sha }}
- name: Tag & push backend image
shell: bash
run: |
docker tag coliee-backend:prod-ci "${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:latest"
docker tag coliee-backend:prod-ci "${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:${{ github.sha }}"
docker push "${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:latest"
docker push "${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:${{ github.sha }}"
- name: Tag & push frontend image
shell: bash
run: |
docker tag coliee-frontend:prod-ci "${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:latest"
docker tag coliee-frontend:prod-ci "${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:${{ github.sha }}"
docker push "${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:latest"
docker push "${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:${{ github.sha }}"

# Docker build test (only on PRs and non-main pushes)
docker_build_test:
Expand All @@ -103,27 +146,52 @@ jobs:
if: github.ref != 'refs/heads/main' || github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Build Docker images
run: docker compose build
- name: Build production and baseline images
shell: bash
run: |
docker build -t coliee-backend:prod-ci -f ./backend/Coliee.API/Dockerfile ./backend/Coliee.API
docker build -t coliee-backend:dev-baseline -f ./backend/Coliee.API/Dockerfile.dev ./backend/Coliee.API
docker build \
-t coliee-frontend:prod-ci \
--build-arg VITE_GOOGLE_CLIENT_ID=ci-placeholder \
-f ./frontend/Dockerfile \
./frontend
docker build -t coliee-frontend:dev-baseline -f ./frontend/Dockerfile.dev ./frontend
- name: Compare image sizes
shell: bash
run: |
backend_prod_size=$(docker image inspect coliee-backend:prod-ci --format '{{.Size}}')
backend_dev_size=$(docker image inspect coliee-backend:dev-baseline --format '{{.Size}}')
frontend_prod_size=$(docker image inspect coliee-frontend:prod-ci --format '{{.Size}}')
frontend_dev_size=$(docker image inspect coliee-frontend:dev-baseline --format '{{.Size}}')

trivy-scan:
runs-on: ubuntu-latest
needs: [docker_push]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
echo "Backend dev size: ${backend_dev_size}"
echo "Backend prod size: ${backend_prod_size}"
echo "Frontend dev size: ${frontend_dev_size}"
echo "Frontend prod size: ${frontend_prod_size}"

if [ "$backend_prod_size" -ge "$backend_dev_size" ]; then
echo "Backend production image is not smaller than the dev baseline."
exit 1
fi

if [ "$frontend_prod_size" -ge "$frontend_dev_size" ]; then
echo "Frontend production image is not smaller than the dev baseline."
exit 1
fi
- name: Run Trivy on backend image
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/coliee-backend:latest'
image-ref: 'coliee-backend:prod-ci'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL'
- name: Run Trivy on frontend image
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/coliee-frontend:latest'
image-ref: 'coliee-frontend:prod-ci'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL'
severity: 'CRITICAL'
38 changes: 37 additions & 1 deletion backend/Coliee.API.Tests/Services/CanvasGraphServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ public TestCanvasGraphService(
_llmService = new LlmService(
canvasRepository,
llmRepository,
(IUserRepository)canvasRepository,
encryptionService,
providerClients,
TimeProvider.System,
Expand Down Expand Up @@ -515,7 +516,7 @@ public Task TestApiKeyAsync(string apiKey, CancellationToken cancellationToken =
}
}

private sealed class InMemoryGraphStore : ICanvasRepository, ILlmRepository
private sealed class InMemoryGraphStore : ICanvasRepository, ILlmRepository, IUserRepository
{
private readonly List<Canvas> _canvases = [];
private readonly List<CanvasNode> _nodes = [];
Expand All @@ -527,6 +528,41 @@ private sealed class InMemoryGraphStore : ICanvasRepository, ILlmRepository
private int _nextConversationId = 1;
private int _nextMessageId = 1;

public Task<User?> GetByIdAsync(int id)
{
if (id <= 0)
return Task.FromResult<User?>(null);

return Task.FromResult<User?>(new User
{
Id = id,
Email = $"user{id}@example.com",
AuthProvider = "local",
CreatedAt = DateTime.UtcNow
});
}

public Task<bool> EmailExistsAsync(string email) => throw new NotImplementedException();
public Task CreateUserAsync(string email, string passwordHash, string? displayName) => throw new NotImplementedException();
public Task CreateGoogleUserAsync(string email, string displayName) => throw new NotImplementedException();
public Task<User?> GetByEmailAsync(string email) => throw new NotImplementedException();
public Task<User?> GetProfileAsync(int userId) => throw new NotImplementedException();
public Task UpdateDisplayNameAsync(int userId, string? displayName) => throw new NotImplementedException();
public Task StoreRefreshTokenAsync(int userId, string token, DateTime expiresAt) => throw new NotImplementedException();
public Task<int?> ValidateRefreshTokenAsync(string token) => throw new NotImplementedException();
public Task DeleteRefreshTokenAsync(string token) => throw new NotImplementedException();
public Task DeleteAllRefreshTokensAsync(int userId) => throw new NotImplementedException();
public Task<string> RotateRefreshTokenAsync(int userId, string oldToken) => throw new NotImplementedException();
public Task RecordFailedLoginAsync(int userId) => throw new NotImplementedException();
public Task ResetFailedLoginAsync(int userId) => throw new NotImplementedException();
public Task UpdatePasswordHashAsync(int userId, string newHash) => throw new NotImplementedException();
public Task<int> CreateOtpAsync(int userId, string otpCode, string purpose, DateTime expiresAtUtc) => throw new NotImplementedException();
public Task<int> CountOtpsCreatedSinceAsync(int userId, string purpose, DateTime sinceUtc) => throw new NotImplementedException();
public Task<int?> ValidateLatestOtpAsync(int userId, string otpCode, string purpose, DateTime utcNow) => throw new NotImplementedException();
public Task MarkEmailVerifiedAsync(int userId) => throw new NotImplementedException();
public Task<string> CreatePasswordResetTokenAsync(int userId) => throw new NotImplementedException();
public Task<int?> ValidatePasswordResetTokenAsync(string token) => throw new NotImplementedException();

public Task<Canvas> CreateCanvasAsync(int userId, string title)
{
var now = DateTime.UtcNow;
Expand Down
Loading
Loading