build(release): check publish credentials before packing - #22
Merged
Merged
Conversation
A stale credential used to surface at the very end of a publish. The last release spent several minutes on tests, the engine-asset probe and the pack before `npm publish` failed on an expired session, and `mcp-publisher` failed after that - so every one of those steps had to be repeated. Both logins now run first. Each has to succeed for the publish to happen anyway, so checking them up front costs nothing and turns a late failure into an immediate one. Gated on --publish. Packing needs no credentials, and this script also runs as a plain build step and inside the validation gate, where prompting for a login would hang it. npm is only prompted for when `npm whoami` already fails, so an existing session is left alone; it stays interactive because the account has 2FA. The mcp-publisher login uses `gh auth token` because the MCP Registry decides which namespaces a token may publish to by calling GET /user/memberships/orgs, which needs the read:org scope. Its own device flow mints a token without that scope, GitHub answers 403, and the registry treats the 403 as "no admin orgs" rather than an error - so publishing silently degrades to io.github.<user>/* and then fails on io.github.codegraph-ai/* with a message blaming organization membership, which is not the cause. That cost a release cycle to diagnose, so the reasoning is recorded next to the call. CODEGRAPH_MCP_TOKEN overrides it for anyone preferring a PAT scoped to read:org alone, since gh's token also carries repo and workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rVbt7rENTwXkdHt3Bpgb5
…ix shellcheck tr classes
🔍 CodeGraph PR Review1 files changed (+153/−0, 2 functions) · Risk: 🟢 low
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The developer asked to commit a pending local edit to scripts/package-npm.sh and push it through the no-mistakes gate. The intent behind that edit, stated earlier in the session, was to move the registry authentication steps -
mcp-publisher login github -token "$(gh auth token)"and annpm logincheck - to the head of the packaging script, so that a stale or missing credential fails immediately instead of after several minutes of tests, engine-asset probing and packing (the failure mode that cost time on the previous release). Constraints agreed during the work: the auth block must be gated on--publishso the pack-only path used by plain builds and the validation gate never prompts and hangs;npm loginshould run only whennpm whoamialready fails, leaving an existing session alone and staying interactive because of the account's 2FA; andgh auth tokenis used for the MCP Registry because it carries theread:orgscope the registry needs, withCODEGRAPH_MCP_TOKENas an override for a narrower PAT. The change was made after 0.20.1 had already been published manually, so it affects only future releases. Per the user's standing instruction, the push must go throughgit push no-mistakesrather than directly to origin.What Changed
scripts/package-npm.sh --publishnow runs a credential preflight before the tests, engine-asset probe and pack: it reports the currentnpm whoamiidentity or starts an interactivenpm login(the account has 2FA), so a dead npm session aborts the run in seconds rather than after several minutes of work. The pack-only path is untouched and still needs no credentials.GET /userand, when the namespace inmcp-package/server.jsonis not the token's own user namespace,GET /user/memberships/orgs, requiringrole/stateofadmin/activefor the owning org. A 403 on the memberships call is reported as a missingread:orgscope, and a token is taken fromCODEGRAPH_MCP_TOKENwhen set, otherwisegh auth token. A missingmcp-publisherbinary warns rather than blocks.mcp-publisher loginis now error-checked there, printing its output plus the two-command recovery for the case where npmjs.com already has the version but the Registry does not.Pipeline review left informational notes on diagnostics in the new auth block (GitHub's body is not echoed on the memberships 403;
curl ... || echo 000can print the status twice on a transport failure) and lint flagged a pre-existing SC2012/SC2035 on the untouched tarball lookup at line 299.Risk Assessment
✅ Low: The round-1 error is genuinely fixed - the 300-second MCP JWT is now minted immediately before
mcp-publisher publishand the up-front gate checks the long-lived GitHub credential instead, with the ownership preflight verified against the live API (role admin / state active for codegraph-ai) so it neither false-passes nor false-blocks; only two diagnostic-message refinements remain in a manually run release script.Testing
I drove the real scripts/package-npm.sh unmodified through 11 end-to-end scenarios with test doubles for npm, gh, mcp-publisher and api.github.com, so nothing was ever published; the package's own npm test (fetch-engine + wrapper-args) ran inside every pack-path scenario and passed, and the release-asset probe hit the real GitHub release. The decisive evidence is each transcript's trailing list of commands the script actually invoked: on every credential-failure path it aborts in under a second with npm test, npm pack and npm publish absent, and on the pack-only path the credential block never runs at all. One scenario checked the preflight against the live api.github.com with the maintainer's real gh token, confirming the actual release credential passes the org-ownership gate. No automated test exists in the repo for this release-time shell script, so this was manual verification captured as CLI transcripts; the change has no UI surface - the terminal output is the end-user experience - so no screenshot applies. Transient tarballs created by the pack step were deleted and the worktree is clean.
Evidence: All 11 scenario transcripts (combined)
Evidence: Fail-fast: stale GitHub token aborts before the tests, asset probe and pack
$ ./scripts/package-npm.sh --publish === CodeGraph npm package builder === Checking publish credentials... ✓ npm authenticated as anvanster ✗ GitHub rejected the token (HTTP 401) - not packaging {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"} Run 'gh auth login', or set CODEGRAPH_MCP_TOKEN to a live PAT with read:org. [exit code 1 after 0.0s] [commands the script actually invoked:] npm whoami gh auth token curl https://api.github.com/user (mode=bad-token)Evidence: Publish path with the real gh token against live api.github.com (npm/mcp-publisher stubbed)
$ ./scripts/package-npm.sh --publish === CodeGraph npm package builder === Checking publish credentials... ✓ npm authenticated as anvanster ✓ anvanster owns codegraph-ai - io.github.codegraph-ai is publishable Removing any bundled binaries (the engine is fetched at install time)... Checking the engine fetch and the wrapper arguments... ✓ package tests pass ... ✓ every engine asset is published for v0.20.1 Packing... ✓ Created: mcp-package/astudioplus-codegraph-mcp-0.20.1.tgz Publishing to npmjs.com... ✓ Published @astudioplus/codegraph-mcp@0.20.1 Updating MCP Registry... ✓ MCP Registry updated [exit code 0 after 4.9s] [commands the script actually invoked:] npm whoami gh auth token npm test npm pack npm publish --access public mcp-publisher login github -token <redacted> mcp-publisher publish --server-json server.jsonEvidence: PAT without read:org gets the accurate diagnostic instead of the registry's bogus membership error
Checking publish credentials... ✓ npm authenticated as anvanster ✗ the GitHub token cannot read organisation membership - not packaging GitHub answered 403 for GET /user/memberships/orgs, which needs read:org. Without it the Registry sees no organisations and refuses io.github.codegraph-ai. Use 'gh auth token', or set CODEGRAPH_MCP_TOKEN to a PAT with read:org. [exit code 1 after 0.1s]Evidence: Pack-only build with no usable credentials never enters the auth block
$ ./scripts/package-npm.sh === CodeGraph npm package builder === Removing any bundled binaries (the engine is fetched at install time)... (no "Checking publish credentials" line - no prompt, no hang) ... ✓ Created: mcp-package/astudioplus-codegraph-mcp-0.20.1.tgz [exit code 0 after 4.1s] [commands the script actually invoked:] npm test npm packEvidence: Test harness (reproducible)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
scripts/package-npm.sh:73- The MCP Registry JWT minted bymcp-publisher loginlives 5 minutes, so moving the login to the head of the script guarantees it is expired by the timemcp-publisher publishruns at line 233. Verified against the real credential issued by registry.modelcontextprotocol.io: the stored JWT hasexp - iat == 300, and the credential file holds only {method, registry, token} - no GitHub token, so the publisher cannot silently refresh it. Between line 73 and line 233 the script runs the package tests (line 107), ~12 curl probes against the GitHub release (line 190),npm pack(line 215) andnpm publish(line 228), which on this 2FA account blocks on a human OTP prompt of unbounded duration. Failure sequence: login succeeds and prints '✓ mcp-publisher authenticated', npm publish succeeds (irreversible for that version), thenmcp-publisher publish401s on an expired token - the exact 'fails at the very end, after everything else passed' outcome the commit exists to remove, now made near-deterministic rather than occasional. Fix: keep the up-front check as a fail-fast probe, but re-runmcp-publisher login github -token "$MCP_TOKEN"immediately before line 233. MCP_TOKEN is already set in the same shell (line 68) and is still in scope there, so this is additive and preserves the commit's intent.scripts/package-npm.sh:74- The up-front gate cannot detect the failure it documents. Per the comment at lines 57-62 (confirmed by the issued token'spermissionsclaim, which is a namespace list: [io.github.anvanster/, io.github.codegraph-ai/]), a token missing read:org does not fail login - the registry treats GitHub's 403 on /user/memberships/orgs as 'no admin orgs' and issues a valid JWT scoped only to io.github.<user>/*. Somcp-publisher loginreturns 0, line 74 prints '✓ mcp-publisher authenticated', and the 403 still arrives at line 233 when publishing io.github.codegraph-ai/codegraph. Conversely the read:org hint at lines 77-78 is printed only on the login-failure branch, which is the one case where read:org is not the cause. To actually gate on this, verify the scope before trusting the login - e.g. probegh api user/memberships/orgs(or curl api.github.com with the token) and fail on 403, or decode thepermissionsclaim of the stored token and assert it covers the namespace in mcp-package/server.json (io.github.codegraph-ai/codegraph).scripts/package-npm.sh:73-mcp-publisher login ... >/dev/null 2>&1discards the tool's own diagnostic, so a login failure caused by an unreachable registry, a revoked token or a malformed PAT is reported only as the fixed read:org/org-ownership hint at lines 77-78. Capture the output (login_log=$(mcp-publisher login github -token "$MCP_TOKEN" 2>&1)) and print it to stderr on the failure branch alongside the hint, matching how the package-test failure at lines 107-110 replays test_log.scripts/package-npm.sh:50-npm whoamiis invoked up to three times (lines 49, 50, 54), each a separate registry round trip, and the value printed at line 50 is re-fetched rather than reused from the line 49 probe. Capture it once (if npm_user=$(npm whoami 2>/dev/null); then ...) and reuse the variable in both the already-logged-in and post-login branches.🔧 Fix: mint MCP token at publish, preflight GitHub org ownership
2 infos still open:
scripts/package-npm.sh:129- The 403 branch asserts a single cause without showing GitHub's own message: unlike the generic failure at lines 136-139, it never calls gh_body_err. GET /user/memberships/orgs also returns 403 when the token is valid and read:org-scoped but not SSO-authorised for the org, or when an org policy blocks the PAT; in those cases the operator is told to get a token with read:org, which they already have. That is the same 'confident message about a cause that is not the actual cause' failure this change exists to remove, just relocated. Addgh_body_errafter line 130 so GitHub's body (e.g. the SAML enforcement URL) is printed alongside the read:org hint.scripts/package-npm.sh:97-curl ... -w '%{http_code}' ... || echo 000emits the status twice on a transport failure: curl already writes000from -w before exiting non-zero, so the fallback appends a second one. Verified locally -curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:9/ || echo 000yields000000, which surfaces as 'GitHub rejected the token (HTTP 000000)' at line 105 or 'HTTP 000000' at line 137. The comparisons still fail closed, so this is diagnostic only. Also,2>/dev/nullat line 97 discards curl's -S error text, so the underlying reason (DNS, TLS, proxy) is lost. Fix: drop the|| echo 000, normalise withcode="${code:-000}", and keep curl's stderr for the failure path.✅ **Test** - passed
✅ No issues found.
./scripts/package-npm.sh(pack-only, no credentials available) - auth block skipped entirely, exits 0; command log shows onlynpm testandnpm pack./scripts/package-npm.sh --publishwith the maintainer's realgh auth tokenagainst the live api.github.com - preflight prints✓ anvanster owns codegraph-ai - io.github.codegraph-ai is publishablebefore the tests/asset probe/pack./scripts/package-npm.sh --publishwith a stale GitHub token (api.github.com/user -> 401) - aborts in 0.0s, nonpm test/npm pack/npm publishinvoked./scripts/package-npm.sh --publishwith a PAT lacking read:org (GET /user/memberships/orgs -> 403) - prints the read:org diagnostic and stops./scripts/package-npm.sh --publishwith a plain (non-owner) codegraph-ai membership - refused withrole/state: member/active./scripts/package-npm.sh --publishwith no gh token and no CODEGRAPH_MCP_TOKEN - refused with thegh auth loginhintCODEGRAPH_MCP_TOKEN=... ./scripts/package-npm.sh --publish- override used,gh auth tokennever called./scripts/package-npm.sh --publishwith no npm session -npm loginruns up-front, before the pack; with a session presentnpm loginis never invoked./scripts/package-npm.sh --publishwith a failingnpm login(EOTP) - stops before any work./scripts/package-npm.sh --publishwith mcp-publisher off PATH - warns at preflight and at the registry step, release not blocked./scripts/package-npm.sh --publishwithmcp-publisher loginfailing afternpm publish- prints the two-command recovery and exits 1cd mcp-package && npm test(fetch-engine + wrapper-args) - run as part of every pack-path scenario, passesgit status --porcelainafter the run - clean, transientmcp-package/*.tgzremoved✅ **Document** - passed
✅ No issues found.
scripts/package-npm.sh:299- shellcheck reports SC2012/SC2035 on the pre-existing tarball lookupTARBALL=$(ls -t *.tgz 2>/dev/null | head -1). It predates this change and is untouched by it, so fixing it would mean altering unrelated packaging behavior; left as-is and reported for visibility.✅ **Push** - passed
✅ No issues found.