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
68 changes: 59 additions & 9 deletions .github/workflows/release-vale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
#
Expand Down Expand Up @@ -269,10 +290,39 @@ 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.
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
Comment thread
thecodedrift marked this conversation as resolved.
permissions:
contents: read
id-token: write # OIDC → short-lived npm auth + build provenance
Expand Down
13 changes: 13 additions & 0 deletions openspec/specs/vale-binary-packages/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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

- **WHEN** the workflow runs and the latest upstream Vale release is already published as a platform package
Expand All @@ -106,6 +108,17 @@ 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: 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
Expand Down
Loading