Skip to content
Open
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
2 changes: 2 additions & 0 deletions openspec/changes/nightly-cli-builds/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-19
215 changes: 215 additions & 0 deletions openspec/changes/nightly-cli-builds/design.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions openspec/changes/nightly-cli-builds/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Why

Work that has merged to `main` but has not been released is unreachable. `@taskless/cli` is a workspace package with a build step, so there is no `npx` incantation against a branch, a tag, or a tarball that a person would realistically type — the only installable artifact is the last release. Between releases, "does the fix actually work when installed?" cannot be answered by anyone, including us.

At the same time `release.yml` has grown two release designs in one file. The credential-free `version` job that consumes untrusted changeset text and the OIDC-credentialed `publish` job share a trigger, a concurrency group, and a header comment that is now wrong about the environment it describes. Adding a third design — an unattended nightly publish — to that file would make the trust story harder to read at exactly the moment it needs to be clearer.

## What Changes

- **Publish `@taskless/cli-nightly` from `main`.** Same source, same `bin: taskless`, published under a different name so nightlies never appear in `@taskless/cli`'s version history. Built from `main` only: the code has already passed review, and no contributor-authored text reaches the credentialed job.
- **Version as `n.m.k-yyyymmddhhmmssx<sha>`** (e.g. `0.11.0-20260818123456x05b3c88`), where `n.m.k` is the bump `main`'s pending changesets propose. The timestamp sorts; the SHA identifies and dedupes.
- **Gate on two questions, in order:** are any changesets pending — any `.changeset/*.md` other than `README.md` (nothing pending, nothing to build), and has this SHA already been published?
- **Split `release.yml` into four workflows,** one release design each: `release-cli-changeset.yml`, `release-cli.yml`, `release-vale.yml`, `release-cli-nightly.yml`.
- **Add an `npm-autopublish` environment** for flows that publish without a human click, and **move the Vale platform packages into it.** `@taskless/cli` keeps `npm-production` and its required reviewer.
- **Correct `release.yml`'s stale header claim** that `npm-production` has no required reviewers. It does — confirmed via `gh api repos/taskless/cli/environments` — and the comment has been asserting the opposite.

**Two prerequisites are human-gated and cannot be worked around by an implementer:** the `npm-autopublish` environment must be created in repository settings, and a trusted-publisher binding for `@taskless/cli-nightly` must be registered on npm. Trusted publishing is configured per package and the package does not exist yet, so the first publish is a deliberate manual step. Until both are done the nightly workflow cannot be verified end to end.

**Delivery is stacked, merging forward, in three PRs: the workflow split, then the nightly, then the Vale move.** The nightly comes before the Vale move deliberately. `npm-autopublish` is an unproven credential path, and Vale's six trusted-publisher bindings were registered against `npm-production` — if they turn out to be environment-scoped, migrating Vale first breaks a working release path with no stored token to fall back on. The nightly exercises the same environment and the same OIDC handshake on a package where a failed first publish costs nothing. See design D8 for the prerequisites, D9 for the ordering, and D10 for the shape.

## Capabilities

### New Capabilities

- `cli-nightly-builds`: publishing every unreleased `main` commit as `@taskless/cli-nightly`, including the version scheme, the two gates, and the guarantee that a nightly is a drop-in for the release it anticipates.

### Modified Capabilities

- `infrastructure`: the release flow becomes one workflow per release design, each with a self-contained trust story; publishing environments are distinguished by whether a human click gates them; the concurrency guarantee is narrowed to the flow that needs it.
- `vale-binary-packages`: the Vale release moves from `npm-production` to `npm-autopublish`, making explicit that its review gate is the manifest-update pull request rather than an environment approval.

## Impact

