@catladder/pipeline 3.13.0 → 3.14.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.
Files changed (107) hide show
  1. package/dist/bash/bashEscape.d.ts +2 -0
  2. package/dist/bash/bashEscape.js +9 -1
  3. package/dist/constants.js +1 -1
  4. package/dist/pipeline/agent/createAgentContext.d.ts +6 -0
  5. package/dist/pipeline/agent/createAgentContext.js +141 -0
  6. package/dist/pipeline/agent/createAgentEventJob.d.ts +2 -0
  7. package/dist/pipeline/agent/createAgentEventJob.js +74 -0
  8. package/dist/pipeline/agent/createAgentReviewJob.d.ts +2 -0
  9. package/dist/pipeline/agent/createAgentReviewJob.js +69 -0
  10. package/dist/pipeline/agent/createJobsForAgentContext.d.ts +2 -0
  11. package/dist/pipeline/agent/createJobsForAgentContext.js +12 -0
  12. package/dist/pipeline/agent/prompts.d.ts +10 -0
  13. package/dist/pipeline/agent/prompts.js +66 -0
  14. package/dist/pipeline/agent/shared.d.ts +8 -0
  15. package/dist/pipeline/agent/shared.js +29 -0
  16. package/dist/pipeline/agent/utils.d.ts +3 -0
  17. package/dist/pipeline/agent/utils.js +16 -0
  18. package/dist/pipeline/createAllJobs.d.ts +5 -1
  19. package/dist/pipeline/createAllJobs.js +32 -6
  20. package/dist/pipeline/createMainPipeline.js +10 -5
  21. package/dist/pipeline/gitlab/createGitlabJobs.d.ts +3 -3
  22. package/dist/pipeline/gitlab/createGitlabJobs.js +13 -11
  23. package/dist/pipeline/gitlab/createGitlabPipeline.js +7 -1
  24. package/dist/rules/index.d.ts +1 -0
  25. package/dist/rules/index.js +8 -3
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/types/agent.d.ts +7 -0
  28. package/dist/types/agent.js +5 -0
  29. package/dist/types/config.d.ts +2 -0
  30. package/dist/types/context.d.ts +7 -0
  31. package/dist/types/index.d.ts +2 -1
  32. package/dist/types/index.js +2 -1
  33. package/dist/types/jobs.d.ts +5 -1
  34. package/dist/types/jobs.js +1 -1
  35. package/examples/__snapshots__/cloud-run-health-check-defaults.test.ts.snap +66 -1
  36. package/examples/__snapshots__/cloud-run-health-check-only-startup.test.ts.snap +66 -1
  37. package/examples/__snapshots__/cloud-run-health-check.test.ts.snap +66 -1
  38. package/examples/__snapshots__/cloud-run-http2.test.ts.snap +66 -1
  39. package/examples/__snapshots__/cloud-run-llama.test.ts.snap +30 -1
  40. package/examples/__snapshots__/cloud-run-memory-limit.test.ts.snap +66 -1
  41. package/examples/__snapshots__/cloud-run-meteor-with-worker.test.ts.snap +66 -1
  42. package/examples/__snapshots__/cloud-run-nextjs.test.ts.snap +66 -1
  43. package/examples/__snapshots__/cloud-run-no-cpu-throttling.test.ts.snap +66 -1
  44. package/examples/__snapshots__/cloud-run-no-service.test.ts.snap +66 -1
  45. package/examples/__snapshots__/cloud-run-non-public.test.ts.snap +66 -1
  46. package/examples/__snapshots__/cloud-run-post-stop-job.test.ts.snap +66 -1
  47. package/examples/__snapshots__/cloud-run-service-custom-vpc-connector.test.ts.snap +66 -1
  48. package/examples/__snapshots__/cloud-run-service-custom-vpc.test.ts.snap +66 -1
  49. package/examples/__snapshots__/cloud-run-service-gen2.test.ts.snap +66 -1
  50. package/examples/__snapshots__/cloud-run-service-increase-timout.test.ts.snap +66 -1
  51. package/examples/__snapshots__/cloud-run-service-with-volumes.test.ts.snap +66 -1
  52. package/examples/__snapshots__/cloud-run-storybook.test.ts.snap +54 -1
  53. package/examples/__snapshots__/cloud-run-with-agents.test.ts.snap +1576 -0
  54. package/examples/__snapshots__/cloud-run-with-gpu.test.ts.snap +66 -1
  55. package/examples/__snapshots__/cloud-run-with-ngnix.test.ts.snap +66 -1
  56. package/examples/__snapshots__/cloud-run-with-sql-legacy-jobs.test.ts.snap +66 -1
  57. package/examples/__snapshots__/cloud-run-with-sql-multiple-dbs.test.ts.snap +170 -1
  58. package/examples/__snapshots__/cloud-run-with-sql-reuse-db.test.ts.snap +118 -1
  59. package/examples/__snapshots__/cloud-run-with-sql.test.ts.snap +66 -1
  60. package/examples/__snapshots__/cloud-run-with-worker.test.ts.snap +66 -1
  61. package/examples/__snapshots__/custom-build-job-with-tests.test.ts.snap +66 -1
  62. package/examples/__snapshots__/custom-build-job.test.ts.snap +54 -1
  63. package/examples/__snapshots__/custom-deploy.test.ts.snap +60 -1
  64. package/examples/__snapshots__/custom-envs.test.ts.snap +64 -1
  65. package/examples/__snapshots__/custom-sbom-java.test.ts.snap +54 -1
  66. package/examples/__snapshots__/custom-verify-job.test.ts.snap +74 -1
  67. package/examples/__snapshots__/git-submodule.test.ts.snap +66 -1
  68. package/examples/__snapshots__/kubernetes-application-customization.test.ts.snap +74 -1
  69. package/examples/__snapshots__/kubernetes-with-cloud-sql.test.ts.snap +74 -1
  70. package/examples/__snapshots__/kubernetes-with-jobs.test.ts.snap +134 -1
  71. package/examples/__snapshots__/kubernetes-with-mongodb.test.ts.snap +74 -1
  72. package/examples/__snapshots__/local-dot-env.test.ts.snap +66 -1
  73. package/examples/__snapshots__/meteor-kubernetes.test.ts.snap +74 -1
  74. package/examples/__snapshots__/multiline-var.test.ts.snap +178 -1
  75. package/examples/__snapshots__/native-app.test.ts.snap +102 -1
  76. package/examples/__snapshots__/node-build-with-custom-image.test.ts.snap +66 -1
  77. package/examples/__snapshots__/node-build-with-docker-additions.test.ts.snap +66 -1
  78. package/examples/__snapshots__/override-secrets.test.ts.snap +66 -1
  79. package/examples/__snapshots__/rails-k8s-with-worker-dockerfile.test.ts.snap +66 -1
  80. package/examples/__snapshots__/rails-k8s-with-worker.test.ts.snap +66 -1
  81. package/examples/__snapshots__/referencing-other-vars.test.ts.snap +178 -1
  82. package/examples/__snapshots__/wait-for-other-deploy.test.ts.snap +86 -1
  83. package/examples/__snapshots__/workspace-api-www-turbo-cache.test.ts.snap +98 -1
  84. package/examples/__snapshots__/workspace-api-www.test.ts.snap +98 -1
  85. package/examples/cloud-run-with-agents.test.ts +11 -0
  86. package/examples/cloud-run-with-agents.ts +36 -0
  87. package/package.json +1 -1
  88. package/src/bash/bashEscape.ts +6 -0
  89. package/src/pipeline/__tests__/__snapshots__/getPipelineStages.test.ts.snap +9 -0
  90. package/src/pipeline/agent/createAgentContext.ts +19 -0
  91. package/src/pipeline/agent/createAgentEventJob.ts +35 -0
  92. package/src/pipeline/agent/createAgentReviewJob.ts +33 -0
  93. package/src/pipeline/agent/createJobsForAgentContext.ts +7 -0
  94. package/src/pipeline/agent/prompts.ts +233 -0
  95. package/src/pipeline/agent/shared.ts +36 -0
  96. package/src/pipeline/agent/utils.ts +9 -0
  97. package/src/pipeline/createAllJobs.ts +20 -1
  98. package/src/pipeline/createJobsForComponent.ts +1 -0
  99. package/src/pipeline/createMainPipeline.ts +19 -4
  100. package/src/pipeline/gitlab/createGitlabJobs.ts +39 -30
  101. package/src/pipeline/gitlab/createGitlabPipeline.ts +9 -1
  102. package/src/rules/index.ts +7 -0
  103. package/src/types/agent.ts +7 -0
  104. package/src/types/config.ts +3 -0
  105. package/src/types/context.ts +8 -0
  106. package/src/types/index.ts +1 -0
  107. 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 -- 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 { ComponentContext, WorkspaceContext } from "../types";
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
  };
