diff --git a/.github/scripts/ciScript.js b/.github/scripts/ciScript.js new file mode 100644 index 00000000..810a91fb --- /dev/null +++ b/.github/scripts/ciScript.js @@ -0,0 +1,83 @@ +module.exports = async ({ github, context, core }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + const prNumber = pr.number; + const prState = pr.state; + + const backendFiles = []; + const mobileFiles = []; + const webFiles = []; + + try { + if (prState === 'closed') { + console.log(`PR state is: ${prState}`); + return { + backendChanged: false, + mobileChanged: false, + webChanged: false + }; + } + + const changedFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: prNumber + } + ); + + changedFiles.forEach((file) => { + const fileName = file.filename; + + if (fileName.startsWith('apps/backend/')) { + backendFiles.push(fileName); + } else if (fileName.startsWith('apps/mobile/')) { + mobileFiles.push(fileName); + } else if (fileName.startsWith('apps/web/')) { + webFiles.push(fileName); + } + }); + + console.log({ + backendFiles, + mobileFiles, + webFiles + }); + + core.setOutput( + "backendFiles", + backendFiles + .map(file => file.replace("apps/backend/", "")) + .join(" ") + ) + + core.setOutput( + "mobileFiles", + mobileFiles + .map(file => file.replace("apps/mobile/", "")) + .join(" ") + ) + + core.setOutput( + "webFiles", + webFiles + .map(file => file.replace("apps/web/", "")) + .join(" ") + ) + + core.setOutput("backendChanged", backendFiles.length > 0) + core.setOutput("mobileChanged", mobileFiles.length > 0) + core.setOutput("webChanged", webFiles.length > 0) + + } catch (error) { + console.error(error); + + return { + backendChanged: false, + mobileChanged: false, + webChanged: false + }; + } +}; \ No newline at end of file diff --git a/.github/scripts/commentResults.js b/.github/scripts/commentResults.js new file mode 100644 index 00000000..50cd1395 --- /dev/null +++ b/.github/scripts/commentResults.js @@ -0,0 +1,101 @@ +module.exports = async ({ + github, + context, + backend, + mobile, + web, + backendLint, + backendTest, + backendTypecheck, + mobileLint, + mobileTest, + webCheck, + webBuild +}) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const prNumber = context.payload.pull_request.number; + + const emoji = (status) => { + if (status === 'success') return '✅'; + if (status === 'failure') return '❌'; + if (status === 'skipped') return '⏭️'; + return '⚪'; + }; + + const label = (status) => { + if (!status) return '⚪ unknown'; + return `${emoji(status)} ${status}`; + }; + + const anyFailure = [ + backend, + mobile, + web + ].includes('failure'); + + const title = anyFailure + ? '❌ Some checks failed' + : '✅ CI completed'; + + const timestamp = new Date().toUTCString(); + + const body = `## CI Results — ${title} + +### 🖥️ Backend (${label(backend)}) +| Check | Status | +|---|---| +| Lint | ${label(backendLint)} | +| Test | ${label(backendTest)} | +| Typecheck | ${label(backendTypecheck)} | + +### 📱 Mobile (${label(mobile)}) +| Check | Status | +|---|---| +| Lint | ${label(mobileLint)} | +| Test | ${label(mobileTest)} | + +### 🌐 Web (${label(web)}) +| Check | Status | +|---|---| +| Check | ${label(webCheck)} | +| Build | ${label(webBuild)} | + +--- +🕐 Last updated: \`${timestamp}\``; + + const COMMENT_MARKER = '## CI Results —'; + + try { + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: prNumber + } + ); + + const existing = comments.find( + c => c.body && c.body.startsWith(COMMENT_MARKER) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body + }); + } + } catch (err) { + console.error(err); + } +}; \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..25f71019 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,134 @@ +name: CI + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + detect-changes: + runs-on: ubuntu-latest + + outputs: + backendChanged: ${{ steps.detect.outputs.backendChanged }} + mobileChanged: ${{ steps.detect.outputs.mobileChanged }} + webChanged: ${{ steps.detect.outputs.webChanged }} + backendFiles: ${{ steps.detect.outputs.backendFiles }} + mobileFiles: ${{ steps.detect.outputs.mobileFiles }} + webFiles: ${{ steps.detect.outputs.webFiles }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Detect changed files + id: detect + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/ciScript.js'); + return await script({ github, context, core }); + + backend-ci: + needs: detect-changes + if: needs.detect-changes.outputs.backendChanged == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Backend lint + id: backend_lint + run: cd apps/backend && pnpm eslint ${{ needs.detect-changes.outputs.backendFiles }} + + - name: Backend test + id: backend_test + run: cd apps/backend && pnpm test ${{ needs.detect-changes.outputs.backendFiles }} + + - name: Backend typecheck + id: backend_typecheck + run: cd apps/backend && pnpm typecheck ${{ needs.detect-changes.outputs.backendFiles }} + + web-ci: + needs: detect-changes + if: needs.detect-changes.outputs.webChanged == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Web check + id: web_check + run: cd apps/web && pnpm check + + - name: Web build + id: web_build + run: cd apps/web && pnpm build + + mobile-ci: + needs: detect-changes + if: needs.detect-changes.outputs.mobileChanged == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Mobile lint + id: mobile_lint + run: cd apps/mobile && pnpm eslint ${{ needs.detect-changes.outputs.mobileFiles }} + + - name: Mobile test + id: mobile_test + run: cd apps/mobile && pnpm test + + comment-results: + needs: + - backend-ci + - web-ci + - mobile-ci + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Comment results + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/commentResults.js'); + + await script({ + github, + context, + backend: '${{ needs.backend-ci.result }}', + web: '${{ needs.web-ci.result }}', + mobile: '${{ needs.mobile-ci.result }}' + }); \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 5406427f..8bc19bf8 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -14,7 +14,8 @@ "db:migrate": "prisma migrate dev", "db:deploy": "prisma migrate deploy", "db:seed": "tsx prisma/seed.ts", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "typecheck": "tsc --noEmit" }, "dependencies": { "@devcard/shared": "workspace:*",