Skip to content

NPM Trusted Release

NPM Trusted Release #6

name: NPM Trusted Release
# Publishes one or more NativeScript iOS npm packages
# (@nativescript/ios-v8, @nativescript/ios-hermes, @nativescript/ios-jsc,
# @nativescript/ios-quickjs, @nativescript/react-native) via npm trusted
# publishing (OIDC).
#
# Each package must be configured on npmjs.com with a trusted publisher that
# points at this repository + workflow + environment. With `engine: all`, the
# workflow fans out across the four iOS engine packages via a matrix; use
# `engine: react-native` to publish @nativescript/react-native.
on:
workflow_dispatch:
inputs:
engine:
description: "Package to release (engine package, react-native, or 'all' for every iOS engine)"
required: true
type: choice
default: v8
options:
- v8
- hermes
- jsc
- quickjs
- react-native
- all
release-type:
description: "Version bump (patch/minor/major publish to 'latest'; prerelease uses 'preid' as the dist-tag)"
required: false
type: choice
default: prerelease
options:
- prerelease
- patch
- minor
- major
version:
description: "Exact npm version to publish; overrides release-type/preid. Use a prerelease version for preview publishes, e.g. 9.0.0-preview.0"
required: false
type: string
preid:
description: "Prerelease identifier (used only when release-type=prerelease; also becomes the npm dist-tag, e.g. next | canary)"
required: false
type: string
default: next
dry-run:
description: "Run release steps without making changes (no git push, no publish)"
required: false
type: boolean
default: true
concurrency:
# Avoid overlapping publishes on the same ref/package selection.
group: npm-trusted-release-${{ github.ref }}-${{ inputs.engine }}
cancel-in-progress: false
env:
XCODE_VERSION: "26.2.0"
jobs:
matrix:
name: Resolve package matrix
runs-on: ubuntu-latest
permissions: {}
outputs:
targets: ${{ steps.compute.outputs.targets }}
steps:
- name: Compute matrix
id: compute
env:
ENGINE: ${{ inputs.engine }}
run: |
set -euo pipefail
case "$ENGINE" in
all)
echo 'targets=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT"
;;
v8|hermes|jsc|quickjs|react-native)
printf 'targets=["%s"]\n' "$ENGINE" >> "$GITHUB_OUTPUT"
;;
*)
echo "Unsupported engine: $ENGINE" >&2
exit 1
;;
esac
build:
name: Build ${{ matrix.target }}
needs: matrix
runs-on: macos-26
permissions:
contents: read
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.matrix.outputs.targets) }}
outputs:
# Per-target outputs aren't natively supported with matrices, so each job
# uploads its computed metadata alongside the tarball artifact.
placeholder: noop
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
submodules: recursive
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Install Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3"
- name: Install Dependencies
run: |
npm install
python3 -m pip install --upgrade pip six
if ! command -v ld64.lld >/dev/null; then
brew list lld || brew install lld
fi
if ! command -v cmake >/dev/null; then
brew list cmake || brew install cmake
fi
if [ ! -x /usr/local/bin/cmake ]; then
sudo mkdir -p /usr/local/bin
sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
fi
- name: Bump version
id: bump
shell: bash
env:
RELEASE_TYPE: ${{ inputs.release-type }}
PACKAGE_VERSION: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
release_type="$RELEASE_TYPE"
package_version="$PACKAGE_VERSION"
preid="$PREID"
target="$TARGET"
if [ "$target" = "react-native" ]; then
pkg_dir="packages/react-native"
package_name="@nativescript/react-native"
tarball_basename="nativescript-react-native"
npm_tag_target="react-native"
else
pkg_dir="packages/ios-${target}"
package_name="@nativescript/ios-${target}"
tarball_basename="nativescript-ios-${target}"
npm_tag_target="ios-${target}"
echo "IOS_VARIANT=ios-${target}" >> "$GITHUB_ENV"
fi
pushd "$pkg_dir" >/dev/null
if [ -n "$package_version" ]; then
npm version "$package_version" --no-git-tag-version >/dev/null
elif [ "$release_type" = "prerelease" ]; then
npm version prerelease --preid "$preid" --no-git-tag-version >/dev/null
else
npm version "$release_type" --no-git-tag-version >/dev/null
fi
NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
popd >/dev/null
NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "$npm_tag_target")
if [ -n "$package_version" ] && [ "$release_type" = "prerelease" ] && [ "$NPM_TAG" = "latest" ]; then
echo "Exact prerelease publishes must include a prerelease identifier (for example 9.0.0-preview.0)." >&2
exit 1
fi
echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT"
echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT"
echo "PACKAGE_DIR=$pkg_dir" >> "$GITHUB_OUTPUT"
echo "PACKAGE_NAME=$package_name" >> "$GITHUB_OUTPUT"
echo "TARBALL_BASENAME=$tarball_basename" >> "$GITHUB_OUTPUT"
echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG)"
- name: Build iOS engine (--${{ matrix.target }})
if: ${{ matrix.target != 'react-native' }}
env:
TARGET: ${{ matrix.target }}
run: ./scripts/build_all_ios.sh "--${TARGET}"
- name: Build @nativescript/react-native
if: ${{ matrix.target == 'react-native' }}
run: |
./scripts/build_all_react_native.sh
./scripts/build_react_native_turbomodule.sh
- name: Record metadata
shell: bash
env:
TARGET: ${{ matrix.target }}
PACKAGE_DIR: ${{ steps.bump.outputs.PACKAGE_DIR }}
PACKAGE_NAME: ${{ steps.bump.outputs.PACKAGE_NAME }}
NPM_VERSION: ${{ steps.bump.outputs.NPM_VERSION }}
NPM_TAG: ${{ steps.bump.outputs.NPM_TAG }}
TARBALL_BASENAME: ${{ steps.bump.outputs.TARBALL_BASENAME }}
run: |
set -euo pipefail
package_dir="$PACKAGE_DIR"
tarball_file="${TARBALL_BASENAME}-${NPM_VERSION}.tgz"
mkdir -p "$package_dir/dist"
cat > "$package_dir/dist/release-meta.json" <<EOF
{
"target": "$TARGET",
"package_dir": "$package_dir",
"package_name": "$PACKAGE_NAME",
"version": "$NPM_VERSION",
"tag": "$NPM_TAG",
"tarball": "$tarball_file"
}
EOF
- name: Upload npm package artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: npm-package-${{ matrix.target }}
path: |
${{ steps.bump.outputs.PACKAGE_DIR }}/dist/${{ steps.bump.outputs.TARBALL_BASENAME }}-${{ steps.bump.outputs.NPM_VERSION }}.tgz
${{ steps.bump.outputs.PACKAGE_DIR }}/dist/release-meta.json
- name: Upload dSYMs artifact
if: ${{ matrix.target != 'react-native' }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: NativeScript-dSYMs-${{ matrix.target }}
path: dist/dSYMs
publish:
name: Publish ${{ matrix.target }}
needs:
- matrix
- build
runs-on: ubuntu-latest
environment:
name: ${{ inputs.dry-run && 'npm-publish-dry-run' || 'npm-publish' }}
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.matrix.outputs.targets) }}
permissions:
contents: read
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: npm-package-${{ matrix.target }}
path: npm-package/${{ matrix.target }}
- name: Update npm (required for OIDC trusted publishing)
run: |
corepack enable npm
corepack install -g npm@11.6.2
test "$(npm --version)" = "11.6.2"
test "$(npx --version)" = "11.6.2"
- name: Read release metadata
id: meta
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
meta="npm-package/${TARGET}/release-meta.json"
if [ ! -f "$meta" ]; then
echo "Missing release metadata at $meta" >&2
exit 1
fi
NPM_VERSION=$(node -e "console.log(require('./$meta').version)")
NPM_TAG=$(node -e "console.log(require('./$meta').tag)")
PACKAGE_NAME=$(node -e "console.log(require('./$meta').package_name)")
TARBALL=$(node -e "console.log(require('./$meta').tarball)")
echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT"
echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT"
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
echo "TARBALL=$TARBALL" >> "$GITHUB_OUTPUT"
- name: Publish package (OIDC trusted publishing)
if: ${{ vars.USE_NPM_TOKEN != 'true' }}
shell: bash
env:
NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }}
NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }}
PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }}
TARBALL: ${{ steps.meta.outputs.TARBALL }}
TARGET: ${{ matrix.target }}
DRY_RUN: ${{ inputs.dry-run }}
NODE_AUTH_TOKEN: ""
run: |
set -euo pipefail
TARBALL_PATH="npm-package/${TARGET}/${TARBALL}"
PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance)
if [ "$DRY_RUN" = "true" ]; then
PUBLISH_ARGS+=(--dry-run)
fi
echo "Publishing ${PACKAGE_NAME}@${NPM_VERSION} (tag: $NPM_TAG, dry-run: $DRY_RUN) via OIDC trusted publishing..."
unset NODE_AUTH_TOKEN
rm -f ~/.npmrc || true
if [ -n "${NPM_CONFIG_USERCONFIG:-}" ]; then
rm -f "$NPM_CONFIG_USERCONFIG" || true
fi
npm publish "${PUBLISH_ARGS[@]}"
- name: Publish package (granular token fallback)
if: ${{ vars.USE_NPM_TOKEN == 'true' }}
shell: bash
env:
NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }}
NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }}
PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }}
TARBALL: ${{ steps.meta.outputs.TARBALL }}
TARGET: ${{ matrix.target }}
DRY_RUN: ${{ inputs.dry-run }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
run: |
set -euo pipefail
TARBALL_PATH="npm-package/${TARGET}/${TARBALL}"
PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance)
if [ "$DRY_RUN" = "true" ]; then
PUBLISH_ARGS+=(--dry-run)
fi
echo "Publishing ${PACKAGE_NAME}@${NPM_VERSION} (tag: $NPM_TAG, dry-run: $DRY_RUN) via granular token..."
npm publish "${PUBLISH_ARGS[@]}"
summary:
name: Release summary
if: always()
needs:
- matrix
- build
- publish
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Print summary
env:
PACKAGE_SELECTION: ${{ inputs.engine }}
RELEASE_TYPE: ${{ inputs.release-type }}
PACKAGE_VERSION: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
DRY_RUN: ${{ inputs.dry-run }}
TARGETS: ${{ needs.matrix.outputs.targets }}
BUILD_RESULT: ${{ needs.build.result }}
PUBLISH_RESULT: ${{ needs.publish.result }}
run: |
echo "Package selection: $PACKAGE_SELECTION"
echo "Release type: $RELEASE_TYPE"
echo "Exact version: $PACKAGE_VERSION"
echo "Preid: $PREID"
echo "Dry run: $DRY_RUN"
echo "Targets: $TARGETS"
echo "Build result: $BUILD_RESULT"
echo "Publish result: $PUBLISH_RESULT"