diff --git a/.github/workflows/shippable_builds.yml b/.github/workflows/shippable_builds.yml index 2e258565e1a..b31f958f7ae 100644 --- a/.github/workflows/shippable_builds.yml +++ b/.github/workflows/shippable_builds.yml @@ -10,7 +10,15 @@ on: skipK9Mail: type: boolean description: Skip building K-9 Mail - + skipBetaBump: + type: boolean + description: Skip version bump (beta) + skipGooglePlay: + type: boolean + description: Skip Google Play publish + draftGooglePlay: + type: boolean + description: Leave Play Store version in draft state jobs: get_environment: @@ -44,7 +52,6 @@ jobs: environment: ${{ needs.get_environment.outputs.releaseEnv }} outputs: matrixInclude: ${{ steps.dump.outputs.matrixInclude }} - matrixIncludeApk: ${{ steps.dump.outputs.matrixIncludeApk }} releaseType: ${{ vars.RELEASE_TYPE }} steps: - name: Show Environment @@ -53,14 +60,18 @@ jobs: env: matrixInclude: ${{ vars.MATRIX_INCLUDE }} releaseType: ${{ vars.RELEASE_TYPE }} - skipThunderbird: ${{ github.event.inputs.skipThunderbird }} - skipK9Mail: ${{ github.event.inputs.skipK9Mail }} + skipThunderbird: ${{ inputs.skipThunderbird }} + skipK9Mail: ${{ inputs.skipK9Mail }} with: script: | let matrix = JSON.parse(process.env.matrixInclude); let skipThunderbird = process.env.skipThunderbird == "true"; let skipK9Mail = process.env.skipK9Mail == "true"; + if (!matrix.every(item => !!item.appName && !!item.packageFormat)) { + core.setFailed("MATRIX_INCLUDE is missing appName or packageFormat"); + } + let matrixFull = matrix.filter(item => { return !((item.appName == "k9mail" && skipK9Mail) || (item.appName == "thunderbird" && skipThunderbird)); @@ -71,22 +82,24 @@ jobs: return; } - const matrixApk = matrixFull.filter(item => item.packageFormat == "apk"); - core.setOutput("matrixIncludeApk", matrixApk); core.setOutput("matrixInclude", matrixFull); await core.summary - .addRaw(`Beginning a ${process.env.releaseType} build with the following configurations:`, true) + .addRaw(`Beginning a ${process.env.releaseType} build with the following configurations:`, true) .addTable([ [ { data: "App Name", header: true }, { data: "Flavor", header: true }, { data: "Format", header: true }, + { data: "Release Target", header: true }, + { data: "Play Store Track", header: true }, ], ...matrixFull.map(item => [ { data: item.appName }, { data: item.packageFlavor || "default" }, { data: item.packageFormat }, + { data: item.releaseTarget?.replace(/\|/g, ", ") || "artifact only" }, + { data: item.playTargetTrack || "none" }, ]) ]) .write(); @@ -99,17 +112,219 @@ jobs: await core.summary.addList(["K-9 Mail is being skipped in this build"]).write(); } + release_commit: + name: Release Bumps + runs-on: ubuntu-latest + needs: [dump_config, get_environment] + if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }} + environment: ${{ needs.get_environment.outputs.releaseEnv }} + strategy: + matrix: + include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}" + permissions: + contents: write + outputs: + k9mail_sha: ${{ steps.commit.outputs.k9mail_sha }} + thunderbird_sha: ${{ steps.commit.outputs.thunderbird_sha }} + steps: + - name: Checkout repository + if: ${{ contains(matrix.releaseTarget, 'github') }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Copy CI gradle.properties + if: ${{ contains(matrix.releaseTarget, 'github') }} + shell: bash + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - uses: actions/setup-java@v4 + if: ${{ contains(matrix.releaseTarget, 'github') }} + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + if: ${{ contains(matrix.releaseTarget, 'github') }} + with: + cache-disabled: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }} + add-job-summary: never + + - name: Get application info + id: appinfo + shell: bash + if: ${{ contains(matrix.releaseTarget, 'github') }} + env: + RELEASE_TYPE: ${{ vars.RELEASE_TYPE }} + PACKAGE_FLAVOR: ${{ matrix.packageFlavor }} + APP_NAME: ${{ matrix.appName }} + run: | + if [[ "${APP_NAME}" == "k9mail" && "${RELEASE_TYPE}" == "beta" ]]; then + # k9mail uses release for betas as well. Later on we should align the structures and + # remove this hack + RELEASE_TYPE=release + fi + + ./gradlew :app-${APP_NAME}:printVersionInfo -PbuildType=${RELEASE_TYPE} -PflavorName=${PACKAGE_FLAVOR} --configure-on-demand + + - name: Bump version code + id: bump_version_code + if: ${{ contains(matrix.releaseTarget, 'github') }} + shell: bash + env: + APP_NAME: ${{ matrix.appName }} + OLD_VERSION_CODE: ${{ steps.appinfo.outputs.VERSION_CODE }} + run: | + NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1)) + sed "s/versionCode = $OLD_VERSION_CODE/versionCode = $NEW_VERSION_CODE/" app-${APP_NAME}/build.gradle.kts > tmp_gradle_kts + + ! diff -u app-${APP_NAME}/build.gradle.kts tmp_gradle_kts # flip return value to force error if no bump + mv tmp_gradle_kts app-${APP_NAME}/build.gradle.kts + + echo "CODE=${NEW_VERSION_CODE}" | tee $GITHUB_OUTPUT + + - name: Bump version suffix + id: bump_version_suffix + if: ${{ !inputs.skipBetaBump && contains(matrix.releaseTarget, 'github') && vars.RELEASE_TYPE == 'beta' }} + shell: bash + env: + APP_NAME: ${{ matrix.appName }} + OLD_VERSION_SUFFIX: ${{ steps.appinfo.outputs.VERSION_NAME_SUFFIX }} + run: | + NEW_VERSION_SUFFIX=b$((${OLD_VERSION_SUFFIX:1} + 1)) + sed "s/versionNameSuffix = \"$OLD_VERSION_SUFFIX\"/versionNameSuffix = \"$NEW_VERSION_SUFFIX\"/" app-${APP_NAME}/build.gradle.kts > tmp_gradle_kts + + ! diff -u app-${APP_NAME}/build.gradle.kts tmp_gradle_kts # flip return value to force error if no bump + mv tmp_gradle_kts app-${APP_NAME}/build.gradle.kts + + echo "SUFFIX=${NEW_VERSION_SUFFIX}" >> $GITHUB_OUTPUT + + cat $GITHUB_OUTPUT + + - name: Render Release Notes + if: ${{ contains(matrix.releaseTarget, 'github') }} + shell: bash + env: + APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }} + APPLICATION_LABEL: ${{ steps.appinfo.outputs.APPLICATION_LABEL }} + VERSION_CODE: ${{ steps.bump_version_code.outputs.CODE }} + FULL_VERSION_NAME: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }} + run: | + echo "

${APPLICATION_LABEL} ${FULL_VERSION_NAME} Release Notes (${VERSION_CODE})

