Fix linting?

This commit is contained in:
Jake Runyan 2026-06-22 03:43:01 -07:00
parent acd95a2033
commit fc9739c504
4 changed files with 86 additions and 74 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
build
.git

View File

@ -1,19 +1,26 @@
FROM node:14 AS build # Shared dependency layer, reused by the ci and build targets.
FROM node:20-alpine AS deps
WORKDIR /app WORKDIR /app
COPY package.json ./
COPY package*.json /app
RUN npm install RUN npm install
COPY . /app # Lint target, used by CI (docker build --target ci).
FROM deps AS ci
COPY . .
RUN npx eslint src --max-warnings=0
# Production build target: bundles the static site.
FROM deps AS build
COPY . .
RUN npm run build RUN npm run build
# Serve the static bundle with nginx.
FROM nginx:alpine FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
# Liveness probe. Use 127.0.0.1 (not localhost): nginx listens IPv4-only and
# busybox wget would resolve localhost to ::1 and get connection refused.
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

127
Jenkinsfile vendored
View File

@ -2,20 +2,16 @@ pipeline {
agent any agent any
environment { environment {
// Secret: Discord webhook used by the post-build notifier.
DISCORD_WEBHOOK = credentials('discord-pws-builds-channel-webhook') DISCORD_WEBHOOK = credentials('discord-pws-builds-channel-webhook')
// Deployment identity / topology (non-secret, declared here for auditability). COMPOSE_SERVICE = 'app'
COMPOSE_PROJECT_NAME = 'jakeswestcoast' CONTAINER_NAME = 'jakeswestcoast'
APP_CONTAINER = 'jakeswestcoast' }
DEPLOY_NETWORK = 'traefik'
// Clean, pinned image used to run static checks in isolation. options {
LINT_IMAGE = 'node:18-alpine' timestamps()
disableConcurrentBuilds()
// Health/smoke probing (served internally on port 80, reached via the container). timeout(time: 30, unit: 'MINUTES')
HEALTH_URL = 'http://localhost:80/'
SMOKE_MARKER = 'id="root"'
} }
stages { stages {
@ -28,65 +24,63 @@ pipeline {
stage('Preflight') { stage('Preflight') {
steps { steps {
sh ''' sh '''
set -e set -eu
[ -n "$DISCORD_WEBHOOK" ] || { echo "ERROR: DISCORD_WEBHOOK credential is missing."; exit 1; } : "${DISCORD_WEBHOOK:?required credential discord-pws-builds-channel-webhook is missing}"
docker network inspect "$DEPLOY_NETWORK" >/dev/null 2>&1 \ for f in Dockerfile package.json docker-compose.yml; do
|| { echo "ERROR: external network '$DEPLOY_NETWORK' does not exist."; exit 1; } [ -f "$f" ] || { echo "ERROR: required file '$f' not found at repo root" >&2; exit 1; }
docker compose config -q \ done
|| { echo "ERROR: docker-compose configuration is invalid."; exit 1; } docker compose config -q
''' '''
} }
} }
stage('Lint & Type-check') { stage('Lint & Type-check') {
steps { steps {
// Run checks in a throwaway container so host state can't mask errors. // Static checks run inside the image's ci target; nothing touches the agent.
sh ''' sh 'docker build --target ci -t ${CONTAINER_NAME}-ci:${BUILD_NUMBER} .'
set -e
docker run --rm -v "$WORKSPACE:/app" -w /app "$LINT_IMAGE" \
sh -c "npm install --no-audit --no-fund && npx eslint src/" \
|| { echo "ERROR: static checks failed."; exit 1; }
'''
} }
} }
stage('Teardown') { stage('Teardown') {
steps { steps {
sh ''' sh '''
set -e set -eu
docker compose down --remove-orphans \ docker compose down --remove-orphans || true
|| { echo "ERROR: failed to tear down the previous deployment."; exit 1; } # container_name is fixed, so a stale container can survive "down"
# and then block "up" with a name conflict; reap it explicitly.
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
''' '''
} }
} }
stage('Build & Deploy') { stage('Build & Deploy') {
steps { steps {
sh ''' // No build-time secrets: the React build consumes only static assets.
set -e sh 'docker compose up -d --build'
docker compose up -d --build \
|| { echo "ERROR: build and deploy failed."; exit 1; }
'''
} }
} }
stage('Health Check') { stage('Health Check') {
steps { steps {
// Poll until the container serves, failing fast if it exits or never becomes ready.
sh ''' sh '''
set -e set -eu
for i in $(seq 1 20); do cid="$(docker compose ps -q "$COMPOSE_SERVICE")"
running=$(docker inspect -f '{{.State.Running}}' "$APP_CONTAINER" 2>/dev/null || echo "false") [ -n "$cid" ] || { echo "ERROR: $COMPOSE_SERVICE container not found" >&2; exit 1; }
[ "$running" = "true" ] || { echo "ERROR: container '$APP_CONTAINER' is not running."; exit 1; }
if docker exec "$APP_CONTAINER" wget -q -O /dev/null "$HEALTH_URL"; then # Wait for the image's HEALTHCHECK to report healthy, failing fast on terminal states.
echo "App is live." deadline=$(( $(date +%s) + 90 ))
exit 0 while :; do
fi status="$(docker inspect -f '{{.State.Status}}' "$cid")"
echo "Waiting for app to become ready ($i)..." health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid")"
sleep 3 [ "$status" = "running" ] && [ "$health" = "healthy" ] && break
[ "$health" = "unhealthy" ] && { echo "ERROR: $COMPOSE_SERVICE reported unhealthy" >&2; exit 1; }
case "$status" in
exited|dead) echo "ERROR: $COMPOSE_SERVICE container $status before becoming healthy" >&2; exit 1 ;;
esac
[ "$(date +%s)" -ge "$deadline" ] && { echo "ERROR: timed out waiting for healthy (status=$status, health=$health)" >&2; exit 1; }
sleep 2
done done
echo "ERROR: app did not become healthy in time." echo "$COMPOSE_SERVICE healthy"
exit 1
''' '''
} }
} }
@ -94,12 +88,14 @@ pipeline {
stage('Smoke Test') { stage('Smoke Test') {
steps { steps {
sh ''' sh '''
set -e set -eu
body=$(docker exec "$APP_CONTAINER" wget -q -O- "$HEALTH_URL") \ cid="$(docker compose ps -q "$COMPOSE_SERVICE")"
|| { echo "ERROR: smoke request failed."; exit 1; } [ -n "$cid" ] || { echo "ERROR: $COMPOSE_SERVICE container not found" >&2; exit 1; }
echo "$body" | grep -q "$SMOKE_MARKER" \
|| { echo "ERROR: smoke test did not find expected content ('$SMOKE_MARKER')."; exit 1; } # Real request from inside the container: GET / must serve the SPA shell.
echo "Smoke test passed." body="$(docker exec "$cid" wget -q -O - http://127.0.0.1:80/)" || { echo "ERROR: GET / did not return a successful response" >&2; exit 1; }
echo "$body" | grep -q 'id="root"' || { echo "ERROR: GET / response missing expected app markup" >&2; exit 1; }
echo "smoke test passed"
''' '''
} }
} }
@ -109,24 +105,26 @@ pipeline {
always { always {
script { script {
def result = currentBuild.currentResult def result = currentBuild.currentResult
def emoji = result == 'SUCCESS' ? ':green_circle:' : (result == 'FAILURE' ? ':red_circle:' : ':yellow_circle:') def emoji = result == 'SUCCESS' ? ':green_circle:' :
def branch = env.GIT_BRANCH ?: env.BRANCH_NAME ?: 'Main/Manual' result == 'FAILURE' ? ':red_circle:' : ':yellow_circle:'
def duration = currentBuild.durationString.replace(' and no weeks', '').replace(' and counting', '')
def commitLines = [] def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'Main/Manual'
for (changeSet in currentBuild.changeSets) {
for (commit in changeSet.items) { def duration = currentBuild.durationString
commitLines.add("> ${commit.msg} (by *${commit.author.fullName}*)") .replace(' and no weeks', '')
} .replace(' and counting', '')
def commits = currentBuild.changeSets.collectMany { set ->
set.items.collect { "> ${it.msg} (by *${it.author.fullName}*)" }
} }
def commits = commitLines ? commitLines.join('\n') : 'No recent changes detected.' def commitText = commits ? commits.join('\n') : 'No recent changes detected.'
def discordDescription = """**Status:** ${emoji} ${result} def discordDescription = """**Status:** ${emoji} ${result}
**Branch:** `${branch}` **Branch:** `${branch}`
**Duration:** :stopwatch: ${duration} **Duration:** :stopwatch: ${duration}
**Commits:** **Commits:**
${commits}""" ${commitText}"""
discordSend( discordSend(
webhookURL: env.DISCORD_WEBHOOK, webhookURL: env.DISCORD_WEBHOOK,
@ -137,12 +135,13 @@ ${commits}"""
) )
} }
} }
failure { failure {
sh ''' sh '''
echo "===== docker compose ps =====" echo "=== docker compose ps ==="
docker compose ps || true docker compose ps || true
echo "===== recent logs =====" echo "=== recent logs ==="
docker compose logs --no-color --tail=200 || true docker compose logs --tail=200 || true
''' '''
} }
} }

View File

@ -21,7 +21,10 @@
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ],
"env": {
"es2021": true
}
}, },
"browserslist": { "browserslist": {
"production": [ "production": [