mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(feedback-sync): count only stable studio tags and merged closing PRs
A fixed entry is upgraded from next to a concrete version only when a merged closing pull request is contained in a studio tag whose release page is neither a draft nor a prerelease. Prerelease status is read from the releases API, not from a version cutoff.
This commit is contained in:
1 parent
178dbb7934
commit
daa13a418a
2 files changed
+61
-16
No files matched your search
@@ -110,11 +110,12 @@ export function closingPulls(number, timeline) {
|
||||
return [...out.values()];
|
||||
}
|
||||
|
||||
// The close event's own commit wins; otherwise the earliest merged closing PR.
|
||||
// Only a merged closing PR proves a fix shipped. The close event's own commit
|
||||
// wins among them; a close by a bare commit, or by hand, yields no commit at all.
|
||||
export function closingCommit(number, timeline, pulls) {
|
||||
const event = [...timeline].reverse().find((e) => e.event === "closed");
|
||||
if (event?.commit_id) return event.commit_id;
|
||||
const merged = closingPulls(number, timeline).map((s) => pulls[s.number]).filter((pr) => pr?.merged_at && pr.merge_commit_sha);
|
||||
const event = [...timeline].reverse().find((e) => e.event === "closed");
|
||||
if (event?.commit_id && merged.some((pr) => pr.merge_commit_sha === event.commit_id)) return event.commit_id;
|
||||
merged.sort((a, b) => Date.parse(a.merged_at) - Date.parse(b.merged_at));
|
||||
return merged[0]?.merge_commit_sha ?? null;
|
||||
}
|
||||
@@ -123,13 +124,19 @@ export function hasOpenLinkedPull(number, timeline) {
|
||||
return closingPulls(number, timeline).some((s) => s.state === "open" && !s.pull_request.merged_at);
|
||||
}
|
||||
|
||||
const STUDIO_TAG = /^studio-v\d+\.\d+\.\d+$/;
|
||||
const isStable = (r) => r && !r.draft && !r.prerelease && STUDIO_TAG.test(r.tag_name);
|
||||
|
||||
// A tag counts as released only when its GitHub release is neither a draft nor a prerelease.
|
||||
export const stableStudioTags = (releases) => new Set(releases.filter(isStable).map((r) => r.tag_name));
|
||||
|
||||
export function newestStudioRelease(releases) {
|
||||
const times = releases.filter((r) => !r.draft && /^studio-v/.test(r.tag_name) && r.published_at).map((r) => Date.parse(r.published_at));
|
||||
const times = releases.filter((r) => isStable(r) && r.published_at).map((r) => Date.parse(r.published_at));
|
||||
return times.length ? Math.max(...times) : null;
|
||||
}
|
||||
|
||||
export function firstTag(tags) {
|
||||
const list = tags.filter((t) => /^studio-v\d+\.\d+\.\d+$/.test(t));
|
||||
export function firstTag(tags, stable) {
|
||||
const list = stable instanceof Set ? tags.filter((t) => STUDIO_TAG.test(t) && stable.has(t)) : [];
|
||||
const key = (t) => t.slice(8).split(".").map(Number);
|
||||
list.sort((a, b) => {
|
||||
const x = key(a), y = key(b);
|
||||
@@ -188,7 +195,8 @@ async function syncOne(row, issue, deps, dry) {
|
||||
const pulls = {};
|
||||
for (const src of closingPulls(number, timeline)) pulls[src.number] = await deps.gh.getPull(src.number);
|
||||
const sha = closingCommit(number, timeline, pulls);
|
||||
version = sha ? firstTag(await deps.tagsContaining(sha)) : null;
|
||||
if (!sha) deps.log(`::notice::#${number} (${row.receipt}) is closed without a merged closing PR; it stays unresolved`);
|
||||
version = sha ? firstTag(await deps.tagsContaining(sha), await deps.stableTags()) : null;
|
||||
}
|
||||
const update = decideStatus({ current: row.status, resolvedVersion: row.resolvedVersion, issue, version, inProgress: hasOpenLinkedPull(number, timeline) });
|
||||
if (!update) return;
|
||||
@@ -293,6 +301,7 @@ const paginate = (path) => {
|
||||
|
||||
export function liveDeps(env) {
|
||||
const base = (env.FEEDBACK_BASE_URL || DEFAULT_BASE).replace(/\/$/, "");
|
||||
const cache = {};
|
||||
const call = async (method, path, body) => {
|
||||
let res;
|
||||
try {
|
||||
@@ -348,6 +357,10 @@ export function liveDeps(env) {
|
||||
const releases = gh(["api", `repos/${REPO}/releases?per_page=10`]) ?? [];
|
||||
return newestStudioRelease(releases);
|
||||
},
|
||||
async stableTags() {
|
||||
cache.stable ??= stableStudioTags(paginate(`repos/${REPO}/releases?per_page=100`));
|
||||
return cache.stable;
|
||||
},
|
||||
async tagsContaining(sha) {
|
||||
if (!/^[0-9a-f]{40}$/.test(sha)) return [];
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
ATTACHMENT_PREFIX, FatalError, MAX_REPLIES, publishable, renderReply, replyMarker, REPO, closingCommit, code, fence, hasMarker, MAX_OPEN, decideStatus, firstTag, hasOpenLinkedPull, isOurs, newestStudioRelease, liveDeps, labelsFor, marker, renderBody, renderTitle, run,
|
||||
ATTACHMENT_PREFIX, FatalError, MAX_REPLIES, publishable, renderReply, replyMarker, REPO, closingCommit, code, fence, hasMarker, MAX_OPEN, decideStatus, firstTag, hasOpenLinkedPull, isOurs, newestStudioRelease, stableStudioTags, liveDeps, labelsFor, marker, renderBody, renderTitle, run,
|
||||
} from "./feedback-sync.mjs";
|
||||
|
||||
const R = "FB-7K3M-9QX2";
|
||||
@@ -86,8 +86,12 @@ const xref = (number, o = {}) => ({
|
||||
source: { issue: { number, pull_request: o.pr ?? {}, state: o.state ?? "closed", body: o.body ?? "Fixes #7", repository: { full_name: o.repo ?? "esengine/DeepSeek-Reasonix" } } },
|
||||
});
|
||||
|
||||
test("closing commit: close event commit wins, else earliest merged same-repo PR that names the issue", () => {
|
||||
assert.equal(closingCommit(7, [{ event: "closed", commit_id: "c1" }], {}), "c1");
|
||||
test("closing commit: needs a merged same-repo PR that names the issue; the close event commit wins among them", () => {
|
||||
assert.equal(closingCommit(7, [{ event: "closed", commit_id: "c1" }], {}), null);
|
||||
const viaPull = [xref(30), { event: "closed", commit_id: "c2" }];
|
||||
assert.equal(closingCommit(7, viaPull, { 30: { merged_at: "2026-09-01T00:00:00Z", merge_commit_sha: "c2" } }), "c2");
|
||||
assert.equal(closingCommit(7, [xref(30), xref(31), { event: "closed", commit_id: "late" }], { 30: { merged_at: "2026-09-01T00:00:00Z", merge_commit_sha: "early" }, 31: { merged_at: "2026-09-02T00:00:00Z", merge_commit_sha: "late" } }), "late");
|
||||
assert.equal(closingCommit(7, [xref(30, { state: "open" })], { 30: { merged_at: null, merge_commit_sha: "unmerged" } }), null);
|
||||
const tl = [xref(20), xref(21, { body: "Closes #7" }), xref(22, { body: "see #7" }), xref(23, { repo: "someone/fork" }), { event: "closed", commit_id: null }];
|
||||
const pulls = {
|
||||
20: { merged_at: "2026-09-02T00:00:00Z", merge_commit_sha: "late" },
|
||||
@@ -99,9 +103,18 @@ test("closing commit: close event commit wins, else earliest merged same-repo PR
|
||||
assert.equal(closingCommit(8, tl, pulls), null);
|
||||
});
|
||||
|
||||
test("first studio tag is the oldest by version, ignoring other namespaces", () => {
|
||||
assert.equal(firstTag(["studio-v2.10.0", "studio-v2.9.1", "v1.2.3", "studio-v2.9.0-rc1"]), "v2.9.1");
|
||||
assert.equal(firstTag([]), null);
|
||||
test("first studio tag is the oldest stable one by version, ignoring other namespaces and prereleases", () => {
|
||||
const stable = new Set(["studio-v2.10.0", "studio-v2.9.1", "studio-v2.9.0-rc1"]);
|
||||
assert.equal(firstTag(["studio-v2.10.0", "studio-v2.9.1", "v1.2.3", "studio-v2.9.0-rc1"], stable), "v2.9.1");
|
||||
assert.equal(firstTag(["studio-v2.9.0", "studio-v2.10.0"], new Set(["studio-v2.10.0"])), "v2.10.0");
|
||||
assert.equal(firstTag(["studio-v2.9.0"], new Set()), null);
|
||||
assert.equal(firstTag(["studio-v2.9.0"]), null);
|
||||
assert.equal(firstTag([], stable), null);
|
||||
});
|
||||
|
||||
test("stable tags come from release pages, never from a version cutoff", () => {
|
||||
const rel = (tag_name, prerelease = false, draft = false) => ({ tag_name, prerelease, draft });
|
||||
assert.deepEqual([...stableStudioTags([rel("studio-v2.28.0", true), rel("studio-v2.29.0"), rel("studio-v2.30.0", false, true), rel("v1.40.0"), rel("studio-v2.31.0-rc1")])], ["studio-v2.29.0"]);
|
||||
});
|
||||
|
||||
test("status decisions", () => {
|
||||
@@ -138,7 +151,7 @@ test("only issues the bot filed with the source label are ours", () => {
|
||||
assert.equal(isOurs({ ...ok, labels: [] }), false);
|
||||
});
|
||||
|
||||
function fakes({ replies = [], ackFails = false, repliesFails = null, issueOf = (n) => ({ ...BOT, number: n, comments: 1 }), comments = [], commentFails = false, recent = [], recordedFails = false, pendingFails = null, open = [], pending = [item()], issues = {}, changed = [], timeline = [], pulls = {}, tags = [], releaseAt = null } = {}) {
|
||||
function fakes({ replies = [], ackFails = false, repliesFails = null, issueOf = (n) => ({ ...BOT, number: n, comments: 1 }), comments = [], commentFails = false, recent = [], recordedFails = false, pendingFails = null, open = [], pending = [item()], issues = {}, changed = [], timeline = [], pulls = {}, tags = [], stable = null, releaseAt = null } = {}) {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
@@ -163,6 +176,7 @@ function fakes({ replies = [], ackFails = false, repliesFails = null, issueOf =
|
||||
getPull: async (n) => pulls[n],
|
||||
},
|
||||
tagsContaining: async () => tags,
|
||||
stableTags: async () => new Set(stable ?? tags),
|
||||
newestReleaseAt: async () => { calls.push(["releaseAt"]); return releaseAt; },
|
||||
};
|
||||
}
|
||||
@@ -250,6 +264,24 @@ test("closed as completed resolves PR -> commit -> tag; unlisted 'next' entries
|
||||
const stillNext = fakes({ ...base, open: [{ receipt: R, issueNumber: 7, status: "fixed", resolvedVersion: "next" }], issues: { 7: closedIssue }, tags: [], releaseAt: NOW - 600000 });
|
||||
await run(stillNext, { now: NOW });
|
||||
assert.ok(!stillNext.calls.some((c) => c[0] === "status"));
|
||||
const prereleaseOnly = fakes({ ...base, open: [{ receipt: R, issueNumber: 7, status: "fixed", resolvedVersion: "next" }], issues: { 7: closedIssue }, tags: ["studio-v2.11.0", "studio-v2.10.0"], stable: ["studio-v2.11.0"], releaseAt: NOW - 600000 });
|
||||
await run(prereleaseOnly, { now: NOW });
|
||||
assert.deepEqual(prereleaseOnly.calls.find((c) => c[0] === "status"), ["status", R, { status: "fixed", resolvedVersion: "v2.11.0" }]);
|
||||
});
|
||||
|
||||
test("a closed issue without a merged closing PR is never given a version, and is reported", async () => {
|
||||
const closedIssue = { number: 7, state: "closed", state_reason: "completed", labels: [] };
|
||||
const handClosed = fakes({ pending: [], timeline: [{ event: "closed", commit_id: "b".repeat(40) }], open: [{ receipt: R, issueNumber: 7, status: "fixed", resolvedVersion: "next" }], issues: { 7: closedIssue }, tags: ["studio-v2.11.0"], releaseAt: Date.now() });
|
||||
await run(handClosed, { full: true });
|
||||
assert.ok(!handClosed.calls.some((c) => c[0] === "status"));
|
||||
assert.ok(handClosed.calls.some((c) => c[0] === "log" && /without a merged closing PR/.test(c[1])));
|
||||
});
|
||||
|
||||
test("an open row that omits resolvedVersion is never upgraded", async () => {
|
||||
const closedIssue = { number: 7, state: "closed", state_reason: "completed", labels: [] };
|
||||
const d = fakes({ pending: [], timeline: [xref(20)], pulls: { 20: { merged_at: "2026-09-01T00:00:00Z", merge_commit_sha: "a".repeat(40) } }, open: [{ receipt: R, issueNumber: 7, status: "fixed" }], issues: { 7: closedIssue }, tags: ["studio-v2.11.0"] });
|
||||
await run(d, { full: true });
|
||||
assert.ok(!d.calls.some((c) => c[0] === "status"));
|
||||
});
|
||||
|
||||
test("--full re-reads every open row", async () => {
|
||||
@@ -306,8 +338,8 @@ test("an unreachable worker is a warning, not a failed job", async () => {
|
||||
});
|
||||
|
||||
test("the release signal ignores drafts and other release lines", () => {
|
||||
const rel = (tag_name, published_at, draft = false) => ({ tag_name, published_at, draft });
|
||||
assert.equal(newestStudioRelease([rel("v1.40.0", "2026-09-30T00:00:00Z"), rel("studio-v2.9.0", "2026-09-28T00:00:00Z"), rel("studio-v2.10.0", "2026-09-29T00:00:00Z", true)]), Date.parse("2026-09-28T00:00:00Z"));
|
||||
const rel = (tag_name, published_at, draft = false, prerelease = false) => ({ tag_name, published_at, draft, prerelease });
|
||||
assert.equal(newestStudioRelease([rel("v1.40.0", "2026-09-30T00:00:00Z"), rel("studio-v2.9.0", "2026-09-28T00:00:00Z"), rel("studio-v2.10.0", "2026-09-29T00:00:00Z", true), rel("studio-v2.11.0", "2026-09-30T00:00:00Z", false, true)]), Date.parse("2026-09-28T00:00:00Z"));
|
||||
assert.equal(newestStudioRelease([]), null);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user