Skip to content

Migrate to npm and publish via the shared release workflow - #5

Closed
JStonevalley wants to merge 7 commits into
mainfrom
claude/handover-context-review-5y5auf
Closed

Migrate to npm and publish via the shared release workflow#5
JStonevalley wants to merge 7 commits into
mainfrom
claude/handover-context-review-5y5auf

Conversation

@JStonevalley

@JStonevalley JStonevalley commented Jul 31, 2026

Copy link
Copy Markdown
Member

Why

The shared publish pipeline in sellpy/package-release-actions has been adopted by pdf-creator, commons, automation-commons and fetch-graphql-schema (automation-commons#16 most recently), all four with real releases through it. This repo is the fifth and last single-package one; after it only design-system remains.

It was also the furthest from the others: the only one publishing on: release rather than on a labelled merge, and the only one on Yarn. The registry becomes the source of truth for the released version, and nothing is pushed to main any more — only a vX.Y.Z tag.

What is here

release.yml becomes a caller stanza, keeping the trigger, concurrency group, validate gate and NPM_AUTOMATION_TOKEN secret name here, which is what the shared workflow leaves to callers. permissions: contents: write on the calling job is required, not optional — a called workflow can lower the permissions it is handed but never raise them. The hand-written ~/.npmrc, the vestigial yarn pack step and --access public go with it.

ci.yml gains workflow_call so the publish can gate on it, and pr-validation.yml is new.

package.json takes a 0.0.0-managed placeholder and drops publishConfig.registry, which outranks the --registry flag the shared workflow publishes with. publishConfig.access: "public" replaces the old --access public flag; the package is already public on npmjs, so this records intent rather than changing anything.

Setup and CI move to npm ci with setup-node's npm cache, and checkout/setup-node go from v3 to v5 (v3 runs on the retired Node 16 runner). CONTRIBUTING.md is rewritten for npm and documents the new release cadence: every merge to main now publishes, and a semver label is mandatory on every PR.

Unique to this repo

This is a Yarn 3 → npm migration as well as a pipeline one, since the shared workflow installs with npm ci. Three things in it would each have broken the release on their own:

react-native-builder-bob cannot find tsc under npm ≥ 9. bob 0.20 locates it by running npm bin, removed in npm 9, then falls back to which tsc with only a warning — so it silently uses a global TypeScript locally and fails hard on a runner that has none. Pinned via bob's tsc option.

npm's hoisting broke the example. Yarn's nmHoistingLimits: workspaces kept expo and the peer deps in example/node_modules; npm hoists them to the root, which breaks the relative import App from '../../App' inside expo/AppEntry.js and leaves the metro aliases pointing at paths that no longer exist. The example now registers its root component from its own index.js and resolves peer deps with require.resolve.

The example moves to Expo 54 and Metro web bundling. expo export:web and @expo/webpack-config were deprecated in SDK 50 and webpack removed in SDK 51, so upgrading that package would only have bought time. webpack.config.js, @expo/webpack-config and babel-loader are deleted and CI runs expo export -p web. Expo 54 brings react 19.1.0, react-native 0.81.5, react-native-web ~0.21.0 and friends, with the root's react/react-native devDependencies tracking the same versions; babel-preset-expo and @react-native/babel-preset become explicit dependencies because they no longer resolve transitively.

Three consequences of that upgrade worth flagging:

  • .nvmrc v18 → v22. RN 0.81 requires Node ≥ 20.19, so v18 fails at install. The shared workflow reads .nvmrc, so releases build on 22 from now on — same as the siblings.
  • A public type widened. React 19 made useRef<T>(null) return RefObject<T | null>, which RefObject<NativeComponent> and RefObject<ScrollView> no longer accept, so bob build failed emitting definitions. Those parameters now take | null. Internal dereferences already null-checked, so no logic changed, and widening leaves existing callers unaffected.
  • Lint was fully broken until @react-native/eslint-config@0.81 turned out to have dropped eslint-plugin-prettier, leaving prettier/prettier unregistered. The config now extends plugin:prettier/recommended.

Testing

The registry read, publish and tag push only run on a merge, so they cannot be verified beforehand — but four sibling repos have released through this exact workflow.

On the first merge, expect Registry latest is 0.1.1, publishing 0.1.2 (patch), then a publish, then tag v0.1.2.

Two gaps to know about: the native dev flow (start/ios/android) has not been run — it needs a device, and the entry point, dependency resolution and Expo major all changed, so start the app once before merging. And the jest suite is a single test.todo, so needs: validate gates lint, typecheck and builds rather than real coverage.

claude added 4 commits July 31, 2026 07:55
Prepares the repo for the shared release pipeline in
sellpy/package-release-actions, which installs with npm ci.

- Drop yarn.lock, .yarn/ (pinned Yarn 3.6.1 release + plugins), .yarnrc.yml
  and packageManager; add package-lock.json.
- resolutions -> overrides. Add a targeted peer override so
  @expo/webpack-config (peer expo@^48) resolves against the example's
  expo@~50; Yarn only warned about this, npm refuses to install.
- Drop the `example` passthrough script: it ran an arbitrary command in a
  workspace, which npm has no equivalent for. Call sites use
  `npm exec -w @sellpy/react-native-scroll-anchor-example -- <cmd>`.
- Pin bob's tsc path. bob 0.20 locates tsc via `npm bin`, removed in npm 9,
  so it silently fell back to a globally installed tsc and would fail in CI.
- Fix the example for npm's hoisting. Yarn used nmHoistingLimits:
  workspaces, so expo and the peer deps sat in example/node_modules; npm
  hoists them to the root, which broke expo/AppEntry.js's relative import and
  left the metro/webpack aliases pointing at paths that no longer exist. The
  example now registers its root component from its own index.js and resolves
  peer deps with require.resolve.

Verified: lint, typecheck, jest, `bob build` and `expo export:web` all pass,
and the packed tarball contains lib/ and src/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
- setup action: npm ci with setup-node's built-in npm cache, replacing the
  yarn install and the hand-rolled actions/cache block. always-auth was a
  no-op without a registry-url.
- ci.yml: yarn -> npm at every call site. `npm test --` forwards the jest
  flags; the example web build goes through `npm exec -w`.
- Add workflow_call to ci.yml so the publish workflow can gate on it.
  Nothing in this repo was callable before.
- Bump checkout to v5 and setup-node to v5; the v3 releases run on the
  retired Node 16 runner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
Replaces the release-triggered publish with the org-wide pipeline in
sellpy/package-release-actions@v1, matching commons, pdf-creator,
automation-commons and fetch-graphql-schema.

- release.yml now fires when a PR to main closes, gates on ci.yml and
  delegates to npm-publish-master.yml@v1. The old hand-written ~/.npmrc, the
  vestigial `yarn pack` step and `--access public` all go with it; the shared
  workflow authenticates, resolves the next version from the registry,
  publishes and pushes a vX.Y.Z tag using GITHUB_TOKEN.
- Add pr-validation.yml so every PR carries exactly one major/minor/patch
  label, which is what the publish reads to decide the bump.

Two follow-ups have to happen outside the repo before the first release:
create the major/minor/patch labels, and confirm NPM_AUTOMATION_TOKEN can
both publish and read this package's versions.

Note the cadence change: every merge to main now publishes, and a semver
label is mandatory on every PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
yarn -> npm throughout, including the note that npm could not be used, and
the example commands rewritten as workspace-scoped npm invocations. Adds a
Releasing section covering the mandatory semver label, the merge-to-main
cadence and the 0.0.0-managed placeholder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
claude added 3 commits July 31, 2026 09:01
publishConfig.access: public replaces the dropped `--access public` flag, so
the intent lives in package.json rather than in a workflow this repo no
longer owns. The shared publish workflow runs a bare `npm publish`, and
nothing about `access` interferes with its `--registry` flag — unlike
publishConfig.registry, which outranks it and is why that field had to go.

Note this is belt-and-braces rather than a fix: access is a registry-side
setting on the package, and `--access` only applies to the first publish of a
scoped name. `npm access get status` reports @sellpy/react-native-scroll-anchor
as public today and dropping the flag would not have changed that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
@expo/webpack-config@19.0.1 declares `expo: "^49.0.7 || ^50.0.0-0"`, so it
accepts the example's expo 50 on its own. The repo was pinned to ^18.0.1,
which resolves to 18.1.4 and still asks for expo ^48.0.17 — the conflict npm
refused to install and Yarn only warned about.

Bumping the dependency fixes the cause rather than silencing the symptom, so
the `overrides` entry added for it goes away. The @types/react override stays;
that one is a deliberate pin, not a workaround.

Verified: clean npm ci, lint, typecheck, jest, and `expo export:web` compiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
Replaces `expo export:web` with `expo export -p web`. The webpack path was
deprecated in SDK 50 and removed in 51, and @expo/webpack-config@19.0.1 is its
last release, so the previous commit's dependency bump only bought time. Metro
is the supported web bundler and needs no separate bundler config: the example
loses webpack.config.js, @expo/webpack-config and babel-loader outright.

Expo 54 is the floor we support, which sets the rest:

- example: expo ~54.0.36, react 19.1.0, react-dom 19.1.0, react-native 0.81.5,
  react-native-web ~0.21.0, expo-status-bar ~3.0.9 — the versions SDK 54
  bundles. `babel-preset-expo` and `@react-native/babel-preset` become explicit
  dependencies; both used to resolve transitively and no longer do.
- root devDependencies track the same react and react-native, so the example
  and the library build against one copy, and @types/react moves to 19 (the
  override with it).
- .nvmrc goes v18 -> v22. React Native 0.81 requires Node >= 20.19, so v18
  would have failed at install; v22 also matches the sibling packages.
- app.json declares `web.bundler: "metro"` and metro.config.js requires
  `expo/metro-config` rather than the `@expo/metro-config` alias.

Two things this surfaced rather than caused:

- **React 19 changed `useRef<T>(null)` to return `RefObject<T | null>`.** The
  public types took `RefObject<NativeComponent>` and `RefObject<ScrollView>`,
  which a ref from `useRef<T>(null)` no longer satisfies, so `bob build` failed
  on the type definitions. The parameters are widened to accept `| null`, which
  is what a React 19 caller actually has. Every internal dereference already
  null-checked, so no logic changed. Callers passing a non-null ref are
  unaffected: this widens what the API accepts.
- **@react-native/eslint-config 0.81 no longer depends on
  eslint-plugin-prettier**, only on eslint-config-prettier, so `prettier/prettier`
  was an unregistered rule and every file errored. The config now extends
  `plugin:prettier/recommended`, which registers the plugin and keeps the
  formatting rule enforced.

Verified from a clean `npm ci`: lint, typecheck, jest, `bob build` via the
prepare hook, and `expo export -p web` — whose bundle contains the library's own
source, confirming the module-resolver alias still resolves through Metro.
`npm pack --dry-run` is unchanged at 73 files.

Untested: the native dev flow (`start` / `ios` / `android`), which needs a
device or simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STVhZLEVATkpeYFJijWQuZ
@JStonevalley
JStonevalley marked this pull request as ready for review July 31, 2026 12:55
@JStonevalley

Copy link
Copy Markdown
Member Author

@ThunbergOlle will migrate this into Sellphone

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants