docs+chore(fp): release pipeline plan and package.json alignment for Trusted Publishing - #365
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).
5 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
Introduces the design and first implementation steps for a senior-grade release pipeline on
@deessejs/fp:NPM_TOKENin repository secrets.main <- staging <- devexecutable, not only documented. All developer PRs now targetstaging.mainis updated only by the release engineer via the auto-generated "Version Packages" PR.This PR contains the plan and the first two non-destructive implementation steps. The GitHub UI configuration and the workflow rewrites are tracked as follow-up work.
What's in this PR
1.
docs/engineering/plans/release-pipeline.md(new, 438 lines)The full release pipeline design:
main,staging,devwith explicit PR direction rules.next/beta), hotfixes.2.
docs/engineering/plans/release-pipeline-github-ui-setup.md(new, 266 lines)Step-by-step runbook for the manual GitHub UI and npmjs.com configuration required to support the pipeline:
deessejs/release-engineersGitHub team.releaseandhotfixenvironments with required reviewers.mainandstaging.v*.@deessejs/fp.3.
packages/fp/package.json(modified)repository.url:https://github.com/nesalia-inc/fp.git→https://github.com/deessejs/fp.git. Required by npm Trusted Publishing; matches the actual git remote.engines.node:">=22.14.0"(pre-requisite for npm CLI 11.5.1+ with OIDC support).publishConfig:access: "public",provenance: true. Ensures every publish carries a Sigstore-signed provenance attestation.What's NOT in this PR
Deliberately deferred to follow-up PRs to keep this one reviewable:
.github/workflows/release.ymlto the hardened publish pipeline..github/workflows/canary.yml(per-PR snapshot publishes)..github/workflows/hotfix.yml(tag-based hotfix pipeline).build.yml,lint.yml,tests.yml,types.ymlto run onstaging(PR + push) and onmain(push only)..github/PULL_REQUEST_TEMPLATE.mdto mention the default PR target.release-pipeline-github-ui-setup.md.Each follow-up is non-destructive and reversible individually.
Test plan
git diffreviewed locally — only the 3 files above changed.pnpm turbo type-checkpasses (no type changes).pnpm turbo lintpasses.package.jsonchanges against the resolved decisions inrelease-pipeline.md§13.Risk
Low. No runtime code change, no CI workflow change, no npm publish performed by this PR. The
package.jsonchanges only affect future publishes (URL validation, engines warning, provenance flag).Rollback
Revert this single merge commit. The branch protection rules (when set up in the follow-up UI work) are independently reversible.
Related
docs/engineering/plans/release-pipeline.mddocs/engineering/plans/release-pipeline-github-ui-setup.mdCLAUDE.md:main <- staging <- dev🤖 Generated with Claude Code