- **Removed**: `.github/workflows/release.yml`.
- **Added**: `.github/workflows/release-cli-changeset.yml` (Version Packages PR, no credential), `.github/workflows/release-cli.yml` (publishes `@taskless/cli`, `npm-production`, approval), `.github/workflows/release-cli-nightly.yml` (nightly, `npm-autopublish`), and a nightly pack script under `.github/scripts/`.
- **Renamed**: `.github/workflows/vale-binaries.yml` → `.github/workflows/release-vale.yml`, with its `publish` job moved to `npm-autopublish`.
- **Outside the repository**: the `npm-autopublish` GitHub environment, and an npm trusted-publisher binding for `@taskless/cli-nightly`.
- **Unchanged**: `packages/cli/package.json` as committed. The rename to `@taskless/cli-nightly` is a pack-time rewrite, the way `vale-prepare.cjs` already stamps the Vale packages. `optionalDependencies` are untouched — a nightly points at the same published, pinned Vale and ast-grep platform packages the release does.
- **Out of scope**: pruning old nightly versions from the registry.

**Tracking:** taskless/cli#111
116 changes: 116 additions & 0 deletions openspec/changes/nightly-cli-builds/specs/cli-nightly-builds/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
## ADDED Requirements

### Requirement: Unreleased work on the default branch is installable as a nightly package

The CLI SHALL be published as `@taskless/cli-nightly` from commits on the default branch that carry unreleased work, so that merged-but-unreleased behavior can be installed and exercised. Nightlies SHALL NOT be published from pull requests: only code that has already merged, and therefore already passed review, is eligible.

The nightly SHALL be built from the same source and the same build as the release it anticipates, differing only in the published package name and version. Its executable SHALL remain `taskless`, so every documented invocation works unchanged against a nightly.

#### Scenario: A push to the default branch with unreleased work publishes a nightly

- **WHEN** a commit is pushed to the default branch and release-pending metadata exists for the CLI
- **THEN** `@taskless/cli-nightly` SHALL be published for that commit

#### Scenario: A pull request publishes nothing

- **WHEN** a pull request is opened, updated, or synchronized
- **THEN** no nightly SHALL be published, and no job holding a publishing credential SHALL be instantiated for it

#### Scenario: A nightly is a drop-in for the release it anticipates

- **WHEN** a nightly is installed
- **THEN** it SHALL provide the `taskless` executable
- **AND** it SHALL resolve the same pinned platform dependencies as the corresponding release

### Requirement: A nightly version names the release it anticipates, the time, and the commit

A nightly version SHALL take the form `<n.m.k>-<yyyymmddhhmmss>x<sha>`, where `n.m.k` is the version the default branch's pending release metadata proposes, `yyyymmddhhmmss` is the build time, and `sha` is the short commit hash.

The pending release metadata describes every package it releases, so `n.m.k` SHALL be selected by matching the CLI package's name, and SHALL NOT be taken by position. Position is correct only while the CLI is the sole package under release management, and a version taken from another package publishes successfully while naming a release that was never proposed.

The timestamp SHALL precede the commit hash, so that lexical comparison of the prerelease identifier orders builds chronologically. The prerelease identifier SHALL contain a non-digit separator between the timestamp and the hash, so that the identifier is never all-digits — an all-digit prerelease identifier is compared numerically and may not begin with a zero, which a hash beginning with `0` would otherwise violate.

#### Scenario: A nightly version is stamped

- **WHEN** pending release metadata proposes `0.11.0`, the build time is `2026-08-18T12:34:56Z`, and the short commit hash is `05b3c88`
- **THEN** the published version SHALL be `0.11.0-20260818123456x05b3c88`

#### Scenario: A hash beginning with zero produces a valid version

- **WHEN** the short commit hash consists only of digits and begins with `0`
- **THEN** the published version SHALL still be a valid semantic version

#### Scenario: The anticipated version is selected by package, not by position

- **WHEN** the pending release metadata describes more than one package
- **THEN** the nightly version SHALL use the entry naming the CLI package

#### Scenario: Nightlies sort chronologically

- **WHEN** two nightlies of the same `n.m.k` are compared
- **THEN** the one built later SHALL sort after the one built earlier

### Requirement: A nightly is installable by default

Every nightly version is a semantic-version prerelease, which a registry does not place on the default install tag on its own. Publishing SHALL therefore explicitly assign the default tag, so that installing the package without a version installs the most recent nightly.

#### Scenario: Installing without a version resolves the latest nightly

- **WHEN** `@taskless/cli-nightly` is installed with no version or tag specified
- **THEN** the most recently published nightly SHALL be installed

