You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
public Release body: github_generated_whats_changed
docs-preview.json includes source_repo=MemTensor/MemOS, source_ref=d48d2a1ae99312a4f9f495e7abab9e4f5409aa0c, product_paths=["apps/memos-local-plugin/**"], would_create_docs_pr=false, and target files content/cn/plugin-changelog.yml / content/en/plugin-changelog.yml
local-plugin changed files: 1
important evidence coverage: 1/1
Plugin tab draft: V7 session defaults fix, with source_refd48d2a1a
Safety
PR/pre-merge dry-run does not create tag, GitHub Release, docs PR, npm publish, OSS upload, pre, gray, or production deployment
verified after latest dry-run: remote refs/tags/v2.0.26 does not exist, and GitHub Release API for v2.0.26 returns 404
public memos-release-notes.md remains whole-repo What's Changed and does not include Plugin tab source_refs or hidden payload
quality-report.json records release_kind=memos_whole_repo, docs_product_extraction=path_filtered, public_release_body=github_generated_whats_changed, and all no_side_effects flags as false
dry_run=false requires publish_confirmation exactly equal to PUBLISH v<version>
dry_run=false defaults to create_draft_release=true, so release.published is only triggered after manual publish
The GitHub API version string 2026-03-10 is a future date (current date is 2026-07-31). GitHub's documented stable API version is 2022-11-28. While GitHub may accept unknown or future-dated version headers gracefully, using an unrecognized version string risks unexpected behavior or rejection. The only documented API version for the releases/generate-notes endpoint is 2022-11-28. This should be corrected to the actual stable version.
The heredoc delimiter EOF is hardcoded without any check that the output value itself contains a line consisting solely of EOF. If any multi-line value (e.g., existingTag.message, evidence.skip_reason, or release note bodies passed through appendOutput) contains \nEOF\n, the GITHUB_OUTPUT file will be truncated at that point and downstream steps will read incorrect or missing values. This is a known correctness issue with naive heredoc usage in GitHub Actions output. Use a randomized or collision-resistant delimiter, or sanitize newlines for single-line values.
💡 Suggested Change
Before:
function appendOutput(name, value) {
if (!process.env.GITHUB_OUTPUT) return;
const text = String(value ?? "");
writeFileSync(process.env.GITHUB_OUTPUT, `${name}<<EOF\n${text}\nEOF\n`, { flag: "a" });
}
After:
function appendOutput(name, value) {
if (!process.env.GITHUB_OUTPUT) return;
const text = String(value ?? "");
// Use a random delimiter to prevent collision with output content
const delimiter = `EOF_${Math.random().toString(36).slice(2)}`;
writeFileSync(process.env.GITHUB_OUTPUT, `${name}<<${delimiter}\n${text}\n${delimiter}\n`, { flag: "a" });
}
For every commit in the git log range, commitBodyExcerpt spawns a separate synchronous execFileSync("git show") subprocess. For releases with hundreds of commits, this creates an O(N) subprocess overhead that blocks the Node.js event loop and can cause CI timeouts. Instead, retrieve all commit bodies in a single git log call using a separator-delimited format (e.g., --format=%H%x00%B%x00 with a NUL separator), or at minimum apply a commit count limit before entering this loop.
When ALLOW_OFFLINE_DOCS_PREVIEW=true and no Doc Agent credentials are available, requestDocAgentDraft returns the offline fallback draft directly via normalizeDraft(localFallbackDraft(evidence)). Back in run(), this draft is then passed to validateDraft(draft, evidence), which will fail if required_source_refs are not covered by the fallback's release_items. The offline fallback uses heuristic pattern-matching (FALLBACK_TOPIC_RULES) and may not cover all required_source_refs, causing run() to throw even though ALLOW_OFFLINE_DOCS_PREVIEW was explicitly set to permit degraded output. The test file confirms this path exists but does not test the run() integration. Consider either skipping the outer validateDraft when the draft originates from the offline fallback, or marking the fallback draft with needs_review: true and downgrading the outer validation to a warning.
The FALLBACK_TOPIC_RULES array contains hardcoded, historically-specific release note text. Any future commit whose subject happens to match a pattern (e.g., any commit mentioning logging or bridge) will unconditionally produce the exact same predefined Chinese and English bullet, regardless of what the actual change was. This creates a silent risk of publishing factually incorrect release notes in offline fallback mode. Additionally, the offline fallback sets needs_review: false and confidence: "medium", so a human reviewer has no signal that the content is pattern-matched rather than evidence-derived. At minimum, the fallback draft should set needs_review: true to flag it for human review before publishing.
💡 Suggested Change
Before:
const FALLBACK_TOPIC_RULES = [
{
pattern: /openrouter/i,
category: "Added",
text_cn: "**OpenRouter 提供商路由**:新增 OpenRouter 路由与 reasoning 配置支持,便于按配置选择模型提供商。",
text_en: "**OpenRouter provider routing**: Added OpenRouter routing and reasoning configuration support for model selection.",
},
After:
return {
ok: true,
needs_review: true, // Always require human review for offline fallback drafts
confidence: items.length ? "medium" : "high",
warnings: ["offline fallback draft; use GitHub Actions with Doc Agent secrets for production quality"],
release_items: items,
The two async tests that mock globalThis.fetch and mutate multiple process.env variables run under node --test without --test-concurrency=1. Node.js 18+ runs top-level tests concurrently by default, so the global state mutations in these tests can race with each other and with the synchronous withFixtureRepo tests that call process.chdir().
Specifically:
One test's globalThis.fetch mock can intercept fetch() calls intended for the other test, making one test's fake always return invalidDraft while the other expects validDraft on the 4th call.
process.env.ALLOW_OFFLINE_DOCS_PREVIEW deletion/restoration is not atomic, so the other test may read undefined mid-execution when it expects "true".
process.chdir() in withFixtureRepo (which runs in many synchronous tests) changes the working directory for all concurrently executing tests, including the async ones that call generateGitHubReleaseNotes (which uses tryGit(["log", ...]) internally via process.cwd()).
Suggested fix: Either pass --test-concurrency=1 in the workflow node --test invocation, or use test's built-in mock API to scope the mocks properly:
# In the workflow files:run: node --test --test-concurrency=1 .github/scripts/prepare-memos-release.test.mjs
Or wrap async tests with Node's test.mock / restore hooks to avoid global mutation.
Temporary directories created by mkdtempSync in withFixtureRepo are never cleaned up. The helper is used in at least 9 test cases, each writing a full git repo with history into os.tmpdir(). Neither a finally block with rmSync(root, { recursive: true }) nor any other cleanup mechanism is present.
While ephemeral CI environments discard the runner disk, running the tests locally will accumulate unbounded memos-release-evidence-* directories. More importantly, the orphaned .git directories can interfere with git commands run from the original working directory if git walks parent directories (e.g., auto-discovery of a repo root).
Suggested fix: Clean up the temp dir in the finally block:
The git revert --no-commit call does not handle potential conflicts. If git cannot cleanly apply the revert (even in this minimal fixture), the command will leave the repository in a conflicted state and the subsequent git commit will fail with an unhelpful error.
Moreover, execFileSync is called without a timeout option (the default is no timeout). In pathological cases — for example if git waits for a conflict resolution prompt on certain git versions — the test process would hang indefinitely, blocking the CI runner.
Suggested fix: Use git revert --no-edit to commit the revert in a single step and avoid staging issues, or add a timeout and error handling:
// Option A: Single-step revert (simpler and avoids the staged-state edge case)git(["revert","--no-edit",featureSha]);// Option B: If two-step is required, add a timeout guardgit(["revert","--no-commit","--no-edit",featureSha]);
The "GitHub release notes fallback stays whole-repo when API access is unavailable" async test creates its own temp directory and git repo using the same process.chdir() / restore pattern as withFixtureRepo, but it is declared as an async test. Since node --test runs top-level tests concurrently, this test's process.chdir(root) executes at an indeterminate time relative to all synchronous withFixtureRepo tests.
A synchronous test calling collectLocalPluginEvidence (which calls sh(["log", ...]) using process.cwd()) could observe the async test's temporary repo path as its working directory, causing git operations to run in the wrong repository and return incorrect or empty results.
Suggested fix: Refactor this test to avoid process.chdir() altogether by passing the cwd explicitly to git operations, or run it within withFixtureRepo (extended to support async callbacks), or enforce serial execution via --test-concurrency=1.
The guard-legacy-publish job is added to both workflow_dispatch and workflow_call triggers. However, the confirmation string "LEGACY PUBLISH memos-local-plugin-v<version>" is trivially constructable by any caller workflow that invokes this via workflow_call. Since workflow_call callers run in the same trusted context as the callee, a caller can always supply legacy_publish_confirmation: "LEGACY PUBLISH memos-local-plugin-v${{ inputs.version }}" with dry_run: false and bypass the guard entirely without any human interaction.
The guard is only meaningful as a friction mechanism for human workflow_dispatch users, but as written it also applies (and can be trivially satisfied) for machine-to-machine workflow_call invocations. The other added workflows (memos-release-publish.yml) will presumably call this via workflow_call and must always supply this string — creating maintenance friction with no real security benefit for that path.
Suggestion: Restrict the guard to workflow_dispatch only using github.event_name, so programmatic workflow_call invocations are not required to provide the confirmation string:
if [ "${DRY_RUN}"="true" ];thenecho"dry_run=true; legacy standalone publish confirmation is not required."exit 0
fiif [ "${{ github.event_name }}"!="workflow_dispatch" ];thenecho"workflow_call path; skipping legacy confirmation check."exit 0
fi
expected="LEGACY PUBLISH memos-local-plugin-v${RELEASE_VERSION}"
...
Alternatively, if the intent is that all callers (including workflow_call) must confirm, document this expectation explicitly and ensure that the calling workflows supply it intentionally.
💡 Suggested Change
Before:
legacy_publish_confirmation:
description: "Required only when dry_run=false. Must exactly equal: LEGACY PUBLISH memos-local-plugin-v<version>"
required: false
type: string
default: ""
After:
legacy_publish_confirmation:
description: "Required only when dry_run=false AND event is workflow_dispatch. Must exactly equal: LEGACY PUBLISH memos-local-plugin-v<version>"
required: false
type: string
default: ""
The GITHUB_EVENT_NAME (i.e., ${{ github.event_name }}) is used inside the script via a direct expression substitution rather than an env var. To check the event name inside the guard script (see suggestion on the first comment), it should be passed through env: to avoid any potential injection:
The Summarize inspection step is missing TARGET_REF and TARGET_SHA from both the env: block and the summary output, even though prepare-memos-release.mjs writes both to GITHUB_OUTPUT (via appendOutput('target_ref', ...) and appendOutput('target_sha', ...)). The equivalent publish workflow (memos-release-publish.yml) includes these in its summary. Without them, it's impossible to tell from the step summary which exact commit was inspected in this post-merge smoke check, making it harder to diagnose discrepancies between runs.
Suggested fix: add TARGET_REF and TARGET_SHA to both the env: block and the echo lines in the summary.
The Summarize inspection step does not echo target_ref or target_sha into the step summary, making it difficult to identify the exact commit that was inspected. The publish workflow includes these fields. Add the missing echo lines after the release_notes_source line.
The 'Summarize inspection' run: block is missing set -euo pipefail at the top, unlike every other run: block in this workflow (and the identical step in the sibling memos-release-post-merge-dry-run.yml). Without set -u, variables that are unset or empty (e.g. PUBLISH_BLOCKED, HAS_PRODUCT_CHANGES, DOCS_ACTION, RELEASE_NOTES_SOURCE, LOCAL_PLUGIN_VERSION, etc.) will silently expand to empty strings rather than causing the step to fail. This hides potential issues where a prior step failed to produce an expected output, resulting in a misleading summary being written to $GITHUB_STEP_SUMMARY with blank values.
Add set -euo pipefail as the first line of this run: block for consistency and reliability. Variables that are intentionally optional already use ${VAR:-n/a} defaults, so enabling set -u would only surface genuinely missing required values.
The || github.ref fallback in the concurrency group is dead code. This workflow is triggered exclusively by pull_request events, so github.event.pull_request.number is always defined and non-empty. The || github.ref branch will never be evaluated.
The sibling memos-release-post-merge-dry-run.yml (push-triggered) uses a static group name memos-release-post-merge-dry-run-main instead, which is the cleaner pattern. Consider removing the dead fallback to avoid confusion for future maintainers who may assume this workflow can also be triggered by push events.
The --force flag on git fetch --tags silently overwrites any locally cached tag refs with whatever is on the remote. While this is intentional for fetching the latest remote state, it could mask a situation where a local tag already exists at a different SHA after the shallow checkout. Using --force here is generally safe in a fresh CI runner, but it's worth being explicit: if the checkout step ever pre-fetches tags (e.g., via fetch-tags: true on actions/checkout), the --force flag would silently discard those refs and replace them with remote values without any warning. Consider adding a comment explaining why --force is intentional, or remove --force if the checkout never pre-fetches tags.
The draft release existence check has subtly incorrect logic for the CREATE_DRAFT_RELEASE=true case. When a draft GitHub Release already exists and CREATE_DRAFT_RELEASE=true, the condition [ "${is_draft}" = "true" ] && [ "${CREATE_DRAFT_RELEASE}" != "true" ] evaluates to false, so the workflow falls through to the ::notice:: line and exits without error — even if the existing draft has stale release notes, points to a different target SHA, or was created by a previous failed run. This means rerunning the workflow after a partial failure will silently leave an incorrect draft in place rather than updating or failing explicitly. Consider adding a check that validates the existing draft release's target SHA matches TARGET_SHA before accepting it as-is.
💡 Suggested Change
Before:
if [ "${is_draft}" = "true" ] && [ "${CREATE_DRAFT_RELEASE}" != "true" ]; then
echo "::error::GitHub Release ${CURRENT_TAG} already exists as draft (${release_url}); publish or delete it manually before rerunning."
exit 1
fi
echo "::notice::GitHub Release ${CURRENT_TAG} already exists (${release_url}); leaving it unchanged."
After:
if [ "${is_draft}" = "true" ] && [ "${CREATE_DRAFT_RELEASE}" != "true" ]; then
echo "::error::GitHub Release ${CURRENT_TAG} already exists as draft (${release_url}); publish or delete it manually before rerunning."
exit 1
fi
# Warn that the existing release (draft or published) is left unchanged.
# Operators should verify the release notes and target SHA are correct.
echo "::warning::GitHub Release ${CURRENT_TAG} already exists (${release_url}); leaving it unchanged. Verify release notes and target SHA are correct before publishing."
The workflow-level permissions: contents: write grants write access to all jobs in this workflow. This is appropriate for the release job, but applying it at the workflow level rather than the job level means any future jobs added to this workflow will inherit this elevated permission by default — violating the principle of least privilege. Move the permission to the release job level so it's scoped only where needed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
area:coreMOS 编排层 / 框架底座 / 跨模块问题status:readyReady for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发
5 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
apps/memos-local-plugin/**while keeping public Release notes as MemOSWhat's Changedapps/memos-local-plugin/**dry_run=falsereleases to Draft GitHub Releases first; publish manually to triggerrelease.publishedrelease-notes.md,evidence.json,docs-preview.md/json, andquality-report.jsonVerification
node --test .github/scripts/prepare-memos-release.test.mjspasses 26/26ruby -ryamlparse formemos-release-publish.yml,memos-release-pre-merge-dry-run.yml, andmemos-release-post-merge-dry-run.ymlgit diff --checkmemos-release-inspection, digestsha256:95ae7526c30a70611e7578f5a9d6433ff7a629e31c98f4d1bde16d823d12e7c5Latest dry-run output
b1976f21f2e9de1db8e2fd0bb12678ab2570fc02v2.0.26v2.0.25...v2.0.26source_id:openclaw-local-pluginrelease_kind:memos_whole_repodocs_product_extraction:path_filteredgithub_generated_whats_changeddocs-preview.jsonincludessource_repo=MemTensor/MemOS,source_ref=d48d2a1ae99312a4f9f495e7abab9e4f5409aa0c,product_paths=["apps/memos-local-plugin/**"],would_create_docs_pr=false, and target filescontent/cn/plugin-changelog.yml/content/en/plugin-changelog.ymlsource_refd48d2a1aSafety
refs/tags/v2.0.26does not exist, and GitHub Release API forv2.0.26returns 404memos-release-notes.mdremains whole-repoWhat's Changedand does not include Plugin tabsource_refsor hidden payloadquality-report.jsonrecordsrelease_kind=memos_whole_repo,docs_product_extraction=path_filtered,public_release_body=github_generated_whats_changed, and allno_side_effectsflags as falsedry_run=falserequirespublish_confirmationexactly equal toPUBLISH v<version>dry_run=falsedefaults tocreate_draft_release=true, sorelease.publishedis only triggered after manual publish