Skip to content

Commit a33c128

Browse files
committed
fix: harden Release Please patch releases
1 parent 6ee0fdf commit a33c128

12 files changed

Lines changed: 746 additions & 94 deletions

.github/workflows/publish.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest
2727
timeout-minutes: 30
2828
outputs:
29+
artifact-name: ${{ steps.artifact-name.outputs.name }}
2930
dist-tag: ${{ steps.version.outputs.dist-tag }}
3031
release-commit: ${{ steps.trust.outputs.release-commit }}
3132
release-tag: ${{ steps.trust.outputs.release-tag }}
@@ -202,10 +203,16 @@ jobs:
202203
--tag "${{ steps.trust.outputs.release-tag }}"
203204
npm run test:examples -- --tarball "${{ steps.pack.outputs.tarball }}"
204205
npm run test:fixtures -- --tarball "${{ steps.pack.outputs.tarball }}"
206+
- name: Name the attempt-qualified release artifact
207+
id: artifact-name
208+
shell: bash
209+
run: |
210+
set -euo pipefail
211+
echo "name=npm-package-${{ steps.version.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
205212
- name: Upload the verified release artifact
206213
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
207214
with:
208-
name: npm-package-${{ steps.version.outputs.version }}
215+
name: ${{ steps.artifact-name.outputs.name }}
209216
path: ${{ steps.pack.outputs.tarball }}
210217
if-no-files-found: error
211218
retention-days: 30
@@ -280,7 +287,7 @@ jobs:
280287
- name: Download the verified release artifact
281288
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
282289
with:
283-
name: npm-package-${{ needs.verify.outputs.version }}
290+
name: ${{ needs.verify.outputs.artifact-name }}
284291
path: release-artifacts
285292
- name: Publish the exact artifact with provenance
286293
env:

.github/workflows/release-please.yml

Lines changed: 196 additions & 11 deletions
Large diffs are not rendered by default.

AGENTS.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,13 @@ manually against the exact merged release commit, and the publish workflow
8686
completed exact-artifact verification, the bounded live smoke, npm OIDC
8787
publication, and registry verification.
8888

89-
Release Please is disabled after its post-merge run generated an unreviewed
90-
`0.2.0` temporary-branch commit and then failed to create a pull request. The
91-
temporary branch is failure evidence only. Do not merge it, use it as a 0.2
92-
starting point, or re-enable Release Please until a separately authorized task
93-
reviews the post-manual-release and pull-request authorization strategy.
89+
Release Please remains disabled between releases after its post-0.1.0 run
90+
generated an unreviewed `0.2.0` temporary-branch commit and then failed to create
91+
a pull request. The temporary branch is failure evidence only. Do not merge it
92+
or use it as a 0.2 starting point. The authorized 0.1.1 maintenance task repairs
93+
the workflow around the current read-only-default Actions baseline with
94+
action-created pull requests enabled; any later enablement still requires an
95+
explicit maintainer request and the fail-closed checks in `RELEASING.md`.
9496

9597
## Product Contract
9698

ARCHITECTURE.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,34 +114,41 @@ version except `0.1.0-alpha.1` and every dist-tag except `next`.
114114

115115
For normal stable patches, Release Please owns the reviewed version/changelog
116116
PR and the immutable tag and GitHub Release. The configuration uses an explicit
117-
`cometapi` component and stable versioning so a root package does not fall into
118-
the single-package tag-discovery ambiguity encountered during 0.1.0. Because a
117+
`cometapi` component and `always-bump-patch` versioning so the authorized 0.1
118+
maintenance window cannot enter 0.2 implicitly and a root package does not fall
119+
into the single-package tag-discovery ambiguity encountered during 0.1.0.
120+
Because a
119121
GitHub Release created with the default `GITHUB_TOKEN` does not start a separate
120122
`release.published` workflow, publication is chained from the successful
121123
Release Please workflow. The handoff accepts only the canonical repository's
122124
successful first-attempt `push` run for `main` at the still-current exact `main`
123125
SHA. The release workflow records `release_created`, SHA, tag, version, URL,
124126
repository, workflow identity, run ID, and attempt in an exact-run artifact.
125127
Publication downloads and validates that artifact before checking the tag and
126-
immutable Release. Runs that fail while preparing a pull request are filtered
127-
out; any successful run without the exact Release Please-created result, tag,
128-
and immutable Release fails before live or registry access. The release outcome
129-
and package artifact are verified independently.
128+
immutable Release. A first-attempt manual run is explicitly release-inert and
129+
must succeed only after validating one action-created patch PR; its event cannot
130+
enter publication. Any successful `push` run without the exact Release
131+
Please-created result, tag, and immutable Release fails before live or registry
132+
access. The release outcome and package artifact are verified independently.
130133

