Merge pull request #122 from TrueNine/dev #108
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
| name: Release Packages | |
| env: | |
| NPM_REGISTRY_URL: https://registry.npmjs.org/ | |
| NPM_PUBLISH_VERIFY_ATTEMPTS: "90" | |
| NPM_PUBLISH_VERIFY_DELAY_SECONDS: "10" | |
| CLI_NATIVE_MODULE_DIRS: | | |
| libraries/logger | |
| libraries/md-compiler | |
| libraries/script-runtime | |
| sdk | |
| CLI_NATIVE_BINDING_PREFIXES: | | |
| napi-logger. | |
| napi-md-compiler. | |
| napi-script-runtime. | |
| napi-memory-sync-cli. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - .github/actions/** | |
| - .github/workflows/build-gui-all.yml | |
| - .github/workflows/release-*.yml | |
| - assets/** | |
| - cli/** | |
| - mcp/** | |
| - gui/** | |
| - libraries/** | |
| - scripts/** | |
| - Cargo.toml | |
| - Cargo.lock | |
| - package.json | |
| - pnpm-lock.yaml | |
| - pnpm-workspace.yaml | |
| - turbo.json | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| # 1. 版本检查(快速,决定是否继续) | |
| check-version: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| publish_cli: ${{ steps.check.outputs.publish_cli }} | |
| publish_mcp: ${{ steps.check.outputs.publish_mcp }} | |
| publish_npm: ${{ steps.check.outputs.publish_npm }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| cache: "false" | |
| install: "false" | |
| - name: Check if should publish | |
| id: check | |
| uses: ./.github/actions/check-release-state | |
| with: | |
| registry-url: ${{ env.NPM_REGISTRY_URL }} | |
| # 1.5. GUI 版本检查(独立于 npm,检查 GitHub Release) | |
| check-gui-version: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| should_release: ${{ steps.check.outputs.should_release }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Check if GUI should be released | |
| id: check | |
| uses: ./.github/actions/check-gui-release-state | |
| with: | |
| github-token: ${{ github.token }} | |
| # 2. 构建 NAPI 二进制(5 平台矩阵) | |
| build-napi: | |
| needs: check-version | |
| if: needs.check-version.outputs.publish_cli == 'true' | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - os: ubuntu-24.04 | |
| rust: x86_64-unknown-linux-gnu | |
| suffix: linux-x64-gnu | |
| - os: ubuntu-24.04 | |
| rust: aarch64-unknown-linux-gnu | |
| suffix: linux-arm64-gnu | |
| cross: true | |
| - os: macos-14 | |
| rust: aarch64-apple-darwin | |
| suffix: darwin-arm64 | |
| - os: macos-14 | |
| rust: x86_64-apple-darwin | |
| suffix: darwin-x64 | |
| - os: windows-latest | |
| rust: x86_64-pc-windows-msvc | |
| suffix: win32-x64-msvc | |
| runs-on: ${{ matrix.target.os }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| - uses: ./.github/actions/setup-rust | |
| with: | |
| targets: ${{ matrix.target.rust }} | |
| cache-key: napi-${{ matrix.target.rust }} | |
| - name: Install cross-compilation tools (aarch64-linux) | |
| if: matrix.target.cross | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV | |
| - name: Build all napi native modules | |
| shell: bash | |
| run: | | |
| while IFS= read -r module_dir; do | |
| if [ -z "$module_dir" ]; then | |
| continue | |
| fi | |
| echo "Building napi in ${module_dir}..." | |
| ( | |
| cd "${module_dir}" && \ | |
| pnpm exec napi build --platform --release --target ${{ matrix.target.rust }} --output-dir dist -- --features napi | |
| ) | |
| done <<< "$CLI_NATIVE_MODULE_DIRS" | |
| - name: Collect .node files into CLI platform package | |
| shell: bash | |
| run: | | |
| target_dir="cli/npm/${{ matrix.target.suffix }}" | |
| mkdir -p "$target_dir" | |
| shopt -s nullglob | |
| while IFS= read -r module_dir; do | |
| if [ -z "$module_dir" ]; then | |
| continue | |
| fi | |
| node_files=("${module_dir}"/dist/*.node) | |
| if [ "${#node_files[@]}" -eq 0 ]; then | |
| echo "ERROR: no .node files found in ${module_dir}/dist" | |
| exit 1 | |
| fi | |
| cp "${node_files[@]}" "$target_dir/" | |
| done <<< "$CLI_NATIVE_MODULE_DIRS" | |
| expected_count=0 | |
| while IFS= read -r binding_prefix; do | |
| if [ -z "$binding_prefix" ]; then | |
| continue | |
| fi | |
| expected_count=$((expected_count + 1)) | |
| matches=("$target_dir"/${binding_prefix}*.node) | |
| if [ "${#matches[@]}" -eq 0 ]; then | |
| echo "ERROR: missing binding with prefix ${binding_prefix} in ${target_dir}" | |
| exit 1 | |
| fi | |
| done <<< "$CLI_NATIVE_BINDING_PREFIXES" | |
| actual_count=$(find "$target_dir" -maxdepth 1 -type f -name '*.node' | wc -l | tr -d ' ') | |
| if [ "$actual_count" -ne "$expected_count" ]; then | |
| echo "ERROR: expected ${expected_count} .node files in ${target_dir}, found ${actual_count}" | |
| exit 1 | |
| fi | |
| echo "Contents of $target_dir:" | |
| ls -la "$target_dir/" | |
| - name: Upload CLI platform package | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: cli-napi-${{ matrix.target.suffix }} | |
| path: cli/npm/${{ matrix.target.suffix }}/ | |
| if-no-files-found: error | |
| # 3. 收集并发布 NAPI 平台子包到 npm | |
| publish-napi: | |
| needs: [check-version, build-napi] | |
| if: needs.check-version.outputs.publish_cli == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| install: "true" | |
| registry-url: https://registry.npmjs.org/ | |
| - name: Preflight npm auth | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/* before rerunning release." | |
| exit 1 | |
| fi | |
| pushd cli >/dev/null | |
| npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}" | |
| npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL") | |
| access_json=$(npm access list packages @truenine --json 2>/dev/null || true) | |
| popd >/dev/null | |
| echo "Authenticated to npm as ${npm_user}" | |
| if [[ -z "${access_json}" || "${access_json}" == "{}" || "${access_json}" == "null" ]]; then | |
| echo "::error::Authenticated as ${npm_user}, but npm did not report package access for @truenine. Replace NPM_TOKEN with a token that has publish permission for existing @truenine/* packages." | |
| exit 1 | |
| fi | |
| if ! jq -e . >/dev/null 2>&1 <<<"$access_json"; then | |
| echo "::warning::npm access list packages returned non-JSON output for ${npm_user}. Falling back to publish-time authorization checks." | |
| exit 0 | |
| fi | |
| for package_json in cli/npm/*/package.json; do | |
| package_name=$(jq -r '.name' "$package_json") | |
| package_access=$(jq -r --arg package_name "$package_name" '.[$package_name] // empty' <<<"$access_json") | |
| if [[ "$package_access" != "read-write" ]]; then | |
| echo "::error::NPM_TOKEN authenticated as ${npm_user}, but ${package_name} access is '${package_access:-missing}'. Expected read-write." | |
| exit 1 | |
| fi | |
| done | |
| - name: Download all platform artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| pattern: cli-napi-* | |
| - name: Distribute artifacts to cli/npm/ directories | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| for artifact_dir in artifacts/cli-napi-*/; do | |
| suffix=$(basename "$artifact_dir" | sed 's/cli-napi-//') | |
| target_dir="cli/npm/${suffix}" | |
| mkdir -p "$target_dir" | |
| echo "Copying from ${artifact_dir} to ${target_dir}" | |
| cp "${artifact_dir}"*.node "$target_dir/" || { echo "ERROR: no .node files found in ${artifact_dir}"; exit 1; } | |
| done | |
| - name: Generate CLI platform package shims | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| dirs=(cli/npm/*/) | |
| if [ "${#dirs[@]}" -eq 0 ]; then | |
| echo "No CLI platform package directories found" | |
| exit 0 | |
| fi | |
| pnpm exec tsx scripts/write-platform-package-shims.ts "${dirs[@]}" | |
| - name: Validate CLI platform packages | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| expected_count=0 | |
| while IFS= read -r binding_prefix; do | |
| if [ -z "$binding_prefix" ]; then | |
| continue | |
| fi | |
| expected_count=$((expected_count + 1)) | |
| done <<< "$CLI_NATIVE_BINDING_PREFIXES" | |
| for target_dir in cli/npm/*/; do | |
| if [ ! -f "${target_dir}package.json" ]; then | |
| continue | |
| fi | |
| if [ ! -f "${target_dir}noop.mjs" ]; then | |
| echo "ERROR: missing ${target_dir}noop.mjs" | |
| exit 1 | |
| fi | |
| if [ ! -f "${target_dir}noop.d.mts" ]; then | |
| echo "ERROR: missing ${target_dir}noop.d.mts" | |
| exit 1 | |
| fi | |
| actual_count=$(find "${target_dir}" -maxdepth 1 -type f -name '*.node' | wc -l | tr -d ' ') | |
| if [ "$actual_count" -ne "$expected_count" ]; then | |
| echo "ERROR: expected ${expected_count} .node files in ${target_dir}, found ${actual_count}" | |
| exit 1 | |
| fi | |
| while IFS= read -r binding_prefix; do | |
| if [ -z "$binding_prefix" ]; then | |
| continue | |
| fi | |
| matches=("${target_dir}"${binding_prefix}*.node) | |
| if [ "${#matches[@]}" -eq 0 ]; then | |
| echo "ERROR: missing binding with prefix ${binding_prefix} in ${target_dir}" | |
| exit 1 | |
| fi | |
| done <<< "$CLI_NATIVE_BINDING_PREFIXES" | |
| done | |
| - name: Publish CLI platform sub-packages | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| registry_version_exists() { | |
| local package_name="$1" | |
| local package_version="$2" | |
| local encoded_package_name | |
| local version_json | |
| local published_version | |
| encoded_package_name=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$package_name") | |
| version_json=$(curl --silent --show-error --fail "${NPM_REGISTRY_URL%/}/${encoded_package_name}/${package_version}" 2>/dev/null || true) | |
| if [[ -z "$version_json" ]]; then | |
| return 1 | |
| fi | |
| published_version=$(jq -r '.version // empty' <<<"$version_json") | |
| [[ "$published_version" == "$package_version" ]] | |
| } | |
| version_exists() { | |
| local package_name="$1" | |
| local package_version="$2" | |
| local published_version | |
| published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true) | |
| if [[ "$published_version" == "$package_version" ]]; then | |
| return 0 | |
| fi | |
| registry_version_exists "$package_name" "$package_version" | |
| } | |
| verify_version_exists() { | |
| local package_name="$1" | |
| local package_version="$2" | |
| local attempts="${NPM_PUBLISH_VERIFY_ATTEMPTS}" | |
| local delay_seconds="${NPM_PUBLISH_VERIFY_DELAY_SECONDS}" | |
| for attempt in $(seq 1 "$attempts"); do | |
| if version_exists "$package_name" "$package_version"; then | |
| echo "Verified ${package_name}@${package_version} on npm" | |
| return 0 | |
| fi | |
| if [[ "$attempt" -eq "$attempts" ]]; then | |
| break | |
| fi | |
| echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..." | |
| sleep "$delay_seconds" | |
| done | |
| echo "::error::${package_name}@${package_version} is still missing from npm after publish." | |
| return 1 | |
| } | |
| publish_package() { | |
| local package_dir="$1" | |
| local package_name | |
| local package_version | |
| local publish_log | |
| package_name=$(jq -r '.name' "${package_dir}package.json") | |
| package_version=$(jq -r '.version' "${package_dir}package.json") | |
| if version_exists "$package_name" "$package_version"; then | |
| echo "${package_name}@${package_version} already exists on npm, skipping" | |
| return 0 | |
| fi | |
| publish_log=$(mktemp) | |
| if (cd "$package_dir" && pnpm publish --access public --no-git-checks) 2>&1 | tee "$publish_log"; then | |
| verify_version_exists "$package_name" "$package_version" | |
| rm -f "$publish_log" | |
| return 0 | |
| fi | |
| if grep -Eiq 'cannot publish over the previously published versions|previously published versions' "$publish_log"; then | |
| echo "${package_name}@${package_version} was already published according to npm, skipping" | |
| rm -f "$publish_log" | |
| return 0 | |
| fi | |
| if version_exists "$package_name" "$package_version"; then | |
| echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping" | |
| rm -f "$publish_log" | |
| return 0 | |
| fi | |
| echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm." | |
| rm -f "$publish_log" | |
| return 1 | |
| } | |
| for dir in cli/npm/*/; do | |
| if [ -f "${dir}package.json" ]; then | |
| publish_package "$dir" | |
| fi | |
| done | |
| # 4. 架构包就绪后,发布主包到 npm | |
| publish-cli: | |
| needs: [check-version, publish-napi] | |
| if: needs.check-version.outputs.publish_cli == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| registry-url: https://registry.npmjs.org/ | |
| - name: Preflight npm auth | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/memory-sync-cli before rerunning release." | |
| exit 1 | |
| fi | |
| pushd cli >/dev/null | |
| npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}" | |
| npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL") | |
| package_name=$(jq -r '.name' package.json) | |
| popd >/dev/null | |
| echo "Authenticated to npm as ${npm_user}" | |
| echo "Deferring publish permission enforcement for ${package_name} to the publish step because npm access output is not stable in this workflow." | |
| - name: Build | |
| run: pnpm -F @truenine/memory-sync-cli run build | |
| - name: Publish to npm | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| package_name=$(jq -r '.name' cli/package.json) | |
| package_version=$(jq -r '.version' cli/package.json) | |
| registry_version_exists() { | |
| local encoded_package_name | |
| local version_json | |
| local published_version | |
| encoded_package_name=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$package_name") | |
| version_json=$(curl --silent --show-error --fail "${NPM_REGISTRY_URL%/}/${encoded_package_name}/${package_version}" 2>/dev/null || true) | |
| if [[ -z "$version_json" ]]; then | |
| return 1 | |
| fi | |
| published_version=$(jq -r '.version // empty' <<<"$version_json") | |
| [[ "$published_version" == "$package_version" ]] | |
| } | |
| version_exists() { | |
| local published_version | |
| published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true) | |
| if [[ "$published_version" == "$package_version" ]]; then | |
| return 0 | |
| fi | |
| registry_version_exists | |
| } | |
| verify_version_exists() { | |
| local attempts="${NPM_PUBLISH_VERIFY_ATTEMPTS}" | |
| local delay_seconds="${NPM_PUBLISH_VERIFY_DELAY_SECONDS}" | |
| for attempt in $(seq 1 "$attempts"); do | |
| if version_exists; then | |
| echo "Verified ${package_name}@${package_version} on npm" | |
| return 0 | |
| fi | |
| if [[ "$attempt" -eq "$attempts" ]]; then | |
| break | |
| fi | |
| echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..." | |
| sleep "$delay_seconds" | |
| done | |
| echo "::error::${package_name}@${package_version} is still missing from npm after publish." | |
| return 1 | |
| } | |
| if version_exists; then | |
| echo "${package_name}@${package_version} already exists on npm, skipping" | |
| exit 0 | |
| fi | |
| publish_log=$(mktemp) | |
| if (cd cli && pnpm publish --access public --no-git-checks) 2>&1 | tee "$publish_log"; then | |
| verify_version_exists | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| if grep -Eiq 'cannot publish over the previously published versions|previously published versions' "$publish_log"; then | |
| echo "${package_name}@${package_version} was already published according to npm, skipping" | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| if version_exists; then | |
| echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping" | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm." | |
| rm -f "$publish_log" | |
| exit 1 | |
| # 4.5. CLI 可用后,发布 MCP 包到 npm | |
| publish-mcp: | |
| needs: [check-version, publish-cli] | |
| if: | | |
| needs.check-version.outputs.publish_mcp == 'true' && | |
| (needs.publish-cli.result == 'success' || needs.publish-cli.result == 'skipped') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| registry-url: https://registry.npmjs.org/ | |
| - name: Preflight npm auth | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/memory-sync-mcp before rerunning release." | |
| exit 1 | |
| fi | |
| pushd mcp >/dev/null | |
| npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}" | |
| npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL") | |
| package_name=$(jq -r '.name' package.json) | |
| popd >/dev/null | |
| echo "Authenticated to npm as ${npm_user}" | |
| echo "Deferring publish permission enforcement for ${package_name} to the publish step because npm access output is not stable in this workflow." | |
| - name: Build | |
| run: pnpm exec turbo run build --filter=@truenine/memory-sync-mcp | |
| - name: Publish to npm | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| package_name=$(jq -r '.name' mcp/package.json) | |
| package_version=$(jq -r '.version' mcp/package.json) | |
| registry_version_exists() { | |
| local encoded_package_name | |
| local version_json | |
| local published_version | |
| encoded_package_name=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$package_name") | |
| version_json=$(curl --silent --show-error --fail "${NPM_REGISTRY_URL%/}/${encoded_package_name}/${package_version}" 2>/dev/null || true) | |
| if [[ -z "$version_json" ]]; then | |
| return 1 | |
| fi | |
| published_version=$(jq -r '.version // empty' <<<"$version_json") | |
| [[ "$published_version" == "$package_version" ]] | |
| } | |
| version_exists() { | |
| local published_version | |
| published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true) | |
| if [[ "$published_version" == "$package_version" ]]; then | |
| return 0 | |
| fi | |
| registry_version_exists | |
| } | |
| verify_version_exists() { | |
| local attempts="${NPM_PUBLISH_VERIFY_ATTEMPTS}" | |
| local delay_seconds="${NPM_PUBLISH_VERIFY_DELAY_SECONDS}" | |
| for attempt in $(seq 1 "$attempts"); do | |
| if version_exists; then | |
| echo "Verified ${package_name}@${package_version} on npm" | |
| return 0 | |
| fi | |
| if [[ "$attempt" -eq "$attempts" ]]; then | |
| break | |
| fi | |
| echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..." | |
| sleep "$delay_seconds" | |
| done | |
| echo "::error::${package_name}@${package_version} is still missing from npm after publish." | |
| return 1 | |
| } | |
| if version_exists; then | |
| echo "${package_name}@${package_version} already exists on npm, skipping" | |
| exit 0 | |
| fi | |
| publish_log=$(mktemp) | |
| if (cd mcp && pnpm publish --access public --no-git-checks) 2>&1 | tee "$publish_log"; then | |
| verify_version_exists | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| if grep -Eiq 'cannot publish over the previously published versions|previously published versions' "$publish_log"; then | |
| echo "${package_name}@${package_version} was already published according to npm, skipping" | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| if version_exists; then | |
| echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping" | |
| rm -f "$publish_log" | |
| exit 0 | |
| fi | |
| echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm." | |
| rm -f "$publish_log" | |
| exit 1 | |
| # 5. 构建 CLI 独立二进制(仅 artifact,不发 Release) | |
| build-binary: | |
| needs: [check-version, check-gui-version, publish-napi] | |
| if: | | |
| (needs.check-version.outputs.publish_cli == 'true' || needs.check-gui-version.outputs.should_release == 'true') && | |
| (needs.publish-napi.result == 'success' || needs.publish-napi.result == 'skipped') | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: ubuntu-24.04 | |
| target: x86_64-unknown-linux-gnu | |
| binary: tnmsc | |
| archive: tnmsc-linux-x86_64.tar.gz | |
| - platform: ubuntu-24.04 | |
| target: aarch64-unknown-linux-gnu | |
| binary: tnmsc | |
| archive: tnmsc-linux-aarch64.tar.gz | |
| cross: true | |
| - platform: macos-14 | |
| target: aarch64-apple-darwin | |
| binary: tnmsc | |
| archive: tnmsc-darwin-aarch64.tar.gz | |
| - platform: macos-14 | |
| target: x86_64-apple-darwin | |
| binary: tnmsc | |
| archive: tnmsc-darwin-x86_64.tar.gz | |
| - platform: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| binary: tnmsc.exe | |
| archive: tnmsc-windows-x86_64.zip | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup-node-pnpm | |
| - name: Build plugin-runtime | |
| shell: bash | |
| run: | | |
| pnpm -F @truenine/memory-sync-cli run build | |
| ls -la cli/dist/plugin-runtime.mjs | |
| - uses: ./.github/actions/setup-rust | |
| with: | |
| targets: ${{ matrix.target }} | |
| cache-key: cli-${{ matrix.target }} | |
| - name: Install cross-compilation tools (aarch64-linux) | |
| if: matrix.cross | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV | |
| - name: Build tnmsc binary (release, with embedded runtime) | |
| run: cargo build --release --target ${{ matrix.target }} -p tnmsc-cli-shell --features tnmsc/embedded-runtime | |
| - name: Run tests (native only) | |
| if: ${{ !matrix.cross }} | |
| run: cargo test --release --target ${{ matrix.target }} -p tnmsc-cli-shell --features tnmsc/embedded-runtime | |
| - name: Package (unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| mkdir -p staging | |
| cp target/${{ matrix.target }}/release/${{ matrix.binary }} staging/ | |
| cp cli/dist/plugin-runtime.mjs staging/ | |
| cd staging | |
| tar czf ../${{ matrix.archive }} * | |
| - name: Package (windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path staging | |
| Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary }}" staging/ | |
| Copy-Item "cli/dist/plugin-runtime.mjs" staging/ | |
| Compress-Archive -Path staging/* -DestinationPath ${{ matrix.archive }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: cli-${{ matrix.target }} | |
| path: ${{ matrix.archive }} | |
| if-no-files-found: error | |
| # 6. 构建 GUI — 三平台并行(fail-fast: 任一失败则全部取消) | |
| build-gui-all: | |
| needs: [check-gui-version] | |
| if: needs.check-gui-version.outputs.should_release == 'true' | |
| uses: ./.github/workflows/build-gui-all.yml | |
| with: | |
| version: ${{ needs.check-gui-version.outputs.version }} | |
| secrets: inherit | |
| # 7. 收集三平台产物,创建 GitHub Release + tag | |
| release-gui-collect: | |
| needs: [check-gui-version, build-gui-all, build-binary] | |
| if: needs.check-gui-version.outputs.should_release == 'true' | |
| permissions: | |
| contents: write | |
| uses: ./.github/workflows/release-gui-collect.yml | |
| with: | |
| version: ${{ needs.check-gui-version.outputs.version }} | |
| secrets: inherit |