chore: add workflows + license

This commit is contained in:
Stevan Freeborn
2025-02-09 13:53:24 -06:00
parent 81eba501d0
commit 79f603c63d
7 changed files with 198 additions and 1 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Deploy
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- '.github/**'
- '.gitignore'
- '.editorconfig'
- 'LICENSE.md'
- '**/*/README.md'
- '**/*/Dockerfile'
jobs:
build:
name: Build and push Docker image
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create version tag
id: version
run: echo "version=$(date +%Y.%m.%d.%H%M%S)-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and push image
working-directory: src/AltGen.API
run: |
TAG=${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ steps.version.outputs.version }}
docker build -t $TAG .
docker push $TAG
deploy:
name: Deploy to server
runs-on: ubuntu-latest
needs: build
steps:
- name: Run image on server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
docker stop api.altgen.stevanfreeborn.com
docker rm api.altgen.stevanfreeborn.com
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ needs.build.outputs.version }}
docker run -d -p 7778:8080 --name api.altgen.stevanfreeborn.com ${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ needs.build.outputs.version }}
+56
View File
@@ -0,0 +1,56 @@
name: Pull Request
defaults:
run:
working-directory: ./src
on:
workflow_dispatch:
pull_request:
paths:
- src/AltGen.API/**
- src/AltGen.API.Tests/**
branches:
- main
jobs:
format:
name: Run dotnet format
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x.x
- name: Format project
run: dotnet format --verbosity normal
- name: Commit Changes
run: |
git config user.name "GitHub Actions"
git config user.email "<>"
if [[ $(git status --porcelain) ]]; then
git add .
git commit -m "chore: format fixes [skip ci]"
git fetch origin
git pull --rebase origin ${{ github.head_ref }}
git push origin HEAD:${{ github.head_ref }}
fi
build_and_test:
name: Run tests
needs: format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x.x
- name: Restore dependencies
run: dotnet restore
- name: Build project
run: dotnet build --no-restore
- name: Test project
run: dotnet test
+21
View File
@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2025 Stevan Freeborn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2
View File
@@ -0,0 +1,2 @@
appsettings*.json
Dockerfile
+17
View File
@@ -0,0 +1,17 @@
# setup stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS setup-stage
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . .
# build stage
FROM setup-stage AS build-stage
RUN dotnet publish -c Release -o dist
# production stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS production-stage
WORKDIR /app
COPY --from=build-stage /app/dist ./
EXPOSE 8080
ENTRYPOINT ["dotnet", "AltGen.API.dll"]
@@ -4,7 +4,8 @@ class GeminiService(HttpClient httpClient) : IGeminiService
{
const string BaseUri = "https://generativelanguage.googleapis.com/v1beta/models";
const string Method = "generateContent";
const string ModelId = "gemini-1.5-flash";
// TODO: Maybe this is configurable by the user via request
const string ModelId = "gemini-2.0-flash";
const string ApiKeyQueryKey = "key";
// TODO: Use Lazy<T> to load the prompt resource
@@ -52,6 +53,8 @@ class GeminiService(HttpClient httpClient) : IGeminiService
var response = await _httpClient.PostAsJsonAsync(requestUri, request, Options);
var content = await response.Content.ReadAsStringAsync();
// TODO: Propogate the error response from gemini in the exception
// we throw here. Need to figure out how gemini returns errors
if (response.IsSuccessStatusCode is false)
{
throw new AltGenException("Failed to generate alt text.");
+43
View File
@@ -1,3 +1,6 @@
using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
@@ -7,6 +10,41 @@ builder.Services.AddHttpClient<IGeminiService, GeminiService>();
builder.Services.AddSingleton<IAltTextProviderFactory, AltTextProviderFactory>();
builder.Services.AddKeyedSingleton<IAltTextProvider, GeminiAltTextProvider>(LLMProvider.Gemini);
builder.Services.AddRateLimiter(
static o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.OnRejected = static (context, rateLimitInfo) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
var retryDate = DateTime.UtcNow.Add(retryAfter);
context.HttpContext.Response.Headers.Append(
"x-retry-after",
retryDate.ToString("o", CultureInfo.InvariantCulture)
);
}
return ValueTask.CompletedTask;
};
o.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
static ctx => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ctx.Request.Headers["X-Forwarded-For"].ToString()
?? ctx.Connection.RemoteIpAddress?.ToString()
?? "unknown",
factory: static partition => new FixedWindowRateLimiterOptions()
{
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromHours(1)
}
)
);
}
);
var app = builder.Build();
if (app.Environment.IsDevelopment())
@@ -14,6 +52,11 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi();
}
if (app.Environment.IsProduction())
{
app.UseRateLimiter();
}
app.UseHttpsRedirection();
app.UseStatusCodePages();