@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.
Files changed (2) hide show
  1. package/dist/cli.js +104 -17
  2. 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 existsSync2, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
6
- import path5 from "node:path";
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 path4 from "node:path";
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(path4.join(tmpdir3(), "growth-agent-pi-"));
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(path4.join(dir, "models.json"), JSON.stringify(config, null, 2));
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 = path5.dirname(fileURLToPath(import.meta.url));
447
+ var HERE = path6.dirname(fileURLToPath(import.meta.url));
366
448
  var PATCH_FILE = "growth-agent.patch";
367
449
  function assetsDir() {
368
- const packaged = path5.join(HERE, "assets", "skills");
369
- return existsSync2(packaged) ? packaged : path5.join(HERE, "..", "assets", "skills");
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 = spawnSync(cmd, args, { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
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 = path5.resolve(process.cwd(), ".pi", "skills");
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 = spawnSync(
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", ["checkout", "--", ".pi"], { allowFail: true });
541
- sh("git", ["clean", "-fd", ".pi"], { allowFail: true });
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 (!existsSync2(m.lockfile)) continue;
654
+ if (!existsSync3(m.lockfile)) continue;
568
655
  console.log(`\u25B8 regenerating ${m.lockfile} from the manifest`);
569
- const result = spawnSync(m.bin, m.args, { stdio: ["ignore", "inherit", "inherit"] });
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 (!existsSync2(PATCH_FILE)) {
676
+ if (!existsSync3(PATCH_FILE)) {
590
677
  console.error(`No ${PATCH_FILE} was produced by the implementer job.`);
591
678
  return 1;
592
679
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthagent/ci",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {