Skip to content

ci: publish to npm via OIDC trusted publishing#71

Merged
adnaan merged 5 commits intomainfrom
ci-publish
Apr 15, 2026
Merged

ci: publish to npm via OIDC trusted publishing#71
adnaan merged 5 commits intomainfrom
ci-publish

Conversation

@adnaan
Copy link
Copy Markdown
Contributor

@adnaan adnaan commented Apr 14, 2026

Summary

  • Make CI the sole publisher of @livetemplate/client, triggered only by the explicit release: published event — no more pasting an npm token (or npm login) when running release.sh.
  • publish.yml now uses npm OIDC trusted publishing (id-token: write) plus npm publish --provenance, so the repo holds zero npm secrets and the package gets a verified provenance badge on npmjs.com. Checkout is pinned to github.event.release.tag_name and a version-match guard rejects releases that point at a mismatched package.json.
  • release.sh keeps its local pre-flight (test, build, verify, package contents) and still tags + pushes + creates the GitHub release — but no longer runs npm publish locally. The publish_npm() function and npm whoami check are gone. Dry-run text and closing summary updated to point at the Actions workflow.

Trusted publisher (already configured)

The npmjs.com Trusted Publisher for @livetemplate/client has been pointed at this repo + publish.yml, so the new workflow can authenticate via OIDC without NPM_TOKEN.

Test plan

  • bash -n scripts/release.sh — syntax check passes
  • dry_run output inspected — no longer mentions a local npm publish step
  • npm test — 340 passed across 20 suites
  • Workflow YAML parses cleanly
  • After merge: cut a small patch release (e.g. v0.8.24) via ./scripts/release.sh and watch the Publish workflow on the Actions tab
  • Confirm npm view @livetemplate/client@<new> shows dist.attestations (provenance uploaded)
  • Confirm the package page on npmjs.com shows a green "Provenance" badge linking to the workflow run

Rollback

  • If CI fails before npm publish: nothing on npm. Re-run the failed workflow from the Actions UI; no need to recreate the tag/release.
  • If CI fails after npm publish but in the verify step: the package is published and fine — npm read-after-write can lag 10–60s. Re-running the verify step (or just running npm view manually) will succeed.
  • If a bad version ships: npm deprecate @livetemplate/client@X.Y.Z "reason". Don't unpublish — npm forbids it after 72h and breaks downstream installs.

🤖 Generated with Claude Code

Make CI the sole publisher of @livetemplate/client, triggered only by the
explicit GitHub release event so ordinary pushes never publish. Removes the
need for a local npm login (or pasted NPM_TOKEN) when running release.sh.

publish.yml now uses npm OIDC trusted publishing (id-token: write) and
npm publish --provenance, so no NPM_TOKEN secret is required and the
package gets a verified provenance attestation on npmjs.com. The checkout
step pins to the release tag to guarantee the published artifact matches
what was tagged, and a version-match guard catches releases pointing at a
mismatched package.json.

release.sh no longer runs npm publish locally — it only bumps the version,
runs tests/build as a pre-flight gate, commits, tags, pushes, and creates
the GitHub release. The release event then triggers publish.yml. Output
text and dry-run summary updated to reflect that npm publish is async.

Requires (one-time, already done): trusted publisher configured for
@livetemplate/client on npmjs.com pointing at this repo + publish.yml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 14, 2026 06:55
@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Review: ci: publish to npm via OIDC trusted publishing

Overall this is a solid improvement — moving from a long-lived NPM_TOKEN secret to OIDC trusted publishing is the right direction. A few observations:

Looks good:

  • id-token: write and contents: read are correctly scoped to the job
  • Pinning checkout to github.event.release.tag_name is an important correctness fix — previously the workflow could build from a later commit than what was tagged
  • The version-match guard is a good safety net against tag/package.json drift
  • Quoting "@livetemplate/client@$PKG_VERSION" in the verify step (vs. the old unquoted form) is a minor but real improvement
  • --access public is required for scoped packages and correctly included

One thing to confirm:

