@kontextmind/kxm 0.7.123 → 0.7.125

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.
@@ -28,6 +28,7 @@ const REBASE_ROUNDS = 5;
28
28
  const POLL_MS = positiveInt(process.env.KXM_LAND_POLL_MS, 30_000);
29
29
  const MERGE_WAIT_MS = 20 * 60 * 1000;
30
30
  const RELEASE_WAIT_MS = 20 * 60 * 1000;
31
+ const RELEASE_PROGRESS_MS = 2 * 60 * 1000;
31
32
  const PUBLISH_WAIT_MS = 10 * 60 * 1000;
32
33
  const DOCS_COMMIT = "docs(roadmap): regenerate after verify";
33
34
  const GENERATOR = "plans/kxm-roadmap/update-dashboard.mjs";
@@ -40,6 +41,7 @@ const UNRELEASED_HEADING = "## Unreleased";
40
41
  const root = process.cwd();
41
42
  let dryRun = false;
42
43
  let bodyFile;
44
+ let requestedTitle;
43
45
  let prNumber;
44
46
  let forceLease = false;
45
47
  let phasesSnapshotted = false;
@@ -198,13 +200,14 @@ function parseArgs(argv) {
198
200
  options.help = true;
199
201
  continue;
200
202
  }
201
- if (arg === "--pr" || arg === "--body-file" || arg === "--stage") {
203
+ if (arg === "--pr" || arg === "--body-file" || arg === "--stage" || arg === "--title") {
202
204
  const value = argv[index + 1];
203
205
  if (value === undefined || value.startsWith("--")) return { error: `${arg} requires a value` };
204
206
  index += 1;
205
207
  if (arg === "--pr") options.pr = value;
206
208
  if (arg === "--body-file") options.bodyFile = value;
207
209
  if (arg === "--stage") options.stage = value;
210
+ if (arg === "--title") options.title = value;
208
211
  continue;
209
212
  }
210
213
  return { error: `unknown argument ${arg}` };
@@ -324,12 +327,24 @@ function pushStage() {
324
327
  return pass("push", { detail: forceLease ? "pushed with lease" : "pushed" });
325
328
  }
326
329
 
330
+ function pullRequestTitle() {
331
+ const explicit = String(requestedTitle ?? "").trim();
332
+ if (explicit) return { ok: true, text: explicit };
333
+ const logged = run("git", ["log", "--reverse", "--format=%s", "origin/main..HEAD"]);
334
+ if (logged.status !== 0) return { ok: false, detail: "no commit subject on origin/main..HEAD" };
335
+ const subject = logged.stdout.split("\n").map((line) => line.trim()).find(Boolean) ?? "";
336
+ if (!subject) return { ok: false, detail: "no commit subject on origin/main..HEAD" };
337
+ return { ok: true, text: subject };
338
+ }
339
+
327
340
  function prStage() {
341
+ const titled = pullRequestTitle();
328
342
  if (dryRun) {
343
+ if (!titled.ok) return refuse("pr", "land_pr_title_missing", titled.detail);
329
344
  return plan("pr", [
330
345
  "gh pr list --head <branch> --state open --json number,title",
331
- "gh pr create --body-file <path>",
332
- ]);
346
+ `gh pr create --title ${titled.text} --body-file <path>`,
347
+ ], titled.text);
333
348
  }
334
349
  const found = ensurePr();
335
350
  if (!found.ok) return refuse("pr", "land_pr_body_missing", found.detail);
@@ -337,12 +352,13 @@ function prStage() {
337
352
  prNumber = String(found.pr.number);
338
353
  return pass("pr", { detail: `reused #${prNumber}` });
339
354
  }
355
+ if (!titled.ok) return refuse("pr", "land_pr_title_missing", titled.detail);
340
356
  if (!bodyFile) return refuse("pr", "land_pr_body_missing", "creating a pull request requires --body-file");
341
357
  if (!existsSync(bodyFile.startsWith("/") ? bodyFile : join(root, bodyFile))) {
342
358
  return refuse("pr", "land_pr_body_missing", `body file not found: ${bodyFile}`);
343
359
  }
