Compare commits

...

8 Commits

Author SHA1 Message Date
James Wampler
1b2ce263e5 Fix deploy-qa health check unreachable from runner's network namespace
All checks were successful
CI / build-and-push (push) Successful in 40s
CI / deploy-qa (push) Successful in 11s
The self-hosted runner runs in its own container on a separate bridge
network, so curling localhost:$QA_ADMIN_PORT hit the runner's own loopback
instead of the docker host's published port, never the QA stack.
Exec into the admin container and curl its own localhost instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 13:29:40 -07:00
James Wampler
b32f5d56db Fix health check path mismatch causing 404s
Some checks failed
CI / build-and-push (push) Successful in 45s
CI / deploy-qa (push) Failing after 1m8s
The app maps health checks at /health (ServiceDefaults), not /api/v1/health.
Compose healthcheck and deploy-qa.sh's wait-loop both hit the wrong path.
Also add an nginx location for /health since it lives outside the /api
prefix that the admin proxy otherwise forwards unchanged.
2026-07-04 13:15:09 -07:00
James Wampler
8d0cef08b1 Fix api container healthcheck failing with no error logs
Some checks failed
CI / build-and-push (push) Successful in 45s
CI / deploy-qa (push) Failing after 1m7s
aspnet base image ships neither curl nor wget, so the compose
healthcheck's wget call failed silently at the exec level (visible only
in docker's health-check log, not app stdout). Install curl in the
Dockerfile.ci final stage and switch the healthcheck to use it.
2026-07-04 13:10:04 -07:00
James Wampler
80d450207c Replace actions/checkout with plain git in deploy-qa job
Some checks failed
CI / build-and-push (push) Successful in 40s
CI / deploy-qa (push) Failing after 1m9s
Self-hosted qa runner has no node in PATH, so the Node-based
actions/checkout@v4 action can't run there. Swap it for a bash git
fetch/checkout, matching the pipeline's no-marketplace-action convention.
2026-07-04 11:42:44 -07:00
James Wampler
2dc40f72ce Add .dockerignore to stop host obj/bin leaking into CI image builds
Some checks failed
CI / build-and-push (push) Successful in 1m3s
CI / deploy-qa (push) Failing after 6s
Dockerfile.ci COPYs full project directories after restoring inside
the container. Without a .dockerignore, the host's obj/bin (built
with a different local SDK than the container's) got copied over the
container's fresh restore output, corrupting project.assets.json and
crashing ResolvePackageAssets with a NullReferenceException on publish.
2026-07-02 20:48:15 -07:00
James Wampler
a81798cffe Pin MessagePack to patched 2.5.302 in AppHost
Some checks failed
CI / build-and-push (push) Failing after 45s
CI / deploy-qa (push) Has been skipped
Aspire.Hosting.PostgreSQL/JavaScript pull in MessagePack 2.5.192
transitively, which has multiple known vulnerabilities and fails the
build with TreatWarningsAsErrors. Pinned a direct reference to the
patched 2.5.302 (same major, no API break).
2026-07-02 13:01:37 -07:00
James Wampler
926af91389 Pin Microsoft.OpenApi to patched 2.9.0 to fix NU1903 build failure
Microsoft.AspNetCore.OpenApi 10.0.5 pulls in Microsoft.OpenApi 2.0.0
transitively, which has a known high-severity vulnerability
(GHSA-v5pm-xwqc-g5wc) and fails the build with TreatWarningsAsErrors.
Pinned a direct reference to the patched 2.9.0 (still 2.x, API-compatible).
2026-07-02 13:00:59 -07:00
James Wampler
37ac4b47f2 Auto-install .NET SDK in CI scripts when runner lacks it
Some checks failed
CI / build-and-push (push) Failing after 12s
CI / deploy-qa (push) Has been skipped
Gitea runner build failed with "dotnet: command not found". Added
ensure_dotnet() to lib.sh, using the vendored dotnet-install.sh to
bootstrap the SDK into .dotnet/ when not already on PATH, called from
build.sh/test.sh/prepush.sh. Also trigger CI on build-runner-fix to
verify the fix.
2026-07-02 12:50:19 -07:00
13 changed files with 65 additions and 5 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
**/bin/
**/obj/
**/node_modules/
**/dist/
.git/
.husky/
docs/

View File

@@ -6,7 +6,7 @@ name: CI
on: on:
push: push:
branches: [main] branches: [main, build-runner-fix]
jobs: jobs:
build-and-push: build-and-push:
@@ -42,7 +42,14 @@ jobs:
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }} QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
steps: steps:
- uses: actions/checkout@v4 # actions/checkout@v4 is a Node-based action; this runner has no node
# in PATH, so checkout plain git instead of via marketplace action.
- name: Checkout
run: |
git init -q .
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
git -c http.extraheader="AUTHORIZATION: bearer ${{ github.token }}" fetch --depth=1 origin "${{ github.sha }}"
git checkout -q FETCH_HEAD
- name: Deploy to QA - name: Deploy to QA
run: ./scripts/ci/deploy-qa.sh run: ./scripts/ci/deploy-qa.sh

3
.gitignore vendored
View File

