ci(release): activate release pipeline on main - #368
Merged
Conversation
Documents the target release process for @deessejs/fp, including: - npm Trusted Publishing via OIDC (no long-lived NPM_TOKEN) - Provenance attestations on every publish - Branch strategy aligned with CLAUDE.md (PRs target staging, Version Packages PR promotes to main) - Changesets workflow with non-blocking enforcement - Canary snapshots, pre-release cycles, and hotfix procedures - Resolved decisions for stage publishing, changeset enforcement, linked groups, hotfix trigger, release engineer team, dev branch - Migration plan and file inventory No code changes. Plan only.
- Update repository.url from github.com/nesalia-inc/fp to github.com/deessejs/fp (matches the actual git remote; required for npm Trusted Publishing validation). - Add engines.node >=22.14.0 (required by npm CLI 11.5.1+ for Trusted Publishing and Sigstore provenance). - Add publishConfig.access=public and publishConfig.provenance=true so every publish from this package emits a provenance attestation. No runtime behavior change.
Step-by-step manual runbook for the GitHub UI and npmjs.com configuration required by the release pipeline plan: - Create deessejs/release-engineers team - Create release and hotfix environments with required reviewers - Configure branch protection on main and staging - Configure tag protection on v* - Register npm Trusted Publisher for @deessejs/fp - Verification checklist and rollback notes No code changes.
Per Q7 decision: no dedicated team, no bypass on main. The release engineer is a role (whoever opens the Version Packages PR), not a permission. Changes in release-pipeline.md: - §1 Goal 6: clarify that there is no bypass on main - §5 Branch Strategy: remove release engineer from rules; the role is whoever opens the Version Packages PR - §6.3: clarify the role is not a special permission - §10 Security Properties: replace "restrict to PR-merge path" with "no silent release path"; reword tag abuse mitigation - §13 Q5: updated resolved decision - Appendix A: removed the team row from the file inventory Changes in release-pipeline-github-ui-setup.md: - Removed §1 (Create the GitHub Team) entirely - Renumbered subsequent sections - §1.1 release env: reviewers are named individuals, not a team - §2.1 main branch protection: no bypass list - §2.2 staging branch protection: no bypass - §3 ruleset: no bypass list - §4 tag protection: no allow-list (workflow is the only creator) - §8 verification checklist: removed team items - Updated design rationale at the top of the doc No code changes.
CI: - Merge build.yml, lint.yml, tests.yml, types.yml into a single .github/workflows/ci.yml with four jobs: lint, typecheck, build, test. - Triggers: pull_request on main and staging, push on main, workflow_dispatch. Matches the branch strategy in docs/engineering/plans/release-pipeline.md (PRs default to staging). - concurrency group with cancel-in-progress for fast feedback on successive pushes. - workflow-level permissions: contents: read (least privilege). - All third-party actions pinned by commit SHA: - actions/checkout 3d3c42e5 (v7.0.1) - pnpm/action-setup 0ebf4713 (v6.0.9) - actions/setup-node 82076278 (v7.0.0) - Node version bumped from 20 to 24 (matches engines.node >= 22.14.0 added in 6acc6dd). - pnpm cache enabled (CI runs benefit from it; the release pipeline keeps package-manager-cache: false as a deliberate hardening). - --force flag on turbo commands to bypass stale cache when the cache key has not changed but the source has. - Build job sets NODE_ENV=production to mirror release-time invariants. PR template: - Add a header notice pointing to docs/engineering/plans/release-pipeline.md and stating that staging is the default PR target. - Add a dedicated Changeset section with two checkboxes (changeset added / does not affect published packages). No behavioral change for end users.
Replaces the legacy release workflow (label 'version bump' +
NPM_TOKEN long-lived secret) with the hardened pipeline from
docs/engineering/plans/release-pipeline.md §7.3.
Trigger changes:
- Drop pull_request.closed with label 'version bump'. The release
is now driven by a push to main, which happens naturally when
the auto-generated 'Version Packages' PR is merged.
- Keep workflow_dispatch but restrict it to inputs.reason
containing 'hotfix' (manual publish path for hotfix recovery).
All other reasons exit early.
Permissions:
- Workflow-level permissions: {} (default deny).
- Job-level: id-token: write (for OIDC), contents: read,
pull-requests: read.
- environment: release (reviewers configured on GitHub side per
docs/engineering/plans/release-pipeline-github-ui-setup.md).
Hardening:
- All third-party actions pinned by commit SHA:
- actions/checkout 3d3c42e5 (v7.0.1)
- pnpm/action-setup 0ebf4713 (v6.0.9)
- actions/setup-node 82076278 (v7.0.0)
- softprops/action-gh-release 3d0d9888 (v3.0.2)
- registry-url set on setup-node (npm uses the registry hint for
OIDC audience validation).
- package-manager-cache: false (release jobs must not consume a
possibly-poisoned cache; intentional divergence from ci.yml
which enables caching for fast feedback).
- npm CLI upgraded explicitly to latest (Trusted Publishing
requires >= 11.5.1).
- Anti-republish guard: queries npm for the current version
before publishing; fails if already published. Prevents
accidental re-publishes on retry.
- Smoke test after build: dynamic import of the published ESM
entry, verifies ok/err/some/none/maybe are functions. Fails
fast if the dist is broken.
- pnpm changeset publish --provenance: explicit --provenance
flag for belt-and-braces compatibility (Trusted Publishing
auto-generates provenance for public + public, but the flag
protects against older npm versions on consumer machines).
Tag handling:
- Tag is created as part of the workflow run (annotated, signed
with github-actions[bot] identity), pushed after publish.
- GitHub Release generated from the same tag with
generate_release_notes: true.
Audit:
- workflow_dispatch input 'reason' is logged in the run record
and gates the hotfix path. Every manual publish leaves a
written justification.
Adds .github/workflows/canary.yml implementing §8.1 of the release
pipeline plan.
Behavior:
- Triggered on pull_request (opened/synchronize/reopened) targeting
staging (the default developer PR target).
- Runs pnpm changeset version --snapshot canary to compute a
snapshot version (0.0.0-canary-<timestamp>) without committing.
- Runs pnpm changeset publish --tag canary --no-git-tag to publish
to the 'canary' npm dist-tag without creating a git tag.
- Comments on the PR with the install command
('pnpm add @deessejs/fp@canary') so reviewers can test the
snapshot locally.
Permissions:
- contents: read
- pull-requests: write (for the comment)
- id-token: write (Trusted Publishing)
Hardening:
- All third-party actions pinned by commit SHA:
- actions/checkout 3d3c42e5 (v7.0.1)
- pnpm/action-setup 0ebf4713 (v6.0.9)
- actions/setup-node 82076278 (v7.0.0)
- actions/github-script 3a2844b7 (v9.0.0)
- package-manager-cache: false (no cache poisoning on a publish path).
- registry-url on setup-node for OIDC audience.
- concurrency group keyed on PR number: a new push cancels the
previous snapshot for the same PR.
Operational note:
- This workflow requires npm Trusted Publishing to be registered for
@deessejs/fp with workflow filename 'canary.yml' and the same
environment, OR with 'npm publish' as an allowed action that also
accepts 'canary' publishes. Adjust the Trusted Publisher entry
on npmjs.com accordingly when this lands on main. Until then,
the workflow will fail at the publish step with an authentication
error — this is intentional (no silent fallback to NPM_TOKEN).
Adds .github/workflows/hotfix.yml implementing §8.3 of the release pipeline plan. Behavior: - Triggered on push of a vX.Y.Z tag to main. The hotfix branch has already been merged into main via a regular PR (the one exception to the 'PRs default to staging' rule, justified by urgency). - The hotfix branch is expected to have bumped the version in packages/fp/package.json before merging. This workflow verifies that the version matches the tag, failing fast otherwise. - Runs the same build + test + smoke-test sequence as release.yml. - Publishes via pnpm changeset publish --provenance --tag latest. - Creates a GitHub Release from the existing tag with auto-generated notes. Permissions: - contents: read - id-token: write (Trusted Publishing) - environment: hotfix (smaller reviewer pool than 'release') Hardening: - All third-party actions pinned by commit SHA: - actions/checkout 3d3c42e5 (v7.0.1) - pnpm/action-setup 0ebf4713 (v6.0.9) - actions/setup-node 82076278 (v7.0.0) - softprops/action-gh-release 3d0d9888 (v3.0.2) - package-manager-cache: false. - Anti-republish guard identical to release.yml. - Tag pattern 'v[0-9]+.[0-9]+.[0-9]+' excludes pre-releases and major-version-only tags by design. - branch filter 'branches: [main]' ensures tags on other branches are ignored. Operational note: - This workflow requires a second Trusted Publisher entry on npmjs.com for @deessejs/fp with workflow filename 'hotfix.yml' and the 'hotfix' environment. Update docs/engineering/plans/release-pipeline-github-ui-setup.md §5 to mention both filenames (release.yml AND hotfix.yml). - After a hotfix lands, a back-merge from main to staging (and dev) is mandatory, plus a regular changeset PR documenting the fix on staging.
The release pipeline now ships three publish-capable workflows (release.yml, canary.yml, hotfix.yml). npm Trusted Publishing matches on the (repository, workflow filename, environment) triple, so we register one entry per workflow. Updates release-pipeline-github-ui-setup.md §5: - §5.1 Trusted Publisher for stable releases (release.yml) - §5.2 Trusted Publisher for hotfixes (hotfix.yml) - §5.3 Trusted Publisher for canary snapshots (canary.yml, optional) - §5.4 Update package publishing access (unchanged, renumbered) - §5.5 Verify (renumbered)
…ternative Documents the conscious trade-off of keeping canary.yml in this iteration rather than adopting pkg.pr.new (the 2026 senior standard). §8.1.1 Known limitations: - Pollutes the public npm registry - Concurrent PRs overwrite each other on the 'canary' dist-tag - Requires a dedicated Trusted Publisher slot - No retention control (npm unpublish constraints) §8.1.2 The senior alternative not adopted yet: - pkg.pr.new (StackBlitz, Cloudflare-backed) - Zero npm pollution, per-SHA isolation, monorepo-aware - Adopted by Vite, Vue, Nuxt, Svelte, Rolldown, Cloudflare - Migration deferred to a follow-up PR §8.1.3 Reminds readers that the release.yml smoke test is the actual safety net; canary is a reviewer courtesy, not a guard. No code changes.
Observed on PR #365: canary.yml failed at publish step because `changeset version --snapshot canary` is a no-op when there is no changeset to snapshot, leaving package.json at its current version. The subsequent publish then tried to push a version that already exists on npm. The canary workflow is meaningful only for PRs that change publishable behavior (which by design have a changeset). PRs without changesets (infra, docs, refactors) should not produce a canary snapshot. Changes: - Add a 'Detect pending changesets' step that runs `pnpm changeset status --since=origin/staging` and sets the 'changesets' output to 'found' or 'not found'. - Guard 'Snapshot version', 'Publish snapshot to canary tag', and 'Comment on PR with install instructions' with `if: steps.changesets.outputs.found == 'true'`. - The 'pnpm build' step still runs unconditionally so the CI signal remains useful for PRs without changesets. Note: 'Detect pending changesets' intentionally runs before 'pnpm build' so we don't pay the build cost on PRs that won't publish.
The previous detection used 'pnpm changeset status --since=origin/staging' and checked for empty output, but that command does not produce empty output when no changesets are pending — it produces a multi-line 'report' style output regardless. Switch to counting changeset files directly under .changeset/, excluding README.md and config.json (the two bookkeeping files that always exist in the directory).
docs+chore(fp): release pipeline plan and package.json alignment for Trusted Publishing
Adds .github/workflows/changeset-version.yml implementing the 'Version Packages PR' step from docs/engineering/plans/release-pipeline.md §6.3. Behavior: - Triggered on push to staging. - Runs changesets/action@v1.9.0 (SHA pinned) to open or update a PR titled 'chore: version packages' from branch 'changesets/version' to main. - The PR contains the result of 'pnpm changeset version': bumped package versions, generated CHANGELOG entries, consumed changeset files. - Merging the PR into main triggers release.yml, which publishes to npm via Trusted Publishing. Important: the 'publish' input is intentionally NOT set on the changesets/action. Publishing is the job of release.yml (with its anti-republish guard, smoke test, and SHA-pinned actions). Setting 'publish' here would cause a double-publish. Permissions: - contents: write (to push the version branch) - pull-requests: write (to open/update the Version Packages PR) Hardening: - All third-party actions pinned by commit SHA: - actions/checkout 3d3c42e5 (v7.0.1) - pnpm/action-setup 0ebf4713 (v6.0.9) - actions/setup-node 82076278 (v7.0.0) - changesets/action a45c4d59 (v1.9.0) - No environment required (this workflow does not publish or touch the npm registry). - concurrency group with cancel-in-progress on staging push (a new push cancels the previous attempt).
ci(changesets): auto-open Version Packages PR on staging push
npm Trusted Publishing allows exactly ONE configuration per
package (verified against npm docs 2026-08-03). The previous
design assumed three entries could be registered (release.yml /
hotfix.yml / canary.yml) — this is not possible.
Refactor to a single entrypoint pattern:
publish.yml
The Trusted Publisher. Registered on npmjs.com with
workflow filename = 'publish.yml', environment = 'release'.
Dispatches to reusable workflows based on event type.
Triggers:
- push to main -> release path
- push tag vX.Y.Z -> hotfix path
- pull_request to staging -> canary path
Declares id-token: write (the only file that does).
_publish-release.yml
Reusable workflow (workflow_call trigger). Performs the stable
release: anti-republish guard, build, test, smoke test,
changeset publish --provenance --tag latest, tag creation,
GitHub Release.
_publish-hotfix.yml
Reusable workflow for tag-driven hotfix path. Verifies that
package.json version matches the tag, anti-republish guard,
build, test, smoke test, publish --tag latest, GitHub Release.
_publish-canary.yml
Reusable workflow for per-PR snapshot publishes. Detects
pending changesets (skip cleanly when none), snapshots, publishes
to dist-tag 'canary', comments on PR.
Design notes:
- Single environment ('release') covers all three flows.
npm validates the entrypoint (publish.yml), not the
reusable workflows.
- Reusable workflows declare permissions but do NOT request
id-token themselves — that is inherited from the entrypoint.
- File naming: leading underscore ('_publish-*') signals that
the file is not standalone; only publish.yml can be triggered
by an event.
- workflow_dispatch inputs (the old release.yml 'reason' field)
are removed; hotfix is now exclusively tag-driven per the
pipeline design.
Removes:
- .github/workflows/release.yml (replaced by _publish-release.yml)
- .github/workflows/hotfix.yml (replaced by _publish-hotfix.yml)
- .github/workflows/canary.yml (replaced by _publish-canary.yml)
No behavioral change for end users; the publish path is
unchanged in shape, only the underlying files are reorganized.
Aligns the runbook with the publish.yml entrypoint refactor implemented in 9beadbb / b0f3c0b. §1 Environments: - Drop the 'hotfix' and 'canary' environment sections. The single 'release' environment now covers all three publish paths (release / hotfix / canary), dispatched via reusable workflows. - Optionally allow v* tags to be deployable to 'release'. §5 Trusted Publisher: - Replace the multi-entry (release.yml + hotfix.yml + canary.yml) guidance with a single entry pointing to publish.yml. - Cite npm docs 2026-08-03 ('one trusted publisher per package') and the Paige Niedringhaus reusable-workflow article. - Revocation note: if a previous entry exists, revoke it first via npm trust CLI or the npmjs.com UI. - Verification: confirm exactly one entry on npmjs.com. §6 Workflow permissions: - Update reference from 'release.yml' to 'publish.yml' for the id-token: write override. §7.2 Code Owners: - Update file references from release.yml to publish.yml and the reusable workflows. §8 Verification Checklist: - Replace 'feat/release-pipeline' with 'feat/publish-entrypoint'. - Replace 'release.yml' with 'publish.yml' as the file that must exist in main. §9 Rollback: - Add npm trust CLI command for revocation. No code changes.
ci(publish): single Trusted Publisher entrypoint + reusable workflows
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the release pipeline implementation from
stagingtomain. This is a one-shot activation PR — no published version change, no changelog entry needed, just moving the pipeline code onto the branch that npm Trusted Publishing validates against.stagingis currently 17 commits ahead ofmain. All those commits implement the new release pipeline: docs, workflows (Trusted Publishing, canary, hotfix, changeset-version, consolidated CI), and thepackage.jsonalignment required for OIDC.Without this PR, npmjs.com rejects the Trusted Publisher configuration because the registered workflow filename
publish.ymldoes not exist onmain. With this PR, the pipeline becomes operational end-to-end.What's in this PR
The 17 commits aggregate into five logical groups:
1. Plan and runbook (5 commits)
docs(engineering): add release pipeline plan— design doc covering goals, architecture, branch strategy, Changesets workflow, Trusted Publishing configuration, side channels (canary / pre-release / hotfix), security properties, migration plan.docs(engineering): add GitHub UI setup guide for release pipeline— manual runbook for environments, branch protection, tag protection, npm Trusted Publisher registration.docs(engineering): drop release-engineers team requirement— refines Q7 decision: no team, no bypass onmain.docs(engineering): document multiple Trusted Publisher entries— runbook update for multi-entry approach (later superseded, see ci(publish): single Trusted Publisher entrypoint + reusable workflows #367).docs(release-pipeline): document canary.yml limitations and senior alternative—pkg.pr.newdocumented as future migration target.docs(github-ui-setup): single Trusted Publisher + single environment— runbook update reflecting the ci(publish): single Trusted Publisher entrypoint + reusable workflows #367 refactor.2. CI consolidation (1 commit)
ci+chore: consolidate CI workflows and update PR template— replacesbuild.yml,lint.yml,tests.yml,types.ymlwith a singleci.yml(4 jobs). Adds the PR template header pointing at the plan and a Changeset section.3. Publish pipeline (5 commits)
ci(release): harden release pipeline for Trusted Publishing— initial hardenedrelease.yml(later refactored).ci(canary): snapshot publish per PR to staging— initialcanary.yml.ci(hotfix): tag-driven publish path for urgent fixes— initialhotfix.yml.ci(canary): skip cleanly when no changeset is present— canary guard against infra-only PRs.ci(canary): fix changeset detection— switch fromchangeset status(ambiguous output) to file counting.4. Trusted Publisher entrypoint refactor (1 commit)
ci(publish): single Trusted Publisher entrypoint + reusable workflows— refactor to the canonical 2026 pattern (npm allows one Trusted Publisher per package). Singlepublish.ymldispatches to_publish-release.yml,_publish-hotfix.yml,_publish-canary.yml.5. Version Packages PR automation (1 commit)
ci(changesets): auto-open Version Packages PR on staging push—changeset-version.ymlopens the reviewable "Version Packages" PR on every staging push.Plus 4 merge commits (one per source PR).
Files changed (15)
Why this PR is necessary
workflow filename = publish.yml,environment = release.publish.ymlonmain, every OIDC publish attempt fails with a workflow-not-found error.publish.yml— they either configure the entrypoint or describe the system.Why this is not a Changesets "Version Packages" PR
The Changesets-driven flow opens a "Version Packages" PR automatically on push to
stagingwhen changesets exist. In this case:stagingis 17 commits ahead ofmain..changeset/*.mdfile (they are all infra: docs, workflows,package.jsonalignment).package.json#versionstays at1.0.1).Opening this manually is appropriate for the pipeline activation, with the normal Changesets-driven flow taking over for subsequent releases.
Test plan
pnpm turbo type-checkpasses.pnpm turbo lintpasses.publish.ymlexists onmain(validates against the npmjs.com Trusted Publisher).stagingopens a "Version Packages" PR viachangeset-version.yml.stagingwith a changeset triggers_publish-canary.ymland publishes a snapshot.vX.Y.Ztag push onmaintriggers_publish-hotfix.yml.maintriggers_publish-release.ymland publishes via Trusted Publishing.Risk
Low. No published version changes. No npm publish triggered by this PR (no changeset, no version bump). The
publish.ymlworkflow runs on the next push tomain, finds no pending changesets, and exits without publishing.The
package.jsonchange (repository.url,engines.node,publishConfig) only affects future publishes — it does not retroactively modify anything.Rollback
Revert this merge commit. All 17 commits revert cleanly since they touch independent files. The Trusted Publisher on npmjs.com stays valid (it points to
publish.ymlwhich still exists post-revert on the now-rolled-back branch). Disabling the Trusted Publisher on npmjs.com is also a one-click rollback path if needed.🤖 Generated with Claude Code