Skip to content

Prepare scoped npm prerelease - #12

Merged
raghubetina merged 1 commit into
mainfrom
codex/scoped-npm-alpha2
Jul 31, 2026
Merged

Prepare scoped npm prerelease#12
raghubetina merged 1 commit into
mainfrom
codex/scoped-npm-alpha2

Conversation

@raghubetina

@raghubetina raghubetina commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • prepare @firstdraft.com/cli@0.1.0-alpha.2 as the first organization-scoped prerelease
  • preserve the firstdraft executable and canonicalize its bin target
  • make the installed-package smoke work with a scoped node_modules path
  • document the failed alpha.1 attempt, scoped bootstrap, and transition to GitHub OIDC trusted publishing

Context

npm rejected the unscoped firstdraft name as too similar to the existing first-draft package before creating a registry package. The protected v0.1.0-alpha.1 Git tag remains immutable, so this uses the next version and leaves the old tag untouched.

This PR only prepares reviewed source. It does not create v0.1.0-alpha.2 or publish to npm.

Verification

  • npm ci --ignore-scripts
  • npm audit — 0 vulnerabilities
  • npm run check — typecheck, lint, formatting, 113 tests, pack allowlist, and installed-tarball smoke
  • npm publish --dry-run --access public --tag next --provenance --ignore-scripts --json
    • identified @firstdraft.com/cli@0.1.0-alpha.2
    • contained 15 allowlisted files
    • retained bin/firstdraft.js at mode 0755
    • emitted no bin normalization warning

@raghubetina

Copy link
Copy Markdown
Contributor Author

Technical review

Correcting my own check on #11

I reported that the name was still free, based on this:

$ curl -s -o /dev/null -w "%{http_code}" https://registry.npmjs.org/firstdraft
404

A 404 means nobody has published it. It does not mean npm will let you. The registry applies a similarity check at publish time, and that is what rejected firstdraft, so the check I ran answered a narrower question than the one that mattered.

The collision is real. first-draft has been on npm since 2019 and is currently at 0.8.0.

Everything in the verification list reproduces

Cloned the branch and ran it:

npm run check                          exit 0
node scripts/run-tests.js              113 tests, 113 pass, 0 fail
npm publish --dry-run --json           @firstdraft.com/cli@0.1.0-alpha.2
                                       15 files
                                       bin/firstdraft.js at 0o755

The 15 paths are the same allowlist as before, so the rename did not disturb the packed contents.

I also checked the dotted scope, since @firstdraft.com is an unusual shape and there are no public packages under it. npm's own validator accepts it:

$ node -e "…validate-npm-package-name('@firstdraft.com/cli')"
{"validForNewPackages":true,"validForOldPackages":true}

So the name is syntactically fine, and @firstdraft.com/cli returns 404 on the registry.

Bumping to alpha.2 rather than reusing alpha.1 is right, given the tag is protected and the workflow asserts GITHUB_REF_NAME equals v$package_version. Reusing the version would have required either moving a protected tag or breaking that assertion.

Finding: the failure that killed alpha.1 is still outside the verification list

This is the same gap, one level up. Every listed check runs locally:

npm publish --dry-run --access public --tag next --provenance --ignore-scripts --json

--dry-run validates the name syntactically, builds the tarball, and prints what it would send. It does not ask the registry whether the publish is allowed. It would have passed for firstdraft too, which is precisely why alpha.1 got as far as a protected tag before failing.

Two registry-side conditions remain unverified, and both fail at the same late point:

The firstdraft.com organization must exist and the publishing identity must belong to it. A registry search under that scope returns zero packages, so nothing here confirms the org has been created.

The scoped name must clear the same similarity policy. Scoping usually settles it, since the check is scoped-aware, but "usually" is what the last attempt relied on.

Both are answerable in seconds with an authenticated npm org ls firstdraft.com, or by loading the org page. Doing that before cutting v0.1.0-alpha.2 converts a failed protected tag into a five-second check. RELEASING.md already documents a bootstrap sequence, so this belongs as a step in it rather than as reviewer folklore.

Finding: the pull request body is malformed

The description contains 22 literal \n sequences and one real newline, so it renders as a single unbroken line with visible backslashes rather than the intended headings and bullets.

Cosmetic, and worth fixing because this body is the record of why the name changed. Someone reading it in six months to understand the alpha.1 failure will be reading one long line.

Two findings

Verify the org and scoped name against the registry before tagging, and re-render the description.

@raghubetina

Copy link
Copy Markdown
Contributor Author

Lesson: "nobody has it" is not "you can have it"

I made this mistake on the previous pull request, so it is a fair one to teach.

The question was whether a package name was available on npm. I checked the obvious way:

$ curl -s -o /dev/null -w "%{http_code}" https://registry.npmjs.org/firstdraft
404

Not found. Available, surely.

npm refused it. There is a package called first-draft, published in 2019, and npm blocks new names that are confusingly similar to existing ones. The similarity rule removes hyphens, dots, and underscores before comparing, so firstdraft and first-draft are the same name as far as the registry is concerned.