131134
Release Please and publication remain separate trust domains. Release Please
132135
does not receive npm OIDC permission; `id-token: write` remains limited to the
133136
protected publish job. Repository variables gate both flows, and reruns remain
134137
fail-closed on exact tag, artifact, dist-tag, integrity, and provenance state.
135138
Release Please itself rejects attempt 2 or later before repository mutation;
136-
the explicitly enabled preparation path uses a new manual dispatch, while only
137-
a new `push` run can enter publication. A merged release PR is accepted for
139+
the explicitly enabled preparation path uses a new manual dispatch with Release
140+
creation disabled, while only a new `push` run can create the Release and enter
141+
publication. The authorized Actions setting lets the default token create the
142+
PR, but bot review cannot satisfy the gate. A merged release PR is accepted for
138143
tagging only after a distinct repository administrator approved its final head.
139144
The workflow checks the triggering SHA against the fetched `main` tip both at
140145
checkout and immediately before Release Please mutation, so an older queued run
141146
cannot release a newer default-branch commit.
142147
It also rejects any pending merged release PR whose merge commit is not the
143-
current push SHA. Manual dispatch is therefore release-inert: it may prepare a
144-
branch only when no merged release PR is awaiting a tag.
148+
current push SHA, and scans the complete pending merged set so a legacy, fork,
149+
alternate, older, or additional PR cannot be tagged. Manual dispatch is
150+
therefore release-inert: it may prepare one canonical action-created PR only
151+
when no merged release PR is awaiting a tag.
145152

146153
## Testing layers
147154

RELEASING.md

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Release status is evidence-based:
1212
| Registry Alpha candidate | The exact `0.1.0-alpha.3` artifact passes package and clean-install gates after preserving the unpublished immutable alpha.2 failure record. |
1313
| Registry Alpha released | The public npm artifact installs from the `next` channel, passes post-publication verification, and has verified provenance plus any documented one-time bootstrap evidence. |
1414
| Stable released | Every stable 0.1.0 local, remote, live, review, provenance, and registry gate has recorded evidence. |
15-
| Stable patch candidate | A maintainer-created Release Please PR has the exact version, changelog, manifest, temporary-anchor removal, complete CI matrix, and human-owner review. |
15+
| Stable patch candidate | An action-created Release Please PR has the exact version, changelog, manifest, temporary-anchor removal, complete CI matrix, and human-owner review. |
1616
| Stable patch released | The immutable Release Please tag and GitHub Release, bounded live smoke, npm OIDC publication, and independent public-registry verification all pass. |
1717

