diff --git a/.github/workflows/test-database.yaml b/.github/workflows/test-database.yaml new file mode 100644 index 000000000..c110b1ca0 --- /dev/null +++ b/.github/workflows/test-database.yaml @@ -0,0 +1,40 @@ +name: Database tests +on: + pull_request: + paths: + - "packages/database/**" +env: + SUPABASE_USE_DB: local + SUPABASE_PROJECT_ID: test + GITHUB_TEST: test + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.15.1 + run_install: false + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + - name: Install Dependencies + run: pnpm install --frozen-lockfile + # - name: Get supabase version + # working-directory: ./packages/database + # run: echo "SUPABASE_VERSION=$(./node_modules/.bin/supabase --version)" >> $GITHUB_ENV + # - name: Cache Docker images. + # uses: AndreKurait/docker-cache@0.6.0 + # with: + # key: docker-${{ runner.os }}-${{ env.SUPABASE_VERSION }} + - name: Setup database + working-directory: ./packages/database + run: pnpm run setup + - name: Serve and run tests + working-directory: ./packages/database + run: pnpm run test:withserve diff --git a/packages/database/package.json b/packages/database/package.json index b0a6c8792..a1f6594f2 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -33,6 +33,7 @@ "lint:fix": "eslint --fix . && tsx scripts/lintSchemas.ts -f && tsx scripts/lintFunctions.ts", "migrate": "tsx scripts/migrate.ts", "test": "pnpm run build && cucumber-js", + "test:withserve": "pnpm run build && tsx scripts/serve_and_test.ts", "genenv": "tsx scripts/createEnv.mts", "gentypes": "tsx scripts/genTypes.ts", "dbdiff": "supabase stop && supabase db diff --use-pg-schema", diff --git a/packages/database/scripts/createEnv.mts b/packages/database/scripts/createEnv.mts index 257de9b20..3227fd271 100644 --- a/packages/database/scripts/createEnv.mts +++ b/packages/database/scripts/createEnv.mts @@ -29,12 +29,17 @@ const getVercelToken = () => { }; const makeFnEnv = (envTxt: string): string => { - return envTxt.split('\n').filter(l=>l.match(/^SUPABASE_\w+_KEY/)).map((l)=> l.replace('SUPABASE_', 'SB_')).join('\n'); -} + return envTxt + .split("\n") + .filter((l) => l.match(/^SUPABASE_\w+_KEY/)) + .map((l) => l.replace("SUPABASE_", "SB_")) + .join("\n"); +}; const makeLocalEnv = () => { execSync("supabase start", { - cwd: projectRoot, stdio: "inherit" + cwd: projectRoot, + stdio: "inherit", }); const stdout = execSync("supabase status -o env", { encoding: "utf8", @@ -54,8 +59,8 @@ const makeLocalEnv = () => { ); writeFileSync( join(projectRoot, "supabase/functions/.env"), - makeFnEnv(prefixed) - ) + makeFnEnv(prefixed), + ); }; const makeBranchEnv = async (vercel: Vercel, vercelToken: string) => { @@ -94,11 +99,11 @@ const makeBranchEnv = async (vercel: Vercel, vercelToken: string) => { throw err; } appendFileSync(".env.branch", `NEXT_API_ROOT="https://${url}/api"\n`); - const fromVercel = readFileSync('.env.branch').toString(); + const fromVercel = readFileSync(".env.branch").toString(); writeFileSync( join(projectRoot, "supabase/functions/.env"), - makeFnEnv(fromVercel) - ) + makeFnEnv(fromVercel), + ); }; const makeProductionEnv = async (vercel: Vercel, vercelToken: string) => { @@ -117,22 +122,26 @@ const makeProductionEnv = async (vercel: Vercel, vercelToken: string) => { `vercel -t ${vercelToken} env pull --environment production .env.production`, ); appendFileSync(".env.production", `NEXT_API_ROOT="https://${url}/api"\n`); - const fromVercel = readFileSync('.env.production').toString(); + const fromVercel = readFileSync(".env.production").toString(); writeFileSync( join(projectRoot, "supabase/functions/.env"), - makeFnEnv(fromVercel) - ) + makeFnEnv(fromVercel), + ); }; const main = async (variant: Variant) => { if (process.env.ROAM_BUILD_SCRIPT) { // special case: production build try { - const response = execSync('curl https://discoursegraphs.com/api/supabase/env'); + const response = execSync( + "curl https://discoursegraphs.com/api/supabase/env", + ); const asJson = JSON.parse(response.toString()) as Record; writeFileSync( join(projectRoot, ".env"), - Object.entries(asJson).map(([k,v])=>`${k}=${v}`).join('\n') + Object.entries(asJson) + .map(([k, v]) => `${k}=${v}`) + .join("\n"), ); return; } catch (e) { @@ -140,10 +149,10 @@ const main = async (variant: Variant) => { return; throw new Error("Could not get environment from site"); } - } - else if ( + } else if ( process.env.HOME === "/vercel" || - process.env.GITHUB_ACTIONS !== undefined + (process.env.GITHUB_ACTIONS !== undefined && + process.env.GITHUB_TEST !== "test") ) // Do not execute in deployment or github action. return; diff --git a/packages/database/scripts/migrate.ts b/packages/database/scripts/migrate.ts index 42e5807ba..3b3549640 100644 --- a/packages/database/scripts/migrate.ts +++ b/packages/database/scripts/migrate.ts @@ -6,7 +6,10 @@ import { getVariant } from "@repo/database/dbDotEnv"; const __dirname = dirname(__filename); const projectRoot = join(__dirname, ".."); -if (process.env.HOME === "/vercel" || process.env.GITHUB_ACTIONS === "true") { +if ( + process.env.HOME === "/vercel" || + (process.env.GITHUB_ACTIONS === "true" && process.env.GITHUB_TEST !== "test") +) { console.log("Skipping in production environment"); process.exit(0); } diff --git a/packages/database/scripts/serve_and_test.ts b/packages/database/scripts/serve_and_test.ts new file mode 100644 index 000000000..954260ede --- /dev/null +++ b/packages/database/scripts/serve_and_test.ts @@ -0,0 +1,80 @@ +import { spawn, execSync } from "node:child_process"; +import { join, dirname } from "path"; + +const scriptDir = dirname(__filename); +const projectRoot = join(scriptDir, ".."); + +if ( + process.env.GITHUB_ACTIONS === "true" && + process.env.GITHUB_TEST !== "test" +) { + console.error("Please set the GITHUB_TEST variable to 'test'"); + process.exit(2); +} +if (process.env.SUPABASE_PROJECT_ID !== "test") { + console.error("Please set the SUPABASE_PROJECT_ID variable to 'test'"); + process.exit(2); +} + +const serve = spawn("supabase", ["functions", "serve"], { + cwd: projectRoot, + detached: true, +}); + +let resolveCallback: ((value: unknown) => void) | undefined = undefined; +let rejectCallback: ((reason: unknown) => void) | undefined = undefined; +let serveSuccess = false; +let timeoutClear: NodeJS.Timeout | undefined = undefined; + +const servingReady = new Promise((rsc, rjc) => { + resolveCallback = rsc; + rejectCallback = rjc; + + // Add timeout + timeoutClear = setTimeout(() => { + rjc(new Error("Timeout waiting for functions to serve")); + }, 30000); // 30 second timeout +}); + +serve.stdout.on("data", (data: Buffer) => { + const output = data.toString(); + console.log(`stdout: ${output}`); + if (output.includes("Serving functions ")) { + console.log("Found serving functions"); + serveSuccess = true; + clearTimeout(timeoutClear); + if (resolveCallback === undefined) throw new Error("did not get callback"); + resolveCallback(null); + } +}); +serve.on("close", () => { + if (!serveSuccess && rejectCallback) + rejectCallback(new Error("serve closed without being ready")); +}); +serve.on("error", (err) => { + if (rejectCallback) rejectCallback(err); +}); + +const doTest = async () => { + await servingReady; + try { + execSync("cucumber-js", { + cwd: projectRoot, + stdio: "inherit", + }); + // will throw on failure + } finally { + if (serve.pid) process.kill(-serve.pid); + } +}; + +doTest() + .then(() => { + console.log("success"); + clearTimeout(timeoutClear); + }) + .catch((err) => { + console.error(err); + clearTimeout(timeoutClear); + process.exit(1); + }); diff --git a/packages/database/src/dbDotEnv.mjs b/packages/database/src/dbDotEnv.mjs index 505be334c..b9025bdd7 100644 --- a/packages/database/src/dbDotEnv.mjs +++ b/packages/database/src/dbDotEnv.mjs @@ -1,5 +1,7 @@ import { readFileSync, existsSync } from "node:fs"; import { join, dirname, basename } from "node:path"; +import process from "node:process"; +import console from "node:console"; import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; @@ -14,7 +16,6 @@ const findRoot = () => { } return dir; }; - export const getVariant = () => { const useDbArgPos = (process.argv || []).indexOf("--use-db"); let variant = @@ -23,9 +24,8 @@ export const getVariant = () => { : process.env["SUPABASE_USE_DB"]; if (variant === undefined) { dotenv.config(); - const dbGlobalEnv = join(findRoot(),'.env'); - if (existsSync(dbGlobalEnv)) - dotenv.config({path: dbGlobalEnv}); + const dbGlobalEnv = join(findRoot(), ".env"); + if (existsSync(dbGlobalEnv)) dotenv.config({ path: dbGlobalEnv }); variant = process.env["SUPABASE_USE_DB"]; } const processHasVars = @@ -39,7 +39,11 @@ export const getVariant = () => { throw new Error("Invalid variant: " + variant); } - if (process.env.HOME === "/vercel" || process.env.GITHUB_ACTIONS === "true") { + if ( + process.env.HOME === "/vercel" || + (process.env.GITHUB_ACTIONS === "true" && + process.env.GITHUB_TEST !== "test") + ) { // deployment should have variables if (!processHasVars) { console.error("Missing SUPABASE variables in deployment"); @@ -76,9 +80,11 @@ export const envContents = () => { if (!path) { // Fallback to process.env when running in production environments const raw = { + /* eslint-disable @typescript-eslint/naming-convention */ SUPABASE_URL: process.env.SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY, NEXT_API_ROOT: process.env.NEXT_API_ROOT, + /* eslint-enable @typescript-eslint/naming-convention */ }; return Object.fromEntries(Object.entries(raw).filter(([, v]) => !!v)); } diff --git a/turbo.json b/turbo.json index a26a287ff..b57ff73d4 100644 --- a/turbo.json +++ b/turbo.json @@ -29,6 +29,7 @@ "GEMINI_API_KEY", "GH_CLIENT_SECRET_PROD", "GITHUB_ACTIONS", + "GITHUB_TEST", "HOME", "OPENAI_API_KEY", "POSTGRES_PASSWORD",