diff --git a/.github/workflows/preview-deployment.yml b/.github/workflows/preview-deployment.yml index 2de7121f..dda81c0a 100644 --- a/.github/workflows/preview-deployment.yml +++ b/.github/workflows/preview-deployment.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: force_native_build: - description: "Force native build" + description: "Force native build (will ignore check-changes logic if re-enabled)" type: boolean default: true required: true @@ -18,6 +18,7 @@ on: - "assets/**" - "package.json" - "app.config.ts" + - "versions.json" - "yarn.lock" - "i18n/**" - "patches/**" @@ -26,12 +27,13 @@ on: # Ensure only one workflow runs at a time concurrency: - group: "preview-deployment" - cancel-in-progress: false + group: "preview-deployment-${{ github.ref }}" + cancel-in-progress: true +# Permissions might be needed if you decide to commit preview build numbers later, +# but for now, not committing them back. permissions: contents: write - id-token: write jobs: # check-changes: @@ -68,16 +70,19 @@ jobs: # echo "has_native_changes=false" >> $GITHUB_OUTPUT # fi - ios-build: + ios-preview-build: # needs: [check-changes] # if: needs.check-changes.outputs.has_native_changes == 'true' runs-on: ubuntu-latest + outputs: + ios_preview_build_number: ${{ steps.update_versions_json_preview.outputs.new_build_number }} steps: - name: Checkout repository uses: actions/checkout@v4 with: - ref: main + ref: ${{ github.ref }} + token: ${{ secrets.PAT_GITHUB }} fetch-depth: 0 - name: Setup node @@ -91,7 +96,42 @@ jobs: - run: corepack enable - name: Install dependencies - run: yarn install + run: yarn install --immutable + + - name: Configure Git + run: | + git config --global user.name "GitHub Actions Bot (Preview Build)" + git config --global user.email "github-actions-bot-preview@users.noreply.github.com" + + - name: Update iOS Preview Build Number in versions.json + id: update_versions_json_preview + run: | + node scripts/update-versions.js --platform=ios --env=preview + # The script outputs 'new_build_number' + + - name: Commit and Push versions.json + run: | + if ! git diff --quiet versions.json; then + echo "versions.json was updated. Committing and pushing..." + git add versions.json + COMMIT_MSG="chore: Update iOS preview build number to ${{ steps.update_versions_json_preview.outputs.new_build_number }}" + git commit -m "$COMMIT_MSG" + git push origin ${{ github.ref_name }} + else + echo "versions.json was not changed. No commit needed." + fi + + - name: Update app.config.ts with New iOS Preview Build Number (local to runner) + id: update_app_config_preview + run: | + NEW_BUILD_NUM_PREVIEW=${{ steps.update_versions_json_preview.outputs.new_build_number }} + if [ -z "$NEW_BUILD_NUM_PREVIEW" ]; then + echo "Error: Failed to get new_build_number from update_versions_json_preview step." + exit 1 + fi + echo "New iOS Preview build number for this build is: $NEW_BUILD_NUM_PREVIEW" + node scripts/update-app-config-buildnum.js --platform=ios --env=preview --buildNumber=$NEW_BUILD_NUM_PREVIEW + echo "app.config.ts updated locally for preview build." # Need this here because the "Setup EAS" setup will execute npx expo config and will need the "build" folder of the plugin to be there - name: Build iOS notification extension plugin @@ -107,10 +147,12 @@ jobs: eas-cache: true patch-watchers: true # Prevents ENOSPC errors on Ubuntu runners - - name: Build and submit to store + - name: Build and submit Preview to store run: | - # Use the --non-interactive flag to run in CI mode - # Use the --auto-submit flag to submit to App Store + # EAS will now use the app.config.ts that has been locally modified + # with the incremented preview build number. + # EXPO_ENV=preview should be set for eas build if your app.config.ts relies on it + # to pick the right settings block. EAS CLI often does this automatically based on profile. eas build --platform ios --profile preview --non-interactive --auto-submit # - name: Resolve Sentry Issues diff --git a/.github/workflows/production-deployment.yml b/.github/workflows/production-deployment.yml index ae7ac2f6..9c0f7bc4 100644 --- a/.github/workflows/production-deployment.yml +++ b/.github/workflows/production-deployment.yml @@ -1,51 +1,54 @@ +# .github/workflows/production-deployment.yml name: Production Deployment on: push: - branches: - - production # Triggers on push to the production branch + tags: # Trigger on new tags that match the pattern + - "v[0-9]+.[0-9]+.[0-9]+-build.[0-9]+" # e.g. v1.0.1-build.123 -# Ensure only one deployment workflow runs at a time for production concurrency: - group: "production-deployment" - cancel-in-progress: true # It's okay to cancel in-progress deployments if a newer commit comes in + group: "production-deployment-${{ github.ref }}" + cancel-in-progress: true permissions: - id-token: write # For EAS Build - contents: read # For checkout + id-token: write + contents: write # For GitHub Release creation jobs: - build-and-deploy: + build_and_deploy: runs-on: ubuntu-latest steps: - - name: Checkout production branch + - name: Checkout code for the specific tag uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch all history to access parent commits + ref: ${{ github.ref }} # Checks out the specific tag that triggered the workflow + fetch-depth: 0 # For release notes - - name: Extract Meaningful Commit Messages - id: commit_messages + - name: Extract Version and Build Info from Tag + id: version_info run: | - # HEAD is the merge commit on the production branch - # HEAD^1 is the first parent (previous state of production) - # HEAD^2 is the second parent (tip of main that was merged) - # This command gets the subject of commits from main introduced in this merge - # It formats them as "- Subject" and handles multi-line messages for the output - # It also reverses the order to show oldest first, newest last. - MESSAGES=$(git log --pretty="format:- %s" HEAD^1..HEAD^2 --reverse) - if [ -z "$MESSAGES" ]; then - # If no distinct commits (e.g., main was already merged or only fast-forward if that was allowed) - # Fallback to the merge commit's subject - MESSAGES="Changes from merge: $(git log -1 --pretty=%s)" + TAG_NAME="${{ github.ref_name }}" # e.g., v1.0.1-build.123 + # Extract package version (e.g., 1.0.1) + APP_VERSION=$(echo "$TAG_NAME" | sed -n 's/^v\([0-9]*\.[0-9]*\.[0-9]*\)-build\.[0-9]*$/\1/p') + # Extract build number (e.g., 123) + BUILD_NUMBER=$(echo "$TAG_NAME" | sed -n 's/^v[0-9]*\.[0-9]*\.[0-9]*-build\.\([0-9]*\)$/\1/p') + + if [ -z "$APP_VERSION" ] || [ -z "$BUILD_NUMBER" ]; then + echo "Error: Could not parse APP_VERSION or BUILD_NUMBER from tag $TAG_NAME" + # Fallback to package.json version if parsing fails, though tag should be the source of truth here + APP_VERSION=$(node -p "require('./package.json').version") + echo "Fallback APP_VERSION: $APP_VERSION" + # Build number might be harder to get as a fallback without reading versions.json for the specific context fi - # Escape for multiline shell output to GITHUB_OUTPUT and EAS message - MESSAGES="${MESSAGES//'%'/'%25'}" - MESSAGES="${MESSAGES//$'\n'/'%0A'}" # For GITHUB_OUTPUT - MESSAGES="${MESSAGES//$'\r'/'%0D'}" - echo "formatted_messages<> $GITHUB_OUTPUT - echo "$MESSAGES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "Release Tag: $TAG_NAME" + echo "App Version: $APP_VERSION" + echo "Build Number: $BUILD_NUMBER" + echo "release_tag=$TAG_NAME" >> $GITHUB_OUTPUT + echo "app_version=$APP_VERSION" >> $GITHUB_OUTPUT + echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT + + # ... (Setup Node, Install Deps, Build Plugin - same as before) ... - name: Setup node uses: actions/setup-node@v4 with: @@ -59,7 +62,6 @@ jobs: - name: Install dependencies run: yarn install --immutable - # Need this here because the "Setup EAS" setup will execute npx expo config and will need the "build" folder of the plugin to be there - name: Build iOS notification extension plugin run: | yarn plugins:build:notification-service-extension @@ -74,19 +76,28 @@ jobs: patch-watchers: true - name: Build and Submit iOS production + id: eas_build + env: + RELEASE_TAG_VAL: ${{ steps.version_info.outputs.release_tag }} run: | - echo "EAS Build Message Content:" - # For the actual EAS command, we need to replace %0A with actual newlines for readability if supported, - # or keep as a long string. EAS CLI message is a single string. - # Let's prepare a version for the CLI command. - CLI_MESSAGE=$(echo "${{ steps.commit_messages.outputs.formatted_messages }}" | sed 's/%0A/\n/g') - # Truncate if too long, EAS might have a limit (e.g. 250 chars, adjust as needed) - # This example truncates to 240 chars to be safe, adding "..." - MAX_LEN=240 - if [ ${#CLI_MESSAGE} -gt $MAX_LEN ]; then - CLI_MESSAGE="$(echo "$CLI_MESSAGE" | cut -c 1-$((MAX_LEN-3)))..." - fi - echo "$CLI_MESSAGE" - - # The EAS build will be associated with this specific commit. + CLI_MESSAGE="Production build for ${RELEASE_TAG_VAL}" + echo "EAS Build Message: $CLI_MESSAGE" + # EAS should pick up version from package.json and build number from app.config.ts + # which were set correctly by prepare-release.yml for this tag eas build --platform ios --profile production --non-interactive --auto-submit --message "$CLI_MESSAGE" + echo "EAS Build and Submission successful." + + - name: Create GitHub Release + if: success() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ steps.version_info.outputs.release_tag }} + APP_VERSION: ${{ steps.version_info.outputs.app_version }} # Just the semver part + BUILD_NUM: ${{ steps.version_info.outputs.build_number }} + run: | + echo "Creating GitHub Release for $RELEASE_TAG..." + gh release create "$RELEASE_TAG" \ + --title "Release $APP_VERSION (Build $BUILD_NUM)" \ + --generate-notes \ + --target ${{ github.ref_name }} # Target the tag itself + echo "GitHub Release $RELEASE_TAG created successfully." diff --git a/.github/workflows/promote-to-production.yml b/.github/workflows/promote-to-production.yml index 217aef64..f8bca93b 100644 --- a/.github/workflows/promote-to-production.yml +++ b/.github/workflows/promote-to-production.yml @@ -3,40 +3,96 @@ name: Promote to Production on: workflow_dispatch: -# Ensure only one promotion workflow runs at a time -concurrency: - group: "promote-to-production-${{ github.ref_name }}" - cancel-in-progress: false - permissions: - contents: write # Needed to push the merge commit + contents: write jobs: - merge-to-production: + prepare_and_merge: + name: Promote iOS Production Build runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.tag_and_commit.outputs.new_tag }} + app_version: ${{ steps.read_package_version.outputs.app_version }} + build_number: ${{ steps.update_versions_json.outputs.new_build_number }} + steps: - - name: Checkout source branch (the one being promoted) + - name: Checkout main branch uses: actions/checkout@v4 with: - ref: ${{ github.ref }} # Use the ref from which the workflow is run (e.g., main) - token: ${{ secrets.PAT_GITHUB }} # Since production is protected, we need a PAT to push to it - fetch-depth: 0 # Fetch history to read package.json + ref: main + token: ${{ secrets.PAT_GITHUB }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - run: corepack enable + - run: yarn install --immutable - name: Configure Git run: | git config --global user.name "GitHub Actions Bot" git config --global user.email "github-actions[bot]@users.noreply.github.com" - - name: Merge source branch into production + - name: Read App Version from package.json + id: read_package_version run: | - COMMIT_MSG="Merge branch '${{ github.ref_name }}' into production" + APP_VERSION=$(node -p "require('./package.json').version") + if [ -z "$APP_VERSION" ]; then + echo "Error: Could not read version from package.json" + exit 1 + fi + echo "App Version from package.json: $APP_VERSION" + echo "app_version=$APP_VERSION" >> $GITHUB_OUTPUT - # Checkout production branch from remote - git fetch origin production - git checkout -B production origin/production + - name: Update iOS Production Build Number in versions.json + id: update_versions_json + run: | + node scripts/update-versions.js --platform=ios --env=production + # The script itself uses `::set-output name=new_build_number::VALUE` + + - name: Update app.config.ts with New iOS Production Build Number + id: update_app_config + run: | + NEW_BUILD_NUM_FROM_VERSIONS_JSON=${{ steps.update_versions_json.outputs.new_build_number }} + if [ -z "$NEW_BUILD_NUM_FROM_VERSIONS_JSON" ]; then + echo "Error: Failed to get new_build_number from update_versions_json step." + exit 1 + fi + echo "New iOS Production build number from versions.json is: $NEW_BUILD_NUM_FROM_VERSIONS_JSON" + node scripts/update-app-config-buildnum.js --platform=ios --env=production --buildNumber=$NEW_BUILD_NUM_FROM_VERSIONS_JSON + + - name: Commit Changes and Create Tag for iOS Production + id: tag_and_commit + run: | + APP_VERSION=${{ steps.read_package_version.outputs.app_version }} + NEW_BUILD_NUMBER=${{ steps.update_versions_json.outputs.new_build_number }} + + git add versions.json + git add app.config.ts - # Merge the source branch (already checked out by the first step, HEAD points to it) - # ${{ github.ref_name }} is the short name of the branch, e.g., "main" - git merge ${{ github.ref_name }} -Xtheirs --no-ff --no-edit -m "$COMMIT_MSG" + COMMIT_MSG="chore(release): Increment iOS Production Build to $NEW_BUILD_NUMBER for App Version $APP_VERSION" + if git diff --staged --quiet; then + echo "No changes to commit for build number." + else + git commit -m "$COMMIT_MSG" + fi + git push origin main + NEW_TAG="v${APP_VERSION}-ios-build.${NEW_BUILD_NUMBER}" + echo "Creating and pushing tag $NEW_TAG..." + git tag -a "$NEW_TAG" -m "Release App Version $APP_VERSION (iOS Production Build $NEW_BUILD_NUMBER)" + git push origin "$NEW_TAG" + echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + + - name: Merge main into production + run: | + echo "Merging main into production..." + git fetch origin production + git checkout -B production origin/production + git merge main -Xtheirs --no-ff -m "Merge branch 'main' into production for release ${{ steps.tag_and_commit.outputs.new_tag }}" git push origin production diff --git a/app.config.ts b/app.config.ts index 8af60db1..5fcbbd31 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,5 +1,6 @@ import { ExpoConfig } from "expo/config" import { version } from "./package.json" +import { android as androidVersions, ios as iosVersions } from "./versions.json" type Environment = "development" | "preview" | "production" @@ -12,6 +13,7 @@ type EnvironmentConfig = { bundleIdentifier: string associatedDomains: string[] googleServicesFile: string + buildNumber: string icon?: { dark: string light: string @@ -21,6 +23,7 @@ type EnvironmentConfig = { android: { package: string googleServicesFile: string + versionCode: number adaptiveIcon: { foregroundImage: string backgroundColor: string @@ -53,6 +56,7 @@ const settings: Record = { "webcredentials:*.preview.convos.org", ], googleServicesFile: "./google-services/google-services-ios-dev.plist", + buildNumber: String(iosVersions.development.buildNumber), icon: { dark: "./assets/icon-dark.png", light: "./assets/icon-light.png", @@ -62,6 +66,7 @@ const settings: Record = { android: { package: "com.convos.dev", googleServicesFile: "./google-services/google-services-android-dev.json", + versionCode: androidVersions.development.buildCode, adaptiveIcon: { foregroundImage: "./assets/adaptive-icon.png", backgroundColor: "#FFFFFF", @@ -83,6 +88,7 @@ const settings: Record = { "webcredentials:*.preview.convos.org", ], googleServicesFile: "./google-services/google-services-ios-preview.plist", + buildNumber: String(iosVersions.preview.buildNumber), icon: { dark: "./assets/icon-preview-dark.png", light: "./assets/icon-preview-dark.png", @@ -92,6 +98,7 @@ const settings: Record = { android: { package: "com.convos.preview", googleServicesFile: "./google-services/google-services-android-preview.json", + versionCode: androidVersions.preview.buildCode, adaptiveIcon: { foregroundImage: "./assets/adaptive-icon.png", backgroundColor: "#FFFFFF", @@ -112,6 +119,7 @@ const settings: Record = { "webcredentials:*.convos.org", ], googleServicesFile: "./google-services/google-services-ios-prod.plist", + buildNumber: String(iosVersions.production.buildNumber), icon: { dark: "./assets/icon-dark.png", light: "./assets/icon-light.png", @@ -121,6 +129,7 @@ const settings: Record = { android: { package: "com.convos.prod", googleServicesFile: "./google-services/google-services-android-prod.json", + versionCode: androidVersions.production.buildCode, adaptiveIcon: { foregroundImage: "./assets/adaptive-icon.png", backgroundColor: "#FFFFFF", @@ -146,7 +155,9 @@ export default () => { userInterfaceStyle: "automatic", version: version, assetBundlePatterns: ["**/*"], - runtimeVersion: version, + runtimeVersion: { + policy: "nativeVersion", + }, updates: { url: "https://u.expo.dev/f9089dfa-8871-4aff-93ea-da08af0370d2", }, @@ -159,6 +170,7 @@ export default () => { }, ios: { bundleIdentifier: config.ios.bundleIdentifier, + buildNumber: config.ios.buildNumber, supportsTablet: true, associatedDomains: config.ios.associatedDomains, googleServicesFile: config.ios.googleServicesFile, @@ -200,6 +212,7 @@ export default () => { }, android: { package: config.android.package, + versionCode: config.android.versionCode, googleServicesFile: config.android.googleServicesFile, intentFilters: [ { diff --git a/eas.json b/eas.json index 1f4510f6..0bf5b16d 100644 --- a/eas.json +++ b/eas.json @@ -1,7 +1,7 @@ { "cli": { "version": ">=16.6.2", - "appVersionSource": "remote" + "appVersionSource": "local" }, "build": { "_shared": { diff --git a/scripts/update-versions.js b/scripts/update-versions.js new file mode 100644 index 00000000..c69d5ed4 --- /dev/null +++ b/scripts/update-versions.js @@ -0,0 +1,77 @@ +// File: scripts/update-versions.js +const fs = require("fs") +const path = require("path") + +const versionsFilePath = path.join(__dirname, "..", "versions.json") + +// Helper to parse arguments like --platform=ios --env=preview +function parseArgs() { + const args = {} + process.argv.slice(2).forEach((arg) => { + if (arg.startsWith("--")) { + const [key, value] = arg.substring(2).split("=") + if (key && value) { + args[key] = value + } else if (key) { + args[key] = true // For flags without values + } + } + }) + return args +} + +function updateBuildNumber() { + const args = parseArgs() + const platform = args.platform // 'ios' or 'android' + const env = args.env // 'production', 'preview', or 'development' + + if (!platform || !["ios", "android"].includes(platform)) { + console.error("Error: Missing or invalid --platform argument. Must be 'ios' or 'android'.") + process.exit(1) + } + + if (!env || !["production", "preview", "development"].includes(env)) { + console.error( + "Error: Missing or invalid --env argument. Must be 'production', 'preview', or 'development'.", + ) + process.exit(1) + } + + let versionsData + try { + versionsData = JSON.parse(fs.readFileSync(versionsFilePath, "utf-8")) + } catch (error) { + console.error(`Error reading versions.json: ${error.message}`) + process.exit(1) + } + + const keyToUpdate = platform === "ios" ? "buildNumber" : "buildCode" + const platformConfig = versionsData[platform] && versionsData[platform][env] + + if (!platformConfig || typeof platformConfig[keyToUpdate] !== "number") { + console.error(`Error: Could not find a numeric ${keyToUpdate} at versions.${platform}.${env}`) + process.exit(1) + } + + const currentBuildNumber = platformConfig[keyToUpdate] + const newBuildNumber = currentBuildNumber + 1 + platformConfig[keyToUpdate] = newBuildNumber + + try { + fs.writeFileSync(versionsFilePath, JSON.stringify(versionsData, null, 2)) + console.log( + `Successfully updated ${platform}.${env}.${keyToUpdate} to ${newBuildNumber} in versions.json`, + ) + // Output the new build number so it can be captured by GitHub Actions + console.log(`::set-output name=new_build_number::${newBuildNumber}`) + } catch (error) { + console.error(`Error writing versions.json: ${error.message}`) + process.exit(1) + } + + return newBuildNumber // Also return for programmatic use if needed +} + +if (require.main === module) { + updateBuildNumber() +} diff --git a/versions.json b/versions.json new file mode 100644 index 00000000..88f7c91a --- /dev/null +++ b/versions.json @@ -0,0 +1,24 @@ +{ + "ios": { + "production": { + "buildNumber": 0 // TODO set the right version using eas:version:get (something like this) + }, + "preview": { + "buildNumber": 0 // TODO set the right version using eas:version:get (something like this) + }, + "development": { + "buildNumber": 0 // TODO set the right version using eas:version:get (something like this) + } + }, + "android": { + "production": { + "buildCode": 0 + }, + "preview": { + "buildCode": 0 + }, + "development": { + "buildCode": 0 + } + } +}