@open-tomato/rafa 0.33.0 → 0.34.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.
@@ -356,7 +356,7 @@ import {
356
356
  worktreeAt,
357
357
  writeSession,
358
358
  writeShippedFile
359
- } from "./index-w3vyb08t.js";
359
+ } from "./index-4b3m7s0z.js";
360
360
  import {
361
361
  PROBE_TIMEOUT_MS,
362
362
  loadPlanPrerequisites,
@@ -368,7 +368,7 @@ import {
368
368
  runPreflight,
369
369
  splitBlockerComment,
370
370
  trackerPathFor
371
- } from "./index-zn08ywsv.js";
371
+ } from "./index-v8d7c6sw.js";
372
372
  import {
373
373
  ACTION_HEADING,
374
374
  CAUSE_HEADING,
@@ -461,7 +461,7 @@ import {
461
461
  versionLine,
462
462
  writeFrontmatter,
463
463
  writePositionFile
464
- } from "./index-kz7e4gtt.js";
464
+ } from "./index-bvfbfa63.js";
465
465
  import {
466
466
  CONFIG_DEFAULTS,
467
467
  CONFIG_FILE,
@@ -476,7 +476,7 @@ import {
476
476
  readItemBytes,
477
477
  readRafaBlocks,
478
478
  resolveTiers
479
- } from "./index-dv0ycmp8.js";
479
+ } from "./index-vp0xghk2.js";
480
480
  import {
481
481
  promotable
482
482
  } from "./index-d6d0942q.js";
@@ -10584,6 +10584,9 @@ function bugLine(criterion, bug) {
10584
10584
  case "commented": {
10585
10585
  return { text: `${which}: recurs in ${bug.ref === null ? "an issue" : issueName2(bug.ref)} on the ${bug.channel} tracker; commented on it.`, warn: false };
10586
10586
  }
10587
+ case "inherited": {
10588
+ return { text: `${which}: not filed: a test failure the run started with.`, warn: false };
10589
+ }
10587
10590
  case "skipped": {
10588
10591
  return { text: `${which}: not filed: ${bug.problem ?? "no reason given"}.`, warn: true };
10589
10592
  }
@@ -10621,7 +10624,8 @@ async function fileFailures(run, epic, failures, commit) {
10621
10624
  tracker,
10622
10625
  privateTracker: run.seams.privateTracker,
10623
10626
  secrets,
10624
- seams: run.seams.findings
10627
+ seams: run.seams.findings,
10628
+ similarity: run.config
10625
10629
  });
10626
10630
  for (const bug of result2.bugs) {
10627
10631
  const line = bugLine(criterion, bug);
@@ -11682,6 +11686,10 @@ var CONFIG_SETTINGS_LINES = Object.freeze([
11682
11686
  "# claims:",
11683
11687
  `# staleAfter: ${CONFIG_DEFAULTS.claimsStaleAfter} # 36h | 3d | disabled; how long a rafa:claimed claim stands before a takeover`,
11684
11688
  `# ahead: ${CONFIG_DEFAULTS.claimsAhead} # off | allow; whether a claim may reach one issue ahead`,
11689
+ "# triage:",
11690
+ "# similarity:",
11691
+ `# threshold: ${String(CONFIG_DEFAULTS.triageSimilarityThreshold)} # above 0 and at most 1 | false; the least score at which triage comments on the nearest open bug`,
11692
+ `# candidates: ${String(CONFIG_DEFAULTS.triageSimilarityCandidates)} # 1..10, the nearest issues a new bug lists as Possible duplicates`,
11685
11693
  "# release:",
11686
11694
  `# enabled: ${String(CONFIG_DEFAULTS.releaseEnabled)} # true | false | auto, which is on when both files below exist`,
11687
11695
  `# versionFile: ${CONFIG_DEFAULTS.releaseVersionFile}`,
@@ -15209,8 +15217,16 @@ async function guardBeforeMerge(step) {
15209
15217
  var NO_WORKFLOW_WARNING = "nothing on GitHub has tested this branch; you are relying on the checks run locally";
15210
15218
  var WORKFLOWS_EXIST_WARNING = "CI may not have started (a path filter, a draft, Actions disabled, or it has not registered yet); this is probably not what you want";
15211
15219
  var UNCHECKED_MERGE_SENTENCE = "Merged with no checks reported, by rafa pr merge --skip-checks.";
15212
- function uncheckedCaseOf(workflowCount) {
15213
- return workflowCount === 0 ? "no-workflow" : "workflows-exist";
15220
+ function uncheckedCaseOf(workflowCount, baseWorkflows = null) {
15221
+ if (workflowCount === 0)
15222
+ return "no-workflow";
15223
+ if (workflowCount !== null && baseWorkflows !== null && !baseWorkflows.runsOnPullRequests) {
15224
+ return "no-pull-request-workflow";
15225
+ }
15226
+ return "workflows-exist";
15227
+ }
15228
+ function noPullRequestWorkflowLine(base) {
15229
+ return `no workflow runs on pull requests into ${base}`;
15214
15230
  }
15215
15231
  function workflowCountLine(workflowCount) {
15216
15232
  if (workflowCount === null)
@@ -15224,26 +15240,181 @@ function skipChecksCommand(number) {
15224
15240
  function uncheckedQuestion(number) {
15225
15241
  return `Merge #${number} with no checks? [y/N]`;
15226
15242
  }
15227
- function uncheckedComment(workflowCount) {
15228
- return `${UNCHECKED_MERGE_SENTENCE}
15243
+ function uncheckedComment(workflowCount, baseLine = null) {
15244
+ const countLine2 = workflowCountLine(workflowCount);
15245
+ const paragraphs = baseLine === null ? [UNCHECKED_MERGE_SENTENCE, countLine2] : [UNCHECKED_MERGE_SENTENCE, countLine2, baseLine];
15246
+ return paragraphs.join(`
15229
15247
 
15230
- ${workflowCountLine(workflowCount)}`;
15248
+ `);
15231
15249
  }
15232
- function readUnchecked(number, workflowCount) {
15233
- const which = uncheckedCaseOf(workflowCount);
15234
- const warning = which === "no-workflow" ? NO_WORKFLOW_WARNING : WORKFLOWS_EXIST_WARNING;
15250
+ function warningOf(which, baseWorkflows) {
15251
+ if (which === "no-workflow")
15252
+ return NO_WORKFLOW_WARNING;
15253
+ if (which === "no-pull-request-workflow" && baseWorkflows !== null)
15254
+ return noPullRequestWorkflowLine(baseWorkflows.base);
15255
+ return WORKFLOWS_EXIST_WARNING;
15256
+ }
15257
+ function readUnchecked(number, workflowCount, baseWorkflows = null) {
15258
+ const which = uncheckedCaseOf(workflowCount, baseWorkflows);
15259
+ const warning = warningOf(which, baseWorkflows);
15260
+ const isBaseCase = which === "no-pull-request-workflow";
15235
15261
  return Object.freeze({
15236
15262
  case: which,
15237
15263
  warning: Object.freeze([workflowCountLine(workflowCount), warning]),
15238
- yesMayAnswer: which === "no-workflow",
15264
+ yesMayAnswer: which !== "workflows-exist",
15239
15265
  question: uncheckedQuestion(number),
15240
- comment: uncheckedComment(workflowCount)
15266
+ comment: uncheckedComment(workflowCount, isBaseCase ? warning : null)
15241
15267
  });
15242
15268
  }
15243
15269
 
15270
+ // src/pr/workflow-triggers.ts
15271
+ var PULL_REQUEST_EVENT = "pull_request";
15272
+ var UNKNOWN = "unknown";
15273
+ var BRACKET_BODY = /^[A-Za-z0-9-]+$/;
15274
+ var REGEX_SPECIAL = /[.*+?^${}()|[\]\\/]/g;
15275
+ function workflowsRunOnPullRequestsInto(workflowTexts, base) {
15276
+ return workflowTexts.some((text) => workflowNamesBase(text, base));
15277
+ }
15278
+ function workflowNamesBase(workflowText, base) {
15279
+ let document;
15280
+ try {
15281
+ document = Bun.YAML.parse(workflowText);
15282
+ } catch {
15283
+ return true;
15284
+ }
15285
+ return readWorkflow(document, base) !== false;
15286
+ }
15287
+ function readWorkflow(document, base) {
15288
+ if (!isPlainObject(document) || !("on" in document))
15289
+ return UNKNOWN;
15290
+ const on = document.on;
15291
+ if (typeof on === "string")
15292
+ return on === PULL_REQUEST_EVENT;
15293
+ if (Array.isArray(on)) {
15294
+ if (!on.every((event) => typeof event === "string"))
15295
+ return UNKNOWN;
15296
+ return on.includes(PULL_REQUEST_EVENT);
15297
+ }
15298
+ if (!isPlainObject(on))
15299
+ return UNKNOWN;
15300
+ if (!(PULL_REQUEST_EVENT in on))
15301
+ return false;
15302
+ return readPullRequestTrigger(on[PULL_REQUEST_EVENT], base);
15303
+ }
15304
+ function readPullRequestTrigger(trigger, base) {
15305
+ if (trigger === null || trigger === undefined)
15306
+ return true;
15307
+ if (!isPlainObject(trigger))
15308
+ return UNKNOWN;
15309
+ const branches = trigger.branches;
15310
+ const ignored = trigger["branches-ignore"];
15311
+ if (branches !== undefined && ignored !== undefined)
15312
+ return UNKNOWN;
15313
+ if (branches !== undefined)
15314
+ return readBranches(branches, base);
15315
+ if (ignored !== undefined)
15316
+ return readBranchesIgnore(ignored, base);
15317
+ return true;
15318
+ }
15319
+ function readBranches(filter, base) {
15320
+ const patterns = patternListOf(filter);
15321
+ if (patterns === null)
15322
+ return UNKNOWN;
15323
+ let named2 = false;
15324
+ for (const pattern of patterns) {
15325
+ const negated = pattern.startsWith("!");
15326
+ const matches = branchPatternMatches(negated ? pattern.slice(1) : pattern, base);
15327
+ if (matches === null)
15328
+ return UNKNOWN;
15329
+ if (matches)
15330
+ named2 = !negated;
15331
+ }
15332
+ return named2;
15333
+ }
15334
+ function readBranchesIgnore(filter, base) {
15335
+ const patterns = patternListOf(filter);
15336
+ if (patterns === null || patterns.some((pattern) => pattern.startsWith("!")))
15337
+ return UNKNOWN;
15338
+ let anyMatch = false;
15339
+ for (const pattern of patterns) {
15340
+ const matches = branchPatternMatches(pattern, base);
15341
+ if (matches === null)
15342
+ return UNKNOWN;
15343
+ anyMatch = anyMatch || matches;
15344
+ }
15345
+ return !anyMatch;
15346
+ }
15347
+ function patternListOf(filter) {
15348
+ if (typeof filter === "string")
15349
+ return [filter];
15350
+ if (Array.isArray(filter) && filter.every((pattern) => typeof pattern === "string"))
15351
+ return filter;
15352
+ return null;
15353
+ }
15354
+ function branchPatternMatches(pattern, branch) {
15355
+ const source = regexSourceOf(pattern);
15356
+ if (source === null)
15357
+ return null;
15358
+ try {
15359
+ return new RegExp(`^${source}$`).test(branch);
15360
+ } catch {
15361
+ return null;
15362
+ }
15363
+ }
15364
+ function regexSourceOf(pattern) {
15365
+ let source = "";
15366
+ let quantifiable = false;
15367
+ let index = 0;
15368
+ while (index < pattern.length) {
15369
+ const char = pattern.charAt(index);
15370
+ if (char === "*") {
15371
+ const isDouble = pattern.charAt(index + 1) === "*";
15372
+ source += isDouble ? ".*" : "[^/]*";
15373
+ index += isDouble ? 2 : 1;
15374
+ quantifiable = false;
15375
+ } else if (char === "?" || char === "+") {
15376
+ if (!quantifiable)
15377
+ return null;
15378
+ source += char;
15379
+ index += 1;
15380
+ quantifiable = false;
15381
+ } else if (char === "[") {
15382
+ const close = pattern.indexOf("]", index + 1);
15383
+ if (close === -1)
15384
+ return null;
15385
+ const body = pattern.slice(index + 1, close);
15386
+ if (!BRACKET_BODY.test(body))
15387
+ return null;
15388
+ source += `[${body}]`;
15389
+ index = close + 1;
15390
+ quantifiable = true;
15391
+ } else if (char === "\\") {
15392
+ if (index + 1 >= pattern.length)
15393
+ return null;
15394
+ source += escapeRegex(pattern.charAt(index + 1));
15395
+ index += 2;
15396
+ quantifiable = true;
15397
+ } else {
15398
+ source += escapeRegex(char);
15399
+ index += 1;
15400
+ quantifiable = true;
15401
+ }
15402
+ }
15403
+ return source;
15404
+ }
15405
+ function escapeRegex(char) {
15406
+ return char.replace(REGEX_SPECIAL, "\\$&");
15407
+ }
15408
+ function isPlainObject(value) {
15409
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15410
+ }
15411
+
15244
15412
  // src/commands/pr/merge-unchecked.ts
15245
15413
  var INDENT4 = " ";
15246
15414
  var YES_ANSWERS3 = ["y", "yes"];
15415
+ var WORKFLOWS_DIR = ".github/workflows/";
15416
+ var WORKFLOW_ENDINGS = [".yml", ".yaml"];
15417
+ var LS_TREE_ENTRY = /^\d+ (\w+) ([0-9a-f]+)\t(.+)$/s;
15247
15418
  function refusal10(head, reading, summary, tail) {
15248
15419
  return new CommandExit(1, [
15249
15420
  `\u274C ${head}`,
@@ -15260,10 +15431,37 @@ async function workflowCountOf(pulls) {
15260
15431
  return null;
15261
15432
  }
15262
15433
  }
15434
+ function workflowBlobsOf(listing) {
15435
+ return listing.split("\x00").map((entry) => LS_TREE_ENTRY.exec(entry)).filter((match) => match !== null).filter(([, type, , path]) => type === "blob" && WORKFLOW_ENDINGS.some((ending) => path?.endsWith(ending) === true)).map(([, , oid]) => oid ?? "");
15436
+ }
15437
+ function readBaseWorkflows(git, base) {
15438
+ if (base === "" || base.startsWith("-"))
15439
+ return null;
15440
+ if (!git(["fetch", "origin", base]).ok)
15441
+ return null;
15442
+ const listed2 = git(["ls-tree", "--full-tree", "-z", `origin/${base}`, WORKFLOWS_DIR]);
15443
+ if (!listed2.ok)
15444
+ return null;
15445
+ const blobs = workflowBlobsOf(listed2.stdout);
15446
+ if (blobs.length === 0)
15447
+ return null;
15448
+ const shown = blobs.map((oid) => git(["cat-file", "blob", oid]));
15449
+ if (shown.some((result2) => !result2.ok))
15450
+ return null;
15451
+ const texts = shown.map((result2) => result2.stdout);
15452
+ return Object.freeze({ base, runsOnPullRequests: workflowsRunOnPullRequestsInto(texts, base) });
15453
+ }
15454
+ function baseWorkflowsOf(options, workflowCount) {
15455
+ const { git, base } = options;
15456
+ if (workflowCount === null || workflowCount === 0 || git === undefined || base === undefined)
15457
+ return null;
15458
+ return readBaseWorkflows(git, base);
15459
+ }
15263
15460
  async function readUncheckedMerge(options) {
15264
15461
  const { pulls, number, yes, summary } = options;
15265
15462
  const workflowCount = await workflowCountOf(pulls);
15266
- const reading = readUnchecked(number, workflowCount);
15463
+ const baseWorkflows = baseWorkflowsOf(options, workflowCount);
15464
+ const reading = readUnchecked(number, workflowCount, baseWorkflows);
15267
15465
  if (yes && !reading.yesMayAnswer) {
15268
15466
  throw refusal10(`rafa pr merge refuses --yes for #${number} with no checks: workflows exist, or their count could not be read.`, reading, summary, "That is more likely a fault than a choice, so a person has to answer. Run it again without --yes on a terminal.");
15269
15467
  }
@@ -15272,7 +15470,7 @@ async function readUncheckedMerge(options) {
15272
15470
  const tail = reading.yesMayAnswer ? "Merge it without the question with --yes." : "--yes is refused here, so run it on a terminal and answer it.";
15273
15471
  throw refusal10(`rafa pr merge asks before merging #${number} with no checks, and standard input is no terminal.`, reading, summary, tail);
15274
15472
  }
15275
- return Object.freeze({ workflowCount, reading, yes });
15473
+ return Object.freeze({ workflowCount, baseWorkflows, reading, yes });
15276
15474
  }
15277
15475
  function isYes(answer2) {
15278
15476
  return answer2 !== null && YES_ANSWERS3.includes(answer2.trim().toLowerCase());
@@ -15454,8 +15652,8 @@ async function reportAfterCleanUp(context, pr, seams, detail) {
15454
15652
  return { unblocked: null, freed };
15455
15653
  }
15456
15654
  async function askToMerge(context, pr, seams, ask) {
15457
- const { number, summary, yes, skipChecks } = ask;
15458
- const unchecked = skipChecks ? await readUncheckedMerge({ pulls: pr.pulls, number, yes, summary, isTerminal: seams.isTerminal }) : null;
15655
+ const { number, summary, yes, skipChecks, base, git } = ask;
15656
+ const unchecked = skipChecks ? await readUncheckedMerge({ pulls: pr.pulls, number, yes, summary, isTerminal: seams.isTerminal, base, git }) : null;
15459
15657
  if (unchecked === null && !yes)
15460
15658
  requireTerminal(seams, summary);
15461
15659
  context.output.info(summary);
@@ -15520,7 +15718,7 @@ async function runMerge2(context, seams) {
15520
15718
  openPrompter: prompterOf(seams)
15521
15719
  });
15522
15720
  const summary = summaryLine2(detail, method);
15523
- const answer2 = guard.go ? await askToMerge(context, pr, seams, { number: detail.number, summary, yes, skipChecks }) : { go: false, unchecked: null };
15721
+ const answer2 = guard.go ? await askToMerge(context, pr, seams, { number: detail.number, summary, yes, skipChecks, base: detail.baseRefName, git }) : { go: false, unchecked: null };
15524
15722
  const answered3 = {
15525
15723
  number: detail.number,
15526
15724
  source: pick.source,
@@ -15584,7 +15782,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15584
15782
  subject: "pr",
15585
15783
  action: "merge",
15586
15784
  summary: "merge a pull request and clean up both branches",
15587
- description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. Where the release runs, the release" + " guard reads the branch before the question: a `collision` is refused unless" + " `dangerous.acceptVersionCollision` is true, and `pr.versionCollision` sets whether a `missing` or" + " `stale` branch merges silently, with a warning, after its own question, or not at all. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " board.relationships set to native it ticks no epic checklist and, in place of that reading, prints the" + " open issues the merge freed, whose blockers GitHub clears by itself, asking nothing and writing nothing. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading, or the freed issues in native mode, are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15785
+ description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow, or" + " where no workflow file on the base has a `pull_request` trigger naming it)," + " and after the merge posts one comment on the pull request saying so. Where the release runs, the release" + " guard reads the branch before the question: a `collision` is refused unless" + " `dangerous.acceptVersionCollision` is true, and `pr.versionCollision` sets whether a `missing` or" + " `stale` branch merges silently, with a warning, after its own question, or not at all. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " board.relationships set to native it ticks no epic checklist and, in place of that reading, prints the" + " open issues the merge freed, whose blockers GitHub clears by itself, asking nothing and writing nothing. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading, or the freed issues in native mode, are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15588
15786
  args: [
15589
15787
  {
15590
15788
  name: "n",
@@ -15600,7 +15798,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15600
15798
  },
15601
15799
  {
15602
15800
  name: "skip-checks",
15603
- description: "Merge a pull request that reports no checks at all, after a warning and its own question." + " Refused where the pull request reports any check; `--yes` beside it is refused where the repository" + " defines a workflow or its workflow count could not be read.",
15801
+ description: "Merge a pull request that reports no checks at all, after a warning and its own question." + " Refused where the pull request reports any check; `--yes` beside it is refused where the repository" + " defines a workflow that may run on pull requests into the base, or its workflow count could not be read.",
15604
15802
  type: "boolean"
15605
15803
  },
15606
15804
  {