Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: CI
on:
workflow_call:
push:
branches:
- main
Expand Down Expand Up @@ -54,8 +55,12 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

- name: Build package
run: yarn prepare
- name: Build and inspect npm package
run: |
yarn build
mkdir artifacts
npm pack --ignore-scripts --pack-destination artifacts
node scripts/check-package.mjs artifacts/*.tgz

build-android:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -98,7 +103,6 @@ jobs:
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"


- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
Expand All @@ -112,7 +116,7 @@ jobs:

- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
JAVA_OPTS: '-XX:MaxHeapSize=6g'
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"

Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Release

on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- '.github/actions/setup/**'
- '.github/workflows/ci.yml'
- '.github/workflows/release.yml'
- 'package.json'
- 'scripts/check-package.mjs'
- 'scripts/check-release.mjs'
- 'tsconfig.build.json'
push:
tags:
- 'v*'

permissions:
contents: read

concurrency:
group: npm-release
cancel-in-progress: false

jobs:
verify:
if: github.event_name != 'pull_request'
uses: ./.github/workflows/ci.yml

package:
needs: verify
if: >-
always() &&
(github.event_name == 'pull_request' || needs.verify.result == 'success')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.0.0
with:
fetch-depth: 0

- name: Setup
uses: ./.github/actions/setup

- name: Validate release tag
if: github.event_name == 'push'
run: |
git merge-base --is-ancestor HEAD origin/main
node scripts/check-release.mjs "$GITHUB_REF_NAME"

- name: Build package
run: yarn build

- name: Pack and inspect
run: |
mkdir artifacts
npm pack --ignore-scripts --pack-destination artifacts
node scripts/check-package.mjs artifacts/*.tgz

- name: Preserve verified package
uses: actions/upload-artifact@v4
with:
name: npm-package
path: artifacts/*.tgz
if-no-files-found: error
retention-days: 1

publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: package
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write
steps:
- name: Use Node.js 24
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false

- name: Download verified package
uses: actions/download-artifact@v4
with:
name: npm-package
path: artifacts

- name: Publish with npm trusted publishing
shell: bash
run: |
set -euo pipefail

package_name=react-native-ease
version="${GITHUB_REF_NAME#v}"
tarball="artifacts/$package_name-$version.tgz"
dist_tag=latest
if [[ "$version" == *-* ]]; then dist_tag=next; fi

local_integrity="sha512-$(openssl dgst -sha512 -binary "$tarball" | openssl base64 -A)"
registry_file="$RUNNER_TEMP/registry.json"
registry_error="$RUNNER_TEMP/registry-error.txt"

if npm view "$package_name@$version" version dist.integrity --json > "$registry_file" 2> "$registry_error"; then
node -e '
const fs = require("node:fs");
const [file, version, integrity] = process.argv.slice(1);
const published = JSON.parse(fs.readFileSync(file, "utf8"));
if (published.version !== version || published["dist.integrity"] !== integrity) {
throw new Error("The existing registry version does not match this release tarball.");
}
' "$registry_file" "$version" "$local_integrity"
echo "$package_name@$version already exists with matching integrity; skipping publish."
exit 0
fi

if ! grep -qE '(^|[^A-Z])E404([^0-9]|$)' "$registry_error"; then
cat "$registry_error" >&2
echo "Registry lookup failed with an error other than E404; refusing to publish." >&2
exit 1
fi

npm publish "$tarball" --provenance --access public --tag "$dist_tag"

for attempt in {1..12}; do
if npm view "$package_name@$version" version dist.integrity --json > "$registry_file" &&
npm view "$package_name" "dist-tags.$dist_tag" --json > "$RUNNER_TEMP/dist-tag.json" &&
node -e '
const fs = require("node:fs");
const [releaseFile, tagFile, version, integrity] = process.argv.slice(1);
const published = JSON.parse(fs.readFileSync(releaseFile, "utf8"));
const tagged = JSON.parse(fs.readFileSync(tagFile, "utf8"));
process.exit(
published.version === version &&
published["dist.integrity"] === integrity &&
tagged === version
? 0
: 1,
);
' "$registry_file" "$RUNNER_TEMP/dist-tag.json" "$version" "$local_integrity"; then
exit 0
fi
sleep 10
done

echo "Registry verification failed." >&2
exit 1
107 changes: 107 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Release process

This repository publishes one npm package, `react-native-ease`. The example and
documentation site are not published to npm. Keep this document aligned with
[the Release workflow](.github/workflows/release.yml).

## One-time trusted-publisher setup

Configure a GitHub Actions trusted publisher in the npm package settings:

| Field | Value |
| ----------------- | ------------------------------------ |
| Organization | `appandflow` |
| Repository | `react-native-ease` |
| Workflow filename | `release.yml` |
| Environment | `release` |
| Allowed action | Direct publishing with `npm publish` |

The workflow uses npm's OpenID Connect integration and does not need an npm
write token. It publishes with Node 24 and npm provenance. See
[npm's trusted publishing guide](https://docs.npmjs.com/trusted-publishers/).

Create the GitHub `release` environment, require a maintainer review, and limit
deployment tags to `v*`. Declaring the environment in the workflow does not
create its protection rules, and the npm trust relationship must also be
configured separately.

## Prepare and verify a candidate

Start from reviewed, current `main`. Check npm before choosing a version; a Git
tag or GitHub release does not prove that a package was published.

```sh
git fetch origin --tags
npm view react-native-ease dist-tags --json
npm view react-native-ease versions --json
```

Use semantic versions. Prereleases such as `X.Y.Z-beta.N` publish to `next`;
stable versions publish to `latest`.

Run the package checks before starting `release-it`:

```sh
yarn install --immutable
yarn format:write
yarn lint
yarn test
yarn build
rm -rf artifacts
mkdir artifacts
npm pack --pack-destination artifacts
node scripts/check-package.mjs artifacts/react-native-ease-X.Y.Z.tgz
node scripts/check-release.mjs vX.Y.Z
```

CI also builds the Android, iOS, and tvOS examples. Device behavior affected by
the release still needs device validation.

After this workflow has been merged into the default branch, use **Actions →
Release → Run workflow** for a publication-free integration test. The manual run
executes the full reusable CI workflow, builds and validates the npm tarball,
and uploads it as a one-day workflow artifact. The publish job only runs for a
matching pushed tag, so a manual run cannot enter the `release` environment or
publish to npm.

The dry run cannot test npm's OIDC trust relationship because npm authenticates
the workflow only when `npm publish` runs. Use the first intentional prerelease
to validate trusted publishing end to end.

## Tag and publish

1. Run `yarn release X.Y.Z` from current `main`. `release-it` creates the release
commit and immutable `vX.Y.Z` tag, pushes them, and creates the GitHub release.
It does not publish to npm locally.
2. The tag starts the Release workflow. It reruns the complete CI suite for the
exact tag, builds and validates the npm tarball, and rejects a tag that differs
from `package.json` or is not contained in `origin/main`.
3. Review the checks and approve the protected `release` environment. The
publish job downloads and publishes the exact tarball verified by the package
job with npm provenance.
4. Confirm the package version, integrity, and dist-tag:

```sh
npm view react-native-ease@X.Y.Z version dist.integrity --json
npm view react-native-ease dist-tags --json
```

The workflow safely skips an already-published version only when the registry's
integrity matches the candidate tarball. A rerun for an older version does not
move a newer dist-tag backward.

## Recovery

- If authentication fails, verify the npm publisher's organization, repository,
workflow filename, and environment. Do not add a long-lived npm token as a
workaround.
- If validation fails before publication, fix the issue on `main` and prepare a
new version. Never move a pushed release tag.
- If a publish is interrupted, query the exact npm version before retrying. The
workflow refuses registry errors other than a real missing-version response.
- npm versions cannot be overwritten. Publish a new version for any correction.
- Do not rerun an old release to change `latest` or `next` after a newer release
has advanced that dist-tag.

Adding this workflow does not publish anything. npm publication begins only when
a matching release tag is pushed and the protected environment is approved.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"docs:build": "yarn --cwd docs build",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"prepare": "bob build",
"build": "bob build",
"test": "jest",
"lint": "yarn lint:eslint && yarn lint:ts && yarn lint:ts:example",
"lint:ts": "tsc",
Expand Down Expand Up @@ -187,7 +188,7 @@
"tagName": "v${version}"
},
"npm": {
"publish": true
"publish": false
},
"github": {
"release": true
Expand Down
Loading
Loading