1818
A build, mock, valid workflow file, successful upload, or HTTP 200 proves only
@@ -232,19 +232,24 @@ The repository maintains four independently auditable workflows:
232232
first failure. Standalone and release smoke jobs share one repository-wide
233233
concurrency group. Scheduled and manual live execution requires
234234
`LIVE_SMOKE_ENABLED=true`.
235-
- `release-please.yml`: stable versioning, an explicit `cometapi` component,
235+
- `release-please.yml`: patch-only versioning, an explicit `cometapi` component,
236236
separate pull requests, and an explicit component/version title. It requires
237237
`RELEASE_PLEASE_ENABLED=true`, accepts only first-attempt runs, and uses the
238-
default `GITHUB_TOKEN`. A first-attempt manual dispatch prepares the patch
239-
branch after the variable is enabled; rerunning an older workflow is
240-
rejected before Release Please can mutate repository state. The
241-
repository deliberately leaves Actions pull-request authorization disabled,
242-
so a maintainer creates the standard PR from the generated branch, applies
243-
the `autorelease: pending` label, and obtains another administrator's approval
244-
on the final head. The post-merge workflow verifies that exact approval even
245-
when the PR used a squash or rebase merge. Release Please then creates the
246-
normal tag and GitHub Release, transitions the release label, and uploads its
247-
exact release outputs as a run-bound artifact.
238+
default `GITHUB_TOKEN`. The authorized repository baseline keeps default
239+
workflow permissions read-only and allows Actions to create pull requests; it
240+
does not make bot review valid release approval. A first-attempt manual
241+
dispatch runs with GitHub
242+
Release creation disabled and prepares exactly one action-authored patch PR
243+
after the variable is enabled; rerunning an older workflow is rejected before
244+
Release Please can mutate repository state. The preparation run succeeds only
245+
after verifying the canonical branch, title, machine-readable body, pending
246+
label, four expected release files, and 0.1.x patch versions. Before a
247+
post-merge `push` may create a Release, the workflow scans every merged `main`
248+
PR carrying `autorelease: pending`, rejects legacy, alternate, fork, older, or
249+
multiple candidates, and requires an administrator's human approval on the
250+
exact final head. Release Please then creates the normal tag and GitHub
251+
Release, transitions the release label, and uploads its exact release outputs
252+
as a run-bound artifact.
248253
The triggering SHA must still equal the fetched `main` tip at checkout and
249254
immediately before the Release Please action; an older queued run stops before
250255
mutation.
@@ -261,15 +266,17 @@ The repository maintains four independently auditable workflows:
261266
`main` SHA. It downloads the output artifact from that exact upstream run and
262267
requires `release_created`, SHA, tag, version, URL, repository, workflow path,
263268
run ID, and attempt to agree before accepting the matching Release
264-
Please-created version tag and immutable GitHub Release. Failed pull-request
265-
preparation runs are filtered out; a successful run without that exact result
266-
fails before live or publication work. The release path then packs and tests one exact artifact, runs the
267-
protected release live smoke, and publishes the same file through npm OIDC. Registry token
268-
credentials are rejected. The workflow verifies the dist-tag, integrity,
269-
provenance attestation, signatures, deduplication, and public installation. A
270-
publish rerun resumes after an already accepted version only when its registry
271-
integrity matches the downloaded artifact, then repeats every bounded
272-
registry-state and signature check.
269+
Please-created version tag and immutable GitHub Release. A successful manual
270+
preparation run is release-inert and cannot enter publication; a successful
271+
`push` run without the exact release result fails before live or publication
272+
work. The release path then packs and tests one exact, attempt-qualified
273+
artifact, runs the protected release live smoke, and publishes the same file
274+
through npm OIDC. Registry token credentials are rejected. Re-running all jobs
275+
creates a new attempt-qualified artifact, while re-running failed downstream
276+
jobs consumes the already verified producer attempt. Publication resumes after
277+
an already accepted version only when its registry integrity matches the
278+
downloaded artifact, then repeats every bounded registry-state and signature
279+
check.
273280

274281
Third-party actions are pinned to full commit SHAs. Workflow permissions remain
275282
read-only except where a documented job requires more; `id-token: write` belongs
@@ -424,8 +431,8 @@ layers:
424431
feature or fix pull request
425432
-> required offline CI
426433
-> merge to the protected default branch
427-
-> generated Release Please branch
428-
-> maintainer-created release PR
434+
-> first-attempt manual Release Please preparation
435+
-> action-created Release Please branch and release PR
429436
-> human review and merge
430437
-> immutable tag and GitHub release
431438
-> rebuild and verify exact artifact
@@ -449,20 +456,38 @@ Please-created `v0.1.1` boundary normally.
449456

450457
Before enabling the repaired workflow, create the standard
451458
`autorelease: pending` and `autorelease: tagged` labels if they are still
452-
absent. The configuration names both labels explicitly. Because Actions
453-
pull-request creation remains disabled, the maintainer-created release PR must
454-
receive `autorelease: pending` before merge so Release Please can discover it
455-
and perform the normal tagged transition with its scoped `issues: write`
456-
permission.
459+
absent. The configuration names both labels explicitly, and the action-created
460+
release PR must receive `autorelease: pending` automatically. Stop if the action
461+
cannot create or label that PR; do not replace the normal flow with a manually
462+
authored PR. Release Please performs the normal tagged transition with its
463+
scoped `issues: write` permission.
464+
465+
Confirm through the repository Actions API that
466+
`default_workflow_permissions=read` and
467+
`can_approve_pull_request_reviews=true`. The latter is the explicitly authorized
468+
0.1.1 baseline solely so the default token can create the Release Please PR.
469+
The release workflow must never change either setting, and any later drift is a
470+
stop condition.
457471

