Rebuild.
Testing / prettier (push) Failing after 1m59s
Testing / unit-test (20.x) (push) Failing after 3m31s
Testing / e2e-test (20.x) (push) Failing after 4m39s

This commit is contained in:
2026-01-01 14:23:49 -05:00
parent ebbaff0628
commit d3e130a8e8
15 changed files with 1603 additions and 930 deletions
+102 -135
View File
@@ -8,11 +8,17 @@ import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff'
import { randomIntFromInterval } from './utils/randomIntFromInterval';
import { sleep } from './utils/sleep';
// This should be a token with access to your repository scoped in as a secret.
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
// GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
// https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#about-the-github_token-secret
// ------------ New Imports ------------
import fetch from 'node-fetch';
// Inputs
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
const DRY_RUN = core.getInput('DRY_RUN') === 'true';
const ocPrefix = '[OpenCommit]'; // Commit prefix to skip
// Platforms: github | gitea | forgejo (via GIT_PLATFORM env)
const PLATFORM = (process.env.GIT_PLATFORM || 'github').toLowerCase();
const octokit = github.getOctokit(GITHUB_TOKEN);
const context = github.context;
const owner = context.repo.owner;
@@ -21,149 +27,132 @@ const repo = context.repo.repo;
type SHA = string;
type Diff = string;
async function getCommitDiff(commitSha: string) {
const diffResponse = await octokit.request<string>(
'GET /repos/{owner}/{repo}/commits/{ref}',
{
owner,
repo,
ref: commitSha,
headers: {
Accept: 'application/vnd.github.v3.diff'
async function fetchDiffFromAPI(commitSha: string) {
if (PLATFORM === 'github') {
const resp = await octokit.request<string>(
'GET /repos/{owner}/{repo}/commits/{ref}',
{
owner,
repo,
ref: commitSha,
headers: { Accept: 'application/vnd.github.v3.diff' }
}
}
);
return { sha: commitSha, diff: diffResponse.data };
);
return resp.data;
} else {
// Gitea/Forgejo unified diff endpoint
const diffUrl = `${process.env.GIT_PLATFORM_API_BASE}/repos/${owner}/${repo}/git/commits/${commitSha}.diff`;
const response = await fetch(diffUrl, {
headers: { Authorization: `token ${GITHUB_TOKEN}` }
});
return response.text();
}
}
async function getCommitDiff(commitSha: string) {
const diff = await fetchDiffFromAPI(commitSha);
return { sha: commitSha, diff };
}
// Filter out merge commits and those already rewritten
function filterCommits(
commits: { id: string; message: string }[]
) {
return commits.filter(({ message }) => {
if (message.startsWith('Merge')) return false;
if (message.startsWith(ocPrefix)) return false;
return true;
});
}
interface DiffAndSHA {
sha: SHA;
diff: Diff;
}
interface MsgAndSHA {
sha: SHA;
msg: string;
}
// send only 3-4 size chunks of diffs in steps,
// because openAI restricts "too many requests" at once with 429 error
// split into chunks for OpenAI
async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
outro(`Improving commit messages in chunks of ${chunkSize}.`);
const improvePromises = diffsAndSHAs!.map((commit) =>
generateCommitMessageByDiff(commit.diff, false)
const improvePromises = diffsAndSHAs!.map((c) =>
generateCommitMessageByDiff(c.diff, false)
);
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
let improved: MsgAndSHA[] = [];
for (let step = 0; step < improvePromises.length; step += chunkSize) {
const chunkOfPromises = improvePromises.slice(step, step + chunkSize);
const chunkPromises = improvePromises.slice(step, step + chunkSize);
try {
const chunkOfImprovedMessages = await Promise.all(chunkOfPromises);
const chunkOfImprovedMessagesBySha = chunkOfImprovedMessages.map(
(improvedMsg, i) => {
const index = improvedMessagesAndSHAs.length;
const sha = diffsAndSHAs![index + i].sha;
return { sha, msg: improvedMsg };
}
const results = await Promise.all(chunkPromises);
results.forEach((msg, idx) => {
const sha = diffsAndSHAs![improved.length + idx].sha;
improved.push({ sha, msg });
});
await sleep(
1000 * randomIntFromInterval(1, 5) +
100 * randomIntFromInterval(1, 5)
);
improvedMessagesAndSHAs.push(...chunkOfImprovedMessagesBySha);
// sometimes openAI errors with 429 code (too many requests),
// so lets sleep a bit
const sleepFor =
1000 * randomIntFromInterval(1, 5) + 100 * randomIntFromInterval(1, 5);
outro(
`Improved ${chunkOfPromises.length} messages. Sleeping for ${sleepFor}`
);
await sleep(sleepFor);
} catch (error) {
outro(error as string);
// if sleeping in try block still fails with 429,
// openAI wants at least 1 minute before next request
const sleepFor = 60000 + 1000 * randomIntFromInterval(1, 5);
outro(`Retrying after sleeping for ${sleepFor}`);
await sleep(sleepFor);
// go to previous step
} catch (e) {
const wait = 60000 + 1000 * randomIntFromInterval(1, 5);
outro(`Retrying after ${wait}`);
await sleep(wait);
step -= chunkSize;
}
}
return improvedMessagesAndSHAs;
return improved;
}
const getDiffsBySHAs = async (SHAs: string[]) => {
const diffPromises = SHAs.map((sha) => getCommitDiff(sha));
const diffs = await Promise.all(diffPromises).catch((error) => {
outro(`Error in Promise.all(getCommitDiffs(SHAs)): ${error}.`);
throw error;
});
return diffs;
};
async function improveCommitMessages(
commitsToImprove: { id: string; message: string }[]
): Promise<void> {
if (commitsToImprove.length) {
outro(`Found ${commitsToImprove.length} commits to improve.`);
} else {
outro('No new commits found.');
commits: { id: string; message: string }[]
) {
const toImprove = filterCommits(commits);
if (!toImprove.length) {
outro('No eligible commits to improve.');
return;
}
outro('Fetching commit diffs by SHAs.');
const commitSHAsToImprove = commitsToImprove.map((commit) => commit.id);
const diffsWithSHAs = await getDiffsBySHAs(commitSHAsToImprove);
outro('Done.');
const shas = toImprove.map((c) => c.id);
const diffs = await Promise.all(shas.map((sha) => getCommitDiff(sha)));
const improvedMessagesWithSHAs = await improveMessagesInChunks(diffsWithSHAs);
const improved = await improveMessagesInChunks(diffs);
console.log(
`Improved ${improvedMessagesWithSHAs.length} commits: `,
improvedMessagesWithSHAs
const changesExist = improved.some(
({ msg }, i) => msg !== toImprove[i].message
);
// Check if there are actually any changes in the commit messages
const messagesChanged = improvedMessagesWithSHAs.some(
({ sha, msg }, index) => msg !== commitsToImprove[index].message
);
if (!messagesChanged) {
console.log('No changes in commit messages detected, skipping rebase');
if (!changesExist) {
outro('No commit message changes.');
return;
}
const createCommitMessageFile = (message: string, index: number) =>
writeFileSync(`./commit-${index}.txt`, message);
improvedMessagesWithSHAs.forEach(({ msg }, i) =>
createCommitMessageFile(msg, i)
if (DRY_RUN) {
outro('Dryrun: logging proposed changes (no rebase/push).');
improved.forEach(({ sha, msg }) =>
console.log(`Would rewrite ${sha} => ${msg}`)
);
return;
}
improved.forEach(({ msg }, i) =>
writeFileSync(`./commit-${i}.txt`, msg)
);
writeFileSync(`./count.txt`, '0');
writeFileSync('./count.txt', '0');
writeFileSync(
'./rebase-exec.sh',
`#!/bin/bash
count=$(cat count.txt)
git commit --amend -F commit-$count.txt
echo $(( count + 1 )) > count.txt`
count=$(cat count.txt)
git commit --amend -F commit-$count.txt
echo $((count+1)) > count.txt`
);
await exec.exec(`chmod +x ./rebase-exec.sh`);
await exec.exec('chmod +x ./rebase-exec.sh');
await exec.exec(
'git',
['rebase', `${commitsToImprove[0].id}^`, '--exec', './rebase-exec.sh'],
['rebase', `${shas[0]}^`, '--exec', './rebase-exec.sh'],
{
env: {
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
@@ -173,53 +162,31 @@ async function improveCommitMessages(
}
);
const deleteCommitMessageFile = (index: number) =>
unlinkSync(`./commit-${index}.txt`);
commitsToImprove.forEach((_commit, i) => deleteCommitMessageFile(i));
toImprove.forEach((_c, i) => unlinkSync(`./commit-${i}.txt`));
unlinkSync('./count.txt');
unlinkSync('./rebase-exec.sh');
outro('Force pushing non-interactively rebased commits into remote.');
await exec.exec('git', ['status']);
// Force push the rebased commits
await exec.exec('git', ['push', `--force`]);
outro('Done 🧙');
await exec.exec('git', ['push', '--force']);
outro('Done');
}
async function run() {
intro('OpenCommit — improving lame commit messages');
intro('OpenCommit — improving commit messages');
try {
if (github.context.eventName === 'push') {
outro(`Processing commits in a Push event`);
if (context.eventName === 'push') {
const payload = context.payload as PushEvent;
await exec.exec('git', ['config', 'user.email', payload.pusher.email!]);
await exec.exec('git', ['config', 'user.name', payload.pusher.name!]);
const payload = github.context.payload as PushEvent;
const commits = payload.commits;
// Set local Git user identity for future git history manipulations
if (payload.pusher.email)
await exec.exec('git', ['config', 'user.email', payload.pusher.email]);
await exec.exec('git', ['config', 'user.name', payload.pusher.name]);
await exec.exec('git', ['status']);
await exec.exec('git', ['log', '--oneline']);
await improveCommitMessages(commits);
await improveCommitMessages(payload.commits);
} else {
outro('Wrong action.');
core.error(
`OpenCommit was called on ${github.context.payload.action}. OpenCommit is supposed to be used on "push" action.`
`Wrong event: expected push, got ${context.eventName}`
);
}
} catch (error: any) {
const err = error?.message || error;
core.setFailed(err);
core.setFailed(error.message || String(error));
}
}
+96
View File
@@ -0,0 +1,96 @@
// src/platform.ts
import { Gitea } from '@go-gitea/sdk.js';
import github from '@actions/github';
import { PushEvent } from '@octokit/webhooks-types';
export const REWRITE_MARKER = '[OPENCOMMIT]';
export type NormalizedCommit = {
id: string;
message: string;
author?: { name?: string; email?: string };
};
export function isGitea(): boolean {
return !!process.env.GITEA_BASE_URL;
}
export async function getPushCommits(): Promise<NormalizedCommit[]> {
if (isGitea()) {
const payload = github.context.payload as any;
const commits = payload.commits as Array<any>;
return commits
.filter((commit) => {
if (commit.message.startsWith('Merge')) return false;
if (commit.message.includes(REWRITE_MARKER)) return false;
return true;
})
.map((commit) => ({
id: commit.id,
message: commit.message,
author: commit.author
? {
name: commit.author.name,
email: commit.author.email ?? undefined, // coerce null → undefined
}
: undefined,
}));
} else {
const payload = github.context.payload as PushEvent;
const commits = payload.commits;
return commits
.filter((commit) => {
if (commit.message.startsWith('Merge')) return false;
if (commit.message.includes(REWRITE_MARKER)) return false;
return true;
})
.map((commit) => ({
id: commit.id,
message: commit.message,
author: {
name: commit.author.name,
email: commit.author.email ?? undefined, // coerce null → undefined
},
}));
}
}
export async function fetchCommitDiff(
sha: string,
giteaConfig?: { baseUrl: string; token: string }
): Promise<{ sha: string; diff: string }> {
if (isGitea() && giteaConfig) {
const gitea = new Gitea({
baseUrl: giteaConfig.baseUrl,
auth: giteaConfig.token,
});
const { data } = await gitea.rest.git.getCommit({
owner: process.env.GITEA_REPO_OWNER!,
repo: process.env.GITEA_REPO_NAME!,
sha,
});
const filesChanged =
data.files?.map((f: any) => `diff --git a/${f.filename} b/${f.filename}`).join('\n') ?? '';
return { sha, diff: filesChanged };
} else {
const octokit = github.getOctokit(process.env.GITHUB_TOKEN!);
const context = github.context;
const { data: diff } = await octokit.request<string>(
'GET /repos/{owner}/{repo}/commits/{ref}',
{
owner: context.repo.owner,
repo: context.repo.repo,
ref: sha,
headers: { Accept: 'application/vnd.github.v3.diff' },
}
);
return { sha, diff };
}
}