bitbucket-gemini-action 1.0.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/.claude/settings.local.json +8 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +150 -0
- package/README.md +375 -0
- package/bitbucket-pipelines.yml +95 -0
- package/bun.lock +227 -0
- package/dist/prepare.js +7111 -0
- package/examples/bitbucket-pipelines-full.yml +157 -0
- package/examples/bitbucket-pipelines-minimal.yml +22 -0
- package/package.json +33 -0
- package/src/bitbucket/api/client.ts +406 -0
- package/src/bitbucket/context.ts +196 -0
- package/src/bitbucket/data/fetcher.ts +195 -0
- package/src/bitbucket/data/formatter.ts +221 -0
- package/src/bitbucket/operations/comments.ts +236 -0
- package/src/bitbucket/types.ts +262 -0
- package/src/bitbucket/validation/permissions.ts +154 -0
- package/src/bitbucket/validation/trigger.ts +175 -0
- package/src/entrypoints/execute.ts +349 -0
- package/src/entrypoints/prepare.ts +216 -0
- package/src/gemini/client.ts +263 -0
- package/src/gemini/presets.ts +2130 -0
- package/src/gemini/prompts.ts +331 -0
- package/src/gemini/tools.ts +226 -0
- package/src/index.ts +71 -0
- package/src/modes/agent/index.ts +119 -0
- package/src/modes/registry.ts +118 -0
- package/src/modes/tag/index.ts +172 -0
- package/src/modes/types.ts +95 -0
- package/src/utils/env.ts +190 -0
- package/src/utils/retry.ts +149 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Mode Implementation
|
|
3
|
+
* Automated execution with explicit prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Mode,
|
|
8
|
+
ModeOptions,
|
|
9
|
+
ModeResult,
|
|
10
|
+
TriggerCheckResult,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import type { ParsedBitbucketContext } from "../../bitbucket/types.js";
|
|
13
|
+
import { AGENT_MODE_SYSTEM_PROMPT } from "../../gemini/prompts.js";
|
|
14
|
+
import { codeReviewTools } from "../../gemini/tools.js";
|
|
15
|
+
|
|
16
|
+
export class AgentMode implements Mode {
|
|
17
|
+
name = "agent" as const;
|
|
18
|
+
description = "Automated execution with explicit prompt from pipeline";
|
|
19
|
+
|
|
20
|
+
shouldTrigger(
|
|
21
|
+
_context: ParsedBitbucketContext,
|
|
22
|
+
options: { triggerPhrase: string; prompt?: string }
|
|
23
|
+
): TriggerCheckResult {
|
|
24
|
+
// Agent mode triggers when explicit prompt is provided
|
|
25
|
+
if (!options.prompt) {
|
|
26
|
+
return {
|
|
27
|
+
shouldTrigger: false,
|
|
28
|
+
reason: "No explicit prompt provided",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
shouldTrigger: true,
|
|
34
|
+
userMessage: options.prompt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async prepare(options: ModeOptions): Promise<ModeResult> {
|
|
39
|
+
const { context, client, prompt } = options;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (!prompt) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
modeContext: {
|
|
46
|
+
prompt: "",
|
|
47
|
+
systemPrompt: "",
|
|
48
|
+
tools: [],
|
|
49
|
+
},
|
|
50
|
+
error: "Agent mode requires an explicit prompt",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Optionally create tracking comment for visibility
|
|
55
|
+
let trackingCommentId: number | undefined;
|
|
56
|
+
if (options.createTrackingComment && context.entityNumber) {
|
|
57
|
+
const trackingComment = await client.createPullRequestComment(
|
|
58
|
+
context.workspace,
|
|
59
|
+
context.repoSlug,
|
|
60
|
+
context.entityNumber,
|
|
61
|
+
this.getInitialTrackingCommentContent(prompt)
|
|
62
|
+
);
|
|
63
|
+
trackingCommentId = trackingComment.id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
modeContext: {
|
|
69
|
+
prompt,
|
|
70
|
+
systemPrompt: this.getSystemPrompt(context),
|
|
71
|
+
tools: this.getAllowedTools(),
|
|
72
|
+
trackingCommentId,
|
|
73
|
+
},
|
|
74
|
+
trackingCommentId,
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
modeContext: {
|
|
80
|
+
prompt: "",
|
|
81
|
+
systemPrompt: "",
|
|
82
|
+
tools: [],
|
|
83
|
+
},
|
|
84
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getSystemPrompt(_context: ParsedBitbucketContext): string {
|
|
90
|
+
return AGENT_MODE_SYSTEM_PROMPT;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getAllowedTools(): string[] {
|
|
94
|
+
return codeReviewTools.map((t) => t.name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
shouldCreateTrackingComment(): boolean {
|
|
98
|
+
// Agent mode doesn't require tracking comment by default
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private getInitialTrackingCommentContent(prompt: string): string {
|
|
103
|
+
// Truncate long prompts for display
|
|
104
|
+
const displayPrompt =
|
|
105
|
+
prompt.length > 200 ? `${prompt.substring(0, 200)}...` : prompt;
|
|
106
|
+
|
|
107
|
+
return `## 🤖 Gemini Agent Task
|
|
108
|
+
|
|
109
|
+
⏳ **Status**: Executing automated task...
|
|
110
|
+
|
|
111
|
+
**Task**:
|
|
112
|
+
> ${displayPrompt}
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
_This comment will be updated with the results._`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const agentMode = new AgentMode();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode Registry
|
|
3
|
+
* Central registry for all execution modes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Mode, ModeName, ModeRegistry } from "./types.js";
|
|
7
|
+
import type { ParsedBitbucketContext } from "../bitbucket/types.js";
|
|
8
|
+
import { tagMode } from "./tag/index.js";
|
|
9
|
+
import { agentMode } from "./agent/index.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registry of all available modes
|
|
13
|
+
*/
|
|
14
|
+
export const modeRegistry: ModeRegistry = {
|
|
15
|
+
tag: tagMode,
|
|
16
|
+
agent: agentMode,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get a mode by name
|
|
21
|
+
*/
|
|
22
|
+
export function getMode(name: ModeName): Mode {
|
|
23
|
+
const mode = modeRegistry[name];
|
|
24
|
+
if (!mode) {
|
|
25
|
+
throw new Error(`Unknown mode: ${name}`);
|
|
26
|
+
}
|
|
27
|
+
return mode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Auto-detect the appropriate mode based on context
|
|
32
|
+
*/
|
|
33
|
+
export function detectMode(
|
|
34
|
+
context: ParsedBitbucketContext,
|
|
35
|
+
options: {
|
|
36
|
+
triggerPhrase: string;
|
|
37
|
+
prompt?: string;
|
|
38
|
+
mode?: ModeName;
|
|
39
|
+
}
|
|
40
|
+
): { mode: Mode; reason: string } {
|
|
41
|
+
// If mode is explicitly specified, use it
|
|
42
|
+
if (options.mode) {
|
|
43
|
+
return {
|
|
44
|
+
mode: getMode(options.mode),
|
|
45
|
+
reason: `Explicitly specified mode: ${options.mode}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If explicit prompt is provided, use agent mode
|
|
50
|
+
if (options.prompt) {
|
|
51
|
+
return {
|
|
52
|
+
mode: agentMode,
|
|
53
|
+
reason: "Explicit prompt provided",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if tag mode should trigger
|
|
58
|
+
const tagTrigger = tagMode.shouldTrigger(context, {
|
|
59
|
+
triggerPhrase: options.triggerPhrase,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (tagTrigger.shouldTrigger) {
|
|
63
|
+
return {
|
|
64
|
+
mode: tagMode,
|
|
65
|
+
reason: "Trigger phrase detected in comment",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Default to agent mode for automation events
|
|
70
|
+
if (
|
|
71
|
+
context.eventType === "manual" ||
|
|
72
|
+
context.eventType === "schedule"
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
mode: agentMode,
|
|
76
|
+
reason: "Automation event (manual/schedule)",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// No mode triggered
|
|
81
|
+
return {
|
|
82
|
+
mode: tagMode,
|
|
83
|
+
reason: "Default mode (no specific trigger)",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if any mode should trigger for the given context
|
|
89
|
+
*/
|
|
90
|
+
export function shouldAnyModeTrigger(
|
|
91
|
+
context: ParsedBitbucketContext,
|
|
92
|
+
options: {
|
|
93
|
+
triggerPhrase: string;
|
|
94
|
+
prompt?: string;
|
|
95
|
+
}
|
|
96
|
+
): boolean {
|
|
97
|
+
// Explicit prompt always triggers agent mode
|
|
98
|
+
if (options.prompt) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check tag mode
|
|
103
|
+
const tagTrigger = tagMode.shouldTrigger(context, {
|
|
104
|
+
triggerPhrase: options.triggerPhrase,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return tagTrigger.shouldTrigger;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List all available modes
|
|
112
|
+
*/
|
|
113
|
+
export function listModes(): Array<{ name: ModeName; description: string }> {
|
|
114
|
+
return Object.values(modeRegistry).map((mode) => ({
|
|
115
|
+
name: mode.name,
|
|
116
|
+
description: mode.description,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Mode Implementation
|
|
3
|
+
* Responds to @gemini mentions in PR comments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Mode,
|
|
8
|
+
ModeOptions,
|
|
9
|
+
ModeResult,
|
|
10
|
+
TriggerCheckResult,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import type { ParsedBitbucketContext } from "../../bitbucket/types.js";
|
|
13
|
+
import { TAG_MODE_SYSTEM_PROMPT } from "../../gemini/prompts.js";
|
|
14
|
+
import { codeReviewTools } from "../../gemini/tools.js";
|
|
15
|
+
|
|
16
|
+
export class TagMode implements Mode {
|
|
17
|
+
name = "tag" as const;
|
|
18
|
+
description = "Responds to @gemini mentions in PR comments";
|
|
19
|
+
|
|
20
|
+
shouldTrigger(
|
|
21
|
+
context: ParsedBitbucketContext,
|
|
22
|
+
options: { triggerPhrase: string; prompt?: string }
|
|
23
|
+
): TriggerCheckResult {
|
|
24
|
+
// Don't trigger if explicit prompt is provided (that's agent mode)
|
|
25
|
+
if (options.prompt) {
|
|
26
|
+
return {
|
|
27
|
+
shouldTrigger: false,
|
|
28
|
+
reason: "Explicit prompt provided, use agent mode",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for comment events
|
|
33
|
+
if (
|
|
34
|
+
context.eventType !== "pullrequest:comment_created" &&
|
|
35
|
+
context.eventType !== "pullrequest:comment_updated"
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
shouldTrigger: false,
|
|
39
|
+
reason: `Event type ${context.eventType} is not a comment event`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for trigger phrase in comment
|
|
44
|
+
const comment = context.comment;
|
|
45
|
+
if (!comment) {
|
|
46
|
+
return {
|
|
47
|
+
shouldTrigger: false,
|
|
48
|
+
reason: "No comment in context",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const triggerPhrase = options.triggerPhrase.toLowerCase();
|
|
53
|
+
const commentContent = comment.content.raw.toLowerCase();
|
|
54
|
+
|
|
55
|
+
if (!commentContent.includes(triggerPhrase)) {
|
|
56
|
+
return {
|
|
57
|
+
shouldTrigger: false,
|
|
58
|
+
reason: `Comment does not contain trigger phrase: ${options.triggerPhrase}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Don't respond to our own comments (prevent infinite loops)
|
|
63
|
+
if (this.isOwnComment(comment.user.account_id)) {
|
|
64
|
+
return {
|
|
65
|
+
shouldTrigger: false,
|
|
66
|
+
reason: "Comment is from the bot itself",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract user message (remove trigger phrase)
|
|
71
|
+
const userMessage = this.extractUserMessage(
|
|
72
|
+
comment.content.raw,
|
|
73
|
+
options.triggerPhrase
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
shouldTrigger: true,
|
|
78
|
+
userMessage,
|
|
79
|
+
mentionAuthor: comment.user.display_name,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async prepare(options: ModeOptions): Promise<ModeResult> {
|
|
84
|
+
const { context, client, triggerPhrase } = options;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Verify trigger
|
|
88
|
+
const triggerCheck = this.shouldTrigger(context, { triggerPhrase });
|
|
89
|
+
if (!triggerCheck.shouldTrigger) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
modeContext: {
|
|
93
|
+
prompt: "",
|
|
94
|
+
systemPrompt: "",
|
|
95
|
+
tools: [],
|
|
96
|
+
},
|
|
97
|
+
error: triggerCheck.reason,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Create tracking comment if enabled
|
|
102
|
+
let trackingCommentId: number | undefined;
|
|
103
|
+
if (options.createTrackingComment && context.entityNumber) {
|
|
104
|
+
const trackingComment = await client.createPullRequestComment(
|
|
105
|
+
context.workspace,
|
|
106
|
+
context.repoSlug,
|
|
107
|
+
context.entityNumber,
|
|
108
|
+
this.getInitialTrackingCommentContent()
|
|
109
|
+
);
|
|
110
|
+
trackingCommentId = trackingComment.id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
modeContext: {
|
|
116
|
+
prompt: triggerCheck.userMessage || "",
|
|
117
|
+
systemPrompt: this.getSystemPrompt(context),
|
|
118
|
+
tools: this.getAllowedTools(),
|
|
119
|
+
trackingCommentId,
|
|
120
|
+
},
|
|
121
|
+
trackingCommentId,
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
modeContext: {
|
|
127
|
+
prompt: "",
|
|
128
|
+
systemPrompt: "",
|
|
129
|
+
tools: [],
|
|
130
|
+
},
|
|
131
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getSystemPrompt(_context: ParsedBitbucketContext): string {
|
|
137
|
+
return TAG_MODE_SYSTEM_PROMPT;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getAllowedTools(): string[] {
|
|
141
|
+
return codeReviewTools.map((t) => t.name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
shouldCreateTrackingComment(): boolean {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private extractUserMessage(
|
|
149
|
+
commentContent: string,
|
|
150
|
+
triggerPhrase: string
|
|
151
|
+
): string {
|
|
152
|
+
// Remove trigger phrase and clean up
|
|
153
|
+
const regex = new RegExp(triggerPhrase, "gi");
|
|
154
|
+
return commentContent.replace(regex, "").trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private isOwnComment(accountId: string): boolean {
|
|
158
|
+
const botAccountId = process.env.BOT_ACCOUNT_ID;
|
|
159
|
+
return botAccountId ? accountId === botAccountId : false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private getInitialTrackingCommentContent(): string {
|
|
163
|
+
return `## 🤖 Gemini Code Review
|
|
164
|
+
|
|
165
|
+
⏳ **Status**: Processing your request...
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
_This comment will be updated with the results._`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const tagMode = new TagMode();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode System Types
|
|
3
|
+
* Defines the interface for execution modes (tag, agent)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParsedBitbucketContext } from "../bitbucket/types.js";
|
|
7
|
+
import type { BitbucketClient } from "../bitbucket/api/client.js";
|
|
8
|
+
|
|
9
|
+
export type ModeName = "tag" | "agent";
|
|
10
|
+
|
|
11
|
+
export interface ModeContext {
|
|
12
|
+
prompt: string;
|
|
13
|
+
systemPrompt: string;
|
|
14
|
+
tools: string[];
|
|
15
|
+
trackingCommentId?: number;
|
|
16
|
+
branchName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ModeOptions {
|
|
20
|
+
context: ParsedBitbucketContext;
|
|
21
|
+
client: BitbucketClient;
|
|
22
|
+
triggerPhrase: string;
|
|
23
|
+
prompt?: string;
|
|
24
|
+
branchPrefix?: string;
|
|
25
|
+
createTrackingComment?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModeResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
modeContext: ModeContext;
|
|
31
|
+
trackingCommentId?: number;
|
|
32
|
+
branchName?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TriggerCheckResult {
|
|
37
|
+
shouldTrigger: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
userMessage?: string;
|
|
40
|
+
mentionAuthor?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mode interface - all modes must implement this
|
|
45
|
+
*/
|
|
46
|
+
export interface Mode {
|
|
47
|
+
/**
|
|
48
|
+
* Mode identifier
|
|
49
|
+
*/
|
|
50
|
+
name: ModeName;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Human-readable description
|
|
54
|
+
*/
|
|
55
|
+
description: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if this mode should be triggered for the given context
|
|
59
|
+
*/
|
|
60
|
+
shouldTrigger(
|
|
61
|
+
context: ParsedBitbucketContext,
|
|
62
|
+
options: {
|
|
63
|
+
triggerPhrase: string;
|
|
64
|
+
prompt?: string;
|
|
65
|
+
}
|
|
66
|
+
): TriggerCheckResult;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Prepare the mode context (create comments, branches, etc.)
|
|
70
|
+
*/
|
|
71
|
+
prepare(options: ModeOptions): Promise<ModeResult>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the system prompt for this mode
|
|
75
|
+
*/
|
|
76
|
+
getSystemPrompt(context: ParsedBitbucketContext): string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get allowed tools for this mode
|
|
80
|
+
*/
|
|
81
|
+
getAllowedTools(): string[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if tracking comment should be created
|
|
85
|
+
*/
|
|
86
|
+
shouldCreateTrackingComment(): boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mode registry type
|
|
91
|
+
*/
|
|
92
|
+
export interface ModeRegistry {
|
|
93
|
+
tag: Mode;
|
|
94
|
+
agent: Mode;
|
|
95
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Utilities
|
|
3
|
+
* Handles environment configuration and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Required environment variables schema
|
|
10
|
+
*/
|
|
11
|
+
const requiredEnvSchema = z.object({
|
|
12
|
+
// Bitbucket Pipeline variables
|
|
13
|
+
BITBUCKET_WORKSPACE: z.string().min(1),
|
|
14
|
+
BITBUCKET_REPO_SLUG: z.string().min(1),
|
|
15
|
+
BITBUCKET_COMMIT: z.string().min(1),
|
|
16
|
+
|
|
17
|
+
// Gemini API
|
|
18
|
+
GEMINI_API_KEY: z.string().min(1).optional(),
|
|
19
|
+
GOOGLE_API_KEY: z.string().min(1).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional environment variables schema
|
|
24
|
+
*/
|
|
25
|
+
const optionalEnvSchema = z.object({
|
|
26
|
+
// Bitbucket credentials
|
|
27
|
+
BITBUCKET_ACCESS_TOKEN: z.string().optional(),
|
|
28
|
+
BITBUCKET_USERNAME: z.string().optional(),
|
|
29
|
+
BITBUCKET_APP_PASSWORD: z.string().optional(),
|
|
30
|
+
|
|
31
|
+
// Pipeline context
|
|
32
|
+
BITBUCKET_PR_ID: z.string().optional(),
|
|
33
|
+
BITBUCKET_BRANCH: z.string().optional(),
|
|
34
|
+
BITBUCKET_BUILD_NUMBER: z.string().optional(),
|
|
35
|
+
BITBUCKET_PIPELINE_UUID: z.string().optional(),
|
|
36
|
+
BITBUCKET_STEP_UUID: z.string().optional(),
|
|
37
|
+
|
|
38
|
+
// Configuration
|
|
39
|
+
TRIGGER_PHRASE: z.string().default("@gemini"),
|
|
40
|
+
GEMINI_MODEL: z.string().default("gemini-2.0-flash"),
|
|
41
|
+
MODE: z.enum(["tag", "agent"]).optional(),
|
|
42
|
+
PROMPT: z.string().optional(),
|
|
43
|
+
CREATE_TRACKING_COMMENT: z.string().default("true"),
|
|
44
|
+
BOT_ACCOUNT_ID: z.string().optional(),
|
|
45
|
+
|
|
46
|
+
// Review presets (comma-separated list)
|
|
47
|
+
// e.g., "junior,nextjs,security" or "senior,architecture,typescript"
|
|
48
|
+
REVIEW_PRESETS: z.string().optional(),
|
|
49
|
+
// Custom additional prompt to append to presets
|
|
50
|
+
CUSTOM_PROMPT: z.string().optional(),
|
|
51
|
+
|
|
52
|
+
// Webhook data
|
|
53
|
+
TRIGGER_EVENT: z.string().optional(),
|
|
54
|
+
TRIGGER_TIMESTAMP: z.string().optional(),
|
|
55
|
+
WEBHOOK_PAYLOAD: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type RequiredEnv = z.infer<typeof requiredEnvSchema>;
|
|
59
|
+
export type OptionalEnv = z.infer<typeof optionalEnvSchema>;
|
|
60
|
+
export type FullEnv = RequiredEnv & OptionalEnv;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate required environment variables
|
|
64
|
+
*/
|
|
65
|
+
export function validateRequiredEnv(): RequiredEnv {
|
|
66
|
+
const result = requiredEnvSchema.safeParse(process.env);
|
|
67
|
+
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
const missing = result.error.errors.map((e) => e.path.join(".")).join(", ");
|
|
70
|
+
throw new Error(`Missing required environment variables: ${missing}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ensure at least one Gemini API key is set
|
|
74
|
+
if (!result.data.GEMINI_API_KEY && !result.data.GOOGLE_API_KEY) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_API_KEY"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all environment configuration
|
|
85
|
+
*/
|
|
86
|
+
export function getEnvConfig(): FullEnv {
|
|
87
|
+
const required = validateRequiredEnv();
|
|
88
|
+
const optional = optionalEnvSchema.parse(process.env);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...required,
|
|
92
|
+
...optional,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get Gemini API key
|
|
98
|
+
*/
|
|
99
|
+
export function getGeminiApiKey(): string {
|
|
100
|
+
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
101
|
+
if (!key) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_API_KEY"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get Bitbucket credentials
|
|
111
|
+
*/
|
|
112
|
+
export function getBitbucketCredentials(): {
|
|
113
|
+
type: "token" | "basic";
|
|
114
|
+
accessToken?: string;
|
|
115
|
+
username?: string;
|
|
116
|
+
appPassword?: string;
|
|
117
|
+
} {
|
|
118
|
+
const accessToken = process.env.BITBUCKET_ACCESS_TOKEN;
|
|
119
|
+
const username = process.env.BITBUCKET_USERNAME;
|
|
120
|
+
const appPassword = process.env.BITBUCKET_APP_PASSWORD;
|
|
121
|
+
|
|
122
|
+
if (accessToken) {
|
|
123
|
+
return { type: "token", accessToken };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (username && appPassword) {
|
|
127
|
+
return { type: "basic", username, appPassword };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Missing Bitbucket credentials. Set BITBUCKET_ACCESS_TOKEN or BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get pipeline URL
|
|
137
|
+
*/
|
|
138
|
+
export function getPipelineUrl(): string | undefined {
|
|
139
|
+
const workspace = process.env.BITBUCKET_WORKSPACE;
|
|
140
|
+
const repoSlug = process.env.BITBUCKET_REPO_SLUG;
|
|
141
|
+
const pipelineUuid = process.env.BITBUCKET_PIPELINE_UUID;
|
|
142
|
+
|
|
143
|
+
if (workspace && repoSlug && pipelineUuid) {
|
|
144
|
+
return `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${pipelineUuid}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if running in CI environment
|
|
152
|
+
*/
|
|
153
|
+
export function isCI(): boolean {
|
|
154
|
+
return (
|
|
155
|
+
!!process.env.BITBUCKET_PIPELINE_UUID || process.env.CI === "true"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get boolean from env variable
|
|
161
|
+
*/
|
|
162
|
+
export function getEnvBool(key: string, defaultValue: boolean = false): boolean {
|
|
163
|
+
const value = process.env[key];
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
return defaultValue;
|
|
166
|
+
}
|
|
167
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse review presets from environment variable
|
|
172
|
+
* @returns Array of preset keys
|
|
173
|
+
*/
|
|
174
|
+
export function getReviewPresets(): string[] {
|
|
175
|
+
const presets = process.env.REVIEW_PRESETS;
|
|
176
|
+
if (!presets) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return presets
|
|
180
|
+
.split(",")
|
|
181
|
+
.map((p) => p.trim().toLowerCase())
|
|
182
|
+
.filter((p) => p.length > 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get custom prompt from environment variable
|
|
187
|
+
*/
|
|
188
|
+
export function getCustomPrompt(): string | undefined {
|
|
189
|
+
return process.env.CUSTOM_PROMPT;
|
|
190
|
+
}
|