@growthagent/ci 0.4.0 → 0.6.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/dist/cli.js +104 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
-
import { cpSync, existsSync as
|
|
6
|
-
import
|
|
4
|
+
import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
5
|
+
import { cpSync, existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
+
import path6 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// ../github/src/app.ts
|
|
@@ -53,6 +53,34 @@ import path3 from "node:path";
|
|
|
53
53
|
import { promisify } from "node:util";
|
|
54
54
|
var run = promisify(execFile);
|
|
55
55
|
|
|
56
|
+
// src/lockfiles.ts
|
|
57
|
+
import { spawnSync } from "node:child_process";
|
|
58
|
+
import { existsSync as existsSync2, rmSync as rmSync2 } from "node:fs";
|
|
59
|
+
import path4 from "node:path";
|
|
60
|
+
var LOCKFILES = [
|
|
61
|
+
"pnpm-lock.yaml",
|
|
62
|
+
"package-lock.json",
|
|
63
|
+
"yarn.lock",
|
|
64
|
+
"bun.lockb",
|
|
65
|
+
"bun.lock",
|
|
66
|
+
"npm-shrinkwrap.json"
|
|
67
|
+
];
|
|
68
|
+
function discardLockfileChanges(cwd = process.cwd()) {
|
|
69
|
+
const git = (args) => spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
70
|
+
const touched = [];
|
|
71
|
+
for (const lockfile of LOCKFILES) {
|
|
72
|
+
if (!existsSync2(path4.join(cwd, lockfile))) continue;
|
|
73
|
+
const tracked = git(["ls-files", "--error-unmatch", "--", lockfile]).status === 0;
|
|
74
|
+
if (tracked) {
|
|
75
|
+
git(["restore", "--staged", "--worktree", "--", lockfile]);
|
|
76
|
+
} else {
|
|
77
|
+
rmSync2(path4.join(cwd, lockfile), { force: true });
|
|
78
|
+
}
|
|
79
|
+
touched.push(lockfile);
|
|
80
|
+
}
|
|
81
|
+
return touched;
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
// src/patch.ts
|
|
57
85
|
var MAX_CAPTURED_LINES = 400;
|
|
58
86
|
var ALLOWED_DEPENDENCIES = ["posthog-js", "posthog-node"];
|
|
@@ -100,6 +128,7 @@ var FORBIDDEN_PATTERNS = [
|
|
|
100
128
|
{ pattern: /^\.github\//i, why: "workflow and CI configuration" },
|
|
101
129
|
{ pattern: /^\.git(\/|$)/i, why: "git internals" },
|
|
102
130
|
{ pattern: /(^|\/)\.env(\.|$)/i, why: "environment files" },
|
|
131
|
+
// except the examples below
|
|
103
132
|
{ pattern: /(^|\/)\.npmrc$|(^|\/)\.yarnrc(\.yml)?$|(^|\/)\.pypirc$/i, why: "registry credentials" },
|
|
104
133
|
{ pattern: /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/i, why: "dependency lockfiles" },
|
|
105
134
|
{ pattern: /(^|\/)(Dockerfile|docker-compose\.ya?ml|Procfile)$/i, why: "build and deployment configuration" },
|
|
@@ -111,6 +140,44 @@ var FORBIDDEN_PATTERNS = [
|
|
|
111
140
|
{ pattern: /\.(pem|p12|pfx|key|keystore|jks)$/i, why: "key material" },
|
|
112
141
|
{ pattern: /(^|\/)node_modules\//i, why: "installed dependencies" }
|
|
113
142
|
];
|
|
143
|
+
var EXAMPLE_ENV = /(^|\/)\.env(\.[\w-]+)*\.(example|sample|template)$/i;
|
|
144
|
+
var CREDENTIAL_PREFIXES = [
|
|
145
|
+
"sk-",
|
|
146
|
+
"sk_live",
|
|
147
|
+
"sk_test",
|
|
148
|
+
"pk_live",
|
|
149
|
+
"rk_live",
|
|
150
|
+
"whsec_",
|
|
151
|
+
"phc_",
|
|
152
|
+
"ghp_",
|
|
153
|
+
"gho_",
|
|
154
|
+
"ghs_",
|
|
155
|
+
"github_pat_",
|
|
156
|
+
"AKIA",
|
|
157
|
+
"AIza",
|
|
158
|
+
"xox",
|
|
159
|
+
"eyJ",
|
|
160
|
+
"-----BEGIN"
|
|
161
|
+
];
|
|
162
|
+
function secretishEnvLine(line) {
|
|
163
|
+
const text = line.trim();
|
|
164
|
+
if (!text || text.startsWith("#")) return null;
|
|
165
|
+
const eq = text.indexOf("=");
|
|
166
|
+
if (eq < 0) return null;
|
|
167
|
+
const key = text.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
168
|
+
const value = text.slice(eq + 1).trim().replace(/^["']|["']$/g, "").trim();
|
|
169
|
+
if (!value) return null;
|
|
170
|
+
if (/^https?:\/\//i.test(value)) return null;
|
|
171
|
+
for (const prefix of CREDENTIAL_PREFIXES) {
|
|
172
|
+
if (value.startsWith(prefix)) {
|
|
173
|
+
return `${key} carries what looks like a real credential`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (value.length >= 20 && /^[A-Za-z0-9_\-.]+$/.test(value)) {
|
|
177
|
+
return `${key} carries a ${value.length}-character opaque value, not a placeholder`;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
114
181
|
var SYMLINK_MODE = "120000";
|
|
115
182
|
var EXECUTABLE_MODE = "100755";
|
|
116
183
|
function parsePatch(patch) {
|
|
@@ -183,6 +250,7 @@ function forbiddenReason(filePath) {
|
|
|
183
250
|
return "an absolute path";
|
|
184
251
|
}
|
|
185
252
|
if (normalised.split("/").includes("..")) return "a parent-directory escape";
|
|
253
|
+
if (EXAMPLE_ENV.test(normalised)) return null;
|
|
186
254
|
for (const { pattern, why } of FORBIDDEN_PATTERNS) {
|
|
187
255
|
if (pattern.test(normalised)) return why;
|
|
188
256
|
}
|
|
@@ -210,6 +278,20 @@ function validatePatch(patch, limits = DEFAULT_PATCH_LIMITS) {
|
|
|
210
278
|
return { ok: false, reason: `the patch changes ${candidate}, which is ${why} \u2014 never modifiable by the agent` };
|
|
211
279
|
}
|
|
212
280
|
}
|
|
281
|
+
if (EXAMPLE_ENV.test(file.path)) {
|
|
282
|
+
if (file.addedLines > file.added.length) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
reason: `the patch adds ${file.addedLines} lines to ${file.path}, more than can be checked for credentials`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
for (const line of file.added) {
|
|
289
|
+
const why = secretishEnvLine(line);
|
|
290
|
+
if (why) {
|
|
291
|
+
return { ok: false, reason: `the patch writes a secret into ${file.path}: ${why}` };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
213
295
|
if (/(^|\/)package\.json$/i.test(file.path)) {
|
|
214
296
|
const why = manifestChangeReason(file);
|
|
215
297
|
if (why) {
|
|
@@ -238,11 +320,11 @@ function validatePatch(patch, limits = DEFAULT_PATCH_LIMITS) {
|
|
|
238
320
|
// src/provider-config.ts
|
|
239
321
|
import { mkdtempSync as mkdtempSync2, writeFileSync } from "node:fs";
|
|
240
322
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
241
|
-
import
|
|
323
|
+
import path5 from "node:path";
|
|
242
324
|
var RUN_TOKEN_ENV = "GX_RUN_TOKEN";
|
|
243
325
|
function writeGoogleProviderConfig(model) {
|
|
244
326
|
const gateway = (process.env.GX_GATEWAY_URL ?? "").replace(/\/$/, "");
|
|
245
|
-
const dir = mkdtempSync2(
|
|
327
|
+
const dir = mkdtempSync2(path5.join(tmpdir3(), "growth-agent-pi-"));
|
|
246
328
|
const config = {
|
|
247
329
|
providers: {
|
|
248
330
|
"growthagent-google": {
|
|
@@ -253,7 +335,7 @@ function writeGoogleProviderConfig(model) {
|
|
|
253
335
|
}
|
|
254
336
|
}
|
|
255
337
|
};
|
|
256
|
-
writeFileSync(
|
|
338
|
+
writeFileSync(path5.join(dir, "models.json"), JSON.stringify(config, null, 2));
|
|
257
339
|
return dir;
|
|
258
340
|
}
|
|
259
341
|
|
|
@@ -362,14 +444,14 @@ function buildPrompt(fields, briefing) {
|
|
|
362
444
|
}
|
|
363
445
|
|
|
364
446
|
// src/cli.ts
|
|
365
|
-
var HERE =
|
|
447
|
+
var HERE = path6.dirname(fileURLToPath(import.meta.url));
|
|
366
448
|
var PATCH_FILE = "growth-agent.patch";
|
|
367
449
|
function assetsDir() {
|
|
368
|
-
const packaged =
|
|
369
|
-
return
|
|
450
|
+
const packaged = path6.join(HERE, "assets", "skills");
|
|
451
|
+
return existsSync3(packaged) ? packaged : path6.join(HERE, "..", "assets", "skills");
|
|
370
452
|
}
|
|
371
453
|
function sh(cmd, args, opts = {}) {
|
|
372
|
-
const r =
|
|
454
|
+
const r = spawnSync2(cmd, args, { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
|
|
373
455
|
if (r.status !== 0 && !opts.allowFail) {
|
|
374
456
|
throw new Error(`${cmd} ${args.join(" ")} exited ${r.status}`);
|
|
375
457
|
}
|
|
@@ -493,7 +575,7 @@ async function implement() {
|
|
|
493
575
|
console.log(
|
|
494
576
|
meta.kind === "instrumentation" ? `\u25B8 instrumentation request #${issue.number}` : `\u25B8 change request #${issue.number} \u2014 flag ${meta.flagKey} on ${meta.surface}`
|
|
495
577
|
);
|
|
496
|
-
const skillsDir =
|
|
578
|
+
const skillsDir = path6.resolve(process.cwd(), ".pi", "skills");
|
|
497
579
|
mkdirSync(skillsDir, { recursive: true });
|
|
498
580
|
cpSync(assetsDir(), skillsDir, { recursive: true });
|
|
499
581
|
const prompt = buildPrompt(meta, extractBriefing(issue.body));
|
|
@@ -509,7 +591,7 @@ async function implement() {
|
|
|
509
591
|
console.log(`\u25B8 running pi (${piProvider}/${model})`);
|
|
510
592
|
let piStatus;
|
|
511
593
|
try {
|
|
512
|
-
const pi =
|
|
594
|
+
const pi = spawnSync2(
|
|
513
595
|
"pi",
|
|
514
596
|
["-p", prompt, "--provider", piProvider, "--model", model, "--thinking", "high"],
|
|
515
597
|
{
|
|
@@ -537,8 +619,13 @@ async function implement() {
|
|
|
537
619
|
console.error("pi exited non-zero \u2014 no patch produced");
|
|
538
620
|
return 1;
|
|
539
621
|
}
|
|
540
|
-
sh("git", ["
|
|
541
|
-
sh("git", ["
|
|
622
|
+
const piTracked = sh("git", ["ls-files", "--", ".pi"], { allowFail: true });
|
|
623
|
+
if (piTracked) sh("git", ["checkout", "--", ".pi"], { allowFail: true });
|
|
624
|
+
sh("git", ["clean", "-fdq", ".pi"], { allowFail: true });
|
|
625
|
+
const discarded = discardLockfileChanges();
|
|
626
|
+
if (discarded.length) {
|
|
627
|
+
console.log(`\u25B8 discarded the model's ${discarded.join(", ")}; the trusted job derives it`);
|
|
628
|
+
}
|
|
542
629
|
sh("git", ["add", "-A"]);
|
|
543
630
|
const patch = sh("git", ["diff", "--staged"]);
|
|
544
631
|
if (!patch.trim()) {
|
|
@@ -564,9 +651,9 @@ function regenerateLockfile() {
|
|
|
564
651
|
{ lockfile: "yarn.lock", bin: "yarn", args: ["install", "--mode=update-lockfile"] }
|
|
565
652
|
];
|
|
566
653
|
for (const m of managers) {
|
|
567
|
-
if (!
|
|
654
|
+
if (!existsSync3(m.lockfile)) continue;
|
|
568
655
|
console.log(`\u25B8 regenerating ${m.lockfile} from the manifest`);
|
|
569
|
-
const result =
|
|
656
|
+
const result = spawnSync2(m.bin, m.args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
570
657
|
if (result.status !== 0) {
|
|
571
658
|
console.error(
|
|
572
659
|
`Could not regenerate ${m.lockfile} (${m.bin} exited ${result.status}). The dependency change is in package.json but the lockfile is stale \u2014 run the install locally on this branch before merging.`
|
|
@@ -586,7 +673,7 @@ function apply() {
|
|
|
586
673
|
const meta = validatedMeta(issue);
|
|
587
674
|
const title = safeTitle(issue);
|
|
588
675
|
const branch = branchFor(issue, meta.slug);
|
|
589
|
-
if (!
|
|
676
|
+
if (!existsSync3(PATCH_FILE)) {
|
|
590
677
|
console.error(`No ${PATCH_FILE} was produced by the implementer job.`);
|
|
591
678
|
return 1;
|
|
592
679
|
}
|