458472
After enabling `RELEASE_PLEASE_ENABLED`, start a new manual dispatch on `main`;
459473
do not rerun the skipped workflow from the repair merge. Only attempt 1 may call
460-
Release Please. The manually dispatched preparation run cannot trigger npm
461-
publication or create a Release: it is accepted only when no merged
474+
Release Please. The workflow rejects any dispatch whose triggering ref is not
475+
`refs/heads/main`, and all preparation and release runs share one main-scoped
476+
concurrency group. The manually dispatched preparation run cannot trigger npm
477+
publication or create a Release: the action receives explicit
478+
`skip-github-release=true`, it is accepted only when no merged
462479
`autorelease: pending` PR exists, and `publish.yml` accepts only an upstream
463-
`push` event. The release-PR merge creates the new first-attempt `push` run that
464-
may tag and publish. A later push cannot tag an older outstanding release PR;
465-
its merge SHA must equal the triggering SHA before Release Please runs.
480+
`push` event. It must succeed after validating the one action-created 0.1.1 PR.
481+
Before mutation, the workflow also rejects any open PR whose head name could be
482+
mistaken for the canonical release branch, including a same-named fork branch.
483+
Remove the one-cycle `last-release-sha` from that branch, complete the
484+
release-ready documentation, run the full matrix on its final head, and obtain
485+
approval from a different human repository administrator. The release-PR merge
486+
creates the new first-attempt `push` run that may tag and publish. A later push
487+
cannot tag an older outstanding release PR; its merge SHA must equal the
488+
triggering SHA before Release Please runs. Immediately before the irreversible
489+
Release Please call, the push run also requires final release metadata and
490+
public documentation, including removal of the one-cycle `last-release-sha`.
466491

467492
The stale branch
468493
`release-please--branches--main--components--cometapi` at
@@ -472,11 +497,14 @@ it still contains the documented generated 0.2.0 state, has no associated open
472497
PR, and contains no independent work. Do not delete or rewrite any other
473498
branch.
474499
475-
For 0.1.1, a normal `fix:` commit after 0.1.0 must produce exactly one patch PR.
476-
Stop if the branch contains 0.2.0, if any version/manifest/changelog value is not
477-
0.1.1, or if the generated PR is not attributable to the explicit `cometapi`
478-
component. Merge is forbidden until Node.js 22 and 24 blocking checks, the
479-
Node.js 26 advisory lane, minimum/locked/latest OpenAI 6.x compatibility,
500+
For 0.1.1, `always-bump-patch` keeps every releasable Conventional Commit on the
501+
0.1.x maintenance line; changing that strategy requires a separately authorized
502+
later milestone. A normal `fix:` commit after 0.1.0 must produce exactly one
503+
patch PR. Stop if the branch contains 0.2.0, if any
504+
version/manifest/changelog value is not 0.1.1, or if the generated PR is not
505+
attributable to the explicit `cometapi` component. Merge is forbidden until
506+
Node.js 22 and 24 blocking checks, the Node.js 26 advisory lane,
507+
minimum/locked/latest OpenAI 6.x compatibility,
480508
package and declaration checks, and human-owner review complete on the final
481509
head. After registry verification, restore `RELEASE_PLEASE_ENABLED=false` and
482510
keep the already enabled scheduled-smoke policy at `LIVE_SMOKE_ENABLED=true`.
@@ -508,12 +536,15 @@ the draft had no tag and was not published until more than eight minutes after
508536
the run failed. Release Please therefore found no discoverable published
509537
release/tag boundary, scanned the older initial feature commit, and prepared an
510538
unrequested 0.2.0 branch update. PR creation then failed for the separate reason
511-
that repository Actions are not authorized to create or approve pull requests.
539+
that repository Actions were not authorized to create or approve pull requests
540+
at that time.
512541
Publishing the manual Release later could not retroactively bound that run, and
513542
leaving `skip-github-release` enabled would continue the split discovery model.
514543
The 0.1.1 repair replaces that historical combination with explicit component
515-
identity, one-cycle history anchoring, maintainer-created/human-reviewed release
516-
PRs, and normal Release Please tag and GitHub Release creation.
544+
identity, one-cycle history anchoring, action-created/human-reviewed release
545+
PRs, and normal Release Please tag and GitHub Release creation. Repository
546+
Actions pull-request authorization is now enabled for that scoped job; the
547+
workflow still uses only its default token and job-local permissions.
517548
518549
## Stable 0.1.0 release evidence
519550
@@ -574,8 +605,9 @@ layers:
574605
publish a package. `RELEASE_PLEASE_ENABLED` was set to `false` before the
575606
closeout push; the branch is retained as failure evidence and must not be
576607
merged or treated as the start of 0.2. Release Please remains disabled until
577-
its post-manual-release discovery and pull-request authorization strategy are
578-
reviewed in a separately authorized maintenance task.
608+
the authorized 0.1.1 repair is merged, the stale branch is revalidated and
609+
removed, and the normal action-created PR path is ready for one first-attempt
610+
preparation run.
579611

580612
## Verification record
581613

0 commit comments

Comments
 (0)