NPM Trusted Release #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: NPM Trusted Release | |
| # Publishes NativeScript runtime npm packages via npm trusted publishing (OIDC). | |
| # | |
| # iOS engine packages: @nativescript/ios-{v8,hermes,jsc,quickjs} | |
| # Android engine packages: @nativescript/android-{v8,hermes,jsc,quickjs-ng,primjs} | |
| # React Native package: @nativescript/react-native | |
| # | |
| # Each package must be configured on npmjs.com with a trusted publisher that | |
| # points at this repository + workflow + environment. | |
| # | |
| # `engines` accepts any comma/space-separated combination (e.g. "v8, quickjs"), | |
| # `all` for every engine a platform supports, or `react-native`. The `ios` and | |
| # `android` checkboxes pick which platforms build the selected engines; primjs | |
| # is Android-only. | |
| # | |
| # The Android runtime lives on a separate branch until it merges to main. | |
| # Dispatch this workflow from main and set `build-ref` to that branch (e.g. | |
| # android-react-native): the run then uses this workflow definition but builds | |
| # and publishes from that branch's sources. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| engines: | |
| description: "Engines to release, comma/space separated: v8, quickjs, jsc, hermes, primjs — or 'all'. Use 'react-native' for @nativescript/react-native." | |
| required: true | |
| type: string | |
| default: v8 | |
| ios: | |
| description: "Build the selected engines for iOS (@nativescript/ios-<engine>)" | |
| required: false | |
| type: boolean | |
| default: true | |
| android: | |
| description: "Build the selected engines for Android (@nativescript/android-<engine>)" | |
| required: false | |
| type: boolean | |
| default: false | |
| build-ref: | |
| description: "Git ref to build from (defaults to the ref the workflow runs on). Use android-react-native for Android builds until it merges." | |
| required: false | |
| type: string | |
| 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 | |
| npm-tag: | |
| description: "Optional npm dist-tag override for publish, e.g. latest for the RN preview default install" | |
| required: false | |
| type: string | |
| 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.engines }}-${{ inputs.ios }}-${{ inputs.android }} | |
| cancel-in-progress: false | |
| env: | |
| XCODE_VERSION: "26.2.0" | |
| # V8 14.9's headers need std::atomic_ref (libc++ from LLVM 19), so NDK r29+. | |
| ANDROID_NDK_VERSION: "29.0.14206865" | |
| jobs: | |
| matrix: | |
| name: Resolve package matrix | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| apple_targets: ${{ steps.compute.outputs.apple_targets }} | |
| android_targets: ${{ steps.compute.outputs.android_targets }} | |
| publish_targets: ${{ steps.compute.outputs.publish_targets }} | |
| steps: | |
| - name: Compute matrix | |
| id: compute | |
| env: | |
| ENGINES: ${{ inputs.engines }} | |
| IOS: ${{ inputs.ios }} | |
| ANDROID: ${{ inputs.android }} | |
| run: | | |
| set -euo pipefail | |
| raw=$(printf '%s' "$ENGINES" | tr '[:upper:]' '[:lower:]' | tr ',' ' ') | |
| IOS_ENGINES="v8 hermes jsc quickjs" | |
| ANDROID_ENGINES="v8 hermes jsc quickjs primjs" | |
| apple=() | |
| android=() | |
| contains() { | |
| local x="$1"; shift | |
| local e | |
| for e in "$@"; do [ "$e" = "$x" ] && return 0; done | |
| return 1 | |
| } | |
| add_apple() { contains "$1" ${apple[@]+"${apple[@]}"} || apple+=("$1"); } | |
| add_android() { contains "$1" ${android[@]+"${android[@]}"} || android+=("$1"); } | |
| # The Android quickjs package keeps the upstream quickjs-ng name. | |
| android_pkg_for() { | |
| case "$1" in | |
| quickjs) echo quickjs-ng ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| for token in $raw; do | |
| [ -n "$token" ] || continue | |
| [ "$token" = "quickjs-ng" ] && token=quickjs | |
| case "$token" in | |
| all) | |
| if [ "$IOS" != "true" ] && [ "$ANDROID" != "true" ]; then | |
| echo "::error::'all' requires at least one platform (ios and/or android) to be checked." | |
| exit 1 | |
| fi | |
| if [ "$IOS" = "true" ]; then | |
| for e in $IOS_ENGINES; do add_apple "ios-$e"; done | |
| fi | |
| if [ "$ANDROID" = "true" ]; then | |
| for e in $ANDROID_ENGINES; do add_android "android-$(android_pkg_for "$e")"; done | |
| fi | |
| ;; | |
| react-native) | |
| add_apple react-native | |
| ;; | |
| v8|hermes|jsc|quickjs|primjs) | |
| matched=false | |
| if [ "$IOS" = "true" ] && [ "$token" != "primjs" ]; then | |
| add_apple "ios-$token" | |
| matched=true | |
| fi | |
| if [ "$ANDROID" = "true" ]; then | |
| add_android "android-$(android_pkg_for "$token")" | |
| matched=true | |
| fi | |
| if [ "$matched" != "true" ]; then | |
| if [ "$token" = "primjs" ] && [ "$IOS" = "true" ]; then | |
| echo "::error::primjs is Android-only; check the android box to release @nativescript/android-primjs." | |
| else | |
| echo "::error::Engine '$token' selected but no platform checked. Enable ios and/or android." | |
| fi | |
| exit 1 | |
| fi | |
| if [ "$token" = "primjs" ] && [ "$IOS" = "true" ]; then | |
| echo "::notice::primjs has no iOS package; releasing the Android package only." | |
| fi | |
| ;; | |
| *) | |
| echo "::error::Unknown engine '$token'. Expected v8, quickjs, jsc, hermes, primjs, react-native, or all." | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| if [ ${#apple[@]} -eq 0 ] && [ ${#android[@]} -eq 0 ]; then | |
| echo "::error::No release targets resolved from engines='$ENGINES' (ios=$IOS, android=$ANDROID)." | |
| exit 1 | |
| fi | |
| to_json() { | |
| if [ $# -eq 0 ]; then printf '[]'; return; fi | |
| local out="[" first=true e | |
| for e in "$@"; do | |
| $first || out+="," | |
| out+="\"$e\"" | |
| first=false | |
| done | |
| printf '%s]' "$out" | |
| } | |
| all_targets=(${apple[@]+"${apple[@]}"} ${android[@]+"${android[@]}"}) | |
| { | |
| echo "apple_targets=$(to_json ${apple[@]+"${apple[@]}"})" | |
| echo "android_targets=$(to_json ${android[@]+"${android[@]}"})" | |
| echo "publish_targets=$(to_json ${all_targets[@]+"${all_targets[@]}"})" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Apple targets: $(to_json ${apple[@]+"${apple[@]}"})" | |
| echo "Android targets: $(to_json ${android[@]+"${android[@]}"})" | |
| build-apple: | |
| name: Build ${{ matrix.target }} | |
| needs: matrix | |
| if: ${{ needs.matrix.outputs.apple_targets != '[]' }} | |
| runs-on: macos-26 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: ${{ fromJson(needs.matrix.outputs.apple_targets) }} | |
| 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: | |
| ref: ${{ inputs.build-ref || github.ref }} | |
| 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 &>/dev/null || brew install lld | |
| fi | |
| if ! command -v cmake >/dev/null; then | |
| brew list cmake &>/dev/null || 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 }} | |
| NPM_TAG_OVERRIDE: ${{ inputs.npm-tag }} | |
| TARGET: ${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| release_type="$RELEASE_TYPE" | |
| package_version="$PACKAGE_VERSION" | |
| preid="$PREID" | |
| npm_tag_override="$NPM_TAG_OVERRIDE" | |
| 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 | |
| engine="${target#ios-}" | |
| pkg_dir="packages/ios-${engine}" | |
| package_name="@nativescript/ios-${engine}" | |
| tarball_basename="nativescript-ios-${engine}" | |
| npm_tag_target="ios-${engine}" | |
| echo "IOS_VARIANT=ios-${engine}" >> "$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 "$npm_tag_override" ]; then | |
| case "$npm_tag_override" in | |
| *[[:space:]]*) | |
| echo "Invalid npm tag override '$npm_tag_override': dist-tags cannot contain whitespace." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| if printf '%s\n' "$npm_tag_override" | grep -Eq '^[0-9]+(\.[0-9]+)*$'; then | |
| echo "Invalid npm tag override '$npm_tag_override': dist-tags must not look like semver versions." >&2 | |
| exit 1 | |
| fi | |
| NPM_TAG="$npm_tag_override" | |
| fi | |
| if [ -n "$package_version" ] && [ "$release_type" = "prerelease" ] && [ -z "$npm_tag_override" ] && [ "$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" | |
| echo "NPM_TAG=$NPM_TAG" | |
| echo "PACKAGE_DIR=$pkg_dir" | |
| echo "PACKAGE_NAME=$package_name" | |
| echo "TARBALL_BASENAME=$tarball_basename" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG)" | |
| - name: Apply QuickJS patches | |
| # A build-ref that vendors QuickJS needs the NativeScript engine changes | |
| # from vendor/quickjs/patches (e.g. JS_WeakRef_Deref / JS_NewString16 | |
| # used by vendor/quickjs/quickjs-api.c) applied onto the pristine | |
| # submodule checkout before the iOS quickjs build. Refs without the | |
| # vendored engine have nothing to patch. | |
| if: ${{ matrix.target != 'react-native' }} | |
| run: | | |
| if [ -f scripts/apply_quickjs_patches.js ]; then | |
| npm run patch-quickjs | |
| else | |
| echo "No vendored QuickJS in this ref; nothing to patch." | |
| fi | |
| - name: Build iOS engine (${{ matrix.target }}) | |
| if: ${{ matrix.target != 'react-native' }} | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| run: ./scripts/build_all_ios.sh "--${TARGET#ios-}" | |
| # The package ships both halves. The Android half is generated -- both | |
| # android/tools (the build-time jars) and android/src/main/java-runtime | |
| # are gitignored -- so without these steps npm pack produces a tarball | |
| # with an Android directory that has no runtime and no tools in it. | |
| - name: Verify Android runtime sources | |
| if: ${{ matrix.target == 'react-native' }} | |
| run: | | |
| if [ ! -d platforms/android/test-app ]; then | |
| echo "::error::The checked-out ref has no Android runtime, so @nativescript/react-native would publish without its Android half. Set build-ref to a branch that contains it (e.g. android-react-native)." | |
| exit 1 | |
| fi | |
| - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 | |
| if: ${{ matrix.target == 'react-native' }} | |
| with: | |
| distribution: temurin | |
| java-version: 17 | |
| - name: Build @nativescript/react-native | |
| if: ${{ matrix.target == 'react-native' }} | |
| run: | | |
| ./scripts/build_all_react_native.sh | |
| # Staging only: copies the runtime C++ and com.tns.* Java into the | |
| # package. No SDK or NDK involved -- the consuming app compiles them. | |
| ./scripts/build_react_native_android.sh | |
| # Builds the metadata/dts/static-binding jars through the test-app | |
| # Gradle build, which configures Android modules and so needs the SDK | |
| # the runner image ships. | |
| ./scripts/build_react_native_android_tools.sh | |
| # Stages the iOS half and packs; run last, it is what produces the tarball. | |
| ./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 | |
| build-android: | |
| name: Build ${{ matrix.target }} | |
| needs: matrix | |
| if: ${{ needs.matrix.outputs.android_targets != '[]' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: ${{ fromJson(needs.matrix.outputs.android_targets) }} | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 | |
| with: | |
| egress-policy: audit | |
| - name: Free disk space | |
| run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL || true | |
| - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 | |
| with: | |
| ref: ${{ inputs.build-ref || github.ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| # The prebuilt engine binaries under platforms/android/test-app/**/libs | |
| # are stored in Git LFS; without this the build links pointer files. | |
| lfs: true | |
| - name: Verify Android runtime sources | |
| run: | | |
| if [ ! -f scripts/build_all_android.sh ] || [ ! -d platforms/android ]; then | |
| echo "::error::The checked-out ref has no Android runtime. Set build-ref to a branch that contains it (e.g. android-react-native)." | |
| exit 1 | |
| fi | |
| - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 | |
| with: | |
| distribution: temurin | |
| java-version: 17 | |
| - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | |
| with: | |
| node-version: 24 | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install Dependencies | |
| run: npm install | |
| - name: Install Android NDK | |
| # sdkmanager is not on PATH on the hosted ubuntu runners; call it from | |
| # ANDROID_HOME. Skip the install when the image already ships this NDK. | |
| run: | | |
| if [ ! -d "${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" ]; then | |
| echo y | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${ANDROID_NDK_VERSION}" >/dev/null | |
| fi | |
| - name: Prepare Android build | |
| run: ./scripts/setup_android.sh | |
| - name: Bump version | |
| id: bump | |
| shell: bash | |
| env: | |
| RELEASE_TYPE: ${{ inputs.release-type }} | |
| PACKAGE_VERSION: ${{ inputs.version }} | |
| PREID: ${{ inputs.preid }} | |
| NPM_TAG_OVERRIDE: ${{ inputs.npm-tag }} | |
| TARGET: ${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| release_type="$RELEASE_TYPE" | |
| package_version="$PACKAGE_VERSION" | |
| preid="$PREID" | |
| npm_tag_override="$NPM_TAG_OVERRIDE" | |
| pkg="${TARGET#android-}" | |
| pkg_dir="packages/android-${pkg}" | |
| package_name="@nativescript/android-${pkg}" | |
| tarball_basename="nativescript-android-${pkg}" | |
| npm_tag_target="android-${pkg}" | |
| case "$pkg" in | |
| v8) gradle_engine="V8-13" ;; | |
| quickjs-ng) gradle_engine="QUICKJS_NG" ;; | |
| hermes) gradle_engine="HERMES" ;; | |
| jsc) gradle_engine="JSC" ;; | |
| primjs) gradle_engine="PRIMJS" ;; | |
| *) | |
| echo "::error::Unknown Android engine package '$pkg'." | |
| exit 1 | |
| ;; | |
| esac | |
| if [ ! -f "$pkg_dir/package.json" ]; then | |
| echo "::error::Missing $pkg_dir/package.json on this ref." | |
| exit 1 | |
| 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 "$npm_tag_override" ]; then | |
| case "$npm_tag_override" in | |
| *[[:space:]]*) | |
| echo "Invalid npm tag override '$npm_tag_override': dist-tags cannot contain whitespace." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| if printf '%s\n' "$npm_tag_override" | grep -Eq '^[0-9]+(\.[0-9]+)*$'; then | |
| echo "Invalid npm tag override '$npm_tag_override': dist-tags must not look like semver versions." >&2 | |
| exit 1 | |
| fi | |
| NPM_TAG="$npm_tag_override" | |
| fi | |
| if [ -n "$package_version" ] && [ "$release_type" = "prerelease" ] && [ -z "$npm_tag_override" ] && [ "$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" | |
| echo "NPM_TAG=$NPM_TAG" | |
| echo "PACKAGE_DIR=$pkg_dir" | |
| echo "PACKAGE_NAME=$package_name" | |
| echo "TARBALL_BASENAME=$tarball_basename" | |
| echo "GRADLE_ENGINE=$gradle_engine" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG, gradle engine: $gradle_engine)" | |
| - name: Build Android runtime (${{ steps.bump.outputs.GRADLE_ENGINE }}) | |
| env: | |
| GIT_COMMIT: ${{ github.sha }} | |
| GRADLE_ENGINE: ${{ steps.bump.outputs.GRADLE_ENGINE }} | |
| run: ./scripts/build_all_android.sh "--engine=${GRADLE_ENGINE}" --binding=napi "-PndkVersion=${ANDROID_NDK_VERSION}" | |
| - name: Pack npm package | |
| shell: bash | |
| env: | |
| GRADLE_ENGINE: ${{ steps.bump.outputs.GRADLE_ENGINE }} | |
| PACKAGE_DIR: ${{ steps.bump.outputs.PACKAGE_DIR }} | |
| PACKAGE_NAME: ${{ steps.bump.outputs.PACKAGE_NAME }} | |
| NPM_VERSION: ${{ steps.bump.outputs.NPM_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| # Mirrors the slug the gradle build derives (see platforms/android/build.gradle). | |
| slug=$(printf '%s' "$GRADLE_ENGINE" | tr '[:upper:]-' '[:lower:]_') | |
| [ "$slug" = "quickjs_ng" ] && slug=quickjs | |
| dist_dir="dist/android_${slug}_napi" | |
| if [ ! -f "$dist_dir/package.json" ]; then | |
| echo "::error::Expected staged package at $dist_dir; the gradle build did not produce it." | |
| exit 1 | |
| fi | |
| # The gradle build stages the generic @nativescript/android package; | |
| # stamp the per-engine name and the bumped version before packing. | |
| node -e ' | |
| const fs = require("fs"); | |
| const [, path, name, version] = process.argv; | |
| const pkg = JSON.parse(fs.readFileSync(path, "utf8")); | |
| pkg.name = name; | |
| pkg.version = version; | |
| fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); | |
| ' "$dist_dir/package.json" "$PACKAGE_NAME" "$NPM_VERSION" | |
| mkdir -p "$PACKAGE_DIR/dist" | |
| (cd "$dist_dir" && npm pack --pack-destination "$GITHUB_WORKSPACE/$PACKAGE_DIR/dist") | |
| - 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" | |
| if [ ! -f "$package_dir/dist/$tarball_file" ]; then | |
| echo "::error::Expected tarball $package_dir/dist/$tarball_file was not produced." | |
| exit 1 | |
| fi | |
| 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 | |
| publish: | |
| name: Publish ${{ matrix.target }} | |
| needs: | |
| - matrix | |
| - build-apple | |
| - build-android | |
| # One of the build jobs is skipped when its platform is unchecked; publish | |
| # as long as neither failed and at least one produced packages. | |
| if: >- | |
| ${{ !cancelled() && | |
| needs.matrix.result == 'success' && | |
| needs.build-apple.result != 'failure' && needs.build-apple.result != 'cancelled' && | |
| needs.build-android.result != 'failure' && needs.build-android.result != 'cancelled' && | |
| (needs.build-apple.result == 'success' || needs.build-android.result == 'success') }} | |
| 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.publish_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" | |
| echo "NPM_TAG=$NPM_TAG" | |
| echo "PACKAGE_NAME=$PACKAGE_NAME" | |
| 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-apple | |
| - build-android | |
| - publish | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Print summary | |
| env: | |
| ENGINES: ${{ inputs.engines }} | |
| IOS: ${{ inputs.ios }} | |
| ANDROID: ${{ inputs.android }} | |
| BUILD_REF: ${{ inputs.build-ref }} | |
| RELEASE_TYPE: ${{ inputs.release-type }} | |
| PACKAGE_VERSION: ${{ inputs.version }} | |
| PREID: ${{ inputs.preid }} | |
| NPM_TAG_OVERRIDE: ${{ inputs.npm-tag }} | |
| DRY_RUN: ${{ inputs.dry-run }} | |
| APPLE_TARGETS: ${{ needs.matrix.outputs.apple_targets }} | |
| ANDROID_TARGETS: ${{ needs.matrix.outputs.android_targets }} | |
| BUILD_APPLE_RESULT: ${{ needs.build-apple.result }} | |
| BUILD_ANDROID_RESULT: ${{ needs.build-android.result }} | |
| PUBLISH_RESULT: ${{ needs.publish.result }} | |
| run: | | |
| echo "Engines: $ENGINES" | |
| echo "Platforms: ios=$IOS android=$ANDROID" | |
| echo "Build ref: ${BUILD_REF:-$GITHUB_REF}" | |
| echo "Release type: $RELEASE_TYPE" | |
| echo "Exact version: $PACKAGE_VERSION" | |
| echo "Preid: $PREID" | |
| echo "NPM tag override: $NPM_TAG_OVERRIDE" | |
| echo "Dry run: $DRY_RUN" | |
| echo "Apple targets: $APPLE_TARGETS" | |
| echo "Android targets: $ANDROID_TARGETS" | |
| echo "Apple build result: $BUILD_APPLE_RESULT" | |
| echo "Android build result: $BUILD_ANDROID_RESULT" | |
| echo "Publish result: $PUBLISH_RESULT" |