From 6d0ed4eee28918e2858bd4cf03713da76fa7447d Mon Sep 17 00:00:00 2001 From: Goon Date: Thu, 28 May 2026 18:57:23 +0700 Subject: [PATCH 1/5] test(integration): serialize abort provider leak checks --- tests/integration/abort_provider_stream_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/abort_provider_stream_test.go b/tests/integration/abort_provider_stream_test.go index 1827c8c0..6d42273f 100644 --- a/tests/integration/abort_provider_stream_test.go +++ b/tests/integration/abort_provider_stream_test.go @@ -16,8 +16,6 @@ import ( // while a Chat call is in flight properly closes the HTTP connection. // Uses a stalling SSE server that only responds if data flows from it. func TestAnthropicChat_AbortContextCancel(t *testing.T) { - t.Parallel() - // Start a stalling SSE server server := newStallingSSEServer(t) defer server.Close() @@ -68,8 +66,6 @@ func TestAnthropicChat_AbortContextCancel(t *testing.T) { // TestOpenAIChat_AbortContextCancel mirrors the Anthropic test for OpenAI-compatible providers. func TestOpenAIChat_AbortContextCancel(t *testing.T) { - t.Parallel() - server := newStallingSSEServer(t) defer server.Close() From 812adeec6a092d685030ed625d5aeea23c9e0920 Mon Sep 17 00:00:00 2001 From: Goon Date: Thu, 28 May 2026 19:44:56 +0700 Subject: [PATCH 2/5] fix(ci): bump beta above latest stable release --- scripts/ci/semantic-beta-version.mjs | 59 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/scripts/ci/semantic-beta-version.mjs b/scripts/ci/semantic-beta-version.mjs index f4a01609..30d3cee1 100644 --- a/scripts/ci/semantic-beta-version.mjs +++ b/scripts/ci/semantic-beta-version.mjs @@ -79,23 +79,41 @@ function writeNoRelease(reason) { console.log(reason); } -const tags = git(["tag", "--merged", "HEAD", "--list", "v[0-9]*"]) - .split(/\r?\n/) - .filter(Boolean) - .map(parseVersion) - .filter(Boolean) - .map((tag) => ({ ...tag, commit: tagCommit(tag.raw) })) - .sort(compareVersion) - .reverse(); +function releaseTags(args = []) { + return git(["tag", ...args, "--list", "v[0-9]*"]) + .split(/\r?\n/) + .filter(Boolean) + .map(parseVersion) + .filter(Boolean) + .map((tag) => ({ ...tag, commit: tagCommit(tag.raw) })) + .sort(compareVersion) + .reverse(); +} -const latest = tags[0]; -const latestStable = tags.find((tag) => !tag.preid); -const latestPrerelease = tags.find((tag) => tag.preid === prerelease); +const versionTags = releaseTags(); +const mergedTags = releaseTags(["--merged", "HEAD"]); +const latest = versionTags[0]; +const latestStable = versionTags.find((tag) => !tag.preid); +function isUsablePrerelease(tag) { + return tag.preid !== prerelease || !latestStable || compareBase(tag, latestStable) > 0; +} + +const usableVersionTags = versionTags.filter(isUsablePrerelease); +const usableMergedTags = mergedTags.filter(isUsablePrerelease); +const latestPrerelease = usableVersionTags.find((tag) => tag.preid === prerelease); +const latestMerged = usableMergedTags[0]; +const latestMergedStable = usableMergedTags.find((tag) => !tag.preid); +const latestMergedPrerelease = usableMergedTags.find((tag) => tag.preid === prerelease); const head = git(["rev-parse", "HEAD"]); -const previousTag = tags.find((tag) => tag.raw !== latest?.raw); -const repairTag = latest?.preid === prerelease && latest.commit === head ? latest : null; -const range = latest ? [`${latest.raw}..HEAD`] : []; -const logRange = repairTag && previousTag ? [`${previousTag.raw}..HEAD`] : range; +const zero = { major: 0, minor: 0, patch: 0 }; +const initial = parseVersion(initialVersion) || { major: 0, minor: 1, patch: 0 }; +const previousMergedTag = usableMergedTags.find((tag) => tag.raw !== latestMerged?.raw); +const repairTag = latestMergedPrerelease?.commit === head + && (!latestStable || compareBase(latestMergedPrerelease, latestStable) > 0) + ? latestMergedPrerelease + : null; +const range = latestMerged ? [`${latestMerged.raw}..HEAD`] : []; +const logRange = repairTag && previousMergedTag ? [`${previousMergedTag.raw}..HEAD`] : range; const log = git(["log", "--format=%B%x1e", ...logRange]); const messages = log.split("\x1e").map((message) => message.trim()).filter(Boolean); @@ -126,6 +144,8 @@ if (repairTag) { process.exit(0); } +const releaseBase = maxBase(latestStable || initial || zero, latestMergedStable || initial || zero); + let level = ""; for (const message of messages) { const next = commitLevel(message); @@ -138,14 +158,11 @@ for (const message of messages) { } if (!level) { - writeNoRelease("No release-worthy conventional commits found since the last release tag."); + writeNoRelease("No release-worthy conventional commits found since the last merged release tag."); process.exit(0); } -const zero = { major: 0, minor: 0, patch: 0 }; -const initial = parseVersion(initialVersion) || { major: 0, minor: 1, patch: 0 }; -const stableBase = latestStable || initial || zero; -const bumpedBase = bump(stableBase, level); +const bumpedBase = bump(releaseBase, level); const prereleaseBase = latestPrerelease || zero; let targetBase = maxBase(bumpedBase, prereleaseBase); if (!latest && compareBase(targetBase, initial) < 0) targetBase = initial; @@ -156,7 +173,7 @@ const nextNumber = compareBase(targetBase, prereleaseBase) === 0 const version = `${versionText(targetBase)}-${prerelease}.${nextNumber}`; const tag = `v${version}`; -if (tags.some((existing) => existing.raw === tag)) { +if (versionTags.some((existing) => existing.raw === tag)) { writeNoRelease(`Computed tag ${tag} already exists.`); process.exit(0); } From a7b58c693377edf2bf478f7ff091465f16bc5ce4 Mon Sep 17 00:00:00 2001 From: Goon Date: Fri, 29 May 2026 09:35:14 +0700 Subject: [PATCH 3/5] fix(ci): push beta repair tags to origin --- .github/workflows/dev-beta-release.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dev-beta-release.yaml b/.github/workflows/dev-beta-release.yaml index b72e7d17..26862fa3 100644 --- a/.github/workflows/dev-beta-release.yaml +++ b/.github/workflows/dev-beta-release.yaml @@ -105,6 +105,8 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" if ! git rev-parse "$TAG" >/dev/null 2>&1; then git tag -a "$TAG" -m "Release $TAG" + fi + if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then git push origin "$TAG" fi - name: Upload release notes From ea107aebc95cab12af348ee6f89c33d8b2a58477 Mon Sep 17 00:00:00 2001 From: Goon Date: Fri, 29 May 2026 10:10:07 +0700 Subject: [PATCH 4/5] test(ci): guard semantic beta versioning --- .github/workflows/ci.yaml | 10 +++ .github/workflows/dev-beta-release.yaml | 2 + scripts/ci/semantic-beta-version.test.mjs | 99 +++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 scripts/ci/semantic-beta-version.test.mjs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6f0d394..4be46789 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,16 @@ permissions: contents: read jobs: + release-versioning: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Release versioning tests + run: node --test scripts/ci/semantic-beta-version.test.mjs + go: runs-on: ubuntu-latest # Postgres service container for integration tests. pgvector/pgvector:pg18 diff --git a/.github/workflows/dev-beta-release.yaml b/.github/workflows/dev-beta-release.yaml index 26862fa3..8bdfdef6 100644 --- a/.github/workflows/dev-beta-release.yaml +++ b/.github/workflows/dev-beta-release.yaml @@ -91,6 +91,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Release versioning tests + run: node --test scripts/ci/semantic-beta-version.test.mjs - name: Fetch upstream release tags run: git fetch --force --tags https://github.com/nextlevelbuilder/goclaw.git "refs/tags/v*:refs/tags/v*" - name: Compute semantic beta version diff --git a/scripts/ci/semantic-beta-version.test.mjs b/scripts/ci/semantic-beta-version.test.mjs new file mode 100644 index 00000000..90006ead --- /dev/null +++ b/scripts/ci/semantic-beta-version.test.mjs @@ -0,0 +1,99 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, "../.."); +const scriptPath = join(repoRoot, "scripts/ci/semantic-beta-version.mjs"); + +function run(cwd, args, env = {}) { + return execFileSync(args[0], args.slice(1), { + cwd, + encoding: "utf8", + env: { ...process.env, ...env }, + stdio: ["ignore", "pipe", "pipe"], + }).trim(); +} + +function git(cwd, ...args) { + return run(cwd, ["git", ...args]); +} + +function writeAndCommit(cwd, name, message) { + writeFileSync(join(cwd, name), `${message}\n`); + git(cwd, "add", name); + git(cwd, "commit", "-m", message); +} + +function tag(cwd, name) { + git(cwd, "tag", "-a", name, "-m", `Release ${name}`); +} + +function createRepo() { + const cwd = mkdtempSync(join(tmpdir(), "goclaw-beta-version-")); + mkdirSync(join(cwd, "scripts/ci"), { recursive: true }); + copyFileSync(scriptPath, join(cwd, "scripts/ci/semantic-beta-version.mjs")); + git(cwd, "init", "-b", "main"); + git(cwd, "config", "user.name", "Version Test"); + git(cwd, "config", "user.email", "version-test@example.com"); + + writeAndCommit(cwd, "base.txt", "fix(base): initial stable"); + tag(cwd, "v3.11.3"); + + git(cwd, "checkout", "-b", "dev"); + writeAndCommit(cwd, "old-beta.txt", "fix(runtime): old beta fix"); + tag(cwd, "v3.12.0-beta.41"); + + git(cwd, "checkout", "main"); + writeAndCommit(cwd, "stable.txt", "feat(release): stable milestone"); + tag(cwd, "v3.12.0"); + + git(cwd, "checkout", "dev"); + writeAndCommit(cwd, "current.txt", "feat(api): new work after stable"); + return cwd; +} + +function runVersionScript(cwd) { + return run(cwd, ["node", "scripts/ci/semantic-beta-version.mjs"], { + INITIAL_VERSION: "3.11.3", + PRERELEASE_ID: "beta", + }); +} + +test("bumps beta above latest stable even when stable tag is not merged into dev", () => { + const cwd = createRepo(); + try { + const output = runVersionScript(cwd); + assert.match(output, /version=3\.13\.0-beta\.1/); + assert.match(output, /tag=v3\.13\.0-beta\.1/); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); + +test("continues prerelease numbering when upstream already has the next beta base", () => { + const cwd = createRepo(); + try { + git(cwd, "checkout", "-b", "upstream-dev"); + writeAndCommit(cwd, "upstream.txt", "fix(vault): upstream beta work"); + tag(cwd, "v3.13.0-beta.2"); + git(cwd, "checkout", "dev"); + + const output = runVersionScript(cwd); + assert.match(output, /version=3\.13\.0-beta\.3/); + assert.match(output, /tag=v3\.13\.0-beta\.3/); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); + +test("dev beta workflow pushes missing origin tags even if a fetched tag exists locally", () => { + const workflow = readFileSync(join(repoRoot, ".github/workflows/dev-beta-release.yaml"), "utf8"); + assert.match(workflow, /git rev-parse "\$TAG"/); + assert.match(workflow, /git ls-remote --exit-code --tags origin "refs\/tags\/\$TAG"/); + assert.match(workflow, /git push origin "\$TAG"/); +}); From 5017f7ca5d1524624e2c2687d87a4dd2dec755e0 Mon Sep 17 00:00:00 2001 From: Goon Date: Fri, 29 May 2026 10:11:30 +0700 Subject: [PATCH 5/5] test(ci): isolate beta version output env --- scripts/ci/semantic-beta-version.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci/semantic-beta-version.test.mjs b/scripts/ci/semantic-beta-version.test.mjs index 90006ead..473323f2 100644 --- a/scripts/ci/semantic-beta-version.test.mjs +++ b/scripts/ci/semantic-beta-version.test.mjs @@ -59,6 +59,7 @@ function createRepo() { function runVersionScript(cwd) { return run(cwd, ["node", "scripts/ci/semantic-beta-version.mjs"], { + GITHUB_OUTPUT: "", INITIAL_VERSION: "3.11.3", PRERELEASE_ID: "beta", });