feat: update changelog for version 1.1.48, fix Google Maps short-link… #107
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: Self-Hosted Mobile Build & Submit | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - "**/*.md" | |
| - "documents/**" | |
| - "logs/**" | |
| workflow_dispatch: | |
| concurrency: | |
| group: self-hosted-mobile-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} | |
| jobs: | |
| build: | |
| name: CI checks + Local Build (ios + android + wear) | |
| runs-on: [self-hosted, macOS, eclipse-timer] | |
| env: | |
| GOOGLE_MAPS_ANDROID_API_KEY: ${{ secrets.GOOGLE_MAPS_ANDROID_API_KEY }} | |
| SENTRY_DISABLE_AUTO_UPLOAD: "true" | |
| SENTRY_DISABLE_XCODE_DEBUG_UPLOAD: "true" | |
| SENTRY_ALLOW_FAILURE: "true" | |
| SENTRY_CLI_EXECUTABLE: "/usr/bin/true" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline | |
| - name: Typecheck | |
| run: pnpm typecheck | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Test | |
| run: pnpm test | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| cache: gradle | |
| - name: Setup Android SDK tools | |
| uses: android-actions/setup-android@v3 | |
| - name: Install Android SDK packages | |
| run: | | |
| set -euo pipefail | |
| sdk_root="" | |
| for candidate in \ | |
| "${ANDROID_SDK_ROOT:-}" \ | |
| "${ANDROID_HOME:-}" \ | |
| "$HOME/Library/Android/sdk" \ | |
| "$HOME/Android/Sdk" \ | |
| "/usr/local/share/android-sdk" \ | |
| "/opt/homebrew/share/android-sdk" | |
| do | |
| if [ -n "$candidate" ] && [ -d "$candidate" ]; then | |
| sdk_root="$candidate" | |
| break | |
| fi | |
| done | |
| if [ ! -d "$sdk_root" ]; then | |
| echo "Android SDK directory not found." | |
| echo "Install Android SDK on the runner or set ANDROID_SDK_ROOT/ANDROID_HOME." | |
| exit 1 | |
| fi | |
| missing_packages=() | |
| [ -d "$sdk_root/platform-tools" ] || missing_packages+=("platform-tools") | |
| [ -d "$sdk_root/platforms/android-36" ] || missing_packages+=("platforms;android-36") | |
| [ -d "$sdk_root/build-tools/36.0.0" ] || missing_packages+=("build-tools;36.0.0") | |
| [ -d "$sdk_root/ndk/27.1.12297006" ] || missing_packages+=("ndk;27.1.12297006") | |
| if [ "${#missing_packages[@]}" -eq 0 ]; then | |
| echo "All required Android SDK packages are already installed in $sdk_root." | |
| exit 0 | |
| fi | |
| echo "Installing missing Android SDK packages: ${missing_packages[*]}" | |
| yes | sdkmanager --licenses >/dev/null || true | |
| sdkmanager "${missing_packages[@]}" | |
| - name: Setup EAS CLI | |
| uses: expo/expo-github-action@v8 | |
| with: | |
| eas-version: latest | |
| token: ${{ secrets.EXPO_TOKEN }} | |
| packager: pnpm | |
| - name: Validate Google Maps Android key | |
| run: | | |
| if [ -z "$GOOGLE_MAPS_ANDROID_API_KEY" ]; then | |
| echo "Missing GOOGLE_MAPS_ANDROID_API_KEY GitHub secret." | |
| echo "Add it in: Settings -> Secrets and variables -> Actions." | |
| exit 1 | |
| fi | |
| - name: Build iOS and Android (AAB + APK + Wear AAB + Wear APK) | |
| working-directory: apps/mobile | |
| env: | |
| EAS_LOCAL_BUILD_ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/raw | |
| SENTRY_DISABLE_AUTO_UPLOAD: "true" | |
| SENTRY_DISABLE_XCODE_DEBUG_UPLOAD: "true" | |
| SENTRY_ALLOW_FAILURE: "true" | |
| SENTRY_CLI_EXECUTABLE: "/usr/bin/true" | |
| run: | | |
| set -euo pipefail | |
| pnpm exec eas build --profile production --platform ios --local --non-interactive | |
| sdk_root="" | |
| for candidate in \ | |
| "${ANDROID_SDK_ROOT:-}" \ | |
| "${ANDROID_HOME:-}" \ | |
| "$HOME/Library/Android/sdk" \ | |
| "$HOME/Android/Sdk" \ | |
| "/usr/local/share/android-sdk" \ | |
| "/opt/homebrew/share/android-sdk" | |
| do | |
| if [ -n "$candidate" ] && [ -d "$candidate" ]; then | |
| sdk_root="$candidate" | |
| break | |
| fi | |
| done | |
| if [ ! -d "$sdk_root" ]; then | |
| echo "Android SDK directory not found." | |
| echo "Install Android SDK on the runner or set ANDROID_SDK_ROOT/ANDROID_HOME." | |
| exit 1 | |
| fi | |
| export ANDROID_SDK_ROOT="$sdk_root" | |
| export ANDROID_HOME="$sdk_root" | |
| escaped_sdk_root="${sdk_root// /\\ }" | |
| printf 'sdk.dir=%s\n' "$escaped_sdk_root" > android/local.properties | |
| echo "Using Android SDK at $sdk_root" | |
| pnpm exec eas build --profile production --platform android --local --non-interactive | |
| pnpm exec eas build --profile production-apk --platform android --local --non-interactive | |
| pnpm exec eas build --profile production-wear --platform android --local --non-interactive | |
| pnpm exec eas build --profile production-wear-apk --platform android --local --non-interactive | |
| - name: Collect build artifacts | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts/submission | |
| version="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/package.json', 'utf8')).version")" | |
| artifact_prefix="eclipse-timer-v${version}" | |
| ios_asset_name="${artifact_prefix}.ipa" | |
| android_aab_asset_name="${artifact_prefix}.aab" | |
| android_apk_asset_name="${artifact_prefix}.apk" | |
| wear_aab_asset_name="${artifact_prefix}-wear.aab" | |
| wear_apk_asset_name="${artifact_prefix}-wear.apk" | |
| ios_artifact="$(find artifacts/raw -type f -name '*.ipa' | head -n 1 || true)" | |
| if [ -z "$ios_artifact" ]; then | |
| echo "Missing iOS artifact (.ipa) in artifacts/raw." | |
| exit 1 | |
| fi | |
| cp "$ios_artifact" "artifacts/submission/$ios_asset_name" | |
| aab_artifacts=() | |
| while IFS= read -r artifact; do | |
| aab_artifacts+=("$artifact") | |
| done < <(find artifacts/raw -type f -name '*.aab' | sort) | |
| if [ "${#aab_artifacts[@]}" -eq 0 ]; then | |
| echo "Missing Android artifacts (.aab) in artifacts/raw." | |
| exit 1 | |
| fi | |
| android_aab_artifact="$(printf '%s\n' "${aab_artifacts[@]}" | grep -iv 'wear' | head -n 1 || true)" | |
| if [ -z "$android_aab_artifact" ]; then | |
| android_aab_artifact="${aab_artifacts[0]}" | |
| fi | |
| if [ -z "$android_aab_artifact" ]; then | |
| echo "Missing Android artifact (.aab) in artifacts/raw." | |
| exit 1 | |
| fi | |
| cp "$android_aab_artifact" "artifacts/submission/$android_aab_asset_name" | |
| wear_aab_artifact="$(find artifacts/raw -type f -name 'wear-release.aab' | head -n 1 || true)" | |
| if [ -z "$wear_aab_artifact" ]; then | |
| wear_aab_artifact="$(printf '%s\n' "${aab_artifacts[@]}" | grep -i 'wear' | head -n 1 || true)" | |
| fi | |
| if [ -z "$wear_aab_artifact" ] && [ "${#aab_artifacts[@]}" -ge 2 ]; then | |
| wear_aab_artifact="${aab_artifacts[$((${#aab_artifacts[@]} - 1))]}" | |
| fi | |
| if [ -z "$wear_aab_artifact" ] || [ "$wear_aab_artifact" = "$android_aab_artifact" ]; then | |
| echo "Missing Wear OS artifact (.aab) in artifacts/raw." | |
| exit 1 | |
| fi | |
| cp "$wear_aab_artifact" "artifacts/submission/$wear_aab_asset_name" | |
| apk_artifacts=() | |
| while IFS= read -r artifact; do | |
| apk_artifacts+=("$artifact") | |
| done < <(find artifacts/raw -type f -name '*.apk' | sort) | |
| if [ "${#apk_artifacts[@]}" -eq 0 ]; then | |
| echo "Missing Android artifacts (.apk) in artifacts/raw." | |
| exit 1 | |
| fi | |
| android_apk_artifact="$(printf '%s\n' "${apk_artifacts[@]}" | grep -iv 'wear' | head -n 1 || true)" | |
| if [ -z "$android_apk_artifact" ]; then | |
| android_apk_artifact="${apk_artifacts[0]}" | |
| fi | |
| if [ -z "$android_apk_artifact" ]; then | |
| echo "Missing Android artifact (.apk) in artifacts/raw." | |
| exit 1 | |
| fi | |
| cp "$android_apk_artifact" "artifacts/submission/$android_apk_asset_name" | |
| wear_apk_artifact="$(find artifacts/raw -type f -name 'wear-release.apk' | head -n 1 || true)" | |
| if [ -z "$wear_apk_artifact" ]; then | |
| wear_apk_artifact="$(printf '%s\n' "${apk_artifacts[@]}" | grep -i 'wear' | head -n 1 || true)" | |
| fi | |
| if [ -z "$wear_apk_artifact" ] && [ "${#apk_artifacts[@]}" -ge 2 ]; then | |
| wear_apk_artifact="${apk_artifacts[$((${#apk_artifacts[@]} - 1))]}" | |
| fi | |
| if [ -z "$wear_apk_artifact" ] || [ "$wear_apk_artifact" = "$android_apk_artifact" ]; then | |
| echo "Missing Wear OS artifact (.apk) in artifacts/raw." | |
| exit 1 | |
| fi | |
| cp "$wear_apk_artifact" "artifacts/submission/$wear_apk_asset_name" | |
| ls -lah artifacts/submission | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: local-mobile-builds-${{ github.run_id }} | |
| path: artifacts/submission | |
| if-no-files-found: error | |
| release_gate: | |
| name: Check mobile version change | |
| runs-on: [self-hosted, macOS, eclipse-timer] | |
| outputs: | |
| should_submit: ${{ steps.gate.outputs.should_submit }} | |
| current_version: ${{ steps.gate.outputs.current_version }} | |
| previous_version: ${{ steps.gate.outputs.previous_version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: true | |
| - name: Determine submit eligibility | |
| id: gate | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| current_version="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/package.json', 'utf8')).version")" | |
| latest_tag="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1 || true)" | |
| previous_version="" | |
| if [ -n "$latest_tag" ]; then | |
| previous_version="${latest_tag#v}" | |
| fi | |
| comparison_result="$( | |
| CURRENT_VERSION="$current_version" PREVIOUS_VERSION="$previous_version" node - <<'NODE' | |
| const semver = /^\d+\.\d+\.\d+$/; | |
| const currentVersion = process.env.CURRENT_VERSION || ""; | |
| const previousVersion = process.env.PREVIOUS_VERSION || ""; | |
| const fail = (message) => { | |
| console.error(message); | |
| process.exit(1); | |
| }; | |
| if (!semver.test(currentVersion)) { | |
| fail(`apps/mobile/package.json version must be x.y.z. Found: ${currentVersion}`); | |
| } | |
| if (!previousVersion) { | |
| process.stdout.write("first_release"); | |
| process.exit(0); | |
| } | |
| if (!semver.test(previousVersion)) { | |
| fail(`Latest release tag must use format vX.Y.Z. Found: v${previousVersion}`); | |
| } | |
| const parse = (value) => value.split(".").map((part) => Number(part)); | |
| const compare = (left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2]; | |
| const versionComparison = compare(parse(currentVersion), parse(previousVersion)); | |
| if (versionComparison > 0) { | |
| process.stdout.write("incremented"); | |
| } else if (versionComparison === 0) { | |
| process.stdout.write("unchanged"); | |
| } else { | |
| process.stdout.write("decremented"); | |
| } | |
| NODE | |
| )" | |
| should_submit="false" | |
| if [ "$comparison_result" = "first_release" ] || [ "$comparison_result" = "incremented" ]; then | |
| should_submit="true" | |
| fi | |
| echo "current_version=$current_version" >> "$GITHUB_OUTPUT" | |
| echo "previous_version=$previous_version" >> "$GITHUB_OUTPUT" | |
| echo "should_submit=$should_submit" >> "$GITHUB_OUTPUT" | |
| if [ "$should_submit" = "true" ]; then | |
| if [ "$comparison_result" = "first_release" ]; then | |
| echo "No previous mobile release tag found." | |
| echo "Submit job will run for both iOS and Android." | |
| else | |
| echo "Detected mobile version increment: $previous_version -> $current_version" | |
| echo "Submit job will run for both iOS and Android." | |
| fi | |
| elif [ "$comparison_result" = "unchanged" ]; then | |
| echo "Mobile version unchanged from latest release tag: $current_version" | |
| echo "Submit job will be skipped." | |
| elif [ "$comparison_result" = "decremented" ]; then | |
| echo "Mobile version $current_version is lower than latest release tag $previous_version." | |
| echo "Submit job will be skipped." | |
| else | |
| echo "Unable to determine mobile release eligibility." | |
| echo "Submit job will be skipped." | |
| fi | |
| submit: | |
| name: Submit to stores | |
| needs: [build, release_gate] | |
| if: ${{ needs.release_gate.outputs.should_submit == 'true' }} | |
| runs-on: [self-hosted, macOS, eclipse-timer] | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: true | |
| - name: Validate release version bump | |
| id: release_meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| latest_tag="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1 || true)" | |
| LATEST_TAG="$latest_tag" node - <<'NODE' | |
| const fs = require('fs'); | |
| const app = JSON.parse(fs.readFileSync('apps/mobile/app.json', 'utf8')); | |
| const pkg = JSON.parse(fs.readFileSync('apps/mobile/package.json', 'utf8')); | |
| const latestTag = process.env.LATEST_TAG || ''; | |
| const packageVersion = pkg.version; | |
| const semver = /^\d+\.\d+\.\d+$/; | |
| const fail = (message) => { | |
| console.error(message); | |
| process.exit(1); | |
| }; | |
| if (typeof packageVersion !== 'string' || !semver.test(packageVersion)) { | |
| fail(`apps/mobile/package.json version must be x.y.z. Found: ${packageVersion}`); | |
| } | |
| if (app?.expo?.version !== undefined) { | |
| fail( | |
| `apps/mobile/app.json expo.version must be omitted. Version is sourced from apps/mobile/package.json via apps/mobile/app.config.ts.` | |
| ); | |
| } | |
| if (app?.expo?.runtimeVersion !== undefined) { | |
| fail( | |
| `apps/mobile/app.json expo.runtimeVersion must be omitted. Runtime version is derived from apps/mobile/package.json via apps/mobile/app.config.ts.` | |
| ); | |
| } | |
| const parse = (value) => value.split('.').map((part) => Number(part)); | |
| const compare = (left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2]; | |
| const currentVersion = packageVersion; | |
| const latestVersion = latestTag.replace(/^v/, ''); | |
| if (latestTag) { | |
| if (!semver.test(latestVersion)) { | |
| fail(`Latest release tag must use format vX.Y.Z. Found: ${latestTag}`); | |
| } | |
| if (compare(parse(currentVersion), parse(latestVersion)) <= 0) { | |
| fail( | |
| `Release version must be incremented. Current version ${currentVersion} must be greater than latest tag ${latestTag}.` | |
| ); | |
| } | |
| } | |
| const outputPath = process.env.GITHUB_OUTPUT; | |
| if (!outputPath) { | |
| fail('GITHUB_OUTPUT is not set.'); | |
| } | |
| fs.appendFileSync(outputPath, `version=${currentVersion}\n`); | |
| fs.appendFileSync(outputPath, `previous_version=${latestVersion}\n`); | |
| fs.appendFileSync(outputPath, `tag_name=v${currentVersion}\n`); | |
| fs.appendFileSync(outputPath, `release_name=Eclipse Timer v${currentVersion}\n`); | |
| const comparisonInfo = latestTag ? ` > ${latestTag}` : ' (no prior release tag found)'; | |
| console.log(`Release version validated: v${currentVersion}${comparisonInfo}`); | |
| NODE | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: local-mobile-builds-${{ github.run_id }} | |
| path: artifacts/submission | |
| - name: Resolve release artifact names | |
| id: artifact_names | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${{ steps.release_meta.outputs.version }}" | |
| artifact_prefix="eclipse-timer-v${version}" | |
| { | |
| echo "ios=${artifact_prefix}.ipa" | |
| echo "android_aab=${artifact_prefix}.aab" | |
| echo "android_apk=${artifact_prefix}.apk" | |
| echo "wear_aab=${artifact_prefix}-wear.aab" | |
| echo "wear_apk=${artifact_prefix}-wear.apk" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Validate submission artifacts | |
| run: | | |
| set -euo pipefail | |
| artifact_root="${{ github.workspace }}/artifacts/submission" | |
| ios_asset="${{ steps.artifact_names.outputs.ios }}" | |
| android_aab_asset="${{ steps.artifact_names.outputs.android_aab }}" | |
| android_apk_asset="${{ steps.artifact_names.outputs.android_apk }}" | |
| wear_aab_asset="${{ steps.artifact_names.outputs.wear_aab }}" | |
| wear_apk_asset="${{ steps.artifact_names.outputs.wear_apk }}" | |
| if [ ! -f "$artifact_root/$ios_asset" ]; then | |
| echo "Missing iOS artifact at $artifact_root/$ios_asset." | |
| exit 1 | |
| fi | |
| if [ ! -f "$artifact_root/$android_aab_asset" ]; then | |
| echo "Missing Android artifact at $artifact_root/$android_aab_asset." | |
| exit 1 | |
| fi | |
| if [ ! -f "$artifact_root/$android_apk_asset" ]; then | |
| echo "Missing Android artifact at $artifact_root/$android_apk_asset." | |
| exit 1 | |
| fi | |
| if [ ! -f "$artifact_root/$wear_aab_asset" ]; then | |
| echo "Missing Wear OS artifact at $artifact_root/$wear_aab_asset." | |
| exit 1 | |
| fi | |
| if [ ! -f "$artifact_root/$wear_apk_asset" ]; then | |
| echo "Missing Wear OS artifact at $artifact_root/$wear_apk_asset." | |
| exit 1 | |
| fi | |
| - name: Prepare GitHub release assets | |
| id: release_assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "files<<EOF" | |
| echo "artifacts/submission/${{ steps.artifact_names.outputs.ios }}" | |
| echo "artifacts/submission/${{ steps.artifact_names.outputs.android_aab }}" | |
| echo "artifacts/submission/${{ steps.artifact_names.outputs.android_apk }}" | |
| echo "artifacts/submission/${{ steps.artifact_names.outputs.wear_aab }}" | |
| echo "artifacts/submission/${{ steps.artifact_names.outputs.wear_apk }}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Prepare GitHub release notes from changelog | |
| id: github_release_notes | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| current_version="${{ steps.release_meta.outputs.version }}" | |
| notes_file="artifacts/submission/github-release-notes.md" | |
| CURRENT_VERSION="$current_version" NOTES_FILE="$notes_file" node - <<'NODE' | |
| const fs = require("fs"); | |
| const currentVersion = process.env.CURRENT_VERSION ?? ""; | |
| const notesFile = process.env.NOTES_FILE ?? ""; | |
| const semver = /^\d+\.\d+\.\d+$/; | |
| if (!semver.test(currentVersion)) { | |
| console.error(`Invalid release version: ${currentVersion}`); | |
| process.exit(1); | |
| } | |
| if (!notesFile) { | |
| console.error("Release notes output path is missing."); | |
| process.exit(1); | |
| } | |
| let changelog = ""; | |
| try { | |
| changelog = fs.readFileSync("CHANGELOG.md", "utf8"); | |
| } catch { | |
| console.error("CHANGELOG.md not found."); | |
| process.exit(1); | |
| } | |
| const escapedVersion = currentVersion.replace(/\./g, "\\."); | |
| const headingRegex = new RegExp(`^## \\[${escapedVersion}\\].*$`, "m"); | |
| const headingMatch = changelog.match(headingRegex); | |
| if (!headingMatch || headingMatch.index === undefined) { | |
| console.error(`No CHANGELOG entry found for version ${currentVersion}.`); | |
| process.exit(1); | |
| } | |
| const fromHeading = changelog.slice(headingMatch.index); | |
| const nextHeadingIndex = fromHeading.slice(headingMatch[0].length).search(/\n## \[/); | |
| const notes = | |
| nextHeadingIndex === -1 | |
| ? fromHeading.trim() | |
| : fromHeading.slice(0, headingMatch[0].length + nextHeadingIndex).trim(); | |
| if (!notes) { | |
| console.error(`CHANGELOG entry for version ${currentVersion} is empty.`); | |
| process.exit(1); | |
| } | |
| fs.mkdirSync("artifacts/submission", { recursive: true }); | |
| fs.writeFileSync(notesFile, `${notes}\n`); | |
| NODE | |
| echo "body_path=$notes_file" >> "$GITHUB_OUTPUT" | |
| - name: Prepare store release notes from changelog (optional) | |
| id: store_notes | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| current_version="${{ steps.release_meta.outputs.version }}" | |
| previous_version="${{ steps.release_meta.outputs.previous_version }}" | |
| notes="$( | |
| CURRENT_VERSION="$current_version" PREVIOUS_VERSION="$previous_version" node - <<'NODE' | |
| const fs = require("fs"); | |
| const currentVersion = process.env.CURRENT_VERSION ?? ""; | |
| const previousVersion = process.env.PREVIOUS_VERSION ?? ""; | |
| const semver = /^\d+\.\d+\.\d+$/; | |
| if (!semver.test(currentVersion)) { | |
| process.exit(0); | |
| } | |
| let changelog = ""; | |
| try { | |
| changelog = fs.readFileSync("CHANGELOG.md", "utf8"); | |
| } catch { | |
| process.exit(0); | |
| } | |
| if (previousVersion && !semver.test(previousVersion)) { | |
| process.exit(0); | |
| } | |
| const parse = (value) => value.split(".").map((part) => Number(part)); | |
| const compare = (left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2]; | |
| const entries = []; | |
| const lines = changelog.split(/\r?\n/); | |
| let activeVersion = ""; | |
| let activeBody = []; | |
| for (const line of lines) { | |
| const headingMatch = line.match(/^## \[(\d+\.\d+\.\d+)\][^\n]*$/); | |
| if (headingMatch) { | |
| if (activeVersion) { | |
| entries.push({ version: activeVersion, body: activeBody.join("\n") }); | |
| } | |
| activeVersion = headingMatch[1]; | |
| activeBody = []; | |
| continue; | |
| } | |
| if (activeVersion) { | |
| activeBody.push(line); | |
| } | |
| } | |
| if (activeVersion) { | |
| entries.push({ version: activeVersion, body: activeBody.join("\n") }); | |
| } | |
| const currentParsed = parse(currentVersion); | |
| const previousParsed = previousVersion ? parse(previousVersion) : null; | |
| const selectedEntries = entries.filter(({ version }) => { | |
| const versionParsed = parse(version); | |
| if (compare(versionParsed, currentParsed) > 0) { | |
| return false; | |
| } | |
| if (!previousParsed) { | |
| return version === currentVersion; | |
| } | |
| return compare(versionParsed, previousParsed) > 0; | |
| }); | |
| const backtickChar = String.fromCharCode(96); | |
| const normalizeBody = (body) => | |
| body | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .filter(Boolean) | |
| .filter((line) => !/^###\s+/i.test(line)) | |
| .map((line) => line.replace(/^[-*]\s+/, "")) | |
| .map((line) => line.replace(/\*\*/g, "")) | |
| .map((line) => line.split(backtickChar).join("")) | |
| .map((line) => line.trim()) | |
| .filter(Boolean); | |
| const rendered = []; | |
| for (const entry of selectedEntries) { | |
| const normalizedLines = normalizeBody(entry.body); | |
| if (!normalizedLines.length) { | |
| continue; | |
| } | |
| rendered.push(`v${entry.version}`); | |
| for (const line of normalizedLines) { | |
| rendered.push(`- ${line}`); | |
| } | |
| rendered.push(""); | |
| } | |
| if (!rendered.length) { | |
| process.exit(0); | |
| } | |
| process.stdout.write(rendered.join("\n").trim()); | |
| NODE | |
| )" | |
| if [ -z "${notes//[[:space:]]/}" ]; then | |
| echo "::notice::No matching CHANGELOG.md notes for version range ${previous_version:-<none>}..$current_version. Continuing without store release notes." | |
| echo "has_store_notes=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| compact_notes="$(printf '%s' "$notes" | tr '\n' ' ' | tr -s '[:space:]' ' ' | sed -e 's/^ //' -e 's/ $//')" | |
| testflight_notes="$notes" | |
| testflight_max_chars=4000 | |
| if [ "${#testflight_notes}" -gt "$testflight_max_chars" ]; then | |
| testflight_notes="${testflight_notes:0:$((testflight_max_chars - 1))}…" | |
| fi | |
| play_notes="$compact_notes" | |
| play_max_chars=500 | |
| if [ "${#play_notes}" -gt "$play_max_chars" ]; then | |
| play_notes="${play_notes:0:$((play_max_chars - 3))}..." | |
| fi | |
| mkdir -p artifacts/submission/whatsnew | |
| printf '%s' "$play_notes" > artifacts/submission/whatsnew/whatsnew-en-US | |
| { | |
| echo "testflight_release_notes<<EOF" | |
| printf '%s\n' "$testflight_notes" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "has_store_notes=true" >> "$GITHUB_OUTPUT" | |
| echo "play_whatsnew_dir=artifacts/submission/whatsnew" >> "$GITHUB_OUTPUT" | |
| - name: Upload iOS artifact to App Store Connect | |
| uses: apple-actions/upload-testflight-build@v3 | |
| with: | |
| app-path: artifacts/submission/${{ steps.artifact_names.outputs.ios }} | |
| issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} | |
| api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} | |
| api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} | |
| - name: Upload Android artifact to Google Play (with notes) | |
| if: ${{ steps.store_notes.outputs.has_store_notes == 'true' }} | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | |
| packageName: com.lallimaven.eclipsetimer | |
| releaseFiles: artifacts/submission/${{ steps.artifact_names.outputs.android_aab }} | |
| track: internal | |
| whatsNewDirectory: ${{ steps.store_notes.outputs.play_whatsnew_dir }} | |
| - name: Upload Android artifact to Google Play | |
| if: ${{ steps.store_notes.outputs.has_store_notes != 'true' }} | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | |
| packageName: com.lallimaven.eclipsetimer | |
| releaseFiles: artifacts/submission/${{ steps.artifact_names.outputs.android_aab }} | |
| track: internal | |
| - name: Upload Wear OS artifact to Google Play (with notes, non-blocking) | |
| id: wear_upload_with_notes | |
| if: ${{ steps.store_notes.outputs.has_store_notes == 'true' }} | |
| continue-on-error: true | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | |
| packageName: com.lallimaven.eclipsetimer | |
| releaseFiles: artifacts/submission/${{ steps.artifact_names.outputs.wear_aab }} | |
| track: internal | |
| whatsNewDirectory: ${{ steps.store_notes.outputs.play_whatsnew_dir }} | |
| - name: Upload Wear OS artifact to Google Play (non-blocking) | |
| id: wear_upload_without_notes | |
| if: ${{ steps.store_notes.outputs.has_store_notes != 'true' }} | |
| continue-on-error: true | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | |
| packageName: com.lallimaven.eclipsetimer | |
| releaseFiles: artifacts/submission/${{ steps.artifact_names.outputs.wear_aab }} | |
| track: internal | |
| - name: Log Wear OS Play upload failure (with notes) | |
| if: ${{ steps.wear_upload_with_notes.outcome == 'failure' }} | |
| run: | | |
| echo "::error::Wear OS AAB upload to Google Play failed (with notes). Continuing pipeline and proceeding to GitHub release." | |
| - name: Log Wear OS Play upload failure | |
| if: ${{ steps.wear_upload_without_notes.outcome == 'failure' }} | |
| run: | | |
| echo "::error::Wear OS AAB upload to Google Play failed. Continuing pipeline and proceeding to GitHub release." | |
| - name: Create GitHub release with mobile artifacts | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.release_meta.outputs.tag_name }} | |
| name: ${{ steps.release_meta.outputs.release_name }} | |
| body_path: ${{ steps.github_release_notes.outputs.body_path }} | |
| generate_release_notes: false | |
| files: ${{ steps.release_assets.outputs.files }} |