Files
TehRiehlBudget/.gitea/workflows/ci.yml
T
TehRiehlDeal a79ee5f479
CI / secrets-scan (push) Successful in 6s
CI / sast (push) Successful in 14s
CI / vuln-scan (push) Successful in 17s
CI / lint (push) Failing after 28s
CI / test (push) Failing after 28s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Pin CI to pnpm 9.14.4 to dodge the strict ERR_PNPM_IGNORED_BUILDS gate
The Install dependencies step has been failing with
ERR_PNPM_IGNORED_BUILDS no matter where I put the onlyBuiltDependencies
allowlist (package.json#pnpm, pnpm-workspace.yaml, project .npmrc) and
no matter which pnpm 10.x is installed. The strict build-script gate was
introduced in pnpm 9.15 / 10.0; pnpm 9.14.4 predates it and just runs
postinstall scripts the way pnpm has for years — matching what the
Dockerfiles already do via corepack `pnpm@9`.

Also reverts the short-lived `--ignore-scripts` install workaround,
which skipped @prisma/client's postinstall and left runtime files
missing so `prisma generate` couldn't complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:47:08 -07:00

424 lines
15 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:
# Pinned to a 9.x release that predates pnpm's strict
# ERR_PNPM_IGNORED_BUILDS gate (added in 9.15 / 10.x). With
# `version: 9` the action was resolving to a newer release that
# blocks the install over our onlyBuiltDependencies allowlist not
# being honored in this runner. 9.14.4 just runs the scripts the
# way pnpm always did, which matches what the Dockerfiles do via
# corepack `pnpm@9` and what production builds rely on.
version: "9.14.4"
- uses: actions/setup-node@v4
with:
node-version: 22
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:
# Pinned to a 9.x release that predates pnpm's strict
# ERR_PNPM_IGNORED_BUILDS gate (added in 9.15 / 10.x). With
# `version: 9` the action was resolving to a newer release that
# blocks the install over our onlyBuiltDependencies allowlist not
# being honored in this runner. 9.14.4 just runs the scripts the
# way pnpm always did, which matches what the Dockerfiles do via
# corepack `pnpm@9` and what production builds rely on.
version: "9.14.4"
- uses: actions/setup-node@v4
with:
node-version: 22
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: |
# Print the finding table to the job log first so the cause of any
# failure is visible inline (the SARIF run below writes only to file).
# The DB downloaded here is cached for the SARIF run that follows.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format table \
tehriehlbudget-backend:${SHA_SHORT} || true
# Real gate + SARIF artifact.
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)
if: always()
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format table \
tehriehlbudget-frontend:${SHA_SHORT} || true
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
# `warn` so the upload doesn't fail the run if a SARIF/SBOM is
# missing because an earlier scan step short-circuited.
if-no-files-found: warn
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: Install cosign
run: |
curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version
- name: Sign images with cosign
env:
COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -eu
# Sign by SHA tag — cosign resolves to the underlying digest, so a
# single signature covers every tag pointing at the same digest
# (version, sha, latest). Harbor looks up signatures by digest.
# Reuses the docker login from earlier in this job for registry auth.
cosign sign --key env://COSIGN_KEY --yes \
"${HARBOR_HOST}/${HARBOR_PROJECT}/backend:${SHA_SHORT}"
cosign sign --key env://COSIGN_KEY --yes \
"${HARBOR_HOST}/${HARBOR_PROJECT}/frontend:${SHA_SHORT}"
- 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