@synkro-sh/cli 1.3.25 → 1.3.26

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/bootstrap.js CHANGED
@@ -2508,7 +2508,7 @@ jobs:
2508
2508
  scan:
2509
2509
  runs-on: ubuntu-latest
2510
2510
  permissions:
2511
- contents: read
2511
+ contents: write
2512
2512
  pull-requests: write
2513
2513
  checks: write
2514
2514
  steps:
@@ -3333,7 +3333,7 @@ function writeConfigEnv(opts) {
3333
3333
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3334
3334
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3335
3335
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3336
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.25")}`
3336
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.26")}`
3337
3337
  ];
3338
3338
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3339
3339
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4374,6 +4374,60 @@ function ghJson(args2) {
4374
4374
  });
4375
4375
  return JSON.parse(out);
4376
4376
  }
4377
+ async function ensureOpenPr(repo, prNumber, sha) {
4378
+ let pr;
4379
+ try {
4380
+ pr = ghJson(["api", `/repos/${repo}/pulls/${prNumber}`]);
4381
+ } catch {
4382
+ return { prNumber, sha, branched: false };
4383
+ }
4384
+ if (pr.state === "open") {
4385
+ return { prNumber, sha, branched: false };
4386
+ }
4387
+ if (pr.merged) {
4388
+ console.log(`PR #${prNumber} is merged. Scanning original diff and posting findings there.
4389
+ `);
4390
+ return { prNumber, sha: pr.head.sha, branched: false };
4391
+ }
4392
+ console.log(`PR #${prNumber} is closed. Creating a scan branch...
4393
+ `);
4394
+ const scanBranch = `synkro/scan-${pr.head.ref}-${Date.now()}`;
4395
+ try {
4396
+ execSync5(`gh api -X POST /repos/${repo}/git/refs --input -`, {
4397
+ encoding: "utf-8",
4398
+ input: JSON.stringify({ ref: `refs/heads/${scanBranch}`, sha: pr.head.sha }),
4399
+ stdio: ["pipe", "pipe", "pipe"]
4400
+ });
4401
+ } catch (err) {
4402
+ console.warn(`Failed to create scan branch: ${err.message}`);
4403
+ return { prNumber, sha, branched: false };
4404
+ }
4405
+ try {
4406
+ const newPrBody = `Security scan of closed PR #${prNumber} (\`${pr.head.ref}\`).
4407
+
4408
+ This PR contains no code changes \u2014 it exists so Synkro can post review findings on an active PR.`;
4409
+ const result = ghJson([
4410
+ "api",
4411
+ "-X",
4412
+ "POST",
4413
+ `/repos/${repo}/pulls`,
4414
+ "-f",
4415
+ `title=Synkro Scan: ${pr.title}`,
4416
+ "-f",
4417
+ `body=${newPrBody}`,
4418
+ "-f",
4419
+ `head=${scanBranch}`,
4420
+ "-f",
4421
+ `base=${pr.base.ref}`
4422
+ ]);
4423
+ console.log(`Opened PR #${result.number} for scan findings.
4424
+ `);
4425
+ return { prNumber: result.number, sha: result.head.sha, branched: true };
4426
+ } catch (err) {
4427
+ console.warn(`Failed to open scan PR: ${err.message}`);
4428
+ return { prNumber, sha, branched: false };
4429
+ }
4430
+ }
4377
4431
  function getPrFiles(repo, prNumber) {
4378
4432
  const data = ghJson([
4379
4433
  "api",
@@ -4766,6 +4820,9 @@ async function scanPrCommand() {
4766
4820
  }
4767
4821
  console.log(`Synkro scan-pr: ${repo}#${prNumber} @ ${sha.slice(0, 7)}
4768
4822
  `);
4823
+ const prTarget = await ensureOpenPr(repo, prNumber, sha);
4824
+ const activePrNumber = prTarget.prNumber;
4825
+ const activeSha = prTarget.sha;
4769
4826
  const orgRules = await fetchOrgRules(gatewayUrl, synkroApiKey);
4770
4827
  const auditRules = orgRules.filter((r) => r.mode === "audit");
4771
4828
  const literalNegativeRules = orgRules.filter((r) => {
@@ -4776,22 +4833,22 @@ async function scanPrCommand() {
4776
4833
  const promptHeader = buildPrPrompt(auditRules);
4777
4834
  let files;
4778
4835
  try {
4779
- files = getPrFiles(repo, prNumber);
4836
+ files = getPrFiles(repo, activePrNumber);
4780
4837
  } catch (err) {
4781
4838
  console.error("Failed to fetch PR files:", err.message);
4782
4839
  process.exit(2);
4783
4840
  }
4784
- const scanCtx = await fetchScanContext(gatewayUrl, synkroApiKey, repo, prNumber, sha);
4841
+ const scanCtx = await fetchScanContext(gatewayUrl, synkroApiKey, repo, activePrNumber, activeSha);
4785
4842
  if (scanCtx.skip) {
4786
- console.log(`Already scanned at ${sha.slice(0, 7)}, skipping.
4843
+ console.log(`Already scanned at ${activeSha.slice(0, 7)}, skipping.
4787
4844
  `);
4788
- postCheckRun(repo, sha, "success", []);
4845
+ postCheckRun(repo, activeSha, "success", []);
4789
4846
  await postEventToBackend({
4790
4847
  gatewayUrl,
4791
4848
  apiKey: synkroApiKey,
4792
4849
  repo,
4793
- prNumber,
4794
- sha,
4850
+ prNumber: activePrNumber,
4851
+ sha: activeSha,
4795
4852
  findings: [],
4796
4853
  filesScanned: 0,
4797
4854
  totalLatencyMs: 0
@@ -4814,13 +4871,13 @@ async function scanPrCommand() {
4814
4871
  console.log(`${files.length} files in PR, ${eligible.length} eligible for scan.
4815
4872
  `);
4816
4873
  if (eligible.length === 0) {
4817
- postCheckRun(repo, sha, "success", []);
4874
+ postCheckRun(repo, activeSha, "success", []);
4818
4875
  await postEventToBackend({
4819
4876
  gatewayUrl,
4820
4877
  apiKey: synkroApiKey,
4821
4878
  repo,
4822
- prNumber,
4823
- sha,
4879
+ prNumber: activePrNumber,
4880
+ sha: activeSha,
4824
4881
  findings: [],
4825
4882
  filesScanned: 0,
4826
4883
  totalLatencyMs: 0
@@ -4847,17 +4904,17 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
4847
4904
  const review = await spawnOpusConsolidator(allFindings, claudeToken);
4848
4905
  console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
4849
4906
  if (review.comments.length > 0) {
4850
- postPrReview(repo, prNumber, sha, review);
4907
+ postPrReview(repo, activePrNumber, activeSha, review);
4851
4908
  }
4852
4909
  }
4853
4910
  const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
4854
- postCheckRun(repo, sha, conclusion, allFindings);
4911
+ postCheckRun(repo, activeSha, conclusion, allFindings);
4855
4912
  await postEventToBackend({
4856
4913
  gatewayUrl,
4857
4914
  apiKey: synkroApiKey,
4858
4915
  repo,
4859
- prNumber,
4860
- sha,
4916
+ prNumber: activePrNumber,
4917
+ sha: activeSha,
4861
4918
  findings: allFindings,
4862
4919
  filesScanned: eligible.length,
4863
4920
  totalLatencyMs