Skip to content

Commit f4008e4

Browse files
committed
ci(publish): classify npm 2fa/granular token residual
Prefer TYPED_BLOCKER=npm_token_2fa_or_granular when registry requires 2FA or granular bypass for publish, while keeping org/scope soft-pass and already-published honesty after crates.io is green.
1 parent bb0041f commit f4008e4

2 files changed

Lines changed: 22 additions & 6 deletions

File tree

.github/workflows/publish.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,17 @@ jobs:
540540
set -e
541541
printf '%s\n' "${out}"
542542
if [[ ${ec} -ne 0 ]]; then
543+
# Prefer specific auth-class residual over the generic org/scope soft-pass.
544+
# Org can exist while classic automation tokens still fail npm publish 2FA policy.
545+
if printf '%s\n' "${out}" | grep -Eqi \
546+
'Two-factor authentication|granular access token|bypass 2fa|bypass two-factor|OTP|one-time password'; then
547+
{
548+
echo "TYPED_BLOCKER=npm_token_2fa_or_granular"
549+
echo "detail=npm publish failed: registry requires two-factor authentication or a granular access token with bypass 2FA for publish. crates.io chain already succeeded (crates job green). @basecrawl org may already exist; NPM_TOKEN must be a publish-capable granular token (bypass 2FA) for @basecrawl/sdk. M25 does not auto-rotate secrets."
550+
} | tee "${RUNNER_TEMP}/npm-typed-blocker.txt"
551+
echo "typed npm 2fa/granular token blocker recorded"
552+
exit 42
553+
fi
543554
# Typed blocker path (VAL-NPM-002): missing/unauthorized @basecrawl scope/org
544555
# after crates already green is an allowed milestone outcome when recorded.
545556
if printf '%s\n' "${out}" | grep -Eqi \
@@ -551,11 +562,11 @@ jobs:
551562
echo "typed npm org/scope blocker recorded"
552563
exit 42
553564
fi
554-
if printf '%s\n' "${out}" | grep -Eqi 'cannot publish over|EPUBLISHCONFLICT|previously published'; then
565+
if printf '%s\n' "${out}" | grep -Eqi 'cannot publish over|EPUBLISHCONFLICT|previously published|already been published'; then
555566
echo "package version already on npm — treating as success"
556567
exit 0
557568
fi
558-
echo "npm publish failed (not a typed org blocker)" >&2
569+
echo "npm publish failed (not a typed auth/org blocker)" >&2
559570
exit ${ec}
560571
fi
561572
echo "npm publish OK for ${NAME}@${VER}"
@@ -570,13 +581,14 @@ jobs:
570581
conclusion="${{ steps.npm_publish.conclusion }}"
571582
echo "npm_publish outcome=${outcome} conclusion=${conclusion}"
572583
if [[ -f "${RUNNER_TEMP}/npm-typed-blocker.txt" ]]; then
573-
echo "::warning::TYPED npm org/scope blocker — crates.io publish succeeded; npm deferred"
584+
blocker="$(grep -E '^TYPED_BLOCKER=' "${RUNNER_TEMP}/npm-typed-blocker.txt" | head -1 | cut -d= -f2- || true)"
585+
echo "::warning::TYPED npm blocker (${blocker:-unknown}) — crates.io publish succeeded; npm deferred"
574586
cat "${RUNNER_TEMP}/npm-typed-blocker.txt"
575-
# Soft-pass so cross-publish topology (crates green → typed npm blocker) is green.
587+
# Soft-pass so cross-publish topology (crates green → typed npm residual) is green.
576588
exit 0
577589
fi
578590
if [[ "${outcome}" == "failure" ]]; then
579-
echo "npm publish failed without typed @basecrawl org/scope classification" >&2
591+
echo "npm publish failed without typed npm auth/org classification" >&2
580592
exit 1
581593
fi
582594
echo "npm job complete"

bindings/node/tests/linux-publish-honesty.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,17 @@ test("npm pack includes linux native binary and honesty files; no multi-OS artif
146146
}
147147
});
148148

149-
test("publish.yml records typed @basecrawl org/scope blocker after crates green", () => {
149+
test("publish.yml records typed npm auth residuals after crates green", () => {
150150
const workflowPath = path.resolve(packageRoot, "../../.github/workflows/publish.yml");
151151
assert.equal(existsSync(workflowPath), true);
152152
const yaml = readFileSync(workflowPath, "utf8");
153153
assert.match(yaml, /@basecrawl\/sdk/);
154154
assert.match(yaml, /NPM_TOKEN/);
155+
// Org-missing and 2fa/granular-token residual classes (soft-pass after crates green).
155156
assert.match(yaml, /TYPED_BLOCKER=npm_org_or_scope/);
157+
assert.match(yaml, /TYPED_BLOCKER=npm_token_2fa_or_granular/);
158+
assert.match(yaml, /granular access token|Two-factor authentication/);
159+
assert.match(yaml, /already been published/);
156160
assert.match(yaml, /needs:\s*\n\s*- version-check\s*\n\s*- crates/m);
157161
// Never hardcode a token value pattern in the workflow.
158162
assert.doesNotMatch(yaml, /NODE_AUTH_TOKEN:\s*['"]?[a-zA-Z0-9_-]{20,}/);

0 commit comments

Comments
 (0)