Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 53 additions & 21 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<package-version>`. For version `0.1.0-alpha.1`, the tag is `v0.1.0-alpha.1`.
then create and push `v<package-version>`. 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.

Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand Down
4 changes: 2 additions & 2 deletions test/package.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down