Releasing shinyreact
Three packages, three independent versions, three tag prefixes. Releasing one does not oblige releasing the others.
| Package | Version lives in | Tag | Automation |
|---|---|---|---|
shinyreact (PyPI) |
pyproject.toml (repo root) version |
py/v1.2.3 |
.github/workflows/release-py.yaml |
@posit-dev/shinyreact (npm) |
pkg-js/package.json version |
js/v1.2.3 |
.github/workflows/release-js.yaml — stages only, a human approves |
shinyreact (R) |
pkg-r/DESCRIPTION Version |
r/v1.2.3 |
.github/workflows/release-r-wasm.yaml attaches wasm assets to the GitHub release — CRAN itself is a manual submission |
The <lang>/v prefix is what keeps the three apart; nothing else distinguishes
them. A bare v1.2.3 tag triggers nothing.
Before any release
The Python and R packages vendor the built JS bundle (pkg-py/src/shinyreact/www/,
pkg-r/inst/lib/shiny/). Releasing either one without rebuilding ships stale
JS, and nothing in the release workflow catches it — the committed copies are
checked by CI on PRs, not at tag time.
git switch main && git pull
make update-dist update-skills update-examples
git status --porcelain # must be empty
A non-empty status here means main was merged with stale generated files: open a normal PR with the regenerated output first, then release.
Then confirm main is green (gh run list --branch main --limit 5) and that the
checks for the package you're releasing actually ran — check-r and friends are
path-filtered and can be skipped rather than passing.
Python → PyPI
- Bump
versionin the rootpyproject.toml(notpkg-py/; there is no pyproject there). Open a PR, merge it. - From the merged main:
git tag py/v1.2.3 && git push origin py/v1.2.3. release-py.yamlverifies the tag matchesuv version --short, runsuv build, publishes with PyPI trusted publishing, and creates the GitHub release with generated notes and the sdist/wheel attached.
If the tag and pyproject.toml disagree the job fails before publishing —
delete the tag (git push --delete origin py/v1.2.3), fix, re-tag.
Trusted publishing must be configured once at
https://pypi.org/manage/project/shinyreact/settings/publishing/ with workflow
release-py.yaml and environment pypi. There is no API token to rotate.
JS → npm
Unlike PyPI, CI cannot publish npm — it can only stage. The tag kicks off a build that uploads a pending tarball; a human then approves it with 2FA. Two steps, and the second one is not yours to do.
Bump
versioninpkg-js/package.json(cd pkg-js && npm version 1.2.3 --no-git-tag-versionalso updatespackage-lock.json), and the matching constants_SHINYREACT_JS_VERSIONinpkg-py/src/shinyreact/_dep.pyand.shinyreact_js_versioninpkg-r/R/dep.R(the shinyreact HTMLDependency version;test_dep.py/test-dep.Rfail on drift). In the same PR, bump the"@posit-dev/shinyreact"range inexamples/09-hmr/package.jsonandexamples/11-npm-bare/package.jsonto^1.2.3— they depend on the released package, not the checkout, so without this they silently stay on the previous line. Open a PR, merge it.git tag js/v1.2.3 && git push origin js/v1.2.3.release-js.yamlverifies the tag matchespackage.json, lints, tests, builds (IIFE + npm ESM + types), and runsnpm stage publish. Nothing is on npm yet. The job summary ends with the approval commands.Hand off to the human. Report the run URL and stop — do not try to approve. Approval needs proof of presence (2FA), which an OIDC token cannot provide:
npm stage list @posit-dev/shinyreact # find the stage id npm stage view <stage-id> # inspect what CI built npm stage download <stage-id> # or pull the actual tarball npm stage approve <stage-id> --otp <code> # publishes it npm stage reject <stage-id> # throws it awayThe Staged Packages tab on https://www.npmjs.com/package/@posit-dev/shinyreact does the same thing in a browser; 2FA is prompted either way. A stage is not on the registry and nobody can install it, so a bad build is rejected rather than unpublished — there is no 72-hour
npm unpublishwindow to race.
There is no NPM_TOKEN, and adding one would be a regression — npm's own
guidance is "when trusted publishing is available for your workflow, always
prefer it over long-lived tokens", and direct publishing with a granular token
is being removed entirely in January 2027. Auth is npm trusted publishing
(OIDC): configured once on the package's Settings page against org
posit-dev, this repo, workflow filename release-js.yaml, and
npm stage publish as the allowed action — so the workflow cannot publish
directly even if someone edits it. Provenance is automatic under OIDC; the
workflow passes no --provenance.
Settings → Publishing access is also set to "Require two-factor authentication and disallow tokens". That closes the side door a token would open without touching CI, since trusted publishing is OIDC rather than a token. npm calls stage-only trusted publishing plus disallowed tokens the maximum security posture; if you ever find yourself creating a token to work around a release, that is the thing you are undoing.
Requires npm ≥ 11.15.0 and Node ≥ 22.14.0. pkg-js/.nvmrc pins Node 22, which
bundles npm 10.x, so the workflow upgrades npm explicitly — if a release fails
with an unknown stage command, that step is why.
A JS release is usually paired with a Python and R release, since the packages
ship the same bundle — but they are separate versions and separate tags. Do the
make update-dist PR first so all three ship the same code.
The two things staging cannot do
- A brand-new package cannot be staged, and trusted publishing cannot be
configured until the package exists.
0.1.0was therefore published by hand from a maintainer's laptop —npm loginandnpm publish --access public, interactively with 2FA, no token created. It has nojs/v0.1.0tag (pushing one would only have triggered a job that failed on a version already taken) and no provenance attestation, since that requires a CI publish. Every release from0.1.1on follows the flow above. - A new scope or a new package name puts you back in that bootstrap case: publish once interactively, then configure the trusted publisher.
R → CRAN
No workflow. CRAN submission is interactive and cannot be automated by a tag. The checklist is generated, not written by hand:
# from pkg-r/ — usethis needs it as the active project
usethis::use_release_issue("1.2.3")
Then work through the issue's bullets in order and tick them off as you go.
usethis knows what a shinyreact release needs better than this file does — it
emits the right checklist for a first release vs. a patch, and it is the source
of truth for the CRAN steps (urlchecker::url_check(), devtools::check(remote = TRUE, manual = TRUE),
revdep checks, devtools::submit_cran(), the post-acceptance version bump).
Two things the generated checklist does not know about:
- Run
make r-checktoo (it fails on NOTEs), not justdevtools::check(). - Its "tag the release" bullet means
r/v1.2.3here, notv1.2.3. Tag after CRAN accepts, thengh release create r/v1.2.3 --generate-notes.
Creating that GitHub release also fires release-r-wasm.yaml, which builds a
webR filesystem image of the package plus its dependencies and attaches
library.data.gz + library.js.metadata to the release. That is what lets a
shinylive app install shinyreact from GitHub (#268):
pak::pak("posit-dev/shinyreact/pkg-r@r/v1.2.3")
shinylive resolves the assets by the ref recorded at install time, so the
release tag and the ref users install must be the same string — r/v1.2.3.
The workflow pins ghcr.io/r-wasm/webr:v0.6.0 to match the webR that shinylive
ships; bump it when shinylive's R version moves. Once shinyreact is on CRAN and
mirrored to repo.r-wasm.org, this matters only for dev installs.
If use_release_issue() is unavailable, open-source:create-release-checklist
produces the same tidyverse checklist.
After any release
Nothing is automated post-release. Check the artifact actually landed
(pip index versions shinyreact, npm view @posit-dev/shinyreact version) and
say so — a green workflow with a skipped publish step is the usual failure.
For npm, a green workflow means staged, not published: npm view will keep
reporting the previous version until a human runs npm stage approve. Don't
report a JS release as done on the strength of a green run.