From 9cbd921c42146969b172ef9741d79e6ea7bdcd78 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Fri, 21 Aug 2026 08:54:47 -0700 Subject: [PATCH 1/2] ci(vale): publish the platform packages from npm-autopublish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vale publish job moves off `npm-production` and onto the reviewer-free `npm-autopublish` environment. This was deliberately sequenced after the nightly, so the environment and the OIDC handshake would be proven on a package where a mistake is cheap before a working release path moved onto it. Two nightlies have now published automatically through it (0.11.0-20260821002453x3f114d6 and 0.11.0-20260821025240x423363a). The review gate does not move — it was never the approval click. A human reviews the upstream Vale version and all six SHA256 digests on the manifest-update pull request, and only merging that can start a publish. An environment approval is a second copy of that gate asked where nothing is left to decide. The publish stays bounded by the branch policy on `npm-autopublish` (`main` only) and by the standing guarantee that a published platform package changes no consumer until someone bumps the CLI's exact pin. The header also now records that the npm trusted-publisher binding names this workflow's filename and environment, since both are things a future edit could change without noticing what breaks. Restores the `vale-binary-packages` requirement paragraph and the "Publishing needs no approval click" scenario that were withheld when `nightly-cli-builds` was archived, because they asserted behavior that had not shipped yet. It has now shipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .github/workflows/release-vale.yml | 43 ++++++++++++++++----- openspec/specs/vale-binary-packages/spec.md | 8 ++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-vale.yml b/.github/workflows/release-vale.yml index 8f16317d..94154232 100644 --- a/.github/workflows/release-vale.yml +++ b/.github/workflows/release-vale.yml @@ -58,13 +58,34 @@ # tarball through the artifact keeps 0755 intact end to end. # # PUBLISHING IDENTITY is inherited, not reinvented: npm trusted publishing, a -# short-lived OIDC-minted token bound to the `npm-production` environment, with -# no stored NPM_TOKEN anywhere. That binding is registered PER PACKAGE on -# npmjs.com and there is nothing to bind until the package name exists, so the -# FIRST publish of each of the six names is a deliberate one-time manual step by -# a maintainer, who then registers the trusted publisher. This workflow assumes -# that has already happened for every name in the manifest, and there is no -# fallback token path here on purpose. +# short-lived OIDC-minted token, with no stored NPM_TOKEN anywhere. That binding +# is registered PER PACKAGE on npmjs.com, and it names this workflow's FILENAME +# and its environment — so renaming this file or changing `environment:` below +# breaks every publish until the six bindings are re-registered to match. There +# is nothing to bind until the package name exists, so the FIRST publish of each +# of the six names is a deliberate one-time manual step by a maintainer, who +# then registers the trusted publisher. This workflow assumes that has already +# happened for every name in the manifest, and there is no fallback token path +# here on purpose. +# +# APPROVAL POLICY: the environment is `npm-autopublish`, which has NO required +# reviewer — unlike `npm-production`, where @taskless/cli still waits for a +# click. That is not a relaxation, because the review already happened +# somewhere better: the manifest-update pull request IS the gate. A human reads +# the upstream Vale version and all six SHA256 digests there, and only merging +# it can start a publish at all. An environment approval would be a second copy +# of that same gate, asked at a point where nothing is left to decide — the +# bytes were fixed when the digests were reviewed, and a reviewer standing at +# the publish step has no new information to act on. Approvals that decide +# nothing get clicked without being read, which weakens the one that matters. +# +# What still bounds an unattended publish: the deployment branch policy on +# `npm-autopublish` restricts it to `main`, so no branch can reach the credential +# by adding a job that names the environment; and publishing a platform package +# changes no consumer, because the CLI pins each one exactly and a new version +# reaches a user only when someone reviews a bump to that pin. Do not "simplify" +# this back onto `npm-production` to make the two release workflows match — they +# differ on purpose, and the reason is the pin, not the package. # # BOOTSTRAPPING A NEW PACKAGE NAME, once: # @@ -271,8 +292,12 @@ jobs: needs: prepare runs-on: ubuntu-latest # The scoping and audit boundary for the release, and where the npm trusted - # publisher for each @taskless/vale-* package is bound. - environment: npm-production + # publisher for each @taskless/vale-* package is bound. Reviewer-free by + # design (see APPROVAL POLICY in the header): the manifest pull request is + # the gate, and this environment's branch policy still confines the + # credential to `main`. Changing this name requires re-registering all six + # bindings on npmjs.com first — the environment is part of the binding. + environment: npm-autopublish permissions: contents: read id-token: write # OIDC → short-lived npm auth + build provenance diff --git a/openspec/specs/vale-binary-packages/spec.md b/openspec/specs/vale-binary-packages/spec.md index fb0966f4..8e65e350 100644 --- a/openspec/specs/vale-binary-packages/spec.md +++ b/openspec/specs/vale-binary-packages/spec.md @@ -91,6 +91,8 @@ Platform packages SHALL be versioned and published by a workflow dedicated to th 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 @@ -106,6 +108,12 @@ A published-version check cannot bound these runs — every run stamps a previou - **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 From afa1d94bcb3febf6b82d62802a0b6ffdcd24a20d Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Fri, 21 Aug 2026 09:16:38 -0700 Subject: [PATCH 2/2] ci(vale): pin the publish job to main in code, not only in settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding on #136: with the required reviewer gone, the environment's deployment branch policy is the ONLY thing keeping an off-`main` `workflow_dispatch` from publishing. Under `npm-production` there were two independent obstacles; this PR removed one of them, so the remaining one should not also be the only one. The guard duplicates the branch policy on purpose. One copy lives in a settings page that can be edited without review and whose relaxation is an absence — nothing turns red when it happens. The other lives here, next to the reasoning, and cannot change without a diff. `github.ref == 'refs/heads/main'` covers both paths into this job. The `push` path is already filtered to `main`, so the guard is a no-op there; the `workflow_dispatch` path is the real case, since a dispatch runs from whatever ref it was launched on. Only `publish` is guarded. `gate` and `prepare` hold no environment, no id-token, and only `contents: read`, so off-`main` they are a harmless dry run of the digest verification and the pack. Guarding them instead would protect `publish` by inference through `needs:` — one control with a spare, which is the shape being corrected. The spec requirement gains the same constraint, so deleting either copy is a spec violation rather than a tidy-up. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .github/workflows/release-vale.yml | 25 +++++++++++++++++++++ openspec/specs/vale-binary-packages/spec.md | 7 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-vale.yml b/.github/workflows/release-vale.yml index 94154232..a516adac 100644 --- a/.github/workflows/release-vale.yml +++ b/.github/workflows/release-vale.yml @@ -290,6 +290,31 @@ jobs: publish: name: Publish to npm needs: prepare + # DELIBERATE DUPLICATE of the `npm-autopublish` deployment branch policy. + # The duplication IS the point: two independent controls, one of which lives + # in a settings page and one of which lives here, in a file that cannot + # change without code review. Do not delete this as redundant with the + # environment — the environment is exactly what it is defending against. + # + # This became load-bearing when the required reviewer went away. Under + # `npm-production` an off-`main` publish had to get past the branch policy + # AND a human clicking approve; with the reviewer gone the branch policy is + # the only thing left, and a settings edit by someone who does not know it + # is load-bearing silently enables publishing from any branch. That failure + # is an ABSENCE, so nothing goes red when it happens. + # + # One expression covers both ways this job can be reached. The `push` path + # is already `branches: [main]`, so the guard is a no-op there and costs + # nothing. The `workflow_dispatch` path is the real case: a dispatch runs + # from whatever ref it was launched on, and `github.ref` is that ref. + # + # Only `publish` is guarded, on purpose. `gate` and `prepare` hold no + # environment, no id-token, and only `contents: read`, so running them off + # `main` publishes nothing — it is a useful dry run of the digest + # verification and the pack. Guarding them instead would protect this job + # only by inference through `needs:`, which is one control with a spare + # rather than two controls. + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest # The scoping and audit boundary for the release, and where the npm trusted # publisher for each @taskless/vale-* package is bound. Reviewer-free by diff --git a/openspec/specs/vale-binary-packages/spec.md b/openspec/specs/vale-binary-packages/spec.md index 8e65e350..bb4fe4a0 100644 --- a/openspec/specs/vale-binary-packages/spec.md +++ b/openspec/specs/vale-binary-packages/spec.md @@ -91,7 +91,7 @@ Platform packages SHALL be versioned and published by a workflow dedicated to th 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. +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. Because removing the approval leaves the branch restriction as the only control over where a publish may run from, that restriction SHALL be enforced twice — once by the environment's configuration and once by the workflow itself, so that neither can be relaxed without the other noticing. #### Scenario: Upstream unchanged publishes nothing @@ -114,6 +114,11 @@ That workflow SHALL publish without a human approval step, using the reviewer-fr - **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: A publish requested from another branch does nothing + +- **WHEN** a publish is requested manually from a ref other than the default branch +- **THEN** no package is published, and this SHALL hold independently of the publishing environment's configuration + #### Scenario: Ordinary pushes do not publish platform packages - **WHEN** a commit is pushed to the default branch