Skip to content

MemOS Local Plugin (V2) — Legacy Standalone Publisher #55

MemOS Local Plugin (V2) — Legacy Standalone Publisher

MemOS Local Plugin (V2) — Legacy Standalone Publisher #55

name: MemOS Local Plugin (V2) — Legacy Standalone Publisher
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 2.0.2 or 2.0.2-beta.1)"
required: true
tag:
description: "npm dist-tag (latest for production, beta/next/alpha for testing)"
required: true
default: "latest"
git_ref:
description: "Git ref to build from (branch, tag, or SHA). Leave blank to use the branch selected above."
required: false
default: ""
release_notes:
description: "Optional Markdown release notes. latest uses Doc Agent evidence; beta/non-latest can use package-only notes."
required: false
default: ""
dry_run:
description: "Draft release notes and build artifacts only. Skip npm publish, tag, GitHub Release, and release PR."
required: true
type: boolean
default: true
recover_existing_npm_release:
description: "Allow reconstructing a missing tag/Release for an npm version. Keep false for normal releases."
required: true
type: boolean
default: false
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: ""
workflow_call:
inputs:
version:
description: "Version to publish or dry-run (e.g. 2.0.10 or 2.0.11-beta.1)"
required: true
type: string
tag:
description: "npm dist-tag (latest for production, beta/next/alpha for testing)"
required: false
type: string
default: "latest"
git_ref:
description: "Git ref to build from. Leave blank to use the caller ref."
required: false
type: string
default: ""
release_notes:
description: "Optional Markdown release notes. latest uses Doc Agent evidence; beta/non-latest can use package-only notes."
required: false
type: string
default: ""
dry_run:
description: "Draft release notes and build artifacts only. Skip npm publish, tag, GitHub Release, and release PR."
required: false
type: boolean
default: true
recover_existing_npm_release:
description: "Allow reconstructing a missing tag/Release for an npm version. Keep false for normal releases."
required: false
type: boolean
default: false
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: ""
concurrency:
group: memos-local-plugin-publish
cancel-in-progress: false
defaults:
run:
working-directory: apps/memos-local-plugin
permissions:
contents: write
pull-requests: write
jobs:
guard-legacy-publish:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate legacy standalone publish confirmation
shell: bash
working-directory: .
env:
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_VERSION: ${{ inputs.version }}
LEGACY_PUBLISH_CONFIRMATION: ${{ inputs.legacy_publish_confirmation }}
run: |
set -euo pipefail
if [ "${DRY_RUN}" = "true" ]; then
echo "dry_run=true; legacy standalone publish confirmation is not required."
exit 0
fi
expected="LEGACY PUBLISH memos-local-plugin-v${RELEASE_VERSION}"
if [ "${LEGACY_PUBLISH_CONFIRMATION}" != "${expected}" ]; then
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
build-prebuilds:
needs: guard-legacy-publish
strategy:
matrix:
include:
- os: macos-14
platform: darwin-arm64
- os: macos-15-intel
platform: darwin-x64
- os: ubuntu-latest
platform: linux-x64
- os: windows-latest
platform: win32-x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.ref }}
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
shell: bash
run: bash ../../.github/scripts/retry.sh --label "npm ci" -- npm ci
- name: Rebuild for x64 under Rosetta (darwin-x64 only)
if: matrix.platform == 'darwin-x64'
shell: bash
run: bash ../../.github/scripts/retry.sh --label "npm rebuild better-sqlite3 under Rosetta" -- arch -x86_64 npm rebuild better-sqlite3
- name: Collect prebuild
shell: bash
run: |
bash ../../.github/scripts/retry.sh --label "collect ${{ matrix.platform }} prebuild" -- bash -euo pipefail -c '
mkdir -p prebuilds/${{ matrix.platform }}
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node prebuilds/${{ matrix.platform }}/
'
- name: Upload prebuild artifact
uses: actions/upload-artifact@v4
with:
name: prebuild-${{ matrix.platform }}
path: apps/memos-local-plugin/prebuilds/${{ matrix.platform }}/better_sqlite3.node
publish:
needs: build-prebuilds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.ref }}
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Test npm publish helper
working-directory: .
run: node --test .github/scripts/publish-local-plugin.test.mjs
- name: Download all prebuilds
uses: actions/download-artifact@v4
with:
path: apps/memos-local-plugin/prebuilds
pattern: prebuild-*
merge-multiple: false
- name: Organize prebuilds
run: |
bash ../../.github/scripts/retry.sh --label "organize prebuild artifacts" -- bash -euo pipefail -c '
cd prebuilds
for dir in prebuild-*; do
platform="${dir#prebuild-}"
mkdir -p "$platform"
mv "$dir/better_sqlite3.node" "$platform/"
rmdir "$dir"
done
echo "Prebuilds collected:"
find . -name "*.node" -exec ls -lh {} \;
'
- name: Install dependencies (skip native build)
run: bash ../../.github/scripts/retry.sh --label "npm ci --ignore-scripts" -- npm ci --ignore-scripts
- name: Install Linux sqlite binding for validation
run: |
bash ../../.github/scripts/retry.sh --label "install linux sqlite binding for validation" -- bash -euo pipefail -c '
test -s prebuilds/linux-x64/better_sqlite3.node
mkdir -p node_modules/better-sqlite3/build/Release
cp prebuilds/linux-x64/better_sqlite3.node node_modules/better-sqlite3/build/Release/better_sqlite3.node
'
- name: Generate telemetry credentials
run: bash ../../.github/scripts/retry.sh --label "generate telemetry credentials" -- node scripts/generate-telemetry-credentials.cjs
env:
MEMOS_ARMS_ENDPOINT: ${{ secrets.MEMOS_ARMS_ENDPOINT }}
MEMOS_ARMS_PID: ${{ secrets.MEMOS_ARMS_PID }}
MEMOS_ARMS_ENV: ${{ secrets.MEMOS_ARMS_ENV }}
- name: Bump version
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
if [[ "${RELEASE_VERSION}" == v* ]]; then
echo "::error::version must not include a leading v."
exit 1
fi
bash ../../.github/scripts/retry.sh --label "bump package version" -- npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version
npm run sync:hermes-version
npm run check:hermes-version
- name: Validate and build package
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
bash ../../.github/scripts/retry.sh --attempts 2 --label "npm run lint" -- npm run lint
bash ../../.github/scripts/retry.sh --attempts 2 --label "npm test" -- npm test
release_dir="${RUNNER_TEMP}/memos-local-plugin-release"
pack_json="${RUNNER_TEMP}/memos-local-plugin-pack.json"
mkdir -p "${release_dir}"
bash ../../.github/scripts/retry.sh --label "npm pack" -- \
bash -euo pipefail -c 'npm pack --json --silent --pack-destination "$1" > "$2"' \
_ "${release_dir}" "${pack_json}"
pack_filename="$(
node -e '
const fs = require("node:fs");
const raw = fs.readFileSync(process.argv[1], "utf8");
const jsonStart = raw.match(/^\[/m);
if (!jsonStart || jsonStart.index === undefined) {
throw new Error("npm pack output did not contain a JSON report");
}
const report = JSON.parse(raw.slice(jsonStart.index));
if (!Array.isArray(report) || report.length !== 1 || !report[0].filename) {
throw new Error("npm pack did not report exactly one tarball");
}
fs.writeFileSync(process.argv[1], `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(report[0].filename);
' "${pack_json}"
)"
release_tarball="${release_dir}/${pack_filename}"
test -s "${release_tarball}"
package_version="$(
tar -xOf "${release_tarball}" package/package.json \
| node -e '
const fs = require("node:fs");
process.stdout.write(JSON.parse(fs.readFileSync(0, "utf8")).version);
'
)"
manifest_version="$(
tar -xOf "${release_tarball}" package/adapters/hermes/plugin.yaml \
| awk '$1 == "version:" { print $2; exit }'
)"
if [ "${package_version}" != "${RELEASE_VERSION}" ]; then
echo "::error::Tarball package.json version ${package_version} does not match ${RELEASE_VERSION}."
exit 1
fi
if [ "${manifest_version}" != "${RELEASE_VERSION}" ]; then
echo "::error::Tarball Hermes manifest version ${manifest_version} does not match ${RELEASE_VERSION}."
exit 1
fi
sha256sum "${release_tarball}" > "${release_tarball}.sha256"
{
echo "RELEASE_TARBALL=${release_tarball}"
echo "RELEASE_TARBALL_SHA256=${release_tarball}.sha256"
} >> "${GITHUB_ENV}"
- name: Draft GitHub Release notes
id: release_notes
working-directory: .
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 }}
DOC_AGENT_RELEASE_NOTES_DRAFT_TOKEN: ${{ secrets.DOC_AGENT_RELEASE_NOTES_DRAFT_TOKEN }}
run: bash .github/scripts/retry.sh --attempts 2 --label "draft GitHub Release notes" -- node .github/scripts/draft-local-plugin-release-notes.mjs
- name: Prepare release notes inspection artifact
working-directory: .
env:
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_NOTES_FILE: ${{ steps.release_notes.outputs.release_notes_file }}
EVIDENCE_FILE: ${{ steps.release_notes.outputs.evidence_file }}
DRAFT_FILE: ${{ steps.release_notes.outputs.draft_file }}
DRAFT_USED: ${{ steps.release_notes.outputs.draft_used }}
PREVIOUS_TAG: ${{ steps.release_notes.outputs.previous_tag }}
CURRENT_TAG: ${{ steps.release_notes.outputs.current_tag }}
CURRENT_REF: ${{ steps.release_notes.outputs.current_ref }}
DRAFT_CONFIDENCE: ${{ steps.release_notes.outputs.draft_confidence }}
MISSING_REQUIRED_COUNT: ${{ steps.release_notes.outputs.missing_required_count }}
VALIDATION_ATTEMPT_COUNT: ${{ steps.release_notes.outputs.validation_attempt_count }}
REPAIR_ATTEMPT_COUNT: ${{ steps.release_notes.outputs.repair_attempt_count }}
run: |
set -euo pipefail
inspection_dir="${RUNNER_TEMP}/memos-local-plugin-release-notes-inspection"
mkdir -p "${inspection_dir}"
if [ -z "${RELEASE_NOTES_FILE}" ] || [ ! -s "${RELEASE_NOTES_FILE}" ]; then
echo "::error::Release notes file was not generated."
exit 1
fi
cp "${RELEASE_NOTES_FILE}" "${inspection_dir}/release-notes.md"
cp "${RUNNER_TEMP}/memos-local-plugin-pack.json" "${inspection_dir}/npm-pack.json"
cp "${RELEASE_TARBALL}" "${inspection_dir}/"
cp "${RELEASE_TARBALL_SHA256}" "${inspection_dir}/"
if [ -n "${EVIDENCE_FILE}" ] && [ -s "${EVIDENCE_FILE}" ]; then
cp "${EVIDENCE_FILE}" "${inspection_dir}/evidence.json"
fi
if [ "${DRAFT_USED:-}" = "true" ]; then
if [ -z "${DRAFT_FILE}" ] || [ ! -s "${DRAFT_FILE}" ]; then
echo "::error::Draft JSON file was not generated."
exit 1
fi
cp "${DRAFT_FILE}" "${inspection_dir}/release-notes-draft.json"
elif [ -n "${DRAFT_FILE}" ] && [ -s "${DRAFT_FILE}" ]; then
cp "${DRAFT_FILE}" "${inspection_dir}/release-notes-draft.json"
fi
{
echo "# MemOS local plugin release notes inspection"
echo
echo "- dry_run: ${DRY_RUN}"
echo "- version: ${RELEASE_VERSION}"
echo "- draft_used: ${DRAFT_USED:-unknown}"
echo "- previous_tag: ${PREVIOUS_TAG:-n/a}"
echo "- current_tag: ${CURRENT_TAG:-n/a}"
echo "- current_ref: ${CURRENT_REF:-n/a}"
echo "- draft_confidence: ${DRAFT_CONFIDENCE:-n/a}"
echo "- missing_required_count: ${MISSING_REQUIRED_COUNT:-n/a}"
echo "- validation_attempt_count: ${VALIDATION_ATTEMPT_COUNT:-n/a}"
echo "- repair_attempt_count: ${REPAIR_ATTEMPT_COUNT:-n/a}"
echo
echo "Files:"
echo
echo "- release-notes.md"
echo "- npm-pack.json"
if [ -n "${EVIDENCE_FILE}" ] && [ -s "${EVIDENCE_FILE}" ]; then
echo "- evidence.json (redacted; no full diff or prompt guidance)"
fi
if [ -f "${inspection_dir}/release-notes-draft.json" ]; then
echo "- release-notes-draft.json"
fi
} > "${inspection_dir}/README.md"
{
echo "### MemOS local plugin release notes"
echo
echo "- dry_run: \`${DRY_RUN}\`"
echo "- draft_used: \`${DRAFT_USED:-unknown}\`"
echo "- previous_tag: \`${PREVIOUS_TAG:-n/a}\`"
echo "- current_tag: \`${CURRENT_TAG:-n/a}\`"
echo "- current_ref: \`${CURRENT_REF:-n/a}\`"
echo "- draft_confidence: \`${DRAFT_CONFIDENCE:-n/a}\`"
echo "- missing_required_count: \`${MISSING_REQUIRED_COUNT:-n/a}\`"
echo "- validation_attempt_count: \`${VALIDATION_ATTEMPT_COUNT:-n/a}\`"
echo "- repair_attempt_count: \`${REPAIR_ATTEMPT_COUNT:-n/a}\`"
echo
echo "Download the workflow artifact \`memos-local-plugin-release-notes-inspection\` to review release-notes.md, release-notes-draft.json, and redacted evidence.json."
} >> "${GITHUB_STEP_SUMMARY}"
- name: Upload release notes inspection artifact
uses: actions/upload-artifact@v4
with:
name: memos-local-plugin-release-notes-inspection
path: ${{ runner.temp }}/memos-local-plugin-release-notes-inspection
if-no-files-found: error
- name: Stop before publish in dry run
if: ${{ inputs.dry_run == true }}
run: |
echo "::notice::dry_run=true; skipped npm publish, tag, GitHub Release, and release PR."
- name: Publish to npm
if: ${{ inputs.dry_run != true }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_NAME: "@memtensor/memos-local-plugin"
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_TAG: memos-local-plugin-v${{ inputs.version }}
NPM_DIST_TAG: ${{ inputs.tag }}
RECOVER_EXISTING_NPM_RELEASE: ${{ inputs.recover_existing_npm_release }}
DOC_AGENT_RELEASE_FAILURE_URL: ${{ secrets.DOC_AGENT_RELEASE_FAILURE_URL }}
DOC_AGENT_RELEASE_NOTES_DRAFT_TOKEN: ${{ secrets.DOC_AGENT_RELEASE_NOTES_DRAFT_TOKEN }}
run: bash ../../.github/scripts/publish-local-plugin.sh
- name: Create release tag and PR
if: ${{ inputs.dry_run != true }}
working-directory: .
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
NPM_DIST_TAG: ${{ inputs.tag }}
RELEASE_NOTES_FILE: ${{ steps.release_notes.outputs.release_notes_file }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
if [ -z "${RELEASE_NOTES_FILE}" ] || [ ! -s "${RELEASE_NOTES_FILE}" ]; then
echo "::error::Release notes file was not generated."
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
release_tag="memos-local-plugin-v${RELEASE_VERSION}"
release_branch="release/${release_tag}"
release_title="release: @memtensor/memos-local-plugin v${RELEASE_VERSION}"
remote_branch_sha() {
local branch="$1"
local out="${RUNNER_TEMP}/memos-local-plugin-remote-branch.txt"
for attempt in 1 2 3; do
set +e
git ls-remote --heads origin "${branch}" >"${out}" 2>&1
status=$?
set -e
if [ "${status}" = 0 ]; then
awk '{print $1}' "${out}"
return 0
fi
sed -n '1,120p' "${out}"
if [ "${attempt}" = 3 ]; then
echo "::error::Failed to check remote branch ${branch} after three attempts."
exit "${status}"
fi
sleep "$((attempt * 5))"
done
}
remote_tag_exists() {
local tag="$1"
for attempt in 1 2 3; do
set +e
git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1
status=$?
set -e
if [ "${status}" = 0 ]; then
return 0
fi
if [ "${status}" = 2 ]; then
return 1
fi
if [ "${attempt}" = 3 ]; then
echo "::error::Failed to check remote tag ${tag} after three attempts."
exit "${status}"
fi
sleep "$((attempt * 5))"
done
}
create_release_if_missing() {
release_flags=()
if [[ "${RELEASE_VERSION}" == *-* || "${NPM_DIST_TAG}" != "latest" ]]; then
release_flags+=(--prerelease)
echo "Marking GitHub Release ${release_tag} as prerelease because version=${RELEASE_VERSION}, npm_dist_tag=${NPM_DIST_TAG}."
fi
for attempt in 1 2 3; do
if gh release view "${release_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "GitHub Release ${release_tag} already exists; leaving it unchanged."
return 0
fi
set +e
gh release create "${release_tag}" \
--repo "${GITHUB_REPOSITORY}" \
--target "$(git rev-parse HEAD)" \
--title "MemOS Local Plugin v${RELEASE_VERSION}" \
--notes-file "${RELEASE_NOTES_FILE}" \
"${release_flags[@]}"
status=$?
set -e
if [ "${status}" = 0 ]; then
return 0
fi
if [ "${attempt}" = 3 ]; then
if gh release view "${release_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "GitHub Release ${release_tag} exists after a failed create response; treating as success."
return 0
fi
echo "::error::Failed to create GitHub Release ${release_tag} after three attempts."
exit "${status}"
fi
sleep "$((attempt * 5))"
done
}
create_pr_if_missing() {
if gh pr view "${release_branch}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "Release PR already exists for ${release_branch}."
return 0
fi
for attempt in 1 2 3; do
set +e
gh pr create \
--repo "${GITHUB_REPOSITORY}" \
--base "${DEFAULT_BRANCH}" \
--head "${release_branch}" \
--title "${release_title}" \
--body "Synchronizes package and Hermes metadata for the published npm release ${RELEASE_VERSION}."
status=$?
set -e
if [ "${status}" = 0 ]; then
return 0
fi
if gh pr view "${release_branch}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "Release PR exists after a failed create response; treating as success."
return 0
fi
if [ "${attempt}" = 3 ]; then
echo "::warning::Failed to create release PR automatically after three attempts. Open a PR from ${release_branch} to ${DEFAULT_BRANCH}."
return 0
fi
sleep "$((attempt * 5))"
done
}
git add \
apps/memos-local-plugin/package.json \
apps/memos-local-plugin/package-lock.json \
apps/memos-local-plugin/adapters/hermes/plugin.yaml
created_release_commit=false
if ! git diff --staged --quiet; then
git commit -m "${release_title}"
created_release_commit=true
fi
if [ "${created_release_commit}" = "true" ]; then
remote_branch_sha="$(remote_branch_sha "${release_branch}")"
if [ -n "${remote_branch_sha}" ]; then
if [ "${remote_branch_sha}" != "$(git rev-parse HEAD)" ]; then
echo "::error::Release branch ${release_branch} already exists at a different commit; refusing to overwrite it."
exit 1
fi
echo "Release branch ${release_branch} already points at this commit."
else
bash .github/scripts/retry.sh --label "push release branch" -- git push origin "HEAD:refs/heads/${release_branch}"
fi
fi
if remote_tag_exists "${release_tag}"; then
echo "Tag ${release_tag} already exists on origin; leaving it unchanged."
else
git tag "${release_tag}"
bash .github/scripts/retry.sh --label "push release tag" -- git push origin "refs/tags/${release_tag}"
fi
create_release_if_missing
if [ "${created_release_commit}" = "true" ]; then
create_pr_if_missing
else
echo "No version bump changes to open as a PR."
fi