Skip to content

ci: npm trusted publishing and GitHub releases - #473

Merged
uniqueg merged 18 commits into
devfrom
ci/trusted-publishing
Oct 27, 2026
Merged

uniqueg merged 18 commits into
devfrom
ci/trusted-publishing

Conversation

@uniqueg

@uniqueg uniqueg commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Third of the stacked pull requests for #464. Stacked on #470: merge it first; until then this diff also shows its commits.

Reworks the npm release for the branch model of the organization's other repositories (dev is the default branch, main points at the latest release) and for npm trusted publishing with staged publishing only (the trusted publishers allow npm stage publish, not npm publish or dist-tag changes; package publishing access is "require 2FA, disallow tokens").

  • No pushes, no tokens. The workflow no longer bumps versions or pushes to main: the bump job, the deploy key (DEPLOY_KEY) and the NPM_TOKEN fallback are gone. The version bump happens in a normal pull request to dev (node scripts/release.js set <version>, which also updates package-lock.json).
  • release.yml / Release on tags v*, all jobs on the tagged commit (github.sha):
    • readiness / Release readiness: fails early unless the tag is a version, the root and all public packages have exactly that version, and the commit is on dev. Computes the dist-tag from the git tags.
    • lint / Lint release: npm ci, npm run lint.
    • publish / Release packages: environment npm (required reviewer), id-token: write only here. Node.js 24 with check-latest and a check for npm 11.15.0 or later (the first with npm stage; Node.js 24.22 bundles 11.19.1). Pins the internal @elixir-cloud/* dependencies to the version (as before, not committed), builds, then runs npm stage publish --provenance --tag <dist-tag> in each public package. Versions already on npm are skipped. A version staged by an earlier attempt fails with a conflict (E409) and counts as staged, so the job can be re-run. The run summary lists the staged versions to approve on npmjs.com; if any failed, it warns not to approve yet.
    • github / Release on GitHub: unchanged (generated notes from the previous version tag, prerelease for - versions).
    • Dry run outside elixir-cloud-aai/cloud-components: staging gets --dry-run, for a test in a fork.
  • Dist-tags: latest for releases and for prereleases of a major version without a release tag yet, so the 2.0.0-alpha.N series keeps going to latest as before (there is no v2.x.y tag). Once 2.0.0 is out, prereleases get their identifier (2.1.0-rc.1 → rc). Without any release tag (e.g. a clone without tags) the step fails instead of defaulting to latest. npm requires --tag for prereleases, and the tag of a staged version can't be changed later.
  • Released packages: every package in packages/ without "private": true. @elixir-cloud/tes has no build (all its scripts are depricated-*), so a release would contain no code: it is now private and no longer released. Its trusted publisher on npm will expire (it can't be validated by a publish before 2026-10-29); recreate it if tes is ever released again. @elixir-cloud/eslint-config is released with the shared version (0.0.1 → 2.0.0-alpha.N) and gets a repository URL for provenance.
  • scripts/release.js (set, check, packages, dist-tag, pin), with tests in scripts/release.test.js (node --test; ci: single CI gate and contribution docs #474 runs them in CI).
  • PR Evaluation runs on pull requests to dev; the unused release script (changeset publish) is removed (ci: single CI gate and contribution docs #474 removes changesets).

The release docs (version bump PR → tag on dev → approve the npm environment → approve the staged versions → fast-forward main) are in CONTRIBUTING.md in #474.

Closes #467
Closes #471

Verification

  • node --test scripts/release.test.js: 9 tests pass. node scripts/release.js dist-tag 2.0.0-alpha.49 → latest, dist-tag 1.1.0-rc.1 → rc; in a repository without tags it fails ("No release tags"). node scripts/release.js check 2.0.0-alpha.49 lists all mismatches and exits 1; check v1 exits 1 ("Not a version").
  • npm version 2.0.0-alpha.49 --no-git-tag-version --include-workspace-root -w … (what set runs) changes only the versions in the root, the given packages and package-lock.json (tried, then reverted).
  • npm stage publish --provenance --tag alpha --dry-run (npm 11.19.1) in eslint-config at 2.0.0-alpha.49: "Staging … with tag alpha … (dry-run)". Without --tag: "You must specify a tag using --tag when publishing a prerelease version". At an existing version: "You cannot publish over the previously published versions".
  • npm source (11.19.1 and 11.21.1): npm stage publish subclasses npm publish (same flags) and enables provenance automatically with OIDC; --provenance makes it explicit.
  • The staging step was run locally with a stub npm (a lookup timing out, a staging failure, an E409, a version already on npm; dry run and real): it continues, lists all results in the summary and exits 1 on failures.
  • actionlint 1.7.12: no findings. zizmor 1.31.1 (offline): no findings for release.yml (findings only in the old pr_evaluation.yml, which ci: single CI gate and contribution docs #474 replaces).
  • npm run lint: passes locally.
  • CI on this PR (after merging dev with build: move CI and Docker to Node.js 22 and 24 #470 and test: drop test scripts of packages without tests #478, head 2eda27f): PR Evaluation passes (conflicts, linting, title, tests on Node.js 22 and 24); the second, skipped PR Evaluation run is the edited event. The RTD check fails here because .readthedocs.yaml comes with docs: host the docs on Read the Docs #476.
  • The workflow itself runs only on a tag. A fork dry run (test tag in uniqueg/cloud-components) is prepared for the maintainer.
  • Reviewed by the adversarial and quality reviewer subagents (2 rounds), plus a quality review of the dist-tag change (fixed: fail without release tags, also in the workflow; git errors; tests and comments). Fixed: checkouts on github.sha, a failing package list no longer hidden, npm lookups failing for other reasons than E404 count as failures, re-runs after partial staging (E409), summary warning, stdin of npm in the loop, CLI tests, fixture cleanup, dead filter, comments.

Declined review findings

  • Move the previous-tag pipeline and the npm version check into scripts/release.js: both were checked by hand (previous tags for alpha/rc/release sequences; npm versions); kept inline to keep this change small before the release.
  • Run the CLI tests on a fixture instead of the repository: they are a deliberate smoke test of the real package list (tes excluded, design included).
  • E409 for a version that is already staged is not verified against the registry: the npm docs only say a staged version "shares the same semver version unique index"; if the registry answers differently, a re-run reports those packages as failed (safe, just red).
  • Squash the intermediate commits: the repository squash-merges.

Decided

  • Dist-tag for the 2.0.0 alphas: latest, as before (maintainer, 2026-10-27); prerelease dist-tags start once 2.0.0 is out.
  • tes: no longer released (maintainer, 2026-10-27); its trusted publisher will expire.

Open questions

None.

🤖 Generated with Claude Code

uniqueg and others added 8 commits October 26, 2026 15:20
Remove the changesets versioning workflow (changesets/action without a
publish input, which only added a Release check to every commit on
main), the generated .changeset/README.md and the root CHANGELOG.md
stub. changeset publish stays the publish command.

Refs #465

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The builder stage of the docs image copies the whole build context.
Send only what the build needs (allow list: workspaces, build scripts,
root manifests), so that local .env* files and other local files never
reach the image, and drop dependencies and build output within.

Closes #463

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin every action to a full commit SHA on its current major version
(checkout v7, setup-node v7, cache v6, semantic PR title v6, ssh-agent
v0.10, push action v1.3.0 instead of master), with Dependabot keeping
the pins up to date. Replace the node_modules caches, whose key hashed a
file that doesn't exist and which used ::set-output, by the npm cache
of setup-node in CI; releases use no dependency cache. Drop the
force-with-lease input of the push action, which doesn't exist (the
input is force_with_lease) and so never had an effect.

Refs #465

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Give Dependabot pull requests a ci: prefix, so that their titles pass
the PR title check. Keep the commented-out release tests job as it was
and state the release cache comment once.

Refs #465

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ubuntu-latest now is Ubuntu 26.04, on which Playwright 1.56 can't
install its browsers, so every test job fails (unrelated to the other
changes). Pin the test job to ubuntu-24.04 until Playwright is
upgraded.

Refs #465

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node.js 18 and 20 are end of life. Test on 22 and 24, lint and release
on 24 (npm 11, needed for trusted publishing), build the docs image on
node:24-alpine and update the development prerequisites in the README.
engines and packageManager stay as they are.

Closes #466

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename publish.yml to release.yml (Release), as in the organization's
other repositories, with job names Lint release, Bump version, Publish
packages. The publish job gets id-token: write (and only it), checks
for npm 11.5.1 or later and publishes through OIDC with provenance.
NPM_TOKEN stays as a fallback: npm tries OIDC first and uses the token
only if the exchange fails; remove it after the first OIDC release.
A new job creates a GitHub release with generated notes from the
previous version tag, as a prerelease for versions with a suffix.

Closes #467

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Order version tags by semver when looking up the previous tag for the
release notes (sort -V puts v2.0.0 before v2.0.0-alpha.1). Configure
the NPM_TOKEN fallback only right before publishing, say in the log
when it is configured, and warn about released versions without
provenance (published with the token). Name the jobs Release packages
and Release on GitHub.

Refs #467

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
elixir-cloud-components Error Error Oct 26, 2026 5:49pm UTC

@changeset-bot

changeset-bot Bot commented Oct 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 80f0e31

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai

sourcery-ai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Modernizes tag-based releases around npm trusted publishing with narrowly scoped OIDC permissions, provenance validation, and automated GitHub releases, while updating the project and CI to Node.js 24 and removing obsolete release artifacts.

Sequence diagram for the trusted npm and GitHub release flow

sequenceDiagram
    participant Maintainer
    participant Actions as GitHub Actions
    participant Bump as Bump version job
    participant Publish as Release packages job
    participant NPM as npm registry
    participant GitHub as GitHub Releases

    Maintainer->>Actions: Push v* tag on main
    Actions->>Bump: Run after lint succeeds
    Bump->>Bump: Push bump: v<version> to main via SSH
    Bump->>Publish: Start publish job with id-token: write
    Publish->>NPM: Exchange OIDC identity for trusted publishing
    NPM-->>Publish: Publish packages with provenance
    alt OIDC exchange fails
        Publish->>NPM: Publish using NPM_TOKEN fallback
    end
    Publish->>NPM: Check dist.attestations.provenance
    Publish->>GitHub: Create release with generated notes
    GitHub-->>Publish: Mark prerelease when tag contains -
Loading

Flow diagram for the tag-based release pipeline

flowchart LR
    Tag[Push v* tag on main] --> Lint[Lint release]
    Lint --> Bump[Bump version]
    Bump --> Publish[Release packages]
    Publish --> Provenance[Check npm provenance]
    Provenance --> GitHub[Release on GitHub]
    Publish -. OIDC with id-token: write .-> NPM[npm trusted publishing]
    Publish -. fallback .-> Token[NPM_TOKEN]
Loading

File-Level Changes

Change Details Files
Replace the npm publishing workflow with an OIDC-based trusted publishing release pipeline while retaining a temporary token fallback.
  • Rename and restructure the tag-triggered workflow into lint, version bump, package release, and GitHub release jobs.
  • Upgrade release tooling to Node.js 24 and require npm 11.5.1 or newer for trusted publishing.
  • Grant id-token: write only to the package publishing job and keep release dependency caching disabled.
  • Configure NPM_TOKEN only immediately before publishing, with a notice when used as fallback.
  • Check published packages for npm provenance and warn when attestations are missing.
  • Update pinned GitHub Actions versions and use safer workflow-level and job-level permissions.
.github/workflows/release.yml
.github/workflows/pr_evaluation.yml
Create GitHub releases automatically for version tags with semver-aware generated notes.
  • Skip creation when the tag already has a GitHub release to support reruns.
  • Determine the previous release tag in semantic-version order, including prerelease handling.
  • Mark hyphenated versions as GitHub prereleases and generate release notes from the previous tag.
.github/workflows/release.yml
Align project runtime requirements and CI maintenance with Node.js 24.
  • Run CI checks on Node.js 22 and 24 using npm caching and Ubuntu 24.04 for Playwright compatibility.
  • Use Node.js 24 as the Docker base image and document Node.js 22 as the minimum development version.
  • Add grouped monthly Dependabot updates for pinned GitHub Actions.
.github/workflows/pr_evaluation.yml
Dockerfile
README.md
.github/dependabot.yml
Remove obsolete changeset and versioning artifacts no longer used by the release process.
  • Delete the changeset README, version workflow, and changelog file.
  • Add a Docker build context ignore file.
.changeset/README.md
.github/workflows/version.yml
CHANGELOG.md
.dockerignore

Assessment against linked issues

Issue Objective Addressed Explanation
#467 Enable npm releases through GitHub OIDC trusted publishing, with npm 11.5.1 or later, id-token: write limited to the publish job, and NPM_TOKEN retained as a temporary fallback; rename the workflow to release.yml so it can be configured as a trusted publisher. ✅
#467 Create a GitHub release for each version tag, with generated release notes and prerelease status for prerelease versions. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@uniqueg
uniqueg marked this pull request as ready for review October 26, 2026 18:24
@uniqueg

uniqueg commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. If the tag, version selection, or build is wrong, this workflow can publish an unintended package version to npm, where consumers may install it and reverting the workflow cannot undo that exposure. Reverting stops future releases, but an already-published version may remain in use.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

uniqueg and others added 10 commits October 27, 2026 02:28
Add scripts/release.js, which sets and checks the shared version of the
root and of all public packages in packages/*, prints their dist-tag and
pins their internal dependencies for publishing (with tests).

@elixir-cloud/tes is deprecated (no build, so a release would have no
code): mark it private, so that it is no longer released. Give
@elixir-cloud/eslint-config a repository URL for provenance. Drop the
unused `release` script (changeset publish).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rework release.yml for the branch model of the other repositories: the
version bump is a pull request to `dev`, a maintainer tags `v<version>`
on `dev`, and the workflow builds from the tag and pushes nothing (no
bump job, no deploy key). It checks that all package versions match the
tag and that the tag is on the default branch before anything else.

The publish job runs in the `npm` environment and stages each public
package with `npm stage publish --provenance --tag <dist-tag>` (trusted
publishing; npm 11.15.0 or later); maintainers approve the staged
versions on npmjs.com. The NPM_TOKEN fallback and the provenance check
are gone. Outside the main repository, staging is a dry run.

PR Evaluation now runs on pull requests to `dev`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review: check out the tagged commit (github.sha) in all jobs,
check the tag against `dev`, fail if the package list is empty or an
npm lookup fails for another reason than "not found", and warn in the
summary not to approve versions while others failed to stage. Test the
command line of scripts/release.js and clean up test fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep Node.js 24 (and 22/24 in the test matrix) in the conflicting
setup-node steps, and run PR Evaluation on pull requests to `dev`, the
default branch now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the second review: `npm view` doesn't see staged versions, so a
re-run tried to stage them again and failed on the conflict (E409)
forever. Count those as staged (the same commit, as tags are
immutable), keep stdin away from npm in the loop, and say in the
header that tags must be on `dev`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided by the maintainer: as before, the 2.0.0 alphas go to `latest`.
Prereleases of a major version without a release yet (no tag
v<major>.x.y without suffix) get `latest`; once the major is released,
prereleases get their identifier (`rc` for 2.1.0-rc.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review: without any release tag (e.g. a clone without tags),
every prerelease would get `latest`. Fail instead, also in the workflow
(the dist-tag is no longer computed inside `echo`), wrap git errors, and
tidy the tests and comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@uniqueg
uniqueg merged commit 73e4324 into dev Oct 27, 2026
14 of 15 checks passed
@uniqueg
uniqueg deleted the ci/trusted-publishing branch October 27, 2026 09:44
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.

ci: remove the NPM_TOKEN fallback ci: npm trusted publishing and GitHub releases

1 participant