diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..53525f5 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,129 @@ +name: frontend-ci + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + +env: + IMAGE: ${{ secrets.HARBOR_HOST }}/movieloop/frontend + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci + - run: npx eslint . + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx tsc -b + + secrets-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + + sast: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: returntocorp/semgrep-action@v1 + with: + config: "p/auto" + + fs-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: aquasecurity/trivy-action@master + with: + scan-type: fs + severity: "HIGH,CRITICAL" + exit-code: "1" + ignore-unfixed: "true" + + build: + runs-on: ubuntu-latest + needs: [lint, typecheck] + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: production + tags: movieloop-frontend:ci-${{ github.sha }} + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + image-scan: + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: production + tags: movieloop-frontend:ci-${{ github.sha }} + load: true + cache-from: type=gha + - uses: aquasecurity/trivy-action@master + with: + image-ref: movieloop-frontend:ci-${{ github.sha }} + severity: "HIGH,CRITICAL" + exit-code: "1" + ignore-unfixed: "true" + + push: + runs-on: ubuntu-latest + needs: [build, image-scan, secrets-scan, sast, fs-scan] + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ secrets.HARBOR_HOST }} + username: ${{ secrets.MOVIELOOP_USERNAME }} + password: ${{ secrets.MOVIELOOP_PASSWORD }} + - uses: docker/metadata-action@v5 + id: meta + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=sha,format=long + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: production + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..e8d5951 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,9 @@ +# Gitleaks config for the movieloop frontend repo. +# Inherits the default rule set; allows .env.example by path. +[extend] +useDefault = true + +[allowlist] +paths = [ + '''(^|/)\.env\.example$''', +] diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..2c9263d --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +public/ diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..4f760ef --- /dev/null +++ b/.trivyignore @@ -0,0 +1,5 @@ +# Suppressed vulnerabilities for movieloop-frontend. +# Format: +# CVE-YYYY-NNNNN # one-line reason — review by YYYY-MM-DD +# +# Keep this list short. Each entry should have an owner and a re-review date. diff --git a/.versionrc.json b/.versionrc.json new file mode 100644 index 0000000..d36283b --- /dev/null +++ b/.versionrc.json @@ -0,0 +1,14 @@ +{ + "types": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "refactor", "section": "Refactor" }, + { "type": "docs", "hidden": true }, + { "type": "chore", "hidden": true }, + { "type": "test", "hidden": true }, + { "type": "ci", "hidden": true }, + { "type": "build", "hidden": true }, + { "type": "style", "hidden": true } + ] +} diff --git a/Dockerfile b/Dockerfile index a62f511..b6a824f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,10 @@ EXPOSE 5173 CMD ["npm", "run", "dev", "--", "--host"] # --- Build stage --- +# No VITE_API_URL build arg: the production image uses runtime config (/config.js) +# rendered at container startup. See docker/40-render-config.sh. FROM node:22-alpine AS build WORKDIR /app -ARG VITE_API_URL -ENV VITE_API_URL=$VITE_API_URL COPY package*.json ./ RUN npm ci COPY . . @@ -19,7 +19,11 @@ RUN npm run build # --- Production stage --- FROM nginx:alpine AS production +RUN apk add --no-cache gettext COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY docker/config.js.template /etc/nginx/templates/config.js.template +COPY docker/40-render-config.sh /docker-entrypoint.d/40-render-config.sh +RUN chmod +x /docker-entrypoint.d/40-render-config.sh EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +# nginx:alpine's upstream entrypoint runs /docker-entrypoint.d/*.sh then launches nginx. diff --git a/docker/40-render-config.sh b/docker/40-render-config.sh new file mode 100755 index 0000000..8317fa2 --- /dev/null +++ b/docker/40-render-config.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Render /config.js from the env var API_URL at container start. +# Runs as part of the official nginx:alpine /docker-entrypoint.d sequence, +# so nginx itself is launched by the upstream entrypoint after this script. +set -e +: "${API_URL:=/api/v1}" +export API_URL +envsubst < /etc/nginx/templates/config.js.template > /usr/share/nginx/html/config.js +echo "[config] API_URL=${API_URL} -> /config.js" diff --git a/docker/config.js.template b/docker/config.js.template new file mode 100644 index 0000000..49cc300 --- /dev/null +++ b/docker/config.js.template @@ -0,0 +1 @@ +window.__APP_CONFIG__ = { apiUrl: "${API_URL}" }; diff --git a/index.html b/index.html index 4083546..f9e8e7b 100644 --- a/index.html +++ b/index.html @@ -29,6 +29,7 @@ })();
+