diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 063bffb..0f31c05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,10 @@ on: jobs: library: name: Lint · Typecheck · Build · Codegen - runs-on: ubuntu-latest + # Public repo: fork pull_request runs never reach the self-hosted Macs (fork-PR approval is on too). + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + # Every job runs on the org's self-hosted Mac minis (no GitHub-hosted minutes). + runs-on: [self-hosted, macOS] steps: - uses: actions/checkout@v4 @@ -34,7 +37,9 @@ jobs: example-android: name: Example · JS + Android build - runs-on: ubuntu-latest + # Public repo: fork pull_request runs never reach the self-hosted Macs (fork-PR approval is on too). + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + runs-on: [self-hosted, macOS] steps: - uses: actions/checkout@v4 @@ -48,8 +53,17 @@ jobs: distribution: temurin java-version: 17 + # No Actions cache on the self-hosted Macs: ~/.gradle persists there and Gradle locks it for + # concurrent builds, while restoring a cache over it from several jobs at once would not be safe. - name: Set up Gradle cache uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + + # The SDK packages example/android pins (compileSdk 35, build-tools 35.0.0, NDK 27.1, and the + # CMake RN's new-architecture build uses), reused when present. The hosted image had them. + - name: Android SDK + run: bash scripts/ci/android-sdk.sh platform-tools "platforms;android-35" "build-tools;35.0.0" "ndk;27.1.12297006" "cmake;3.22.1" - name: Install library dependencies run: npm ci diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 719b861..883fc60 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -8,7 +8,10 @@ on: jobs: ios: name: Build example (iOS simulator) - runs-on: macos-15 + # Public repo: fork pull_request runs never reach the self-hosted Macs (fork-PR approval is on too). + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + # Every job runs on the org's self-hosted Mac minis (no GitHub-hosted minutes). + runs-on: [self-hosted, macOS] steps: - uses: actions/checkout@v4 @@ -17,12 +20,18 @@ jobs: node-version: 20 cache: npm + # ruby/setup-ruby's prebuilt Rubies only install on GitHub-hosted images. scripts/ci/ruby.sh uses + # Homebrew's Ruby with every gem in RUNNER_TEMP (nothing global); bundler then installs the + # Gemfile's CocoaPods there. - name: Set up Ruby + CocoaPods (bundler) - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - working-directory: example + run: bash scripts/ci/ruby.sh + + - name: Install CocoaPods (bundler) + working-directory: example + env: + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + run: bundle install --jobs 4 --retry 3 - name: Show Xcode version run: xcodebuild -version @@ -57,6 +66,7 @@ jobs: -configuration Debug \ -sdk iphonesimulator \ -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$RUNNER_TEMP/DerivedData" \ CODE_SIGNING_ALLOWED=NO \ -quiet \ build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 183ff21..22bd304 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,9 @@ permissions: jobs: publish: name: Build & publish + # The one job in the org still on a GitHub-hosted runner, on purpose: `npm publish --provenance` + # only works on GitHub-hosted runners (the npm registry rejects provenance built on a self-hosted one). + # Moving it means publishing without provenance; that is a product decision, not a CI change. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/scripts/ci/android-sdk.sh b/scripts/ci/android-sdk.sh new file mode 100755 index 0000000..ece3670 --- /dev/null +++ b/scripts/ci/android-sdk.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Make the Android SDK packages a CI job needs available on a shared self-hosted Mac. +# +# bash scripts/ci/android-sdk.sh ... e.g. platform-tools "platforms;android-34" +# +# SDK: $ANDROID_HOME, else $ANDROID_SDK_ROOT, else ~/Library/Android/sdk (where React Native and +# Android Studio put it). Packages already there are reused; missing ones are installed with a pinned +# cmdline-tools. The SDK is a versioned store (every package lives under its own versioned path), so +# sharing it between jobs is like sharing a tool cache: each job still names the exact versions it +# builds with. Installs run under a machine-wide lock, so two jobs never run sdkmanager over the +# same SDK at once. Nothing outside the SDK directory is touched. +# +# In GitHub Actions it exports ANDROID_HOME / ANDROID_SDK_ROOT and puts platform-tools on PATH. +set -euo pipefail + +CMDLINE_TOOLS_ZIP="commandlinetools-mac-11076708_latest.zip" # cmdline-tools 12.0, same as the Android SDK repo +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}}" +LOCK="${ANDROID_SDK_LOCK_FILE:-$HOME/.linktrail-android-sdk.lock}" + +LOCKED=0 +[[ "${1:-}" == --locked ]] && { LOCKED=1; shift; } +[[ $# -gt 0 ]] || { echo "usage: $0 ..." >&2; exit 2; } + +missing() { + local pkg out=() + for pkg in "$@"; do + # "platforms;android-34" lives in $SDK/platforms/android-34; every package has a package.xml. + [[ -f "$SDK/${pkg//;//}/package.xml" ]] || out+=("$pkg") + done + printf '%s\n' ${out[@]+"${out[@]}"} +} + +install_missing() { + local todo=() + while IFS= read -r pkg; do [[ -n "$pkg" ]] && todo+=("$pkg"); done < <(missing "$@") + if [[ ${#todo[@]} -eq 0 ]]; then + echo "==> Android SDK $SDK: all packages present" + return 0 + fi + local sdkmanager="$SDK/cmdline-tools/latest/bin/sdkmanager" + if [[ ! -x "$sdkmanager" ]]; then + echo "==> installing the command-line tools into $SDK" + local tmp + tmp="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/android-cmdline.XXXXXX")" + curl -fsSL --retry 3 -o "$tmp/tools.zip" "https://dl.google.com/android/repository/$CMDLINE_TOOLS_ZIP" + unzip -q "$tmp/tools.zip" -d "$tmp" + mkdir -p "$SDK/cmdline-tools" + rm -rf "$SDK/cmdline-tools/latest" + mv "$tmp/cmdline-tools" "$SDK/cmdline-tools/latest" + rm -rf "$tmp" + fi + yes | "$sdkmanager" --sdk_root="$SDK" --licenses >/dev/null 2>&1 || true + echo "==> installing into $SDK: ${todo[*]}" + "$sdkmanager" --sdk_root="$SDK" --install "${todo[@]}" +} + +# Re-entered with the lock held: install, then return to the caller, which does the exports. +if [[ $LOCKED -eq 1 ]]; then + install_missing "$@" + exit 0 +fi + +mkdir -p "$SDK" +if [[ -n "$(missing "$@")" ]]; then + # macOS has no flock(1): perl takes the lock and keeps the fd open across exec into this script, + # so it is released the moment the install finishes or dies. + echo "==> waiting for the Android SDK lock ($LOCK)" + ANDROID_HOME="$SDK" perl -MFcntl=:flock -e ' + $^F = 255; # keep the lock fd open across exec + open(my $fh, ">>", shift) or die "open lock: $!"; + flock($fh, LOCK_EX) or die "flock: $!"; + exec @ARGV or die "exec: $!"; + ' "$LOCK" bash "$0" --locked "$@" +fi + +[[ -n "${GITHUB_ENV:-}" ]] && { echo "ANDROID_HOME=$SDK"; echo "ANDROID_SDK_ROOT=$SDK"; } >> "$GITHUB_ENV" +[[ -n "${GITHUB_PATH:-}" ]] && echo "$SDK/platform-tools" >> "$GITHUB_PATH" +echo "==> Android SDK: $SDK" diff --git a/scripts/ci/ruby.sh b/scripts/ci/ruby.sh new file mode 100755 index 0000000..789c33e --- /dev/null +++ b/scripts/ci/ruby.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# A Ruby + bundler for this CI job only, on a shared self-hosted Mac. Replaces ruby/setup-ruby, whose +# prebuilt Rubies only install on GitHub-hosted images. +# +# bash scripts/ci/ruby.sh then: bundle install / bundle exec pod … (in example/) +# +# Gems go to $RUNNER_TEMP (GEM_HOME, BUNDLE_PATH, BUNDLE_APP_CONFIG), so nothing is installed globally +# and nothing is left on the Mac. The system Ruby (2.6) is too old for CocoaPods' dependencies, so this +# uses Homebrew's Ruby: the `ruby` formula when installed, else the portable Ruby Homebrew ships for +# itself. Exports to the later steps through $GITHUB_ENV and $GITHUB_PATH. +set -euo pipefail +: "${RUNNER_TEMP:?RUNNER_TEMP is not set}" + +BREW="$(command -v brew || true)" +for c in /opt/homebrew/bin/brew /usr/local/bin/brew; do + [ -n "$BREW" ] && break + [ -x "$c" ] && BREW="$c" +done +[ -n "$BREW" ] || { echo "::error::Homebrew not found on this runner; it provides the Ruby for CocoaPods."; exit 1; } + +RUBY_BIN="" +for d in "$("$BREW" --prefix ruby 2>/dev/null || true)/bin" "$("$BREW" --repository)/Library/Homebrew/vendor/portable-ruby/current/bin"; do + if [ -x "$d/ruby" ] && "$d/ruby" -e 'exit(RUBY_VERSION >= "3.1" ? 0 : 1)'; then RUBY_BIN="$d"; break; fi +done +[ -n "$RUBY_BIN" ] || { echo "::error::No Ruby >= 3.1 from Homebrew on this runner (brew install ruby, or run any brew command once to fetch its portable Ruby)."; exit 1; } + +ROOT="$RUNNER_TEMP/ruby" +rm -rf "$ROOT"; mkdir -p "$ROOT/gems/bin" +if [ -n "${GITHUB_ENV:-}" ]; then + { + echo "GEM_HOME=$ROOT/gems" + echo "GEM_PATH=$ROOT/gems" + echo "BUNDLE_PATH=$ROOT/bundle" + echo "BUNDLE_APP_CONFIG=$ROOT/bundle-config" + echo "CP_HOME_DIR=$ROOT/cocoapods-home" + } >> "$GITHUB_ENV" +fi +if [ -n "${GITHUB_PATH:-}" ]; then + { echo "$ROOT/gems/bin"; echo "$RUBY_BIN"; } >> "$GITHUB_PATH" +fi +echo "Ruby $("$RUBY_BIN/ruby" -e 'print RUBY_VERSION') ($RUBY_BIN), gems in $ROOT"