@@ -39,5 +39,6 @@ export const createJobsForComponentContext = (
39
39
  : [];
40
40
 
41
41
  const customJobs = getCustomJobs(context);
42
+
42
43
  return [...buildJobs, ...deployJobs, ...customJobs];
43
44
  };
@@ -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
- const allJobs = [...allWorkspaceJobs, ...allComponentJobs].reduce(
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 [RULE_IS_MAIN_BRANCH_AND_NOT_RELEASE_COMMIT];
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" ? `${job.stage} ${context.env}` : job.stage;
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: envMode !== "none" ? context.env : undefined,
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
- return [...allJobs.workspaces, ...allJobs.components].flatMap(
284
- ({ context, jobs }) => {
285
- return jobs.map((job) => {
286
- const [fullJobName, gitlabJob] = makeGitlabJob(
287
- context,
288
- job,
289
- allJobs,
290
- baseRules,
291
- );
292
- return {
293
- name: fullJobName,
294
- gitlabJob,
295
- context,
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
- : getGitlabNeedsForComponentJob(context, job, allJobs);
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: {
@@ -55,9 +62,10 @@ export const createGitlabPipelineWithDefaults = ({
55
62
  if: RULE_IS_MAIN_BRANCH_AND_NOT_RELEASE_COMMIT.if,
56
63
  variables: {
57
64
  PIPELINE_ICON: "🐱🔨",
58
- PIPELINE_NAME: "Main $CI_COMMIT_REF_NAME",
65
+ PIPELINE_NAME: "Main",
59
66
  },
60
67
  },
68
+
61
69
  {
62
70
  when: "always", // fallback
63
71
  variables: {
@@ -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,
@@ -0,0 +1,7 @@
1
+ export type AgentConfig = {
2
+ type: "claude";
3
+ agentUser?: {
4
+ username: string;
5
+ userId: string;
6
+ };
7
+ };
@@ -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
  /**
@@ -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
+ };
@@ -4,3 +4,4 @@ export * from "./context";
4
4
  export * from "./jobDefinition";
5
5
  export * from "./pipeline";
6
6
  export * from "./jobs";
7
+ export * from "./agent";
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
  };