Merge pull request #182 from Resgrid/develop #333
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: React Native CI/CD | |
| on: | |
| push: | |
| branches: [main, master] | |
| paths-ignore: | |
| - '**.md' | |
| - 'LICENSE' | |
| - 'docs/**' | |
| pull_request: | |
| branches: [main, master] | |
| workflow_dispatch: | |
| inputs: | |
| buildType: | |
| type: choice | |
| description: 'Build type to run' | |
| options: | |
| - dev | |
| - prod-apk | |
| - prod-aab | |
| - ios-dev | |
| - ios-adhoc | |
| - ios-prod | |
| - all | |
| platform: | |
| type: choice | |
| description: 'Platform to build' | |
| default: 'all' | |
| options: | |
| - android | |
| - ios | |
| - all | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }} | |
| EXPO_APPLE_PASSWORD: ${{ secrets.EXPO_APPLE_PASSWORD }} | |
| EXPO_TEAM_ID: ${{ secrets.EXPO_TEAM_ID }} | |
| CREDENTIALS_JSON_BASE64: ${{ secrets.CREDENTIALS_JSON_BASE64 }} | |
| POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| UNIT_BASE_API_URL: ${{ secrets.UNIT_BASE_API_URL }} | |
| UNIT_CHANNEL_API_URL: ${{ secrets.UNIT_CHANNEL_API_URL }} | |
| UNIT_LOGGING_KEY: ${{ secrets.UNIT_LOGGING_KEY }} | |
| UNIT_MAPBOX_DLKEY: ${{ secrets.UNIT_MAPBOX_DLKEY }} | |
| UNIT_MAPBOX_PUBKEY: ${{ secrets.UNIT_MAPBOX_PUBKEY }} | |
| UNIT_SENTRY_DSN: ${{ secrets.UNIT_SENTRY_DSN }} | |
| UNIT_ANDROID_KS: ${{ secrets.UNIT_ANDROID_KS }} | |
| UNIT_GOOGLE_SERVICES: ${{ secrets.UNIT_GOOGLE_SERVICES }} | |
| UNIT_IOS_GOOGLE_SERVICES: ${{ secrets.UNIT_IOS_GOOGLE_SERVICES }} | |
| MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| APPLE_APIKEY: ${{ secrets.APPLE_APIKEY }} | |
| MATCH_UNIT_BUNDLEID: ${{ secrets.MATCH_UNIT_BUNDLEID }} | |
| MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} | |
| MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} | |
| EXPO_ACCOUNT_OWNER: ${{ secrets.EXPO_ACCOUNT_OWNER }} | |
| BUNDLE_ID: ${{ secrets.MATCH_UNIT_BUNDLEID }} | |
| EAS_PROJECT_ID: ${{ secrets.EAS_PROJECT_ID }} | |
| SCHEME: ${{ secrets.SCHEME }} | |
| UNIT_IOS_CERT: ${{ secrets.UNIT_IOS_CERT }} | |
| EXPO_ASC_API_KEY_PATH: ${{ secrets.EXPO_ASC_API_KEY_PATH }} | |
| EXPO_ASC_KEY_ID: ${{ secrets.EXPO_ASC_KEY_ID }} | |
| EXPO_ASC_ISSUER_ID: ${{ secrets.EXPO_ASC_ISSUER_ID }} | |
| EXPO_APPLE_TEAM_ID: ${{ secrets.EXPO_TEAM_ID }} | |
| EXPO_APPLE_TEAM_TYPE: ${{ secrets.EXPO_APPLE_TEAM_TYPE }} | |
| UNIT_APTABASE_APP_KEY: ${{ secrets.UNIT_APTABASE_APP_KEY }} | |
| UNIT_APTABASE_URL: ${{ secrets.UNIT_APTABASE_URL }} | |
| UNIT_COUNTLY_APP_KEY: ${{ secrets.UNIT_COUNTLY_APP_KEY }} | |
| UNIT_COUNTLY_SERVER_URL: ${{ secrets.UNIT_COUNTLY_SERVER_URL }} | |
| UNIT_APP_KEY: ${{ secrets.UNIT_APP_KEY }} | |
| APP_KEY: ${{ secrets.APP_KEY }} | |
| NODE_OPTIONS: --openssl-legacy-provider | |
| CHANGERAWR_API_KEY: ${{ secrets.CHANGERAWR_API_KEY }} | |
| CHANGERAWR_API_URL: ${{ secrets.CHANGERAWR_API_URL }} | |
| jobs: | |
| check-skip: | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} | |
| steps: | |
| - name: Skip CI check | |
| run: echo "Proceeding with workflow" | |
| test: | |
| needs: check-skip | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: 🏗 Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: 🏗 Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| cache: 'yarn' | |
| - name: 📦 Setup yarn cache | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| ~/.cache/yarn | |
| node_modules | |
| key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-yarn- | |
| - name: 📦 Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: 🧪 Run Checks and Tests | |
| run: yarn check-all | |
| build-and-deploy: | |
| needs: test | |
| if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| matrix: | |
| platform: [android, ios] | |
| runs-on: ${{ matrix.platform == 'ios' && 'macos-15' || 'ubuntu-latest' }} | |
| environment: RNBuild | |
| steps: | |
| - name: 🏗 Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: 🏗 Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| cache: 'yarn' | |
| - name: Setup Expo | |
| uses: expo/expo-github-action@v8 | |
| with: | |
| expo-version: latest | |
| eas-version: latest | |
| token: ${{ secrets.EXPO_TOKEN }} | |
| - name: 📦 Setup yarn cache | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| ~/.cache/yarn | |
| node_modules | |
| key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-yarn- | |
| - name: 📦 Install dependencies | |
| run: | | |
| yarn install --frozen-lockfile | |
| - name: 📋 Create Google Json File | |
| if: ${{ matrix.platform == 'android' }} | |
| run: | | |
| echo $UNIT_GOOGLE_SERVICES | base64 -d > google-services.json | |
| - name: 📋 Create Google Json File for iOS | |
| if: ${{ matrix.platform == 'ios' }} | |
| run: | | |
| echo $UNIT_IOS_GOOGLE_SERVICES | base64 -d > GoogleService-Info.plist | |
| - name: 📋 Update package.json Versions | |
| run: | | |
| # Ensure jq exists on both Linux and macOS | |
| if ! command -v jq >/dev/null 2>&1; then | |
| echo "Installing jq..." | |
| if [[ "$RUNNER_OS" == "Linux" ]]; then | |
| sudo apt-get update && sudo apt-get install -y jq | |
| elif [[ "$RUNNER_OS" == "macOS" ]]; then | |
| brew update && brew install jq | |
| else | |
| echo "Unsupported OS for auto-install of jq" >&2; exit 1 | |
| fi | |
| fi | |
| # Fix the main entry in package.json | |
| if [ -f ./package.json ]; then | |
| # Create a backup | |
| cp package.json package.json.bak | |
| # Update the package.json | |
| jq '.version = "7.${{ github.run_number }}"' package.json > package.json.tmp && mv package.json.tmp package.json | |
| jq '.versionCode = "7${{ github.run_number }}"' package.json > package.json.tmp && mv package.json.tmp package.json | |
| echo "Updated package.json versions" | |
| cat package.json | grep "version" | |
| cat package.json | grep "versionCode" | |
| else | |
| echo "package.json not found" | |
| exit 1 | |
| fi | |
| - name: 📱 Setup EAS build cache | |
| uses: actions/cache@v3 | |
| with: | |
| path: ~/.eas-build-local | |
| key: ${{ runner.os }}-eas-build-local-${{ hashFiles('**/package.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-eas-build-local- | |
| - name: 🔄 Verify EAS CLI installation | |
| run: | | |
| echo "EAS CLI version:" | |
| eas --version | |
| - name: 📋 Create iOS Cert | |
| run: | | |
| echo $UNIT_IOS_CERT | base64 -d > AuthKey_HRBP5FNJN6.p8 | |
| - name: 📋 Restore gradle.properties | |
| env: | |
| GRADLE_PROPERTIES: ${{ secrets.GRADLE_PROPERTIES }} | |
| shell: bash | |
| run: | | |
| mkdir -p ~/.gradle/ | |
| echo ${GRADLE_PROPERTIES} > ~/.gradle/gradle.properties | |
| - name: 📱 Build Development APK | |
| if: (matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'dev')) | |
| run: | | |
| # Build with increased memory limit | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform android --profile development --local --non-interactive --output=./ResgridUnit-dev.apk | |
| env: | |
| NODE_ENV: development | |
| - name: 📱 Build Production APK | |
| if: (matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'prod-apk')) | |
| run: | | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform android --profile production-apk --local --non-interactive --output=./ResgridUnit-prod.apk | |
| env: | |
| NODE_ENV: production | |
| - name: 📱 Build Production AAB | |
| if: (matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'prod-aab')) | |
| run: | | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform android --profile production --local --non-interactive --output=./ResgridUnit-prod.aab | |
| env: | |
| NODE_ENV: production | |
| - name: 📱 Build iOS Development | |
| if: (matrix.platform == 'ios' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'ios-dev')) | |
| run: | | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform ios --profile development --local --non-interactive --output=./ResgridUnit-ios-dev.ipa | |
| env: | |
| NODE_ENV: development | |
| - name: 📱 Build iOS Ad-Hoc | |
| if: (matrix.platform == 'ios' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'ios-adhoc')) | |
| run: | | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform ios --profile internal --local --non-interactive --output=./ResgridUnit-ios-adhoc.ipa | |
| env: | |
| NODE_ENV: production | |
| - name: 📱 Build iOS Production | |
| if: (matrix.platform == 'ios' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'ios-prod')) | |
| run: | | |
| export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096" | |
| eas build --platform ios --profile production --local --non-interactive --output=./ResgridUnit-ios-prod.ipa | |
| env: | |
| NODE_ENV: production | |
| - name: 📦 Upload build artifacts to GitHub | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: app-builds-${{ matrix.platform }} | |
| path: | | |
| ./ResgridUnit-dev.apk | |
| ./ResgridUnit-prod.apk | |
| ./ResgridUnit-prod.aab | |
| ./ResgridUnit-ios-dev.ipa | |
| ./ResgridUnit-ios-adhoc.ipa | |
| ./ResgridUnit-ios-prod.ipa | |
| retention-days: 7 | |
| - name: 📦 Setup Firebase CLI | |
| uses: w9jds/setup-firebase@main | |
| with: | |
| tools-version: 11.9.0 | |
| firebase_token: ${{ secrets.FIREBASE_TOKEN }} | |
| - name: 📦 Upload Android artifact to Firebase App Distribution | |
| if: (matrix.platform == 'android') | |
| run: | | |
| firebase appdistribution:distribute ./ResgridUnit-prod.apk --app ${{ secrets.FIREBASE_ANDROID_APP_ID }} --groups "testers" | |
| - name: 📦 Upload iOS artifact to Firebase App Distribution | |
| if: (matrix.platform == 'ios') | |
| run: | | |
| firebase appdistribution:distribute ./ResgridUnit-ios-adhoc.ipa --app ${{ secrets.FIREBASE_IOS_APP_ID }} --groups "testers" | |
| - name: 📋 Prepare Release Notes file | |
| if: ${{ matrix.platform == 'android' }} | |
| run: | | |
| set -eo pipefail | |
| # Function to extract release notes from PR body | |
| extract_release_notes() { | |
| local body="$1" | |
| # Remove "Summary by CodeRabbit" section and auto-generated comment line | |
| local cleaned_body="$(printf '%s\n' "$body" \ | |
| | grep -v '<!-- end of auto-generated comment: release notes by coderabbit.ai -->' \ | |
| | awk ' | |
| BEGIN { skip=0 } | |
| /^## Summary by CodeRabbit/ { skip=1; next } | |
| /^## / && skip==1 { skip=0 } | |
| skip==0 { print } | |
| ')" | |
| # Try to extract content under "## Release Notes" heading | |
| local notes="$(printf '%s\n' "$cleaned_body" \ | |
| | awk 'f && /^## /{exit} /^## Release Notes/{f=1; next} f')" | |
| # If no specific section found, use the entire cleaned body (up to first 500 chars for safety) | |
| if [ -z "$notes" ]; then | |
| notes="$(printf '%s\n' "$cleaned_body" | head -c 500)" | |
| fi | |
| printf '%s\n' "$notes" | |
| } | |
| # Determine source of release notes | |
| NOTES="" | |
| # Check if this was triggered by a push event (likely a merge) | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| echo "Fetching PR body for merged commit..." | |
| # First, try to find PR number from commit message (most reliable) | |
| PR_FROM_COMMIT=$(git log -1 --pretty=%B | grep -oE '#[0-9]+' | head -1 | tr -d '#' || echo "") | |
| if [ -n "$PR_FROM_COMMIT" ]; then | |
| echo "Found PR #$PR_FROM_COMMIT from commit message" | |
| PR_BODY=$(gh pr view "$PR_FROM_COMMIT" --json body --jq '.body' 2>/dev/null || echo "") | |
| if [ -n "$PR_BODY" ]; then | |
| NOTES="$(extract_release_notes "$PR_BODY")" | |
| fi | |
| else | |
| echo "No PR reference in commit message, searching by commit SHA..." | |
| # Get PRs that contain this commit (using GitHub API to search by commit) | |
| PR_NUMBERS=$(gh api \ | |
| "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \ | |
| --jq '.[].number' 2>/dev/null || echo "") | |
| if [ -n "$PR_NUMBERS" ]; then | |
| # Take the first PR found (most recently merged) | |
| PR_NUMBER=$(echo "$PR_NUMBERS" | head -n 1) | |
| echo "Found PR #$PR_NUMBER associated with commit" | |
| # Fetch the PR body | |
| PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null || echo "") | |
| if [ -n "$PR_BODY" ]; then | |
| NOTES="$(extract_release_notes "$PR_BODY")" | |
| fi | |
| else | |
| echo "No associated PR found for this commit" | |
| fi | |
| fi | |
| fi | |
| # Fallback to recent commits if no PR body found | |
| if [ -z "$NOTES" ]; then | |
| echo "No PR body found, using recent commits..." | |
| NOTES="$(git log -n 5 --pretty=format:'- %s')" | |
| fi | |
| # Fail if no notes extracted | |
| if [ -z "$NOTES" ]; then | |
| echo "Error: No release notes extracted" >&2 | |
| exit 1 | |
| fi | |
| # Write header and notes to file | |
| { | |
| echo "## Version 7.${{ github.run_number }} - $(date +%Y-%m-%d)" | |
| echo | |
| printf '%s\n' "$NOTES" | |
| } > RELEASE_NOTES.md | |
| echo "Release notes prepared:" | |
| cat RELEASE_NOTES.md | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: 📝 Send Release Notes to Changerawr | |
| if: ${{ matrix.platform == 'android' }} | |
| run: | | |
| set -eo pipefail | |
| # Check if required secrets are set | |
| if [ -z "$CHANGERAWR_API_URL" ] || [ -z "$CHANGERAWR_API_KEY" ]; then | |
| echo "⚠️ Changerawr API credentials not configured, skipping release notes submission" | |
| exit 0 | |
| fi | |
| # Read release notes | |
| RELEASE_NOTES=$(cat RELEASE_NOTES.md) | |
| VERSION="7.${{ github.run_number }}" | |
| # Prepare JSON payload | |
| PAYLOAD=$(jq -n \ | |
| --arg version "$VERSION" \ | |
| --arg notes "$RELEASE_NOTES" \ | |
| --arg buildNumber "${{ github.run_number }}" \ | |
| --arg commitSha "${{ github.sha }}" \ | |
| --arg buildUrl "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| '{ | |
| "version": $version, | |
| "title": ("Release v" + $version), | |
| "content": "$notes", | |
| }') | |
| echo "Sending release notes to Changerawr..." | |
| # Send to Changerawr API | |
| RESPONSE=$(curl -X POST "$CHANGERAWR_API_URL" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer $CHANGERAWR_API_KEY" \ | |
| -d "$PAYLOAD" \ | |
| -w "\n%{http_code}" \ | |
| -s) | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -n1) | |
| RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d') | |
| if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then | |
| echo "✅ Successfully sent release notes to Changerawr (HTTP $HTTP_CODE)" | |
| echo "Response: $RESPONSE_BODY" | |
| else | |
| echo "⚠️ Failed to send release notes to Changerawr (HTTP $HTTP_CODE)" | |
| echo "Response: $RESPONSE_BODY" | |
| # Don't fail the build, just warn | |
| fi | |
| - name: �📦 Create Release | |
| if: ${{ matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'prod-apk') }} | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: '7.${{ github.run_number }}' | |
| commit: ${{ github.sha }} | |
| makeLatest: true | |
| allowUpdates: true | |
| name: '7.${{ github.run_number }}' | |
| artifacts: './ResgridUnit-prod.apk' | |
| bodyFile: 'RELEASE_NOTES.md' |