@@ -468,3 +468,6 @@ lerna-debug.log*
# Windows thumbnail cache # Windows thumbnail cache
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
# Self-installed .NET SDK (scripts/ci/lib.sh ensure_dotnet, used when a CI runner lacks the SDK)
/.dotnet/

View File

@@ -36,7 +36,7 @@ services:
db: db:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/v1/health 2>/dev/null || exit 1"] test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5

View File

@@ -6,6 +6,8 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
cd "$CI_ROOT" cd "$CI_ROOT"
ensure_dotnet
log "Restoring and publishing MicCheck.Api (Release)" log "Restoring and publishing MicCheck.Api (Release)"
dotnet publish src/api/MicCheck.Api/MicCheck.Api.csproj -c Release dotnet publish src/api/MicCheck.Api/MicCheck.Api.csproj -c Release

View File

@@ -24,9 +24,14 @@ $COMPOSE down -v
log "Starting QA stack" log "Starting QA stack"
$COMPOSE up -d $COMPOSE up -d
log "Waiting for API health check via admin proxy on port $QA_ADMIN_PORT" log "Waiting for API health check via admin proxy"
# Checked with `docker compose exec` rather than curling the published host
# port: CI runs this script inside a runner container on its own bridge
# network, where "localhost:$QA_ADMIN_PORT" is the runner's own loopback, not
# the docker host's - it can never reach a host-published port. Exec'ing into
# the admin container and curling its own localhost sidesteps that entirely.
attempts=30 attempts=30
until curl -fsS "http://localhost:${QA_ADMIN_PORT}/api/v1/health" >/dev/null 2>&1; do until $COMPOSE exec -T admin curl -fsS http://localhost/health >/dev/null 2>&1; do
attempts=$((attempts - 1)) attempts=$((attempts - 1))
if [[ "$attempts" -le 0 ]]; then if [[ "$attempts" -le 0 ]]; then
fail "QA stack did not become healthy in time" fail "QA stack did not become healthy in time"

View File

@@ -37,6 +37,23 @@ image_names() {
ADMIN_IMAGE="$REGISTRY/$REGISTRY_OWNER/miccheck-admin" ADMIN_IMAGE="$REGISTRY/$REGISTRY_OWNER/miccheck-admin"
} }
# Installs .NET into $CI_ROOT/.dotnet via the vendored dotnet-install.sh if
# `dotnet` isn't already on PATH, then prepends it to PATH for this process.
# Keeps bare runners (no SDK preinstalled) working the same as a dev machine.
ensure_dotnet() {
if command -v dotnet > /dev/null 2>&1; then
return 0
fi
local install_dir="$CI_ROOT/.dotnet"
if [[ ! -x "$install_dir/dotnet" ]]; then
log "dotnet not found on PATH; installing .NET SDK via dotnet-install.sh"
bash "$CI_ROOT/dotnet-install.sh" --channel LTS --install-dir "$install_dir"
fi
export PATH="$install_dir:$PATH"
export DOTNET_ROOT="$install_dir"
}
registry_login() { registry_login() {
require_registry_vars require_registry_vars
[[ -n "$REGISTRY_USER" ]] || fail "REGISTRY_USER env var is required to push images" [[ -n "$REGISTRY_USER" ]] || fail "REGISTRY_USER env var is required to push images"

View File

@@ -5,6 +5,8 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
cd "$CI_ROOT" cd "$CI_ROOT"
ensure_dotnet
log "Building full solution (Debug)" log "Building full solution (Debug)"
dotnet build MicCheck.slnx -c Debug dotnet build MicCheck.slnx -c Debug

View File

@@ -6,6 +6,8 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh
cd "$CI_ROOT" cd "$CI_ROOT"
ensure_dotnet
log "Running MicCheck.Api.Tests.Unit" log "Running MicCheck.Api.Tests.Unit"
dotnet test tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj -c Release --logger trx dotnet test tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj -c Release --logger trx

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.4.2" /> <PackageReference Include="Aspire.Hosting.JavaScript" Version="13.4.2" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.0" /> <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.0" />
<PackageReference Include="MessagePack" Version="2.5.302" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -20,6 +20,14 @@ server {
proxy_read_timeout 30s; proxy_read_timeout 30s;
} }
# Proxy the API's health endpoint, which lives outside the /api prefix.
location = /health {
resolver 127.0.0.11 valid=10s ipv6=off;
set $api_upstream http://api:8080;
proxy_pass $api_upstream/health;
proxy_http_version 1.1;
}
# SPA fallback — all other paths serve index.html so Vue Router handles them # SPA fallback — all other paths serve index.html so Vue Router handles them
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;

View File

@@ -17,6 +17,11 @@ 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 RUN dotnet publish src/api/MicCheck.Api/MicCheck.Api.csproj -c Release -o /out --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0 FROM mcr.microsoft.com/dotnet/aspnet:10.0
# curl is used by the docker-compose healthcheck; the base image ships neither
# curl nor wget, so the healthcheck silently fails as "unhealthy" without it.
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY --from=build /out . COPY --from=build /out .
EXPOSE 8080 EXPOSE 8080

View File

@@ -12,6 +12,7 @@
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.OpenApi" Version="2.9.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">