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
71 changes: 71 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: HRM Dashboard - CI

on:
push:
branches:
- leader
pull_request:
branches:
- leader

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Type check
run: pnpm exec tsc --noEmit

- name: Knip
run: pnpm run knip

- name: Format check
run: pnpm run format:check

- name: Security audit
run: pnpm audit --prod --audit-level=high

- name: Validate PR commits
if: github.event_name == 'pull_request'
id: lint
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

- name: Post Comment on Failure
if: failure() && steps.lint.conclusion == 'failure'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
:no_entry: **Commit Message Linting Failed**

One or more of your commit messages do not follow the project's conventions.
Please review the [commitlint logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.

You can also refer to the [commit message guidelines](https://github.com/conventional-changelog/commitlint/#what-is-commitlint) for help.
33 changes: 0 additions & 33 deletions .github/workflows/commit-lint.yml

This file was deleted.

52 changes: 52 additions & 0 deletions scripts/utils/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readFile } from 'fs/promises';

export function getChangedFilesFromDiff(diff: string): string[] {
const files: string[] = [];
const lines = diff.split('\n');
for (const line of lines) {
if (line.startsWith('diff --git')) {
const pathLine = line.substring('diff --git '.length);
let filePathA;
if (pathLine.startsWith('"')) {
const endIndex = pathLine.indexOf('"', 1);
filePathA = pathLine.substring(1, endIndex);
} else {
filePathA = pathLine.split(' ')[0];
}

if (filePathA && filePathA.startsWith('a/')) {
files.push(filePathA.substring(2));
}
}
}
return files;
}

export function parseSpecializedRules(content: string): { key: string; value: string }[] {
const rules: { key: string; value: string }[] = [];
const lines = content.split('\n');
for (const line of lines) {
if (line.startsWith('#')) {
const trimmedLine = line.substring(1).trim();
const parts = trimmedLine.split(':');
if (parts.length >= 2) {
const key = parts[0].trim();

Check failure on line 33 in scripts/utils/rules.ts

View workflow job for this annotation

GitHub Actions / pr-quality / 🏗️ Build Check

Object is possibly 'undefined'.
const value = parts.slice(1).join(':').trim();
if (key && value) {
rules.push({ key, value });
}
}
}
}
return rules;
}

export async function getSpecializedRules(filePath: string): Promise<Record<string, string>> {
const content = await readFile(filePath, 'utf-8');
const rules = parseSpecializedRules(content);
const rulesObject: Record<string, string> = {};
for (const rule of rules) {
rulesObject[rule.key] = rule.value;
}
return rulesObject;
}
116 changes: 116 additions & 0 deletions tests/unit/scripts/utils/rules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
parseSpecializedRules,
getSpecializedRules,
getChangedFilesFromDiff,
} from '../../../../scripts/utils/rules';
import { readFile } from 'fs/promises';

// Mock the fs/promises module
jest.mock('fs/promises', () => ({
readFile: jest.fn(),
}));

// Type cast the mock for easier use
const mockedReadFile = readFile as jest.Mock;

describe('Specialized Rules', () => {
afterEach(() => {
jest.clearAllMocks();
});

describe('getChangedFilesFromDiff', () => {
it('should extract changed files from a standard git diff', () => {
const diff = `
diff --git a/file1.txt b/file1.txt
index e69de29..d00491f 100644
--- a/file1.txt
+++ b/file1.txt
@@ -0,0 +1 @@
+hello
diff --git a/src/component.tsx b/src/component.tsx
index 8b13789..9e6a2c1 100644
--- a/src/component.tsx
+++ b/src/component.tsx
@@ -1,1 +1,1 @@
-<div>old</div>
+<div>new</div>
`;
const expected = ['file1.txt', 'src/component.tsx'];
expect(getChangedFilesFromDiff(diff).sort()).toEqual(expected.sort());
});

it('should return an empty array for an empty diff string', () => {
expect(getChangedFilesFromDiff('')).toEqual([]);
});

it('should handle file paths with spaces', () => {
const diff = 'diff --git "a/path with spaces/file.js" "b/path with spaces/file.js"';
expect(getChangedFilesFromDiff(diff)).toEqual(['path with spaces/file.js']);
});
});

describe('parseSpecializedRules', () => {
it('should parse rules from commented lines', () => {
const content = `
# rule: require-tests
# level: high
# description: All new features must have tests.
const x = 1;
`;
const expected = [
{ key: 'rule', value: 'require-tests' },
{ key: 'level', value: 'high' },
{ key: 'description', value: 'All new features must have tests.' },
];
expect(parseSpecializedRules(content)).toEqual(expected);
});

it('should return an empty array for content with no rule comments', () => {
const content = 'const y = 2;';
expect(parseSpecializedRules(content)).toEqual([]);
});

it('should ignore lines that are not valid key-value pairs', () => {
const content = `
# rule: valid
# not a rule
# another-rule: also valid
`;
const expected = [
{ key: 'rule', value: 'valid' },
{ key: 'another-rule', value: 'also valid' },
];
expect(parseSpecializedRules(content)).toEqual(expected);
});
});

describe('getSpecializedRules', () => {
it('should read a file and return a rule object', async () => {
const filePath = 'a/file/with/rules.md';
const fileContent = `
# max-length: 80
# min-length: 10
`;
mockedReadFile.mockResolvedValue(fileContent);
const expected = {
'max-length': '80',
'min-length': '10',
};
await expect(getSpecializedRules(filePath)).resolves.toEqual(expected);
expect(readFile).toHaveBeenCalledWith(filePath, 'utf-8');
});

it('should return an empty object if the file contains no rules', async () => {
const filePath = 'a/file/without/rules.txt';
mockedReadFile.mockResolvedValue('Some content');
await expect(getSpecializedRules(filePath)).resolves.toEqual({});
});

it('should propagate errors from readFile', async () => {
const filePath = 'nonexistent/file.txt';
const error = new Error('File not found');
mockedReadFile.mockRejectedValue(error);
await expect(getSpecializedRules(filePath)).rejects.toThrow('File not found');
});
});
});
Loading