build(deps): bump the dependencies group across 1 directory with 6 updates #464
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Frontend CI/CD | |
| # Simplified Testing Strategy: TypeScript-first quality gates for solo maintainer | |
| # - CI runs only: TypeScript compilation + ESLint + Next.js build | |
| # - E2E tests (Playwright) available locally only (not in CI) | |
| # - Path-based triggering provides cost optimization | |
| # - Manual force deployment option available via workflow_dispatch | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'apps/web/**' | |
| - 'pnpm-lock.yaml' | |
| - '.github/workflows/frontend-ci.yml' | |
| - '.github/workflows/reusable-security-scan.yml' | |
| # Exclude documentation and non-functional changes to save costs | |
| - '!apps/web/**/*.md' | |
| - '!apps/web/**/*.txt' | |
| - '!apps/web/**/*.gitignore' | |
| - '!apps/web/**/README*' | |
| - '!apps/web/**/.editorconfig' | |
| - '!apps/web/**/*.test.*' # Test files (tests run locally only, not in CI) | |
| # No `paths:` filter on pull_request — deliberately. See backend-ci.yml for the | |
| # full rationale: "Lint & Type Check" is a REQUIRED status check, and a path | |
| # filter here means it never reports on a backend-only PR, so branch protection | |
| # blocks that PR forever. The workflow now always triggers and the expensive | |
| # jobs skip when apps/web is untouched (see the `changes` job) — a skipped job | |
| # still reports its check. The `push` filter above is untouched, so deploys | |
| # stay path-gated. | |
| pull_request: | |
| branches: [main] | |
| workflow_call: | |
| inputs: | |
| skip-deploy: | |
| description: 'Skip deployment steps' | |
| required: false | |
| type: boolean | |
| default: false | |
| workflow_dispatch: | |
| inputs: | |
| deploy: | |
| description: 'Deploy to production' | |
| required: true | |
| default: false | |
| type: boolean | |
| force_deploy: | |
| description: 'Force deployment even without code changes' | |
| required: false | |
| default: false | |
| type: boolean | |
| deploy_ref: | |
| description: 'Git SHA or tag to deploy (for rollbacks). Leave empty for current main.' | |
| required: false | |
| default: '' | |
| type: string | |
| env: | |
| REGISTRY: us-east1-docker.pkg.dev | |
| PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | |
| REGION: us-east1 | |
| SERVICE_NAME: nosilha-frontend | |
| IMAGE_TAG: ${{ inputs.deploy_ref || github.sha }} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Cheap path detection so the heavy jobs below can skip on PRs that do not | |
| # touch the frontend, while still reporting their (required) check as skipped. | |
| changes: | |
| name: Detect Frontend Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| frontend: ${{ steps.filter.outputs.frontend }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| frontend: | |
| - 'apps/web/**' | |
| - '!apps/web/**/*.md' | |
| - '!apps/web/**/*.txt' | |
| - '!apps/web/**/README*' | |
| - '!apps/web/**/.gitignore' | |
| - '!apps/web/**/.editorconfig' | |
| - 'pnpm-lock.yaml' | |
| - 'package.json' | |
| - '.github/workflows/frontend-ci.yml' | |
| - '.github/workflows/reusable-security-scan.yml' | |
| security-scan: | |
| name: Security Scanning | |
| needs: [changes] | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.frontend == 'true' | |
| uses: ./.github/workflows/reusable-security-scan.yml | |
| with: | |
| components: 'frontend' | |
| include-lint-analysis: true | |
| test-and-lint: | |
| name: Lint & Type Check | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| # Skips (and so reports "skipped", satisfying branch protection) when the PR | |
| # does not touch the frontend. Always runs on push/dispatch. | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.frontend == 'true' | |
| defaults: | |
| run: | |
| working-directory: ./apps/web | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup Node & pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| # Generate Velite content types before type checking | |
| - name: Generate content types | |
| run: pnpm build:content | |
| # Run linting and type checking | |
| # Note: ESLint with SARIF output is handled by reusable-security-scan.yml to avoid duplication | |
| - name: Run linting and type checks | |
| run: | | |
| # Run linting and type checking in parallel. | |
| # Bare `wait` always exits 0, so wait on each PID to propagate failures. | |
| # `|| rc=$?` keeps `set -e` from aborting before both results are captured. | |
| lint_rc=0 | |
| tsc_rc=0 | |
| pnpm lint & lint_pid=$! | |
| pnpm exec tsc --noEmit & tsc_pid=$! | |
| wait $lint_pid || lint_rc=$? | |
| wait $tsc_pid || tsc_rc=$? | |
| echo "eslint exit=$lint_rc, tsc exit=$tsc_rc" | |
| [ $lint_rc -eq 0 ] && [ $tsc_rc -eq 0 ] | |
| env: | |
| NODE_ENV: production | |
| - name: Build application | |
| run: pnpm build | |
| env: | |
| NODE_ENV: production | |
| NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }} | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_GA_ID: ${{ secrets.NEXT_PUBLIC_GA_ID }} | |
| NEXT_PUBLIC_CLARITY_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_CLARITY_PROJECT_ID }} | |
| bundle-analysis: | |
| name: Bundle Size Analysis | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| # Also gated on `changes` — this job runs a full build, and without the | |
| # workflow-level path filter it would otherwise fire on backend-only PRs. | |
| if: | | |
| github.event_name == 'pull_request' && | |
| !github.event.pull_request.draft && | |
| needs.changes.outputs.frontend == 'true' | |
| defaults: | |
| run: | |
| working-directory: ./apps/web | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Check for relevant changes | |
| id: changes | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| bundle: | |
| - 'apps/web/package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'apps/web/src/**' | |
| - 'apps/web/next.config.ts' | |
| - name: Setup pnpm | |
| if: ${{ steps.changes.outputs.bundle == 'true' }} | |
| uses: pnpm/action-setup@v6 | |
| - name: Setup Node.js 24 | |
| if: ${{ steps.changes.outputs.bundle == 'true' }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| - name: Install dependencies | |
| if: ${{ steps.changes.outputs.bundle == 'true' }} | |
| run: pnpm install --frozen-lockfile | |
| - name: Build and check bundle size | |
| if: ${{ steps.changes.outputs.bundle == 'true' }} | |
| run: | | |
| pnpm build | |
| # Check bundle size and fail if exceeds 500KB threshold | |
| echo "Analyzing bundle size..." | |
| BUNDLE_SIZE_KB=$(du -sk .next/static/chunks | cut -f1) | |
| BUNDLE_SIZE_MB=$(echo "scale=2; $BUNDLE_SIZE_KB / 1024" | bc) | |
| echo "Total bundle size: ${BUNDLE_SIZE_MB}MB (${BUNDLE_SIZE_KB}KB)" | |
| # Fail if bundle exceeds 500KB (512000KB with some buffer) | |
| if [ $BUNDLE_SIZE_KB -gt 512000 ]; then | |
| echo "❌ Bundle size exceeds 500KB limit!" | |
| echo "Current size: ${BUNDLE_SIZE_MB}MB" | |
| echo "Maximum allowed: 500KB" | |
| exit 1 | |
| else | |
| echo "✅ Bundle size within limits (max 500KB)" | |
| fi | |
| # Run bundlesize check if configured | |
| if [ -f ".bundlesizerc" ] || grep -q "bundlesize" package.json; then | |
| npx bundlesize | |
| fi | |
| env: | |
| NODE_ENV: production | |
| NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }} | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_GA_ID: ${{ secrets.NEXT_PUBLIC_GA_ID }} | |
| NEXT_PUBLIC_CLARITY_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_CLARITY_PROJECT_ID }} | |
| # Note: Path-based triggering (on.push.paths) provides cost optimization | |
| # while ensuring reliable deployment when the workflow runs | |
| build: | |
| name: Build & Package | |
| runs-on: ubuntu-latest | |
| needs: [security-scan, test-and-lint] | |
| # Always build when workflow runs (reliability over cost optimization) | |
| if: | | |
| (github.ref == 'refs/heads/main' && !inputs.skip-deploy) || | |
| (github.event_name == 'workflow_dispatch' && (inputs.deploy == true || inputs.force_deploy == true)) | |
| outputs: | |
| image-tag: ${{ steps.meta.outputs.tags }} | |
| image-digest: ${{ steps.build.outputs.digest }} | |
| defaults: | |
| run: | |
| working-directory: ./apps/web | |
| # Add OIDC permissions | |
| permissions: | |
| contents: read | |
| id-token: write # Required for OIDC token requests | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.deploy_ref || github.sha }} | |
| - name: Setup Node & pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| install-dependencies: 'false' | |
| - name: GCP Authentication | |
| uses: ./.github/actions/gcp-auth | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/nosilha-frontend/nosilha-web-ui | |
| tags: | | |
| type=ref,event=branch | |
| type=sha,prefix={{branch}}- | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./apps/web/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/nosilha-frontend/nosilha-web-ui:${{ github.sha }} | |
| ${{ github.ref == 'refs/heads/main' && format('{0}/{1}/nosilha-frontend/nosilha-web-ui:latest', env.REGISTRY, env.PROJECT_ID) || '' }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| provenance: false | |
| build-args: | | |
| NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }} | |
| NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_GA_ID=${{ secrets.NEXT_PUBLIC_GA_ID }} | |
| NEXT_PUBLIC_CLARITY_PROJECT_ID=${{ secrets.NEXT_PUBLIC_CLARITY_PROJECT_ID }} | |
| deploy-production: | |
| name: Deploy to Production | |
| runs-on: ubuntu-latest | |
| needs: [build] | |
| if: | | |
| (github.ref == 'refs/heads/main' && needs.build.result == 'success' && !inputs.skip-deploy) || | |
| (github.event_name == 'workflow_dispatch' && needs.build.result == 'success' && (inputs.deploy == true || inputs.force_deploy == true)) | |
| environment: production | |
| # Add OIDC permissions | |
| permissions: | |
| contents: read | |
| id-token: write # Required for OIDC token requests | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: GCP Authentication | |
| uses: ./.github/actions/gcp-auth | |
| with: | |
| configure-docker: 'false' | |
| - name: Deploy to Cloud Run (Production) | |
| run: | | |
| gcloud run deploy ${{ env.SERVICE_NAME }} \ | |
| --image=${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/nosilha-frontend/nosilha-web-ui:${{ github.sha }} \ | |
| --platform=managed \ | |
| --region=${{ env.REGION }} \ | |
| --service-account=nosilha-frontend-runner@${{ env.PROJECT_ID }}.iam.gserviceaccount.com \ | |
| --memory=512Mi \ | |
| --cpu=1 \ | |
| --min-instances=0 \ | |
| --max-instances=2 \ | |
| --timeout=300 | |
| - name: Health Check | |
| run: | | |
| SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} --region=${{ env.REGION }} --format='value(status.url)') | |
| echo "Service URL: $SERVICE_URL" | |
| # Wait for service to be ready | |
| sleep 30 | |
| # Basic health check | |
| curl -f "$SERVICE_URL" || (echo "Health check failed" && exit 1) | |
| - name: Warm ISR Cache | |
| run: | | |
| SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} --region=${{ env.REGION }} --format='value(status.url)') | |
| # Trigger homepage revalidation so Instagram data fetches with runtime token | |
| curl -sf -X POST "$SERVICE_URL/api/revalidate" \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Revalidate-Secret: ${{ secrets.REVALIDATE_SECRET }}" \ | |
| -d '{"path": "/"}' || echo "Cache warm-up failed (non-fatal)" |