" | tee -a $GITHUB_STEP_SUMMARY
+          mkdir -p ./app-metadata/${APPLICATION_ID}/en-US/changelogs
+          python ./scripts/render-notes.py ${APPLICATION_ID} ${FULL_VERSION_NAME} ${VERSION_CODE} | tee -a $GITHUB_STEP_SUMMARY
+          echo "
" | tee -a $GITHUB_STEP_SUMMARY + + - name: Validate Release Notes Length + if: ${{ contains(matrix.releaseTarget, 'github') }} + shell: bash + env: + APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }} + VERSION_CODE: ${{ steps.bump_version_code.outputs.CODE }} + run: | + wc -c ./app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt + RELNOTES_LENGTH=$(wc -c ./app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt | awk '{print $1}') + + if [[ "${RELNOTES_LENGTH}" -gt 500 ]]; then + echo "Release Notes are too long. Found ${RELNOTES_LENGTH} characters, need a maximum of 500" + exit 1 + fi + + - name: Release Commits + if: ${{ contains(matrix.releaseTarget, 'github') }} + id: commit + shell: bash + env: + APPLICATION_LABEL: ${{ steps.appinfo.outputs.APPLICATION_LABEL }} + APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }} + APP_NAME: ${{ matrix.appName }} + FULL_VERSION_NAME: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }} + run: | + git config --global user.name "GitHub Actions Bot" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # We need the metadata to point to the right application for the release commit + rm metadata + ln -sf app-metadata/${APPLICATION_ID} metadata + + # Add changelogs, build version changes and metadata symlink + git add ./app-metadata/${APPLICATION_ID}/en-US/changelogs/* + git add ./app-${APP_NAME}/src/main/res/raw/changelog_master.xml + git add ./app-${APP_NAME}/build.gradle.kts + git add metadata + + # Ready to commit. Make sure to pull again to reduce likelihood of race conditions + git status + git pull + git commit -m "Release: ${APPLICATION_LABEL} ${FULL_VERSION_NAME}" + git log -n 5 + + set +e + git push + GIT_RESULT=$? + set -e + + if [ $GIT_RESULT -gt 0 ]; then + echo "Push rejected, trying again once in 5 seconds" + sleep 5 + git pull --rebase -X ours + git push + fi + + echo "${APP_NAME}_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + - name: Summary + if: ${{ contains(matrix.releaseTarget, 'github') }} + uses: actions/github-script@v7 + env: + bump_sha: ${{ steps.commit.outputs.sha }} + applicationId: ${{ steps.appinfo.outputs.APPLICATION_ID }} + oldFullVersion: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.appinfo.outputs.VERSION_NAME_SUFFIX }} + oldVersionCode: ${{ steps.appinfo.outputs.VERSION_CODE }} + newFullVersion: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }} + newVersionCode: ${{ steps.bump_version_code.outputs.CODE }} + with: + script: | + let env = process.env; + console.log(env); + await core.summary + .addRaw(`Version for ${env.applicationId} bumped from ${env.oldFullVersion} (${env.oldVersionCode}) to ${env.newFullVersion} (${env.newVersionCode}) in `) + .addLink(process.env.bump_sha, `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${env.bump_sha}`) + .addEOL() + .write(); + build_unsigned: name: Build Unsigned runs-on: ubuntu-latest timeout-minutes: 90 - needs: [dump_config, get_environment] + needs: [dump_config, get_environment, release_commit] strategy: matrix: include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}" environment: ${{ needs.get_environment.outputs.releaseEnv }} steps: - - uses: actions/checkout@v4 + - name: Get release sha + id: sha + shell: bash + env: + THUNDERBIRD_SHA: ${{ needs.release_commit.outputs.thunderbird_sha }} + K9MAIL_SHA: ${{ needs.release_commit.outputs.k9mail_sha }} + APP_NAME: ${{ matrix.appName }} + run: | + case "${APP_NAME}" in + thunderbird) APP_SHA=$THUNDERBIRD_SHA ;; + k9mail) APP_SHA=$K9MAIL_SHA ;; + esac + + echo "app_sha=$APP_SHA" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ steps.sha.outputs.app_sha }} - name: Copy CI gradle.properties shell: bash @@ -124,6 +339,7 @@ jobs: uses: gradle/actions/setup-gradle@v4 with: cache-disabled: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }} + add-job-summary: on-failure - name: Build It shell: bash @@ -135,7 +351,7 @@ jobs: run: | if [[ "$APP_NAME" = "thunderbird" && "$PACKAGE_FORMAT" = "apk" ]]; then BUILD_COMMAND="assemble${PACKAGE_FLAVOR^}${RELEASE_TYPE^}" - elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "bundle" ]]; then + elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "aab" ]]; then BUILD_COMMAND="bundle${PACKAGE_FLAVOR^}${RELEASE_TYPE^}" elif [[ "$APP_NAME" = "k9mail" ]]; then BUILD_COMMAND="assembleRelease" @@ -154,18 +370,18 @@ jobs: RELEASE_TYPE: ${{ vars.RELEASE_TYPE }} UPLOAD_PATH: "uploads" run: | - OUT_BASE=app-${APP_NAME}/build/outputs/${PACKAGE_FORMAT} + OUT_BASE=app-${APP_NAME}/build/outputs/ if [[ "$APP_NAME" = "thunderbird" && "$PACKAGE_FORMAT" = "apk" ]]; then - OUT_PATH="${OUT_BASE}/${PACKAGE_FLAVOR}/${RELEASE_TYPE}" + OUT_PATH="${OUT_BASE}/apk/${PACKAGE_FLAVOR}/${RELEASE_TYPE}" OUT_FILE="app-${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-unsigned.apk" UPLOAD_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.apk" - elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "bundle" ]]; then - OUT_PATH="${OUT_BASE}/${PACKAGE_FLAVOR}${RELEASE_TYPE^}" + elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "aab" ]]; then + OUT_PATH="${OUT_BASE}/bundle/${PACKAGE_FLAVOR}${RELEASE_TYPE^}" OUT_FILE="app-${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.aab" UPLOAD_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.aab" elif [[ "$APP_NAME" = "k9mail" ]]; then - OUT_PATH="${OUT_BASE}/release" + OUT_PATH="${OUT_BASE}/apk/release" OUT_FILE="app-${APP_NAME}-release-unsigned.apk" UPLOAD_FILE="${APP_NAME}-default-${RELEASE_TYPE}.apk" else @@ -219,6 +435,15 @@ jobs: keyPassword: ${{ secrets.KEY_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + - name: Rename assets + if: ${{ matrix.packageFormat == 'apk' }} + env: + APP_NAME: ${{ matrix.appName }} + PACKAGE_FLAVOR: ${{ matrix.packageFlavor || 'default' }} + run: | + mv uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-signed.apk uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.apk + rm uploads/*-aligned.apk + - name: Remove JKS file shell: bash run: | @@ -230,7 +455,7 @@ jobs: name: signed-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }} if-no-files-found: error path: | - uploads/*-signed.apk + uploads/*.apk uploads/*.aab pre_publish: @@ -248,39 +473,65 @@ jobs: run: | true - github_release: - name: GitHub Release - needs: [pre_publish, dump_config] - if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }} + publish_release: + name: Publish Release + needs: [pre_publish, dump_config, release_commit] + if: ${{ !failure() && !cancelled() }} # Run if previous step is skipped runs-on: ubuntu-latest strategy: matrix: - include: "${{ fromJSON(needs.dump_config.outputs.matrixIncludeApk) }}" - environment: gh-releases + include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}" + environment: publish_release env: RELEASE_TYPE: ${{ needs.dump_config.outputs.releaseType }} APP_NAME: ${{ matrix.appName }} PACKAGE_FLAVOR: ${{ matrix.packageFlavor || 'default' }} + PACKAGE_FORMAT: ${{ matrix.packageFormat }} steps: - uses: actions/download-artifact@v4 with: name: signed-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }} path: "uploads/" - - name: Get APK Info - id: apkinfo + - name: Get Package Info + id: pkginfo shell: bash + env: + GH_TOKEN: ${{ github.token }} run: | - APK_FILE="uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-signed.apk" - LATEST_BUILD_TOOLS=$(ls -d ${ANDROID_SDK_ROOT}/build-tools/* | sort -V | tail -n1) - AAPT=${LATEST_BUILD_TOOLS}/aapt - - NAME=$(${AAPT} dump badging $APK_FILE | sed -n "s/.*application-label:'\([^']*\)'.*/\1/p") - VERSION=$(${AAPT} dump badging $APK_FILE | sed -n "s/.*versionName='\([^']*\)'.*/\1/p") + PKG_FILE="uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.${PACKAGE_FORMAT}" + if [[ "${PACKAGE_FORMAT}" == "apk" ]]; then + LATEST_BUILD_TOOLS=$(ls -d ${ANDROID_SDK_ROOT}/build-tools/* | sort -V | tail -n1) + AAPT=${LATEST_BUILD_TOOLS}/aapt + + VERSION_NAME=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package:.*versionName='\([^']*\)'.*$/\1/p") + VERSION_CODE=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package:.*versionCode='\([^']*\)'.*$/\1/p") + APPLICATION_ID=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package: name='\([^']*\)'.*$/\1/p") + APPLICATION_LABEL=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^application-label:'\([^']*\)'.*$/\1/p") + elif [[ "${PACKAGE_FORMAT}" == "aab" ]]; then + if [ ! -f bundletool.jar ]; then + gh release download -R google/bundletool -p 'bundletool-all-*.jar' -O bundletool.jar + fi + BUNDLETOOL="java -jar bundletool.jar" + + VERSION_NAME=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@android:versionName') + VERSION_CODE=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@android:versionCode') + APPLICATION_ID=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@package') + + # Unfortunately no application label in the bundle + case "$APPLICATION_ID" in + net.thunderbird.android) APPLICATION_LABEL="Thunderbird" ;; + net.thunderbird.android.beta) APPLICATION_LABEL="Thunderbird Beta" ;; + net.thunderbird.android.daily) APPLICATION_LABEL="Thunderbird Daily" ;; + com.fsck.k9) APPLICATION_LABEL="K-9 Mail" ;; + esac + fi - echo "TAG_NAME=${APP_NAME^^}_${VERSION//./_}" >> $GITHUB_OUTPUT - echo "FULL_VERSION_NAME=${NAME} ${VERSION}" >> $GITHUB_OUTPUT - echo "VERSION_NAME=${VERSION}" >> $GITHUB_OUTPUT + echo "TAG_NAME=${APP_NAME^^}_${VERSION_NAME//./_}" >> $GITHUB_OUTPUT + echo "FULL_VERSION_NAME=${APPLICATION_LABEL} ${VERSION_NAME}" >> $GITHUB_OUTPUT + echo "VERSION_NAME=${VERSION_NAME}" >> $GITHUB_OUTPUT + echo "VERSION_CODE=${VERSION_CODE}" >> $GITHUB_OUTPUT + echo "APPLICATION_ID=${APPLICATION_ID}" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT @@ -288,49 +539,108 @@ jobs: id: rename shell: bash env: - VERSION_NAME: ${{ steps.apkinfo.outputs.VERSION_NAME }} + VERSION_NAME: ${{ steps.pkginfo.outputs.VERSION_NAME }} run: | - APK_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-signed.apk" - APK_FILE_PRETTY="${APP_NAME}-${VERSION_NAME}.apk" - mv uploads/${APK_FILE} uploads/${APK_FILE_PRETTY} + PKG_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.${PACKAGE_FORMAT}" + PKG_FILE_PRETTY="${APP_NAME}-${VERSION_NAME}.${PACKAGE_FORMAT}" + mv uploads/${PKG_FILE} uploads/${PKG_FILE_PRETTY} - echo "APK_FILE=${APK_FILE_PRETTY}" >> $GITHUB_OUTPUT - ls -l uploads/${APK_FILE_PRETTY} + echo "PKG_FILE=${PKG_FILE_PRETTY}" >> $GITHUB_OUTPUT + ls -l uploads/${PKG_FILE_PRETTY} - name: App Token Generate uses: actions/create-github-app-token@v1 + if: ${{ contains(matrix.releaseTarget, 'github') && vars.RELEASER_APP_CLIENT_ID }} id: app-token with: app-id: ${{ vars.RELEASER_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} - - name: Publish - id: publish + - name: Get release sha + id: sha + shell: bash + env: + THUNDERBIRD_SHA: ${{ needs.release_commit.outputs.thunderbird_sha }} + K9MAIL_SHA: ${{ needs.release_commit.outputs.k9mail_sha }} + APP_NAME: ${{ matrix.appName }} + run: | + case "${APP_NAME}" in + thunderbird) APP_SHA=$THUNDERBIRD_SHA ;; + k9mail) APP_SHA=$K9MAIL_SHA ;; + esac + + echo "app_sha=$APP_SHA" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + - name: Publish to GitHub Releases + id: publish_gh + if: ${{ contains(matrix.releaseTarget, 'github') }} uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: - token: ${{ steps.app-token.outputs.token }} - target_commitish: ${{ github.sha }} - tag_name: ${{ steps.apkinfo.outputs.TAG_NAME }} - name: ${{ steps.apkinfo.outputs.FULL_VERSION_NAME }} + token: ${{ steps.app-token.outputs.token || github.token }} + target_commitish: ${{ steps.sha.outputs.app_sha }} + tag_name: ${{ steps.pkginfo.outputs.TAG_NAME }} + name: ${{ steps.pkginfo.outputs.FULL_VERSION_NAME }} prerelease: ${{ env.RELEASE_TYPE != 'release' }} fail_on_unmatched_files: true files: | - uploads/${{ steps.rename.outputs.APK_FILE }} + uploads/${{ steps.rename.outputs.PKG_FILE }} + + - name: Adjust release notes for play store upload + if: ${{ !inputs.skipGooglePlay && contains(matrix.releaseTarget, 'play') && matrix.playTargetTrack }} + shell: bash + env: + VERSION_CODE: ${{ steps.pkginfo.outputs.VERSION_CODE }} + APPLICATION_ID: ${{ steps.pkginfo.outputs.APPLICATION_ID }} + REPO: ${{ github.repository }} + APP_SHA: ${{ steps.sha.outputs.app_sha }} + run: | + # r0adkll/upload-google-play expects the release notes in a different structure + FILEPATH=app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt + mkdir whatsnew + wget -O whatsnew/whatsnew-en-US https://raw.githubusercontent.com/${REPO}/${APP_SHA}/${FILEPATH} + + - name: Publish to Google Play + id: publish_play + uses: r0adkll/upload-google-play@v1 + if: ${{ !inputs.skipGooglePlay && contains(matrix.releaseTarget, 'play') && matrix.playTargetTrack }} + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_ACCOUNT }} + packageName: ${{ steps.pkginfo.outputs.APPLICATION_ID }} + track: ${{ matrix.playTargetTrack }} + releaseName: ${{ steps.pkginfo.outputs.VERSION_NAME }} + status: completed + changesNotSentForReview: ${{ inputs.draftGooglePlay }} + whatsNewDirectory: whatsnew + releaseFiles: | + uploads/${{ steps.rename.outputs.PKG_FILE }} - name: Summary uses: actions/github-script@v7 env: - tagName: ${{ steps.apkinfo.outputs.TAG_NAME }} - fullVersionName: ${{ steps.apkinfo.outputs.FULL_VERSION_NAME }} - releaseUrl: ${{ steps.publish.outputs.url }} - assets: ${{ steps.publish.outputs.assets }} + tagName: ${{ steps.pkginfo.outputs.TAG_NAME }} + fullVersionName: ${{ steps.pkginfo.outputs.FULL_VERSION_NAME }} + ghReleaseUrl: ${{ steps.publish_gh.outputs.url }} + playTargetTrack: ${{ matrix.playTargetTrack }} + applicationId: ${{ steps.pkginfo.outputs.APPLICATION_ID }} + app_sha: ${{ steps.sha.outputs.app_sha }} with: script: | - let assets = JSON.parse(process.env.assets); - await core.summary - .addRaw(`Release `) - .addLink(process.env.fullVersionName, process.env.releaseUrl) + .addHeading(`${process.env.fullVersionName} (${process.env.applicationId})`, 2) .addRaw(`Tag ${process.env.tagName} at `) - .addLink(context.sha, `${context.server_url}/${context.repository}/commit/${context.sha}`) + .addLink(process.env.app_sha, `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.app_sha}`) + .addEOL() .write(); + + if (process.env.ghReleaseUrl) { + await core.summary + .addRaw(`Released to Github at `) + .addLink(process.env.ghReleaseUrl, process.env.ghReleaseUrl) + .addEOL() + .write(); + } + + if (process.env.playTargetTrack) { + await core.summary.addRaw(`Released to the ${process.env.playTargetTrack} track on Google Play`, true).write(); + } diff --git a/app-k9mail/badging/release-badging.txt b/app-k9mail/badging/release-badging.txt index ea7a9e5f7a0..425388ffd8e 100644 --- a/app-k9mail/badging/release-badging.txt +++ b/app-k9mail/badging/release-badging.txt @@ -54,6 +54,7 @@ application-label-lv:'K-9 pasts' application-label-ml:'K-9 Mail' application-label-nb:'K-9 e-post' application-label-nl:'K-9 Mail' +application-label-nn:'K-9 e-post' application-label-pl:'K-9 Mail' application-label-pt:'K-9 Mail' application-label-pt-BR:'K-9 Mail' @@ -68,8 +69,8 @@ application-label-sv:'K-9 Mail' application-label-tr:'K-9 Posta' application-label-uk:'K-9 Mail' application-label-vi:'Thư K-9' -application-label-zh:'K-9 邮件' -application-label-zh-CN:'K-9 邮件' +application-label-zh:'K-9 Mail' +application-label-zh-CN:'K-9 Mail' application-label-zh-TW:'K-9 Mail' application-icon-120:'res/drawable-v26/ic_launcher.xml' application-icon-160:'res/drawable-v26/ic_launcher.xml' @@ -93,6 +94,6 @@ other-receivers other-services supports-screens: 'small' 'normal' 'large' 'xlarge' supports-any-density: 'true' -locales: '--_--' 'ar' 'be' 'bg' 'br' 'ca' 'co' 'cs' 'cy' 'da' 'de' 'el' 'en' 'en-GB' 'eo' 'es' 'et' 'eu' 'fa' 'fi' 'fr' 'fy' 'gd' 'gl' 'hr' 'hu' 'in' 'is' 'it' 'iw' 'ja' 'ko' 'lt' 'lv' 'ml' 'nb' 'nl' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'sk' 'sl' 'sq' 'sr' 'sv' 'tr' 'uk' 'vi' 'zh' 'zh-CN' 'zh-TW' +locales: '--_--' 'ar' 'be' 'bg' 'br' 'ca' 'co' 'cs' 'cy' 'da' 'de' 'el' 'en' 'en-GB' 'eo' 'es' 'et' 'eu' 'fa' 'fi' 'fr' 'fy' 'gd' 'gl' 'hr' 'hu' 'in' 'is' 'it' 'iw' 'ja' 'ko' 'lt' 'lv' 'ml' 'nb' 'nl' 'nn' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'sk' 'sl' 'sq' 'sr' 'sv' 'tr' 'uk' 'vi' 'zh' 'zh-CN' 'zh-TW' densities: '120' '160' '240' '320' '480' '640' '65534' native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' diff --git a/app-k9mail/build.gradle.kts b/app-k9mail/build.gradle.kts index 7f6904b6f08..63d3968993e 100644 --- a/app-k9mail/build.gradle.kts +++ b/app-k9mail/build.gradle.kts @@ -1,3 +1,7 @@ +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + plugins { id(ThunderbirdPlugins.App.androidCompose) alias(libs.plugins.dependency.guard) @@ -25,6 +29,7 @@ dependencies { implementation(projects.feature.widget.unread) implementation(projects.feature.telemetry.noop) implementation(projects.feature.funding.noop) + implementation(projects.feature.onboarding.migration.noop) implementation(libs.androidx.work.runtime) @@ -47,7 +52,8 @@ android { testApplicationId = "com.fsck.k9.tests" versionCode = 39004 - versionName = "6.905-SNAPSHOT" + versionName = "9.0" + versionNameSuffix = "-SNAPSHOT" // Keep in sync with the resource string array "supported_languages" resourceConfigurations.addAll( @@ -88,6 +94,7 @@ android { "ml", "nb", "nl", + "nn", "pl", "pt_BR", "pt_PT", @@ -152,3 +159,44 @@ android { dependencyGuard { configuration("releaseRuntimeClasspath") } + +tasks.create("printVersionInfo") { + val targetBuildType = project.findProperty("buildType") ?: "debug" + + doLast { + android.applicationVariants.all { variant -> + if (variant.buildType.name == targetBuildType) { + val flavor = variant.mergedFlavor + + var buildTypeSource = android.sourceSets.getByName(targetBuildType).res.srcDirs.first() + var stringsXmlFile = File(buildTypeSource, "values/strings.xml") + if (!stringsXmlFile.exists()) { + buildTypeSource = android.sourceSets.getByName("main").res.srcDirs.first() + stringsXmlFile = File(buildTypeSource, "values/strings.xml") + } + + val xmlDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsXmlFile) + val xPath = XPathFactory.newInstance().newXPath() + val expression = "/resources/string[@name='app_name']/text()" + val appName = xPath.evaluate(expression, xmlDocument, XPathConstants.STRING) as String + + val output = """ + APPLICATION_ID=${variant.applicationId} + APPLICATION_LABEL=$appName + VERSION_CODE=${flavor.versionCode} + VERSION_NAME=${flavor.versionName} + VERSION_NAME_SUFFIX=${flavor.versionNameSuffix ?: ""} + FULL_VERSION_NAME=${flavor.versionName}${flavor.versionNameSuffix ?: ""} + """.trimIndent() + + println(output) + val githubOutput = System.getenv("GITHUB_OUTPUT") + if (githubOutput != null) { + val outputFile = File(githubOutput) + outputFile.writeText(output + "\n") + } + } + true + } + } +} diff --git a/app-k9mail/dependencies/releaseRuntimeClasspath.txt b/app-k9mail/dependencies/releaseRuntimeClasspath.txt index c49f3b7192a..68832eb68a7 100644 --- a/app-k9mail/dependencies/releaseRuntimeClasspath.txt +++ b/app-k9mail/dependencies/releaseRuntimeClasspath.txt @@ -1,9 +1,9 @@ -androidx.activity:activity-compose:1.9.2 -androidx.activity:activity-ktx:1.9.2 -androidx.activity:activity:1.9.2 +androidx.activity:activity-compose:1.9.3 +androidx.activity:activity-ktx:1.9.3 +androidx.activity:activity:1.9.3 androidx.annotation:annotation-experimental:1.4.1 -androidx.annotation:annotation-jvm:1.8.2 -androidx.annotation:annotation:1.8.2 +androidx.annotation:annotation-jvm:1.9.0 +androidx.annotation:annotation:1.9.0 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 @@ -11,50 +11,50 @@ androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 androidx.biometric:biometric:1.1.0 androidx.browser:browser:1.3.0 -androidx.camera:camera-camera2:1.3.1 -androidx.camera:camera-core:1.3.1 -androidx.camera:camera-lifecycle:1.3.1 -androidx.camera:camera-video:1.3.1 -androidx.camera:camera-view:1.3.1 +androidx.camera:camera-camera2:1.3.4 +androidx.camera:camera-core:1.3.4 +androidx.camera:camera-lifecycle:1.3.4 +androidx.camera:camera-video:1.3.4 +androidx.camera:camera-view:1.3.4 androidx.cardview:cardview:1.0.0 -androidx.collection:collection-jvm:1.4.2 -androidx.collection:collection-ktx:1.4.2 -androidx.collection:collection:1.4.2 -androidx.compose.animation:animation-android:1.7.0 -androidx.compose.animation:animation-core-android:1.7.0 -androidx.compose.animation:animation-core:1.7.0 -androidx.compose.animation:animation:1.7.0 -androidx.compose.foundation:foundation-android:1.7.0 -androidx.compose.foundation:foundation-layout-android:1.7.0 -androidx.compose.foundation:foundation-layout:1.7.0 -androidx.compose.foundation:foundation:1.7.0 +androidx.collection:collection-jvm:1.4.4 +androidx.collection:collection-ktx:1.4.4 +androidx.collection:collection:1.4.4 +androidx.compose.animation:animation-android:1.7.4 +androidx.compose.animation:animation-core-android:1.7.4 +androidx.compose.animation:animation-core:1.7.4 +androidx.compose.animation:animation:1.7.4 +androidx.compose.foundation:foundation-android:1.7.4 +androidx.compose.foundation:foundation-layout-android:1.7.4 +androidx.compose.foundation:foundation-layout:1.7.4 +androidx.compose.foundation:foundation:1.7.4 androidx.compose.material3:material3-android:1.3.0 androidx.compose.material3:material3:1.3.0 -androidx.compose.material:material-icons-core-android:1.7.0 -androidx.compose.material:material-icons-core:1.7.0 -androidx.compose.material:material-icons-extended-android:1.7.0 -androidx.compose.material:material-icons-extended:1.7.0 -androidx.compose.material:material-ripple-android:1.7.0 -androidx.compose.material:material-ripple:1.7.0 -androidx.compose.runtime:runtime-android:1.7.0 -androidx.compose.runtime:runtime-saveable-android:1.7.0 -androidx.compose.runtime:runtime-saveable:1.7.0 -androidx.compose.runtime:runtime:1.7.0 -androidx.compose.ui:ui-android:1.7.0 -androidx.compose.ui:ui-geometry-android:1.7.0 -androidx.compose.ui:ui-geometry:1.7.0 -androidx.compose.ui:ui-graphics-android:1.7.0 -androidx.compose.ui:ui-graphics:1.7.0 -androidx.compose.ui:ui-text-android:1.7.0 -androidx.compose.ui:ui-text:1.7.0 -androidx.compose.ui:ui-tooling-preview-android:1.7.0 -androidx.compose.ui:ui-tooling-preview:1.7.0 -androidx.compose.ui:ui-unit-android:1.7.0 -androidx.compose.ui:ui-unit:1.7.0 -androidx.compose.ui:ui-util-android:1.7.0 -androidx.compose.ui:ui-util:1.7.0 -androidx.compose.ui:ui:1.7.0 -androidx.compose:compose-bom:2024.09.00 +androidx.compose.material:material-icons-core-android:1.7.4 +androidx.compose.material:material-icons-core:1.7.4 +androidx.compose.material:material-icons-extended-android:1.7.4 +androidx.compose.material:material-icons-extended:1.7.4 +androidx.compose.material:material-ripple-android:1.7.4 +androidx.compose.material:material-ripple:1.7.4 +androidx.compose.runtime:runtime-android:1.7.4 +androidx.compose.runtime:runtime-saveable-android:1.7.4 +androidx.compose.runtime:runtime-saveable:1.7.4 +androidx.compose.runtime:runtime:1.7.4 +androidx.compose.ui:ui-android:1.7.4 +androidx.compose.ui:ui-geometry-android:1.7.4 +androidx.compose.ui:ui-geometry:1.7.4 +androidx.compose.ui:ui-graphics-android:1.7.4 +androidx.compose.ui:ui-graphics:1.7.4 +androidx.compose.ui:ui-text-android:1.7.4 +androidx.compose.ui:ui-text:1.7.4 +androidx.compose.ui:ui-tooling-preview-android:1.7.4 +androidx.compose.ui:ui-tooling-preview:1.7.4 +androidx.compose.ui:ui-unit-android:1.7.4 +androidx.compose.ui:ui-unit:1.7.4 +androidx.compose.ui:ui-util-android:1.7.4 +androidx.compose.ui:ui-util:1.7.4 +androidx.compose.ui:ui:1.7.4 +androidx.compose:compose-bom:2024.10.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.0.4 androidx.constraintlayout:constraintlayout:2.1.4 @@ -71,42 +71,42 @@ androidx.dynamicanimation:dynamicanimation:1.0.0 androidx.emoji2:emoji2-views-helper:1.3.0 androidx.emoji2:emoji2:1.3.0 androidx.exifinterface:exifinterface:1.3.6 -androidx.fragment:fragment-compose:1.8.3 -androidx.fragment:fragment-ktx:1.8.3 -androidx.fragment:fragment:1.8.3 +androidx.fragment:fragment-compose:1.8.4 +androidx.fragment:fragment-ktx:1.8.4 +androidx.fragment:fragment:1.8.4 androidx.graphics:graphics-path:1.0.1 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.8.5 -androidx.lifecycle:lifecycle-common-jvm:2.8.5 -androidx.lifecycle:lifecycle-common:2.8.5 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.5 -androidx.lifecycle:lifecycle-livedata-core:2.8.5 -androidx.lifecycle:lifecycle-livedata-ktx:2.8.5 -androidx.lifecycle:lifecycle-livedata:2.8.5 -androidx.lifecycle:lifecycle-process:2.8.5 -androidx.lifecycle:lifecycle-runtime-android:2.8.5 -androidx.lifecycle:lifecycle-runtime-compose-android:2.8.5 -androidx.lifecycle:lifecycle-runtime-compose:2.8.5 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.5 -androidx.lifecycle:lifecycle-runtime-ktx:2.8.5 -androidx.lifecycle:lifecycle-runtime:2.8.5 -androidx.lifecycle:lifecycle-service:2.8.5 -androidx.lifecycle:lifecycle-viewmodel-android:2.8.5 -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.5 -androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.5 -androidx.lifecycle:lifecycle-viewmodel:2.8.5 +androidx.lifecycle:lifecycle-common-java8:2.8.6 +androidx.lifecycle:lifecycle-common-jvm:2.8.6 +androidx.lifecycle:lifecycle-common:2.8.6 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.6 +androidx.lifecycle:lifecycle-livedata-core:2.8.6 +androidx.lifecycle:lifecycle-livedata-ktx:2.8.6 +androidx.lifecycle:lifecycle-livedata:2.8.6 +androidx.lifecycle:lifecycle-process:2.8.6 +androidx.lifecycle:lifecycle-runtime-android:2.8.6 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.6 +androidx.lifecycle:lifecycle-runtime-compose:2.8.6 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.6 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 +androidx.lifecycle:lifecycle-runtime:2.8.6 +androidx.lifecycle:lifecycle-service:2.8.6 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.6 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.6 +androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 +androidx.lifecycle:lifecycle-viewmodel:2.8.6 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.1.0 -androidx.navigation:navigation-common-ktx:2.8.0 -androidx.navigation:navigation-common:2.8.0 -androidx.navigation:navigation-compose:2.8.0 -androidx.navigation:navigation-fragment:2.8.0 -androidx.navigation:navigation-runtime-ktx:2.8.0 -androidx.navigation:navigation-runtime:2.8.0 -androidx.navigation:navigation-ui:2.8.0 +androidx.navigation:navigation-common-ktx:2.8.3 +androidx.navigation:navigation-common:2.8.3 +androidx.navigation:navigation-compose:2.8.3 +androidx.navigation:navigation-fragment:2.8.3 +androidx.navigation:navigation-runtime-ktx:2.8.3 +androidx.navigation:navigation-runtime:2.8.3 +androidx.navigation:navigation-ui:2.8.3 androidx.preference:preference:1.2.1 androidx.print:print:1.0.0 androidx.profileinstaller:profileinstaller:1.3.1 @@ -156,7 +156,6 @@ com.mikepenz:fastadapter-extensions-expandable:5.7.0 com.mikepenz:fastadapter-extensions-swipe:5.7.0 com.mikepenz:fastadapter-extensions-utils:5.7.0 com.mikepenz:fastadapter:5.7.0 -com.mikepenz:materialdrawer:9.0.2 com.squareup.moshi:moshi:1.15.1 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.9.0 @@ -188,19 +187,19 @@ org.apache.httpcomponents.core5:httpcore5:5.2.4 org.apache.james:apache-mime4j-core:0.8.9 org.apache.james:apache-mime4j-dom:0.8.9 org.jetbrains.compose.runtime:runtime:1.5.12 -org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.0.20 -org.jetbrains.kotlin:kotlin-bom:2.0.20 -org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 -org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.20 -org.jetbrains.kotlin:kotlin-stdlib:2.0.20 +org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.0.21 +org.jetbrains.kotlin:kotlin-bom:2.0.21 +org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.8 org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3 @@ -208,7 +207,7 @@ org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3 org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 -org.jetbrains:annotations:24.1.0 +org.jetbrains:annotations:26.0.1 org.jsoup:jsoup:1.17.2 org.minidns:minidns-client:1.0.5 org.minidns:minidns-core:1.0.5 diff --git a/app-k9mail/src/main/kotlin/app/k9mail/K9KoinModule.kt b/app-k9mail/src/main/kotlin/app/k9mail/K9KoinModule.kt index 941d77ca8df..8fbde70c231 100644 --- a/app-k9mail/src/main/kotlin/app/k9mail/K9KoinModule.kt +++ b/app-k9mail/src/main/kotlin/app/k9mail/K9KoinModule.kt @@ -3,11 +3,13 @@ package app.k9mail import app.k9mail.auth.K9OAuthConfigurationFactory import app.k9mail.core.common.oauth.OAuthConfigurationFactory import app.k9mail.core.common.provider.AppNameProvider +import app.k9mail.core.common.provider.BrandNameProvider import app.k9mail.core.featureflag.FeatureFlagFactory import app.k9mail.core.ui.theme.api.FeatureThemeProvider import app.k9mail.core.ui.theme.api.ThemeProvider import app.k9mail.dev.developmentModuleAdditions import app.k9mail.feature.funding.featureFundingModule +import app.k9mail.feature.onboarding.migration.onboardingMigrationModule import app.k9mail.feature.telemetry.telemetryModule import app.k9mail.feature.widget.shortcut.LauncherShortcutActivity import app.k9mail.featureflag.K9FeatureFlagFactory @@ -22,18 +24,20 @@ import com.fsck.k9.provider.UnreadWidgetProvider import com.fsck.k9.widget.list.MessageListWidgetProvider import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named +import org.koin.dsl.binds import org.koin.dsl.module val appModule = module { includes(appWidgetModule) includes(telemetryModule) includes(featureFundingModule) + includes(onboardingMigrationModule) single(named("ClientInfoAppName")) { BuildConfig.CLIENT_INFO_APP_NAME } single(named("ClientInfoAppVersion")) { BuildConfig.VERSION_NAME } single { appConfig } single { K9OAuthConfigurationFactory() } - single { K9AppNameProvider(androidContext()) } + single { K9AppNameProvider(androidContext()) } binds arrayOf(AppNameProvider::class, BrandNameProvider::class) single { K9ThemeProvider() } single { K9FeatureThemeProvider() } single { K9FeatureFlagFactory() } diff --git a/app-k9mail/src/main/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt b/app-k9mail/src/main/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt index 6ce0002e09b..bf3b6ad3eeb 100644 --- a/app-k9mail/src/main/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt +++ b/app-k9mail/src/main/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt @@ -2,12 +2,9 @@ package app.k9mail.featureflag import app.k9mail.core.featureflag.FeatureFlag import app.k9mail.core.featureflag.FeatureFlagFactory -import app.k9mail.core.featureflag.FeatureFlagKey class K9FeatureFlagFactory : FeatureFlagFactory { override fun createFeatureCatalog(): List { - return listOf( - FeatureFlag(FeatureFlagKey("material3_navigation_drawer"), false), - ) + return listOf() } } diff --git a/app-k9mail/src/main/kotlin/app/k9mail/provider/K9AppNameProvider.kt b/app-k9mail/src/main/kotlin/app/k9mail/provider/K9AppNameProvider.kt index 7e4d095d979..ebbf468f3df 100644 --- a/app-k9mail/src/main/kotlin/app/k9mail/provider/K9AppNameProvider.kt +++ b/app-k9mail/src/main/kotlin/app/k9mail/provider/K9AppNameProvider.kt @@ -2,12 +2,17 @@ package app.k9mail.provider import android.content.Context import app.k9mail.core.common.provider.AppNameProvider +import app.k9mail.core.common.provider.BrandNameProvider import com.fsck.k9.R class K9AppNameProvider( context: Context, -) : AppNameProvider { +) : AppNameProvider, BrandNameProvider { override val appName: String by lazy { context.getString(R.string.app_name) } + + override val brandName: String by lazy { + context.getString(R.string.app_name) + } } diff --git a/app-k9mail/src/main/res/values-enm/strings.xml b/app-k9mail/src/main/res/values-enm/strings.xml new file mode 100644 index 00000000000..a6b3daec935 --- /dev/null +++ b/app-k9mail/src/main/res/values-enm/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-k9mail/src/main/res/values-kab/strings.xml b/app-k9mail/src/main/res/values-kab/strings.xml new file mode 100644 index 00000000000..5f57a58706e --- /dev/null +++ b/app-k9mail/src/main/res/values-kab/strings.xml @@ -0,0 +1,4 @@ + + + Imayl K-9 + \ No newline at end of file diff --git a/app-k9mail/src/main/res/values-kk/strings.xml b/app-k9mail/src/main/res/values-kk/strings.xml new file mode 100644 index 00000000000..a6b3daec935 --- /dev/null +++ b/app-k9mail/src/main/res/values-kk/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-k9mail/src/main/res/values-pt/strings.xml b/app-k9mail/src/main/res/values-pt/strings.xml new file mode 100644 index 00000000000..a6b3daec935 --- /dev/null +++ b/app-k9mail/src/main/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-k9mail/src/main/res/values-zh-rCN/strings.xml b/app-k9mail/src/main/res/values-zh-rCN/strings.xml index 58052439ded..f7944fb85d2 100644 --- a/app-k9mail/src/main/res/values-zh-rCN/strings.xml +++ b/app-k9mail/src/main/res/values-zh-rCN/strings.xml @@ -1,4 +1,4 @@ - K-9 邮件 + K-9 Mail \ No newline at end of file diff --git a/app-k9mail/src/main/res/values/themes.xml b/app-k9mail/src/main/res/values/themes.xml index 4ba92509fdf..dcb8bc77f85 100644 --- a/app-k9mail/src/main/res/values/themes.xml +++ b/app-k9mail/src/main/res/values/themes.xml @@ -46,9 +46,6 @@ #336699 #bbb #888 - - @style/Widget.MaterialDrawerStyle.K9.Light - @style/Widget.MaterialDrawerHeaderStyle.K9.Light - - - - - - - - - - - - diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt deleted file mode 100644 index fa5a3fb2f8d..00000000000 --- a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/K9DrawerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.fsck.k9.ui - -import app.k9mail.core.android.testing.RobolectricTest -import assertk.assertThat -import assertk.assertions.isEqualTo -import assertk.assertions.size -import com.fsck.k9.core.R -import org.junit.Test -import org.robolectric.RuntimeEnvironment - -class K9DrawerTest : RobolectricTest() { - @Test - fun testAccountColorLengthEqualsDrawerColorLength() { - val resources = RuntimeEnvironment.getApplication().resources - - val lightColors = resources.getIntArray(R.array.account_colors) - val darkColors = resources.getIntArray(R.array.drawer_account_accent_color_dark_theme) - - assertThat(darkColors).size().isEqualTo(lightColors.size) - } -} diff --git a/scripts/render-notes.py b/scripts/render-notes.py new file mode 100644 index 00000000000..bcf2ae09b27 --- /dev/null +++ b/scripts/render-notes.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +import argparse +import os +import requests +import yaml + +from jinja2 import Template + + +def render_notes( + version, + versioncode, + application, + applicationid, + printonly=False, + notesrepo="thunderbird/thunderbird-notes", + notesbranch="master", +): + """Update changelog files based on release notes from thunderbird-notes.""" + tb_notes_filename = f"{version}.yml" + tb_notes_directory = "android_release" + if "0b" in version: + tb_notes_filename = f"{version[0:-1]}eta.yml" + tb_notes_directory = "android_beta" + tb_notes_url = os.path.join( + f"https://raw.githubusercontent.com/{notesrepo}/", + f"refs/heads/{notesbranch}", + tb_notes_directory, + tb_notes_filename, + ) + + response = requests.get(tb_notes_url) + response.raise_for_status() + yaml_content = yaml.safe_load(response.text) + + render_data = {"releases": {}} + for release in reversed(yaml_content["release"]["releases"]): + vers = release["version"] + render_data["releases"][vers] = {} + render_data["releases"][vers]["version"] = vers + render_data["releases"][vers]["versioncode"] = int(versioncode) + render_data["releases"][vers]["application"] = application + render_data["releases"][vers]["date"] = release["release_date"] + render_data["releases"][vers]["changes"] = [] + for note in yaml_content["notes"]: + if "0b" in version: + if note["group"] == int(vers[-1]): + render_data["releases"][vers]["changes"].append(note["note"]) + else: + render_data["releases"][vers]["changes"].append(note["note"]) + + render_files = { + "changelog_master": { + "template": "./scripts/templates/changelog_master.xml", + "outfile": f"./app-{application}/src/main/res/raw/changelog_master.xml", + "render_data": render_data["releases"][version], + }, + "changelog.txt": { + "template": "./scripts/templates/changelog.txt", + "outfile": f"./app-metadata/{applicationid}/en-US/changelogs/{versioncode}.txt", + "render_data": render_data["releases"][version], + }, + } + + for render_file in render_files: + with open(render_files[render_file]["template"], "r") as file: + template = file.read() + template = Template(template) + rendered = template.render(render_files[render_file]["render_data"]) + if render_file == "changelog_master": + with open(render_files[render_file]["outfile"], "r") as file: + lines = file.readlines() + for index, line in enumerate(lines): + if "" in line: + if version in lines[index + 1]: + break + lines.insert(index + 1, rendered) + break + if not printonly: + with open(render_files[render_file]["outfile"], "w") as file: + file.writelines(lines) + elif render_file == "changelog.txt": + stripped = rendered.lstrip() + if not printonly: + with open(render_files[render_file]["outfile"], "x") as file: + file.write(stripped) + print(stripped) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--print", + "-p", + action="store_true", + help="Only print the processed release notes", + ) + parser.add_argument( + "--repository", + "-r", + default="thunderbird/thunderbird-notes", + help="Repository to retrieve thunderbird-notes from", + ) + parser.add_argument( + "--branch", + "-b", + default="master", + help="Branch to retrieve thunderbird-notes from", + ) + parser.add_argument( + "applicationid", + type=str, + choices=[ + "net.thunderbird.android", + "net.thunderbird.android.beta", + "com.fsck.k9", + ], + help="thunderbird or k9mail", + ) + parser.add_argument("version", type=str, help="Version name for this release") + parser.add_argument("versioncode", type=str, help="Version code for this release") + args = parser.parse_args() + + if args.applicationid == "com.fsck.k9": + application = "k9mail" + else: + application = "thunderbird" + + render_notes( + args.version, + args.versioncode, + application, + args.applicationid, + printonly=args.print, + notesrepo=args.repository, + notesbranch=args.branch, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/setup_release_automation b/scripts/setup_release_automation index 55adc6beba9..60d0cb96f07 100644 --- a/scripts/setup_release_automation +++ b/scripts/setup_release_automation @@ -1,6 +1,7 @@ #!/usr/bin/env python # See docs/CI/Release_Automation.md for more details +# Run this from the .signing directory with all the keys and properties files in it. # python -m venv venv; source venv/bin/activate; pip install requests, pynacl @@ -22,15 +23,23 @@ CHANNEL_ENVIRONMENTS = { "MATRIX_INCLUDE": [ { "appName": "thunderbird", + "releaseTarget": "github", "packageFormat": "apk", "packageFlavor": "foss", }, { "appName": "thunderbird", - "packageFormat": "bundle", + "releaseTarget": "play", + "playTargetTrack": "internal", + "packageFormat": "aab", "packageFlavor": "full", }, - {"appName": "k9mail", "packageFormat": "apk"}, + { + "appName": "k9mail", + "releaseTarget": "github|play", + "playTargetTrack": "internal", + "packageFormat": "apk", + }, ], }, }, @@ -41,15 +50,23 @@ CHANNEL_ENVIRONMENTS = { "MATRIX_INCLUDE": [ { "appName": "thunderbird", + "releaseTarget": "github", "packageFormat": "apk", "packageFlavor": "foss", }, { "appName": "thunderbird", - "packageFormat": "bundle", + "releaseTarget": "play", + "playTargetTrack": "internal", + "packageFormat": "aab", "packageFlavor": "full", }, - {"appName": "k9mail", "packageFormat": "apk"}, + { + "appName": "k9mail", + "releaseTarget": "github|play", + "playTargetTrack": "internal", + "packageFormat": "apk", + }, ], }, }, @@ -65,7 +82,9 @@ CHANNEL_ENVIRONMENTS = { }, { "appName": "thunderbird", - "packageFormat": "bundle", + "releaseTarget": "play", + "packageFormat": "aab", + "playTargetTrack": "internal", "packageFlavor": "full", }, ], @@ -75,46 +94,38 @@ CHANNEL_ENVIRONMENTS = { SIGNING_ENVIRONMENTS = { - "k9mail_release_default": [ - "k9.release.signing.properties", - "k9-release-signing.jks", - "release", - ], - "k9mail_beta_default": [ - "k9.release.signing.properties", - "k9-release-signing.jks", - "beta", - ], - "thunderbird_daily_foss": [ - "tb.daily.signing.properties", - "tb-daily-signing.jks", - "daily", - ], - "thunderbird_daily_full": [ - "tb.daily.upload.properties", - "tb-daily-upload-01.jks", - "daily", - ], - "thunderbird_beta_foss": [ - "tb.beta.signing.properties", - "tb-beta-signing.jks", - "beta", - ], - "thunderbird_beta_full": [ - "tb.beta.upload.properties", - "tb-beta-upload-01.jks", - "beta", - ], - "thunderbird_release_foss": [ - "tb.release.signing.properties", - "tb-release-signing.jks", - "release", - ], - "thunderbird_release_full": [ - "tb.release.upload.properties", - "tb-release-upload-01.jks", - "release", - ], + "k9mail_release_default": { + "props": "k9.release.signing.properties", + "branch": "release", + }, + "k9mail_beta_default": { + "props": "k9.release.signing.properties", + "branch": "beta", + }, + "thunderbird_daily_foss": { + "props": "tb.daily.signing.properties", + "branch": "main", + }, + "thunderbird_daily_full": { + "props": "tb.daily.upload.properties", + "branch": "main", + }, + "thunderbird_beta_foss": { + "props": "tb.beta.signing.properties", + "branch": "beta", + }, + "thunderbird_beta_full": { + "props": "tb.beta.upload.properties", + "branch": "beta", + }, + "thunderbird_release_foss": { + "props": "tb.release.signing.properties", + "branch": "release", + }, + "thunderbird_release_full": { + "props": "tb.release.upload.properties", + "branch": "release", + }, } @@ -210,14 +221,14 @@ def set_github_environment_variable(repo, name, value, environment_name): # Function to create GitHub environment if it doesn't exist -def create_github_environment(repo, environment_name, branch=None, approvers=None): +def create_github_environment(repo, environment_name, branches=None, approvers=None): url = f"https://api.github.com/repos/{repo}/environments/{environment_name}" headers = { "Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json", } data = {} - if branch: + if branches: data["deployment_branch_policy"] = { "custom_branch_policies": True, "protected_branches": False, @@ -243,22 +254,22 @@ def create_github_environment(repo, environment_name, branch=None, approvers=Non f"Failed to create environment {environment_name}. Response: {response.status_code}, {response.text}" ) - if branch: + for branch in branches or []: url = f"https://api.github.com/repos/{repo}/environments/{environment_name}/deployment-branch-policies" data = {"name": branch, "type": "branch"} response = requests.post(url, headers=headers, json=data) if response.status_code == 200: print( - f"\tEnvironment branch protection for {environment_name} created successfully." + f"\tBranch protection on {branch} for {environment_name} created successfully." ) elif response.status_code == 409: print( - f"\tEnvironment branch protection for {environment_name} already exists." + f"\tBranch protection on {branch} for {environment_name} already exists." ) else: raise Exception( - f"Failed to create environment {environment_name}. Response: {response.status_code}, {response.text}" + f"Failed to create branch protection for {branch} on {environment_name}. Response: {response.status_code}, {response.text}" ) @@ -306,7 +317,7 @@ def create_approver_environment(repo, environment_name, approvers): ) -def create_signing_environment(repo, environment, branch, props_file, jks_file): +def create_signing_environment(repo, environment, branch, props_file): # Read the key.properties file key_props = read_key_properties(props_file) @@ -321,10 +332,10 @@ def create_signing_environment(repo, environment, branch, props_file, jks_file): return # Base64 encode the JKS file to create SIGNING_KEY - SIGNING_KEY = encode_jks_file(jks_file) + SIGNING_KEY = encode_jks_file(key_props.get("storeFile")) # Create the environment if it doesn't exist - create_github_environment(repo, environment, branch=branch) + create_github_environment(repo, environment, branches=[branch]) # Fetch the public key from GitHub for the specific environment public_key_data = get_github_public_key(repo, environment) @@ -351,22 +362,41 @@ def create_signing_environment(repo, environment, branch, props_file, jks_file): ) +def create_release_environment(repo, branches): + environment = "publish_release" + + create_github_environment(repo, environment, branches=branches) + + public_key_data = get_github_public_key(repo, environment) + public_key = public_key_data["key"] + key_id = public_key_data["key_id"] + + with open("play-store-account.json") as fp: + encrypted_play_account = encrypt_secret(public_key, fp.read()) + + set_github_environment_secret( + repo, "PLAY_STORE_ACCOUNT", encrypted_play_account, key_id, environment + ) + + with open("thunderbird-mobile-gh-releaser-bot.pem") as fp: + encrypted_releaser_key = encrypt_secret(public_key, fp.read()) + with open("thunderbird-mobile-gh-releaser-bot.clientid.txt") as fp: + releaser_client_id = fp.read().strip() + + set_github_environment_secret( + repo, "RELEASER_APP_PRIVATE_KEY", encrypted_releaser_key, key_id, environment + ) + + set_github_environment_variable( + repo, "RELEASER_APP_CLIENT_ID", releaser_client_id, environment + ) + + def main(): # Argument parsing for positional inputs and repo flag parser = argparse.ArgumentParser( description="Set GitHub environment secrets for specific or all environments." ) - parser.add_argument( - "--props", - "-p", - help="Path to the key.properties file (for single environment).", - ) - parser.add_argument( - "--jks", "-j", help="Path to the .jks keystore file (for single environment)." - ) - parser.add_argument( - "--environment", "-e", help="GitHub environment name (for single environment)." - ) parser.add_argument( "--repo", "-r", @@ -374,14 +404,10 @@ def main(): help="GitHub repository in the format 'owner/repo'.", ) parser.add_argument( - "--all-environments", - "-a", - action="store_true", - help="Create all environments based on predefined paths and rules.", + "--skip", "-s", action="append", help="Skip this named environment" ) - parser.add_argument("--branch", "-b", help="Branch to limit the environment to") parser.add_argument( - "--skip", "-s", action="append", help="In all mode, skip this environment", default=[] + "--only", "-o", action="append", help="Only include this named environment" ) args = parser.parse_args() @@ -393,62 +419,62 @@ def main(): "GITHUB_TOKEN environment variable is not set. Please set it before running the script." ) - if args.all_environments: - skipset = set(args.skip) - # All environments creation mode - if "publish_hold" in skipset: - print("Skipping environment publish_hold") - else: - create_github_environment( - args.repo, "publish_hold", approvers=PUBLISH_APPROVERS - ) + if args.skip and args.only: + print("Error: Cannot supply both skip and only") + return - # Channel environments - for environment_name, data in CHANNEL_ENVIRONMENTS.items(): - if environment_name in skipset: - print(f"Skipping channel environment {environment_name}") - continue + includeset = set( + list(CHANNEL_ENVIRONMENTS.keys()) + + list(SIGNING_ENVIRONMENTS.keys()) + + [ + "publish_hold", + "publish_release", + ] + ) + if args.skip: + for skip in args.skip: + includeset.remove(skip) - create_github_environment( - args.repo, environment_name, branch=data["branch"] - ) + if args.only: + includeset = set(args.only) - for name, value in data["variables"].items(): - if isinstance(value, dict) or isinstance(value, list): - value = json.dumps(value) - set_github_environment_variable( - args.repo, name, value, environment_name - ) - - # Signing environments - for environment_name, paths in SIGNING_ENVIRONMENTS.items(): - if environment_name in skipset: - print(f"Skipping signing environment {environment_name}") - continue - - props_file, jks_file, branch = paths - - if not os.path.exists(props_file) or not os.path.exists(jks_file): - print( - f"Skipping {environment_name}: Missing key.properties or .jks file." - ) - continue - - create_signing_environment( - args.repo, environment_name, branch, props_file, jks_file - ) - else: - # Single environment creation mode - if not all([args.props, args.jks, args.environment, args.branch]): - print( - "Error: You must provide --props, --jks, and --environment for single environment creation." - ) - return + # Publish hold environment + if "publish_hold" in includeset: + create_github_environment( + args.repo, "publish_hold", approvers=PUBLISH_APPROVERS + ) + + # Channel environments + for environment_name, data in CHANNEL_ENVIRONMENTS.items(): + if environment_name not in includeset: + continue + + create_github_environment( + args.repo, environment_name, branches=[data["branch"]] + ) + + for name, value in data["variables"].items(): + if isinstance(value, dict) or isinstance(value, list): + value = json.dumps(value) + set_github_environment_variable(args.repo, name, value, environment_name) + + # Signing environments + for environment_name, data in SIGNING_ENVIRONMENTS.items(): + if environment_name not in includeset: + continue + + if not os.path.exists(data["props"]): + print(f"Skipping {environment_name}: Missing key .properties file") + continue create_signing_environment( - args.repo, args.environment, args.branch, args.props, args.jks + args.repo, environment_name, data["branch"], data["props"] ) + # Publish environment + if "publish_release" in includeset: + create_release_environment(args.repo, ["main", "beta", "release"]) + if __name__ == "__main__": main() diff --git a/scripts/templates/changelog.txt b/scripts/templates/changelog.txt new file mode 100644 index 00000000000..efa569e59ba --- /dev/null +++ b/scripts/templates/changelog.txt @@ -0,0 +1,7 @@ +{%- if application == 'thunderbird' -%} +Thunderbird for Android version {{ version }}, based on K-9 Mail. Changes include: +{%- endif -%} +{%- for note in changes %} +- {{ note }} +{%- endfor %} + diff --git a/scripts/templates/changelog_master.xml b/scripts/templates/changelog_master.xml new file mode 100644 index 00000000000..ce2034375d5 --- /dev/null +++ b/scripts/templates/changelog_master.xml @@ -0,0 +1,6 @@ + +{%- for note in changes %} + {{ note }} +{%- endfor %} + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 29448a18885..a49d4994648 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,9 @@ include( ":feature:onboarding:main", ":feature:onboarding:welcome", ":feature:onboarding:permissions", + ":feature:onboarding:migration:api", + ":feature:onboarding:migration:thunderbird", + ":feature:onboarding:migration:noop", ) include(