Merge pull request #485 from frauniki/add-prettier-ci

chore: Add Prettier format check to CI and format code
This commit is contained in:
GPT8
2025-06-15 12:18:31 +03:00
committed by GitHub
13 changed files with 153 additions and 112 deletions
+18
View File
@@ -51,3 +51,21 @@ jobs:
run: npm run build run: npm run build
- name: Run E2E Tests - name: Run E2E Tests
run: npm run test:e2e run: npm run test:e2e
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Prettier
run: npm run format:check
- name: Prettier Output
if: failure()
run: |
echo "Prettier check failed. Please run 'npm run format' to fix formatting issues."
exit 1
+1
View File
@@ -51,6 +51,7 @@
"deploy:patch": "npm version patch && npm run deploy:build", "deploy:patch": "npm version patch && npm run deploy:build",
"lint": "eslint src --ext ts && tsc --noEmit", "lint": "eslint src --ext ts && tsc --noEmit",
"format": "prettier --write src", "format": "prettier --write src",
"format:check": "prettier --check src",
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit", "test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
"test:all": "npm run test:unit:docker && npm run test:e2e:docker", "test:all": "npm run test:unit:docker && npm run test:e2e:docker",
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .", "test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
+6 -4
View File
@@ -23,7 +23,10 @@ export class MistralAiEngine implements AiEngine {
if (!config.baseURL) { if (!config.baseURL) {
this.client = new Mistral({ apiKey: config.apiKey }); this.client = new Mistral({ apiKey: config.apiKey });
} else { } else {
this.client = new Mistral({ apiKey: config.apiKey, serverURL: config.baseURL }); this.client = new Mistral({
apiKey: config.apiKey,
serverURL: config.baseURL
});
} }
} }
@@ -50,13 +53,12 @@ export class MistralAiEngine implements AiEngine {
const completion = await this.client.chat.complete(params); const completion = await this.client.chat.complete(params);
if (!completion.choices) if (!completion.choices) throw Error('No completion choice available.');
throw Error('No completion choice available.')
const message = completion.choices[0].message; const message = completion.choices[0].message;
if (!message || !message.content) if (!message || !message.content)
throw Error('No completion choice available.') throw Error('No completion choice available.');
let content = message.content as string; let content = message.content as string;
return removeContentTags(content, 'think'); return removeContentTags(content, 'think');
+36 -36
View File
@@ -6,42 +6,42 @@ import { AiEngine, AiEngineConfig } from './Engine';
interface MLXConfig extends AiEngineConfig {} interface MLXConfig extends AiEngineConfig {}
export class MLXEngine implements AiEngine { export class MLXEngine implements AiEngine {
config: MLXConfig; config: MLXConfig;
client: AxiosInstance; client: AxiosInstance;
constructor(config) { constructor(config) {
this.config = config; this.config = config;
this.client = axios.create({ this.client = axios.create({
url: config.baseURL url: config.baseURL
? `${config.baseURL}/${config.apiKey}` ? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:8080/v1/chat/completions', : 'http://localhost:8080/v1/chat/completions',
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const params = {
messages,
temperature: 0,
top_p: 0.1,
repetition_penalty: 1.5,
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const choices = response.data.choices;
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
} }
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>):
Promise<string | undefined> {
const params = {
messages,
temperature: 0,
top_p: 0.1,
repetition_penalty: 1.5,
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const choices = response.data.choices;
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
}
}
} }
+6 -3
View File
@@ -14,7 +14,10 @@ const generateCommitMessageChatCompletionPrompt = async (
fullGitMojiSpec: boolean, fullGitMojiSpec: boolean,
context: string context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => { ): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec, context); const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
fullGitMojiSpec,
context
);
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT]; const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
@@ -38,7 +41,7 @@ const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async ( export const generateCommitMessageByDiff = async (
diff: string, diff: string,
fullGitMojiSpec: boolean = false, fullGitMojiSpec: boolean = false,
context: string = "" context: string = ''
): Promise<string> => { ): Promise<string> => {
try { try {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt( const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
@@ -75,7 +78,7 @@ export const generateCommitMessageByDiff = async (
const messages = await generateCommitMessageChatCompletionPrompt( const messages = await generateCommitMessageChatCompletionPrompt(
diff, diff,
fullGitMojiSpec, fullGitMojiSpec,
context, context
); );
const engine = getEngine(); const engine = getEngine();
+48 -37
View File
@@ -56,10 +56,11 @@ const llmReadableRules: {
blankline: (key, applicable) => blankline: (key, applicable) =>
`There should ${applicable} be a blank line at the beginning of the ${key}.`, `There should ${applicable} be a blank line at the beginning of the ${key}.`,
caseRule: (key, applicable, value: string | Array<string>) => caseRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be in ${Array.isArray(value) `The ${key} should ${applicable} be in ${
? `one of the following case: Array.isArray(value)
? `one of the following case:
- ${value.join('\n - ')}.` - ${value.join('\n - ')}.`
: `${value} case.` : `${value} case.`
}`, }`,
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`, emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
enumRule: (key, applicable, value: string | Array<string>) => enumRule: (key, applicable, value: string | Array<string>) =>
@@ -67,17 +68,18 @@ const llmReadableRules: {
- ${Array.isArray(value) ? value.join('\n - ') : value}.`, - ${Array.isArray(value) ? value.join('\n - ') : value}.`,
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) => enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
`The ${key} should ${applicable} be one of the following values: `The ${key} should ${applicable} be one of the following values:
- ${Array.isArray(value) - ${
Array.isArray(value)
? value ? value
.map((v) => { .map((v) => {
const description = getTypeRuleExtraDescription(v, prompt); const description = getTypeRuleExtraDescription(v, prompt);
if (description) { if (description) {
return `${v} (${description})`; return `${v} (${description})`;
} else return v; } else return v;
}) })
.join('\n - ') .join('\n - ')
: value : value
}.`, }.`,
fullStopRule: (key, applicable, value: string) => fullStopRule: (key, applicable, value: string) =>
`The ${key} should ${applicable} end with '${value}'.`, `The ${key} should ${applicable} end with '${value}'.`,
maxLengthRule: (key, applicable, value: string) => maxLengthRule: (key, applicable, value: string) =>
@@ -214,16 +216,20 @@ const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
const GEN_COMMITLINT_CONSISTENCY_PROMPT = ( const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
prompts: string[] prompts: string[]
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [ ): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
{ {
role: 'system', role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature. content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
Here are the specific requirements and conventions that should be strictly followed: Here are the specific requirements and conventions that should be strictly followed:
Commit Message Conventions: Commit Message Conventions:
- The commit message consists of three parts: Header, Body, and Footer. - The commit message consists of three parts: Header, Body, and Footer.
- Header: - Header:
- Format: ${config.OCO_OMIT_SCOPE ? '`<type>: <subject>`' : '`<type>(<scope>): <subject>`'} - Format: ${
config.OCO_OMIT_SCOPE
? '`<type>: <subject>`'
: '`<type>(<scope>): <subject>`'
}
- ${prompts.join('\n- ')} - ${prompts.join('\n- ')}
JSON Output Format: JSON Output Format:
@@ -246,9 +252,9 @@ Additional Details:
- Allowing the server to listen on a port specified through the environment variable is considered a new feature. - Allowing the server to listen on a port specified through the environment variable is considered a new feature.
Example Git Diff is to follow:` Example Git Diff is to follow:`
}, },
INIT_DIFF_PROMPT INIT_DIFF_PROMPT
]; ];
/** /**
* Prompt to have LLM generate a message using @commitlint rules. * Prompt to have LLM generate a message using @commitlint rules.
@@ -262,25 +268,30 @@ const INIT_MAIN_PROMPT = (
prompts: string[] prompts: string[]
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({ ): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system', role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config.OCO_WHY ? 'and WHY the changes were done' : '' content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
}. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message. config.OCO_WHY ? 'and WHY the changes were done' : ''
${config.OCO_EMOJI }. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
? 'Use GitMoji convention to preface the commit.' ${
: 'Do not preface the commit with anything.' config.OCO_EMOJI
} ? 'Use GitMoji convention to preface the commit.'
${config.OCO_DESCRIPTION : 'Do not preface the commit with anything.'
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' }
: "Don't add any descriptions to the commit, only commit message." ${
} config.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message."
}
Use the present tense. Use ${language} to answer. Use the present tense. Use ${language} to answer.
${config.OCO_ONE_LINE_COMMIT ${
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.' config.OCO_ONE_LINE_COMMIT
: '' ? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
} : ''
${config.OCO_OMIT_SCOPE }
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>' ${
: '' config.OCO_OMIT_SCOPE
} ? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: ''
}
You will strictly follow the following conventions to generate the content of the commit message: You will strictly follow the following conventions to generate the content of the commit message:
- ${prompts.join('\n- ')} - ${prompts.join('\n- ')}
+14 -14
View File
@@ -155,9 +155,9 @@ const INIT_MAIN_PROMPT = (
}); });
export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam = export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam =
{ {
role: 'user', role: 'user',
content: `diff --git a/src/server.ts b/src/server.ts content: `diff --git a/src/server.ts b/src/server.ts
index ad4db42..f3b18a9 100644 index ad4db42..f3b18a9 100644
--- a/src/server.ts --- a/src/server.ts
+++ b/src/server.ts +++ b/src/server.ts
@@ -181,7 +181,7 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
+app.listen(process.env.PORT || PORT, () => { +app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`); + console.log(\`Server listening on port \${PORT}\`);
});` });`
}; };
const COMMIT_TYPES = { const COMMIT_TYPES = {
fix: '🐛', fix: '🐛',
@@ -193,19 +193,19 @@ const generateCommitString = (
message: string message: string
): string => { ): string => {
const cleanMessage = removeConventionalCommitWord(message); const cleanMessage = removeConventionalCommitWord(message);
return config.OCO_EMOJI return config.OCO_EMOJI ? `${COMMIT_TYPES[type]} ${cleanMessage}` : message;
? `${COMMIT_TYPES[type]} ${cleanMessage}`
: message;
}; };
const getConsistencyContent = (translation: ConsistencyPrompt) => { const getConsistencyContent = (translation: ConsistencyPrompt) => {
const fixMessage = config.OCO_OMIT_SCOPE && translation.commitFixOmitScope const fixMessage =
? translation.commitFixOmitScope config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
: translation.commitFix; ? translation.commitFixOmitScope
: translation.commitFix;
const featMessage = config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope const featMessage =
? translation.commitFeatOmitScope config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
: translation.commitFeat; ? translation.commitFeatOmitScope
: translation.commitFeat;
const fix = generateCommitString('fix', fixMessage); const fix = generateCommitString('fix', fixMessage);
const feat = config.OCO_ONE_LINE_COMMIT const feat = config.OCO_ONE_LINE_COMMIT
@@ -250,7 +250,7 @@ export const getMainCommitPrompt = async (
INIT_DIFF_PROMPT, INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT( INIT_CONSISTENCY_PROMPT(
commitLintConfig.consistency[ commitLintConfig.consistency[
translation.localLanguage translation.localLanguage
] as ConsistencyPrompt ] as ConsistencyPrompt
) )
]; ];
+8 -2
View File
@@ -4,7 +4,10 @@
* @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>') * @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>')
* @returns The content with the specified tags and their contents removed, and trimmed * @returns The content with the specified tags and their contents removed, and trimmed
*/ */
export function removeContentTags<T extends string | null | undefined>(content: T, tag: string): T { export function removeContentTags<T extends string | null | undefined>(
content: T,
tag: string
): T {
if (!content || typeof content !== 'string') { if (!content || typeof content !== 'string') {
return content; return content;
} }
@@ -29,7 +32,10 @@ export function removeContentTags<T extends string | null | undefined>(content:
} }
} }
// Check for closing tag // Check for closing tag
else if (content.substring(i, i + closeTag.length) === closeTag && depth > 0) { else if (
content.substring(i, i + closeTag.length) === closeTag &&
depth > 0
) {
depth--; depth--;
if (depth === 0) { if (depth === 0) {
i = i + closeTag.length - 1; // Skip the closing tag i = i + closeTag.length - 1; // Skip the closing tag