Skip to content

Commit 55d7d56

Browse files
authored
feat(ci): add selective backend/web/mobile checks with PR comments (#450)
* fix: Fixed linting issues * feat(ci): add selective monorepo CI and PR result comments
1 parent 5a5ebba commit 55d7d56

4 files changed

Lines changed: 258 additions & 1 deletion

File tree

.github/scripts/ciScript.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module.exports = async ({ github, context, core }) => {
2+
const owner = context.repo.owner;
3+
const repo = context.repo.repo;
4+
const pr = context.payload.pull_request;
5+
const prNumber = pr.number;
6+
const prState = pr.state;
7+
8+
const backendFiles = [];
9+
const mobileFiles = [];
10+
const webFiles = [];
11+
12+
try {
13+
if (prState === 'closed') {
14+
console.log(`PR state is: ${prState}`);
15+
return {
16+
backendChanged: false,
17+
mobileChanged: false,
18+
webChanged: false
19+
};
20+
}
21+
22+
const changedFiles = await github.paginate(
23+
github.rest.pulls.listFiles,
24+
{
25+
owner,
26+
repo,
27+
pull_number: prNumber
28+
}
29+
);
30+
31+
changedFiles.forEach((file) => {
32+
const fileName = file.filename;
33+
34+
if (fileName.startsWith('apps/backend/')) {
35+
backendFiles.push(fileName);
36+
} else if (fileName.startsWith('apps/mobile/')) {
37+
mobileFiles.push(fileName);
38+
} else if (fileName.startsWith('apps/web/')) {
39+
webFiles.push(fileName);
40+
}
41+
});
42+
43+
console.log({
44+
backendFiles,
45+
mobileFiles,
46+
webFiles
47+
});
48+
49+
core.setOutput("backendChanged",backendFiles.length > 0)
50+
core.setOutput("mobileChanged",mobileFiles.length > 0)
51+
core.setOutput("webChanged",webFiles.length > 0)
52+
53+
} catch (error) {
54+
console.error(error);
55+
56+
return {
57+
backendChanged: false,
58+
mobileChanged: false,
59+
webChanged: false
60+
};
61+
}
62+
};

.github/scripts/commentResults.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module.exports = async ({ github, context, backend, mobile, web }) => {
2+
const owner = context.repo.owner;
3+
const repo = context.repo.repo;
4+
const pr = context.payload.pull_request;
5+
const prNumber = pr.number;
6+
7+
const statusEmoji = (status) => {
8+
if (status === 'success') return '✅';
9+
if (status === 'failure') return '❌';
10+
if (status === 'skipped') return '⏭️';
11+
return '⚪';
12+
};
13+
14+
const statusLabel = (status) => {
15+
if (status === 'skipped') return `${statusEmoji(status)} Skipped — no changes detected`;
16+
return `${statusEmoji(status)} ${status}`;
17+
};
18+
19+
const results = [backend, mobile, web];
20+
const allSkipped = results.every((s) => s === 'skipped');
21+
const anyFailure = results.some((s) => s === 'failure');
22+
const allPassed = results.every((s) => s === 'success' || s === 'skipped');
23+
24+
let title;
25+
if (allSkipped) {
26+
title = '⏭️ No changes detected — all checks skipped';
27+
} else if (anyFailure) {
28+
title = '❌ Some checks failed';
29+
} else if (allPassed) {
30+
title = '✅ All checks passed';
31+
} else {
32+
title = '⚪ Checks completed';
33+
}
34+
35+
const timestamp = new Date().toUTCString();
36+
37+
const body = `## CI Results — ${title}
38+
39+
| Check | Status |
40+
|---|---|
41+
| 🖥️ Backend | ${statusLabel(backend)} |
42+
| 📱 Mobile | ${statusLabel(mobile)} |
43+
| 🌐 Web | ${statusLabel(web)} |
44+
45+
> ⏭️ **Skipped** means no files were changed in that area — the check was not needed.
46+
47+
---
48+
🕐 Last updated: \`${timestamp}\``;
49+
50+
const COMMENT_MARKER = '## CI Results —';
51+
52+
try {
53+
const comments = await github.paginate(github.rest.issues.listComments, {
54+
owner,
55+
repo,
56+
issue_number: prNumber,
57+
});
58+
59+
const existingComment = comments.find(
60+
(c) => c.body && c.body.startsWith(COMMENT_MARKER)
61+
);
62+
63+
if (existingComment) {
64+
await github.rest.issues.updateComment({
65+
owner,
66+
repo,
67+
comment_id: existingComment.id,
68+
body,
69+
});
70+
console.log(`Updated existing comment: ${existingComment.id}`);
71+
} else {
72+
await github.rest.issues.createComment({
73+
owner,
74+
repo,
75+
issue_number: prNumber,
76+
body,
77+
});
78+
console.log('Created new CI results comment');
79+
}
80+
} catch (error) {
81+
console.error('Failed to post comment:', error);
82+
}
83+
};

.github/workflows/ci.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: CI
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
pull-requests: write
9+
10+
jobs:
11+
detect-changes:
12+
runs-on: ubuntu-latest
13+
14+
outputs:
15+
backendChanged: ${{ steps.detect.outputs.backendChanged }}
16+
mobileChanged: ${{ steps.detect.outputs.mobileChanged }}
17+
webChanged: ${{ steps.detect.outputs.webChanged }}
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22+
23+
- name: Detect changed files
24+
id: detect
25+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
26+
with:
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
script: |
29+
const script = require('./.github/scripts/ciScript.js');
30+
return await script({ github, context, core });
31+
32+
backend-ci:
33+
needs: detect-changes
34+
if: needs.detect-changes.outputs.backendChanged == 'true'
35+
runs-on: ubuntu-latest
36+
37+
steps:
38+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
39+
40+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
41+
with:
42+
node-version: 22
43+
44+
- uses: pnpm/action-setup@v6.0.8
45+
46+
- run: pnpm install
47+
- run: cd apps/backend && pnpm lint
48+
- run: cd apps/backend && pnpm test
49+
- run: cd apps/backend && pnpm typecheck
50+
51+
web-ci:
52+
needs: detect-changes
53+
if: needs.detect-changes.outputs.webChanged == 'true'
54+
runs-on: ubuntu-latest
55+
56+
steps:
57+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
58+
59+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
60+
with:
61+
node-version: 22
62+
63+
- uses: pnpm/action-setup@v6.0.8
64+
65+
- run: pnpm install
66+
- run: cd apps/web && pnpm check
67+
- run: cd apps/web && pnpm build
68+
69+
mobile-ci:
70+
needs: detect-changes
71+
if: needs.detect-changes.outputs.mobileChanged == 'true'
72+
runs-on: ubuntu-latest
73+
74+
steps:
75+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
76+
77+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
78+
with:
79+
node-version: 22
80+
81+
- uses: pnpm/action-setup@v6.0.8
82+
83+
- run: pnpm install
84+
- run: cd apps/mobile && pnpm lint
85+
- run: cd apps/mobile && pnpm test
86+
87+
comment-results:
88+
needs:
89+
- backend-ci
90+
- web-ci
91+
- mobile-ci
92+
if: always()
93+
runs-on: ubuntu-latest
94+
95+
steps:
96+
- name: Checkout repository
97+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
98+
99+
- name: Comment results
100+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
101+
with:
102+
github-token: ${{ secrets.GITHUB_TOKEN }}
103+
script: |
104+
const script = require('./.github/scripts/commentResults.js');
105+
await script({
106+
github,
107+
context,
108+
backend: '${{ needs.backend-ci.result }}',
109+
web: '${{ needs.web-ci.result }}',
110+
mobile: '${{ needs.mobile-ci.result }}'
111+
});

apps/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"db:migrate": "prisma migrate dev",
1515
"db:deploy": "prisma migrate deploy",
1616
"db:seed": "tsx prisma/seed.ts",
17-
"db:studio": "prisma studio"
17+
"db:studio": "prisma studio",
18+
"typecheck": "tsc --noEmit"
1819
},
1920
"dependencies": {
2021
"@devcard/shared": "workspace:*",

0 commit comments

Comments
 (0)