@@ -49,19 +49,60 @@ jobs:
4949 };
5050 const defaultReviewers = ['bladehan1', 'kuny0707'];
5151
52+ // ── Normalize helper ─────────────────────────────────────
53+ // Strip spaces, hyphens, underscores and lower-case so that
54+ // "VM", " json rpc ", "chain-base", "Json_Rpc" all normalize
55+ // to their canonical key form ("vm", "jsonrpc", "chainbase").
56+ const normalize = s => s.toLowerCase().replace(/[\s\-_]/g, '');
57+
5258 // ── Extract scope from conventional commit title ──────────
5359 // Format: type(scope): description
60+ // Also supports: type(scope1,scope2): description
5461 const scopeMatch = title.match(/^\w+\(([^)]+)\):/);
55- const scope = scopeMatch ? scopeMatch[1].toLowerCase() : null;
62+ const rawScope = scopeMatch ? scopeMatch[1] : null;
5663
5764 core.info(`PR title : ${title}`);
58- core.info(`Detected scope: ${scope || '(none — using default )'}`);
65+ core.info(`Raw scope: ${rawScope || '(none)'}`);
5966
6067 // ── Determine reviewers ───────────────────────────────────
61- let reviewers = scope && scopeReviewers[scope]
62- ? scopeReviewers[scope]
68+ // 1. Split by comma to support multi-scope: feat(vm,rpc): ...
69+ // 2. Normalize each scope token
70+ // 3. Match against keys: exact match first, then contains match
71+ // (longest key wins to avoid "net" matching inside "jsonrpc")
72+ let matched = new Set();
73+ let matchedScopes = [];
74+
75+ if (rawScope) {
76+ const tokens = rawScope.split(',').map(s => normalize(s.trim()));
77+ // Pre-sort keys by length descending so longer keys match first
78+ const sortedKeys = Object.keys(scopeReviewers)
79+ .sort((a, b) => b.length - a.length);
80+
81+ for (const token of tokens) {
82+ if (!token) continue;
83+ // Exact match
84+ if (scopeReviewers[token]) {
85+ matchedScopes.push(token);
86+ scopeReviewers[token].forEach(r => matched.add(r));
87+ continue;
88+ }
89+ // Contains match: token contains a key, or key contains token
90+ // Prefer longest key that matches
91+ const found = sortedKeys.find(k => token.includes(k) || k.includes(token));
92+ if (found) {
93+ matchedScopes.push(`${token}→${found}`);
94+ scopeReviewers[found].forEach(r => matched.add(r));
95+ }
96+ }
97+ }
98+
99+ let reviewers = matched.size > 0
100+ ? [...matched]
63101 : defaultReviewers;
64102
103+ core.info(`Matched scopes: ${matchedScopes.length > 0 ? matchedScopes.join(', ') : '(none — using default)'}`);
104+ core.info(`Candidate reviewers: ${reviewers.join(', ')}`);
105+
65106 // Exclude the PR author from the reviewer list
66107 reviewers = reviewers.filter(r => r.toLowerCase() !== prAuthor.toLowerCase());
67108
0 commit comments