From 8f53c9fc4194f8093541fa3505712c84775f8d65 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Fri, 31 Jul 2026 17:44:19 -0500 Subject: [PATCH] Prepare scoped npm prerelease 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. --- README.md | 4 ++- RELEASING.md | 74 ++++++++++++++++++++++++++++------------ SECURITY.md | 2 +- package-lock.json | 8 ++--- package.json | 6 ++-- scripts/smoke-package.js | 4 +-- test/package.test.js | 4 +-- 7 files changed, 68 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ce0c66c..c6ee7ab 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,12 @@ prereleases, and publishing the CLI does not make the wider First Draft service Once npm reports a public alpha, install the current prerelease explicitly: ```sh -npm install --global firstdraft@next +npm install --global @firstdraft.com/cli@next firstdraft --version ``` +The npm package is `@firstdraft.com/cli`; it installs the `firstdraft` executable. + There is intentionally no stable `latest` release yet. Pin an exact prerelease version instead of `next` when a repeatable installation matters. Remote Plan push, status, and compilation commands require a compatible First Draft service and are currently intended for coordinated trials. diff --git a/RELEASING.md b/RELEASING.md index 8350160..8844e5f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,38 +11,70 @@ Before the first release, a repository administrator must: the public release refs anonymously. 2. Protect `main` with pull-request and CI requirements, and add a `v*` tag ruleset that restricts tag creation, update, and deletion. -3. Create a GitHub environment named `npm`, restrict it to release tags, require a reviewer, prevent self-review, - and add the environment variable `NPM_RELEASE_ENABLED=true`. The workflow fails before publishing when this - variable is absent. -4. Confirm that the bootstrap publisher account has write-protecting 2FA enabled. The first publish creates this - unscoped package under that account; organization access cannot be granted before the package exists. -5. Create a one-day granular npm token with read/write access to All Packages, no organization-management access, - and bypass 2FA enabled. A new unscoped package cannot yet be selected individually. Add it directly as the `npm` - environment secret `NPM_TOKEN`; never put it in an Issue, chat, workflow file, repository file, or command - history. +3. Create a GitHub environment named `npm`, restrict it to release tags, require an explicit reviewer, disable + administrator bypass, and add the environment variable `NPM_RELEASE_ENABLED=true`. The workflow fails before + publishing when this variable is absent. +4. Confirm that the `firstdraft.com` npm organization exists and that the bootstrap publisher belongs to it with + permission to publish public packages. The publisher account must have write-protecting 2FA enabled. Before the + first scoped tag, verify the authenticated identity, organization membership, and absence of an existing package: + + ```sh + 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. + +5. Create a one-day granular npm token with read/write access limited to the `@firstdraft.com` scope, no + organization-management access, and bypass 2FA enabled. The not-yet-created package cannot be selected + individually. Add the token directly as the `npm` environment secret `NPM_TOKEN`; never put it in an Issue, + chat, workflow file, repository file, or command history. The token is a one-time bootstrap credential. After the package exists, use the repository-pinned Node.js 24.18.0 -toolchain with npm 11.16.0 to give the npm organization durable read/write access and configure trusted publishing: +toolchain with npm 11.16.0 to verify the organization's durable read/write access. Grant it only if the package did +not inherit access for the `developers` team: ```sh npm --version -npm access grant read-write firstdraft.com:developers firstdraft +npm access list packages firstdraft.com:developers '@firstdraft.com/cli' --json +npm access grant read-write firstdraft.com:developers '@firstdraft.com/cli' ``` +Using an interactive npm login backed by the account's 2FA, configure trusted publishing for the exact package, +repository, workflow, and protected environment. Do not use the bypass-2FA bootstrap token for trust setup: + ```sh -npm trust github firstdraft \ +npm trust github '@firstdraft.com/cli' \ --repository firstdraft/cli \ --file publish.yml \ --environment npm \ --allow-publish -npm trust list firstdraft +npm trust list '@firstdraft.com/cli' ``` Confirm the listed relationship identifies `firstdraft/cli`, `publish.yml`, the `npm` environment, and publish permission. Before creating another release tag, merge a follow-up pull request that removes the `NODE_AUTH_TOKEN` environment from the publish step. Then remove the GitHub secret, revoke the bootstrap token, and configure the -package to disallow token publication. The workflow continues through GitHub OIDC without a persistent npm -credential. +package to disallow token publication: + +```sh +npm access set mfa=publish '@firstdraft.com/cli' +``` + +Confirm that the package's npm Publishing access now requires 2FA and disallows tokens. The workflow continues +through GitHub OIDC without a persistent npm credential. Apply this restriction only after the trusted publisher +has been verified. + +## Previous publication attempt + +The immutable `v0.1.0-alpha.1` tag records the first reviewed release candidate. On July 31, 2026, npm rejected its +unscoped `firstdraft` name as too similar to the existing `first-draft` package before creating a registry package. +Do not move or reuse that tag or version. The first organization-scoped candidate is `@firstdraft.com/cli` version +`0.1.0-alpha.2`. ## Prepare a release @@ -62,7 +94,7 @@ credential. ## Publish The manual boundary is creation of the version tag. From an up-to-date, clean `main`, verify the intended commit and -then create and push `v`. For version `0.1.0-alpha.1`, the tag is `v0.1.0-alpha.1`. +then create and push `v`. For version `0.1.0-alpha.2`, the tag is `v0.1.0-alpha.2`. Push one release tag at a time; the workflow serializes publication, but GitHub retains at most one pending run in a concurrency group. @@ -79,7 +111,7 @@ workflow from the tagged commit. Before approving the `npm` deployment, the revi - The tag, package version, and commit SHA are the intended release. - The commit is a known reviewed state in protected `main` history and its required checks passed. - `.github/workflows/publish.yml` at that commit is the reviewed workflow, still selects the `npm` environment, and - publishes only under `next` with provenance. + publishes the public `@firstdraft.com/cli` package only under `next` with provenance. - The unprivileged verification job passed for that exact commit. Do not move or reuse a release tag. If the tagged commit is not a first-parent state of `main`, merge the intended @@ -91,13 +123,13 @@ After publication, inspect the registry before retrying any reported failure; th the exact version, `next` dist-tag, integrity metadata, and provenance metadata: ```sh -npm view firstdraft@0.1.0-alpha.1 \ +npm view '@firstdraft.com/cli@0.1.0-alpha.2' \ version dist.integrity dist.shasum repository.url engines bin --json -npm dist-tag ls firstdraft +npm dist-tag ls '@firstdraft.com/cli' ``` -Install `firstdraft@0.1.0-alpha.1` into a fresh temporary prefix, confirm `firstdraft --version`, compare the packed -file list with the release workflow, and run `npm audit signatures` after an exact installation. +Install `@firstdraft.com/cli@0.1.0-alpha.2` into a fresh temporary prefix, confirm `firstdraft --version`, compare the +packed file list with the release workflow, and run `npm audit signatures` after an exact installation. A published version cannot be overwritten or reused. For a bad release, move `next` to a known-good version, deprecate the bad version, and publish a corrected higher version. Treat unpublishing as an exceptional incident diff --git a/SECURITY.md b/SECURITY.md index b21b9f6..eb05054 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,6 +6,6 @@ sensitive details in a public Issue. ## Supported versions -Before the first public npm publication, there is no supported release. During alpha, only the prerelease currently +No version is supported until the first public prerelease is published. During alpha, only the prerelease currently identified by npm's `next` tag receives security fixes. Earlier prereleases and unreleased source snapshots are not supported release lines. The alpha interface may change without backward compatibility between versions. diff --git a/package-lock.json b/package-lock.json index 6b8e629..c6faf64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "firstdraft", - "version": "0.1.0-alpha.1", + "name": "@firstdraft.com/cli", + "version": "0.1.0-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "firstdraft", - "version": "0.1.0-alpha.1", + "name": "@firstdraft.com/cli", + "version": "0.1.0-alpha.2", "license": "MIT", "bin": { "firstdraft": "bin/firstdraft.js" diff --git a/package.json b/package.json index 88ebfa7..36c4a0c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "firstdraft", - "version": "0.1.0-alpha.1", + "name": "@firstdraft.com/cli", + "version": "0.1.0-alpha.2", "description": "Command-line interface for First Draft", "license": "MIT", "type": "module", "bin": { - "firstdraft": "./bin/firstdraft.js" + "firstdraft": "bin/firstdraft.js" }, "files": [ "bin", diff --git a/scripts/smoke-package.js b/scripts/smoke-package.js index 5cda37b..3472dde 100644 --- a/scripts/smoke-package.js +++ b/scripts/smoke-package.js @@ -16,14 +16,14 @@ import path from "node:path"; const npmCli = requiredEnvironmentVariable("npm_execpath"); -/** @type {{version: string}} */ +/** @type {{name: string, version: string}} */ const packageMetadata = JSON.parse(readFileSync("package.json", "utf8")); const temporaryDirectory = mkdtempSync(path.join(tmpdir(), "firstdraft-cli-")); const installationDirectory = path.join(temporaryDirectory, "installation"); const packedExecutable = path.join( installationDirectory, "node_modules", - "firstdraft", + ...packageMetadata.name.split("/"), "bin", "firstdraft.js", ); diff --git a/test/package.test.js b/test/package.test.js index 1141fe4..357f88d 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -44,10 +44,10 @@ function releaseSourceChecks(jobSource) { } test("package metadata preserves the audited runtime boundary", () => { - assert.equal(metadata.name, "firstdraft"); + assert.equal(metadata.name, "@firstdraft.com/cli"); assert.equal(metadata.type, "module"); assert.equal(metadata.engines.node, ">=22.0.0"); - assert.deepEqual(metadata.bin, { firstdraft: "./bin/firstdraft.js" }); + assert.deepEqual(metadata.bin, { firstdraft: "bin/firstdraft.js" }); assert.deepEqual(metadata.files, ["bin", "src"]); assert.equal(metadata.scripts.test, "node scripts/run-tests.js");