Skip to content

ci: automate paired local plugin releases after MemOS preparation - #2249

Merged
syzsunshine219 merged 5 commits into
mainfrom
ci/memos-local-plugin-auto-release
Aug 28, 2026
Merged

ci: automate paired local plugin releases after MemOS preparation#2249
syzsunshine219 merged 5 commits into
mainfrom
ci/memos-local-plugin-auto-release

Conversation

@MLittleprince

Copy link
Copy Markdown
Collaborator

Summary

This PR is intentionally stacked on #2243. Because #2243 comes from a fork, GitHub cannot use its head as this PR's base; until #2243 merges, the Files changed view temporarily includes that prerequisite commit. It keeps MemOS Release — Prepare as the trusted release-branch preparation step, then adds the post-merge MemOS/local-plugin release automation.

  • automatically starts MemOS Release — Publish after an eligible same-repository release/vX.Y.Z, dev-vX.Y.Z, or dev-X.Y.Z PR is merged to main
  • locks the release to the PR merge commit and validates both MemOS version declarations
  • generates the whole-repository MemOS Release notes while extracting local-plugin evidence only from apps/memos-local-plugin/**
  • releases a local-plugin patch only when the final evidence contains a real user-visible feature, fix, performance change, or impact-bearing refactor
  • filters test/docs/version/build noise and removes reverted changes while retaining later genuine reapplications
  • stages the MemOS and paired local-plugin tags/Draft Releases before any npm or docs side effect
  • publishes and verifies npm only after a human publishes the MemOS Release, then publishes the paired local-plugin Draft so its release.published webhook is the sole docs trigger
  • keeps the standalone local-plugin workflow for prerelease, major/minor, and urgent stable releases
  • adds bounded retry, integrity, idempotency, recovery, source-ref, bilingual quality, and sensitive-data guards
  • adds a Chinese operator/runbook document under .github/release-notes/

Safety boundaries

  • Merging this PR does not trigger a release because its branch name is not an eligible release branch.
  • Automatic post-merge releases always create Draft Releases first.
  • npm, Plugin tab, pre, and gray do not start until a human publishes the MemOS Draft.
  • Production publication remains manual.
  • No token, endpoint, npm credential, or OSS credential is committed or written into artifacts.

Validation

  • node --test .github/scripts/*.test.mjs: 155 passed
  • node --test .github/scripts/prepare-memos-release.test.mjs: 64 passed
  • workflow YAML parse: 4 passed
  • bash -n .github/scripts/publish-local-plugin.sh: passed
  • npm --prefix apps/memos-local-plugin run check:hermes-version: passed
  • npm --prefix apps/memos-local-plugin run lint: passed
  • focused Hermes version tests: 7 passed
  • git diff --check: passed
  • sensitive-value scan: no findings
  • real-history dry run for MemOS 2.0.29: passed; it verified local-plugin baseline v2.0.15, detected three plugin-path changes as non-user-visible maintenance, skipped v2.0.16, and recorded no npm/docs/deployment side effects

Merge order

  1. Review and merge feat: automate MemOS release preparation #2243 first.
  2. Refresh this PR; GitHub will remove feat: automate MemOS release preparation #2243's prerequisite diff automatically.
  3. Mark this PR ready, re-run required checks if necessary, and merge it.

This ordering keeps the preparation automation and paired local-plugin automation reviewable as separate changes.

@Memtensor-AI Memtensor-AI added area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 14, 2026
@MLittleprince
MLittleprince force-pushed the ci/memos-local-plugin-auto-release branch from 082f375 to c301e03 Compare August 18, 2026 07:53
@syzsunshine219
syzsunshine219 marked this pull request as ready for review August 28, 2026 03:19
@syzsunshine219
syzsunshine219 merged commit 84ae381 into main Aug 28, 2026
16 of 19 checks passed
@syzsunshine219
syzsunshine219 deleted the ci/memos-local-plugin-auto-release branch August 28, 2026 03:27
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2249
Task: d39384fdd273407c
Base: main
Head: ci/memos-local-plugin-auto-release

🔍 OpenCodeReview found 13 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. .github/scripts/prepare-memos-release.test.mjs (L195)

This throws assertion about repositoryReleaseNotesPath is placed inside the test named "rejects unsafe repository-authored release notes", but it exercises a completely separate concern (path validation, not credential safety). When this assertion fails, the failure message will be attributed to the wrong test, making diagnosis harder. The path validation assertion belongs in the companion test "adds optional repository-authored notes without replacing generated notes", or in its own dedicated test block.


2. .github/scripts/prepare-memos-release.test.mjs (L509-L526)

Using assert.deepEqual against a literal { accepted, relationship } object will cause the test to fail if validateStableLocalPluginSourceLineage ever returns additional diagnostic fields (e.g., tag_commit, reason). Since the function is specifically designed to carry lineage metadata, this is likely. Prefer testing the individual properties of interest with assert.equal so the test remains valid as the return shape evolves:

const r1 = validateStableLocalPluginSourceLineage({ ...common, tagIsTargetAncestor: true });
assert.equal(r1.accepted, true);
assert.equal(r1.relationship, "target_history");

3. .github/scripts/prepare-memos-release.test.mjs (L1082-L1083)

The fixture was changed from a tests/e2e/ path to src/session-defaults.ts. The test is named "filters standalone local-plugin release metadata from docs evidence" and its purpose is to verify that release-commit metadata (#12, #13) is filtered out while the real change commit (#11) is retained. With a src/ file the commit is user-facing, so has_user_facing_product_changes is true and important_commits includes #11 — consistent with the existing assertions.

However, the original fixture exercised a subtler case: the only real change was a test-file commit (which is maintenance-only), confirming that non-user-facing changes are correctly omitted from important_commits even after filtering metadata commits. That specific boundary is no longer covered here and is now only exercised by the separate "skips feat-labeled commits" test. This reduces the test's stated scope to just metadata filtering, which doesn't match the test name ("filters standalone local-plugin release metadata from docs evidence") as well now that the fixture is a straightforward user-facing src change.


4. .github/workflows/memos-release-pre-merge-dry-run.yml (L80)

The glob .github/scripts/*.test.mjs now runs all test files in that directory, but the paths: trigger only lists a subset of them (memos-version.test.mjs, prepare-memos-release.test.mjs, publish-local-plugin.test.mjs). Files such as append-local-plugin-release-intent.test.mjs, audit-local-plugin-package.test.mjs, create-local-plugin-github-release.test.mjs, draft-local-plugin-release-notes.test.mjs, inspect-local-plugin-release-state.test.mjs, local-plugin-release-contract.test.mjs, publish-paired-local-plugin-release.test.mjs, and wait-for-local-plugin-npm-release.test.mjs are executed by the runner but won't trigger this workflow when they change. A regression in those scripts will not be caught by this pre-merge check until a different listed file also changes.

Consider either:

  • Adding .github/scripts/*.test.mjs to the paths: filter (or a pattern like .github/scripts/**), or
  • Keeping the explicit list in paths: in sync with the glob.
💡 Suggested Change

Before:

        run: node --test .github/scripts/*.test.mjs

After:

        run: node --test .github/scripts/*.test.mjs

5. .github/workflows/memos-local-plugin-publish.yml (L190-L196)

The auto_post_merge_release flag unconditionally bypasses the caller_publish_confirmation guard for all DOCS_SYNC_MODE=paired_with_memos_release real publishes, regardless of publish_phase. This is intentional for the automated post-merge path, but the flag itself is a free-form boolean workflow input with default: false. Any caller that sets auto_post_merge_release: true will skip the human confirmation check. Since this is a workflow_call input (not constrained to a specific caller), if a misconfigured or new caller passes auto_post_merge_release: true with publish_phase: full, it can trigger a real npm publish + tag push without any confirmation string. Consider restricting this bypass to publish_phase == 'publish_npm_only' (the only phase actually used in the automated post-merge path), or assert in the guard that auto_post_merge_release=true requires publish_phase != 'full'.


6. .github/workflows/memos-local-plugin-publish.yml (L304-L306)

The (inputs.publish_phase || 'full') expression is used in multiple if: conditions across the workflow. In GitHub Actions expression context, when inputs.publish_phase is the empty string '' (which is falsy), the || operator correctly falls back to 'full'. However, when this workflow is triggered via workflow_dispatch (not workflow_call), publish_phase is not declared as a workflow_dispatch input, so inputs.publish_phase evaluates to ''. The || fallback handles that correctly. The concern is that publish_phase only has a default on the workflow_call block—if GitHub ever passes the literal string "" for missing workflow_dispatch inputs, the || 'full' guard saves correctness. This is safe as-is, but the repeated (inputs.publish_phase || 'full') pattern across five different if: expressions is fragile: a single typo in any one of them would silently change gating behavior. Consider extracting the resolved phase into a job output or GITHUB_ENV variable in the guard job and referencing that output instead.


7. .github/workflows/memos-local-plugin-publish.yml (L267-L272)

The read -r commit_sha parent_sha extra_parent word-split on commit_line relies on IFS splitting. If package_source_sha is an octopus merge commit with three or more parents, extra_parent will capture only the first extra parent SHA; subsequent extra parents will be silently ignored and the check [ -n "${extra_parent:-}" ] will still correctly trigger. However, if git rev-list output unexpectedly contains trailing whitespace or a newline (e.g., due to locale settings), the extra_parent check could miss it. The safer idiom is to count parents explicitly: git cat-file -p "${package_source_sha}" | grep -c '^parent ' and assert the count is exactly 1, rather than relying on positional word-splitting.


8. .github/workflows/memos-local-plugin-publish.yml (L805)

The ALLOW_STAGED_TAG_BEFORE_NPM env var is constructed entirely from an expression evaluated at workflow parse time: ${{ (inputs.publish_phase || 'full') == 'publish_npm_only' && 'true' || 'false' }}. This is correct logic but note the value is a string 'true' or 'false', not a boolean, which matches the shell script's case check. No issue with the value itself, but if inputs.publish_phase is undefined (empty) and the || fallback produces 'full', the expression evaluates correctly to 'false'. Just be aware this means for workflow_dispatch callers (where publish_phase is not an input), this will always be 'false', which is the intended behaviour for standalone legacy publishes.


9. .github/workflows/memos-release-publish-main.yml (L96)

The PR trigger guard hardcodes 'main' instead of comparing against the dynamically-resolved $DEFAULT_BRANCH. The workflow_dispatch path above correctly uses $DEFAULT_BRANCH, but the pull_request path short-circuits to a literal string. If the repository's default branch is renamed, these two guards will diverge: workflow_dispatch runs will fail with a clear error while pull_request events targeting the new default branch will silently be ignored (or, conversely, PRs targeting a branch literally named main in a repo whose default is different would pass this check).

Suggestion: replace "main" with "$DEFAULT_BRANCH" to stay consistent with the dispatch path and avoid future drift.

💡 Suggested Change

Before:

          elif [[ "$EVENT_NAME" == "pull_request" && "$PR_MERGED" == "true" && "$PR_BASE_REF" == "main" ]]; then

After:

          elif [[ "$EVENT_NAME" == "pull_request" && "$PR_MERGED" == "true" && "$PR_BASE_REF" == "$DEFAULT_BRANCH" ]]; then

10. .github/workflows/memos-release-publish-main.yml (L597-L598)

dry_run is hardcoded to false in this job while every other real-publish job derives it from needs.prepare.outputs.dry_run. Although the job can only run when release succeeded (which itself requires dry_run != 'true'), the hardcoded value masks the intent and creates a latent risk: if the job's if: condition is ever adjusted to include non-release paths, or if an idempotent re-run on a non-dry-run attempt evaluates the condition while the operator intended a preview, it will perform a live npm publish. Using the prepare output makes the behavior explicit and consistent.

Suggestion: derive dry_run from the prepare job output, matching the pattern on line 300.

💡 Suggested Change

Before:

      dry_run: false
      recover_existing_npm_release: true

After:

      dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
      recover_existing_npm_release: true

11. .github/workflows/memos-release-publish-main.yml (L117-L121)

The prepare job's if: condition uses a malformed expression — there is an opening ${ instead of ${{. This is a YAML typo that will cause the condition to be treated as a literal string (which evaluates to truthy), bypassing the intended guard and making the prepare job run unconditionally for all events on the repository.

💡 Suggested Change

Before:

if: >-
      ${{
        github.repository == 'MemTensor/MemOS' &&
        needs.classify_release_trigger.outputs.is_release_trigger == 'true'
      }}

After:

    if: >-
      ${{
        github.repository == 'MemTensor/MemOS' &&
        needs.classify_release_trigger.outputs.is_release_trigger == 'true'
      }}

12. .github/workflows/memos-release-publish-paired-local-plugin.yml (L154)

Setting MEMOS_RELEASE_TAG_OVERRIDE unconditionally (even during release.published events) forces loadMemOSRelease to always make a redundant gh api call to re-fetch data already retrieved by the resolve job. More critically, this bypasses the event-file path entirely, meaning if resolve-paired-local-plugin outputs a stale or mismatched tag (e.g. due to a race between two concurrent release.published triggers for different tags), the publish job could publish the wrong release. On the original single-job path, the publish step read from GITHUB_EVENT_PATH, which is always scoped to the exact triggering event.

Consider leaving MEMOS_RELEASE_TAG_OVERRIDE unset on the release.published path (i.e. omit it here, or gate it like the resolve job does) so the publish step reads the authoritative event file instead of a propagated output.


13. .github/workflows/memos-release-publish-paired-local-plugin.yml (L125-L133)

When resolve-paired-local-plugin is skipped because its own if condition is false (e.g. the repository guard fails), GitHub Actions propagates a skipped result for publish-local-plugin-npm as well. In that case needs.resolve-paired-local-plugin.outputs.status evaluates to an empty string (outputs are not populated for skipped jobs), and needs.publish-local-plugin-npm.result is 'skipped'. The compound condition therefore evaluates to false, which is safe — the job correctly does not run. However, this behaviour is implicit and relies on empty-string falsy semantics inside ${{ }}. Adding an explicit needs.resolve-paired-local-plugin.result == 'success' guard makes the intent unambiguous and prevents future confusion if the condition logic is refactored:

if: >-
  ${{
    needs.resolve-paired-local-plugin.result == 'success' &&
    needs.resolve-paired-local-plugin.outputs.status == 'staged' &&
    needs.publish-local-plugin-npm.result == 'success'
  }}

🧹 Filtered 2 low-confidence OCR finding(s) before posting/fix-loop (duplicate: 2).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch ci/memos-local-plugin-auto-release git@github.com:MemTensor/MemOS.git /data/test-workspaces/d39384fdd273407c/repo
Cloning into '/data/test-workspaces/d39384fdd273407c/repo'...
warning: Could not find remote branch ci/memos-local-plugin-auto-release to clone.
fatal: Remote branch ci/memos-local-plugin-auto-release not found in upstream origin
Branch: ci/memos-local-plugin-auto-release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants