qa-environment-aspire #2
48
.github/workflows/ci.yml
vendored
Normal file
48
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# CI/CD pipeline for MicCheck. Read by both Gitea Actions and GitHub Actions
|
||||
# (both look under .github/workflows/). Every non-checkout step just invokes a
|
||||
# bash script under scripts/ci/, so the entire pipeline is reproducible by
|
||||
# running the same scripts locally - no marketplace build/test/push actions.
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REGISTRY: ${{ secrets.REGISTRY }}
|
||||
REGISTRY_OWNER: ${{ secrets.REGISTRY_OWNER }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: ./scripts/ci/build.sh
|
||||
|
||||
- name: Test
|
||||
run: ./scripts/ci/test.sh
|
||||
|
||||
- name: Build Docker images
|
||||
run: ./scripts/ci/docker-build.sh
|
||||
|
||||
- name: Push Docker images
|
||||
run: ./scripts/ci/docker-push.sh
|
||||
|
||||
deploy-qa:
|
||||
needs: build-and-push
|
||||
runs-on: [self-hosted, qa]
|
||||
env:
|
||||
REGISTRY: ${{ secrets.REGISTRY }}
|
||||
REGISTRY_OWNER: ${{ secrets.REGISTRY_OWNER }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
|
||||
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to QA
|
||||
run: ./scripts/ci/deploy-qa.sh
|
||||
1
.husky/pre-push
Executable file
1
.husky/pre-push
Executable file
@@ -0,0 +1 @@
|
||||
exec ./scripts/ci/prepush.sh
|
||||
12
deploy/qa/.env.example
Normal file
12
deploy/qa/.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copy to .env (or export in CI) and fill in real values.
|
||||
# Used by scripts/ci/*.sh and deploy/qa/docker-compose.qa.yml.
|
||||
|
||||
# Gitea container registry
|
||||
REGISTRY=gitea.example.com
|
||||
REGISTRY_OWNER=your-org-or-user
|
||||
REGISTRY_USER=ci-bot
|
||||
REGISTRY_TOKEN=changeme
|
||||
|
||||
# QA environment
|
||||
JWT_SECRET_KEY=change-this-to-a-random-32-plus-char-secret
|
||||
QA_ADMIN_PORT=3001
|
||||
61
deploy/qa/docker-compose.qa.yml
Normal file
61
deploy/qa/docker-compose.qa.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
name: miccheck-qa
|
||||
|
||||
# Dedicated, network-isolated QA stack. Not connected to the dev compose
|
||||
# stack's network - runs entirely on its own bridge network below, and the
|
||||
# database has no published host port.
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: miccheck
|
||||
POSTGRES_USER: miccheck
|
||||
POSTGRES_PASSWORD: password
|
||||
volumes:
|
||||
- miccheck-qa-pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- qa
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U miccheck -d miccheck"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
image: ${API_IMAGE}:qa
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
ASPNETCORE_URLS: http://+:8080
|
||||
ConnectionStrings__miccheck: "Host=db;Database=miccheck;Username=miccheck;Password=password"
|
||||
Jwt__SecretKey: ${JWT_SECRET_KEY}
|
||||
Jwt__Issuer: MicCheck
|
||||
Jwt__Audience: MicCheck
|
||||
networks:
|
||||
- qa
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/v1/health 2>/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
admin:
|
||||
image: ${ADMIN_IMAGE}:qa
|
||||
ports:
|
||||
- "${QA_ADMIN_PORT:-3001}:80"
|
||||
networks:
|
||||
- qa
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
|
||||
networks:
|
||||
qa:
|
||||
name: miccheck-qa-net
|
||||
|
||||
volumes:
|
||||
miccheck-qa-pgdata:
|
||||
name: miccheck-qa-pgdata
|
||||
28
package-lock.json
generated
Normal file
28
package-lock.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "mic-check",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mic-check",
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
package.json
Normal file
11
package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "mic-check",
|
||||
"private": true,
|
||||
"description": "Repo-root package - hosts the Husky pre-push hook only.",
|
||||
"scripts": {
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
}
|
||||
18
scripts/ci/build.sh
Executable file
18
scripts/ci/build.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles the API (and its dependents) and builds the admin SPA.
|
||||
# Acts as the compile gate before tests/image builds run. TreatWarningsAsErrors
|
||||
# is enabled across the solution, so this also fails on any compiler warning.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
|
||||
log "Restoring and publishing MicCheck.Api (Release)"
|
||||
dotnet publish src/api/MicCheck.Api/MicCheck.Api.csproj -c Release
|
||||
|
||||
log "Installing admin dependencies (npm ci)"
|
||||
npm --prefix src/admin ci
|
||||
|
||||
log "Building admin SPA (vite build)"
|
||||
npm --prefix src/admin run build
|
||||
|
||||
log "build.sh complete"
|
||||
37
scripts/ci/deploy-qa.sh
Executable file
37
scripts/ci/deploy-qa.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploys the freshly-pushed :qa images to the dedicated, network-isolated QA
|
||||
# stack and recreates the miccheck database in an empty state. Safe/idempotent
|
||||
# to re-run: `down -v` removes the Postgres data volume, and the API's own
|
||||
# startup logic (EF Core migrations + idempotent dev seeding) rebuilds it.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
image_names
|
||||
registry_login
|
||||
|
||||
export API_IMAGE ADMIN_IMAGE
|
||||
export JWT_SECRET_KEY="${JWT_SECRET_KEY:?JWT_SECRET_KEY env var is required}"
|
||||
export QA_ADMIN_PORT="${QA_ADMIN_PORT:-3001}"
|
||||
|
||||
COMPOSE="docker compose -p miccheck-qa -f deploy/qa/docker-compose.qa.yml"
|
||||
|
||||
log "Pulling latest :qa images"
|
||||
$COMPOSE pull
|
||||
|
||||
log "Tearing down existing QA stack and wiping the database volume"
|
||||
$COMPOSE down -v
|
||||
|
||||
log "Starting QA stack"
|
||||
$COMPOSE up -d
|
||||
|
||||
log "Waiting for API health check via admin proxy on port $QA_ADMIN_PORT"
|
||||
attempts=30
|
||||
until curl -fsS "http://localhost:${QA_ADMIN_PORT}/api/v1/health" >/dev/null 2>&1; do
|
||||
attempts=$((attempts - 1))
|
||||
if [[ "$attempts" -le 0 ]]; then
|
||||
fail "QA stack did not become healthy in time"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log "QA environment deployed and healthy at http://localhost:${QA_ADMIN_PORT}"
|
||||
23
scripts/ci/docker-build.sh
Executable file
23
scripts/ci/docker-build.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds self-contained CI images for the API and admin apps and tags them
|
||||
# with both the current git sha and "qa" (the tag the QA compose stack pulls).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
image_names
|
||||
|
||||
log "Building $API_IMAGE:$GIT_SHA / :qa"
|
||||
docker build \
|
||||
-f src/api/MicCheck.Api/Dockerfile.ci \
|
||||
-t "$API_IMAGE:$GIT_SHA" \
|
||||
-t "$API_IMAGE:qa" \
|
||||
.
|
||||
|
||||
log "Building $ADMIN_IMAGE:$GIT_SHA / :qa"
|
||||
docker build \
|
||||
-f src/admin/Dockerfile.ci \
|
||||
-t "$ADMIN_IMAGE:$GIT_SHA" \
|
||||
-t "$ADMIN_IMAGE:qa" \
|
||||
src/admin
|
||||
|
||||
log "docker-build.sh complete"
|
||||
17
scripts/ci/docker-push.sh
Executable file
17
scripts/ci/docker-push.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pushes the images built by docker-build.sh (git-sha and qa tags) to the
|
||||
# Gitea container registry.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
image_names
|
||||
registry_login
|
||||
|
||||
for tag in "$GIT_SHA" qa; do
|
||||
log "Pushing $API_IMAGE:$tag"
|
||||
docker push "$API_IMAGE:$tag"
|
||||
log "Pushing $ADMIN_IMAGE:$tag"
|
||||
docker push "$ADMIN_IMAGE:$tag"
|
||||
done
|
||||
|
||||
log "docker-push.sh complete"
|
||||
46
scripts/ci/lib.sh
Executable file
46
scripts/ci/lib.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for scripts/ci/*.sh.
|
||||
# Every script in this directory is meant to run identically in CI and on a
|
||||
# developer's machine - no Gitea/GitHub-specific built-in actions, just bash.
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Repo root, regardless of caller's cwd.
|
||||
CI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Registry configuration. All values come from the environment (CI secrets or
|
||||
# a developer's shell) - nothing is hardcoded, per project convention.
|
||||
REGISTRY="${REGISTRY:-}"
|
||||
REGISTRY_OWNER="${REGISTRY_OWNER:-}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-}"
|
||||
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
|
||||
|
||||
GIT_SHA="$(git -C "$CI_ROOT" rev-parse --short HEAD)"
|
||||
|
||||
require_registry_vars() {
|
||||
[[ -n "$REGISTRY" ]] || fail "REGISTRY env var is required (e.g. gitea.example.com)"
|
||||
[[ -n "$REGISTRY_OWNER" ]] || fail "REGISTRY_OWNER env var is required (e.g. your gitea org/user)"
|
||||
}
|
||||
|
||||
# Populates API_IMAGE / ADMIN_IMAGE, e.g. gitea.example.com/james/miccheck-api
|
||||
image_names() {
|
||||
require_registry_vars
|
||||
API_IMAGE="$REGISTRY/$REGISTRY_OWNER/miccheck-api"
|
||||
ADMIN_IMAGE="$REGISTRY/$REGISTRY_OWNER/miccheck-admin"
|
||||
}
|
||||
|
||||
registry_login() {
|
||||
require_registry_vars
|
||||
[[ -n "$REGISTRY_USER" ]] || fail "REGISTRY_USER env var is required to push images"
|
||||
[[ -n "$REGISTRY_TOKEN" ]] || fail "REGISTRY_TOKEN env var is required to push images"
|
||||
log "Logging in to $REGISTRY as $REGISTRY_USER"
|
||||
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||
}
|
||||
23
scripts/ci/prepush.sh
Executable file
23
scripts/ci/prepush.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Husky pre-push hook body. Compiles everything and runs the fast test suites
|
||||
# (no Docker, no registry) so broken code/tests never leave the workstation.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
|
||||
log "Building full solution (Debug)"
|
||||
dotnet build MicCheck.slnx -c Debug
|
||||
|
||||
log "Running MicCheck.Api.Tests.Unit"
|
||||
dotnet test tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj -c Debug
|
||||
|
||||
log "Installing admin dependencies (npm ci)"
|
||||
npm --prefix src/admin ci
|
||||
|
||||
log "Running admin Jest tests"
|
||||
npm --prefix src/admin test
|
||||
|
||||
log "Building admin SPA (vite build)"
|
||||
npm --prefix src/admin run build
|
||||
|
||||
log "prepush.sh complete - ok to push"
|
||||
15
scripts/ci/test.sh
Executable file
15
scripts/ci/test.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the fast test suites: .NET unit tests (EF InMemory, no DB/Docker needed)
|
||||
# and the admin Jest suite. Integration tests are intentionally excluded here -
|
||||
# they require a live API + Postgres (see readme in the Integration test project).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
|
||||
cd "$CI_ROOT"
|
||||
|
||||
log "Running MicCheck.Api.Tests.Unit"
|
||||
dotnet test tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj -c Release --logger trx
|
||||
|
||||
log "Running admin Jest tests"
|
||||
npm --prefix src/admin test
|
||||
|
||||
log "test.sh complete"
|
||||
19
src/admin/Dockerfile.ci
Normal file
19
src/admin/Dockerfile.ci
Normal file
@@ -0,0 +1,19 @@
|
||||
# Self-contained CI/QA image for the admin SPA. Unlike the dev Dockerfile
|
||||
# (which serves a host-built ./dist via bind mount), this builds the SPA
|
||||
# inside the image so it can be pushed to a registry and run standalone.
|
||||
#
|
||||
# Build context is src/admin:
|
||||
# docker build -f src/admin/Dockerfile.ci -t miccheck-admin:qa src/admin
|
||||
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
23
src/api/MicCheck.Api/Dockerfile.ci
Normal file
23
src/api/MicCheck.Api/Dockerfile.ci
Normal file
@@ -0,0 +1,23 @@
|
||||
# Self-contained CI/QA image for MicCheck.Api. Unlike the dev Dockerfile
|
||||
# (which expects a host-published /app to be bind-mounted), this builds the
|
||||
# app inside the image so it can be pushed to a registry and run standalone.
|
||||
#
|
||||
# Build context must be the repo root (needs ServiceDefaults + the API project):
|
||||
# docker build -f src/api/MicCheck.Api/Dockerfile.ci -t miccheck-api:qa .
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj src/MicCheck.ServiceDefaults/
|
||||
COPY src/api/MicCheck.Api/MicCheck.Api.csproj src/api/MicCheck.Api/
|
||||
RUN dotnet restore src/api/MicCheck.Api/MicCheck.Api.csproj
|
||||
|
||||
COPY src/MicCheck.ServiceDefaults/ src/MicCheck.ServiceDefaults/
|
||||
COPY src/api/MicCheck.Api/ src/api/MicCheck.Api/
|
||||
RUN dotnet publish src/api/MicCheck.Api/MicCheck.Api.csproj -c Release -o /out --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /out .
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "MicCheck.Api.dll"]
|
||||
Reference in New Issue
Block a user