The 404 was accurate. It just answered "has anyone published this?" when the question was "will you accept this?"

The general shape

Checking for absence tests availability. It does not test permission. These come apart wherever a namespace has policy attached, which is most places worth naming something:

npm blocks typosquat-adjacent names, as above.

RubyGems has similar protections, and additionally will not let you reclaim a name that was published and then yanked.

DNS will happily tell you a domain is unregistered while a trademark holder is entirely willing to take it from you afterwards.

GitHub organizations hold reserved words and names tied to dormant accounts that no API will report as taken.

S3 buckets are globally unique, so a name free in your account may be held silently in someone else's.

Database uniqueness, which is the version every Rails developer has already met:

if User.where(email: email).none?
  User.create!(email: email)   # racy, and the constraint is the real authority
end

The check and the write are separate operations, and something can change in between. This is why the unique index exists and why rescue ActiveRecord::RecordNotUnique is not paranoia.

Local validation is not server acceptance either

The same PR shows the second half of the pattern. The release process runs:

npm publish --dry-run --json

That builds the tarball, validates the name syntactically, and prints exactly what would be sent. It is a genuinely useful check, and it passed for the rejected name too, because it never contacts the registry to ask.

A dry run tells you your request is well-formed. It does not tell you it will be accepted. The same gap appears in terraform plan against permissions you lack, in a migration that runs locally against a schema production does not have, and in any client-side form validation.

What to do about it

Claim the namespace before you build the process around it. Publishing a placeholder version, or just registering the org, converts an assumption into a fact. Here the name was assumed through an entire release workflow, a protected tag, and an approval environment, and failed at the last step.

Make the cheap authoritative check part of the checklist. For npm that is asking the registry whether you may publish, not whether something exists. Where the authoritative check needs credentials, that is a hint it is the one that matters, because it is the one the server actually evaluates.

When a check comes back negative, ask what question it answered. A 404 is a fact about the registry's contents. Whether you may add to those contents is a different question with a different answer, and the two look identical until the moment they do not.

npm rejected the unscoped package name as too similar to an
existing package. Publish the next immutable candidate in the
firstdraft.com organization while preserving the firstdraft
executable.

Canonicalize the bin target so npm no longer warns while
normalizing it, and record the scoped bootstrap, trusted-publisher
transition, and alpha.1 recovery history.
@raghubetina
raghubetina force-pushed the codex/scoped-npm-alpha2 branch from 2a28688 to 8f53c9f Compare July 31, 2026 22:55
@raghubetina

Copy link
Copy Markdown
Contributor Author

Addressed both hosted findings in 8f53c9f.

  • Replaced the malformed PR body; its headings and lists now render normally.
  • Added an explicit first-scoped-release preflight to RELEASING.md:
    • npm whoami
    • npm org ls firstdraft.com --json
    • npm view @firstdraft.com/cli name --json
  • Ran the authenticated checks: the active identity is raghubetina, firstdraft.com reports raghubetina as owner, and the exact scoped coordinate remains E404.

The checklist deliberately states the evidence boundary: the checks prove scope authority and package absence. npm exposes no non-mutating registry endpoint that guarantees admission. Publishing under an owned scope is the documented remedy for the unscoped similarity rejection, and the workflow continues to require public access explicitly.

Verification on the exact pushed tree:

  • npm audit: 0 vulnerabilities
  • npm run check: typecheck, lint, formatting, 113 tests, pack allowlist, and installed-tarball smoke
  • Opus/high re-review: Approve, no material findings

Hosted CI is rerunning on 8f53c9f.

@raghubetina

Copy link
Copy Markdown
Contributor Author

Follow-up on 8f53c9f: both findings closed

The description now renders properly: 23 real newlines, zero literal \n.

The registry preflight landed in RELEASING.md, and the caveat attached to it is better than what I asked for:

npm whoami
npm org ls firstdraft.com --json
npm view '@firstdraft.com/cli' name --json

The last command should return E404 before the first publication. It proves only that the package is absent; the first two commands establish authority over the scope. npm provides no non-mutating registry preflight that guarantees a new package will be accepted, but using an owned scope is npm's documented remedy for an unscoped similarity rejection.

That draws the line in the right place. npm view returning 404 is the check I got wrong on #11, and saying explicitly that it proves absence rather than permission is more precise than the finding was. Splitting authority (whoami, org ls) from availability (view) means the two questions cannot be confused again, and admitting that no preflight fully guarantees acceptance is more honest than implying the checklist removes the risk.

Confirmed the rest of the branch is untouched, since the rebase makes the compare view misleading:

package.json           unchanged
test/package.test.js   unchanged
scripts/smoke-package.js  unchanged

So the verification I reproduced on 2a28688 still stands: npm run check exit 0, 113 tests, and a dry run identifying @firstdraft.com/cli@0.1.0-alpha.2 with 15 files and bin/firstdraft.js at 0o755.

No further findings.

@raghubetina
raghubetina merged commit 01bce6a into main Jul 31, 2026
4 checks passed
@raghubetina
raghubetina deleted the codex/scoped-npm-alpha2 branch July 31, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant