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");