diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ca73a2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/bin +**/obj +**/node_modules +.git +.github +deploy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da6110a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: ci + +# Pull requests: build, test and build the image without pushing. +# Push to master (direct or merged PR) or a manual run on master: release. +# GitVersion picks the next version (patch by default, "+semver: minor|major" in a commit message for more), +# the image is pushed as and latest, and the git tag v is pushed so the next release counts from it. +# Argo CD Image Updater (infrastructure repo) deploys the newest ; nothing is committed back. +on: + push: + branches: [master] + paths-ignore: [deploy/**, '**.md'] + pull_request: + branches: [master] + workflow_dispatch: + +# Releases run one at a time, so each one computes its version after the previous one pushed its tag. +concurrency: + group: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'release' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + IMAGE: ghcr.io/greboh/hearth-api + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + # global.json (SDK pin, Microsoft Testing Platform runner) lives in src/ + working-directory: src + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - run: dotnet build Hearth.slnx -c Release + - name: Run tests + run: | + mapfile -t projects < <(git ls-files '*Tests.csproj') + if [ ${#projects[@]} -eq 0 ]; then + echo "No test projects found" + exit 0 + fi + for project in "${projects[@]}"; do + dotnet test "$project" -c Release + done + + image: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + env: + RELEASE: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gittools/actions/gitversion/setup@v4 + with: + versionSpec: 6.x + - id: gitversion + uses: gittools/actions/gitversion/execute@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + if: env.RELEASE == 'true' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + push: ${{ env.RELEASE == 'true' }} + tags: | + ${{ env.IMAGE }}:${{ steps.gitversion.outputs.majorMinorPatch }} + ${{ env.IMAGE }}:latest + cache-from: type=gha,scope=api + cache-to: type=gha,mode=max,scope=api + - name: Push release tag + if: env.RELEASE == 'true' + env: + TAG: v${{ steps.gitversion.outputs.majorMinorPatch }} + run: | + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null; then + echo "$TAG already exists" + exit 0 + fi + git tag "$TAG" "$GITHUB_SHA" + git push origin "$TAG" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8c9907 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY src/ . +RUN dotnet restore Hosts/Hearth.Api/Hearth.Api.csproj +RUN dotnet publish Hosts/Hearth.Api/Hearth.Api.csproj -c Release --no-restore -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +USER app +ENTRYPOINT ["dotnet", "Hearth.Api.dll"] diff --git a/GitVersion.yaml b/GitVersion.yaml new file mode 100644 index 0000000..dc94b89 --- /dev/null +++ b/GitVersion.yaml @@ -0,0 +1,6 @@ +mode: ContinuousDeployment +branches: + main: + regex: ^master$ + label: ci + increment: Patch diff --git a/deploy/api/deployment.yaml b/deploy/api/deployment.yaml new file mode 100644 index 0000000..323e8c4 --- /dev/null +++ b/deploy/api/deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hearth-api + labels: + app: hearth-api +spec: + # One replica with Recreate: schema upgrades and scheduled syncs must run in one process at a time. + # Do not scale or switch to RollingUpdate before the worker split (Obsidian: Topics/Architecture, Singleton work). + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: hearth-api + template: + metadata: + labels: + app: hearth-api + spec: + containers: + - name: api + image: ghcr.io/greboh/hearth-api + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /health + port: http + livenessProbe: + httpGet: + path: /alive + port: http + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 256Mi diff --git a/deploy/api/ingress.yaml b/deploy/api/ingress.yaml new file mode 100644 index 0000000..f89ab50 --- /dev/null +++ b/deploy/api/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hearth-api + annotations: + traefik.ingress.kubernetes.io/router.middlewares: services-hearth-api-strip@kubernetescrd + # Owns the certificate until the web ingress exists; the web ingress then reuses hearth-greboh-dev-tls. + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - hearth.greboh.dev + secretName: hearth-greboh-dev-tls + rules: + - host: hearth.greboh.dev + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: hearth-api + port: + name: http diff --git a/deploy/api/kustomization.yaml b/deploy/api/kustomization.yaml new file mode 100644 index 0000000..20d597e --- /dev/null +++ b/deploy/api/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: services +# No image tag here: Argo CD Image Updater (infrastructure repo) sets the newest released version on the Application. +resources: + - deployment.yaml + - service.yaml + - middleware.yaml + - ingress.yaml diff --git a/deploy/api/middleware.yaml b/deploy/api/middleware.yaml new file mode 100644 index 0000000..8bf4957 --- /dev/null +++ b/deploy/api/middleware.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: hearth-api-strip +spec: + stripPrefix: + prefixes: + - /api diff --git a/deploy/api/service.yaml b/deploy/api/service.yaml new file mode 100644 index 0000000..c449931 --- /dev/null +++ b/deploy/api/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: hearth-api +spec: + selector: + app: hearth-api + ports: + - name: http + port: 80 + targetPort: http