From 5facd83ff2a0586037cd0b1e43313c18d1ca1f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 8 Sep 2026 08:31:58 +0200 Subject: [PATCH 1/4] ci(publish): install only the package being published The publish job ran yarn install at the repo root, whose workspaces glob is packages/* + apps/* + apps/legacy/*. Building one package pulled 1659 packages and 597.4 MiB for eight React Native example apps that are never built: 164s of a 197s job, against 19s for bob and tsc. yarn workspaces focus installs only this package's tree. It is built into Yarn 4, the same command family as the workspaces foreach already used in CI, so no plugin is needed. Measured locally: 709 packages, 71.29 MiB. Also stopped the postinstall native download (RNET_SKIP_DOWNLOAD), which this job has no use for since it runs bob and tsc and the files list excludes third-party/include from the tarball, and stopped the second install: the workflow installed, then create-package.sh installed again. --- .github/workflows/npm-publish.yml | 20 +++++++++++++++++-- .../scripts/create-package.sh | 6 +++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 01c273a957..89aee65917 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -50,6 +50,13 @@ jobs: RELEASE_TYPE: ${{ inputs.release-type || 'nightly' }} DRY_RUN: ${{ inputs.dry-run || false }} DIST_TAG_OVERRIDE: ${{ inputs.dist-tag || '' }} + # Nothing this job does needs the native artifacts: it runs bob and tsc, + # and the "files" list excludes third-party/include from the tarball. The + # postinstall download only cost time and printed iOS-backend warnings on + # a Linux runner. + RNET_SKIP_DOWNLOAD: 1 + # create-package.sh installs too; the focused install below covers it. + RNE_SKIP_INSTALL: 1 steps: - name: Checkout uses: actions/checkout@v6 @@ -71,8 +78,17 @@ jobs: if: ${{ env.EXECUTORCH_VERSION == 'PLACEHOLDER' }} run: exit 1 # this should never happen - - name: Install monorepo dependencies - run: yarn install --immutable + # The root workspaces glob is packages/* + apps/* + apps/legacy/*, so a + # plain `yarn install` pulled 1659 packages (597 MiB) to build one of + # them: 164s of a 197s job, against 19s for the actual build. `workspaces + # focus` installs only this package's tree; it is built into Yarn 4, the + # same command family as the `workspaces foreach` already used in CI. + # + # This drops --immutable, which `focus` does not accept. The lockfile is + # already asserted immutable by the CI workflow on every pull request, and + # a release publishes from a branch that passed it. + - name: Install the core package's dependencies + run: yarn workspaces focus react-native-executorch - name: Set tag run: | diff --git a/packages/react-native-executorch/scripts/create-package.sh b/packages/react-native-executorch/scripts/create-package.sh index 3dc178b57c..9b747bc885 100755 --- a/packages/react-native-executorch/scripts/create-package.sh +++ b/packages/react-native-executorch/scripts/create-package.sh @@ -24,7 +24,11 @@ trap restore_phonemis EXIT rm -rf "$PHONEMIS_DIR/data" "$PHONEMIS_DIR/test" "$PHONEMIS_DIR/scripts" rm -f "$PHONEMIS_DIR/requirements.txt" -yarn install --immutable +# The publish workflow installs before calling this script, so it sets +# RNE_SKIP_INSTALL to avoid a second pass. A local run still installs. +if [ -z "${RNE_SKIP_INSTALL:-}" ]; then + yarn install --immutable +fi # Match the version line by pattern, not by line number: `nativeLibsVersion` # sits next to it, so a positional edit rewrites the wrong field if either From fc5ad43920126fd7047606f106fbf6cd7074962d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 8 Sep 2026 08:39:58 +0200 Subject: [PATCH 2/4] ci(publish): focus the satellite installs too The satellite workflow passed 'yarn install --immutable && yarn workspace react-native-executorch prepare' as its install command, so each of the three runs paid for the same whole-monorepo install as the core job, plus the core package's postinstall native download on top. Each satellite declares react-native-executorch as a workspace:* devDependency, so focusing on the satellite pulls the core workspace in with it and the core prepare still runs. Verified locally from a clean node_modules for all three: core prepare succeeds and each satellite's tsc exits 0. --- .github/workflows/npm-publish-satellites.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/npm-publish-satellites.yml b/.github/workflows/npm-publish-satellites.yml index 6ca34c113a..cb7c43e74e 100644 --- a/.github/workflows/npm-publish-satellites.yml +++ b/.github/workflows/npm-publish-satellites.yml @@ -43,6 +43,11 @@ jobs: if: github.repository == 'software-mansion/react-native-executorch' runs-on: ubuntu-latest environment: deployment + env: + # The core package's postinstall downloads the native artifacts. Packing a + # satellite runs tsc against the core's type declarations and needs none + # of them. + RNET_SKIP_DOWNLOAD: 1 permissions: # `contents: write` is required when perform-git-operations pushes a tag. contents: write @@ -63,9 +68,12 @@ jobs: with: package-name: 'react-native-executorch-bare-resource-fetcher' package-json-path: 'packages/bare-resource-fetcher/package.json' - # Satellites type-check against the core package's published type - # declarations, so build it before packing the satellite. - install-dependencies-command: 'yarn install --immutable && yarn workspace react-native-executorch prepare' + # `workspaces focus` installs this satellite and the workspaces it + # depends on, instead of the whole monorepo: the root globs pull in + # eight React Native example apps, 1660 packages and 597 MiB, to build + # one small package. Satellites type-check against the core package's + # type declarations, so it still gets built before the satellite packs. + install-dependencies-command: 'yarn workspaces focus react-native-executorch-bare-resource-fetcher && yarn workspace react-native-executorch prepare' release-type: ${{ inputs.release-type }} version: ${{ inputs.version }} perform-git-operations: ${{ inputs.perform-git-operations }} @@ -77,7 +85,8 @@ jobs: with: package-name: 'react-native-executorch-expo-resource-fetcher' package-json-path: 'packages/expo-resource-fetcher/package.json' - install-dependencies-command: 'yarn install --immutable && yarn workspace react-native-executorch prepare' + # See the first satellite above for why this is a focused install. + install-dependencies-command: 'yarn workspaces focus react-native-executorch-expo-resource-fetcher && yarn workspace react-native-executorch prepare' release-type: ${{ inputs.release-type }} version: ${{ inputs.version }} perform-git-operations: ${{ inputs.perform-git-operations }} @@ -89,7 +98,8 @@ jobs: with: package-name: 'react-native-executorch-webrtc' package-json-path: 'packages/react-native-executorch-webrtc/package.json' - install-dependencies-command: 'yarn install --immutable && yarn workspace react-native-executorch prepare' + # See the first satellite above for why this is a focused install. + install-dependencies-command: 'yarn workspaces focus react-native-executorch-webrtc && yarn workspace react-native-executorch prepare' release-type: ${{ inputs.release-type }} version: ${{ inputs.version }} perform-git-operations: ${{ inputs.perform-git-operations }} From 13a15b5ce907600ef84cc3a2d7409e360c8d136a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 8 Sep 2026 08:44:44 +0200 Subject: [PATCH 3/4] fix(satellites): keep the tsc build-info out of the published tarball All three satellites pointed tsBuildInfoFile at ./lib/typescript inside outDir, and their files list ships all of lib/, so every published tarball carried tsconfig.tsbuildinfo: TypeScript's incremental build cache, useless to consumers and a third of the packed size. Found while checking the 0.10.0 dry run of the bare fetcher: 18 files, one of them the build info. Moving it to .cache/, which is already gitignored and outside outDir, packs 17. The core package was never affected; it sets no tsBuildInfoFile. --- packages/bare-resource-fetcher/tsconfig.json | 2 +- packages/expo-resource-fetcher/tsconfig.json | 2 +- packages/react-native-executorch-webrtc/tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bare-resource-fetcher/tsconfig.json b/packages/bare-resource-fetcher/tsconfig.json index fa51f58d53..428fb9c744 100644 --- a/packages/bare-resource-fetcher/tsconfig.json +++ b/packages/bare-resource-fetcher/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "lib", "declaration": true, "declarationMap": true, - "tsBuildInfoFile": "./lib/typescript/tsconfig.tsbuildinfo", + "tsBuildInfoFile": "./.cache/tsconfig.tsbuildinfo", "composite": true, "allowJs": false, "allowUnreachableCode": false, diff --git a/packages/expo-resource-fetcher/tsconfig.json b/packages/expo-resource-fetcher/tsconfig.json index fa51f58d53..428fb9c744 100644 --- a/packages/expo-resource-fetcher/tsconfig.json +++ b/packages/expo-resource-fetcher/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "lib", "declaration": true, "declarationMap": true, - "tsBuildInfoFile": "./lib/typescript/tsconfig.tsbuildinfo", + "tsBuildInfoFile": "./.cache/tsconfig.tsbuildinfo", "composite": true, "allowJs": false, "allowUnreachableCode": false, diff --git a/packages/react-native-executorch-webrtc/tsconfig.json b/packages/react-native-executorch-webrtc/tsconfig.json index cadd2509aa..eefcf5b357 100644 --- a/packages/react-native-executorch-webrtc/tsconfig.json +++ b/packages/react-native-executorch-webrtc/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "lib", "declaration": true, "declarationMap": true, - "tsBuildInfoFile": "./lib/typescript/tsconfig.tsbuildinfo", + "tsBuildInfoFile": "./.cache/tsconfig.tsbuildinfo", "composite": true, "allowJs": false, "allowUnreachableCode": false, From e822625c006a237ba9ed7b41d83178254d55b0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 8 Sep 2026 10:37:53 +0200 Subject: [PATCH 4/4] fix(publish): reject a dist-tag npm will reject, not one that merely looks like a version The override guard required a leading letter, on the theory that this ruled out semver. It did not. npm's actual rule is that a dist-tag must not parse as a semver range, and every example this input documented passes the old check and fails at npm: v0.8 parses as >=0.8.0 <0.9.0-0, v0.10.0 as 0.10.0. The 0.10.0 dry run hit exactly this, ten steps in, after building and packing the tarball. Ask semver instead of approximating it, and say what to use instead. The package resolves from the repo root under the focused install this branch introduces, and the check runs after that step. Also rewrote the input description: it said 'e.g. v0.8', which is one of the values npm refuses, and did not say the field is the npm dist-tag rather than the git tag to publish from. --- .github/workflows/npm-publish.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 89aee65917..03cf204010 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -15,7 +15,7 @@ on: - legacy # maintenance release of an older line default: nightly dist-tag: - description: 'Publish under this exact dist-tag instead, e.g. v0.8 for a maintenance line (leave empty to use release-type)' + description: 'npm dist-tag to publish under instead of the one release-type implies, e.g. v0-8 for a maintenance line. NOT the git tag. Leave empty for a normal release.' required: false type: string default: '' @@ -101,10 +101,12 @@ jobs: echo "dist-tag override needs a stable build: set release-type to latest or legacy." >&2 exit 1 fi - # npm rejects a dist-tag that parses as a semver range, so require a - # leading letter. That also rules out bare versions like `0.8`. - if [[ ! "$DIST_TAG_OVERRIDE" =~ ^[a-zA-Z][a-zA-Z0-9._-]*$ ]]; then - echo "Invalid dist-tag: '$DIST_TAG_OVERRIDE' (expected e.g. v0.8)" >&2 + # npm's rule is that a dist-tag must not parse as a semver range, + # so ask semver rather than approximate it. The previous check only + # required a leading letter, which let through every tag this input + # documented: `v0.8` parses as `>=0.8.0 <0.9.0-0`, `v0.10.0` as + # `0.10.0`. Both reached npm and failed there instead of here. + if ! node -e 'const r=require("semver").validRange(process.argv[1]); if (r) { console.error(`Invalid dist-tag: ${process.argv[1]} parses as the semver range ${r}, and npm rejects those. Use a form that is not a version, e.g. v0-8 for the 0.8 line.`); process.exit(1); }' "$DIST_TAG_OVERRIDE"; then exit 1 fi echo "TAG=$DIST_TAG_OVERRIDE" >> $GITHUB_ENV