forgedock 1.48.1 → 1.50.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.
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
- import { parseDependsOn } from "../bin/engine/phases.mjs";
12
+ import { parseDependsOn, isTrustedComment } from "../bin/engine/phases.mjs";
13
+ import { parseState } from "../bin/engine/state.mjs";
13
14
 
14
15
  /** Milestones in train order. Input: [{ title, number, due_on }]. */
15
16
  export function orderMilestones(milestones) {
@@ -60,7 +61,9 @@ export async function readLive({ gh, repo }) {
60
61
  * Parallel-start gate: dependencies of `issues` (not themselves in the batch) that are not merged into `baseOf(issue)`.
61
62
  * A dep is merged when it is CLOSED by a PR that is merged with that base, or whose merge commit is an ancestor of
62
63
  * that base (`isAncestor(sha, base)`, e.g. it arrived through a milestone sync). With no linked PR (GitHub links only
63
- * default-branch merges, #5086) the merged `feat/issue-<dep>-lean` PR is looked up instead. Unreadable issues are skipped (fail open:
64
+ * default-branch merges, #5086) the PR is looked up instead, in order: the trusted FORGE:STATE `mergedTo` record whose PR reads merged
65
+ * with the recorded commit (#5120), the merged `feat/issue-<dep>-lean` PR, then merged PRs on the base whose title/body closes the
66
+ * dependency (#5120). Each lookup that fails finds nothing. Unreadable issues are skipped (fail open:
64
67
  * the engine's own openDependency still guards each lane); an unreadable dep counts as unmet.
65
68
  * `sha`/`kind` (#5031): `kind` is "commit" when the dependency merged but its commit is not on the base yet (`sha`), else "other".
66
69
  * @returns {Promise<Array<{ issue: number, dep: number, base: string, why: string, sha: string|null, kind: "commit"|"other" }>>}
@@ -95,12 +98,26 @@ export async function unmetDependencies({ issues, baseOf, gh, repo, isAncestor }
95
98
  await check(parse(await gh(["pr", "view", String(pr.number), ...r, "--json", "baseRefName,mergedAt,mergeCommit"])));
96
99
  if (merged) break;
97
100
  }
98
- // #5086: GitHub links closing PRs only for default-branch merges, so a milestone-lane dependency has no references.
99
- // Fall back to the lane PR by head-branch convention; a failed or non-array lookup finds nothing (still refused).
101
+ // #5086/#5120: GitHub links closing PRs only for default-branch merges, so a milestone-lane dependency has no references.
102
+ // Fall back to the engine's record, the lane PR by head-branch convention, then PR bodies; a failed or non-array lookup finds nothing.
103
+ const list = async (args) => { try { const j = parse(await gh(["pr", "list", ...r, ...args])); return Array.isArray(j) ? j : []; } catch { return []; } };
100
104
  if (!merged && !missing) {
101
- let prs = [];
102
- try { prs = parse(await gh(["pr", "list", ...r, "--head", `feat/issue-${dep}-lean`, "--state", "merged", "--json", "number,baseRefName,mergedAt,mergeCommit", "--limit", "5"])); } catch { prs = []; }
103
- for (const p of Array.isArray(prs) ? prs : []) { await check(p); if (merged) break; }
105
+ try {
106
+ const rec = parse(await gh(["api", `repos/${repo ?? "{owner}/{repo}"}/issues/${dep}`]));
107
+ const m = isTrustedComment(rec) && typeof rec.body === "string" ? parseState(rec.body)?.mergedTo : null;
108
+ if (m && m.lane === base && Number.isInteger(m.pr) && typeof m.sha === "string" && /^[0-9a-f]{7,64}$/i.test(m.sha)) {
109
+ const p = parse(await gh(["pr", "view", String(m.pr), ...r, "--json", "baseRefName,mergedAt,mergeCommit"]));
110
+ if (p?.mergeCommit?.oid === m.sha) await check(p);
111
+ }
112
+ } catch { /* no usable record */ }
113
+ }
114
+ if (!merged && !missing) {
115
+ for (const p of await list(["--head", `feat/issue-${dep}-lean`, "--state", "merged", "--json", "number,baseRefName,mergedAt,mergeCommit", "--limit", "5"])) { await check(p); if (merged) break; }
116
+ }
117
+ if (!merged && !missing) {
118
+ const closes = new RegExp(`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\b:?\\s+(?:[\\w.-]+/[\\w.-]+)?#${dep}(?!\\d)`, "i");
119
+ const prs = await list(["--state", "merged", "--base", base, "--search", `${dep} in:body`, "--json", "number,baseRefName,mergedAt,mergeCommit,title,body", "--limit", "20"]);
120
+ for (const p of prs.filter((x) => closes.test(`${x?.title ?? ""}\n${x?.body ?? ""}`))) { await check(p); if (merged) break; }
104
121
  }
105
122
  if (!merged) sha = missing;
106
123
  if (!merged) why = missing ? `merge commit ${missing.slice(0, 10)} is not on ${base}` : `closed but not merged into ${base}`;