The version-match guard strips a leading v with ${GITHUB_REF_NAME#v}. If a tag is ever created without the v prefix (e.g. 1.0.0 instead of v1.0.0), this silently skips the strip and comparison still works — so there's no failure mode here, just worth noting the assumption is baked in.

Potential gap in the verify step:

npm view immediately after publish can fail due to npm registry replication lag (10–60s). The rollback notes already call this out, but the step has no retry logic — a transient lag would fail the workflow and require a manual re-run. Consider wrapping it with a simple retry loop:

for i in 1 2 3; do
  npm view "@livetemplate/client@$PKG_VERSION" && break
  echo "Attempt $i failed, retrying in 30s..."
  sleep 30
done

This is non-blocking for merging, but worth a follow-up issue to avoid spurious failures on an otherwise successful publish.

No blockers. The core security goal is met and the implementation is clean.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the release process so @livetemplate/client is published to npm exclusively from GitHub Actions using npm OIDC trusted publishing (no stored npm token), triggered by release: published.

Changes:

  • Remove local npm publish from scripts/release.sh and update dry-run/final output to point to CI publishing.
  • Update .github/workflows/publish.yml to publish via OIDC (id-token: write) with npm publish --provenance, and pin builds to the release tag.
  • Add a guard in CI to ensure the release tag version matches package.json before publishing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
scripts/release.sh Stops local npm publishing; clarifies that GitHub release triggers CI publishing.
.github/workflows/publish.yml Switches npm auth to OIDC + provenance; checks out the release tag and validates version match.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/publish.yml Outdated
npm view @livetemplate/client@$PACKAGE_VERSION
PKG_VERSION=$(node -p "require('./package.json').version")
echo "Published version: $PKG_VERSION"
npm view "@livetemplate/client@$PKG_VERSION"
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The post-publish verification step runs a single npm view, but npm read-after-write can lag (often 10–60s). This can cause the workflow to fail even though npm publish succeeded. Consider adding a small retry loop with backoff (or making this check non-blocking) so transient registry propagation doesn’t break releases.

Suggested change
npm view "@livetemplate/client@$PKG_VERSION"
for attempt in 1 2 3 4 5; do
if npm view "@livetemplate/client@$PKG_VERSION"; then
echo "Package is visible on npm."
exit 0
fi
if [ "$attempt" -lt 5 ]; then
delay=$((attempt * 10))
echo "Package not visible yet on npm (attempt $attempt/5). Retrying in ${delay}s..."
sleep "$delay"
fi
done
echo "Package @livetemplate/client@$PKG_VERSION was not visible on npm after multiple attempts."
exit 1

Copilot uses AI. Check for mistakes.
Both Claude and Copilot review bots flagged that the post-publish
'npm view' verification step runs a single query with no retry logic.
npm's read-after-write can lag 10–60s due to registry replication, so
a transient propagation delay would fail the workflow even though
'npm publish' succeeded — forcing a manual re-run on an otherwise
successful release.

Wrap the check in a bounded 5-attempt loop with progressive backoff
(10s, 20s, 30s, 40s = up to 100s total wait), exiting on first
success. If the package still isn't visible after 5 attempts, exit 1
so genuine publish failures are still surfaced.

Addresses review comments from claude[bot] and Copilot on #71.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Review

This is a solid improvement. OIDC trusted publishing eliminates the NPM_TOKEN secret, provenance attestation improves supply chain transparency, and pinning the checkout to github.event.release.tag_name prevents a tag-points-at-wrong-commit edge case. The version-match guard is a good safety net.

One thing to verify: actions/setup-node@v4 — the diff doesn't show whether registry-url: 'https://registry.npmjs.org' is still set in that step. For OIDC-based publishing, npm needs a properly configured .npmrc pointing at the registry. If registry-url was only present alongside NODE_AUTH_TOKEN and got removed, the publish step could fail silently or fall back to the wrong registry. Worth confirming the setup-node step still has registry-url set (even without a token).

Minor note: If the verify step exhausts all 5 retries and exits 1, the package is already live on npm — the CI failure would be a false alarm. The rollback section in the PR description covers this correctly, but it's worth adding an inline comment in the workflow (above exit 1) so whoever investigates the alert doesn't start a panic rollback.

Otherwise: clean, well-scoped change. No bugs or security issues found.

Claude review on dc8dd26 pointed out that if the retry loop exhausts all
5 attempts and exits 1, the package has already been published — the
failure is a registry propagation lag, not a publish failure. Without
context, whoever investigates the red CI run might start an unwarranted
'npm deprecate' rollback on a perfectly good release.

Add an inline comment at the exit path making this operationally
obvious, plus a second echo clarifying that the publish step itself
succeeded. Verify step logic is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Good change overall — moving to OIDC trusted publishing eliminates the NPM_TOKEN secret and adds provenance attestation without much added complexity.

One thing to verify: The setup-node step needs registry-url: 'https://registry.npmjs.org' for OIDC to work (it writes the .npmrc that npm reads to know where to request the OIDC token exchange). The diff doesn't show that part of publish.yml — if it was already there, you're fine; if not, the npm publish step will fail to authenticate.

Minor concern on the verify step: The final exit 1 when the package isn't visible after 5 retries will mark the workflow run as failed even though the package is actually published. That risks someone seeing a red CI run and attempting a rollback for what's just a registry propagation delay. The inline comment explains it well, but consider exit 0 here (or lowering the exit code to a warning) since a failed publish would have already aborted the workflow at the publish step itself.

Everything else looks solid:

  • Checkout pinned to github.event.release.tag_name
  • Version-match guard before publish ✓
  • Least-privilege permissions (contents: read, id-token: write) ✓
  • release.sh cleanly delegates publish to CI ✓

Claude review on 61a5dc8 pointed out that exiting non-zero when the
5-attempt verify loop exhausts is semantically wrong: by definition the
publish step already succeeded to reach the verify step, so a red CI run
on a successful release just invites a panicked rollback on what is
actually registry replication lag.

Switch the exhausted-retries path from 'exit 1' to a GitHub Actions
'::warning::' annotation that shows up in the run summary. The workflow
still completes green (matching reality: the package IS published), but
the anomaly is surfaced clearly in the UI with actionable guidance
('manually confirm with npm view ... before taking any action').

A genuine publish failure would have aborted the workflow at the
'npm publish' step itself, so no signal is lost by this change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Review: ci: publish to npm via OIDC trusted publishing

The approach is sound and well-executed. A few notes:

Looks good:

  • OIDC + provenance is the right move — eliminates a long-lived secret and adds supply-chain traceability
  • Pinning checkout to the release tag name is an important correctness fix
  • The version-match guard prevents tag/package.json drift from silently publishing the wrong version
  • The retry loop with progressive backoff in the verify step is well-handled; the final-attempt condition correctly skips the trailing sleep

One note on the verify step:

After 5 failed attempts the step emits a warning annotation and exits 0, so the overall workflow run stays green. This is the stated intent and the reasoning is sound (npm publish success is the authoritative signal). Just make sure your team is aware that a warning annotation can be easy to miss in the GitHub Actions summary — worth noting in a runbook or RELEASING.md if one exists.

Minor:
The GITHUB_REF_NAME#v strip silently no-ops if a tag lacks a v prefix, so the comparison still works either way. No action needed, just documenting the assumption.

No blockers. The security improvement is real and the implementation is clean.

Step 6 of the Release Process in CONTRIBUTING.md still said 'Publish to
npm' as something scripts/release.sh does locally, which has not been
true since the OIDC workflow landed. Update the section to describe the
actual two-stage flow:

1. release.sh: validate, version-bump, tests, build, tag, push, GitHub
   release (no local npm publish)
2. publish.yml workflow: triggered by release:published event, runs
   OIDC trusted publishing with --provenance

Also add a 'Watching a release' subsection covering the three outcomes
an operator might see on the Actions tab (green / green-with-warning /
failed-publish), with specific guidance on the ::warning:: annotation
added in the previous commit so nobody rolls back a successfully
published package on a propagation lag. Claude review on 51c6da8
suggested capturing this operator-facing context in a runbook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Review: ci: publish to npm via OIDC trusted publishing

Overall this is a solid improvement — removing the NPM_TOKEN secret, adding provenance attestation, and pinning checkout to the release tag are all good practices.

One notable gap: no npm test in the publish workflow.
CI is now the sole publisher, but the workflow only runs npm run build before publishing. Tests run locally in release.sh, but not in the CI environment that actually does the publish. If a test failure is environment-specific, it won't be caught before the package lands on npm. Consider adding npm test between the Build and Verify version steps.

Minor: GITHUB_REF_NAME assumption in version check.
The TAG_VERSION="${GITHUB_REF_NAME#v}" stripping works correctly for v-prefixed tags (e.g. v1.2.3), but silently passes for tags without a v prefix (e.g. 1.2.3) since the substitution is a no-op. This is fine if your tag convention is consistent, but worth noting if future tags ever deviate.

Everything else looks good:

  • --provenance --access public are both correct for a scoped public package
  • The retry/backoff loop in the verify step is well-implemented and the warning annotation (instead of hard failure) on propagation lag is the right call
  • Version-match guard prevents mis-tagged releases
  • Rollback instructions in the PR description and CONTRIBUTING.md are accurate (npm deprecate vs npm unpublish)

@adnaan
Copy link
Copy Markdown
Contributor Author

adnaan commented Apr 15, 2026

Thanks for the review — a quick clarification on the npm test point so nobody acts on it:

One notable gap: no npm test in the publish workflow.

npm test is in publish.yml already — it's at line 29, between npm ci (line 26) and npm run build (line 32):

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

It was present in the very first commit of this PR and I haven't touched it since, so it doesn't appear in any of the later diffs the review workflow sees. The environment-specific-failure concern is already covered — tests run in CI before npm publish, and a failure there aborts the workflow before anything hits npm.

The v-prefix note on GITHUB_REF_NAME#v is accurate: it's a no-op on tags without a v prefix, which is fine given our convention (every tag in git tag --list 'v*' uses the prefix, and release.sh always creates them with v). Not changing this.

No code change for this round.

@adnaan adnaan merged commit 9053e38 into main Apr 15, 2026
6 checks passed
@adnaan adnaan deleted the ci-publish branch April 15, 2026 02:12
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