diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7f35d2cb1..c3847e761 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "dotnet-format" ] + }, + "cake.tool": { + "version": "2.2.0", + "commands": [ + "dotnet-cake" + ] } } } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 096b69d47..ea0f17c95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,10 @@ jobs: name: Android runs-on: windows-2022 needs: setup + strategy: + fail-fast: false + matrix: + variant: ['prod', 'qa'] steps: - name: Setup NuGet uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6 @@ -67,7 +71,7 @@ jobs: - name: Set up MSBuild uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab - + - name: Work Around for broken Windows 2022 Runner Image run: | Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\" @@ -87,7 +91,6 @@ jobs: Write-Host "components were not installed" exit 1 } - - name: Print environment run: | nuget help | grep Version @@ -98,7 +101,8 @@ jobs: - name: Checkout repo uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - + with: + fetch-depth: 0 - name: Decrypt secrets env: DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} @@ -109,12 +113,17 @@ jobs: --output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg - gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - --output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg shell: bash - + - name: Decrypt secrets - Google Services + if: ${{ matrix.variant == 'prod' }} + env: + DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} + run: | + gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ + --output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg + shell: bash - name: Increment version run: | BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER)) @@ -142,26 +151,35 @@ jobs: run: dotnet test test/Core.Test/Core.Test.csproj - name: Build Play Store publisher + if: ${{ matrix.variant == 'prod' }} run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release - - name: Build for Play Store + - name: Setup Android build (${{ matrix.variant }}) + run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }} + + - name: Build Android run: | $configuration = "Release"; Write-Output "########################################" Write-Output "##### Build $configuration Configuration" Write-Output "########################################" - msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration" + shell: pwsh - - name: Sign for Play Store + - name: Sign Android Build env: PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} run: | $androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj"); - + $packageName = "com.x8bit.bitwarden"; + + if ("${{ matrix.variant }}" -ne "prod") + { + $packageName = "com.x8bit.bitwarden.${{ matrix.variant }}"; + } Write-Output "########################################" Write-Output "##### Sign Google Play Bundle Release Configuration" Write-Output "########################################" @@ -175,9 +193,8 @@ jobs: Write-Output "##### Copy Google Play Bundle to project root" Write-Output "########################################" - $signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab"); - $signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab"); - + $signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab"); + $signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab"); Copy-Item $signedAabPath $signedAabDestPath Write-Output "########################################" @@ -193,33 +210,41 @@ jobs: Write-Output "##### Copy Release APK to project root" Write-Output "########################################" - $signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk"); - $signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk"); + $signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk"); + $signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk"); Copy-Item $signedApkPath $signedApkDestPath shell: pwsh - - - name: Upload Play Store .aab artifact + - name: Upload Prod .aab artifact + if: ${{ matrix.variant == 'prod' }} uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.aab path: ./com.x8bit.bitwarden.aab if-no-files-found: error - - name: Upload Play Store .apk artifact + - name: Upload Prod .apk artifact + if: ${{ matrix.variant == 'prod' }} uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.apk path: ./com.x8bit.bitwarden.apk if-no-files-found: error + - name: Upload Other .apk artifact + if: ${{ matrix.variant != 'prod' }} + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 + with: + name: com.x8bit.bitwarden.${{ matrix.variant }}.apk + path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk + if-no-files-found: error + - name: Deploy to Play Store - if: | - (github.ref == 'refs/heads/master' - && needs.setup.outputs.rc_branch_exists == 0 - && needs.setup.outputs.hotfix_branch_exists == 0) - || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) - || github.ref == 'refs/heads/hotfix-rc' + if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master' + && needs.setup.outputs.rc_branch_exists == 0 + && needs.setup.outputs.hotfix_branch_exists == 0) + || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) + || github.ref == 'refs/heads/hotfix-rc' ) }} run: | PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll" CREDS_PATH="$HOME/secrets/play_creds.json" diff --git a/.gitignore b/.gitignore index 383caf2b4..67fa6064c 100644 --- a/.gitignore +++ b/.gitignore @@ -208,4 +208,5 @@ FakesAssemblies/ # Other project.lock.json .DS_Store -src/App/Css \ No newline at end of file +src/App/Css +tools diff --git a/build.cake b/build.cake new file mode 100644 index 000000000..18aa49154 --- /dev/null +++ b/build.cake @@ -0,0 +1,345 @@ +#addin nuget:?package=Cake.FileHelpers&version=5.0.0 +#addin nuget:?package=Cake.AndroidAppManifest&version=1.1.2 +#addin nuget:?package=Cake.Plist&version=0.7.0 +#addin nuget:?package=Cake.Incubator&version=7.0.0 +#tool dotnet:?package=GitVersion.Tool&version=5.10.3 +using Path = System.IO.Path; + +var debugScript = Argument("debugScript", false); +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); +var variant = Argument("variant", "dev"); + +abstract record VariantConfig( + string AppName, + string AndroidPackageName, + string iOSBundleId, + string ApsEnvironment + ); + +const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden"; +const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden"; + +record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development"); +record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development"); +record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production"); +record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production"); + +VariantConfig GetVariant() => variant.ToLower() switch{ + "qa" => new QA(), + "beta" => new Beta(), + "prod" => new Prod(), + _ => new Dev() +}; + +GitVersion _gitVersion; //will be set by GetGitInfo task +var _slnPath = Path.Combine(""); //base path used to access files. If build.cake file is moved, just update this +string _androidPackageName = string.Empty; //will be set by UpdateAndroidManifest task +string CreateFeatureBranch(string prevVersionName, GitVersion git) => $"{prevVersionName}-{git.BranchName.Replace("/","-")}"; +string GetVersionName(string prevVersionName, VariantConfig buildVariant, GitVersion git) => buildVariant is Prod? prevVersionName : CreateFeatureBranch(prevVersionName, git); +int CreateBuildNumber(int previousNumber) => ++previousNumber; + +Task("GetGitInfo") + .Does(()=> { + _gitVersion = GitVersion(new GitVersionSettings()); + + if(debugScript) + { + Information($"GitVersion Dump:\n{_gitVersion.Dump()}"); + } + + Information("Git data Load successfully."); + }); + +#region Android +Task("UpdateAndroidAppIcon") + .Does(()=>{ + //TODO we'll implement variant icons later + //manifest.ApplicationIcon = "@mipmap/ic_launcher"; + Information($"Updated Androix App Icon with success"); + }); + + +Task("UpdateAndroidManifest") + .IsDependentOn("GetGitInfo") + .Does(()=> + { + var buildVariant = GetVariant(); + var manifestPath = Path.Combine(_slnPath, "src", "Android", "Properties", "AndroidManifest.xml"); + + // Cake.AndroidAppManifest doesn't currently enable us to access nested items so, quick (not ideal) fix: + var manifestText = FileReadText(manifestPath); + manifestText = manifestText.Replace("com.x8bit.bitwarden.", buildVariant.AndroidPackageName + "."); + manifestText = manifestText.Replace("android:label=\"Bitwarden\"", $"android:label=\"{buildVariant.AppName}\""); + FileWriteText(manifestPath, manifestText); + + var manifest = DeserializeAppManifest(manifestPath); + + var prevVersionCode = manifest.VersionCode; + var prevVersionName = manifest.VersionName; + _androidPackageName = manifest.PackageName; + + //manifest.VersionCode = CreateBuildNumber(prevVersionCode); + manifest.VersionName = GetVersionName(prevVersionName, buildVariant, _gitVersion); + manifest.PackageName = buildVariant.AndroidPackageName; + manifest.ApplicationLabel = buildVariant.AppName; + + //Information($"AndroidManigest.xml VersionCode from {prevVersionCode} to {manifest.VersionCode}"); + Information($"AndroidManigest.xml VersionName from {prevVersionName} to {manifest.VersionName}"); + Information($"AndroidManigest.xml PackageName from {_androidPackageName} to {buildVariant.AndroidPackageName}"); + Information($"AndroidManigest.xml ApplicationLabel to {buildVariant.AppName}"); + + SerializeAppManifest(manifestPath, manifest); + + Information("AndroidManifest updated with success!"); + }); + +void ReplaceInFile(string filePath, string oldtext, string newtext) +{ + var fileText = FileReadText(filePath); + + if(string.IsNullOrEmpty(fileText) || !fileText.Contains(oldtext)) + { + throw new Exception($"Couldn't find {filePath} or it didn't contain: {oldtext}"); + } + + fileText = fileText.Replace(oldtext, newtext); + + FileWriteText(filePath, fileText); + Information($"{filePath} modified successfully."); +} + +Task("UpdateAndroidCodeFiles") + .IsDependentOn("UpdateAndroidManifest") + .Does(()=> { + var buildVariant = GetVariant(); + + //We're not using _androidPackageName here because the codefile is currently slightly different string than the one in AndroidManifest.xml + var keyName = "com.8bit.bitwarden"; + var fixedPackageName = buildVariant.AndroidPackageName.Replace("x8bit", "8bit"); + var filePath = Path.Combine(_slnPath, "src", "Android", "Services", "BiometricService.cs"); + ReplaceInFile(filePath, keyName, fixedPackageName); + + var packageFileList = new string[] { + Path.Combine(_slnPath, "src", "Android", "MainActivity.cs"), + Path.Combine(_slnPath, "src", "Android", "MainApplication.cs"), + Path.Combine(_slnPath, "src", "Android", "Constants.cs"), + Path.Combine(_slnPath, "src", "Android", "Accessibility", "AccessibilityService.cs"), + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillHelpers.cs"), + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "ClearClipboardAlarmReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "EventUploadReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "PackageReplacedReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "RestrictionsChangedReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Services", "DeviceActionService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "AutofillTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "GeneratorTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "MyVaultTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "google-services.json"), + Path.Combine(_slnPath, "store", "google", "Publisher", "Program.cs"), + }; + + foreach(string path in packageFileList) + { + ReplaceInFile(path, "com.x8bit.bitwarden", buildVariant.AndroidPackageName); + } + + var labelFileList = new string[] { + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"), + }; + + foreach(string path in labelFileList) + { + ReplaceInFile(path, "Bitwarden\"", $"{buildVariant.AppName}\""); + } + }); +#endregion Android + +#region iOS +enum iOSProjectType +{ + Null, + MainApp, + Autofill, + Extension, + ShareExtension +} + +string GetiOSBundleId(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch +{ + iOSProjectType.Autofill => $"{buildVariant.iOSBundleId}.autofill", + iOSProjectType.Extension => $"{buildVariant.iOSBundleId}.find-login-action-extension", + iOSProjectType.ShareExtension => $"{buildVariant.iOSBundleId}.share-extension", + _ => buildVariant.iOSBundleId +}; + +string GetiOSBundleName(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch +{ + iOSProjectType.Autofill => $"{buildVariant.AppName} Autofill", + iOSProjectType.Extension => $"{buildVariant.AppName} Extension", + iOSProjectType.ShareExtension => $"{buildVariant.AppName} Share Extension", + _ => buildVariant.AppName +}; + +private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, GitVersion git, iOSProjectType projectType = iOSProjectType.MainApp) +{ + var plistFile = File(plistPath); + dynamic plist = DeserializePlist(plistFile); + + var prevVersionName = plist["CFBundleShortVersionString"]; + var prevVersionString = plist["CFBundleVersion"]; + var prevVersion = int.Parse(plist["CFBundleVersion"]); + var prevBundleId = plist["CFBundleIdentifier"]; + var prevBundleName = plist["CFBundleName"]; + //var newVersion = CreateBuildNumber(prevVersion).ToString(); + var newVersionName = GetVersionName(prevVersionName, buildVariant, git); + var newBundleId = GetiOSBundleId(buildVariant, projectType); + var newBundleName = GetiOSBundleName(buildVariant, projectType); + + plist["CFBundleName"] = newBundleName; + plist["CFBundleDisplayName"] = newBundleName; + //plist["CFBundleVersion"] = newVersion; + plist["CFBundleShortVersionString"] = newVersionName; + plist["CFBundleIdentifier"] = newBundleId; + + if(projectType == iOSProjectType.MainApp) + { + plist["CFBundleURLTypes"][0]["CFBundleURLName"] = $"{buildVariant.iOSBundleId}.url"; + } + + if(projectType == iOSProjectType.Extension) + { + var keyText = plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"]; + plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId); + } + + SerializePlist(plistFile, plist); + + Information($"Changed app name from {prevBundleName} to {newBundleName}"); + //Information($"Changed Bundle Version from {prevVersion} to {newVersion}"); + Information($"Changed Bundle Short Version name from {prevVersionName} to {newVersionName}"); + Information($"Changed Bundle Identifier from {prevBundleId} to {newBundleId}"); + Information($"{plistPath} updated with success!"); +} + +private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant) +{ + var EntitlementlistFile = File(entitlementsPath); + dynamic Entitlements = DeserializePlist(EntitlementlistFile); + + Entitlements["aps-environment"] = buildVariant.ApsEnvironment; + Entitlements["keychain-access-groups"] = new List() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId }; + Entitlements["com.apple.security.application-groups"] = new List() { $"group.{buildVariant.iOSBundleId}" };; + + Information($"Changed ApsEnvironment name to {buildVariant.ApsEnvironment}"); + Information($"Changed keychain-access-groups bundleID to {buildVariant.iOSBundleId}"); + + SerializePlist(EntitlementlistFile, Entitlements); + + Information($"{entitlementsPath} updated with success!"); +} + +Task("UpdateiOSIcon") + .Does(()=>{ + //TODO we'll implement variant icons later + Information($"Updating IOS App Icon"); + }); + +Task("UpdateiOSPlist") + .IsDependentOn("GetGitInfo") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSAutofillPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSExtensionPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSShareExtensionPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSCodeFiles") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var fileList = new string[] { + Path.Combine(_slnPath, "src", "iOS.Core", "Utilities", "iOSCoreHelpers.cs"), + Path.Combine(_slnPath, "src", "iOS.Core", "Constants.cs"), + Path.Combine(".github", "resources", "export-options-ad-hoc.plist"), + Path.Combine(".github", "resources", "export-options-app-store.plist"), + }; + + foreach(string path in fileList) + { + ReplaceInFile(path, "com.8bit.bitwarden", buildVariant.iOSBundleId); + } + }); +#endregion iOS + +#region Main Tasks +Task("Android") + //.IsDependentOn("UpdateAndroidAppIcon") + .IsDependentOn("UpdateAndroidManifest") + .IsDependentOn("UpdateAndroidCodeFiles") + .Does(()=> + { + Information("Android app updated"); + }); + +Task("iOS") + //.IsDependentOn("UpdateiOSIcon") + .IsDependentOn("UpdateiOSPlist") + .IsDependentOn("UpdateiOSAutofillPlist") + .IsDependentOn("UpdateiOSExtensionPlist") + .IsDependentOn("UpdateiOSShareExtensionPlist") + .IsDependentOn("UpdateiOSCodeFiles") + .Does(()=> + { + Information("iOS app updated"); + }); + +Task("Default") + .Does(() => { + var usage = @"Missing target. + +Usage: + dotnet cake build.cake --target (Android | iOS) --variant (dev | qa | beta | prod) + +Options: + --debugScript= Script debug mode. +"; + Information(usage); + }); +#endregion Main Tasks + +RunTarget(target); \ No newline at end of file