diff --git a/.github/workflows/build-and-release.yaml b/.github/workflows/build-and-release.yaml
new file mode 100644
index 0000000..d119a32
--- /dev/null
+++ b/.github/workflows/build-and-release.yaml
@@ -0,0 +1,90 @@
+name: Build and Release Auth Chart
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+env:
+ IMAGE_NAME: auth
+ REGISTRY: ghcr.io
+ OCI_REPO: ghcr.io/abstractize
+ PROJECT_NAME: todo
+
+jobs:
+ docker-build:
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set lowercase env vars
+ id: set_env
+ run: |
+ USERNAME=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')
+ REPO_NAME=$(echo "${{ github.repository }}" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]')
+ echo "USERNAME=$USERNAME" >> $GITHUB_ENV
+ echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
+ echo "USERNAME=$USERNAME" >> $GITHUB_OUTPUT
+ echo "REPO_NAME=$REPO_NAME" >> $GITHUB_OUTPUT
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ file: ./Dockerfile
+ push: true
+ build-args: |
+ GITHUB_USERNAME=${{ github.repository_owner }}
+ secrets: |
+ GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
+ tags: |
+ ${{ env.OCI_REPO }}/docker-images/${{ env.PROJECT_NAME }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.OCI_REPO }}/docker-images/${{ env.PROJECT_NAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
+
+ helm-release:
+ runs-on: ubuntu-latest
+ needs: docker-build
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Helm
+ uses: azure/setup-helm@v4
+ with:
+ version: v3.14.0
+
+ - name: Update Chart.yaml version and appVersion
+ run: |
+ sudo apt-get update && sudo apt-get install -y yq
+ VERSION="0.1.${GITHUB_RUN_NUMBER}"
+ yq -y -i ".appVersion = \"${GITHUB_SHA}\" | .version = \"$VERSION\"" .helm/Chart.yaml
+
+ - name: Lint Helm chart
+ run: helm lint .helm
+
+ - name: Package Helm chart
+ run: helm package .helm --destination .helm-dist
+
+ - name: Push Helm chart to GHCR (OCI)
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
+ CHART_FILE=$(ls .helm-dist/*.tgz)
+ helm push $CHART_FILE oci://${{ env.OCI_REPO }}/helm-charts/${{ env.PROJECT_NAME }}
\ No newline at end of file
diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml
new file mode 100644
index 0000000..1bf6c3f
--- /dev/null
+++ b/.github/workflows/pr-validation.yaml
@@ -0,0 +1,51 @@
+name: PR Validation
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ helm-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Helm
+ uses: azure/setup-helm@v4
+ with:
+ version: v3.14.0
+
+ - name: Helm lint
+ run: helm lint .helm
+
+ docker-build-test:
+ permissions:
+ contents: read
+ packages: write
+
+ runs-on: ubuntu-latest
+ needs: helm-lint
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build Docker image with secret
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: Dockerfile
+ push: false
+ load: true
+ build-args: |
+ GITHUB_USERNAME=${{ github.repository_owner }}
+ secrets: |
+ GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
+ tags: test-image:pr-${{ github.event.pull_request.number }}
+
+ - name: Run container test
+ run: docker run --rm --entrypoint sleep test-image:pr-${{ github.event.pull_request.number }} 5
\ No newline at end of file
diff --git a/.helm/.helmignore b/.helm/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/.helm/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/.helm/Chart.yaml b/.helm/Chart.yaml
new file mode 100644
index 0000000..74b8dbd
--- /dev/null
+++ b/.helm/Chart.yaml
@@ -0,0 +1,7 @@
+apiVersion: v2
+name: auth-service
+description: Helm chart for the Auth Service of the TODO app
+type: application
+version: 0.1.0
+appVersion: "latest"
+icon: https://raw.githubusercontent.com/Abstractize/todo.auth/main/.helm/icons/auth.svg
diff --git a/.helm/icons/auth.svg b/.helm/icons/auth.svg
new file mode 100644
index 0000000..7fecf33
--- /dev/null
+++ b/.helm/icons/auth.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/.helm/templates/_helpers.tpl b/.helm/templates/_helpers.tpl
new file mode 100644
index 0000000..e2664fa
--- /dev/null
+++ b/.helm/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "auth-service.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "auth-service.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "auth-service.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "auth-service.labels" -}}
+helm.sh/chart: {{ include "auth-service.chart" . }}
+{{ include "auth-service.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "auth-service.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "auth-service.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "auth-service.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "auth-service.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/.helm/templates/deployment.yaml b/.helm/templates/deployment.yaml
new file mode 100644
index 0000000..d330de4
--- /dev/null
+++ b/.helm/templates/deployment.yaml
@@ -0,0 +1,30 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "auth-service.fullname" . }}
+ labels:
+ app: {{ include "auth-service.name" . }}
+ chart: {{ include "auth-service.chart" . }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ app: {{ include "auth-service.name" . }}
+ template:
+ metadata:
+ labels:
+ app: {{ include "auth-service.name" . }}
+ spec:
+ containers:
+ - name: {{ include "auth-service.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.service.port }}
+ envFrom:
+ - configMapRef:
+ name: {{ .Values.configMapName }}
+ - secretRef:
+ name: {{ .Values.secretName }}
+ resources:
+ {{- toYaml .Values.resources | nindent 12 }}
\ No newline at end of file
diff --git a/.helm/templates/hpa.yaml b/.helm/templates/hpa.yaml
new file mode 100644
index 0000000..80bdc04
--- /dev/null
+++ b/.helm/templates/hpa.yaml
@@ -0,0 +1,20 @@
+{{- if .Values.hpa.enabled }}
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: {{ include "auth-service.fullname" . }}
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: {{ include "auth-service.fullname" . }}
+ minReplicas: {{ .Values.hpa.minReplicas }}
+ maxReplicas: {{ .Values.hpa.maxReplicas }}
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
+{{- end }}
\ No newline at end of file
diff --git a/.helm/templates/service.yaml b/.helm/templates/service.yaml
new file mode 100644
index 0000000..3f21a35
--- /dev/null
+++ b/.helm/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "auth-service.fullname" . }}
+ labels:
+ app: {{ include "auth-service.name" . }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: {{ .Values.service.port }}
+ protocol: TCP
+ name: http
+ selector:
+ app: {{ include "auth-service.name" . }}
\ No newline at end of file
diff --git a/.helm/templates/tests/test-connection.yaml b/.helm/templates/tests/test-connection.yaml
new file mode 100644
index 0000000..782482a
--- /dev/null
+++ b/.helm/templates/tests/test-connection.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: "{{ include "auth-service.fullname" . }}-test-connection"
+ labels:
+ app: {{ include "auth-service.name" . }}
+ annotations:
+ "helm.sh/hook": test
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ containers:
+ - name: wget
+ image: busybox:1.36
+ command: ['wget']
+ args: ['-qO-', 'http://{{ include "auth-service.fullname" . }}:{{ .Values.service.port }}']
+ restartPolicy: Never
\ No newline at end of file
diff --git a/.helm/values.yaml b/.helm/values.yaml
new file mode 100644
index 0000000..f507f6e
--- /dev/null
+++ b/.helm/values.yaml
@@ -0,0 +1,27 @@
+replicaCount: 1
+
+image:
+ repository: ghcr.io/abstractize/docker-images/todo/auth
+ tag: latest
+ pullPolicy: IfNotPresent
+
+service:
+ type: ClusterIP
+ port: 8080
+
+resources: {}
+
+hpa:
+ enabled: true
+ minReplicas: 1
+ maxReplicas: 3
+ targetCPUUtilizationPercentage: 80
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
+configMapName: infra-config
+secretName: infra-secrets
diff --git a/Dockerfile b/Dockerfile
index 77f7bec..605e97c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,16 +1,54 @@
-FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+# Stage 1: Base runtime image (minimal & secure)
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS base
WORKDIR /app
+
+ENV DOTNET_RUNNING_IN_CONTAINER=true \
+ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
+ ASPNETCORE_URLS=http://+:8080
+
EXPOSE 8080
+# Labels for image metadata (useful for CI/CD and Helm)
+ARG VERSION
+LABEL org.opencontainers.image.source="https://github.com/Abstractize/todo.auth-service"
+LABEL org.opencontainers.image.version=$VERSION
+
+# Stage 2: Build and publish
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+
+ARG GITHUB_USERNAME
WORKDIR /src
+
COPY src .
WORKDIR /src/API
-RUN dotnet restore
+RUN --mount=type=secret,id=GITHUB_TOKEN bash -c '\
+ TOKEN=$(cat /run/secrets/GITHUB_TOKEN) && \
+ echo "" > nuget.config && \
+ echo "" >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo " " >> nuget.config && \
+ echo "" >> nuget.config \
+ '
+
+RUN dotnet restore --configfile /src/API/nuget.config
RUN dotnet publish -c Release -o /app/publish
+# Stage 3: Final runtime image
FROM base AS final
WORKDIR /app
+
+RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
+USER appuser
+
COPY --from=build /app/publish .
+
ENTRYPOINT ["dotnet", "API.dll"]
\ No newline at end of file