RESEARCH · research.md
Research: First npm Release, Safe Release Automation, and Documentation Site
Raw markdown
# Research: First npm Release, Safe Release Automation, and Documentation Site
**Feature**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md) | **Date**: 2026-09-27
Each decision lists what was chosen, why, and what else was considered. Facts marked
*measured* were checked in the development container on 2026-09-27.
## R1. Package name and registry state
**Decision**: Publish as the unscoped package `speckit-eye`, public access.
**Rationale**: *Measured*: `npm view speckit-eye` returns `E404`, so the name is free. It
matches the binary name and every command in the README (`npx speckit-eye …`).
**Alternatives considered**: A scope (`@serafac/speckit-eye`): it would make `npx speckit-eye`
fail and every documented command longer. Kept only as the fallback if the name is taken before
the first release (the release stops at R6's "not yet published" check, and the guide says what
to do).
## R2. docmd package and version
**Decision**: Use `@docmd/core`, pinned exactly (`0.9.7`) as a dev dependency, run through the
`docmd` binary from the lockfile.
**Rationale**: *Measured*: `@mgks/docmd` (the name most guides still show) is marked
"Discontinued from 0.8.x, use @docmd/core instead" and only re-exports `@docmd/core`.
`@docmd/core` 0.9.7 builds this repository's `docs/` in about 1.4 s, needs Node ≥ 20 (the repo
needs ≥ 22), is MIT-licensed, and has `docmd validate`, which exits 1 on broken relative links
(used as the PR check, FR-023). Pinning exactly, with the lockfile, means the site build cannot
change underneath us (FR-017); Dependabot proposes upgrades (R13).
*Measured*: with `pnpm install`, docmd works although the install scripts of `esbuild` and
`@docmd/engine-rust` are not run (docmd falls back to its JS engine, "Rust engine not supported
on this platform"). Both are added to `allowBuilds` as `false` in `pnpm-workspace.yaml`,
following the existing `@parcel/watcher` entry, so no install script of the docs toolchain runs.
**Alternatives considered**: `npx @docmd/core` in the workflow without a dependency: unpinned,
not in the lockfile, and not reproducible locally. VitePress, Docusaurus, MkDocs: the user asked
for docmd.
## R3. docmd configuration
**Decision**: One small `docmd.config.js` at the repository root:
- `title: "speckit-eye"`, `src: "docs"`, `out: "site"`.
- `url` read from `package.json` `homepage` (the site URL has one definition, §III). docmd
derives the base path (`/speckit-eye/`) from its path; generated links are relative (`./usage/`),
*measured*.
- `markdown: { breaks: false, linkify: false }`. *Measured*: the defaults (`breaks: true`,
`linkify: true`) turn every hard-wrapped line of our docs into a `<br>` and turn prose like
"spec.md" into a link to `https://spec.md` (`.md` is a country domain).
- `navigation`: explicit order — Home, Usage, Hosting a snapshot, Architecture, Releasing,
plus external entries "Contributing" (`CONTRIBUTING.md`), "Development" (`DEVELOPMENT.md`), "Project status & live demo" (`/status/`), "npm", "GitHub".
- The `ai` and `analytics` plugins are turned off: no chat widget, no tracking, nothing loaded
from other servers. The default search, sitemap, SEO, `llms.txt` outputs stay (zero config).
**Rationale**: The smallest config that renders our existing Markdown correctly (KISS).
**Alternatives considered**: No config file (docmd's zero-config mode): renders `<br>` in every
paragraph and bogus `spec.md` links, and no fixed navigation order.
## R4. Documentation sources and what moves
**Decision**:
- `docs/index.md` (new) is the site's home page: what speckit-eye is, install, quick start, links
to npm, GitHub and the dashboard. It links to the other pages; it does not copy the README
(§III).
- `docs/releasing.md` (new) is the release guide (FR-018). `DEVELOPMENT.md`'s "Release process"
section is reduced to a summary and a link (FR-019); the existing release checklist moves into
the guide, where the automated checks now enforce it.
- `docs/contributing.md` is **not** created: the site navigation links to `CONTRIBUTING.md` and
`DEVELOPMENT.md` on GitHub instead of duplicating them.
- The screenshots move from `docs/*.png` to `docs/assets/`. *Measured*: docmd copies only
`docs/assets/` into the site (`assets/`); images next to the Markdown are not copied. README's
`<picture>` sources are updated to the new paths.
- Links from `docs/` into `specs/` (e.g. `../specs/001-…/contracts/cli.md`) become absolute
GitHub URLs (`https://github.com/SerafAC/speckit-eye/blob/main/specs/…`). *Measured*:
`docmd validate` reports them as broken because `specs/` is not part of the site. Absolute
links work both on GitHub and on the site.
- *Measured*: `docmd validate` also flags the two example links written inside inline code in
`docs/usage.md` ("Links to other documents", `./plan.md`, `contracts/cli.md`). These are
rephrased so that the example paths are not in link position, keeping the meaning.
**Rationale**: `docs/` stays the single source (FR-020); every guide is reachable from the home
page in one click (SC-006).
## R5. Project dashboard on the site (`/status/`)
**Decision**: After `docmd build`, run the tool from the same checkout:
`node bin/speckit-eye.js --build . --out site/status --base <site base>status/ --home <site URL>`.
A small script, `scripts/build-site.js`, derives base and home URL from `package.json`
`homepage` and runs both builds (`pnpm run docs:build`). It deletes `site/` first, so the
dashboard's "refuses a non-empty folder it did not create" rule is never hit.
**Rationale**: The dashboard is built from the commit being deployed, so it is both the project
status and a demo of unreleased behavior (spec assumption). `--out site/status` satisfies the
tool's output rules (not the project folder, not inside `specs/` or `.specify/`). The footer
already shows the version and "generated <time>".
**Alternatives considered**: Building with the published npm version (`npx speckit-eye@latest`):
would not exist before the first release and would lag behind `specs/`. Hard-coding
`/speckit-eye/status/` in a package script: duplicates the site URL (§III).
## R6. A way back from the dashboard to the docs (FR-027): new `--home <url>` build option
**Decision**: Add a build-only option `--home <url>` to speckit-eye. When given, the sidebar
(and the document-page rail and mobile menu) shows a "Home" link to that URL, under the
project name. Only `http:` and `https:` URLs are accepted (exit code 2 otherwise). Without the
option, pages are byte-for-byte unchanged.
**Rationale**: The dashboard pages are produced by speckit-eye, so the only clean way to put a
link on them is an option of the tool. It is useful to every project that hosts a snapshot next
to its own docs, it is optional, and it stays within the strict CSP (a plain link).
**Alternatives considered**: Post-processing the generated HTML in the site script: brittle,
couples the site to the page markup. Relying on the browser's Back button: fails for visitors
who arrive on `/status/` directly from the README (FR-026). A free-form label option: no need
(YAGNI).
## R7. Publishing credentials: trusted publishing, with a one-time bootstrap
**Decision**: Publish from GitHub Actions with **npm trusted publishing** (OIDC,
`id-token: write`; npm CLI ≥ 11.5.1, so the publish job uses Node 24). Provenance is attached
(`--provenance`, and `publishConfig.provenance: true`). For the **first** publish only: npm lets a
trusted publisher be configured only on a package that already exists, so the first run uses a
granular npm token with write access and a 1-day expiry, stored as the `NPM_TOKEN` secret of the
`npm` environment (not a repository secret). Right after 1.0.0 is out, the guide has the
maintainer: configure the trusted publisher (repository `SerafAC/speckit-eye`, workflow
`release.yml`, environment `npm`), set the package to "require two-factor authentication and
disallow tokens", revoke the token and delete the secret. The npm CLI tries OIDC first and falls
back to the token, so the workflow is the same before and after.
**Rationale**: FR-013: no long-lived publish credential; FR-006-style provenance for SC-004.
The bootstrap token lives one day, only in an environment that requires the owner's approval.
If npm offers configuring a trusted publisher before the first publish by the time of release,
the guide says to skip the token.
**Alternatives considered**: A permanent automation token: long-lived secret (FR-013).
Publishing 1.0.0 from the maintainer's laptop: violates FR-004 and would have no provenance.
A placeholder `0.0.0` publish to create the package: publishes a useless version forever.
## R8. What is published is what was tested
**Decision**: The release packs once (`pnpm pack`, which runs `prepack` → `build:assets`),
checks that tarball's file list against an allowed list, uploads it as a workflow artifact, and
the publish job publishes **that file** (`npm publish ./speckit-eye-X.Y.Z.tgz`). Installs use
`pnpm install --frozen-lockfile` everywhere. A `prepublishOnly` script refuses `npm publish` from
the repository folder outside CI (publishing a tarball runs no scripts, so the workflow is not
affected).
**Rationale**: FR-002, FR-004, FR-017. The allowed list (in `scripts/release/pack-check.js`) is the
same list as the release checklist in `DEVELOPMENT.md` today; it is an independent oracle for the
outcome of `package.json` `files`, not a second definition of it.
## R9. Version bump: a script plus a manual workflow that opens a PR
**Decision**: `scripts/release/release.js bump <kind> [--preid rc] [--version X.Y.Z]` (kinds:
`patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`, or `--version` for
an explicit version such as the first release's `1.0.0`, which equals the current version).
It updates `package.json` `version` and `CHANGELOG.md` (moves the Unreleased entries under
`## [X.Y.Z] - YYYY-MM-DD`, leaves an empty `## [Unreleased]`, rewrites the comparison links at the
bottom) and fails with a message if Unreleased is empty. The workflow `bump.yml`
(`workflow_dispatch` with those inputs) runs it on `main`, commits to the fixed branch
`release/next` (force-pushed), and opens the PR — or updates the open one — with `gh`.
The next-version arithmetic is ~30 lines in the script (no `semver` dependency) and unit tested.
**Rationale**: FR-005–FR-008. A fixed branch means a second bump updates the same PR instead of
opening a conflicting one (edge case). `gh` is preinstalled on GitHub runners, so no third-party
action is needed.
**Gotcha and fix**: pushes and PRs made with `GITHUB_TOKEN` do not start other workflows, so CI
would not run on the bump PR. `workflow_dispatch` is the documented exception, so `ci.yml` gains a
`workflow_dispatch` trigger and `bump.yml` runs `gh workflow run ci.yml --ref release/next`; the
check runs attach to the PR's head commit. The repository setting "Allow GitHub Actions to create
and approve pull requests" must be on (one-time setup in the guide).
**Alternatives considered**: `npm version`: creates commits/tags itself and knows nothing about
the changelog. changesets / release-please / semantic-release: each replaces the hand-written
Keep a Changelog file the constitution requires with its own model; far more machinery than one
script (KISS). A GitHub App token for the PR: more setup than the dispatch fix.
## R10. From merged bump to npm: one release workflow
**Decision**: `.github/workflows/release.yml`, triggered by:
1. `push` to `main` touching `package.json` or `CHANGELOG.md` (the automatic path, clarified);
2. `push` of a `v*` tag (a tag pushed by hand, recovery path);
3. `workflow_dispatch` run on a tag ref, no inputs (re-run for an existing tag). There is no dry-run
mode: the rehearsal is the real run up to the approval step, where every check has already run
and rejecting publishes nothing (clarified after analysis).
Jobs:
- **prepare** (read-only): for a push to `main`, decides whether the head commit is a release:
it came from a PR whose head branch is `release/next` (GitHub API: commit → pulls), the tag
`vX.Y.Z` does not exist yet, and `CHANGELOG.md` has a `## [X.Y.Z]` section. Anything else ends
the run successfully with "not a release commit" (edge case: hand-edited versions do not
release). For a tag or dispatch, the version comes from the tag. Outputs version, tag, commit,
dist-tag.
- **ci**: the whole CI workflow via `uses: ./.github/workflows/ci.yml` (`ci.yml` gains
`workflow_call`): unit tests on Node 22 and 24, E2E on Linux/macOS/Windows, WSL (FR-011).
- **verify**: commit is an ancestor of `origin/main` (FR-009); tag, `package.json` and the newest
changelog section agree and the section is not empty (FR-010); the version is not on npm yet
(`npm view speckit-eye@X version` must fail with E404); `pnpm pack` + pack check (FR-002);
release notes extracted from the changelog; tarball and notes uploaded as an artifact.
- **publish** (needs ci + verify; `environment: npm`, which requires the repository owner's
approval, FR-012): creates and pushes the annotated tag if it does not exist yet, then
`npm publish <tarball> --access public --provenance --tag <latest|next>`. The tag is created only after approval, so a rejected or failed check leaves no tag.
- **github-release** (needs publish): `gh release create vX.Y.Z --verify-tag
--notes-file notes.md` (`--prerelease` for pre-releases, FR-015). Kept as its own job so that
"Re-run failed jobs" retries only it, never the publish (edge case).
`concurrency: { group: release, cancel-in-progress: false }`.
**Rationale**: GitHub does not start a new workflow run for a tag pushed with `GITHUB_TOKEN`, so
tagging and publishing must happen in the same run (clarified: nobody types the tag). Every check
runs before the approval request, so the maintainer approves only green releases.
**Alternatives considered**: Tag in a separate job before approval: a rejected release would leave
a tag. A PAT to push the tag and trigger a second workflow: a long-lived credential.
## R11. Release safety hardening
**Decision**:
- `permissions: {}` at workflow level in every workflow, then the minimum per job (FR-014):
`contents: read` by default; `contents: write` only for tag push, PR branch push and release
creation; `pull-requests: write` only in `bump.yml`; `actions: write` only to dispatch CI;
`id-token: write` only in `publish` and the Pages deploy; `pages: write` only in deploy.
- Every third-party action pinned to a full commit SHA with the version in a comment;
`actions/checkout` with `persist-credentials: false` except where the job pushes.
- The `npm` environment: required reviewer = repository owner, self-review allowed (clarified),
deployment branches and tags limited to `main` and `v*`.
- Guide setup steps: a tag ruleset protecting `v*` tags from update and deletion; GitHub
"immutable releases" turned on; private vulnerability reporting turned on.
- Pre-releases publish with the `next` dist-tag, stable with `latest` (FR-016); the dist-tag is
derived from the version (a `-` means pre-release), never typed.
## R12. Documentation deploy workflow
**Decision**: `.github/workflows/pages.yml`:
- Triggers: `push` to `main` and `pull_request`, both filtered to paths that affect the site
(`docs/**`, `docmd.config.js`, `scripts/build-site.js`, `src/**`, `bin/**`, `specs/**`,
`.specify/**`, `package.json`, `pnpm-lock.yaml`, the workflow itself), plus `workflow_dispatch`
(FR-022, and "no needless rebuild").
- Job **build** (always): `pnpm install --frozen-lockfile`, `pnpm run docs:build`,
`pnpm exec docmd validate`; uploads with `actions/upload-pages-artifact`.
- Job **deploy** (only on `main`/dispatch, not PRs): `actions/deploy-pages`, environment
`github-pages`.
- `concurrency: { group: pages, cancel-in-progress: false }`: GitHub keeps only the newest pending
run, and a running deploy is never cut off (FR-024). A failed build never reaches deploy, so the
live site stays (FR-024, edge case).
**Rationale**: GitHub's own Pages actions deploy an artifact without a `gh-pages` branch. If
Pages is not enabled with source "GitHub Actions", `deploy-pages` fails with an explicit error
(edge case); the guide covers the one-time setting.
## R13. Open-source project health files
**Decision**: `CONTRIBUTING.md` (how to propose changes, the Spec Kit flow, quality gates, links
to `DEVELOPMENT.md`), `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), `SECURITY.md` (supported
versions: the latest 1.x minor; report through GitHub private vulnerability reporting),
`.github/ISSUE_TEMPLATE/` (`bug_report.yml`, `feature_request.yml`, `config.yml` with blank issues
off and a contact link to the private advisory form), `.github/pull_request_template.md` (the
constitution's Quality Gates as a checklist), `.github/dependabot.yml` (weekly, `npm` and
`github-actions`, minor/patch grouped). README gains badges (CI, npm version, license) and links
to the docs site and the dashboard. `package.json` gains `repository`, `homepage`, `bugs`,
`keywords`, `author`, `publishConfig`.
The Code of Conduct needs an enforcement contact. It is left for the maintainer to fill in
(task marked as needing their input); the plan does not publish a personal address.
## R14. How this feature is tested (constitution §IV, §V)
**Decision**:
- **Unit** (`node:test`, no I/O): `scripts/release/*` pure functions — next version, changelog
release/notes/link rewriting, release-commit decision, verify rules, pack allowed list;
`scripts/build-site.js` path derivation; the new `--home` option in `args.js` and `layout.js`.
- **E2E** (Playwright, new `tooling` project in Chromium, test names `003 USn FR-xxx …`):
- `release-package.spec.js` (US1): pack, check contents, install the tarball into a temp
folder, run `npx speckit-eye --serve` on a fixture copy, load the overview.
- `release-flow.spec.js` (US2): run `release.js bump/verify/notes` on a temp copy of the repo
files, and each deliberately broken input (SC-003) ends with a non-zero exit and nothing
written.
- `site.spec.js` (US3, US4): `pnpm run docs:build`, serve `site/` under `/speckit-eye/`, check
home links, navigation to every guide, crawl for broken internal links (SC-006), open
`/status/`, see the overview with counts matching `specs/` (SC-007), follow "Home" back.
- `repo-health.spec.js` (US5): community files, templates, README badges and links exist.
- **Workflow rehearsal** (manual, quickstart): the real 1.0.0 release run, reviewed at the approval
step before it publishes anything; plus the negative checks of quickstart §7.
**Rationale**: Workflow YAML itself cannot be unit tested; keeping every decision in tested
scripts leaves the YAML as wiring. The `tooling` project runs these suites once, not in every
browser engine.
Feature: spec.md | Plan: plan.md | Date: 2026-09-27
Each decision lists what was chosen, why, and what else was considered. Facts marked measured were checked in the development container on 2026-09-27.
01 R1. Package name and registry state
Decision: Publish as the unscoped package speckit-eye, public access.
Rationale: Measured: npm view speckit-eye returns E404, so the name is free. It
matches the binary name and every command in the README (npx speckit-eye …).
Alternatives considered: A scope (@serafac/speckit-eye): it would make npx speckit-eye
fail and every documented command longer. Kept only as the fallback if the name is taken before
the first release (the release stops at R6's "not yet published" check, and the guide says what
to do).
02 R2. docmd package and version
Decision: Use @docmd/core, pinned exactly (0.9.7) as a dev dependency, run through the
docmd binary from the lockfile.
Rationale: Measured: @mgks/docmd (the name most guides still show) is marked
"Discontinued from 0.8.x, use @docmd/core instead" and only re-exports @docmd/core.
@docmd/core 0.9.7 builds this repository's docs/ in about 1.4 s, needs Node ≥ 20 (the repo
needs ≥ 22), is MIT-licensed, and has docmd validate, which exits 1 on broken relative links
(used as the PR check, FR-023). Pinning exactly, with the lockfile, means the site build cannot
change underneath us (FR-017); Dependabot proposes upgrades (R13).
Measured: with pnpm install, docmd works although the install scripts of esbuild and
@docmd/engine-rust are not run (docmd falls back to its JS engine, "Rust engine not supported
on this platform"). Both are added to allowBuilds as false in pnpm-workspace.yaml,
following the existing @parcel/watcher entry, so no install script of the docs toolchain runs.
Alternatives considered: npx @docmd/core in the workflow without a dependency: unpinned,
not in the lockfile, and not reproducible locally. VitePress, Docusaurus, MkDocs: the user asked
for docmd.
03 R3. docmd configuration
Decision: One small docmd.config.js at the repository root:
title: "speckit-eye",src: "docs",out: "site".urlread frompackage.jsonhomepage(the site URL has one definition, §III). docmd derives the base path (/speckit-eye/) from its path; generated links are relative (./usage/), measured.markdown: { breaks: false, linkify: false }. Measured: the defaults (breaks: true,linkify: true) turn every hard-wrapped line of our docs into a<br>and turn prose like "spec.md" into a link tohttps://spec.md(.mdis a country domain).navigation: explicit order — Home, Usage, Hosting a snapshot, Architecture, Releasing, plus external entries "Contributing" (CONTRIBUTING.md), "Development" (DEVELOPMENT.md), "Project status & live demo" (/status/), "npm", "GitHub".- The
aiandanalyticsplugins are turned off: no chat widget, no tracking, nothing loaded from other servers. The default search, sitemap, SEO,llms.txtoutputs stay (zero config).
Rationale: The smallest config that renders our existing Markdown correctly (KISS).
Alternatives considered: No config file (docmd's zero-config mode): renders <br> in every
paragraph and bogus spec.md links, and no fixed navigation order.
04 R4. Documentation sources and what moves
Decision:
docs/index.md(new) is the site's home page: what speckit-eye is, install, quick start, links to npm, GitHub and the dashboard. It links to the other pages; it does not copy the README (§III).docs/releasing.md(new) is the release guide (FR-018).DEVELOPMENT.md's "Release process" section is reduced to a summary and a link (FR-019); the existing release checklist moves into the guide, where the automated checks now enforce it.docs/contributing.mdis not created: the site navigation links toCONTRIBUTING.mdandDEVELOPMENT.mdon GitHub instead of duplicating them.- The screenshots move from
docs/*.pngtodocs/assets/. Measured: docmd copies onlydocs/assets/into the site (assets/); images next to the Markdown are not copied. README's<picture>sources are updated to the new paths. - Links from
docs/intospecs/(e.g.../specs/001-…/contracts/cli.md) become absolute GitHub URLs (https://github.com/SerafAC/speckit-eye/blob/main/specs/…). Measured:docmd validatereports them as broken becausespecs/is not part of the site. Absolute links work both on GitHub and on the site. - Measured:
docmd validatealso flags the two example links written inside inline code indocs/usage.md("Links to other documents",./plan.md,contracts/cli.md). These are rephrased so that the example paths are not in link position, keeping the meaning.
Rationale: docs/ stays the single source (FR-020); every guide is reachable from the home
page in one click (SC-006).
05 R5. Project dashboard on the site (/status/)
Decision: After docmd build, run the tool from the same checkout:
node bin/speckit-eye.js --build . --out site/status --base <site base>status/ --home <site URL>.
A small script, scripts/build-site.js, derives base and home URL from package.json
homepage and runs both builds (pnpm run docs:build). It deletes site/ first, so the
dashboard's "refuses a non-empty folder it did not create" rule is never hit.
Rationale: The dashboard is built from the commit being deployed, so it is both the project
status and a demo of unreleased behavior (spec assumption). --out site/status satisfies the
tool's output rules (not the project folder, not inside specs/ or .specify/). The footer
already shows the version and "generated <time>".
Alternatives considered: Building with the published npm version (npx speckit-eye@latest):
would not exist before the first release and would lag behind specs/. Hard-coding
/speckit-eye/status/ in a package script: duplicates the site URL (§III).
06 R6. A way back from the dashboard to the docs (FR-027): new --home <url> build option
Decision: Add a build-only option --home <url> to speckit-eye. When given, the sidebar
(and the document-page rail and mobile menu) shows a "Home" link to that URL, under the
project name. Only http: and https: URLs are accepted (exit code 2 otherwise). Without the
option, pages are byte-for-byte unchanged.
Rationale: The dashboard pages are produced by speckit-eye, so the only clean way to put a link on them is an option of the tool. It is useful to every project that hosts a snapshot next to its own docs, it is optional, and it stays within the strict CSP (a plain link).
Alternatives considered: Post-processing the generated HTML in the site script: brittle,
couples the site to the page markup. Relying on the browser's Back button: fails for visitors
who arrive on /status/ directly from the README (FR-026). A free-form label option: no need
(YAGNI).
07 R7. Publishing credentials: trusted publishing, with a one-time bootstrap
Decision: Publish from GitHub Actions with npm trusted publishing (OIDC,
id-token: write; npm CLI ≥ 11.5.1, so the publish job uses Node 24). Provenance is attached
(--provenance, and publishConfig.provenance: true). For the first publish only: npm lets a
trusted publisher be configured only on a package that already exists, so the first run uses a
granular npm token with write access and a 1-day expiry, stored as the NPM_TOKEN secret of the
npm environment (not a repository secret). Right after 1.0.0 is out, the guide has the
maintainer: configure the trusted publisher (repository SerafAC/speckit-eye, workflow
release.yml, environment npm), set the package to "require two-factor authentication and
disallow tokens", revoke the token and delete the secret. The npm CLI tries OIDC first and falls
back to the token, so the workflow is the same before and after.
Rationale: FR-013: no long-lived publish credential; FR-006-style provenance for SC-004. The bootstrap token lives one day, only in an environment that requires the owner's approval. If npm offers configuring a trusted publisher before the first publish by the time of release, the guide says to skip the token.
Alternatives considered: A permanent automation token: long-lived secret (FR-013).
Publishing 1.0.0 from the maintainer's laptop: violates FR-004 and would have no provenance.
A placeholder 0.0.0 publish to create the package: publishes a useless version forever.
08 R8. What is published is what was tested
Decision: The release packs once (pnpm pack, which runs prepack → build:assets),
checks that tarball's file list against an allowed list, uploads it as a workflow artifact, and
the publish job publishes that file (npm publish ./speckit-eye-X.Y.Z.tgz). Installs use
pnpm install --frozen-lockfile everywhere. A prepublishOnly script refuses npm publish from
the repository folder outside CI (publishing a tarball runs no scripts, so the workflow is not
affected).
Rationale: FR-002, FR-004, FR-017. The allowed list (in scripts/release/pack-check.js) is the
same list as the release checklist in DEVELOPMENT.md today; it is an independent oracle for the
outcome of package.json files, not a second definition of it.
09 R9. Version bump: a script plus a manual workflow that opens a PR
Decision: scripts/release/release.js bump <kind> [--preid rc] [--version X.Y.Z] (kinds:
patch, minor, major, prepatch, preminor, premajor, prerelease, or --version for
an explicit version such as the first release's 1.0.0, which equals the current version).
It updates package.json version and CHANGELOG.md (moves the Unreleased entries under
## [X.Y.Z] - YYYY-MM-DD, leaves an empty ## [Unreleased], rewrites the comparison links at the
bottom) and fails with a message if Unreleased is empty. The workflow bump.yml
(workflow_dispatch with those inputs) runs it on main, commits to the fixed branch
release/next (force-pushed), and opens the PR — or updates the open one — with gh.
The next-version arithmetic is ~30 lines in the script (no semver dependency) and unit tested.
Rationale: FR-005–FR-008. A fixed branch means a second bump updates the same PR instead of
opening a conflicting one (edge case). gh is preinstalled on GitHub runners, so no third-party
action is needed.
Gotcha and fix: pushes and PRs made with GITHUB_TOKEN do not start other workflows, so CI
would not run on the bump PR. workflow_dispatch is the documented exception, so ci.yml gains a
workflow_dispatch trigger and bump.yml runs gh workflow run ci.yml --ref release/next; the
check runs attach to the PR's head commit. The repository setting "Allow GitHub Actions to create
and approve pull requests" must be on (one-time setup in the guide).
Alternatives considered: npm version: creates commits/tags itself and knows nothing about
the changelog. changesets / release-please / semantic-release: each replaces the hand-written
Keep a Changelog file the constitution requires with its own model; far more machinery than one
script (KISS). A GitHub App token for the PR: more setup than the dispatch fix.
10 R10. From merged bump to npm: one release workflow
Decision: .github/workflows/release.yml, triggered by:
pushtomaintouchingpackage.jsonorCHANGELOG.md(the automatic path, clarified);pushof av*tag (a tag pushed by hand, recovery path);workflow_dispatchrun on a tag ref, no inputs (re-run for an existing tag). There is no dry-run mode: the rehearsal is the real run up to the approval step, where every check has already run and rejecting publishes nothing (clarified after analysis).
Jobs:
- prepare (read-only): for a push to
main, decides whether the head commit is a release: it came from a PR whose head branch isrelease/next(GitHub API: commit → pulls), the tagvX.Y.Zdoes not exist yet, andCHANGELOG.mdhas a## [X.Y.Z]section. Anything else ends the run successfully with "not a release commit" (edge case: hand-edited versions do not release). For a tag or dispatch, the version comes from the tag. Outputs version, tag, commit, dist-tag. - ci: the whole CI workflow via
uses: ./.github/workflows/ci.yml(ci.ymlgainsworkflow_call): unit tests on Node 22 and 24, E2E on Linux/macOS/Windows, WSL (FR-011). - verify: commit is an ancestor of
origin/main(FR-009); tag,package.jsonand the newest changelog section agree and the section is not empty (FR-010); the version is not on npm yet (npm view speckit-eye@X versionmust fail with E404);pnpm pack+ pack check (FR-002); release notes extracted from the changelog; tarball and notes uploaded as an artifact. - publish (needs ci + verify;
environment: npm, which requires the repository owner's approval, FR-012): creates and pushes the annotated tag if it does not exist yet, thennpm publish <tarball> --access public --provenance --tag <latest|next>. The tag is created only after approval, so a rejected or failed check leaves no tag. - github-release (needs publish):
gh release create vX.Y.Z --verify-tag --notes-file notes.md(--prereleasefor pre-releases, FR-015). Kept as its own job so that "Re-run failed jobs" retries only it, never the publish (edge case).
concurrency: { group: release, cancel-in-progress: false }.
Rationale: GitHub does not start a new workflow run for a tag pushed with GITHUB_TOKEN, so
tagging and publishing must happen in the same run (clarified: nobody types the tag). Every check
runs before the approval request, so the maintainer approves only green releases.
Alternatives considered: Tag in a separate job before approval: a rejected release would leave a tag. A PAT to push the tag and trigger a second workflow: a long-lived credential.
11 R11. Release safety hardening
Decision:
permissions: {}at workflow level in every workflow, then the minimum per job (FR-014):contents: readby default;contents: writeonly for tag push, PR branch push and release creation;pull-requests: writeonly inbump.yml;actions: writeonly to dispatch CI;id-token: writeonly inpublishand the Pages deploy;pages: writeonly in deploy.- Every third-party action pinned to a full commit SHA with the version in a comment;
actions/checkoutwithpersist-credentials: falseexcept where the job pushes. - The
npmenvironment: required reviewer = repository owner, self-review allowed (clarified), deployment branches and tags limited tomainandv*. - Guide setup steps: a tag ruleset protecting
v*tags from update and deletion; GitHub "immutable releases" turned on; private vulnerability reporting turned on. - Pre-releases publish with the
nextdist-tag, stable withlatest(FR-016); the dist-tag is derived from the version (a-means pre-release), never typed.
12 R12. Documentation deploy workflow
Decision: .github/workflows/pages.yml:
- Triggers:
pushtomainandpull_request, both filtered to paths that affect the site (docs/**,docmd.config.js,scripts/build-site.js,src/**,bin/**,specs/**,.specify/**,package.json,pnpm-lock.yaml, the workflow itself), plusworkflow_dispatch(FR-022, and "no needless rebuild"). - Job build (always):
pnpm install --frozen-lockfile,pnpm run docs:build,pnpm exec docmd validate; uploads withactions/upload-pages-artifact. - Job deploy (only on
main/dispatch, not PRs):actions/deploy-pages, environmentgithub-pages. concurrency: { group: pages, cancel-in-progress: false }: GitHub keeps only the newest pending run, and a running deploy is never cut off (FR-024). A failed build never reaches deploy, so the live site stays (FR-024, edge case).
Rationale: GitHub's own Pages actions deploy an artifact without a gh-pages branch. If
Pages is not enabled with source "GitHub Actions", deploy-pages fails with an explicit error
(edge case); the guide covers the one-time setting.
13 R13. Open-source project health files
Decision: CONTRIBUTING.md (how to propose changes, the Spec Kit flow, quality gates, links
to DEVELOPMENT.md), CODE_OF_CONDUCT.md (Contributor Covenant 2.1), SECURITY.md (supported
versions: the latest 1.x minor; report through GitHub private vulnerability reporting),
.github/ISSUE_TEMPLATE/ (bug_report.yml, feature_request.yml, config.yml with blank issues
off and a contact link to the private advisory form), .github/pull_request_template.md (the
constitution's Quality Gates as a checklist), .github/dependabot.yml (weekly, npm and
github-actions, minor/patch grouped). README gains badges (CI, npm version, license) and links
to the docs site and the dashboard. package.json gains repository, homepage, bugs,
keywords, author, publishConfig.
The Code of Conduct needs an enforcement contact. It is left for the maintainer to fill in (task marked as needing their input); the plan does not publish a personal address.
14 R14. How this feature is tested (constitution §IV, §V)
Decision:
- Unit (
node:test, no I/O):scripts/release/*pure functions — next version, changelog release/notes/link rewriting, release-commit decision, verify rules, pack allowed list;scripts/build-site.jspath derivation; the new--homeoption inargs.jsandlayout.js. - E2E (Playwright, new
toolingproject in Chromium, test names003 USn FR-xxx …):release-package.spec.js(US1): pack, check contents, install the tarball into a temp folder, runnpx speckit-eye --serveon a fixture copy, load the overview.release-flow.spec.js(US2): runrelease.js bump/verify/noteson a temp copy of the repo files, and each deliberately broken input (SC-003) ends with a non-zero exit and nothing written.site.spec.js(US3, US4):pnpm run docs:build, servesite/under/speckit-eye/, check home links, navigation to every guide, crawl for broken internal links (SC-006), open/status/, see the overview with counts matchingspecs/(SC-007), follow "Home" back.repo-health.spec.js(US5): community files, templates, README badges and links exist.
- Workflow rehearsal (manual, quickstart): the real 1.0.0 release run, reviewed at the approval step before it publishes anything; plus the negative checks of quickstart §7.
Rationale: Workflow YAML itself cannot be unit tested; keeping every decision in tested
scripts leaves the YAML as wiring. The tooling project runs these suites once, not in every
browser engine.