2bdc4a9748
Restore and build pass on the self-hosted runner but the test run dies after ~3s — too fast to have executed 306 tests. The suite passes on Windows, in a clean Linux container, under a 1GB memory cap, in CI's exact step order, and with the SDK installed to a custom dir without DOTNET_ROOT, so the trigger is specific to this runner rather than the code. Adds a one-test host smoke step (separates "host cannot start" from "the suite takes the host down" using step boundaries, since job logs are not readable via the API) and disables xUnit collection parallelism for the full run — the same remedy the frontend already needs (--runInBand) on this resource-flaky runner. All 306 tests still run; only concurrency changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
7.7 KiB
YAML
166 lines
7.7 KiB
YAML
name: CI and Deploy
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
pull_request:
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup .NET (resilient)
|
|
shell: bash
|
|
# actions/setup-dotnet on this single self-hosted runner intermittently
|
|
# leaves a partial extraction in the shared tool-cache ("tar: Cannot open:
|
|
# File exists") or corrupts the SDK download. Install into a clean private
|
|
# dir via dotnet-install.sh and retry once on failure, mirroring the
|
|
# npm ci / NuGet retries elsewhere in this workflow.
|
|
run: |
|
|
install() {
|
|
curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
|
|
rm -rf "$HOME/.dotnet"
|
|
bash /tmp/dotnet-install.sh --channel 9.0 --install-dir "$HOME/.dotnet"
|
|
}
|
|
install || ( echo "dotnet install failed ($?) — retrying once..." && install )
|
|
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
|
|
"$HOME/.dotnet/dotnet" --info
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
cache-dependency-path: job-tracker-ui/package-lock.json
|
|
|
|
- name: Build backend
|
|
run: dotnet build JobTrackerApi/JobTrackerApi.csproj --configuration Release
|
|
|
|
# The "Build backend" step above only builds JobTrackerApi, so the test project needs its own
|
|
# restore + build. These are separate steps on purpose: this runner is flaky (see the SDK and
|
|
# npm retries in this file) and the job log is not readable from the API, so distinct steps make
|
|
# the failing phase identifiable from step boundaries alone. Restore retries once, mirroring the
|
|
# npm ci pattern below. The suite itself is never weakened or filtered.
|
|
- name: Restore backend tests
|
|
run: |
|
|
restore() { dotnet restore JobTrackerApi.Tests/JobTrackerApi.Tests.csproj; }
|
|
restore || ( echo "NuGet restore failed ($?) — retrying once..." && restore )
|
|
|
|
- name: Build backend tests
|
|
run: dotnet build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore
|
|
|
|
# Restore and build both pass on this runner, but the test run dies after ~3s -- far too fast to
|
|
# have executed 306 tests. The suite passes on Windows, in a clean Linux container, under a 1GB
|
|
# memory cap, and with the SDK installed to a custom dir without DOTNET_ROOT, so the cause is
|
|
# specific to this runner. This one-test smoke separates "the test host cannot start at all"
|
|
# from "something in the suite takes the host down"; the log is not readable via the API, so the
|
|
# step boundary is the signal.
|
|
- name: Test backend (host smoke)
|
|
run: dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-build --filter "FullyQualifiedName~CvBuilderTests.Every_catalog_theme_renders_valid_html"
|
|
|
|
# Collection parallelism is disabled here for the same reason the frontend runs --runInBand:
|
|
# this runner is resource-flaky (documented npm ci SIGSEGV and SDK cache corruption above).
|
|
# Every one of the 306 tests still runs -- only the concurrency changes.
|
|
- name: Test backend
|
|
run: dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-build -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
|
|
|
- name: Install frontend deps
|
|
working-directory: job-tracker-ui
|
|
env:
|
|
npm_config_audit: 'false'
|
|
npm_config_fund: 'false'
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
# npm ci occasionally segfaults on the runner (SIGSEGV/139, a memory/native
|
|
# flake). Retry once with a clean node_modules before failing the job.
|
|
npm ci --no-audit --no-fund \
|
|
|| ( echo "npm ci failed ($?) — cleaning node_modules and retrying once..." \
|
|
&& rm -rf node_modules \
|
|
&& npm ci --no-audit --no-fund )
|
|
|
|
- name: Test frontend
|
|
working-directory: job-tracker-ui
|
|
# Run the WHOLE suite. Never whitelist test files here again: the previous
|
|
# whitelist silently skipped new suites and let two regressions reach main.
|
|
run: npm test -- --watchAll=false --runInBand
|
|
|
|
- name: Build frontend
|
|
working-directory: job-tracker-ui
|
|
env:
|
|
CI: 'false'
|
|
GENERATE_SOURCEMAP: 'false'
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
# CRA's build (Terser minify + fork-ts-checker workers) has repeatedly died silently on
|
|
# this runner with no error output (OOM/SIGSEGV signature — same resource-starved-runner
|
|
# class as the npm ci and dotnet-install flakes elsewhere in this workflow). Retry once.
|
|
run: |
|
|
npm run build \
|
|
|| ( echo "Frontend build failed ($?) — retrying once..." && npm run build )
|
|
|
|
deploy:
|
|
needs: test
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Run remote deploy
|
|
uses: appleboy/ssh-action@v1.0.3
|
|
with:
|
|
host: ${{ secrets.PROD_HOST }}
|
|
username: ${{ secrets.PROD_USER }}
|
|
key: ${{ secrets.PROD_SSH_KEY }}
|
|
command_timeout: 40m
|
|
script: |
|
|
set -euo pipefail
|
|
if [ ! -d /opt/job-tracker/app/.git ]; then
|
|
echo "Expected git checkout at /opt/job-tracker/app but .git was not found."
|
|
exit 1
|
|
fi
|
|
cd /opt/job-tracker/app
|
|
if ! git fetch --all --prune; then
|
|
echo "git fetch failed on server. Check remote auth/URL for /opt/job-tracker/app."
|
|
exit 1
|
|
fi
|
|
if ! git rev-parse --verify --quiet ${{ github.sha }} >/dev/null; then
|
|
echo "Commit ${{ github.sha }} is not available in the server checkout after fetch."
|
|
exit 1
|
|
fi
|
|
git reset --hard ${{ github.sha }}
|
|
git clean -fd
|
|
chmod +x deploy/deploy.sh
|
|
APP_VERSION=${{ github.run_number }} \
|
|
APP_COMMIT_SHA=${{ github.sha }} \
|
|
APP_BUILD_STAMP="$(date -u +'%Y-%m-%d %H:%M UTC')" \
|
|
./deploy/deploy.sh
|
|
docker compose ps
|
|
AI_CONTAINER_ID="$(docker compose ps -q ai-service)"
|
|
if [ -z "$AI_CONTAINER_ID" ]; then
|
|
echo "AI service container id could not be resolved after deploy. Continuing because AI is not a deploy gate for the core app."
|
|
else
|
|
ATTEMPTS=90
|
|
SLEEP_SECS=2
|
|
i=1
|
|
while [ "$i" -le "$ATTEMPTS" ]; do
|
|
HEALTH_STATUS="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$AI_CONTAINER_ID" 2>/dev/null || echo unknown)"
|
|
if [ "$HEALTH_STATUS" = "healthy" ]; then
|
|
break
|
|
fi
|
|
if [ "$HEALTH_STATUS" = "unhealthy" ]; then
|
|
echo "AI service became unhealthy during deploy readiness wait. Continuing because AI is not a deploy gate for the core app."
|
|
docker compose logs --tail=200 ai-service || true
|
|
break
|
|
fi
|
|
sleep "$SLEEP_SECS"
|
|
i=$((i + 1))
|
|
done
|
|
if [ "${HEALTH_STATUS:-unknown}" != "healthy" ]; then
|
|
echo "AI service did not become healthy within $((ATTEMPTS * SLEEP_SECS)) seconds. Final status: ${HEALTH_STATUS:-unknown}. Continuing because AI is not a deploy gate for the core app."
|
|
docker compose ps
|
|
docker compose logs --tail=200 ai-service || true
|
|
fi
|
|
fi
|