Skip to content

Commit 85145cf

Browse files
committed
ci: harden npm release publish checks
1 parent 8f50e9e commit 85145cf

File tree

1 file changed

+243
-9
lines changed

1 file changed

+243
-9
lines changed

.github/workflows/release-cli.yml

Lines changed: 243 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Release Packages
22

33
env:
44
NODE_VERSION: '25'
5+
NPM_REGISTRY_URL: https://registry.npmjs.org/
56
CLI_NATIVE_MODULE_DIRS: |
67
libraries/logger
78
libraries/md-compiler
@@ -66,13 +67,13 @@ jobs:
6667
local output_key="$2"
6768
local version
6869
local name
69-
local npm_version
70+
local published_version
7071
7172
version=$(jq -r '.version' "$package_json_path")
7273
name=$(jq -r '.name' "$package_json_path")
73-
npm_version=$(npm view "$name" version --registry https://registry.npmjs.org/ 2>/dev/null || echo "")
74+
published_version=$(npm view "${name}@${version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || echo "")
7475
75-
if [[ "$version" != "$npm_version" ]]; then
76+
if [[ "$version" != "$published_version" ]]; then
7677
echo "$name@$version is not published to npm, will publish"
7778
echo "${output_key}=true" >> "$GITHUB_OUTPUT"
7879
return 0
@@ -242,6 +243,37 @@ jobs:
242243
with:
243244
node-version: ${{ env.NODE_VERSION }}
244245
registry-url: https://registry.npmjs.org/
246+
- name: Preflight npm auth
247+
shell: bash
248+
env:
249+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
250+
run: |
251+
set -euo pipefail
252+
253+
if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then
254+
echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/* before rerunning release."
255+
exit 1
256+
fi
257+
258+
npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}"
259+
npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL")
260+
echo "Authenticated to npm as ${npm_user}"
261+
262+
access_json=$(npm access list packages @truenine --json 2>/dev/null || true)
263+
if [[ -z "${access_json}" || "${access_json}" == "{}" || "${access_json}" == "null" ]]; then
264+
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."
265+
exit 1
266+
fi
267+
268+
for package_json in cli/npm/*/package.json; do
269+
package_name=$(jq -r '.name' "$package_json")
270+
package_access=$(jq -r --arg package_name "$package_name" '.[$package_name] // empty' <<<"$access_json")
271+
272+
if [[ "$package_access" != "read-write" ]]; then
273+
echo "::error::NPM_TOKEN authenticated as ${npm_user}, but ${package_name} access is '${package_access:-missing}'. Expected read-write."
274+
exit 1
275+
fi
276+
done
245277
- name: Download all platform artifacts
246278
uses: actions/download-artifact@v4
247279
with:
@@ -302,10 +334,72 @@ jobs:
302334
env:
303335
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
304336
run: |
337+
set -euo pipefail
338+
339+
version_exists() {
340+
local package_name="$1"
341+
local package_version="$2"
342+
local published_version
343+
344+
published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true)
345+
[[ "$published_version" == "$package_version" ]]
346+
}
347+
348+
verify_version_exists() {
349+
local package_name="$1"
350+
local package_version="$2"
351+
local attempts=10
352+
local delay_seconds=6
353+
354+
for attempt in $(seq 1 "$attempts"); do
355+
if version_exists "$package_name" "$package_version"; then
356+
echo "Verified ${package_name}@${package_version} on npm"
357+
return 0
358+
fi
359+
360+
echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..."
361+
sleep "$delay_seconds"
362+
done
363+
364+
echo "::error::${package_name}@${package_version} is still missing from npm after publish."
365+
return 1
366+
}
367+
368+
publish_package() {
369+
local package_dir="$1"
370+
local package_name
371+
local package_version
372+
local publish_log
373+
374+
package_name=$(jq -r '.name' "${package_dir}package.json")
375+
package_version=$(jq -r '.version' "${package_dir}package.json")
376+
377+
if version_exists "$package_name" "$package_version"; then
378+
echo "${package_name}@${package_version} already exists on npm, skipping"
379+
return 0
380+
fi
381+
382+
publish_log=$(mktemp)
383+
if (cd "$package_dir" && pnpm publish --access public --no-git-checks) 2>&1 | tee "$publish_log"; then
384+
verify_version_exists "$package_name" "$package_version"
385+
rm -f "$publish_log"
386+
return 0
387+
fi
388+
389+
if version_exists "$package_name" "$package_version"; then
390+
echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping"
391+
rm -f "$publish_log"
392+
return 0
393+
fi
394+
395+
echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm."
396+
rm -f "$publish_log"
397+
return 1
398+
}
399+
305400
for dir in cli/npm/*/; do
306401
if [ -f "${dir}package.json" ]; then
307-
echo "Publishing ${dir}..."
308-
(cd "$dir" && pnpm publish --access public --no-git-checks) || echo "⚠️ Failed to publish ${dir}, may already exist"
402+
publish_package "$dir"
309403
fi
310404
done
311405
@@ -323,13 +417,83 @@ jobs:
323417
with:
324418
node-version: ${{ env.NODE_VERSION }}
325419
registry-url: https://registry.npmjs.org/
420+
- name: Preflight npm auth
421+
shell: bash
422+
env:
423+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
424+
run: |
425+
set -euo pipefail
426+
427+
if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then
428+
echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/memory-sync-cli before rerunning release."
429+
exit 1
430+
fi
431+
432+
npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}"
433+
npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL")
434+
echo "Authenticated to npm as ${npm_user}"
435+
436+
access_json=$(npm access list packages @truenine --json 2>/dev/null || true)
437+
package_name=$(jq -r '.name' cli/package.json)
438+
package_access=$(jq -r --arg package_name "$package_name" '.[$package_name] // empty' <<<"${access_json:-{}}")
439+
440+
if [[ "$package_access" != "read-write" ]]; then
441+
echo "::error::NPM_TOKEN authenticated as ${npm_user}, but ${package_name} access is '${package_access:-missing}'. Expected read-write."
442+
exit 1
443+
fi
326444
- name: Build
327445
run: pnpm -F @truenine/memory-sync-cli run build
328446
- name: Publish to npm
329-
working-directory: ./cli
330-
run: pnpm publish --access public --no-git-checks
447+
shell: bash
331448
env:
332449
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
450+
run: |
451+
set -euo pipefail
452+
453+
package_name=$(jq -r '.name' cli/package.json)
454+
package_version=$(jq -r '.version' cli/package.json)
455+
456+
version_exists() {
457+
local published_version
458+
published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true)
459+
[[ "$published_version" == "$package_version" ]]
460+
}
461+
462+
verify_version_exists() {
463+
local attempts=10
464+
local delay_seconds=6
465+
466+
for attempt in $(seq 1 "$attempts"); do
467+
if version_exists; then
468+
echo "Verified ${package_name}@${package_version} on npm"
469+
return 0
470+
fi
471+
472+
echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..."
473+
sleep "$delay_seconds"
474+
done
475+
476+
echo "::error::${package_name}@${package_version} is still missing from npm after publish."
477+
return 1
478+
}
479+
480+
if version_exists; then
481+
echo "${package_name}@${package_version} already exists on npm, skipping"
482+
exit 0
483+
fi
484+
485+
if (cd cli && pnpm publish --access public --no-git-checks); then
486+
verify_version_exists
487+
exit 0
488+
fi
489+
490+
if version_exists; then
491+
echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping"
492+
exit 0
493+
fi
494+
495+
echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm."
496+
exit 1
333497
334498
# 4.5. CLI 可用后,发布 MCP 包到 npm
335499
publish-mcp:
@@ -347,13 +511,83 @@ jobs:
347511
with:
348512
node-version: ${{ env.NODE_VERSION }}
349513
registry-url: https://registry.npmjs.org/
514+
- name: Preflight npm auth
515+
shell: bash
516+
env:
517+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
518+
run: |
519+
set -euo pipefail
520+
521+
if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then
522+
echo "::error::NPM_TOKEN is missing. Configure a publish-capable npm token for @truenine/memory-sync-mcp before rerunning release."
523+
exit 1
524+
fi
525+
526+
npm config set //registry.npmjs.org/:_authToken "${NODE_AUTH_TOKEN}"
527+
npm_user=$(npm whoami --registry "$NPM_REGISTRY_URL")
528+
echo "Authenticated to npm as ${npm_user}"
529+
530+
access_json=$(npm access list packages @truenine --json 2>/dev/null || true)
531+
package_name=$(jq -r '.name' mcp/package.json)
532+
package_access=$(jq -r --arg package_name "$package_name" '.[$package_name] // empty' <<<"${access_json:-{}}")
533+
534+
if [[ "$package_access" != "read-write" ]]; then
535+
echo "::error::NPM_TOKEN authenticated as ${npm_user}, but ${package_name} access is '${package_access:-missing}'. Expected read-write."
536+
exit 1
537+
fi
350538
- name: Build
351539
run: pnpm exec turbo run build --filter=@truenine/memory-sync-mcp
352540
- name: Publish to npm
353-
working-directory: ./mcp
354-
run: pnpm publish --access public --no-git-checks
541+
shell: bash
355542
env:
356543
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
544+
run: |
545+
set -euo pipefail
546+
547+
package_name=$(jq -r '.name' mcp/package.json)
548+
package_version=$(jq -r '.version' mcp/package.json)
549+
550+
version_exists() {
551+
local published_version
552+
published_version=$(npm view "${package_name}@${package_version}" version --registry "$NPM_REGISTRY_URL" 2>/dev/null || true)
553+
[[ "$published_version" == "$package_version" ]]
554+
}
555+
556+
verify_version_exists() {
557+
local attempts=10
558+
local delay_seconds=6
559+
560+
for attempt in $(seq 1 "$attempts"); do
561+
if version_exists; then
562+
echo "Verified ${package_name}@${package_version} on npm"
563+
return 0
564+
fi
565+
566+
echo "Waiting for ${package_name}@${package_version} to appear on npm (${attempt}/${attempts})..."
567+
sleep "$delay_seconds"
568+
done
569+
570+
echo "::error::${package_name}@${package_version} is still missing from npm after publish."
571+
return 1
572+
}
573+
574+
if version_exists; then
575+
echo "${package_name}@${package_version} already exists on npm, skipping"
576+
exit 0
577+
fi
578+
579+
if (cd mcp && pnpm publish --access public --no-git-checks); then
580+
verify_version_exists
581+
exit 0
582+
fi
583+
584+
if version_exists; then
585+
echo "${package_name}@${package_version} already exists on npm after publish attempt, skipping"
586+
exit 0
587+
fi
588+
589+
echo "::error::Failed to publish ${package_name}@${package_version}. Exact version is still missing from npm."
590+
exit 1
357591
358592
# 5. 构建 CLI 独立二进制(仅 artifact,不发 Release)
359593
build-binary:

0 commit comments

Comments
 (0)