diff --git a/.github/scripts/prepare-memos-release.test.mjs b/.github/scripts/prepare-memos-release.test.mjs index d88795930..ffe51247e 100644 --- a/.github/scripts/prepare-memos-release.test.mjs +++ b/.github/scripts/prepare-memos-release.test.mjs @@ -842,7 +842,7 @@ test("legacy standalone local-plugin publisher requires an extra non-dry-run con assert.match(workflow, /ALLOW_STAGED_TAG_BEFORE_NPM/); assert.match(workflow, /audit-local-plugin-package\.mjs/); assert.match(workflow, /wait-for-local-plugin-npm-release\.test\.mjs/); - assert.match(workflow, /NPM_VISIBILITY_TIMEOUT_SECONDS: "150"/); + assert.match(workflow, /NPM_VISIBILITY_TIMEOUT_SECONDS: "600"/); assert.match(workflow, /FORCE_PACKAGE_ONLY_RELEASE: \$\{\{ inputs\.tag != 'latest' \|\| contains\(inputs\.version, '-'\) \}\}/); assert.match(workflow, /if \[ -n "\$\{DOCS_SYNC_MODE\}" \]; then/); assert.doesNotMatch(workflow, /EVENT_NAME: \$\{\{ github\.event_name \}\}/); diff --git a/.github/scripts/publish-local-plugin.sh b/.github/scripts/publish-local-plugin.sh index 034a00d8c..399b8ad10 100755 --- a/.github/scripts/publish-local-plugin.sh +++ b/.github/scripts/publish-local-plugin.sh @@ -13,7 +13,7 @@ if [ ! -s "${RELEASE_TARBALL}" ]; then exit 2 fi -npm_visibility_timeout_seconds="${NPM_VISIBILITY_TIMEOUT_SECONDS:-150}" +npm_visibility_timeout_seconds="${NPM_VISIBILITY_TIMEOUT_SECONDS:-600}" npm_visibility_interval_seconds="${NPM_VISIBILITY_INTERVAL_SECONDS:-10}" npm_visibility_request_timeout_seconds="${NPM_VISIBILITY_REQUEST_TIMEOUT_SECONDS:-8}" npm_registry_url="https://registry.npmjs.org" @@ -336,7 +336,7 @@ if npm_version_exists; then published_version_visible=true published_version_preexisting=true if [ "${RECOVER_EXISTING_NPM_RELEASE:-false}" != "true" ]; then - echo "::error::${PACKAGE_NAME}@${RELEASE_VERSION} already exists. Normal releases require an unused version; enable recovery only after release-owner verification of a partial failure." + echo "::error::${PACKAGE_NAME}@${RELEASE_VERSION} already exists. Normal releases require an unused version; inspect npm first and use explicit recovery only after release-owner verification of a partial failure." exit 1 fi if remote_tag_exists "${RELEASE_TAG}"; then diff --git a/.github/scripts/publish-local-plugin.test.mjs b/.github/scripts/publish-local-plugin.test.mjs index 2e7864545..e661dfd1c 100644 --- a/.github/scripts/publish-local-plugin.test.mjs +++ b/.github/scripts/publish-local-plugin.test.mjs @@ -121,7 +121,7 @@ if [[ "\${1:-}" == *"wait-for-local-plugin-npm-release.mjs" ]]; then printf '%s' "\${count}" > "\${increment_file}" case "\${NPM_MOCK_SCENARIO}" in always-missing|publish-fails) - echo "::error::npm release was not fully visible within 150s" + echo "::error::npm release was not fully visible within \${NPM_VISIBILITY_TIMEOUT_SECONDS:-600}s" exit 1 ;; integrity-mismatch) @@ -130,7 +130,7 @@ if [[ "\${1:-}" == *"wait-for-local-plugin-npm-release.mjs" ]]; then ;; *) if [ "\${NPM_MOCK_DIST_TAG_VERSION:-\${RELEASE_VERSION}}" != "\${RELEASE_VERSION}" ]; then - echo "::error::npm release was not fully visible within 150s: dist-tag \${NPM_DIST_TAG} did not point to \${RELEASE_VERSION}" + echo "::error::npm release was not fully visible within \${NPM_VISIBILITY_TIMEOUT_SECONDS:-600}s: dist-tag \${NPM_DIST_TAG} did not point to \${RELEASE_VERSION}" exit 1 fi echo '{"ok":true,"attempts":3}' @@ -330,6 +330,7 @@ test("stops before tag creation when publish succeeds but visibility remains del assert.equal(result.metadataWaitCount, 1); assert.equal(result.packCount, 0); assert.match(result.stdout + result.stderr, /Refusing to issue a second publish request/); + assert.match(result.stdout + result.stderr, /within 600s/); }); test("allows npm publish after a staged paired Draft Release only in npm-only phase", () => { @@ -461,4 +462,5 @@ test("rejects an already-used npm version outside explicit recovery", () => { assert.equal(result.publishCount, 0); assert.equal(result.packCount, 0); assert.match(result.stdout + result.stderr, /Normal releases require an unused version/); + assert.match(result.stdout + result.stderr, /inspect npm first/); }); diff --git a/.github/scripts/wait-for-local-plugin-npm-release.mjs b/.github/scripts/wait-for-local-plugin-npm-release.mjs index 2c00bb198..fda985310 100644 --- a/.github/scripts/wait-for-local-plugin-npm-release.mjs +++ b/.github/scripts/wait-for-local-plugin-npm-release.mjs @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; const INTEGRITY_PATTERN = /^sha512-[A-Za-z0-9+/]+={0,2}$/; +export const DEFAULT_NPM_VISIBILITY_TIMEOUT_SECONDS = 600; function clean(value) { return String(value ?? "").trim(); @@ -103,7 +104,7 @@ export async function waitForNpmReleaseVisibility( distTag, expectedIntegrity, registryUrl = "https://registry.npmjs.org", - timeoutMs = 150_000, + timeoutMs = DEFAULT_NPM_VISIBILITY_TIMEOUT_SECONDS * 1000, intervalMs = 10_000, requestTimeoutMs = 8_000, } = {}, @@ -208,7 +209,12 @@ export async function run(env = process.env) { distTag: env.NPM_DIST_TAG, expectedIntegrity, registryUrl: env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org", - timeoutMs: positiveInteger(env.NPM_VISIBILITY_TIMEOUT_SECONDS, 150, "timeout") * 1000, + timeoutMs: + positiveInteger( + env.NPM_VISIBILITY_TIMEOUT_SECONDS, + DEFAULT_NPM_VISIBILITY_TIMEOUT_SECONDS, + "timeout", + ) * 1000, intervalMs: positiveInteger(env.NPM_VISIBILITY_INTERVAL_SECONDS, 10, "interval") * 1000, requestTimeoutMs: positiveInteger(env.NPM_VISIBILITY_REQUEST_TIMEOUT_SECONDS, 8, "request timeout") * 1000, diff --git a/.github/scripts/wait-for-local-plugin-npm-release.test.mjs b/.github/scripts/wait-for-local-plugin-npm-release.test.mjs index b3aabf84e..d9f9b4f2b 100644 --- a/.github/scripts/wait-for-local-plugin-npm-release.test.mjs +++ b/.github/scripts/wait-for-local-plugin-npm-release.test.mjs @@ -6,6 +6,7 @@ import { join } from "node:path"; import test from "node:test"; import { + DEFAULT_NPM_VISIBILITY_TIMEOUT_SECONDS, inspectNpmReleaseVisibility, npmPackumentUrl, tarballIntegrity, @@ -224,3 +225,35 @@ test("uses a hard deadline for an unavailable registry version", async () => { assert.equal(clock, 30_000); assert.equal(attempts, 3); }); + +test("uses the ten-minute default visibility deadline", async () => { + let clock = 0; + let attempts = 0; + await assert.rejects( + waitForNpmReleaseVisibility( + { + packageName: "@memtensor/memos-local-plugin", + version: "2.0.14", + distTag: "latest", + expectedIntegrity: integrity, + intervalMs: 10_000, + requestTimeoutMs: 1_000, + }, + { + fetchImpl: async () => { + attempts += 1; + return response({}, 404); + }, + sleep: async (milliseconds) => { + clock += milliseconds; + }, + now: () => clock, + log: () => {}, + }, + ), + /not fully visible within 600s/, + ); + assert.equal(DEFAULT_NPM_VISIBILITY_TIMEOUT_SECONDS, 600); + assert.equal(clock, 600_000); + assert.equal(attempts, 60); +}); diff --git a/.github/workflows/memos-local-plugin-publish.yml b/.github/workflows/memos-local-plugin-publish.yml index 52d634384..60478c396 100644 --- a/.github/workflows/memos-local-plugin-publish.yml +++ b/.github/workflows/memos-local-plugin-publish.yml @@ -30,7 +30,7 @@ on: type: boolean default: true recover_existing_npm_release: - description: "Allow reconstructing a missing tag for an existing npm version. Keep false for normal releases." + description: "Recover a verified partial publish only; keep false for normal releases." required: true type: boolean default: false @@ -799,7 +799,7 @@ jobs: NPM_DIST_TAG: ${{ inputs.tag }} RECOVER_EXISTING_NPM_RELEASE: ${{ inputs.recover_existing_npm_release }} RELEASE_METADATA_STATE: ${{ steps.release_state.outputs.state }} - NPM_VISIBILITY_TIMEOUT_SECONDS: "150" + NPM_VISIBILITY_TIMEOUT_SECONDS: "600" NPM_VISIBILITY_INTERVAL_SECONDS: "10" NPM_VISIBILITY_REQUEST_TIMEOUT_SECONDS: "8" ALLOW_STAGED_TAG_BEFORE_NPM: ${{ (inputs.publish_phase || 'full') == 'publish_npm_only' && 'true' || 'false' }}