Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
230 lines
5.4 KiB
TypeScript
230 lines
5.4 KiB
TypeScript
const IDENTITY_PATTERNS = [
|
|
/noreply@anthropic\.com/i,
|
|
/@anthropic\.com/i,
|
|
/cursoragent@cursor\.com/i,
|
|
/copilot-swe-agent\[bot\]/i,
|
|
];
|
|
|
|
const SIGNATURE_PATTERNS = [
|
|
/Generated with \[Claude Code\]\(/i,
|
|
/Co-Authored-By:[^\n]*<[^>]*@anthropic\.com>/i,
|
|
/(Generated|Made|Built) with \[Cursor( Agent)?\]\(/i,
|
|
/Co-Authored-By:[^\n]*cursoragent/i,
|
|
];
|
|
|
|
type Commit = {
|
|
sha: string;
|
|
commit: {
|
|
message: string;
|
|
author: { name: string; email: string };
|
|
committer: { name: string; email: string };
|
|
};
|
|
};
|
|
|
|
type ProseSource = {
|
|
kind: string;
|
|
ref: string;
|
|
body: string;
|
|
};
|
|
|
|
async function githubGet<TResponse>(
|
|
url: string,
|
|
token: string,
|
|
): Promise<TResponse> {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/vnd.github+json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
|
|
}
|
|
|
|
return (await response.json()) as TResponse;
|
|
}
|
|
|
|
async function githubGetPaginated<TItem>(
|
|
baseUrl: string,
|
|
token: string,
|
|
): Promise<TItem[]> {
|
|
const items: TItem[] = [];
|
|
let page = 1;
|
|
|
|
for (;;) {
|
|
const separator = baseUrl.includes('?') ? '&' : '?';
|
|
const batch = await githubGet<TItem[]>(
|
|
`${baseUrl}${separator}per_page=100&page=${page}`,
|
|
token,
|
|
);
|
|
|
|
items.push(...batch);
|
|
|
|
if (batch.length < 100) {
|
|
return items;
|
|
}
|
|
|
|
page += 1;
|
|
}
|
|
}
|
|
|
|
function matchPatterns(text: string, patterns: RegExp[]): string[] {
|
|
if (!text) {
|
|
return [];
|
|
}
|
|
|
|
return patterns.flatMap((pattern) => {
|
|
const match = text.match(pattern);
|
|
return match ? [match[0]] : [];
|
|
});
|
|
}
|
|
|
|
function findCommitMatches(commit: Commit): string[] {
|
|
const identityHaystack = [
|
|
commit.commit.author.name,
|
|
commit.commit.author.email,
|
|
commit.commit.committer.name,
|
|
commit.commit.committer.email,
|
|
commit.commit.message,
|
|
].join(' ');
|
|
|
|
return [
|
|
...matchPatterns(identityHaystack, IDENTITY_PATTERNS),
|
|
...matchPatterns(commit.commit.message, SIGNATURE_PATTERNS),
|
|
];
|
|
}
|
|
|
|
async function fetchPrCommits(
|
|
repo: string,
|
|
prNumber: string,
|
|
token: string,
|
|
): Promise<Commit[]> {
|
|
return githubGetPaginated<Commit>(
|
|
`https://api.github.com/repos/${repo}/pulls/${prNumber}/commits`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
async function fetchProseSources(
|
|
repo: string,
|
|
prNumber: string,
|
|
token: string,
|
|
): Promise<ProseSource[]> {
|
|
const base = `https://api.github.com/repos/${repo}`;
|
|
const sources: ProseSource[] = [];
|
|
|
|
const pullRequest = await githubGet<{
|
|
body: string | null;
|
|
html_url: string;
|
|
}>(`${base}/pulls/${prNumber}`, token);
|
|
sources.push({
|
|
kind: 'PR description',
|
|
ref: pullRequest.html_url,
|
|
body: pullRequest.body ?? '',
|
|
});
|
|
|
|
const conversationComments = await githubGetPaginated<{
|
|
body: string | null;
|
|
html_url: string;
|
|
}>(`${base}/issues/${prNumber}/comments`, token);
|
|
for (const comment of conversationComments) {
|
|
sources.push({
|
|
kind: 'Conversation comment',
|
|
ref: comment.html_url,
|
|
body: comment.body ?? '',
|
|
});
|
|
}
|
|
|
|
const reviewComments = await githubGetPaginated<{
|
|
body: string | null;
|
|
html_url: string;
|
|
}>(`${base}/pulls/${prNumber}/comments`, token);
|
|
for (const comment of reviewComments) {
|
|
sources.push({
|
|
kind: 'Inline review comment',
|
|
ref: comment.html_url,
|
|
body: comment.body ?? '',
|
|
});
|
|
}
|
|
|
|
const reviews = await githubGetPaginated<{
|
|
body: string | null;
|
|
html_url: string;
|
|
}>(`${base}/pulls/${prNumber}/reviews`, token);
|
|
for (const review of reviews) {
|
|
sources.push({
|
|
kind: 'Review summary',
|
|
ref: review.html_url,
|
|
body: review.body ?? '',
|
|
});
|
|
}
|
|
|
|
return sources;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const token = process.env.GITHUB_TOKEN;
|
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
const prNumber = process.env.PR_NUMBER;
|
|
|
|
if (!token || !repo || !prNumber) {
|
|
console.error(
|
|
'Error: GITHUB_TOKEN, GITHUB_REPOSITORY and PR_NUMBER are required',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const [commits, proseSources] = await Promise.all([
|
|
fetchPrCommits(repo, prNumber, token),
|
|
fetchProseSources(repo, prNumber, token),
|
|
]);
|
|
|
|
let violations = 0;
|
|
|
|
for (const commit of commits) {
|
|
const matches = findCommitMatches(commit);
|
|
|
|
if (matches.length > 0) {
|
|
console.error(
|
|
`::error::Commit ${commit.sha} contains a blocked bot attribution or signature (matched: ${matches.join(', ')})`,
|
|
);
|
|
violations += 1;
|
|
}
|
|
}
|
|
|
|
for (const source of proseSources) {
|
|
const matches = matchPatterns(source.body, SIGNATURE_PATTERNS);
|
|
|
|
if (matches.length > 0) {
|
|
console.error(
|
|
`::error::${source.kind} contains a bot signature (matched: ${matches.join(', ')}) — ${source.ref}`,
|
|
);
|
|
violations += 1;
|
|
}
|
|
}
|
|
|
|
if (violations > 0) {
|
|
console.error(
|
|
`\nFound ${violations} item(s) attributed to blocked bot contributors.`,
|
|
);
|
|
console.error(
|
|
'For commits: rewrite the author/committer and strip Co-Authored-By trailers, then force-push:',
|
|
);
|
|
console.error(
|
|
" git rebase -i --exec 'git commit --amend --reset-author --no-edit' origin/main",
|
|
);
|
|
console.error(
|
|
'For the PR description, comments or reviews: remove the auto-generated attribution footer.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('No blocked contributors found in PR commits, description, comments or reviews.');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('Error:', error);
|
|
process.exit(1);
|
|
});
|