From 57a286082528088a23a58c38935a43caad47c5fe Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Sat, 7 Dec 2024 21:10:51 -0800 Subject: [PATCH 1/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa17b36a..04bb2fab 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ git clone https://github.com//peterportal-client 4. Run `pnpm install` to install all node dependencies for the site and API. This may take a few minutes. -5. Rename the `.env.example` file in the api directory to `.env`. This includes the minimum environment variables needed for running the backend. +5. Make a copy of the `.env.example` file in the api directory and name it `.env`. This includes the minimum environment variables needed for running the backend. 6. (Optional) Set up your own PostgreSQL database and Google OAuth to be able to test features that require signing in such as leaving reviews or saving roadmaps to your account. Add additional variables/secrets to the .env file from the previous step. From f3a09d872993a4d6bf9b24b8edd7b54b000078a1 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Sun, 8 Dec 2024 14:31:52 -0800 Subject: [PATCH 2/5] remove review updated_at default --- api/drizzle/0001_heavy_xavin.sql | 1 + api/drizzle/meta/0001_snapshot.json | 634 ++++++++++++++++++++++++++++ api/drizzle/meta/_journal.json | 7 + api/src/db/schema.ts | 2 +- 4 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 api/drizzle/0001_heavy_xavin.sql create mode 100644 api/drizzle/meta/0001_snapshot.json diff --git a/api/drizzle/0001_heavy_xavin.sql b/api/drizzle/0001_heavy_xavin.sql new file mode 100644 index 00000000..b08b743a --- /dev/null +++ b/api/drizzle/0001_heavy_xavin.sql @@ -0,0 +1 @@ +ALTER TABLE "review" ALTER COLUMN "updated_at" DROP DEFAULT; \ No newline at end of file diff --git a/api/drizzle/meta/0001_snapshot.json b/api/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..305c2084 --- /dev/null +++ b/api/drizzle/meta/0001_snapshot.json @@ -0,0 +1,634 @@ +{ + "id": "5cf771c6-fe23-4061-932b-81c6c0c7a8ae", + "prevId": "150643a6-771c-4cba-b1bc-b3cb060d15ba", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.planner": { + "name": "planner", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "planner_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "years": { + "name": "years", + "type": "jsonb[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "planners_user_id_idx": { + "name": "planners_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "planner_user_id_user_id_fk": { + "name": "planner_user_id_user_id_fk", + "tableFrom": "planner", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report": { + "name": "report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "report_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "review_id": { + "name": "review_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_review_id_idx": { + "name": "reports_review_id_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_review_id_review_id_fk": { + "name": "report_review_id_review_id_fk", + "tableFrom": "report", + "tableTo": "review", + "columnsFrom": ["review_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "review_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "professor_id": { + "name": "professor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anonymous": { + "name": "anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grade_received": { + "name": "grade_received", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "for_credit": { + "name": "for_credit", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "quarter": { + "name": "quarter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "take_again": { + "name": "take_again", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "textbook": { + "name": "textbook", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "attendance": { + "name": "attendance", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "reviews_professor_id_idx": { + "name": "reviews_professor_id_idx", + "columns": [ + { + "expression": "professor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_course_id_idx": { + "name": "reviews_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_user_id_user_id_fk": { + "name": "review_user_id_user_id_fk", + "tableFrom": "review", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_review": { + "name": "unique_review", + "nullsNotDistinct": false, + "columns": ["user_id", "professor_id", "course_id"] + } + }, + "policies": {}, + "checkConstraints": { + "rating_check": { + "name": "rating_check", + "value": "\"review\".\"rating\" >= 1 AND \"review\".\"rating\" <= 5" + }, + "difficulty_check": { + "name": "difficulty_check", + "value": "\"review\".\"difficulty\" >= 1 AND \"review\".\"difficulty\" <= 5" + } + }, + "isRLSEnabled": false + }, + "public.saved_course": { + "name": "saved_course", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saved_course_user_id_user_id_fk": { + "name": "saved_course_user_id_user_id_fk", + "tableFrom": "saved_course", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saved_course_user_id_course_id_pk": { + "name": "saved_course_user_id_course_id_pk", + "columns": ["user_id", "course_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sid": { + "name": "sid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sess": { + "name": "sess", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expire": { + "name": "expire", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transferred_course": { + "name": "transferred_course", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "course_name": { + "name": "course_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "units": { + "name": "units", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "transferred_courses_user_id_idx": { + "name": "transferred_courses_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transferred_course_user_id_user_id_fk": { + "name": "transferred_course_user_id_user_id_fk", + "tableFrom": "transferred_course", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "google_id": { + "name": "google_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_roadmap_edit_at": { + "name": "last_roadmap_edit_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_google_id": { + "name": "unique_google_id", + "nullsNotDistinct": false, + "columns": ["google_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vote": { + "name": "vote", + "schema": "", + "columns": { + "review_id": { + "name": "review_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "votes_user_id_idx": { + "name": "votes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vote_review_id_review_id_fk": { + "name": "vote_review_id_review_id_fk", + "tableFrom": "vote", + "tableTo": "review", + "columnsFrom": ["review_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vote_user_id_user_id_fk": { + "name": "vote_user_id_user_id_fk", + "tableFrom": "vote", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vote_review_id_user_id_pk": { + "name": "vote_review_id_user_id_pk", + "columns": ["review_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "votes_vote_check": { + "name": "votes_vote_check", + "value": "\"vote\".\"vote\" = 1 OR \"vote\".\"vote\" = -1" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json index 28471cbc..a8247973 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1731572839686, "tag": "0000_third_nekra", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1733697058204, + "tag": "0001_heavy_xavin", + "breakpoints": true } ] } diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index f9fb1cd0..7166150d 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -56,7 +56,7 @@ export const review = pgTable( difficulty: integer('difficulty').notNull(), gradeReceived: text('grade_received').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow(), + updatedAt: timestamp('updated_at'), forCredit: boolean('for_credit').notNull(), quarter: text('quarter').notNull(), takeAgain: boolean('take_again').notNull(), From fda540a98cf8b0d3943d7ab6d5f7e0f34aabb188 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:25:46 -0800 Subject: [PATCH 3/5] fix: search page crash (#530) * Fix search page crash * Shorten gradeDistData non-empty condition --- site/src/component/GradeDist/GradeDist.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index 70fcc82a..1d517f61 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -29,14 +29,14 @@ const GradeDist: FC = (props) => { * @param props attributes received from the parent element */ - const [gradeDistData, setGradeDistData] = useState(null!); + const [gradeDistData, setGradeDistData] = useState(); const [chartType, setChartType] = useState('bar'); const [currentQuarter, setCurrentQuarter] = useState(''); const [currentProf, setCurrentProf] = useState(''); - const [profEntries, setProfEntries] = useState(null!); + const [profEntries, setProfEntries] = useState(); const [currentCourse, setCurrentCourse] = useState(''); - const [courseEntries, setCourseEntries] = useState(null!); - const [quarterEntries, setQuarterEntries] = useState(null!); + const [courseEntries, setCourseEntries] = useState(); + const [quarterEntries, setQuarterEntries] = useState(); const fetchGradeDistData = useCallback(() => { let requests: Promise[]; @@ -74,7 +74,7 @@ const GradeDist: FC = (props) => { const professors: Set = new Set(); const result: Entry[] = []; - gradeDistData.forEach((match) => match.instructors.forEach((prof) => professors.add(prof))); + gradeDistData!.forEach((match) => match.instructors.forEach((prof) => professors.add(prof))); Array.from(professors) .sort((a, b) => a.localeCompare(b)) @@ -92,7 +92,7 @@ const GradeDist: FC = (props) => { const courses: Set = new Set(); const result: Entry[] = []; - gradeDistData.forEach((match) => courses.add(match.department + ' ' + match.courseNumber)); + gradeDistData!.forEach((match) => courses.add(match.department + ' ' + match.courseNumber)); Array.from(courses) .sort((a, b) => a.localeCompare(b)) @@ -104,7 +104,7 @@ const GradeDist: FC = (props) => { // update list of professors/courses when new course/professor is detected useEffect(() => { - if (gradeDistData && gradeDistData.length !== 0) { + if (gradeDistData?.length) { if (props.course) { createProfEntries(); } else if (props.professor) { @@ -121,7 +121,7 @@ const GradeDist: FC = (props) => { const quarters: Set = new Set(); const result: Entry[] = [{ value: 'ALL', text: 'All Quarters' }]; - gradeDistData + gradeDistData! .filter((entry) => { if (props.course && entry.instructors.includes(currentProf)) { return true; @@ -156,7 +156,7 @@ const GradeDist: FC = (props) => { // update list of quarters when new professor/course is chosen useEffect(() => { - if ((currentProf || currentCourse) && gradeDistData.length !== 0) { + if ((currentProf || currentCourse) && gradeDistData?.length) { createQuarterEntries(); } }, [currentProf, currentCourse, createQuarterEntries, gradeDistData]); @@ -230,7 +230,7 @@ const GradeDist: FC = (props) => { ); - if (gradeDistData !== null && gradeDistData.length !== 0) { + if (gradeDistData?.length) { const graphProps = { gradeData: gradeDistData, quarter: currentQuarter, From 68f5e96bed9f1a92fec051c00782eed4e2d5070d Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:31:06 -0800 Subject: [PATCH 4/5] Update PR template (#527) --- .github/pull_request_template.md | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8c8259d5..524add78 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,36 +6,14 @@ ## Screenshots - - + - - +## Test Plan -## Steps to verify/test this change: - -- [ ] Verify changes work as expected on staging instance - - -## Final Checks: - -- [ ] Verify successful deployment - -(optional) - -- [ ] Write tests -- [ ] Write documentation + ## Issues - + Closes # From b966cb593b4c562f3f3724c62de50f06982f3355 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:37:00 -0800 Subject: [PATCH 5/5] refactor: split GitHub workflows (#528) * Split build and deploy workflow for prod and staging * Created actions for repeated steps * Fixed concurrency groups * Added condition to not deploy staging on fork --- .github/actions/setup-pnpm/action.yml | 27 ++++++++++ .github/workflows/build-and-deploy.yml | 74 -------------------------- .github/workflows/clean-up-pr.yml | 61 --------------------- .github/workflows/deploy-prod.yml | 46 ++++++++++++++++ .github/workflows/deploy-staging.yml | 50 +++++++++++++++++ .github/workflows/lint.yml | 28 ++-------- .github/workflows/remove-staging.yml | 41 ++++++++++++++ 7 files changed, 167 insertions(+), 160 deletions(-) create mode 100644 .github/actions/setup-pnpm/action.yml delete mode 100644 .github/workflows/build-and-deploy.yml delete mode 100644 .github/workflows/clean-up-pr.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/deploy-staging.yml create mode 100644 .github/workflows/remove-staging.yml diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml new file mode 100644 index 00000000..41137e7b --- /dev/null +++ b/.github/actions/setup-pnpm/action.yml @@ -0,0 +1,27 @@ +name: Setup pnpm +runs: + using: composite + steps: + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml deleted file mode 100644 index b8a11908..00000000 --- a/.github/workflows/build-and-deploy.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build and deploy - -on: - push: - branches: - - main - pull_request: - types: - - opened - - reopened - - synchronize - -# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - -jobs: - build_and_deploy: - name: Build and deploy PeterPortal - runs-on: ubuntu-latest - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no deploy')) - environment: - name: ${{ (github.event_name == 'pull_request' && format('staging-{0}', github.event.pull_request.number)) || 'production' }} - url: https://${{ (github.event_name == 'pull_request' && format('staging-{0}.', github.event.pull_request.number)) || '' }}peterportal.org - - steps: - - name: Check Out Repo - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Dependencies - run: pnpm install - env: - HUSKY: 0 - - - name: Build and deploy - run: pnpm sst deploy --stage ${{ (github.event_name == 'pull_request' && format('staging-{0}', github.event.pull_request.number)) || 'prod' }} - env: - CI: false - PUBLIC_API_URL: ${{secrets.PUBLIC_API_URL}} - DATABASE_URL: ${{ github.event_name == 'pull_request' && secrets.DEV_DATABASE_URL || secrets.PROD_DATABASE_URL }} - SESSION_SECRET: ${{secrets.SESSION_SECRET}} - GOOGLE_CLIENT: ${{secrets.GOOGLE_CLIENT}} - GOOGLE_SECRET: ${{secrets.GOOGLE_SECRET}} - GRECAPTCHA_SECRET: ${{secrets.GRECAPTCHA_SECRET}} - ADMIN_EMAILS: ${{secrets.ADMIN_EMAILS}} - PRODUCTION_DOMAIN: ${{secrets.PRODUCTION_DOMAIN}} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - NODE_ENV: ${{ github.event_name == 'pull_request' && 'staging' || 'production' }} - ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/clean-up-pr.yml b/.github/workflows/clean-up-pr.yml deleted file mode 100644 index d25d107d..00000000 --- a/.github/workflows/clean-up-pr.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Clean up PR - -on: - pull_request: - types: [closed] - -# use pr number for group instead of github.ref because ref will be main branch when the PR closes which is not a unique group for the PR -concurrency: - group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - clean-up-pr: - runs-on: ubuntu-latest - - steps: - - name: Check Out Repo - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Dependencies - run: pnpm install - env: - HUSKY: 0 - - - name: Remove staging stack - run: pnpm sst remove --stage staging-${{ github.event.pull_request.number }} - env: - CI: false - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - - name: Deactivate deployment - uses: strumwolf/delete-deployment-environment@v3.0.0 - with: - environment: staging-${{ github.event.pull_request.number }} - token: ${{ secrets.GITHUB_TOKEN }} - onlyDeactivateDeployments: true diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..a74d9617 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,46 @@ +name: Deploy production + +on: + push: + branches: + - main + +# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + build_and_deploy: + name: Build and deploy + runs-on: ubuntu-latest + environment: + name: production + url: https://peterportal.org + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Build and deploy + run: pnpm sst deploy --stage prod + env: + DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }} + NODE_ENV: production + + PUBLIC_API_URL: ${{ secrets.PUBLIC_API_URL }} + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + GOOGLE_CLIENT: ${{ secrets.GOOGLE_CLIENT }} + GOOGLE_SECRET: ${{ secrets.GOOGLE_SECRET }} + GRECAPTCHA_SECRET: ${{ secrets.GRECAPTCHA_SECRET }} + ADMIN_EMAILS: ${{ secrets.ADMIN_EMAILS }} + PRODUCTION_DOMAIN: ${{ secrets.PRODUCTION_DOMAIN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 00000000..0edc4a30 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,50 @@ +name: Deploy staging + +on: + pull_request: + types: + - opened + - reopened + - synchronize + +# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment +concurrency: + group: staging-${{ github.event.pull_request.number }} + +jobs: + build_and_deploy: + name: Build and deploy + runs-on: ubuntu-latest + # don't run if labeled "no deploy" && don't run on PRs from forks + if: (!contains(github.event.pull_request.labels.*.name, 'no deploy')) && github.event.pull_request.head.repo.full_name == github.repository + environment: + name: staging-${{ github.event.pull_request.number }} + url: https://staging-${{ github.event.pull_request.number }}.peterportal.org + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Build and deploy + run: pnpm sst deploy --stage staging-${{ github.event.pull_request.number }} + env: + DATABASE_URL: ${{ secrets.DEV_DATABASE_URL }} + NODE_ENV: staging + + PUBLIC_API_URL: ${{ secrets.PUBLIC_API_URL }} + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + GOOGLE_CLIENT: ${{ secrets.GOOGLE_CLIENT }} + GOOGLE_SECRET: ${{ secrets.GOOGLE_SECRET }} + GRECAPTCHA_SECRET: ${{ secrets.GRECAPTCHA_SECRET }} + ADMIN_EMAILS: ${{ secrets.ADMIN_EMAILS }} + PRODUCTION_DOMAIN: ${{ secrets.PRODUCTION_DOMAIN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea0aec03..b35febac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,34 +15,12 @@ jobs: lint: name: Lint and check formatting runs-on: ubuntu-latest - steps: - - name: Check Out Repo + - name: Checkout repo uses: actions/checkout@v4 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Install Dependencies run: pnpm install diff --git a/.github/workflows/remove-staging.yml b/.github/workflows/remove-staging.yml new file mode 100644 index 00000000..0165c1cd --- /dev/null +++ b/.github/workflows/remove-staging.yml @@ -0,0 +1,41 @@ +name: Remove staging + +on: + pull_request: + types: + - closed + +# use pr number for group instead of github.ref because ref will be main branch when the PR closes which is not a unique group for the PR +# group should match with deploy-staging workflow so those don't run concurrently (if someone closes/reopens a PR) +concurrency: + group: staging-${{ github.event.pull_request.number }} + +jobs: + clean-up-pr: + runs-on: ubuntu-latest + # don't run on PRs from forks + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install Dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Remove staging + run: pnpm sst remove --stage staging-${{ github.event.pull_request.number }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Deactivate deployment + uses: strumwolf/delete-deployment-environment@v3.0.0 + with: + environment: staging-${{ github.event.pull_request.number }} + token: ${{ secrets.GITHUB_TOKEN }} + onlyDeactivateDeployments: true