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.
Files changed (47) hide show
  1. package/.gitlab/duo/chat-rules.md +40 -0
  2. package/.gitlab/duo/mr-review-instructions.md +44 -0
  3. package/.gitlab-ci.yml +136 -0
  4. package/README.md +585 -57
  5. package/action.yml +21 -2
  6. package/dist/applicant-scorer.js +27 -105
  7. package/dist/cli.js +1045 -34
  8. package/dist/commands/apply-for-issue.js +396 -0
  9. package/dist/commands/bounty-hunter.js +441 -0
  10. package/dist/commands/contribute.js +245 -51
  11. package/dist/commands/drips.js +351 -0
  12. package/dist/commands/gitlab-issues.js +87 -0
  13. package/dist/commands/gitlab-mrs.js +163 -0
  14. package/dist/commands/gitlab-pipelines.js +95 -0
  15. package/dist/commands/prs.js +3 -3
  16. package/dist/core/github.js +24 -0
  17. package/dist/core/gitlab.js +233 -0
  18. package/dist/gitlab-agents/ci-recovery-agent.js +173 -0
  19. package/dist/gitlab-agents/contributor-scoring-agent.js +159 -0
  20. package/dist/gitlab-agents/grade-assignment-agent.js +252 -0
  21. package/dist/gitlab-agents/mr-review-agent.js +200 -0
  22. package/dist/gitlab-agents/reminder-agent.js +164 -0
  23. package/dist/grade-assignment.js +262 -0
  24. package/dist/remind-contributors.js +127 -0
  25. package/dist/review-and-merge.js +125 -0
  26. package/examples/gitpadi.yml +152 -0
  27. package/package.json +20 -4
  28. package/src/applicant-scorer.ts +33 -141
  29. package/src/cli.ts +1078 -34
  30. package/src/commands/apply-for-issue.ts +452 -0
  31. package/src/commands/bounty-hunter.ts +529 -0
  32. package/src/commands/contribute.ts +264 -50
  33. package/src/commands/drips.ts +408 -0
  34. package/src/commands/gitlab-issues.ts +87 -0
  35. package/src/commands/gitlab-mrs.ts +185 -0
  36. package/src/commands/gitlab-pipelines.ts +104 -0
  37. package/src/commands/prs.ts +3 -3
  38. package/src/core/github.ts +24 -0
  39. package/src/core/gitlab.ts +397 -0
  40. package/src/gitlab-agents/ci-recovery-agent.ts +201 -0
  41. package/src/gitlab-agents/contributor-scoring-agent.ts +196 -0
  42. package/src/gitlab-agents/grade-assignment-agent.ts +275 -0
  43. package/src/gitlab-agents/mr-review-agent.ts +231 -0
  44. package/src/gitlab-agents/reminder-agent.ts +203 -0
  45. package/src/grade-assignment.ts +283 -0
  46. package/src/remind-contributors.ts +159 -0
  47. 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 score-applicant'
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
@@ -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 { Octokit } from '@octokit/rest';
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
- const octokit = new Octokit({ auth: GITHUB_TOKEN });
21
- // ── Application Detection ──────────────────────────────────────────────
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); });