344
360
  const branch = found.branch || branchName();
345
- const created = gh(["pr", "create", "--head", branch, "--title", branch, "--body-file", bodyFile]);
361
+ const created = gh(["pr", "create", "--head", branch, "--title", titled.text, "--body-file", bodyFile]);
346
362
  if (created.status !== 0) return refuse("pr", "land_pr_body_missing", created.stderr || created.stdout);
347
363
  const again = ensurePr();
348
364
  if (!again.ok || !again.pr?.number) return refuse("pr", "land_pr_body_missing", "pull request was not created");
@@ -782,27 +798,54 @@ function mergeStage() {
782
798
  return pass("merge", { detail: `squashed #${prNumber}` });
783
799
  }
784
800
 
785
- function selectRun(runs, title, after) {
801
+ function selectRun(runs, title, after, earliest = false) {
786
802
  if (!Array.isArray(runs)) return undefined;
787
803
  const matches = runs.filter((runRow) => {
788
804
  if (!runRow || typeof runRow !== "object") return false;
789
805
  const display = String(runRow.displayTitle ?? "");
790
- if (title && !display.includes(title)) return false;
806
+ if (!earliest && title && !display.includes(title)) return false;
791
807
  const created = Date.parse(String(runRow.createdAt ?? ""));
792
- if (Number.isFinite(after) && Number.isFinite(created) && created < after) return false;
808
+ if (earliest) {
809
+ if (!Number.isFinite(after) || !Number.isFinite(created) || created <= after) return false;
810
+ } else if (Number.isFinite(after) && Number.isFinite(created) && created < after) {
811
+ return false;
812
+ }
793
813
  return true;
794
814
  });
795
- matches.sort((left, right) => Date.parse(String(right.createdAt ?? "")) - Date.parse(String(left.createdAt ?? "")));
815
+ matches.sort((left, right) => {
816
+ const delta = Date.parse(String(left.createdAt ?? "")) - Date.parse(String(right.createdAt ?? ""));
817
+ return earliest ? delta : -delta;
818
+ });
796
819
  return matches[0];
797
820
  }
798
821
 
799
- function waitForWorkflow(workflow, title, after) {
822
+ function workflowRunId(run) {
823
+ if (!run || typeof run !== "object" || run.databaseId === undefined || run.databaseId === null) return "";
824
+ return String(run.databaseId);
825
+ }
826
+
827
+ function noteReleaseWait(workflow, started, logged) {
828
+ const minutes = Math.floor((Date.now() - started) / RELEASE_PROGRESS_MS) * 2;
829
+ if (minutes < 2 || minutes <= logged.minutes) return;
830
+ logged.minutes = minutes;
831
+ emit({ stage: "release", ok: true, detail: `waiting ${workflow} ${minutes}m` });
832
+ }
833
+
834
+ function waitForWorkflow(workflow, title, after, earliest = false) {
835
+ const started = Date.now();
836
+ const logged = { minutes: 0 };
800
837
  let failure = "";
801
838
  const polled = poll(RELEASE_WAIT_MS, () => {
802
- const listed = ghJson(["run", "list", `--workflow=${workflow}`, "--json", "status,conclusion,displayTitle,createdAt"]);
803
- if (!listed.ok) return { done: false, detail: listed.result.stderr || listed.result.stdout };
804
- const match = selectRun(listed.value, title, after);
805
- if (!match) return { done: false };
839
+ const listed = ghJson(["run", "list", `--workflow=${workflow}`, "--json", "databaseId,status,conclusion,displayTitle,createdAt"]);
840
+ if (!listed.ok) {
841
+ noteReleaseWait(workflow, started, logged);
842
+ return { done: false, detail: listed.result.stderr || listed.result.stdout };
843
+ }
844
+ const match = selectRun(listed.value, title, after, earliest);
845
+ if (!match) {
846
+ noteReleaseWait(workflow, started, logged);
847
+ return { done: false };
848
+ }
806
849
  const conclusion = String(match.conclusion ?? "").toLowerCase();
807
850
  const status = String(match.status ?? "").toLowerCase();
808
851
  if (conclusion === "success") return { done: true, run: match };
@@ -810,6 +853,7 @@ function waitForWorkflow(workflow, title, after) {
810
853
  failure = `${workflow} concluded ${conclusion}`;
811
854
  return { done: true, failed: true };
812
855
  }
856
+ noteReleaseWait(workflow, started, logged);
813
857
  return { done: false };
814
858
  });
815
859
  if (polled.failed) return { ok: false, detail: failure };
@@ -820,11 +864,11 @@ function waitForWorkflow(workflow, title, after) {
820
864
  function releaseStage() {
821
865
  if (dryRun) {
822
866
  return plan("release", [
823
- "gh run list --workflow=auto-release.yml --json status,conclusion,displayTitle,createdAt",
867
+ "gh run list --workflow=auto-release.yml --json databaseId,status,conclusion,displayTitle,createdAt",
824
868
  "git ls-remote --tags origin",
825
- "gh run list --workflow=release.yml --json status,conclusion,displayTitle,createdAt",
869
+ "gh run list --workflow=release.yml --json databaseId,status,conclusion,displayTitle,createdAt",
826
870
  "npm view @kontextmind/kxm@<version> version",
827
- ], "npm poll is 10 minutes");
871
+ ], "npm poll is 10 minutes; Release is matched by time after Auto-Release");
828
872
  }
829
873
  const context = readReleaseContext();
830
874
  const found = ensurePr();
@@ -838,8 +882,12 @@ function releaseStage() {
838
882
  if (!tags.tag || (previous && versionCompare(tags.tag, previous) <= 0)) {
839
883
  return refuse("release", "land_release_failed", `tag ${tags.tag || "(none)"} is not newer than ${previous || "(none)"}`);
840
884
  }
841
- const release = waitForWorkflow("release.yml", title, after);
885
+ const autoCreated = Date.parse(String(auto.run?.createdAt ?? ""));
886
+ const release = waitForWorkflow("release.yml", "", autoCreated, true);
842
887
  if (!release.ok) return refuse("release", "land_release_failed", release.detail);
888
+ const autoReleaseRunId = workflowRunId(auto.run);
889
+ const releaseRunId = workflowRunId(release.run);
890
+ writeReleaseContext({ autoReleaseRunId, releaseRunId });
843
891
  const version = tags.tag.replace(/^v/, "");
844
892
  const published = poll(PUBLISH_WAIT_MS, () => {
845
893
  const viewed = run("npm", ["view", `@kontextmind/kxm@${version}`, "version"]);
@@ -847,7 +895,7 @@ function releaseStage() {
847
895
  return { done: false };
848
896
  });
849
897
  if (!published.version) return refuse("release", "land_publish_timeout", `npm view @kontextmind/kxm@${version} version`);
850
- return pass("release", { detail: `PUBLISHED ${published.version}` });
898
+ return pass("release", { detail: `PUBLISHED ${published.version} auto-release ${autoReleaseRunId} release ${releaseRunId}` });
851
899
  }
852
900
 
853
901
  function taskStatuses(tasks) {
@@ -950,12 +998,13 @@ function main() {
950
998
  emit({
951
999
  stage: "usage",
952
1000
  ok: true,
953
- detail: "node scripts/pr-land.mjs [--pr <n>] [--body-file <path>] [--stage <name>] [--json] [--dry-run]",
1001
+ detail: "node scripts/pr-land.mjs [--pr <n>] [--title <text>] [--body-file <path>] [--stage <name>] [--json] [--dry-run]",
954
1002
  });
955
1003
  return 0;
956
1004
  }
957
1005
  dryRun = parsed.options.dryRun;
958
1006
  bodyFile = parsed.options.bodyFile;
1007
+ requestedTitle = parsed.options.title;
959
1008
  prNumber = parsed.options.pr;
960
1009
  const names = parsed.options.stage ? [parsed.options.stage] : STAGES;
961
1010
  for (const name of names) {