Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions .github/scripts/ciScript.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
const isTestFile = (file) => /\.(test|spec)\.[jt]sx?$/.test(file);

const deriveTestFiles = (files) => {
return files.map((file) => {
if (isTestFile(file)) return file;

const withoutExt = file.replace(/\.[jt]sx?$/, '');
const parts = withoutExt.split('/');
const baseName = parts[parts.length - 1];
const dir = parts.slice(0, -1).join('/');

return `${dir}/__tests__/${baseName}.test.ts`;
});
};

module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
Expand Down Expand Up @@ -40,15 +55,19 @@ module.exports = async ({ github, context, core }) => {
}
});

console.log({
backendFiles,
mobileFiles,
webFiles
});
const strippedBackend = backendFiles.map(f => f.replace('apps/backend/', ''));
const strippedMobile = mobileFiles.map(f => f.replace('apps/mobile/', ''));

console.log({ backendFiles, mobileFiles, webFiles });

core.setOutput("backendChanged",backendFiles.length > 0)
core.setOutput("mobileChanged",mobileFiles.length > 0)
core.setOutput("webChanged",webFiles.length > 0)
core.setOutput('backendFiles', strippedBackend.join(' '));
core.setOutput('mobileFiles', strippedMobile.join(' '));
core.setOutput('webFiles', webFiles.map(f => f.replace('apps/web/', '')).join(' '));
core.setOutput('backendTestFiles', deriveTestFiles(strippedBackend).join(' '));
core.setOutput('mobileTestFiles', deriveTestFiles(strippedMobile).join(' '));
core.setOutput('backendChanged', backendFiles.length > 0);
core.setOutput('mobileChanged', mobileFiles.length > 0);
core.setOutput('webChanged', webFiles.length > 0);

} catch (error) {
console.error(error);
Expand Down
115 changes: 66 additions & 49 deletions .github/scripts/commentResults.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,100 @@
module.exports = async ({ github, context, backend, mobile, web }) => {
module.exports = async ({
github,
context,
backend,
mobile,
web,
backendLint,
backendTest,
backendTypecheck,
mobileLint,
mobileTest,
webCheck,
webBuild,
backendLintOutput,
mobileLintOutput,
}) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
const prNumber = context.payload.pull_request.number;

const statusEmoji = (status) => {
if (status === 'success') return '';
if (status === 'failure') return '';
if (status === 'skipped') return '⏭️';
return '';
const status = (s) => {
if (s === 'success') return 'PASS';
if (s === 'failure') return 'FAIL';
if (s === 'skipped') return 'SKIP';
return '-';
};

const statusLabel = (status) => {
if (status === 'skipped') return `${statusEmoji(status)} Skipped — no changes detected`;
return `${statusEmoji(status)} ${status}`;
const lintDetails = (output) => {
if (!output || !output.trim()) return '';
return `\n<details>\n<summary>View lint errors</summary>\n\n\`\`\`\n${output.trim()}\n\`\`\`\n</details>`;
};

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');
const anyFailure = [backend, mobile, web].includes('failure');
const title = anyFailure ? 'CI — Checks Failed' : 'CI — All Checks Passed';
const timestamp = new Date().toUTCString();

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 body = `## ${title}

const timestamp = new Date().toUTCString();
### Backend — ${status(backend)}

| Check | Result |
|---|---|
| Lint | ${status(backendLint)} |
| Test | ${status(backendTest)} |
| Typecheck | ${status(backendTypecheck)} |
${backendLint === 'failure' ? lintDetails(backendLintOutput) : ''}

const body = `## CI Results — ${title}
### Mobile — ${status(mobile)}

| Check | Status |
| Check | Result |
|---|---|
| 🖥️ Backend | ${statusLabel(backend)} |
| 📱 Mobile | ${statusLabel(mobile)} |
| 🌐 Web | ${statusLabel(web)} |
| Lint | ${status(mobileLint)} |
| Test | ${status(mobileTest)} |
${mobileLint === 'failure' ? lintDetails(mobileLintOutput) : ''}

> ⏭️ **Skipped** means no files were changed in that area — the check was not needed.
### Web — ${status(web)}

| Check | Result |
|---|---|
| Check | ${status(webCheck)} |
| Build | ${status(webBuild)} |

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

const COMMENT_MARKER = '## CI Results —';
const COMMENT_MARKER = '## CI —';

try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
});
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)
const existing = comments.find(
c => c.body && c.body.startsWith(COMMENT_MARKER)
);

if (existingComment) {
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body,
comment_id: existing.id,
body
});
console.log(`Updated existing comment: ${existingComment.id}`);
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
body
});
console.log('Created new CI results comment');
}
} catch (error) {
console.error('Failed to post comment:', error);
} catch (err) {
console.error(err);
}
};
121 changes: 101 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ jobs:
runs-on: ubuntu-latest

