gitpadi 2.0.7 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab/duo/chat-rules.md +40 -0
- package/.gitlab/duo/mr-review-instructions.md +44 -0
- package/.gitlab-ci.yml +136 -0
- package/README.md +585 -57
- package/action.yml +21 -2
- package/dist/applicant-scorer.js +27 -105
- package/dist/cli.js +1045 -34
- package/dist/commands/apply-for-issue.js +396 -0
- package/dist/commands/bounty-hunter.js +441 -0
- package/dist/commands/contribute.js +245 -51
- package/dist/commands/drips.js +351 -0
- package/dist/commands/gitlab-issues.js +87 -0
- package/dist/commands/gitlab-mrs.js +163 -0
- package/dist/commands/gitlab-pipelines.js +95 -0
- package/dist/commands/prs.js +3 -3
- package/dist/core/github.js +24 -0
- package/dist/core/gitlab.js +233 -0
- package/dist/gitlab-agents/ci-recovery-agent.js +173 -0
- package/dist/gitlab-agents/contributor-scoring-agent.js +159 -0
- package/dist/gitlab-agents/grade-assignment-agent.js +252 -0
- package/dist/gitlab-agents/mr-review-agent.js +200 -0
- package/dist/gitlab-agents/reminder-agent.js +164 -0
- package/dist/grade-assignment.js +262 -0
- package/dist/remind-contributors.js +127 -0
- package/dist/review-and-merge.js +125 -0
- package/examples/gitpadi.yml +152 -0
- package/package.json +20 -4
- package/src/applicant-scorer.ts +33 -141
- package/src/cli.ts +1078 -34
- package/src/commands/apply-for-issue.ts +452 -0
- package/src/commands/bounty-hunter.ts +529 -0
- package/src/commands/contribute.ts +264 -50
- package/src/commands/drips.ts +408 -0
- package/src/commands/gitlab-issues.ts +87 -0
- package/src/commands/gitlab-mrs.ts +185 -0
- package/src/commands/gitlab-pipelines.ts +104 -0
- package/src/commands/prs.ts +3 -3
- package/src/core/github.ts +24 -0
- package/src/core/gitlab.ts +397 -0
- package/src/gitlab-agents/ci-recovery-agent.ts +201 -0
- package/src/gitlab-agents/contributor-scoring-agent.ts +196 -0
- package/src/gitlab-agents/grade-assignment-agent.ts +275 -0
- package/src/gitlab-agents/mr-review-agent.ts +231 -0
- package/src/gitlab-agents/reminder-agent.ts +203 -0
- package/src/grade-assignment.ts +283 -0
- package/src/remind-contributors.ts +159 -0
- package/src/review-and-merge.ts +143 -0
package/action.yml
CHANGED
|
@@ -7,7 +7,7 @@ branding:
|
|
|
7
7
|
|
|
8
8
|
inputs:
|
|
9
9
|
action:
|
|
10
|
-
description: 'Which action to run: create-issues, review-pr, or
|
|
10
|
+
description: 'Which action to run: create-issues, review-pr, score-applicant, remind-contributors, review-and-merge, or grade-assignment'
|
|
11
11
|
required: true
|
|
12
12
|
github-token:
|
|
13
13
|
description: 'GitHub token with repo/issues permissions'
|
|
@@ -33,6 +33,14 @@ inputs:
|
|
|
33
33
|
description: 'End index for issue creation range'
|
|
34
34
|
required: false
|
|
35
35
|
default: '999'
|
|
36
|
+
auto-merge:
|
|
37
|
+
description: 'Whether to auto-merge PRs that pass all checks (for review-and-merge action)'
|
|
38
|
+
required: false
|
|
39
|
+
default: 'true'
|
|
40
|
+
pass-threshold:
|
|
41
|
+
description: 'Minimum score (0-100) to auto-merge student assignments (for grade-assignment action)'
|
|
42
|
+
required: false
|
|
43
|
+
default: '40'
|
|
36
44
|
|
|
37
45
|
runs:
|
|
38
46
|
using: 'composite'
|
|
@@ -63,6 +71,8 @@ runs:
|
|
|
63
71
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
64
72
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
65
73
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
74
|
+
AUTO_MERGE: ${{ inputs.auto-merge }}
|
|
75
|
+
PASS_THRESHOLD: ${{ inputs.pass-threshold }}
|
|
66
76
|
run: |
|
|
67
77
|
cd ${{ github.action_path }}
|
|
68
78
|
|
|
@@ -84,9 +94,18 @@ runs:
|
|
|
84
94
|
score-applicant)
|
|
85
95
|
npx tsx src/applicant-scorer.ts
|
|
86
96
|
;;
|
|
97
|
+
remind-contributors)
|
|
98
|
+
npx tsx src/remind-contributors.ts
|
|
99
|
+
;;
|
|
100
|
+
review-and-merge)
|
|
101
|
+
npx tsx src/review-and-merge.ts
|
|
102
|
+
;;
|
|
103
|
+
grade-assignment)
|
|
104
|
+
npx tsx src/grade-assignment.ts
|
|
105
|
+
;;
|
|
87
106
|
*)
|
|
88
107
|
echo "❌ Unknown action: $INPUT_ACTION"
|
|
89
|
-
echo " Valid actions: create-issues, review-pr, score-applicant"
|
|
108
|
+
echo " Valid actions: create-issues, review-pr, score-applicant, remind-contributors, review-and-merge, grade-assignment"
|
|
90
109
|
exit 1
|
|
91
110
|
;;
|
|
92
111
|
esac
|
package/dist/applicant-scorer.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Usage via Action: action: score-applicant
|
|
7
7
|
// Usage (CLI): GITHUB_TOKEN=xxx ISSUE_NUMBER=5 COMMENT_ID=123 npx tsx src/applicant-scorer.ts
|
|
8
|
-
import {
|
|
8
|
+
import { initGitHub, getOctokit } from './core/github.js';
|
|
9
|
+
import { isApplicationComment, fetchProfile, scoreApplicant, TIER_EMOJI, } from './core/scorer.js';
|
|
9
10
|
// ── Config ─────────────────────────────────────────────────────────────
|
|
10
11
|
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
|
11
12
|
const OWNER = process.env.GITHUB_OWNER || process.env.GITHUB_REPOSITORY?.split('/')[0] || '';
|
|
@@ -17,111 +18,9 @@ if (!GITHUB_TOKEN) {
|
|
|
17
18
|
console.error('❌ GITHUB_TOKEN required');
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const APPLICATION_PATTERNS = [
|
|
23
|
-
/i('d| would) (like|love|want) to (work on|tackle|take|pick up|handle)/i,
|
|
24
|
-
/can i (work on|take|pick up|handle|be assigned)/i,
|
|
25
|
-
/assign (this |it )?(to )?me/i,
|
|
26
|
-
/i('m| am) interested/i,
|
|
27
|
-
/let me (work on|take|handle)/i,
|
|
28
|
-
/i('ll| will) (work on|take|handle|do)/i,
|
|
29
|
-
/i want to contribute/i,
|
|
30
|
-
/please assign/i,
|
|
31
|
-
/i can (do|handle|take care of|work on)/i,
|
|
32
|
-
/picking this up/i,
|
|
33
|
-
/claiming this/i,
|
|
34
|
-
];
|
|
35
|
-
function isApplicationComment(body) {
|
|
36
|
-
return APPLICATION_PATTERNS.some((p) => p.test(body));
|
|
37
|
-
}
|
|
38
|
-
// ── Fetch Profile ──────────────────────────────────────────────────────
|
|
39
|
-
async function fetchProfile(username, commentBody) {
|
|
40
|
-
const { data: user } = await octokit.users.getByUsername({ username });
|
|
41
|
-
const accountAge = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400000);
|
|
42
|
-
let totalContributions = 0;
|
|
43
|
-
try {
|
|
44
|
-
const { data: events } = await octokit.activity.listPublicEventsForUser({ username, per_page: 100 });
|
|
45
|
-
totalContributions = events.length;
|
|
46
|
-
}
|
|
47
|
-
catch { /* continue */ }
|
|
48
|
-
let prsMerged = 0, prsOpen = 0, issuesCreated = 0;
|
|
49
|
-
try {
|
|
50
|
-
const { data: mp } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:merged` });
|
|
51
|
-
prsMerged = mp.total_count;
|
|
52
|
-
const { data: op } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:open` });
|
|
53
|
-
prsOpen = op.total_count;
|
|
54
|
-
const { data: ci } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:issue author:${username}` });
|
|
55
|
-
issuesCreated = ci.total_count;
|
|
56
|
-
}
|
|
57
|
-
catch { /* rate limit */ }
|
|
58
|
-
let relevantLanguages = [];
|
|
59
|
-
try {
|
|
60
|
-
const { data: repos } = await octokit.repos.listForUser({ username, sort: 'updated', per_page: 20 });
|
|
61
|
-
const langs = new Set();
|
|
62
|
-
repos.forEach((r) => { if (r.language)
|
|
63
|
-
langs.add(r.language); });
|
|
64
|
-
relevantLanguages = Array.from(langs);
|
|
65
|
-
}
|
|
66
|
-
catch { /* continue */ }
|
|
67
|
-
let hasReadme = false;
|
|
68
|
-
try {
|
|
69
|
-
await octokit.repos.get({ owner: username, repo: username });
|
|
70
|
-
hasReadme = true;
|
|
71
|
-
}
|
|
72
|
-
catch { /* no readme */ }
|
|
73
|
-
return {
|
|
74
|
-
username, avatarUrl: user.avatar_url, accountAge,
|
|
75
|
-
publicRepos: user.public_repos, followers: user.followers,
|
|
76
|
-
totalContributions, repoContributions: prsMerged + prsOpen + issuesCreated,
|
|
77
|
-
relevantLanguages, hasReadme, prsMerged, prsOpen, issuesCreated, commentBody,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// ── Scoring Algorithm ──────────────────────────────────────────────────
|
|
81
|
-
function scoreApplicant(p, issueLabels) {
|
|
82
|
-
const b = { accountMaturity: 0, repoExperience: 0, githubPresence: 0, activityLevel: 0, applicationQuality: 0, languageRelevance: 0, total: 0 };
|
|
83
|
-
// 1. Account Maturity (0-15)
|
|
84
|
-
b.accountMaturity = p.accountAge > 730 ? 15 : p.accountAge > 365 ? 12 : p.accountAge > 180 ? 9 : p.accountAge > 90 ? 6 : p.accountAge > 30 ? 3 : 1;
|
|
85
|
-
// 2. Repo Experience (0-30) — highest weight
|
|
86
|
-
b.repoExperience = Math.min(15, p.prsMerged * 5) + Math.min(5, p.prsOpen * 3) + Math.min(5, p.issuesCreated * 2) + Math.min(5, p.repoContributions);
|
|
87
|
-
// 3. GitHub Presence (0-15)
|
|
88
|
-
b.githubPresence = Math.min(5, Math.floor(p.publicRepos / 5)) + Math.min(5, Math.floor(p.followers / 10)) + (p.hasReadme ? 5 : 0);
|
|
89
|
-
// 4. Activity Level (0-15)
|
|
90
|
-
b.activityLevel = p.totalContributions >= 80 ? 15 : p.totalContributions >= 50 ? 12 : p.totalContributions >= 30 ? 9 : p.totalContributions >= 15 ? 6 : p.totalContributions >= 5 ? 3 : 1;
|
|
91
|
-
// 5. Application Quality (0-15)
|
|
92
|
-
const words = p.commentBody.split(/\s+/).length;
|
|
93
|
-
const hasApproach = /approach|plan|implement|would|will|by|using|step/i.test(p.commentBody);
|
|
94
|
-
const hasExp = /experience|worked|built|familiar|know|background/i.test(p.commentBody);
|
|
95
|
-
b.applicationQuality = (words >= 50 && hasApproach && hasExp) ? 15 : (words >= 30 && (hasApproach || hasExp)) ? 12 : (words >= 20 && hasApproach) ? 9 : words >= 15 ? 6 : words >= 8 ? 3 : 1;
|
|
96
|
-
// 6. Language Relevance (0-10) — auto-detect from repo languages
|
|
97
|
-
b.languageRelevance = 5; // default neutral
|
|
98
|
-
if (issueLabels.length > 0 && p.relevantLanguages.length > 0) {
|
|
99
|
-
// Approximate: check if user knows popular labels' likely languages
|
|
100
|
-
const labelLower = issueLabels.map((l) => l.toLowerCase()).join(' ');
|
|
101
|
-
const hasRust = p.relevantLanguages.includes('Rust');
|
|
102
|
-
const hasTS = p.relevantLanguages.includes('TypeScript') || p.relevantLanguages.includes('JavaScript');
|
|
103
|
-
const needsRust = /contract|rust|wasm|soroban/i.test(labelLower);
|
|
104
|
-
const needsTS = /backend|frontend|api|websocket|typescript/i.test(labelLower);
|
|
105
|
-
let matches = 0, needed = 0;
|
|
106
|
-
if (needsRust) {
|
|
107
|
-
needed++;
|
|
108
|
-
if (hasRust)
|
|
109
|
-
matches++;
|
|
110
|
-
}
|
|
111
|
-
if (needsTS) {
|
|
112
|
-
needed++;
|
|
113
|
-
if (hasTS)
|
|
114
|
-
matches++;
|
|
115
|
-
}
|
|
116
|
-
if (needed > 0)
|
|
117
|
-
b.languageRelevance = Math.round((matches / needed) * 10);
|
|
118
|
-
}
|
|
119
|
-
b.total = b.accountMaturity + b.repoExperience + b.githubPresence + b.activityLevel + b.applicationQuality + b.languageRelevance;
|
|
120
|
-
const tier = b.total >= 75 ? 'S' : b.total >= 55 ? 'A' : b.total >= 40 ? 'B' : b.total >= 25 ? 'C' : 'D';
|
|
121
|
-
return { ...p, score: b.total, breakdown: b, tier };
|
|
122
|
-
}
|
|
21
|
+
// Initialise shared GitHub client so core/scorer.ts functions work
|
|
22
|
+
initGitHub(GITHUB_TOKEN, OWNER, REPO);
|
|
123
23
|
// ── Build Comments ─────────────────────────────────────────────────────
|
|
124
|
-
const TIER_EMOJI = { S: '🏆', A: '🟢', B: '🟡', C: '🟠', D: '🔴' };
|
|
125
24
|
function buildReviewComment(s, count) {
|
|
126
25
|
let c = `## 🤖 Contributor Agent — Applicant Review\n\n`;
|
|
127
26
|
c += `**Applicant:** @${s.username} ${TIER_EMOJI[s.tier]} **Tier ${s.tier}** (${s.score}/100)\n\n`;
|
|
@@ -181,6 +80,7 @@ async function main() {
|
|
|
181
80
|
console.error('❌ ISSUE_NUMBER required');
|
|
182
81
|
process.exit(1);
|
|
183
82
|
}
|
|
83
|
+
const octokit = getOctokit();
|
|
184
84
|
// Get trigger comment
|
|
185
85
|
let trigger;
|
|
186
86
|
if (COMMENT_ID) {
|
|
@@ -252,5 +152,27 @@ async function main() {
|
|
|
252
152
|
}
|
|
253
153
|
console.log(`✅ Comparison posted (${allScored.length} candidates)\n`);
|
|
254
154
|
}
|
|
155
|
+
// Auto-assign the top-scoring S or A tier candidate
|
|
156
|
+
const sorted = [...allScored].sort((a, b) => b.score - a.score);
|
|
157
|
+
const top = sorted[0];
|
|
158
|
+
if (top && (top.tier === 'S' || top.tier === 'A')) {
|
|
159
|
+
// Only assign if the issue has no assignees yet
|
|
160
|
+
const currentAssignees = issue.assignees?.map((a) => a.login) || [];
|
|
161
|
+
if (currentAssignees.length === 0) {
|
|
162
|
+
await octokit.issues.addAssignees({
|
|
163
|
+
owner: OWNER,
|
|
164
|
+
repo: REPO,
|
|
165
|
+
issue_number: ISSUE_NUMBER,
|
|
166
|
+
assignees: [top.username],
|
|
167
|
+
});
|
|
168
|
+
console.log(`✅ Auto-assigned @${top.username} to issue #${ISSUE_NUMBER} (Tier ${top.tier}, ${top.score}/100)`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(`ℹ️ Issue already assigned to: ${currentAssignees.join(', ')} — skipping auto-assign`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (top) {
|
|
175
|
+
console.log(`ℹ️ Top candidate @${top.username} is Tier ${top.tier} (${top.score}/100) — below auto-assign threshold (S/A)`);
|
|
176
|
+
}
|
|
255
177
|
}
|
|
256
178
|
main().catch((e) => { console.error('Fatal:', e); process.exit(1); });
|