claude-git-hooks 2.30.2 → 2.32.0
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/CHANGELOG.md +60 -9
- package/CLAUDE.md +117 -87
- package/README.md +117 -93
- package/lib/commands/close-release.js +7 -7
- package/lib/commands/create-pr.js +34 -21
- package/lib/utils/authorization.js +6 -7
- package/lib/utils/github-api.js +92 -60
- package/lib/utils/github-client.js +5 -105
- package/lib/utils/label-resolver.js +232 -0
- package/lib/utils/remote-config.js +102 -0
- package/lib/utils/reviewer-selector.js +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: reviewer-selector.js
|
|
3
|
+
* Purpose: Team-based reviewer selection for Pull Requests
|
|
4
|
+
*
|
|
5
|
+
* Resolves team members via GitHub Teams API, excludes the PR author
|
|
6
|
+
* and excluded reviewers (from remote config.json + local config),
|
|
7
|
+
* and returns eligible reviewers. Falls back to config-based reviewers
|
|
8
|
+
* if team resolution fails.
|
|
9
|
+
*
|
|
10
|
+
* Exclusion sources (merged, deduplicated):
|
|
11
|
+
* 1. Remote: config.json → general.github.pr.excludeReviewers (all repos)
|
|
12
|
+
* 2. Remote: config.json → {repoFullName}.github.pr.excludeReviewers (repo-specific)
|
|
13
|
+
* 3. Local: config.github.pr.excludeReviewers (per-repo override)
|
|
14
|
+
* 4. PR author (always excluded)
|
|
15
|
+
*
|
|
16
|
+
* Merge priority: remote general < remote repo-specific < local overrides
|
|
17
|
+
*
|
|
18
|
+
* Design: teamSlug is an explicit parameter (default: 'automation').
|
|
19
|
+
* Future auto-detection (e.g. from repos.listTeams) can be plugged in
|
|
20
|
+
* by the caller without changing this module.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import logger from './logger.js';
|
|
24
|
+
import { listTeamMembers } from './github-api.js';
|
|
25
|
+
import { fetchRemoteConfig } from './remote-config.js';
|
|
26
|
+
|
|
27
|
+
/** Default team slug — single place to change when auto-detection is added */
|
|
28
|
+
const DEFAULT_TEAM = 'automation';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the merged exclusion set from remote config.json + local config.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} repoFullName - Full repo name (e.g. 'mscope-S-L/git-hooks')
|
|
34
|
+
* @param {string[]} localExclude - Local excludeReviewers array from config
|
|
35
|
+
* @returns {Promise<Set<string>>} Set of usernames to exclude
|
|
36
|
+
*/
|
|
37
|
+
export async function _buildExclusionSet(repoFullName, localExclude = []) {
|
|
38
|
+
const excluded = new Set();
|
|
39
|
+
|
|
40
|
+
// Remote config: general + repo-specific
|
|
41
|
+
try {
|
|
42
|
+
const remoteConfig = await fetchRemoteConfig('config.json');
|
|
43
|
+
if (remoteConfig) {
|
|
44
|
+
const generalExclude =
|
|
45
|
+
remoteConfig.general?.github?.pr?.excludeReviewers || [];
|
|
46
|
+
const repoExclude =
|
|
47
|
+
remoteConfig[repoFullName]?.github?.pr?.excludeReviewers || [];
|
|
48
|
+
|
|
49
|
+
generalExclude.forEach((u) => excluded.add(u));
|
|
50
|
+
repoExclude.forEach((u) => excluded.add(u));
|
|
51
|
+
|
|
52
|
+
logger.debug('reviewer-selector - _buildExclusionSet', 'Remote exclusions loaded', {
|
|
53
|
+
generalCount: generalExclude.length,
|
|
54
|
+
repoSpecificCount: repoExclude.length,
|
|
55
|
+
repoFullName
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logger.debug('reviewer-selector - _buildExclusionSet', 'Remote config fetch failed', {
|
|
60
|
+
error: err.message
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Local overrides (highest priority, additive)
|
|
65
|
+
localExclude.forEach((u) => excluded.add(u));
|
|
66
|
+
|
|
67
|
+
logger.debug('reviewer-selector - _buildExclusionSet', 'Final exclusion set', {
|
|
68
|
+
count: excluded.size,
|
|
69
|
+
users: [...excluded]
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return excluded;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Select reviewers for a Pull Request from a GitHub team
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} options
|
|
79
|
+
* @param {string} options.org - GitHub organization (e.g. 'mscope-S-L')
|
|
80
|
+
* @param {string} options.repoFullName - Full repo name (e.g. 'mscope-S-L/git-hooks')
|
|
81
|
+
* @param {string} [options.teamSlug='automation'] - Team slug to resolve members from
|
|
82
|
+
* @param {string} options.prAuthor - PR author login (excluded from selection)
|
|
83
|
+
* @param {string[]} [options.configReviewers=[]] - Fallback reviewers from config
|
|
84
|
+
* @param {string[]} [options.excludeReviewers=[]] - Local exclude list from config
|
|
85
|
+
* @returns {Promise<string[]>} Selected reviewer logins
|
|
86
|
+
*/
|
|
87
|
+
export async function selectReviewers({
|
|
88
|
+
org,
|
|
89
|
+
repoFullName,
|
|
90
|
+
teamSlug = DEFAULT_TEAM,
|
|
91
|
+
prAuthor,
|
|
92
|
+
configReviewers = [],
|
|
93
|
+
excludeReviewers = []
|
|
94
|
+
}) {
|
|
95
|
+
logger.debug('reviewer-selector - selectReviewers', 'Starting reviewer selection', {
|
|
96
|
+
org,
|
|
97
|
+
repoFullName,
|
|
98
|
+
teamSlug,
|
|
99
|
+
prAuthor,
|
|
100
|
+
configReviewersCount: configReviewers.length,
|
|
101
|
+
localExcludeCount: excludeReviewers.length
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Build merged exclusion set (remote general + remote repo-specific + local)
|
|
105
|
+
const exclusionSet = await _buildExclusionSet(repoFullName, excludeReviewers);
|
|
106
|
+
// Always exclude the PR author
|
|
107
|
+
exclusionSet.add(prAuthor);
|
|
108
|
+
|
|
109
|
+
// Try team-based resolution
|
|
110
|
+
try {
|
|
111
|
+
const members = await listTeamMembers(org, teamSlug);
|
|
112
|
+
const eligible = members
|
|
113
|
+
.map((m) => m.login)
|
|
114
|
+
.filter((login) => !exclusionSet.has(login));
|
|
115
|
+
|
|
116
|
+
logger.debug('reviewer-selector - selectReviewers', 'Team members resolved', {
|
|
117
|
+
teamSlug,
|
|
118
|
+
totalMembers: members.length,
|
|
119
|
+
eligibleCount: eligible.length,
|
|
120
|
+
excludedCount: members.length - eligible.length
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (eligible.length > 0) {
|
|
124
|
+
logger.info(
|
|
125
|
+
`📋 Found ${eligible.length} reviewer(s) from team '${teamSlug}': ${eligible.join(', ')}`
|
|
126
|
+
);
|
|
127
|
+
return eligible;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
logger.debug(
|
|
131
|
+
'reviewer-selector - selectReviewers',
|
|
132
|
+
'No eligible team members, falling back to config'
|
|
133
|
+
);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.warning(
|
|
136
|
+
`⚠️ Could not resolve team '${teamSlug}': ${err.message}. Falling back to config reviewers.`
|
|
137
|
+
);
|
|
138
|
+
logger.debug('reviewer-selector - selectReviewers', 'Team resolution failed', {
|
|
139
|
+
error: err.message,
|
|
140
|
+
teamSlug
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: config-based reviewers
|
|
145
|
+
const fallback = configReviewers.filter((r) => !exclusionSet.has(r));
|
|
146
|
+
|
|
147
|
+
if (fallback.length > 0) {
|
|
148
|
+
logger.info(`📋 Found ${fallback.length} reviewer(s) from config: ${fallback.join(', ')}`);
|
|
149
|
+
} else {
|
|
150
|
+
logger.info('📋 Found 0 reviewer(s): none');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|