8c10124272
CI / test (push) Successful in 25s
CI / lint (push) Successful in 28s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 12s
CI / sast (push) Successful in 10s
CI / build-images (push) Failing after 51s
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Wires up the CD half of the pipeline. New jobs build multi-stage Docker images for the frontend and backend, run a Trivy image scan that fails on HIGH/CRITICAL findings, and push to harbor.tehriehldeal.com on main only. Each push tags <version> (from package.json), <sha>, and latest; a pre-push existence check refuses to overwrite a version tag that already points at a different digest, forcing a real bump. The Vite frontend now reads runtime config from window.__RUNTIME_CONFIG__, populated by /config.js which nginx renders from container env vars at startup via envsubst. A getConfig() helper falls back to import.meta.env for `pnpm dev` and Vitest, so existing test scaffolding keeps working. PWA workbox excludes /config.js from precache and serves it NetworkOnly to keep stale config from surviving a container restart. Bumps frontend 0.0.0→0.1.0 and backend 0.0.1→0.1.0 (production deployment is a meaningful new capability for both packages). Also fixes four pre-existing tsc -b errors that the new vite build step in the frontend Dockerfile would otherwise hit: global.fetch → globalThis.fetch in three test files, null-guard in Activity.tsx account filter, type cast on Recharts Pie onClick in Dashboard.tsx, typed callback signature on the auth.test.ts onAuthStateChange mock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
366 lines
12 KiB
YAML
366 lines
12 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
pull_request:
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 9
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
run: pnpm --filter tehriehlbudget-backend exec prisma generate
|
|
|
|
- name: Backend tests (Jest)
|
|
run: pnpm test:backend
|
|
|
|
- name: Frontend tests (Vitest)
|
|
# Supabase's createClient validates the URL at module load, so the
|
|
# frontend tests need *some* value to import lib/supabase without
|
|
# throwing. Real auth calls are stubbed in the tests, so these are
|
|
# purely structural placeholders — never used over the wire.
|
|
env:
|
|
VITE_SUPABASE_URL: http://localhost:54321
|
|
VITE_SUPABASE_ANON_KEY: placeholder-anon-key-for-tests-only
|
|
run: pnpm test:frontend
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 9
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
# Backend ESLint imports types from the generated Prisma client.
|
|
run: pnpm --filter tehriehlbudget-backend exec prisma generate
|
|
|
|
- name: Lint
|
|
run: pnpm lint
|
|
|
|
- name: Format check
|
|
run: pnpm format:check
|
|
|
|
secrets-scan:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Install gitleaks
|
|
run: |
|
|
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
|
|
-o /tmp/gitleaks.tar.gz
|
|
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
|
|
sudo mv /tmp/gitleaks /usr/local/bin/gitleaks
|
|
gitleaks version
|
|
|
|
- name: Run gitleaks
|
|
# TODO: flip to --exit-code 1 once the baseline is clean
|
|
run: |
|
|
gitleaks detect \
|
|
--source . \
|
|
--config .gitleaks.toml \
|
|
--report-format sarif \
|
|
--report-path gitleaks.sarif \
|
|
--redact \
|
|
--no-git \
|
|
--exit-code 0
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
if: always()
|
|
with:
|
|
name: gitleaks-report
|
|
path: gitleaks.sarif
|
|
|
|
vuln-scan:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Trivy filesystem scan (vuln + misconfig)
|
|
# TODO: flip --exit-code to 1 once baseline is clean
|
|
run: |
|
|
docker run --rm -v "$PWD:/src" aquasec/trivy:latest \
|
|
fs /src \
|
|
--severity HIGH,CRITICAL \
|
|
--scanners vuln,misconfig \
|
|
--format sarif \
|
|
--output /src/trivy-fs.sarif \
|
|
--exit-code 0
|
|
|
|
- name: Trivy SBOM (CycloneDX)
|
|
run: |
|
|
docker run --rm -v "$PWD:/src" aquasec/trivy:latest \
|
|
fs /src \
|
|
--format cyclonedx \
|
|
--output /src/sbom.cdx.json
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
if: always()
|
|
with:
|
|
name: trivy-reports
|
|
path: |
|
|
trivy-fs.sarif
|
|
sbom.cdx.json
|
|
|
|
sast:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Run Semgrep
|
|
# TODO: drop the trailing "|| true" once the baseline is clean
|
|
run: |
|
|
docker run --rm -v "$PWD:/src" returntocorp/semgrep:latest \
|
|
semgrep scan \
|
|
--config p/typescript \
|
|
--config p/react \
|
|
--config p/nodejsscan \
|
|
--config p/owasp-top-ten \
|
|
--sarif --output /src/semgrep.sarif \
|
|
--error /src || true
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
if: always()
|
|
with:
|
|
name: semgrep-report
|
|
path: semgrep.sarif
|
|
|
|
build-images:
|
|
runs-on: ubuntu-latest
|
|
needs: [test, lint, secrets-scan, vuln-scan, sast]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Compute image tags
|
|
run: |
|
|
SHA_SHORT=$(git rev-parse --short HEAD)
|
|
BACKEND_VERSION=$(jq -r .version tehriehlbudget-backend/package.json)
|
|
FRONTEND_VERSION=$(jq -r .version tehriehlbudget-frontend/package.json)
|
|
echo "SHA_SHORT=${SHA_SHORT}" >> "$GITHUB_ENV"
|
|
echo "BACKEND_VERSION=${BACKEND_VERSION}" >> "$GITHUB_ENV"
|
|
echo "FRONTEND_VERSION=${FRONTEND_VERSION}" >> "$GITHUB_ENV"
|
|
|
|
- name: Build backend image
|
|
run: |
|
|
docker buildx build --load \
|
|
-f tehriehlbudget-backend/Dockerfile \
|
|
-t tehriehlbudget-backend:${SHA_SHORT} \
|
|
.
|
|
|
|
- name: Build frontend image
|
|
run: |
|
|
docker buildx build --load \
|
|
-f tehriehlbudget-frontend/Dockerfile \
|
|
-t tehriehlbudget-frontend:${SHA_SHORT} \
|
|
.
|
|
|
|
- name: Save images for downstream jobs
|
|
run: |
|
|
docker save \
|
|
tehriehlbudget-backend:${SHA_SHORT} \
|
|
tehriehlbudget-frontend:${SHA_SHORT} \
|
|
-o /tmp/images.tar
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
with:
|
|
name: built-images
|
|
path: /tmp/images.tar
|
|
retention-days: 1
|
|
|
|
image-scan:
|
|
runs-on: ubuntu-latest
|
|
needs: build-images
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/download-artifact@v3
|
|
with:
|
|
name: built-images
|
|
path: /tmp
|
|
|
|
- name: Load images
|
|
run: |
|
|
docker load -i /tmp/images.tar
|
|
SHA_SHORT=$(git rev-parse --short HEAD)
|
|
echo "SHA_SHORT=${SHA_SHORT}" >> "$GITHUB_ENV"
|
|
|
|
- name: Trivy image scan — backend (HIGH/CRITICAL gate)
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v "$PWD:/src" \
|
|
aquasec/trivy:latest image \
|
|
--severity HIGH,CRITICAL \
|
|
--exit-code 1 \
|
|
--ignore-unfixed \
|
|
--format sarif --output /src/trivy-image-backend.sarif \
|
|
tehriehlbudget-backend:${SHA_SHORT}
|
|
|
|
- name: Trivy image scan — frontend (HIGH/CRITICAL gate)
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v "$PWD:/src" \
|
|
aquasec/trivy:latest image \
|
|
--severity HIGH,CRITICAL \
|
|
--exit-code 1 \
|
|
--ignore-unfixed \
|
|
--format sarif --output /src/trivy-image-frontend.sarif \
|
|
tehriehlbudget-frontend:${SHA_SHORT}
|
|
|
|
- name: Trivy SBOM — backend
|
|
if: always()
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v "$PWD:/src" \
|
|
aquasec/trivy:latest image \
|
|
--format cyclonedx --output /src/sbom-backend.cdx.json \
|
|
tehriehlbudget-backend:${SHA_SHORT}
|
|
|
|
- name: Trivy SBOM — frontend
|
|
if: always()
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v "$PWD:/src" \
|
|
aquasec/trivy:latest image \
|
|
--format cyclonedx --output /src/sbom-frontend.cdx.json \
|
|
tehriehlbudget-frontend:${SHA_SHORT}
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
if: always()
|
|
with:
|
|
name: trivy-image-reports
|
|
path: |
|
|
trivy-image-backend.sarif
|
|
trivy-image-frontend.sarif
|
|
sbom-backend.cdx.json
|
|
sbom-frontend.cdx.json
|
|
|
|
push:
|
|
runs-on: ubuntu-latest
|
|
needs: image-scan
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
env:
|
|
HARBOR_HOST: harbor.tehriehldeal.com
|
|
HARBOR_PROJECT: tehriehlbudget
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
# Need full history so the back-pushed git tag can be created against
|
|
# the right commit, and so token-auth on push to origin works.
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/download-artifact@v3
|
|
with:
|
|
name: built-images
|
|
path: /tmp
|
|
|
|
- name: Load images and compute tag inputs
|
|
run: |
|
|
docker load -i /tmp/images.tar
|
|
SHA_SHORT=$(git rev-parse --short HEAD)
|
|
BACKEND_VERSION=$(jq -r .version tehriehlbudget-backend/package.json)
|
|
FRONTEND_VERSION=$(jq -r .version tehriehlbudget-frontend/package.json)
|
|
echo "SHA_SHORT=${SHA_SHORT}" >> "$GITHUB_ENV"
|
|
echo "BACKEND_VERSION=${BACKEND_VERSION}" >> "$GITHUB_ENV"
|
|
echo "FRONTEND_VERSION=${FRONTEND_VERSION}" >> "$GITHUB_ENV"
|
|
|
|
- name: Refuse to overwrite an existing version tag
|
|
env:
|
|
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
|
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
|
|
run: |
|
|
set -eu
|
|
check_tag() {
|
|
local svc="$1" ver="$2"
|
|
local url="https://${HARBOR_HOST}/api/v2.0/projects/${HARBOR_PROJECT}/repositories/${svc}/artifacts/${ver}/tags"
|
|
local code
|
|
code=$(curl -s -o /dev/null -w "%{http_code}" -u "${HARBOR_USERNAME}:${HARBOR_PASSWORD}" "${url}")
|
|
if [ "$code" = "200" ]; then
|
|
echo "::error::Tag ${HARBOR_PROJECT}/${svc}:${ver} already exists in Harbor. Bump ${svc}/package.json before merging."
|
|
exit 1
|
|
fi
|
|
if [ "$code" != "404" ]; then
|
|
echo "::warning::Unexpected status ${code} checking ${url} — proceeding."
|
|
fi
|
|
}
|
|
check_tag backend "${BACKEND_VERSION}"
|
|
check_tag frontend "${FRONTEND_VERSION}"
|
|
|
|
- name: Log in to Harbor
|
|
env:
|
|
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
|
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
|
|
run: |
|
|
echo "${HARBOR_PASSWORD}" | docker login "${HARBOR_HOST}" -u "${HARBOR_USERNAME}" --password-stdin
|
|
|
|
- name: Tag and push backend image
|
|
run: |
|
|
REPO="${HARBOR_HOST}/${HARBOR_PROJECT}/backend"
|
|
docker tag tehriehlbudget-backend:${SHA_SHORT} ${REPO}:${BACKEND_VERSION}
|
|
docker tag tehriehlbudget-backend:${SHA_SHORT} ${REPO}:${SHA_SHORT}
|
|
docker tag tehriehlbudget-backend:${SHA_SHORT} ${REPO}:latest
|
|
docker push ${REPO}:${BACKEND_VERSION}
|
|
docker push ${REPO}:${SHA_SHORT}
|
|
docker push ${REPO}:latest
|
|
|
|
- name: Tag and push frontend image
|
|
run: |
|
|
REPO="${HARBOR_HOST}/${HARBOR_PROJECT}/frontend"
|
|
docker tag tehriehlbudget-frontend:${SHA_SHORT} ${REPO}:${FRONTEND_VERSION}
|
|
docker tag tehriehlbudget-frontend:${SHA_SHORT} ${REPO}:${SHA_SHORT}
|
|
docker tag tehriehlbudget-frontend:${SHA_SHORT} ${REPO}:latest
|
|
docker push ${REPO}:${FRONTEND_VERSION}
|
|
docker push ${REPO}:${SHA_SHORT}
|
|
docker push ${REPO}:latest
|
|
|
|
- name: Push back per-package git tags
|
|
env:
|
|
# Auto-injected per-run token; has push permission to this repo.
|
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
git config user.name "gitea-actions[bot]"
|
|
git config user.email "gitea-actions[bot]@tehriehldeal.com"
|
|
git tag "backend-v${BACKEND_VERSION}" -m "backend ${BACKEND_VERSION}" 2>/dev/null || true
|
|
git tag "frontend-v${FRONTEND_VERSION}" -m "frontend ${FRONTEND_VERSION}" 2>/dev/null || true
|
|
ORIGIN="$(git remote get-url origin | sed -E "s#https?://([^/]+)#https://${GITEA_TOKEN}@\1#")"
|
|
# A failed tag push shouldn't undo a successful image push — log
|
|
# and proceed so we don't poison subsequent retries.
|
|
git push "$ORIGIN" --tags || echo "::warning::Tag push to origin failed — images are pushed; create the tags manually if needed."
|
|
|
|
- name: Log out of Harbor
|
|
if: always()
|
|
run: docker logout "${HARBOR_HOST}" || true
|