|
| 1 | +import * as core from '@actions/core'; |
| 2 | +import {context} from '@actions/github'; |
| 3 | +import {GoogleGenerativeAI} from '@google/generative-ai'; |
| 4 | +import {Octokit} from '@octokit/rest'; |
| 5 | +import {ANGULAR_ROBOT, getAuthTokenFor, revokeActiveInstallationToken} from '../../utils.js'; |
| 6 | +import {components} from '@octokit/openapi-types'; |
| 7 | + |
| 8 | +export class IssueLabeling { |
| 9 | + static run = async () => { |
| 10 | + const token = await getAuthTokenFor(ANGULAR_ROBOT); |
| 11 | + const git = new Octokit({auth: token}); |
| 12 | + try { |
| 13 | + const inst = new this(git, core); |
| 14 | + await inst.run(); |
| 15 | + } finally { |
| 16 | + await revokeActiveInstallationToken(git); |
| 17 | + } |
| 18 | + }; |
| 19 | + |
| 20 | + /** Set of area labels available in the current repository. */ |
| 21 | + repoAreaLabels = new Set<string>(); |
| 22 | + /** The issue data fetched from Github. */ |
| 23 | + issueData?: components['schemas']['issue']; |
| 24 | + |
| 25 | + constructor( |
| 26 | + private git: Octokit, |
| 27 | + private coreService: typeof core, |
| 28 | + ) {} |
| 29 | + |
| 30 | + async run() { |
| 31 | + const {issue} = context; |
| 32 | + if (!issue || !issue.number) { |
| 33 | + this.coreService.info('No issue context found. Skipping.'); |
| 34 | + return; |
| 35 | + } |
| 36 | + this.coreService.info(`Issue #${issue.number}`); |
| 37 | + |
| 38 | + // Initialize labels and issue data |
| 39 | + await this.initialize(); |
| 40 | + |
| 41 | + const model = this.getGenerativeModel(); |
| 42 | + |
| 43 | + const prompt = ` |
| 44 | +You are a helper for an open source repository. |
| 45 | +Your task is to allow the user to categorize the issue with an "area: " label. |
| 46 | +The following is the issue title and body: |
| 47 | +
|
| 48 | +Title: ${this.issueData!.title} |
| 49 | +Body: |
| 50 | +${this.issueData!.body} |
| 51 | +
|
| 52 | +The available area labels are: |
| 53 | +${Array.from(this.repoAreaLabels) |
| 54 | + .map((label) => ` - ${label}`) |
| 55 | + .join('\n')} |
| 56 | +
|
| 57 | +Based on the content, which area label is the best fit? |
| 58 | +Respond ONLY with the exact label name (e.g. "area: core"). |
| 59 | +If you are strictly unsure or if multiple labels match equally well, respond with "ambiguous". |
| 60 | +If no area label applies, respond with "none". |
| 61 | +`; |
| 62 | + |
| 63 | + try { |
| 64 | + const result = await model.generateContent(prompt); |
| 65 | + const response = result.response; |
| 66 | + const text = response.text().trim(); |
| 67 | + |
| 68 | + this.coreService.info(`Gemini suggested label: ${text}`); |
| 69 | + |
| 70 | + if (this.repoAreaLabels.has(text)) { |
| 71 | + await this.addLabel(text); |
| 72 | + } else { |
| 73 | + this.coreService.info( |
| 74 | + `Generated label "${text}" is not in the list of valid area labels or is "ambiguous"/"none".`, |
| 75 | + ); |
| 76 | + } |
| 77 | + } catch (e) { |
| 78 | + this.coreService.error('Failed to generate content from Gemini.'); |
| 79 | + this.coreService.setFailed(e as Error); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + getGenerativeModel() { |
| 84 | + const apiKey = this.coreService.getInput('google-generative-ai-key', {required: true}); |
| 85 | + const genAI = new GoogleGenerativeAI(apiKey); |
| 86 | + return genAI.getGenerativeModel({model: 'gemini-2.0-flash'}); |
| 87 | + } |
| 88 | + |
| 89 | + async addLabel(label: string) { |
| 90 | + const {number: issue_number, owner, repo} = context.issue; |
| 91 | + try { |
| 92 | + await this.git.issues.addLabels({repo, owner, issue_number, labels: [label]}); |
| 93 | + this.coreService.info(`Added ${label} label to Issue #${issue_number}`); |
| 94 | + } catch (err) { |
| 95 | + this.coreService.error(`Failed to add ${label} label to Issue #${issue_number}`); |
| 96 | + this.coreService.debug(err as string); |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + async initialize() { |
| 101 | + const {owner, repo} = context.issue; |
| 102 | + await Promise.all([ |
| 103 | + this.git |
| 104 | + .paginate(this.git.issues.listLabelsForRepo, {owner, repo}) |
| 105 | + .then((labels) => |
| 106 | + labels |
| 107 | + .filter((l) => l.name.startsWith('area: ')) |
| 108 | + .forEach((l) => this.repoAreaLabels.add(l.name)), |
| 109 | + ), |
| 110 | + this.git.issues.get({owner, repo, issue_number: context.issue.number}).then((resp) => { |
| 111 | + this.issueData = resp.data; |
| 112 | + }), |
| 113 | + ]); |
| 114 | + |
| 115 | + if (this.repoAreaLabels.size === 0) { |
| 116 | + this.coreService.warning('No area labels found in the repository.'); |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + if (!this.issueData) { |
| 121 | + this.coreService.error('Failed to fetch issue data.'); |
| 122 | + return; |
| 123 | + } |
| 124 | + } |
| 125 | +} |
0 commit comments