outputs:
backendChanged: ${{ steps.detect.outputs.backendChanged }}
mobileChanged: ${{ steps.detect.outputs.mobileChanged }}
webChanged: ${{ steps.detect.outputs.webChanged }}
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 }}
backendTestFiles: ${{ steps.detect.outputs.backendTestFiles }}
mobileTestFiles: ${{ steps.detect.outputs.mobileTestFiles }}

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ github.event.pull_request.head.sha }}


- name: Detect changed files
id: detect
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
Expand All @@ -34,8 +41,15 @@ jobs:
if: needs.detect-changes.outputs.backendChanged == 'true'
runs-on: ubuntu-latest

outputs:
lint_result: ${{ steps.backend_lint.outcome }}
test_result: ${{ steps.backend_test.outcome }}
typecheck_result: ${{ steps.backend_typecheck.outcome }}

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
Expand All @@ -44,17 +58,43 @@ jobs:
- 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

- name: Backend lint
id: backend_lint
continue-on-error: true
run: cd apps/backend && pnpm eslint ${{ needs.detect-changes.outputs.backendFiles }}

- name: Backend test
id: backend_test
if: needs.detect-changes.outputs.backendTestFiles != ''
continue-on-error: true
run: cd apps/backend && pnpm test --passWithNoTests ${{ needs.detect-changes.outputs.backendTestFiles }}

- name: Backend typecheck
id: backend_typecheck
continue-on-error: true
run: cd apps/backend && pnpm typecheck

- name: Fail job if any check failed
if: >
steps.backend_lint.outcome == 'failure' ||
steps.backend_test.outcome == 'failure' ||
steps.backend_typecheck.outcome == 'failure'
run: exit 1

web-ci:
needs: detect-changes
if: needs.detect-changes.outputs.webChanged == 'true'
runs-on: ubuntu-latest

outputs:
check_result: ${{ steps.web_check.outcome }}
build_result: ${{ steps.web_build.outcome }}

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
Expand All @@ -63,16 +103,36 @@ jobs:
- uses: pnpm/action-setup@v6.0.8

- run: pnpm install
- run: cd apps/web && pnpm check
- run: cd apps/web && pnpm build

- name: Web check
id: web_check
continue-on-error: true
run: cd apps/web && pnpm check

- name: Web build
id: web_build
continue-on-error: true
run: cd apps/web && pnpm build

- name: Fail job if any check failed
if: >
steps.web_check.outcome == 'failure' ||
steps.web_build.outcome == 'failure'
run: exit 1

mobile-ci:
needs: detect-changes
if: needs.detect-changes.outputs.mobileChanged == 'true'
runs-on: ubuntu-latest

outputs:
lint_result: ${{ steps.mobile_lint.outcome }}
test_result: ${{ steps.mobile_test.outcome }}

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
Expand All @@ -81,8 +141,23 @@ jobs:
- uses: pnpm/action-setup@v6.0.8

- run: pnpm install
- run: cd apps/mobile && pnpm lint
- run: cd apps/mobile && pnpm test

- name: Mobile lint
id: mobile_lint
continue-on-error: true
run: cd apps/mobile && pnpm eslint ${{ needs.detect-changes.outputs.mobileFiles }}

- name: Mobile test
id: mobile_test
if: needs.detect-changes.outputs.mobileTestFiles != ''
continue-on-error: true
run: cd apps/mobile && pnpm test --passWithNoTests ${{ needs.detect-changes.outputs.mobileTestFiles }}

- name: Fail job if any check failed
if: >
steps.mobile_lint.outcome == 'failure' ||
steps.mobile_test.outcome == 'failure'
run: exit 1

comment-results:
needs:
Expand All @@ -93,19 +168,25 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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({
await script({
github,
context,
backend: '${{ needs.backend-ci.result }}',
web: '${{ needs.web-ci.result }}',
mobile: '${{ needs.mobile-ci.result }}'
});
backend: '${{ needs.backend-ci.result }}',
web: '${{ needs.web-ci.result }}',
mobile: '${{ needs.mobile-ci.result }}',
backendLint: '${{ needs.backend-ci.outputs.lint_result }}',
backendTest: '${{ needs.backend-ci.outputs.test_result }}',
backendTypecheck: '${{ needs.backend-ci.outputs.typecheck_result }}',
webCheck: '${{ needs.web-ci.outputs.check_result }}',
webBuild: '${{ needs.web-ci.outputs.build_result }}',
mobileLint: '${{ needs.mobile-ci.outputs.lint_result }}',
mobileTest: '${{ needs.mobile-ci.outputs.test_result }}',
});