Skip to content

docs+chore(fp): release pipeline plan and package.json alignment for Trusted Publishing - #365

Merged
codewizdave merged 12 commits into
stagingfrom
feat/release-pipeline
Aug 3, 2026
Merged

docs+chore(fp): release pipeline plan and package.json alignment for Trusted Publishing#365
codewizdave merged 12 commits into
stagingfrom
feat/release-pipeline

Conversation

@martyy-code

Copy link
Copy Markdown
Contributor

Summary

Introduces the design and first implementation steps for a senior-grade release pipeline on @deessejs/fp:

  • Authenticate publishes with OIDC (npm Trusted Publishing). Removes the need for a long-lived NPM_TOKEN in repository secrets.
  • Emit provenance attestations on every published package, signed by Sigstore and linked to the build workflow.
  • Make main <- staging <- dev executable, not only documented. All developer PRs now target staging. main is updated only by the release engineer via the auto-generated "Version Packages" PR.
  • Human-gated releases with reviewable version diffs, environment protection rules, and a hardened publish pipeline.

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:

  • Goals, non-goals, current-state baseline.
  • Target architecture diagram (PR → staging → Version Packages PR → main → publish).
  • Branch strategy: main, staging, dev with explicit PR direction rules.
  • Changesets workflow (daily practice, enforcement, Version Packages PR, publish trigger).
  • Publish pipeline: npm Trusted Publishing + provenance + hardening (anti-republish guard, smoke test, SHA-pinned actions).
  • Side channels: canary snapshots per PR, pre-release cycles (next/beta), hotfixes.
  • Resolved decisions (stage publishing, changeset enforcement, linked groups, hotfix trigger, release engineer identity, dev branch status).
  • Migration plan, observability checklist, file inventory, references.

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:

  • Create the deessejs/release-engineers GitHub team.
  • Create release and hotfix environments with required reviewers.
  • Configure branch protection on main and staging.
  • Configure tag protection on v*.
  • Register the npm Trusted Publisher for @deessejs/fp.
  • Verification checklist and rollback notes.

3. packages/fp/package.json (modified)

  • repository.url: https://github.com/nesalia-inc/fp.githttps://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:

  • Rewriting .github/workflows/release.yml to the hardened publish pipeline.
  • Adding .github/workflows/canary.yml (per-PR snapshot publishes).
  • Adding .github/workflows/hotfix.yml (tag-based hotfix pipeline).
  • Adjusting build.yml, lint.yml, tests.yml, types.yml to run on staging (PR + push) and on main (push only).
  • Updating .github/PULL_REQUEST_TEMPLATE.md to mention the default PR target.
  • The manual GitHub UI configuration described in release-pipeline-github-ui-setup.md.

Each follow-up is non-destructive and reversible individually.

Test plan

  • git diff reviewed locally — only the 3 files above changed.
  • pnpm turbo type-check passes (no type changes).
  • pnpm turbo lint passes.
  • Manual review of both new docs for technical accuracy.
  • Manual review of the package.json changes against the resolved decisions in release-pipeline.md §13.

Risk

Low. No runtime code change, no CI workflow change, no npm publish performed by this PR. The package.json changes 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

  • Plan: docs/engineering/plans/release-pipeline.md
  • UI setup guide: docs/engineering/plans/release-pipeline-github-ui-setup.md
  • Branching model in CLAUDE.md: main <- staging <- dev

🤖 Generated with Claude Code

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).
@codewizdave
codewizdave merged commit a153e71 into staging Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants