Skip to content

ci(release): activate release pipeline on main - #368

Merged
codewizdave merged 18 commits into
mainfrom
staging
Aug 3, 2026
Merged

ci(release): activate release pipeline on main#368
codewizdave merged 18 commits into
mainfrom
staging

Conversation

@martyy-code

Copy link
Copy Markdown
Contributor

Summary

Brings the release pipeline implementation from staging to main. 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.

staging is currently 17 commits ahead of main. All those commits implement the new release pipeline: docs, workflows (Trusted Publishing, canary, hotfix, changeset-version, consolidated CI), and the package.json alignment required for OIDC.

Without this PR, npmjs.com rejects the Trusted Publisher configuration because the registered workflow filename publish.yml does not exist on main. 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 on main.
  • 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 alternativepkg.pr.new documented 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 — replaces build.yml, lint.yml, tests.yml, types.yml with a single ci.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 hardened release.yml (later refactored).
  • ci(canary): snapshot publish per PR to staging — initial canary.yml.
  • ci(hotfix): tag-driven publish path for urgent fixes — initial hotfix.yml.
  • ci(canary): skip cleanly when no changeset is present — canary guard against infra-only PRs.
  • ci(canary): fix changeset detection — switch from changeset 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). Single publish.yml dispatches 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 pushchangeset-version.yml opens the reviewable "Version Packages" PR on every staging push.

Plus 4 merge commits (one per source PR).

Files changed (15)

.github/PULL_REQUEST_TEMPLATE.md                   |   7 +
.github/workflows/_publish-canary.yml              | 101 +++++
.github/workflows/_publish-hotfix.yml              |  89 ++++
.github/workflows/_publish-release.yml             |  88 ++++
.github/workflows/build.yml                        |  41 --
.github/workflows/changeset-version.yml            |  55 +++
.github/workflows/ci.yml                           |  87 ++++
.github/workflows/lint.yml                         |  41 --
.github/workflows/publish.yml                      |  65 +++
.github/workflows/release.yml                      | 111 -----
.github/workflows/tests.yml                        |  41 --
.github/workflows/types.yml                        |  41 --
docs/engineering/plans/release-pipeline-github-ui-setup.md | 234 +++++++++++
docs/engineering/plans/release-pipeline.md         | 464 +++++++++++++++++++++
packages/fp/package.json                           |   9 +-
15 files changed, 1198 insertions(+), 276 deletions(-)

Why this PR is necessary

  • npm Trusted Publishing validates that the registered workflow filename exists at the registered path on the default branch.
  • The Trusted Publisher is registered as workflow filename = publish.yml, environment = release.
  • Without publish.yml on main, every OIDC publish attempt fails with a workflow-not-found error.
  • The other 16 commits are inert without 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 staging when changesets exist. In this case:

  • staging is 17 commits ahead of main.
  • None of those 17 commits added a .changeset/*.md file (they are all infra: docs, workflows, package.json alignment).
  • Changesets has no bump to propose. The PR would be empty or absent.
  • The activation is one-shot and has no user-visible semver impact (package.json#version stays at 1.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-check passes.
  • pnpm turbo lint passes.
  • Post-merge: publish.yml exists on main (validates against the npmjs.com Trusted Publisher).
  • Post-merge: a push to staging opens a "Version Packages" PR via changeset-version.yml.
  • Post-merge: a PR to staging with a changeset triggers _publish-canary.yml and publishes a snapshot.
  • Post-merge: a vX.Y.Z tag push on main triggers _publish-hotfix.yml.
  • Post-merge: merging a "Version Packages" PR into main triggers _publish-release.yml and publishes via Trusted Publishing.

Risk

Low. No published version changes. No npm publish triggered by this PR (no changeset, no version bump). The publish.yml workflow runs on the next push to main, finds no pending changesets, and exits without publishing.

The package.json change (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.yml which 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

martyy-code and others added 18 commits August 3, 2026 11:00
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
@codewizdave
codewizdave merged commit 11bdaf7 into main 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