Skip to content
62 changes: 62 additions & 0 deletions .github/scripts/ciScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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("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
};
}
};
83 changes: 83 additions & 0 deletions .github/scripts/commentResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module.exports = async ({ github, context, backend, mobile, web }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;

const statusEmoji = (status) => {
if (status === 'success') return '✅';
if (status === 'failure') return '❌';
if (status === 'skipped') return '⏭️';
return '⚪';
};

const statusLabel = (status) => {
if (status === 'skipped') return `${statusEmoji(status)} Skipped — no changes detected`;
return `${statusEmoji(status)} ${status}`;
};

const results = [backend, mobile, web];
const allSkipped = results.every((s) => s === 'skipped');
const anyFailure = results.some((s) => s === 'failure');
const allPassed = results.every((s) => s === 'success' || s === 'skipped');

let title;
if (allSkipped) {
title = '⏭️ No changes detected — all checks skipped';
} else if (anyFailure) {
title = '❌ Some checks failed';
} else if (allPassed) {
title = '✅ All checks passed';
} else {
title = '⚪ Checks completed';
}

const timestamp = new Date().toUTCString();

const body = `## CI Results — ${title}

| Check | Status |
|---|---|
| 🖥️ Backend | ${statusLabel(backend)} |
| 📱 Mobile | ${statusLabel(mobile)} |
| 🌐 Web | ${statusLabel(web)} |

> ⏭️ **Skipped** means no files were changed in that area — the check was not needed.

---
🕐 Last updated: \`${timestamp}\``;

const COMMENT_MARKER = '## CI Results —';

try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
});

const existingComment = comments.find(
(c) => c.body && c.body.startsWith(COMMENT_MARKER)
);

if (existingComment) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body,
});
console.log(`Updated existing comment: ${existingComment.id}`);
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
console.log('Created new CI results comment');
}
} catch (error) {
console.error('Failed to post comment:', error);
}
};
111 changes: 111 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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 }}

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Detect changed files
id: detect
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
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
- run: cd apps/backend && pnpm lint
- run: cd apps/backend && pnpm test
- run: cd apps/backend && pnpm typecheck

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
- run: cd apps/web && pnpm check
- 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
- run: cd apps/mobile && pnpm lint
- run: cd apps/mobile && pnpm test

comment-results:
needs:
- backend-ci
- web-ci
- mobile-ci
if: always()
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- 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 }}'
});
3 changes: 2 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down