@catladder/pipeline 3.13.1 → 3.14.1
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/dist/bash/bashEscape.d.ts +2 -0
- package/dist/bash/bashEscape.js +9 -1
- package/dist/constants.js +1 -1
- package/dist/pipeline/agent/createAgentContext.d.ts +6 -0
- package/dist/pipeline/agent/createAgentContext.js +141 -0
- package/dist/pipeline/agent/createAgentEventJob.d.ts +2 -0
- package/dist/pipeline/agent/createAgentEventJob.js +74 -0
- package/dist/pipeline/agent/createAgentReviewJob.d.ts +2 -0
- package/dist/pipeline/agent/createAgentReviewJob.js +69 -0
- package/dist/pipeline/agent/createJobsForAgentContext.d.ts +2 -0
- package/dist/pipeline/agent/createJobsForAgentContext.js +12 -0
- package/dist/pipeline/agent/prompts.d.ts +10 -0
- package/dist/pipeline/agent/prompts.js +66 -0
- package/dist/pipeline/agent/shared.d.ts +8 -0
- package/dist/pipeline/agent/shared.js +29 -0
- package/dist/pipeline/agent/utils.d.ts +3 -0
- package/dist/pipeline/agent/utils.js +16 -0
- package/dist/pipeline/createAllJobs.d.ts +5 -1
- package/dist/pipeline/createAllJobs.js +32 -6
- package/dist/pipeline/createMainPipeline.js +10 -5
- package/dist/pipeline/gitlab/createGitlabJobs.d.ts +3 -3
- package/dist/pipeline/gitlab/createGitlabJobs.js +13 -11
- package/dist/pipeline/gitlab/createGitlabPipeline.js +6 -0
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +8 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/agent.d.ts +7 -0
- package/dist/types/agent.js +5 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/context.d.ts +7 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +2 -1
- package/dist/types/jobs.d.ts +5 -1
- package/dist/types/jobs.js +1 -1
- package/examples/__snapshots__/cloud-run-health-check-defaults.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-health-check-only-startup.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-health-check.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-http2.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-llama.test.ts.snap +29 -0
- package/examples/__snapshots__/cloud-run-memory-limit.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-meteor-with-worker.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-nextjs.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-no-cpu-throttling.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-no-service.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-non-public.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-post-stop-job.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-service-custom-vpc-connector.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-service-custom-vpc.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-service-gen2.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-service-increase-timout.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-service-with-volumes.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-storybook.test.ts.snap +53 -0
- package/examples/__snapshots__/cloud-run-with-agents.test.ts.snap +1576 -0
- package/examples/__snapshots__/cloud-run-with-gpu.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-with-ngnix.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-with-sql-legacy-jobs.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-with-sql-multiple-dbs.test.ts.snap +169 -0
- package/examples/__snapshots__/cloud-run-with-sql-reuse-db.test.ts.snap +117 -0
- package/examples/__snapshots__/cloud-run-with-sql.test.ts.snap +65 -0
- package/examples/__snapshots__/cloud-run-with-worker.test.ts.snap +65 -0
- package/examples/__snapshots__/custom-build-job-with-tests.test.ts.snap +65 -0
- package/examples/__snapshots__/custom-build-job.test.ts.snap +53 -0
- package/examples/__snapshots__/custom-deploy.test.ts.snap +59 -0
- package/examples/__snapshots__/custom-envs.test.ts.snap +63 -0
- package/examples/__snapshots__/custom-sbom-java.test.ts.snap +53 -0
- package/examples/__snapshots__/custom-verify-job.test.ts.snap +73 -0
- package/examples/__snapshots__/git-submodule.test.ts.snap +65 -0
- package/examples/__snapshots__/kubernetes-application-customization.test.ts.snap +73 -0
- package/examples/__snapshots__/kubernetes-with-cloud-sql.test.ts.snap +73 -0
- package/examples/__snapshots__/kubernetes-with-jobs.test.ts.snap +133 -0
- package/examples/__snapshots__/kubernetes-with-mongodb.test.ts.snap +73 -0
- package/examples/__snapshots__/local-dot-env.test.ts.snap +65 -0
- package/examples/__snapshots__/meteor-kubernetes.test.ts.snap +73 -0
- package/examples/__snapshots__/multiline-var.test.ts.snap +177 -0
- package/examples/__snapshots__/native-app.test.ts.snap +101 -0
- package/examples/__snapshots__/node-build-with-custom-image.test.ts.snap +65 -0
- package/examples/__snapshots__/node-build-with-docker-additions.test.ts.snap +65 -0
- package/examples/__snapshots__/override-secrets.test.ts.snap +65 -0
- package/examples/__snapshots__/rails-k8s-with-worker-dockerfile.test.ts.snap +65 -0
- package/examples/__snapshots__/rails-k8s-with-worker.test.ts.snap +65 -0
- package/examples/__snapshots__/referencing-other-vars.test.ts.snap +177 -0
- package/examples/__snapshots__/wait-for-other-deploy.test.ts.snap +85 -0
- package/examples/__snapshots__/workspace-api-www-turbo-cache.test.ts.snap +97 -0
- package/examples/__snapshots__/workspace-api-www.test.ts.snap +97 -0
- package/examples/cloud-run-with-agents.test.ts +11 -0
- package/examples/cloud-run-with-agents.ts +36 -0
- package/package.json +1 -1
- package/src/bash/bashEscape.ts +6 -0
- package/src/pipeline/__tests__/__snapshots__/getPipelineStages.test.ts.snap +9 -0
- package/src/pipeline/agent/createAgentContext.ts +19 -0
- package/src/pipeline/agent/createAgentEventJob.ts +35 -0
- package/src/pipeline/agent/createAgentReviewJob.ts +33 -0
- package/src/pipeline/agent/createJobsForAgentContext.ts +7 -0
- package/src/pipeline/agent/prompts.ts +233 -0
- package/src/pipeline/agent/shared.ts +36 -0
- package/src/pipeline/agent/utils.ts +9 -0
- package/src/pipeline/createAllJobs.ts +20 -1
- package/src/pipeline/createJobsForComponent.ts +1 -0
- package/src/pipeline/createMainPipeline.ts +19 -4
- package/src/pipeline/gitlab/createGitlabJobs.ts +39 -30
- package/src/pipeline/gitlab/createGitlabPipeline.ts +8 -0
- package/src/rules/index.ts +7 -0
- package/src/types/agent.ts +7 -0
- package/src/types/config.ts +3 -0
- package/src/types/context.ts +8 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +6 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// prompts.ts
|
|
2
|
+
type Ctx = { agentUserName: string };
|
|
3
|
+
|
|
4
|
+
const header = () => `
|
|
5
|
+
Project ID: $CI_PROJECT_ID
|
|
6
|
+
GitLab Host: $CI_SERVER_URL
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
const identity = ({ agentUserName }: Ctx) => `
|
|
10
|
+
## Identity
|
|
11
|
+
- Your GitLab username is "${agentUserName}".
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const goldenRules = ({ agentUserName }: Ctx) => `
|
|
15
|
+
## Golden Rules
|
|
16
|
+
- Use the \`gitlab-mcp\` tool for ALL GitLab actions. If a needed action is missing, use GitLab REST/GraphQL API directly as a fallback.
|
|
17
|
+
- NEVER mention yourself ("@${agentUserName}").
|
|
18
|
+
- NEVER push to main/default or any protected branch. Always create a new branch and open a Merge Request (MR).
|
|
19
|
+
- Do not create an MR for a **closed** issue.
|
|
20
|
+
- Keep actions minimal and idempotent. Avoid duplicate comments or duplicate MRs.
|
|
21
|
+
- Use ONE stable \`source_branch\` per run; do not regenerate its name later.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const commentGuidelines = () => `
|
|
25
|
+
## Comment Guidelines (flexible, not verbatim)
|
|
26
|
+
- Keep tone professional, friendly, and concise.
|
|
27
|
+
- Always @-mention the human author when replying; never mention yourself.
|
|
28
|
+
- Acknowledgements: confirm you saw the request and you’ll handle it.
|
|
29
|
+
- MR updates: acknowledge feedback and say you’ll apply/have applied the change.
|
|
30
|
+
- Q&A: answer directly first; add context/links only if useful.
|
|
31
|
+
- Avoid repeating identical boilerplate across comments.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const mcpAndApi = () => `
|
|
35
|
+
## Tools & API (MCP-first, REST/GraphQL fallback)
|
|
36
|
+
Use these \`gitlab-mcp\` capabilities when available (names illustrative—match the actual tool schema):
|
|
37
|
+
|
|
38
|
+
- **Comments**
|
|
39
|
+
- \`gitlab-mcp.comment.create({ project_id: $CI_PROJECT_ID, target: "issue"|"mr", iid, body })\`
|
|
40
|
+
|
|
41
|
+
- **Branch**
|
|
42
|
+
- \`gitlab-mcp.branch.create({ project_id: $CI_PROJECT_ID, from: "<default_branch>", name: "<source_branch>" })\`
|
|
43
|
+
|
|
44
|
+
- **Commits & push**
|
|
45
|
+
- \`gitlab-mcp.commit.push({ project_id: $CI_PROJECT_ID, branch: "<source_branch>", message, files: [{ path, content | patch }] })\`
|
|
46
|
+
|
|
47
|
+
- **Merge Requests**
|
|
48
|
+
- \`gitlab-mcp.merge_request.create({ project_id: $CI_PROJECT_ID, source_branch, target_branch: "<default_branch>", title, description, assign_to_self: true })\`
|
|
49
|
+
- \`gitlab-mcp.merge_request.update({ project_id: $CI_PROJECT_ID, mr_iid, ... })\`
|
|
50
|
+
- \`gitlab-mcp.merge_request.rebase({ project_id: $CI_PROJECT_ID, mr_iid, onto: "<default_branch>" })\`
|
|
51
|
+
|
|
52
|
+
- **Read/verify**
|
|
53
|
+
- \`gitlab-mcp.project.get({ project_id: $CI_PROJECT_ID })\` → default branch
|
|
54
|
+
- \`gitlab-mcp.repo.compare({ project_id: $CI_PROJECT_ID, from: "<default_branch>", to: "<source_branch>" })\`
|
|
55
|
+
- \`gitlab-mcp.repo.branch.get({ project_id: $CI_PROJECT_ID, name: "<source_branch>" })\`
|
|
56
|
+
- \`gitlab-mcp.repo.commits.list({ project_id: $CI_PROJECT_ID, ref_name: "<source_branch>", per_page: 1 })\`
|
|
57
|
+
|
|
58
|
+
### Fallback: Direct GitLab API
|
|
59
|
+
If MCP lacks an operation, call GitLab’s REST/GraphQL API directly.
|
|
60
|
+
|
|
61
|
+
- **Authentication**
|
|
62
|
+
Use the environment variable \`GITLAB_PERSONAL_ACCESS_TOKEN\`.
|
|
63
|
+
Send it in the HTTP header:
|
|
64
|
+
\`\`\`
|
|
65
|
+
Private-Token: $GITLAB_PERSONAL_ACCESS_TOKEN
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
- **Host & project variables**
|
|
69
|
+
- API base URL: \`$CI_SERVER_URL/api/v4\`
|
|
70
|
+
- Project ID: \`$CI_PROJECT_ID\`
|
|
71
|
+
|
|
72
|
+
- **Examples**
|
|
73
|
+
- Get project (default branch):
|
|
74
|
+
\`GET $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID\`
|
|
75
|
+
- Get/create branch:
|
|
76
|
+
\`GET|POST $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/repository/branches\`
|
|
77
|
+
- Compare refs:
|
|
78
|
+
\`GET $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/repository/compare?from=<default>&to=<source>\`
|
|
79
|
+
- List commits:
|
|
80
|
+
\`GET $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/repository/commits?ref_name=<branch>&per_page=1\`
|
|
81
|
+
- Create comment on issue/MR:
|
|
82
|
+
\`POST $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/issues/:iid/notes\`
|
|
83
|
+
\`POST $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/merge_requests/:iid/notes\`
|
|
84
|
+
- Create MR:
|
|
85
|
+
\`POST $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/merge_requests\`
|
|
86
|
+
- Get MR changes:
|
|
87
|
+
\`GET $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/merge_requests/:iid/changes\`
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const outputDiscipline = ({ agentUserName }: Ctx) => `
|
|
91
|
+
## Output Discipline
|
|
92
|
+
- Prefer \`gitlab-mcp\` tool calls. If unavailable, output direct API requests (endpoint, method, headers, JSON body).
|
|
93
|
+
- Keep comments concise and professional.
|
|
94
|
+
- Never include "@${agentUserName}" in any body.
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
// --- Event-specific sections ---
|
|
98
|
+
|
|
99
|
+
const eventSelfParse = () => `
|
|
100
|
+
## Self-Parse the Raw Payload (no preprocessing available)
|
|
101
|
+
From \`event_json\`, extract:
|
|
102
|
+
- kind: "issue" | "merge_request" | "note"
|
|
103
|
+
- target + iid from URL:
|
|
104
|
+
- \`/-/issues/<n>\` → target="issue", iid=<n>
|
|
105
|
+
- \`/-/merge_requests/<n>\` → target="mr", iid=<n>
|
|
106
|
+
- note_id if present (\`#note_<id>\`)
|
|
107
|
+
- description/body text, state, author \`user_username\`, timestamps
|
|
108
|
+
- project id/path; detect default branch via tool/API when needed
|
|
109
|
+
|
|
110
|
+
If any key is missing, choose the safest minimal action or briefly explain via a comment.
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
const eventWorkflow = () => `
|
|
114
|
+
## High-Reliability Workflow (sequence + postconditions)
|
|
115
|
+
Follow this order for any change work:
|
|
116
|
+
|
|
117
|
+
1) **Acknowledge** with a short comment on the issue/MR thread.
|
|
118
|
+
2) **Discover default branch** (e.g., "main") via MCP or API.
|
|
119
|
+
3) **Create a working branch** from default (stable name, e.g., \`fix/issue-<iid>-<slug>\` or \`feat/issue-<iid>-<slug>\`).
|
|
120
|
+
4) **Write changes → commit → push to remote branch.**
|
|
121
|
+
5) **Verify push landed**:
|
|
122
|
+
- Fetch latest commit on \`source_branch\`; record its short SHA.
|
|
123
|
+
- Compare default vs \`source_branch\` and ensure \`diffs.length > 0\`.
|
|
124
|
+
6) **Create or update MR** ONLY if there is a non-empty diff.
|
|
125
|
+
- Include \`Closes #<issue_iid>\` in MR description when applicable.
|
|
126
|
+
- Assign yourself to the MR.
|
|
127
|
+
7) **Follow-up comment** with branch name, commit short SHA, files changed count, and MR link.
|
|
128
|
+
8) **If verification fails**:
|
|
129
|
+
- Do NOT create the MR.
|
|
130
|
+
- Comment the exact failure and retry once with a fresh branch name. If still failing, comment and stop.
|
|
131
|
+
|
|
132
|
+
For Q&A-only (no code changes), just post a concise, helpful answer on the same issue/MR.
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
// --- MR-specific sections ---
|
|
136
|
+
|
|
137
|
+
const mrScope = ({ agentUserName }: Ctx) => `
|
|
138
|
+
## Identity & Scope
|
|
139
|
+
- Your GitLab username is "${agentUserName}".
|
|
140
|
+
- This prompt runs in the context of ONE MR (no webhook).
|
|
141
|
+
- You may review, comment, rebase, and push updates **to the MR's source branch**.
|
|
142
|
+
- You must **never merge** the MR yourself.
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const mrWorkflow = () => `
|
|
146
|
+
## High-Reliability Review Workflow
|
|
147
|
+
Follow this sequence with verification at each step:
|
|
148
|
+
|
|
149
|
+
1) **Collect context**
|
|
150
|
+
- Get MR metadata (source_branch, target_branch, state, draft/WIP).
|
|
151
|
+
- Fetch the full changeset/diffs and open discussions (notes, threads, unresolved discussions).
|
|
152
|
+
- Read existing reviews/comments to avoid duplication.
|
|
153
|
+
- (Optional) Fetch recent CI pipeline(s) for this MR SHA/branch).
|
|
154
|
+
|
|
155
|
+
2) **Code review**
|
|
156
|
+
- Identify required changes (bugs, tests, style, security, perf, docs).
|
|
157
|
+
- If no meaningful changes are needed:
|
|
158
|
+
- Post a concise review comment summarizing findings.
|
|
159
|
+
- Ask for review by a **recent active human contributor** (not you).
|
|
160
|
+
|
|
161
|
+
3) **If changes are needed**
|
|
162
|
+
- Post a short acknowledgment comment on the MR.
|
|
163
|
+
- **Rebase** the MR onto the target/default branch (resolve trivial conflicts).
|
|
164
|
+
- Implement minimal, safe changes; keep commits small and clear.
|
|
165
|
+
- **Push** to the MR's **source_branch**.
|
|
166
|
+
- **Verify push landed** (latest commit short SHA; compare target vs source shows diffs > 0).
|
|
167
|
+
- Comment summarizing what changed and why.
|
|
168
|
+
|
|
169
|
+
4) **CI pipeline**
|
|
170
|
+
- Check pipeline status for the new commit on the MR branch.
|
|
171
|
+
- Retry/re-run if allowed on flaky failures; fix minimal issues; push again if needed.
|
|
172
|
+
- If still failing, comment with failure summary and next steps.
|
|
173
|
+
|
|
174
|
+
5) **Assign human reviewer if ready**
|
|
175
|
+
- If discussions are resolved and CI is passing (or running), request review from a recent active human contributor (not you).
|
|
176
|
+
|
|
177
|
+
6) **Stdout summary**
|
|
178
|
+
- Print concise summary: commits pushed (short SHAs), files changed count, discussions resolved/left, CI status, and requested reviewers.
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
const fallbackApiAuth = () => `
|
|
182
|
+
## Fallback API Auth (if MCP lacks a method)
|
|
183
|
+
- Base URL: \`$CI_SERVER_URL/api/v4\`
|
|
184
|
+
- Project: \`$CI_PROJECT_ID\`
|
|
185
|
+
- Header: \`Private-Token: $GITLAB_PERSONAL_ACCESS_TOKEN\`
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
// ---------- Public builders ----------
|
|
189
|
+
|
|
190
|
+
export const getEventPrompt = ({ agentUserName }: Ctx) => `
|
|
191
|
+
You are a GitLab assistant bot. You receive ONE raw GitLab webhook JSON payload.
|
|
192
|
+
|
|
193
|
+
${header()}
|
|
194
|
+
---
|
|
195
|
+
event_json:
|
|
196
|
+
$(cat $TRIGGER_PAYLOAD)
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
${identity({ agentUserName })}
|
|
200
|
+
${goldenRules({ agentUserName })}
|
|
201
|
+
${eventSelfParse()}
|
|
202
|
+
${eventWorkflow()}
|
|
203
|
+
${commentGuidelines()}
|
|
204
|
+
${mcpAndApi()}
|
|
205
|
+
${outputDiscipline({ agentUserName })}
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
export const getMergeRequestPrompt = ({ agentUserName }: Ctx) => `
|
|
209
|
+
You are a GitLab assistant bot reviewing and updating a single Merge Request (MR).
|
|
210
|
+
|
|
211
|
+
${header()}
|
|
212
|
+
---
|
|
213
|
+
merge_request_iid: $CI_MERGE_REQUEST_IID
|
|
214
|
+
title: $CI_MERGE_REQUEST_TITLE
|
|
215
|
+
description: $CI_MERGE_REQUEST_DESCRIPTION
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
${mrScope({ agentUserName })}
|
|
219
|
+
${goldenRules({ agentUserName })}
|
|
220
|
+
${mrWorkflow()}
|
|
221
|
+
${commentGuidelines()}
|
|
222
|
+
${mcpAndApi()}
|
|
223
|
+
${fallbackApiAuth()}
|
|
224
|
+
## Output Discipline (MR)
|
|
225
|
+
- Prefer \`gitlab-mcp\` tool calls; if unavailable, provide explicit REST calls (method, url, headers, body).
|
|
226
|
+
- At the end, print a **plain-text** summary to STDOUT including:
|
|
227
|
+
- \`source_branch\` and \`target_branch\`
|
|
228
|
+
- commits pushed (short SHAs)
|
|
229
|
+
- number of files changed
|
|
230
|
+
- CI status/result
|
|
231
|
+
- reviewers requested (if any)
|
|
232
|
+
- Do **not** merge the MR yourself under any circumstance.
|
|
233
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeBackTicks,
|
|
3
|
+
escapeDoubleQuotes,
|
|
4
|
+
escapeNewlines,
|
|
5
|
+
} from "../../bash/bashEscape";
|
|
6
|
+
import type { AgentContext, CatladderJob } from "../../types";
|
|
7
|
+
|
|
8
|
+
export const createBaseAgentJob = (
|
|
9
|
+
context: AgentContext,
|
|
10
|
+
): Omit<CatladderJob, "name" | "rules" | "script"> => ({
|
|
11
|
+
stage: "agents",
|
|
12
|
+
envMode: "none",
|
|
13
|
+
image: "node:24-alpine3.21",
|
|
14
|
+
variables: {
|
|
15
|
+
MAX_MCP_OUTPUT_TOKENS: "75000",
|
|
16
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: "$AGENT_GITLAB_PERSONAL_ACCESS_TOKEN", // TODO: we don't have global secret keys to configure yet
|
|
17
|
+
GITLAB_API_URL: "$CI_API_V4_URL",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const baseSetupScript = [
|
|
22
|
+
"apk update",
|
|
23
|
+
"apk add --no-cache git curl bash",
|
|
24
|
+
"npm install -g @anthropic-ai/claude-code",
|
|
25
|
+
"claude mcp add gitlab --env GITLAB_PERSONAL_ACCESS_TOKEN=$GITLAB_PERSONAL_ACCESS_TOKEN --env GITLAB_API_URL=$GITLAB_API_URL --env USE_PIPELINE='true' -- npx -y @zereight/mcp-gitlab",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const callClaude = ({ prompt }: { prompt: string }) => {
|
|
29
|
+
return [
|
|
30
|
+
`export PROMPT="${escapeNewlines(
|
|
31
|
+
escapeDoubleQuotes(escapeBackTicks(prompt)),
|
|
32
|
+
)}"`,
|
|
33
|
+
//'echo "$PROMPT"',
|
|
34
|
+
`claude -p "$PROMPT" --permission-mode acceptEdits --allowedTools "Bash(*) Read(*) Edit(*) Write(*) mcp__gitlab" --verbose --debug`,
|
|
35
|
+
];
|
|
36
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AgentContext } from "../../types";
|
|
2
|
+
|
|
3
|
+
export const getAgentUserName = (context: AgentContext) => {
|
|
4
|
+
return context.agentConfig.agentUser?.username ?? "agent.claude";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const getAgentUserId = (context: AgentContext) => {
|
|
8
|
+
return context.agentConfig.agentUser?.userId ?? "$DEFAULT_AGENT_USER_ID";
|
|
9
|
+
};
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { CreateAllComponentsContextProps } from "../context/createAllComponentsContext";
|
|
2
2
|
import { createAllComponentsContext } from "../context/createAllComponentsContext";
|
|
3
3
|
import { createWorkspaceContext } from "../context/createWorkspaceContext";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentContext,
|
|
6
|
+
ComponentContext,
|
|
7
|
+
WorkspaceContext,
|
|
8
|
+
} from "../types";
|
|
5
9
|
import { componentContextHasWorkspaceBuild } from "../types";
|
|
6
10
|
import type { CatladderJob } from "../types/jobs";
|
|
7
11
|
import { createJobsForComponentContext } from "./createJobsForComponent";
|
|
8
12
|
import { createJobsForWorkspace } from "./createJobsForWorkspace";
|
|
13
|
+
import { createJobsForAgentContext } from "./agent/createJobsForAgentContext";
|
|
14
|
+
import { createAgentContext } from "./agent/createAgentContext";
|
|
9
15
|
|
|
10
16
|
export type AllCatladderJobs = {
|
|
11
17
|
workspaces: Array<{
|
|
@@ -16,6 +22,10 @@ export type AllCatladderJobs = {
|
|
|
16
22
|
context: ComponentContext;
|
|
17
23
|
jobs: Array<CatladderJob>;
|
|
18
24
|
}>;
|
|
25
|
+
agents: Array<{
|
|
26
|
+
context: AgentContext;
|
|
27
|
+
jobs: Array<CatladderJob>;
|
|
28
|
+
}>;
|
|
19
29
|
};
|
|
20
30
|
|
|
21
31
|
export const createAllJobs = async ({
|
|
@@ -68,5 +78,14 @@ export const createAllJobs = async ({
|
|
|
68
78
|
jobs: createJobsForComponentContext(context),
|
|
69
79
|
};
|
|
70
80
|
}),
|
|
81
|
+
agents: await Promise.all(
|
|
82
|
+
Object.keys(config.agents ?? {}).map(async (agentName) => {
|
|
83
|
+
const context = await createAgentContext({ agentName, config });
|
|
84
|
+
return {
|
|
85
|
+
context,
|
|
86
|
+
jobs: createJobsForAgentContext(context),
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
).then((f) => f.flat()),
|
|
71
90
|
};
|
|
72
91
|
};
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
RULE_IS_MAIN_BRANCH_AND_NOT_RELEASE_COMMIT,
|
|
3
3
|
RULE_IS_MERGE_REQUEST,
|
|
4
4
|
RULE_IS_TAGGED_RELEASE,
|
|
5
|
+
RULE_NEVER_ON_AGENT_TRIGGER,
|
|
5
6
|
} from "../rules";
|
|
6
7
|
import type {
|
|
7
8
|
ComponentContext,
|
|
@@ -66,7 +67,16 @@ export const createMainPipeline = async <T extends PipelineType>(
|
|
|
66
67
|
);
|
|
67
68
|
return aIndex - bIndex;
|
|
68
69
|
});
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
const allGlobalJobs = allJobsPerTrigger.filter(
|
|
72
|
+
(j) => j.context?.type === "agent",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const allJobs = [
|
|
76
|
+
...allWorkspaceJobs,
|
|
77
|
+
...allComponentJobs,
|
|
78
|
+
...allGlobalJobs,
|
|
79
|
+
].reduce(
|
|
70
80
|
(acc, { gitlabJob, name, context }) => {
|
|
71
81
|
// merge jobs, if a job is already there, merge the rules
|
|
72
82
|
// this is currently needed because of envMode: "none", which creates the same job for all triggers, so it can appear multiple times
|
|
@@ -109,10 +119,15 @@ function getGitlabRulesForTrigger(trigger: PipelineTrigger): GitlabRule[] {
|
|
|
109
119
|
// taggedRelease: on tag
|
|
110
120
|
switch (trigger) {
|
|
111
121
|
case "mainBranch":
|
|
112
|
-
return [
|
|
122
|
+
return [
|
|
123
|
+
RULE_NEVER_ON_AGENT_TRIGGER,
|
|
124
|
+
RULE_IS_MAIN_BRANCH_AND_NOT_RELEASE_COMMIT,
|
|
125
|
+
];
|
|
113
126
|
case "mr":
|
|
114
|
-
return [RULE_IS_MERGE_REQUEST];
|
|
127
|
+
return [RULE_NEVER_ON_AGENT_TRIGGER, RULE_IS_MERGE_REQUEST];
|
|
115
128
|
case "taggedRelease":
|
|
116
|
-
return [RULE_IS_TAGGED_RELEASE];
|
|
129
|
+
return [RULE_NEVER_ON_AGENT_TRIGGER, RULE_IS_TAGGED_RELEASE];
|
|
117
130
|
}
|
|
131
|
+
|
|
132
|
+
throw new Error(`${trigger} is not supported`);
|
|
118
133
|
}
|
|
@@ -2,6 +2,7 @@ import { isEmpty, isObject, merge } from "lodash";
|
|
|
2
2
|
import { getInjectVarsScript } from "../../bash/getInjectVarsScript";
|
|
3
3
|
import { BASE_RETRY } from "../../defaults";
|
|
4
4
|
import type {
|
|
5
|
+
AgentContext,
|
|
5
6
|
ComponentContext,
|
|
6
7
|
Context,
|
|
7
8
|
GitlabJobDef,
|
|
@@ -17,7 +18,7 @@ import { getBashVariable } from "../../bash/BashExpression";
|
|
|
17
18
|
|
|
18
19
|
export type GitlabJobWithContext = {
|
|
19
20
|
gitlabJob: GitlabJobDef;
|
|
20
|
-
context: Context | null;
|
|
21
|
+
context: Context | AgentContext | null;
|
|
21
22
|
};
|
|
22
23
|
export type AllGitlabJobs = (GitlabJobWithContext & { name: string })[];
|
|
23
24
|
|
|
@@ -29,14 +30,14 @@ const getFullJobName = ({
|
|
|
29
30
|
allJobs,
|
|
30
31
|
env,
|
|
31
32
|
}: {
|
|
32
|
-
type: "component" | "workspace";
|
|
33
|
+
type: "component" | "workspace" | "agent";
|
|
33
34
|
name: string;
|
|
34
35
|
baseName: string;
|
|
35
36
|
allJobs: AllCatladderJobs;
|
|
36
37
|
env?: string | null;
|
|
37
38
|
}) => {
|
|
38
39
|
const shouldAddIcon = allJobs.workspaces.length > 0;
|
|
39
|
-
const icon = type === "component" ? "🔹" : "🔸";
|
|
40
|
+
const icon = type === "component" ? "🔹" : type === "agent" ? "🤖" : "🔸";
|
|
40
41
|
const prefix = shouldAddIcon ? icon + " " : "";
|
|
41
42
|
if (env) {
|
|
42
43
|
return `${prefix}${baseName} ${name} | ${env} `;
|
|
@@ -98,7 +99,7 @@ const getJobName = (need: CatladderJobNeed) =>
|
|
|
98
99
|
isObject(need) ? need.job : need;
|
|
99
100
|
|
|
100
101
|
export const makeGitlabJob = (
|
|
101
|
-
context: Context,
|
|
102
|
+
context: Context | AgentContext,
|
|
102
103
|
job: CatladderJob<string>,
|
|
103
104
|
allJobs: AllCatladderJobs,
|
|
104
105
|
baseRules?: GitlabRule[],
|
|
@@ -118,7 +119,9 @@ export const makeGitlabJob = (
|
|
|
118
119
|
...rest
|
|
119
120
|
} = job;
|
|
120
121
|
const stage =
|
|
121
|
-
envMode === "stagePerEnv"
|
|
122
|
+
envMode === "stagePerEnv" && context.type !== "agent"
|
|
123
|
+
? `${job.stage} ${context.env}`
|
|
124
|
+
: job.stage;
|
|
122
125
|
|
|
123
126
|
const deduplicatedGitlabNeeds: GitlabJobDef["needs"] = getGitlabNeeds(
|
|
124
127
|
context,
|
|
@@ -130,7 +133,8 @@ export const makeGitlabJob = (
|
|
|
130
133
|
type: context.type,
|
|
131
134
|
name,
|
|
132
135
|
baseName: context.name,
|
|
133
|
-
env:
|
|
136
|
+
env:
|
|
137
|
+
envMode !== "none" && context.type !== "agent" ? context.env : undefined,
|
|
134
138
|
allJobs,
|
|
135
139
|
});
|
|
136
140
|
|
|
@@ -184,6 +188,8 @@ export const makeGitlabJob = (
|
|
|
184
188
|
];
|
|
185
189
|
|
|
186
190
|
const gitlabJob: GitlabJobDef = {
|
|
191
|
+
retry: BASE_RETRY,
|
|
192
|
+
interruptible: true,
|
|
187
193
|
...rest,
|
|
188
194
|
rules: rules.length > 0 ? rules : undefined,
|
|
189
195
|
variables: {
|
|
@@ -196,8 +202,6 @@ export const makeGitlabJob = (
|
|
|
196
202
|
|
|
197
203
|
// sort in a predictable manner for snapshot tests
|
|
198
204
|
needs: deduplicatedGitlabNeeds,
|
|
199
|
-
retry: BASE_RETRY,
|
|
200
|
-
interruptible: true,
|
|
201
205
|
};
|
|
202
206
|
const modified = addGitlabEnvironment(
|
|
203
207
|
context,
|
|
@@ -210,7 +214,7 @@ export const makeGitlabJob = (
|
|
|
210
214
|
};
|
|
211
215
|
|
|
212
216
|
const addGitlabEnvironment = (
|
|
213
|
-
context: Context,
|
|
217
|
+
context: Context | AgentContext,
|
|
214
218
|
catladderJobEnvironment: CatladderJob["environment"],
|
|
215
219
|
job: GitlabJobDef,
|
|
216
220
|
allJobs: AllCatladderJobs,
|
|
@@ -219,7 +223,7 @@ const addGitlabEnvironment = (
|
|
|
219
223
|
return job;
|
|
220
224
|
}
|
|
221
225
|
if (context.type !== "component") {
|
|
222
|
-
// don't add enviornment for workspace jobs atm.
|
|
226
|
+
// don't add enviornment for workspace and agent jobs atm.
|
|
223
227
|
return job;
|
|
224
228
|
}
|
|
225
229
|
const { env, name, environment } = context;
|
|
@@ -280,36 +284,41 @@ export const createGitlabJobs = async (
|
|
|
280
284
|
baseRules?: GitlabRule[],
|
|
281
285
|
): Promise<AllGitlabJobs> => {
|
|
282
286
|
// TODO: add workspace jobs
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
...allJobs.workspaces,
|
|
290
|
+
...allJobs.components,
|
|
291
|
+
...allJobs.agents,
|
|
292
|
+
].flatMap(({ context, jobs }) => {
|
|
293
|
+
return jobs.map((job) => {
|
|
294
|
+
const [fullJobName, gitlabJob] = makeGitlabJob(
|
|
295
|
+
context,
|
|
296
|
+
job,
|
|
297
|
+
allJobs,
|
|
298
|
+
baseRules,
|
|
299
|
+
);
|
|
300
|
+
return {
|
|
301
|
+
name: fullJobName,
|
|
302
|
+
gitlabJob,
|
|
303
|
+
context,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
});
|
|
300
307
|
};
|
|
301
308
|
|
|
302
309
|
function getGitlabNeeds(
|
|
303
|
-
context: Context,
|
|
310
|
+
context: Context | AgentContext,
|
|
304
311
|
job: CatladderJob<string>,
|
|
305
312
|
allJobs: AllCatladderJobs,
|
|
306
313
|
): GitlabJobDef["needs"] {
|
|
307
314
|
const needs =
|
|
308
315
|
context.type === "workspace"
|
|
309
316
|
? getGitlabNeedsForWorkspaceJob(context, job, allJobs)
|
|
310
|
-
:
|
|
317
|
+
: context.type === "agent"
|
|
318
|
+
? (job.needs ?? null)
|
|
319
|
+
: getGitlabNeedsForComponentJob(context, job, allJobs);
|
|
311
320
|
|
|
312
|
-
return deduplicateNeeds(needs);
|
|
321
|
+
return needs ? deduplicateNeeds(needs) : undefined;
|
|
313
322
|
}
|
|
314
323
|
function deduplicateNeeds(needs: GitlabJobDef["needs"]): GitlabJobDef["needs"] {
|
|
315
324
|
return needs
|
|
@@ -37,6 +37,13 @@ export const createGitlabPipelineWithDefaults = ({
|
|
|
37
37
|
workflow: {
|
|
38
38
|
name: "$PIPELINE_ICON $PIPELINE_NAME",
|
|
39
39
|
rules: [
|
|
40
|
+
{
|
|
41
|
+
if: '$CI_PIPELINE_SOURCE == "trigger"',
|
|
42
|
+
variables: {
|
|
43
|
+
PIPELINE_ICON: "🤖",
|
|
44
|
+
PIPELINE_NAME: "Thinking...",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
40
47
|
{
|
|
41
48
|
if: RULE_IS_MERGE_REQUEST.if,
|
|
42
49
|
variables: {
|
|
@@ -58,6 +65,7 @@ export const createGitlabPipelineWithDefaults = ({
|
|
|
58
65
|
PIPELINE_NAME: "Main",
|
|
59
66
|
},
|
|
60
67
|
},
|
|
68
|
+
|
|
61
69
|
{
|
|
62
70
|
when: "always", // fallback
|
|
63
71
|
variables: {
|
package/src/rules/index.ts
CHANGED
|
@@ -18,6 +18,11 @@ export const RULE_NEVER_ON_RELEASE_COMMIT: GitlabRule = {
|
|
|
18
18
|
if: RULE_CONDITION_RELEASE_COMMIT,
|
|
19
19
|
when: "never",
|
|
20
20
|
};
|
|
21
|
+
// currently, we consider all triggered pipelines as agent triggers
|
|
22
|
+
export const RULE_NEVER_ON_AGENT_TRIGGER: GitlabRule = {
|
|
23
|
+
if: '$CI_PIPELINE_SOURCE == "trigger"',
|
|
24
|
+
when: "never",
|
|
25
|
+
};
|
|
21
26
|
|
|
22
27
|
export const RULE_NEVER_ON_SCHEDULE: GitlabRule = {
|
|
23
28
|
if: '$CI_PIPELINE_SOURCE == "schedule"',
|
|
@@ -37,6 +42,7 @@ export const RULE_CONDITION_HOTFIX_BRANCH =
|
|
|
37
42
|
|
|
38
43
|
export const RULES_RELEASE: GitlabRule[] = [
|
|
39
44
|
RULE_NEVER_ON_RELEASE_COMMIT,
|
|
45
|
+
RULE_NEVER_ON_AGENT_TRIGGER,
|
|
40
46
|
RULE_NEVER_ON_SCHEDULE,
|
|
41
47
|
{
|
|
42
48
|
if: RULE_CONDITION_MAIN_BRANCH + ' && $AUTO_RELEASE == "true"',
|
|
@@ -54,6 +60,7 @@ export const RULES_RELEASE: GitlabRule[] = [
|
|
|
54
60
|
|
|
55
61
|
export const RULES_MANUAL_RELEASE: GitlabRule[] = [
|
|
56
62
|
RULE_NEVER_ON_RELEASE_COMMIT,
|
|
63
|
+
RULE_NEVER_ON_AGENT_TRIGGER,
|
|
57
64
|
RULE_NEVER_ON_SCHEDULE,
|
|
58
65
|
{
|
|
59
66
|
if: RULE_CONDITION_MAIN_BRANCH,
|
package/src/types/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { CatladderJob } from "./jobs";
|
|
|
5
5
|
import type { ComponentContext } from "./context";
|
|
6
6
|
import type { PartialDeep } from "./utils";
|
|
7
7
|
import type { PipelineType, WorkspaceBuildConfig } from "..";
|
|
8
|
+
import type { AgentConfig } from "./agent";
|
|
8
9
|
|
|
9
10
|
export const ALL_PIPELINE_TRIGGERS = [
|
|
10
11
|
"mainBranch",
|
|
@@ -210,6 +211,8 @@ export type Config<C extends ConfigProps = never> = {
|
|
|
210
211
|
*/
|
|
211
212
|
domainCanonical?: string;
|
|
212
213
|
|
|
214
|
+
agents?: Record<string, AgentConfig>;
|
|
215
|
+
|
|
213
216
|
// shared workspace Builds
|
|
214
217
|
builds?: Record<string, WorkspaceBuildConfig>;
|
|
215
218
|
/**
|
package/src/types/context.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type { PredefinedVariables, SecretEnvVar } from "../context";
|
|
8
8
|
import type { DeployConfig } from "../deploy";
|
|
9
9
|
import type { VariableValue } from "../variables/VariableValue";
|
|
10
|
+
import type { AgentConfig } from "./agent";
|
|
10
11
|
import type {
|
|
11
12
|
ComponentConfig,
|
|
12
13
|
Config,
|
|
@@ -214,3 +215,10 @@ export type WorkspaceContext = {
|
|
|
214
215
|
pipelineType: PipelineType;
|
|
215
216
|
env: string;
|
|
216
217
|
};
|
|
218
|
+
|
|
219
|
+
export type AgentContext = {
|
|
220
|
+
type: "agent";
|
|
221
|
+
name: string;
|
|
222
|
+
fullConfig: Config;
|
|
223
|
+
agentConfig: AgentConfig;
|
|
224
|
+
};
|
package/src/types/index.ts
CHANGED
package/src/types/jobs.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const BASE_STAGES = [
|
|
|
15
15
|
"build",
|
|
16
16
|
"deploy",
|
|
17
17
|
"verify",
|
|
18
|
+
"agents",
|
|
18
19
|
"rollback",
|
|
19
20
|
"stop",
|
|
20
21
|
] as const;
|
|
@@ -129,4 +130,9 @@ export type CatladderJob<S = BaseStage> = {
|
|
|
129
130
|
* tags for the underlying job runner (e.g gitlab)
|
|
130
131
|
*/
|
|
131
132
|
jobTags?: string[];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* whether the job is interruptible (default: true)
|
|
136
|
+
*/
|
|
137
|
+
interruptible?: boolean;
|
|
132
138
|
};
|