### Requirement: A nightly build is bounded by pending release metadata and by the commit

Two gates SHALL decide whether a nightly is built, evaluated in this order.

First, whether any release metadata is pending. When none is pending, nothing is unreleased, there is no proposed version to name, and no nightly SHALL be built. This gate SHALL be evaluated before any dependency installation, so the common case exits at the cheapest possible point.

Second, whether this commit already has a nightly. Published versions SHALL be queried and tested for one whose prerelease identifier ends with the short commit hash; if one exists, no nightly SHALL be built.

The proposed `n.m.k` SHALL be read from the release tool's structured output file rather than from its console output, which also carries unrelated diagnostics.

#### Scenario: Nothing pending publishes nothing

- **WHEN** a commit is pushed to the default branch and no release metadata is pending
- **THEN** no nightly SHALL be published
- **AND** the run SHALL exit before installing dependencies

#### Scenario: The release merge publishes the release and no nightly

- **WHEN** the version pull request merges, consuming all pending release metadata and bumping the package version
- **THEN** the real release SHALL be published
- **AND** no nightly SHALL be published, with no rule special-casing that commit

#### Scenario: A re-run of an already-built commit publishes nothing

- **WHEN** the nightly flow runs again for a commit that already has a published nightly
- **THEN** no nightly SHALL be published

#### Scenario: A chore commit alongside pending work still yields a nightly

- **WHEN** a commit that changes no CLI source is pushed while release metadata is pending
- **THEN** a nightly SHALL be published for that commit, because the commit — and therefore the artifact it describes — is new

### Requirement: Nightlies do not enter the released package's history

Nightlies SHALL be published under a package name distinct from the released CLI, so that the released package's version history contains only releases. The rename SHALL be applied when the publishable artifact is produced, leaving the package manifest in version control unchanged.

#### Scenario: The released package carries no nightly versions

- **WHEN** the published versions of the released CLI package are listed
- **THEN** no nightly version SHALL appear among them

#### Scenario: The committed manifest is unchanged

- **WHEN** the repository is inspected after a nightly is published
- **THEN** the CLI package manifest in version control SHALL still declare the released package name and version

### Requirement: A nightly publish carries the same attestation as a release

Nightly publishing SHALL authenticate with a short-lived credential minted for the run rather than a stored registry token, and SHALL attach build provenance.

#### Scenario: A nightly is published with provenance

- **WHEN** a nightly is published
- **THEN** the published version SHALL carry a build-provenance attestation
- **AND** no long-lived registry token SHALL be present in the environment
74 changes: 74 additions & 0 deletions openspec/changes/nightly-cli-builds/specs/infrastructure/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## ADDED Requirements

### Requirement: Each release flow lives in its own workflow file

Every release flow SHALL be defined in a workflow file of its own, carrying exactly one release design and a self-contained account of its trust boundary. A workflow file SHALL NOT combine a flow that consumes contributor-authored text with a flow that holds a publishing credential.

There SHALL be one workflow for opening the version pull request, one for publishing the CLI, one for publishing the Vale platform packages, and one for publishing the CLI nightly.

#### Scenario: The version flow holds no credential

- **WHEN** inspecting the workflow that runs the version bump and opens the version pull request
- **THEN** it SHALL request no OIDC identity, reference no publishing environment, and contain no publish step

#### Scenario: Each publishing flow is separately readable

- **WHEN** inspecting any workflow that publishes to the registry
- **THEN** its trust boundary — what it may publish, what gates it, and where its credential comes from — SHALL be documented in that file, without reference to another workflow's reasoning

#### Scenario: Workflow documentation matches the live configuration

- **WHEN** a workflow's comments describe the approval policy of the environment it uses
- **THEN** that description SHALL match the environment's configured policy

### Requirement: A credential-free gate decides whether a credentialed publish job exists

Each publishing flow SHALL decide whether there is anything to publish in a job holding no publishing credential and no OIDC identity, and SHALL instantiate the credentialed job only when that gate says yes. The gate SHALL live in the same workflow file as the job it protects.

#### Scenario: An ordinary push instantiates no credentialed job

