From 5aebe8abbe826a7638fd3ada142d9710a8d2503e Mon Sep 17 00:00:00 2001 From: Harold Torres Date: Wed, 1 Apr 2026 12:26:47 +1100 Subject: [PATCH] ci: add GitHub Actions workflows for CI and Cloudflare deployment and update documentation and diagramas --- .github/workflows/ci.yml | 92 ++++++++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 53 +++++++++++++++++++++ README.md | 30 +++++++++++- docs/diagrams.md | 35 ++++++++++++++ package.json | 2 + types/images.d.ts | 47 ++++++++++++++++++ 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 types/images.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..25eaa57 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel in-progress runs for the same branch/PR to avoid wasted compute +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint & Type-check + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm lint + + - name: Run TypeScript type-check + run: pnpm type-check + + test: + name: Test & Coverage + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests with coverage + run: pnpm test:ci --coverage --coverageReporters=json-summary --coverageReporters=text + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage/ + retention-days: 7 + + # Only update the badge on pushes to main (not on PRs) + - name: Generate coverage badge + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: tj-actions/coverage-badge-js@v2 + with: + output: coverage-badge.svg + + - name: Commit coverage badge + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update coverage badge [skip ci]" + file_pattern: coverage-badge.svg + commit_user_name: github-actions[bot] + commit_user_email: github-actions[bot]@users.noreply.github.com diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7ac0d4f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,53 @@ +name: Deploy Cloudflare Worker + +on: + push: + branches: [main] + paths: + - "workers/**" + - "wrangler.supabase.toml" + - ".github/workflows/deploy.yml" + +# Prevent concurrent deploys — only one at a time, never cancel mid-deploy +concurrency: + group: deploy-worker + cancel-in-progress: false + +jobs: + deploy: + name: Deploy Supabase Worker + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Deploy worker to Cloudflare + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --config wrangler.supabase.toml + secrets: | + SUPABASE_URL + SUPABASE_API_KEY + WORKER_SHARED_SECRET + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_API_KEY: ${{ secrets.SUPABASE_API_KEY }} + WORKER_SHARED_SECRET: ${{ secrets.WORKER_SHARED_SECRET }} diff --git a/README.md b/README.md index 96f7eef..e5cd4bf 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ PlotlineAI is a group movie recommendation app. Each participant shares their tastes -- favourite film, preferred era, current mood, and a favourite film personality -- and the system uses **embedding-based vector search** combined with a **language model** to surface movies the whole group will enjoy. [![Live Demo](https://img.shields.io/badge/demo-plotline--ai-blue)](https://plotline-ai.vercel.app/) +[![CI](https://github.com/CodeHunt101/plotline-ai/actions/workflows/ci.yml/badge.svg)](https://github.com/CodeHunt101/plotline-ai/actions/workflows/ci.yml) +![Coverage](./coverage-badge.svg) ## Table of Contents @@ -12,6 +14,7 @@ PlotlineAI is a group movie recommendation app. Each participant shares their ta - [Getting Started](#getting-started) - [Project Structure](#project-structure) - [Cloudflare Workers](#cloudflare-workers) +- [CI/CD](#cicd) - [AI Limitations](#ai-limitations) --- @@ -72,7 +75,7 @@ graph TD SvcTMDB --> TMDB ``` -> Full diagrams — React component tree and AI fallback circuit breaker → [`docs/diagrams.md`](./docs/diagrams.md) +> Full diagrams — React component tree, AI fallback circuit breaker, and CI/CD pipeline → [`docs/diagrams.md`](./docs/diagrams.md) --- @@ -329,6 +332,31 @@ To enable it: The workflow also supports manual runs from the **Actions** tab via `workflow_dispatch`. +## CI/CD + +This project uses GitHub Actions for continuous integration and automated Cloudflare Worker deploys. + +### Workflows + +| Workflow | Trigger | What it does | +| ------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `ci.yml` | Push / PR → `main` | ESLint, TypeScript type-check, Jest (95% coverage threshold). Uploads a coverage report artifact and auto-commits a coverage badge to `main`. | +| `deploy.yml` | Push → `main` (worker files only) | Deploys the Supabase Cloudflare Worker via Wrangler. | +| `supabase-keepalive.yml` | Daily schedule | Runs a keepalive query against Supabase so the free-tier project stays active. | + +### Required GitHub secrets + +Open **Settings → Secrets and variables → Actions** in your GitHub repo and add the following secrets: + +| Secret | Used by | Where to find it | +| ----------------------- | ------------------------ | --------------------------------------------------- | +| `SUPABASE_DB_URL` | `supabase-keepalive.yml` | Supabase dashboard → Connect → Transaction mode | +| `CLOUDFLARE_API_TOKEN` | `deploy.yml` | Cloudflare dashboard → My Profile → API Tokens | +| `CLOUDFLARE_ACCOUNT_ID` | `deploy.yml` | Cloudflare dashboard → right-hand sidebar | +| `SUPABASE_URL` | `deploy.yml` | Supabase dashboard → Project Settings → Data API | +| `SUPABASE_API_KEY` | `deploy.yml` | Supabase dashboard → Project Settings → Data API | +| `WORKER_SHARED_SECRET` | `deploy.yml` | Must match `SUPABASE_WORKER_SECRET` in `.env.local` | + ## Project Structure ``` diff --git a/docs/diagrams.md b/docs/diagrams.md index 0747473..15290d1 100644 --- a/docs/diagrams.md +++ b/docs/diagrams.md @@ -210,3 +210,38 @@ flowchart TD ValidJSON -- No --> ExtractHeuristics["fallbackMovieRecommendations
Extract titles from vector text"] ExtractHeuristics --> ReturnRec ``` + +--- + +## 5. CI/CD Pipeline + +Full lifecycle from a code change to production, via GitHub Actions and Vercel. + +```mermaid +flowchart TD + Dev(["👨‍💻 Developer\npushes code"]) + + Dev --> PR["Open Pull Request\n→ main"] + Dev --> DirectPush["Direct push\n→ main"] + + PR --> CI_PR["ci.yml\nLint · Type-check · Tests"] + CI_PR --> CIResult{"All checks\npassed?"} + + CIResult -- "❌ Fail" --> Block["PR blocked\nfix and re-push"] + Block --> PR + + CIResult -- "✅ Pass" --> VercelPreview["Vercel\nPreview Deployment\nunique URL per PR"] + VercelPreview --> Review["Code review\n+ preview testing"] + Review --> Merge["Merge PR → main"] + + DirectPush --> MainPush["Push lands on main"] + Merge --> MainPush + + MainPush --> CI_Main["ci.yml\nLint · Type-check · Tests\n+ update coverage badge"] + MainPush --> VercelProd["Vercel\nProduction Deployment\nplotline-ai.vercel.app"] + + CI_Main --> WorkerChanged{"Worker files\nchanged?"} + WorkerChanged -- No --> Done(["✅ Done"]) + WorkerChanged -- Yes --> Deploy["deploy.yml\nWrangler deploy\nCloudflare Worker"] + Deploy --> Done +``` diff --git a/package.json b/package.json index 43f1e10..7e62847 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "plotline-ai", "version": "0.1.0", "private": true, + "packageManager": "pnpm@10.15.1", "scripts": { "dev": "next dev", "build": "next build", @@ -12,6 +13,7 @@ "test": "jest", "test:ci": "jest --ci", "test:coverage": "jest --coverage", + "validate:push": "pnpm type-check && pnpm lint && pnpm test:coverage", "prepare": "husky" }, "lint-staged": { diff --git a/types/images.d.ts b/types/images.d.ts new file mode 100644 index 0000000..aaad16e --- /dev/null +++ b/types/images.d.ts @@ -0,0 +1,47 @@ +// Type declarations for static image imports (PNG, JPG, SVG, WebP, AVIF, ICO, BMP, GIF). +// Next.js generates these via next-env.d.ts at runtime, but that file is gitignored. +// This file ensures tsc resolves image imports correctly in CI and other non-Next.js contexts. +declare module "*.png" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.jpg" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.jpeg" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.webp" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.avif" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.svg" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.gif" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.ico" { + const value: import("next/image").StaticImageData; + export default value; +} + +declare module "*.bmp" { + const value: import("next/image").StaticImageData; + export default value; +}