diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 06970dce713..af4a35e84e6 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -232,5 +232,7 @@ jobs: - name: Publish env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} OPENCLAW_PREPACK_PREPARED: "1" run: bash scripts/openclaw-npm-publish.sh --publish "${{ steps.publish_tarball.outputs.path }}" diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index 3507a0b68a1..77470923de3 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -211,4 +211,7 @@ jobs: fi - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}" diff --git a/scripts/lib/npm-publish-plan.mjs b/scripts/lib/npm-publish-plan.mjs index cd2503fd3e2..81ad807465e 100644 --- a/scripts/lib/npm-publish-plan.mjs +++ b/scripts/lib/npm-publish-plan.mjs @@ -24,6 +24,12 @@ const CORRECTION_VERSION_REGEX = * @property {("latest" | "beta")[]} mirrorDistTags */ +/** + * @typedef {object} NpmDistTagMirrorAuth + * @property {boolean} hasAuth + * @property {"node-auth-token" | "npm-token" | "none"} source + */ + /** * @param {string} version * @param {Record} groups @@ -174,3 +180,24 @@ export function resolveNpmPublishPlan(version, currentBetaVersion) { mirrorDistTags: ["beta"], }; } + +/** + * @param {{ + * nodeAuthToken?: string | null | undefined; + * npmToken?: string | null | undefined; + * }} [params] + * @returns {NpmDistTagMirrorAuth} + */ +export function resolveNpmDistTagMirrorAuth(params = {}) { + const nodeAuthToken = params.nodeAuthToken?.trim(); + if (nodeAuthToken) { + return { hasAuth: true, source: "node-auth-token" }; + } + + const npmToken = params.npmToken?.trim(); + if (npmToken) { + return { hasAuth: true, source: "npm-token" }; + } + + return { hasAuth: false, source: "none" }; +} diff --git a/scripts/openclaw-npm-publish.sh b/scripts/openclaw-npm-publish.sh index 79a95fa437b..b1b2876cf43 100644 --- a/scripts/openclaw-npm-publish.sh +++ b/scripts/openclaw-npm-publish.sh @@ -14,21 +14,27 @@ package_version="$(node -p "require('./package.json').version")" current_beta_version="$(npm view openclaw dist-tags.beta 2>/dev/null || true)" mapfile -t publish_plan < <( PACKAGE_VERSION="${package_version}" CURRENT_BETA_VERSION="${current_beta_version}" node --import tsx --input-type=module <<'EOF' -import { resolveNpmPublishPlan } from "./scripts/openclaw-npm-release-check.ts"; +import { + resolveNpmDistTagMirrorAuth, + resolveNpmPublishPlan, +} from "./scripts/openclaw-npm-release-check.ts"; const plan = resolveNpmPublishPlan( process.env.PACKAGE_VERSION ?? "", process.env.CURRENT_BETA_VERSION, ); +const auth = resolveNpmDistTagMirrorAuth(); console.log(plan.channel); console.log(plan.publishTag); console.log(plan.mirrorDistTags.join(",")); +console.log(auth.source); EOF ) release_channel="${publish_plan[0]}" publish_tag="${publish_plan[1]}" mirror_dist_tags_csv="${publish_plan[2]:-}" +mirror_auth_source="${publish_plan[3]:-none}" publish_cmd=(npm publish) if [[ -n "${publish_target}" ]]; then publish_cmd+=("${publish_target}") @@ -41,10 +47,27 @@ echo "Resolved release channel: ${release_channel}" echo "Resolved publish tag: ${publish_tag}" echo "Resolved mirror dist-tags: ${mirror_dist_tags_csv:-}" echo "Publish auth: GitHub OIDC trusted publishing" +echo "Mirror dist-tag auth source: ${mirror_auth_source}" if [[ -n "${publish_target}" ]]; then echo "Resolved publish target: ${publish_target}" fi +mirror_auth_token="" +case "${mirror_auth_source}" in + node-auth-token) + mirror_auth_token="${NODE_AUTH_TOKEN:-}" + ;; + npm-token) + mirror_auth_token="${NPM_TOKEN:-}" + ;; +esac + +if [[ -n "${mirror_dist_tags_csv}" && -z "${mirror_auth_token}" ]]; then + echo "npm dist-tag mirroring requires explicit npm auth via NODE_AUTH_TOKEN or NPM_TOKEN." >&2 + echo "Refusing publish before npm latest/beta promotion can diverge." >&2 + exit 1 +fi + printf 'Publish command:' printf ' %q' "${publish_cmd[@]}" printf '\n' @@ -52,10 +75,16 @@ printf '\n' "${publish_cmd[@]}" if [[ -n "${mirror_dist_tags_csv}" ]]; then + mirror_userconfig="$(mktemp)" + trap 'rm -f "${mirror_userconfig}"' EXIT + chmod 0600 "${mirror_userconfig}" + printf '%s\n' "//registry.npmjs.org/:_authToken=${mirror_auth_token}" > "${mirror_userconfig}" + IFS=',' read -r -a mirror_dist_tags <<< "${mirror_dist_tags_csv}" for dist_tag in "${mirror_dist_tags[@]}"; do [[ -n "${dist_tag}" ]] || continue echo "Mirroring openclaw@${package_version} onto dist-tag ${dist_tag}" - npm dist-tag add "openclaw@${package_version}" "${dist_tag}" + NPM_CONFIG_USERCONFIG="${mirror_userconfig}" \ + npm dist-tag add "openclaw@${package_version}" "${dist_tag}" done fi diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 99195d043a2..0da9da36214 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -6,6 +6,7 @@ import { basename } from "node:path"; import { pathToFileURL } from "node:url"; import { compareReleaseVersions as compareReleaseVersionsBase, + resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase, parseReleaseVersion as parseReleaseVersionBase, resolveNpmPublishPlan as resolveNpmPublishPlanBase, } from "./lib/npm-publish-plan.mjs"; @@ -47,6 +48,11 @@ export type NpmPublishPlan = { publishTag: "latest" | "beta"; mirrorDistTags: ("latest" | "beta")[]; }; + +export type NpmDistTagMirrorAuth = { + hasAuth: boolean; + source: "node-auth-token" | "npm-token" | "none"; +}; const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw"; const MAX_CALVER_DISTANCE_DAYS = 2; const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"]; @@ -80,6 +86,16 @@ export function resolveNpmPublishPlan( return resolveNpmPublishPlanBase(version, currentBetaVersion) as NpmPublishPlan; } +export function resolveNpmDistTagMirrorAuth(params?: { + nodeAuthToken?: string | null; + npmToken?: string | null; +}): NpmDistTagMirrorAuth { + return resolveNpmDistTagMirrorAuthBase({ + nodeAuthToken: params?.nodeAuthToken ?? process.env.NODE_AUTH_TOKEN, + npmToken: params?.npmToken ?? process.env.NPM_TOKEN, + }) as NpmDistTagMirrorAuth; +} + export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null { const trimmed = version.trim(); if (!trimmed) { diff --git a/scripts/plugin-npm-publish.sh b/scripts/plugin-npm-publish.sh index 521aa08ae68..b2c8e92c81b 100644 --- a/scripts/plugin-npm-publish.sh +++ b/scripts/plugin-npm-publish.sh @@ -20,21 +20,30 @@ package_version="$(node -e 'const pkg = require(require("node:path").resolve(pro current_beta_version="$(npm view "${package_name}" dist-tags.beta 2>/dev/null || true)" mapfile -t publish_plan < <( PACKAGE_VERSION="${package_version}" CURRENT_BETA_VERSION="${current_beta_version}" node --input-type=module <<'EOF' -import { resolveNpmPublishPlan } from "./scripts/lib/npm-publish-plan.mjs"; +import { + resolveNpmDistTagMirrorAuth, + resolveNpmPublishPlan, +} from "./scripts/lib/npm-publish-plan.mjs"; const plan = resolveNpmPublishPlan( process.env.PACKAGE_VERSION ?? "", process.env.CURRENT_BETA_VERSION, ); +const auth = resolveNpmDistTagMirrorAuth({ + nodeAuthToken: process.env.NODE_AUTH_TOKEN, + npmToken: process.env.NPM_TOKEN, +}); console.log(plan.channel); console.log(plan.publishTag); console.log(plan.mirrorDistTags.join(",")); +console.log(auth.source); EOF ) release_channel="${publish_plan[0]}" publish_tag="${publish_plan[1]}" mirror_dist_tags_csv="${publish_plan[2]:-}" +mirror_auth_source="${publish_plan[3]:-none}" publish_cmd=(npm publish --access public --tag "${publish_tag}" --provenance) echo "Resolved package dir: ${package_dir}" @@ -45,6 +54,23 @@ echo "Resolved release channel: ${release_channel}" echo "Resolved publish tag: ${publish_tag}" echo "Resolved mirror dist-tags: ${mirror_dist_tags_csv:-}" echo "Publish auth: GitHub OIDC trusted publishing" +echo "Mirror dist-tag auth source: ${mirror_auth_source}" + +mirror_auth_token="" +case "${mirror_auth_source}" in + node-auth-token) + mirror_auth_token="${NODE_AUTH_TOKEN:-}" + ;; + npm-token) + mirror_auth_token="${NPM_TOKEN:-}" + ;; +esac + +if [[ -n "${mirror_dist_tags_csv}" && -z "${mirror_auth_token}" ]]; then + echo "npm dist-tag mirroring requires explicit npm auth via NODE_AUTH_TOKEN or NPM_TOKEN." >&2 + echo "Refusing publish before npm latest/beta promotion can diverge." >&2 + exit 1 +fi printf 'Publish command:' printf ' %q' "${publish_cmd[@]}" @@ -59,11 +85,17 @@ fi "${publish_cmd[@]}" if [[ -n "${mirror_dist_tags_csv}" ]]; then + mirror_userconfig="$(mktemp)" + trap 'rm -f "${mirror_userconfig}"' EXIT + chmod 0600 "${mirror_userconfig}" + printf '%s\n' "//registry.npmjs.org/:_authToken=${mirror_auth_token}" > "${mirror_userconfig}" + IFS=',' read -r -a mirror_dist_tags <<< "${mirror_dist_tags_csv}" for dist_tag in "${mirror_dist_tags[@]}"; do [[ -n "${dist_tag}" ]] || continue echo "Mirroring ${package_name}@${package_version} onto dist-tag ${dist_tag}" - npm dist-tag add "${package_name}@${package_version}" "${dist_tag}" + NPM_CONFIG_USERCONFIG="${mirror_userconfig}" \ + npm dist-tag add "${package_name}@${package_version}" "${dist_tag}" done fi ) diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index 12159f30e78..045ffdc703b 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -7,6 +7,7 @@ import { parseNpmPackJsonOutput, parseReleaseTagVersion, parseReleaseVersion, + resolveNpmDistTagMirrorAuth, resolveNpmPublishPlan, resolveNpmCommandInvocation, utcCalendarDayDistance, @@ -116,6 +117,44 @@ describe("resolveNpmPublishPlan", () => { }); }); +describe("resolveNpmDistTagMirrorAuth", () => { + it("prefers NODE_AUTH_TOKEN when both auth env vars exist", () => { + expect( + resolveNpmDistTagMirrorAuth({ + nodeAuthToken: "node-token", + npmToken: "npm-token", + }), + ).toEqual({ + hasAuth: true, + source: "node-auth-token", + }); + }); + + it("falls back to NPM_TOKEN when NODE_AUTH_TOKEN is missing", () => { + expect( + resolveNpmDistTagMirrorAuth({ + nodeAuthToken: " ", + npmToken: "npm-token", + }), + ).toEqual({ + hasAuth: true, + source: "npm-token", + }); + }); + + it("reports missing auth when neither token exists", () => { + expect( + resolveNpmDistTagMirrorAuth({ + nodeAuthToken: "", + npmToken: undefined, + }), + ).toEqual({ + hasAuth: false, + source: "none", + }); + }); +}); + describe("compareReleaseVersions", () => { it("treats stable as newer than same-day beta", () => { expect(compareReleaseVersions("2026.3.29", "2026.3.29-beta.2")).toBe(1);