- **WHEN** a commit is pushed to the default branch and nothing needs publishing
- **THEN** no job holding an OIDC identity or referencing a publishing environment SHALL run

#### Scenario: The gate and the job it protects are not separated

- **WHEN** inspecting a publishing workflow
- **THEN** the gate deciding whether to publish SHALL be defined in that same file

### Requirement: Publishing environments are distinguished by whether a human approves each release

The repository SHALL define two publishing environments. One SHALL require a human reviewer and SHALL be used by flows where approval decides what users receive by default. The other SHALL require no reviewer and SHALL be used by flows whose review gate is code review or a reviewed pull request, and whose output reaches no user until a separate reviewed change adopts it.

The reviewer-free environment SHALL still carry a branch policy restricting it to the default branch, and SHALL remain the audit boundary and the binding point for the registry's trusted-publisher configuration.

#### Scenario: The released CLI requires an approval

- **WHEN** the released CLI is published
- **THEN** the run SHALL use the environment with a required reviewer

#### Scenario: Unattended flows use the reviewer-free environment

- **WHEN** a flow publishes without a human click
- **THEN** it SHALL use the reviewer-free environment
- **AND** that environment SHALL restrict deployments to the default branch

#### Scenario: A publishing name has a trusted publisher before it is used

- **WHEN** a workflow publishes a package name for the first time
- **THEN** a trusted-publisher binding for that name SHALL already be registered, since the binding is per package and cannot exist before the name does

### Requirement: Release flows are serialized only where a race can corrupt shared state

The workflow that opens and updates the version pull request SHALL run under a concurrency group, because two runs racing on that branch is a real failure.

Publishing workflows SHALL NOT be serialized merely because they publish. Where a credential-free gate makes a duplicate run a no-op, the flow SHALL run unserialized, and the residual window in which two runs both observe "not yet published" SHALL be handled by treating a publish failure as possibly-already-published rather than as an error.

#### Scenario: The version flow is serialized

- **WHEN** two commits are pushed to the default branch in quick succession
- **THEN** the version pull request flow SHALL process them one at a time

#### Scenario: A duplicate publish run is a no-op

- **WHEN** two runs of a publishing flow evaluate the same commit or version
- **THEN** at most one artifact SHALL be published, and the other run SHALL neither fail nor publish a duplicate
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## MODIFIED Requirements

### Requirement: Platform packages are released by their own workflow, tracking upstream

Platform packages SHALL be versioned and published by a workflow dedicated to them, independent of the workflow that releases packages managed by changesets. That workflow SHALL compare the latest upstream Vale release against what the repository has already published, and SHALL publish only when upstream is ahead.

A published-version check cannot bound these runs — every run stamps a previously unused timestamp — so the upstream comparison SHALL be what prevents redundant publishing.

That workflow SHALL publish without a human approval step, using the reviewer-free publishing environment. Its review gate is the manifest-update pull request, where a human reviews the upstream version and every checksum before anything may be published; an environment approval would be a second copy of a gate that already exists, at a point where nothing is being decided. The publish remains bounded by the branch policy on that environment and by the requirement below that a published platform package changes no consumer.

#### Scenario: Upstream unchanged publishes nothing

- **WHEN** the workflow runs and the latest upstream Vale release is already published as a platform package
- **THEN** no package is versioned or published

#### Scenario: A new upstream release opens a pull request rather than publishing

- **WHEN** the workflow runs and upstream Vale is ahead of what the repository has published
- **THEN** it opens a pull request updating the pinned Vale version and the committed checksums, and publishes nothing

#### Scenario: Merging the update publishes the set

- **WHEN** that pull request is merged
- **THEN** every supported platform package is stamped with the same version and published together, verified against the checksums that were just reviewed

#### Scenario: Publishing needs no approval click

- **WHEN** the publish phase runs after a merged manifest update
- **THEN** it SHALL proceed without waiting for an environment approval
- **AND** it SHALL run in the reviewer-free publishing environment, restricted to the default branch

#### Scenario: Ordinary pushes do not publish platform packages

- **WHEN** a commit is pushed to the default branch
- **THEN** the changeset-managed release flow publishes no platform package
Loading
Loading