diff --git a/.github/scripts/draft-local-plugin-release-notes.mjs b/.github/scripts/draft-local-plugin-release-notes.mjs index 94046b024..3ea25c6fe 100644 --- a/.github/scripts/draft-local-plugin-release-notes.mjs +++ b/.github/scripts/draft-local-plugin-release-notes.mjs @@ -37,7 +37,7 @@ const CURRENT_TAG_PREFIX = "memos-local-plugin-v"; const TAG_PREFIXES = [CURRENT_TAG_PREFIX, "openclaw-local-plugin-v"]; const RELEASE_NOTES_MARKER = "doc-agent-release-notes-json"; const RELEASE_CATEGORY_ORDER = ["Added", "Improved", "Fixed"]; -const MAX_DRAFT_REPAIR_ATTEMPTS = 2; +const MAX_DRAFT_REPAIR_ATTEMPTS = 3; const RELEASE_TO_DOC_CATEGORY = { Added: "New Features", Improved: "Improvements", @@ -72,6 +72,12 @@ export function displayVersion(raw) { return value ? `v${value}` : ""; } +export function isLegacyPackageOnlyRelease({ targetVersion, npmDistTag = "" } = {}) { + const parsed = parseSemver(targetVersion); + const distTag = String(npmDistTag || "").trim(); + return Boolean(parsed?.prerelease) || Boolean(distTag && distTag !== "latest"); +} + export function versionFromTag(tag) { for (const prefix of TAG_PREFIXES) { if (tag.startsWith(prefix)) { @@ -83,7 +89,7 @@ export function versionFromTag(tag) { export function parseSemver(version) { const cleaned = cleanVersion(version); - const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+]([0-9A-Za-z.-]+))?$/); + const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/); if (!match) return null; return { major: Number(match[1]), @@ -173,10 +179,16 @@ function listProductTags() { .filter((item) => item.version && parseSemver(item.version)); } -export function findPreviousTag(targetVersion, currentTag) { - const candidates = listProductTags() +export function selectPreviousTag( + tags, + targetVersion, + currentTag, + { includePrerelease = true } = {}, +) { + const candidates = tags .filter((item) => item.tag !== currentTag) .filter((item) => compareSemver(item.version, targetVersion) < 0) + .filter((item) => includePrerelease || !parseSemver(item.version)?.prerelease) .sort((a, b) => { const versionOrder = compareSemver(b.version, a.version); if (versionOrder !== 0) return versionOrder; @@ -187,6 +199,10 @@ export function findPreviousTag(targetVersion, currentTag) { return candidates[0]?.tag || ""; } +export function findPreviousTag(targetVersion, currentTag, options = {}) { + return selectPreviousTag(listProductTags(), targetVersion, currentTag, options); +} + function parseCommits(previousTag, currentRef) { const text = sh([ "log", @@ -428,6 +444,17 @@ export function ensureSourceHint(notes) { return notes.includes("doc-agent: source-id=") ? notes : `${notes.trim()}\n\n${hint}\n`; } +export function validateLegacyPackageNotes(notes) { + const text = String(notes || "").trim(); + if (!/^## Changelog\s*$/m.test(text)) { + fail("Legacy package release notes must contain a '## Changelog' heading."); + } + if (/doc-agent:\s*source-id=|doc-agent-release-notes-json/.test(text)) { + fail("Legacy package release notes must not include Doc Agent source hints or docs payloads."); + } + return `${text}\n`; +} + function normalizeReleaseCategory(value) { const text = String(value || "").trim(); return RELEASE_CATEGORY_ORDER.includes(text) ? text : ""; @@ -1007,6 +1034,54 @@ function markdownFromReleaseItems(items, coverage) { return `${lines.join("\n").trim()}\n`; } +export function legacyPackageDraftFromEvidence(evidence, { npmDistTag = "" } = {}) { + const version = evidence?.target_version || ""; + const targetPackageVersion = cleanVersion(version); + const gitRef = evidence?.git_ref || ""; + const previousTag = evidence?.previous_tag || ""; + const currentTag = evidence?.current_tag || ""; + const distTag = String(npmDistTag || "").trim() || "beta"; + const changedFileCount = Array.isArray(evidence?.changed_files) ? evidence.changed_files.length : 0; + const commitCount = Array.isArray(evidence?.commits) ? evidence.commits.length : 0; + const packageChanges = Array.isArray(evidence?.package_changes) ? evidence.package_changes : []; + const versionChange = packageChanges.find((item) => item.field === "version"); + const lines = [ + "## Changelog", + "", + "### Prerelease", + `- Published ${PRODUCT_TITLE.en} ${version} as a package prerelease for validation through the npm \`${distTag}\` dist-tag.`, + "", + "### Release Evidence", + `- Package tag: ${currentTag}`, + `- Previous package tag: ${previousTag}`, + `- Source commit: ${gitRef}`, + `- Local plugin commits: ${commitCount}`, + `- Local plugin changed files: ${changedFileCount}`, + ]; + const previousPackageVersion = versionChange?.before || "unknown"; + lines.push(`- Package version: ${previousPackageVersion} -> ${targetPackageVersion}`); + lines.push(""); + lines.push("This legacy prerelease is package-only and does not update the MemOS-Docs Plugin tab."); + return { + ok: true, + needs_review: false, + confidence: "legacy-package-only", + release_items: [], + coverage: { + needs_review: false, + required_count: 0, + covered_required_count: 0, + missing_required_count: 0, + covered_refs: [], + missing_required: [], + invalid_item_refs: [], + policy: "legacy local-plugin prereleases are package-only and do not create docs payloads", + }, + warnings: ["legacy package-only prerelease skipped Doc Agent draft and docs payload generation"], + release_notes_markdown: `${lines.join("\n").trim()}\n`, + }; +} + export function postprocessDraftFromEvidence(draft, evidence) { const inputItems = Array.isArray(draft?.release_items) ? draft.release_items.map(normalizeReleaseItem).filter(Boolean) @@ -1290,6 +1365,8 @@ export async function main() { if (!targetVersion) fail("RELEASE_VERSION is required."); const currentTag = process.env.RELEASE_TAG || `${CURRENT_TAG_PREFIX}${targetVersion}`; + const npmDistTag = String(process.env.NPM_DIST_TAG || "").trim(); + const legacyPackageOnly = isLegacyPackageOnlyRelease({ targetVersion, npmDistTag }); const notesPath = process.env.RELEASE_NOTES_FILE || join(tmpdir(), `memos-local-plugin-${targetVersion}-release-notes.md`); @@ -1297,14 +1374,19 @@ export async function main() { const manualNotes = String(process.env.MANUAL_RELEASE_NOTES || "").trim(); if (manualNotes) { - writeFileSync(notesPath, ensureSourceHint(validateManualNotes(manualNotes)), "utf8"); + const notes = legacyPackageOnly + ? validateLegacyPackageNotes(manualNotes) + : ensureSourceHint(validateManualNotes(manualNotes)); + writeFileSync(notesPath, notes, "utf8"); appendOutput("release_notes_file", notesPath); appendOutput("draft_used", "false"); console.log(`Using manually provided release notes: ${notesPath}`); return; } - const previousTag = findPreviousTag(targetVersion, currentTag); + const previousTag = findPreviousTag(targetVersion, currentTag, { + includePrerelease: legacyPackageOnly, + }); if (!previousTag) { fail(`Cannot find a previous local plugin tag before ${currentTag}.`); } @@ -1314,6 +1396,31 @@ export async function main() { const evidencePath = join(tmpdir(), `memos-local-plugin-${targetVersion}-evidence.json`); writeFileSync(evidencePath, JSON.stringify(evidenceForInspection(evidence), null, 2), "utf8"); + if (legacyPackageOnly) { + const draft = legacyPackageDraftFromEvidence(evidence, { npmDistTag }); + const draftPath = join(tmpdir(), `memos-local-plugin-${targetVersion}-release-notes-draft.json`); + writeFileSync(draftPath, JSON.stringify(draftForInspection(draft), null, 2), "utf8"); + writeFileSync(notesPath, draft.release_notes_markdown, "utf8"); + + appendOutput("release_notes_file", notesPath); + appendOutput("evidence_file", evidencePath); + appendOutput("draft_file", draftPath); + appendOutput("draft_used", "false"); + appendOutput("previous_tag", previousTag); + appendOutput("current_tag", currentTag); + appendOutput("current_ref", currentRef); + appendOutput("draft_confidence", draft.confidence); + appendOutput("missing_required_count", "0"); + appendOutput("validation_attempt_count", "0"); + appendOutput("repair_attempt_count", "0"); + + console.log(`Generated package-only prerelease notes without Doc Agent: ${notesPath}`); + console.log(`Previous tag: ${previousTag}`); + console.log(`Current tag: ${currentTag}`); + console.log(`Current evidence ref: ${currentRef}`); + return; + } + const draft = await requestValidatedDraft(evidence); if (!draft.ok || draft.needs_review) { fail(`Postprocessed release notes require review: ${JSON.stringify(draft.validation_report || draft.coverage || {})}`); diff --git a/.github/scripts/draft-local-plugin-release-notes.test.mjs b/.github/scripts/draft-local-plugin-release-notes.test.mjs index 1fb28b456..40abe1174 100644 --- a/.github/scripts/draft-local-plugin-release-notes.test.mjs +++ b/.github/scripts/draft-local-plugin-release-notes.test.mjs @@ -10,12 +10,17 @@ import { draftForInspection, evidenceForInspection, ensureSourceHint, + isLegacyPackageOnlyRelease, + legacyPackageDraftFromEvidence, + parseSemver, RELEASE_NOTE_GUIDANCE, postprocessDraftFromEvidence, reportExternalFailureFromEnv, requestDraft, requestValidatedDraft, resolveCurrentRef, + selectPreviousTag, + validateLegacyPackageNotes, validateManualNotes, versionFromTag, } from "./draft-local-plugin-release-notes.mjs"; @@ -46,6 +51,70 @@ test("normalizes only real local-plugin tag families", () => { assert.equal(versionFromTag("v2.0.10"), ""); }); +test("detects legacy package-only prereleases", () => { + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13-beta.1", npmDistTag: "beta" }), true); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13-beta.1", npmDistTag: "latest" }), true); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13", npmDistTag: "beta" }), true); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13", npmDistTag: "next" }), true); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13", npmDistTag: "latest" }), false); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13", npmDistTag: "" }), false); +}); + +test("treats SemVer build metadata as stable metadata, not a prerelease channel", () => { + assert.equal(parseSemver("2.0.13+build.7").prerelease, ""); + assert.equal(parseSemver("2.0.13-beta.1+build.7").prerelease, "beta.1"); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13+build.7", npmDistTag: "latest" }), false); + assert.equal(isLegacyPackageOnlyRelease({ targetVersion: "2.0.13-beta.1+build.7", npmDistTag: "latest" }), true); +}); + +test("uses the previous stable tag for docs-generating latest releases", () => { + const tags = [ + { tag: "memos-local-plugin-v2.0.12", version: "2.0.12" }, + { tag: "memos-local-plugin-v2.0.13-beta.1", version: "2.0.13-beta.1" }, + { tag: "memos-local-plugin-v2.0.13", version: "2.0.13" }, + ]; + + assert.equal( + selectPreviousTag(tags, "2.0.13", "memos-local-plugin-v2.0.13", { includePrerelease: false }), + "memos-local-plugin-v2.0.12", + ); + assert.equal( + selectPreviousTag(tags, "2.0.13", "memos-local-plugin-v2.0.13", { includePrerelease: true }), + "memos-local-plugin-v2.0.13-beta.1", + ); +}); + +test("generates package-only prerelease notes without docs payloads", () => { + const draft = legacyPackageDraftFromEvidence( + { + current_tag: "memos-local-plugin-v2.0.13-beta.1", + previous_tag: "memos-local-plugin-v2.0.12", + target_version: "v2.0.13-beta.1", + git_ref: "abc1234", + changed_files: [{ status: "M", path: "apps/memos-local-plugin/package.json" }], + commits: [ + { + sha: "abc1234000000000000000000000000000000000", + short_sha: "abc1234", + subject: "test: prepare local plugin beta package", + }, + ], + package_changes: [{ field: "version", before: "2.0.12", after: "2.0.13-beta.1" }], + }, + { npmDistTag: "beta" }, + ); + + assert.equal(draft.ok, true); + assert.equal(draft.needs_review, false); + assert.equal(draft.confidence, "legacy-package-only"); + assert.equal(draft.coverage.missing_required_count, 0); + assert.match(draft.release_notes_markdown, /## Changelog/); + assert.match(draft.release_notes_markdown, /npm `beta` dist-tag/); + assert.match(draft.release_notes_markdown, /package-only/); + assert.doesNotMatch(draft.release_notes_markdown, /doc-agent: source-id=/); + assert.doesNotMatch(draft.release_notes_markdown, /doc-agent-release-notes-json/); +}); + test("uses an existing release tag as the evidence endpoint", () => { const exists = (ref) => ref === "memos-local-plugin-v2.0.10" || ref === "manual-ref"; assert.equal( @@ -458,6 +527,69 @@ test("repairs postprocessed language validation issues with exact context", asyn assert.match(result.release_notes_markdown, /doc-agent-release-notes-json/); }); +test("standalone latest draft validation allows one initial response plus three repairs by default", async () => { + const repairEvidence = { + commits: [ + { + sha: "abc12340000000000000000000000000000000", + short_sha: "abc1234", + subject: "feat: add plugin health dashboard (#3001)", + }, + ], + release_note_guidance: { + source_ref_category_hints: [ + { + category: "Added", + source_refs: ["abc1234", "#3001"], + subject: "feat: add plugin health dashboard (#3001)", + }, + ], + }, + }; + const badDraft = { + ok: true, + needs_review: false, + release_items: [ + { + category: "Added", + text_cn: "Plugin health dashboard", + text_en: "插件健康看板", + source_refs: ["abc1234", "#3001"], + }, + ], + coverage: { required_count: 1, covered_required_count: 1, missing_required_count: 0 }, + warnings: [], + }; + const goodDraft = { + ok: true, + needs_review: false, + release_items: [ + { + category: "Added", + text_cn: "**插件健康看板**:新增本地插件健康状态展示。", + text_en: "**Plugin Health Dashboard**: Added local plugin health status visibility.", + source_refs: ["abc1234", "#3001"], + }, + ], + coverage: { required_count: 1, covered_required_count: 1, missing_required_count: 0 }, + warnings: [], + }; + let calls = 0; + const requestImpl = async () => { + calls += 1; + return calls < 4 ? badDraft : goodDraft; + }; + + const result = await requestValidatedDraft(repairEvidence, { requestImpl }); + + assert.equal(calls, 4); + assert.equal(result.ok, true); + assert.equal(result.needs_review, false); + assert.equal(result.validation_attempt_count, 4); + assert.equal(result.repair_attempt_count, 3); + assert.match(result.release_notes_markdown, /插件健康看板/); +}); + test("stops release-note repair after two validation repair attempts", async () => { const repairEvidence = { commits: [ @@ -560,6 +692,23 @@ test("manual notes require bilingual evidence refs and passed coverage", () => { ); }); +test("legacy package manual notes reject docs payloads", () => { + const valid = `## Changelog + +### Prerelease +- Published MemOS Local Plugin v2.0.13-beta.1 as a beta package.`; + assert.equal(validateLegacyPackageNotes(valid), `${valid}\n`); + assert.throws( + () => validateLegacyPackageNotes(`${valid}\n\n`), + /must not include Doc Agent source hints/, + ); + assert.throws( + () => validateLegacyPackageNotes(`${valid}\n\n`), + /must not include Doc Agent source hints/, + ); + assert.throws(() => validateLegacyPackageNotes("### Prerelease\n- missing changelog"), /Changelog/); +}); + test("retries transient draft failures and passes prior error context", async () => { const previous = { ...process.env }; try { diff --git a/.github/scripts/prepare-memos-release.mjs b/.github/scripts/prepare-memos-release.mjs index 9674eab97..270c69535 100644 --- a/.github/scripts/prepare-memos-release.mjs +++ b/.github/scripts/prepare-memos-release.mjs @@ -327,7 +327,11 @@ function isImportantCommit(commit, { revertedKeys = new Set() } = {}) { function isMaintenanceOnlyCommit(commit) { const subject = String(commit?.subject || ""); if (/^release:\s*merge\b/i.test(subject)) return false; - if (/^release:\s+(?:@memtensor\/memos-local-plugin|memos-local-plugin\b|openclaw local plugin\b)/i.test(subject)) { + if ( + /^release\s*[/:]\s*(?:@memtensor\/memos-local-plugin|memos[-/\s]+local[-/\s]+plugin\b|openclaw\s+local\s+plugin\b)/i.test( + subject, + ) + ) { return true; } if (/^(?:chore|build|ci)(\([^)]+\))?:\s*(?:release|bump|update)\b.*(?:@memtensor\/memos-local-plugin|memos-local-plugin)/i.test(subject)) { @@ -493,6 +497,7 @@ export function validateLocalPluginVersionPlan(evidence, expectedVersionInput = evidence.local_plugin_package_version_raw || evidence.local_plugin_version_raw, "local plugin package.json version", ); + const currentPackageIsPrerelease = (parseSemver(currentPackageVersion)?.prerelease || []).length > 0; const packageOrder = compareSemver(currentPackageVersion, previousPackageVersion); const packageVsReleasedOrder = compareSemver(currentPackageVersion, previousReleasedVersion); if (packageOrder < 0) { @@ -516,7 +521,11 @@ export function validateLocalPluginVersionPlan(evidence, expectedVersionInput = : ""; resolvedVersion = currentPackageVersion; versionSource = `${PRODUCT_PATH}/package.json`; - if (packageVsReleasedOrder <= 0) { + if (currentPackageIsPrerelease) { + resolvedVersion = incrementPatchVersion(previousReleasedVersion); + versionSource = "auto_patch_from_previous_released_version_prerelease_package_ignored"; + autoIncremented = true; + } else if (packageVsReleasedOrder <= 0) { resolvedVersion = incrementPatchVersion(previousReleasedVersion); versionSource = "auto_patch_from_previous_released_version"; autoIncremented = true; @@ -863,17 +872,34 @@ const FALLBACK_TOPIC_RULES = [ text_cn: "**模型配置与提供商兼容性**:优化 LLM、embedding 与 provider 配置处理,提升不同模型服务的接入稳定性。", text_en: "**Model configuration and provider compatibility**: Improved LLM, embedding, and provider configuration handling.", }, + { + pattern: /recall|retrieval|host input|inject|rank|dedupe|keyword|relevance/i, + category: "Improved", + text_cn: "**召回相关性与宿主输入处理**:优化本地检索、去重、排序和宿主输入注入流程,提升上下文召回质量。", + text_en: "**Recall relevance and host input handling**: Improved local retrieval, dedupe, ranking, and host-input injection for better context recall.", + }, ]; +function fallbackSubjectText(text) { + return String(text || "") + .replace(/^revert\s+"([^"]+)".*$/i, "$1") + .replace(/^(feat|fix|perf|refactor|chore|docs|test|ci|build|style|revert)(\([^)]+\))?!?:\s*/i, "") + .replace(/\s+by\s+@\S+.*$/i, "") + .replace(/\s+\(#\d+\)\s*$/g, "") + .replace(/\s+#\d+\s*$/g, "") + .trim(); +} + export function fallbackTopicForText(text, { allowGeneric = false } = {}) { const source = String(text || ""); const rule = FALLBACK_TOPIC_RULES.find((item) => item.pattern.test(source)); if (rule) return rule; if (!allowGeneric) return null; + const cleaned = fallbackSubjectText(source); return { category: /^feat/i.test(source) ? "Added" : /^fix|^revert/i.test(source) ? "Fixed" : "Improved", - text_cn: `**${PRODUCT_TITLE.zh}更新**:${source.replace(CJK_GLOBAL_RE, "").trim() || "本地插件能力完成更新。"}`, - text_en: `**${PRODUCT_TITLE.en} update**: ${source.replace(CJK_GLOBAL_RE, "").trim() || "Release evidence updated."}`, + text_cn: `**${PRODUCT_TITLE.zh}更新**:${cleaned.replace(CJK_GLOBAL_RE, "").trim() || "本地插件能力完成更新。"}`, + text_en: `**${PRODUCT_TITLE.en} update**: ${cleaned.replace(CJK_GLOBAL_RE, "").trim() || "Release evidence updated."}`, }; } diff --git a/.github/scripts/prepare-memos-release.test.mjs b/.github/scripts/prepare-memos-release.test.mjs index 98931e769..f0f734a7c 100644 --- a/.github/scripts/prepare-memos-release.test.mjs +++ b/.github/scripts/prepare-memos-release.test.mjs @@ -217,6 +217,41 @@ test("resolves the local plugin docs version from package or auto patch incremen }); assert.throws(() => validateLocalPluginVersionPlan(evidence, "2.0.12"), /does not match/); + assert.deepEqual( + validateLocalPluginVersionPlan( + { + ...evidence, + local_plugin_previous_version: "v2.0.11", + local_plugin_previous_version_raw: "2.0.11", + local_plugin_version: "v2.0.12-beta.1", + local_plugin_version_raw: "2.0.12-beta.1", + local_plugin_version_changed: true, + local_plugin_package_previous_version: "v2.0.11", + local_plugin_package_previous_version_raw: "2.0.11", + local_plugin_package_version: "v2.0.12-beta.1", + local_plugin_package_version_raw: "2.0.12-beta.1", + local_plugin_package_version_changed: true, + }, + "2.0.12", + ), + { + ok: true, + expected_version: "v2.0.12", + previous_version: "v2.0.11", + version: "v2.0.12", + version_changed: true, + version_required: true, + version_source: "auto_patch_from_previous_released_version_prerelease_package_ignored", + auto_incremented: true, + input_ignored: false, + input_ignored_reason: "", + input_raw: "2.0.12", + package_previous_version: "v2.0.11", + package_version: "v2.0.12-beta.1", + package_version_changed: true, + }, + ); + assert.deepEqual( validateLocalPluginVersionPlan({ ...evidence, @@ -435,7 +470,8 @@ test("legacy standalone local-plugin publisher requires an extra non-dry-run con assert.match(workflow, /guard-legacy-publish:/); assert.match(workflow, /guard-legacy-publish:\n\s+runs-on: ubuntu-latest\n\s+timeout-minutes: 5/); assert.match(workflow, /expected="LEGACY PUBLISH memos-local-plugin-v\$\{RELEASE_VERSION\}"/); - assert.match(workflow, /current official path is MemOS Release — Publish/); + assert.match(workflow, /standalone local-plugin npm publisher for beta or latest package releases/); + assert.match(workflow, /MemOS Release — Publish remains the weekly whole-repo release path/); assert.match(workflow, /needs: guard-legacy-publish/); }); @@ -598,6 +634,8 @@ test("filters standalone local-plugin release metadata from docs evidence", () = ); writeRepoFile("apps/memos-local-plugin/package-lock.json", "{\"lockfileVersion\": 3}\n"); commitAll("release: @memtensor/memos-local-plugin v9.9.1 (#12)"); + writeRepoFile("apps/memos-local-plugin/package-lock.json", "{\"lockfileVersion\": 3, \"packages\": {}}\n"); + commitAll("Release/memos local plugin v9.9.1 beta.1 (#13)"); const result = collectLocalPluginEvidence({ previousTag: "v9.9.0", @@ -765,6 +803,27 @@ test("fallback topic rewrites V7 session default fixes into user-facing docs cop assert.doesNotMatch(topic.text_cn, /fix\(plugin\)/); }); +test("fallback topic rewrites recall and host input work without raw commit prefixes", () => { + const topic = fallbackTopicForText("fix(plugin): improve recall relevance and host input handling (#2196)", { + allowGeneric: true, + }); + assert.equal(topic.category, "Improved"); + assert.match(topic.text_cn, /召回相关性与宿主输入处理/); + assert.match(topic.text_en, /Recall relevance and host input handling/); + assert.doesNotMatch(topic.text_cn, /fix\(plugin\)/); + assert.doesNotMatch(topic.text_en, /fix\(plugin\)/); +}); + +test("generic fallback strips Conventional Commit prefixes before validation", () => { + const topic = fallbackTopicForText("fix(plugin): stabilize sandbox widget (#9999)", { + allowGeneric: true, + }); + assert.equal(topic.category, "Fixed"); + assert.match(topic.text_cn, /stabilize sandbox widget/); + assert.doesNotMatch(topic.text_cn, /fix\(plugin\)/); + assert.doesNotMatch(topic.text_en, /fix\(plugin\)/); +}); + test("GitHub release notes fallback stays whole-repo when API access is unavailable", async () => { const originalCwd = process.cwd(); const root = mkdtempSync(join(tmpdir(), "memos-release-notes-fallback-")); diff --git a/.github/workflows/memos-local-plugin-publish.yml b/.github/workflows/memos-local-plugin-publish.yml index 4d466cf35..196f698f7 100644 --- a/.github/workflows/memos-local-plugin-publish.yml +++ b/.github/workflows/memos-local-plugin-publish.yml @@ -15,7 +15,7 @@ on: required: false default: "" release_notes: - description: "Optional Markdown release notes. Leave blank to let the configured draft service use real git evidence before publish." + description: "Optional Markdown release notes. latest uses Doc Agent evidence; beta/non-latest can use package-only notes." required: false default: "" dry_run: @@ -50,7 +50,7 @@ on: type: string default: "" release_notes: - description: "Optional Markdown release notes. Leave blank to let the configured draft service use evidence." + description: "Optional Markdown release notes. latest uses Doc Agent evidence; beta/non-latest can use package-only notes." required: false type: string default: "" @@ -103,7 +103,8 @@ jobs: expected="LEGACY PUBLISH memos-local-plugin-v${RELEASE_VERSION}" if [ "${LEGACY_PUBLISH_CONFIRMATION}" != "${expected}" ]; then - echo "::error::This workflow is the legacy standalone local-plugin publisher. The current official path is MemOS Release — Publish with v* tags and apps/memos-local-plugin/** extraction." + echo "::error::This workflow is the standalone local-plugin npm publisher for beta or latest package releases." + echo "::error::MemOS Release — Publish remains the weekly whole-repo release path and can also update the Plugin tab from apps/memos-local-plugin/** changes." echo "::error::To intentionally run this legacy publisher, set legacy_publish_confirmation exactly to: ${expected}" exit 1 fi @@ -289,6 +290,7 @@ jobs: env: RELEASE_VERSION: ${{ inputs.version }} RELEASE_TAG: memos-local-plugin-v${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} MANUAL_RELEASE_NOTES: ${{ inputs.release_notes }} DOC_AGENT_RELEASE_NOTES_DRAFT_URL: ${{ secrets.DOC_AGENT_RELEASE_NOTES_DRAFT_URL }} DOC_AGENT_RELEASE_FAILURE_URL: ${{ secrets.DOC_AGENT_RELEASE_FAILURE_URL }}