diff --git a/README.md b/README.md index c87c67f..c06692f 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,11 @@ The following job setup process is demonstrated in this video: 4. Select the credential name from the drop-down list - The credential must belong to the Google Play account which owns the app to be uploaded 5. Enter paths and/or patterns pointing to the AAB or APKs to be uploaded - - This can be a glob pattern, e.g. `build/**/*-release.apk`, or a filename, both relative to the root of the workspace + - This can be a glob pattern, e.g. `'build/**/*-release.apk'`, or a filename, both relative to the root of the workspace - Multiple patterns or filenames can be entered, if separated by commas - - If nothing is entered, the default is `**/build/outputs/**/*.aab, **/build/outputs/**/*.apk` + - If nothing is entered, the default is `'**/build/outputs/**/*.aab, **/build/outputs/**/*.apk'` 6. Choose the track to which the APKs should be deployed - - If nothing is entered, the default is `production` + - If nothing is entered, the default is `'production'` 7. Optionally specify a [rollout percentage][gp-docs-rollout] - If nothing is entered, the default is to roll out to 100% of users 8. Optionally choose "Add language" to associate release notes with the uploaded APK(s) @@ -141,7 +141,7 @@ The following job setup process is demonstrated in this video: ###### APK expansion files You can optionally add up to two [expansion files][gp-docs-expansions] (main + patch) for each APK being uploaded. -A list of expansion files can be specified in the same way as APKs, though note that they must be named in the format `[main|patch]``...obb`. +A list of expansion files can be specified in the same way as APKs, though note that they must be named in the format `[main|patch]...obb`. You can also enable the "Re-use expansion files from existing APKs where necessary" option, which will automatically the most-recent expansion files to newly uploaded APKs. @@ -166,16 +166,17 @@ Note that you should avoid using these steps in a `parallel` block, as the Googl ##### Uploading an AAB or APK The `androidApkUpload` build step lets you upload Android App Bundle (AAB) or APK files. -| Parameter | Type | Example | Default | Description | -|------------------------------------|---------|-----------------------------------|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| googlePlayCredentialsId | string | My Google Play account | (none) | Name of the Google Service Account credential created in Jenkins | -| filesPattern | string | `release/my-app.aab` | `**/build/outputs/**/*.aab, **/build/outputs/**/*.apk` | Comma-separated glob patterns or filenames pointing to the app files to upload, relative to the root of the workspace | -| trackName | string | `internal` | `production` | Google Play track to which the app files should be published | -| rolloutPercent | number | `0.01` | `100.0` | The rollout percentage to set on the track | -| deobfuscationFiles
Pattern | string | `**/build/outputs/**/mapping.txt` | (none) | Comma-separated glob patterns or filenames pointing to ProGuard mapping files to associate with the uploaded app files | -| expansionFilesPattern | string | `**/*.obb` | (none) | Comma-separated glob patterns or filenames pointing to expansion files to associate with the uploaded app files | -| usePreviousExpansion
FilesIfMissing | boolean | `false` | `true` | Whether to re-use the existing expansion files that have already been uploaded to Google Play for this app, if any expansion files are missing | -| recentChangeList | list | (see below) | (empty) | List of recent change texts to associate with the upload app files | +| Parameter | Type | Example | Default | Description | +|------------------------------------|---------|-----------------------------------|----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| googlePlayCredentialsId | string | My Google Play account | (none) | Name of the Google Service Account credential created in Jenkins | +| filesPattern | string | `'release/my-app.aab'` | `'**/build/outputs/**/*.aab, **/build/outputs/**/*.apk'` | Comma-separated glob patterns or filenames pointing to the app files to upload, relative to the root of the workspace | +| trackName | string | `'internal'` | `'production'` | Google Play track to which the app files should be published | +| rolloutPercentage | string | `'1.5'` | `'100.0'` | The rollout percentage to set on the track | +| ~rolloutPercent~ | number | `0.01` | `100.0` | (deprecated, but still supported; use `rolloutPercent` instead — it takes priority if both are defined) | +| deobfuscationFiles
Pattern | string | `**/build/outputs/**/mapping.txt` | (none) | Comma-separated glob patterns or filenames pointing to ProGuard mapping files to associate with the uploaded app files | +| expansionFilesPattern | string | `**/*.obb` | (none) | Comma-separated glob patterns or filenames pointing to expansion files to associate with the uploaded app files | +| usePreviousExpansion
FilesIfMissing | boolean | `false` | `true` | Whether to re-use the existing expansion files that have already been uploaded to Google Play for this app, if any expansion files are missing | +| recentChangeList | list | (see below) | (empty) | List of recent change texts to associate with the upload app files | The only mandatory parameter is `googlePlayCredentialId`: ```groovy @@ -208,15 +209,16 @@ androidApkUpload googleCredentialsId: 'My Google Play account', ##### Updating release tracks with existing app versions The `androidApkMove` build step lets you move existing Android app versions to another release track, and/or update the rollout percentage. -| Parameter | Type | Example | Default | Description | -|-------------------------|---------|----------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| googlePlayCredentialsId | string | My Google Play account | (none) | Name of the Google Service Account credential created in Jenkins | -| trackName | string | `internal` | `production` | Google Play release track to update with the given app versions | -| rolloutPercent | number | `0.01` | `100.0` | The rollout percentage to set on the given release track | -| fromVersionCode | boolean | `true` | `false` | If true, the `applicationId` and `versionCodes` parameters will be used. Otherwise the `filesPattern` parameter will be used | -| applicationId | string | `com.example.app` | (none) | The application ID of the app to update | -| versionCodes | string | `1281, 1282, 1283` | (none) | The version codes to set on the given release track | -| filesPattern | string | `release/my-app.aab` | `**/build/outputs/**/*.aab, **/build/outputs/**/*.apk` | Comma-separated glob patterns or filenames pointing to the files from which the application ID and version codes should be read | +| Parameter | Type | Example | Default | Description | +|-------------------------|---------|------------------------|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| googlePlayCredentialsId | string | My Google Play account | (none) | Name of the Google Service Account credential created in Jenkins | +| trackName | string | `'internal'` | `'production'` | Google Play release track to update with the given app versions | +| rolloutPercentage | number | `'1.5'` | `'100.0'` | The rollout percentage to set on the given release track | +| ~rolloutPercent~ | number | `0.01` | `100.0` | (deprecated, but still supported; use `rolloutPercent` instead — it takes priority if both are defined) | +| fromVersionCode | boolean | `true` | `false` | If true, the `applicationId` and `versionCodes` parameters will be used. Otherwise the `filesPattern` parameter will be used | +| applicationId | string | `'com.example.app'` | (none) | The application ID of the app to update | +| versionCodes | string | `'1281, 1282, 1283'` | (none) | The version codes to set on the given release track | +| filesPattern | string | `'release/my-app.aab'` | `'**/build/outputs/**/*.aab, **/build/outputs/**/*.apk'` | Comma-separated glob patterns or filenames pointing to the files from which the application ID and version codes should be read | The `googlePlayCredentialId` parameter is mandatory, plus either an application ID and version code(s), or AAB or APK file(s) to read this information from. @@ -224,7 +226,7 @@ For example, this would move the given versions to the production track, and mak ```groovy androidApkMove googleCredentialsId: 'My Google Play account', applicationId: 'com.example.app', - versionCodes: '1281, 1282, 1283', + versionCodes: '1281, 1282, 1283' ``` Or moving versions from alpha (for example), to 50% of beta users, reading the application ID and version codes from APK files in the workspace: @@ -239,7 +241,6 @@ androidApkMove googleCredentialsId: 'My Google Play account', ##### Backwards-compatibility Version 3.0 of the plugin deprecated some parameters used by the build steps, but they will remain supported for the foreseeable future: - `apkFilesPattern` is deprecated — `filesPattern` should be used instead -- `rolloutPercentage` is deprecated; it required a string rather than a number, which was not ideal — `rolloutPercent` should be used instead In addition, version 3.0 introduced the default values shown in the tables above, so those parameters can optionally now be omitted. diff --git a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher.java b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher.java index 6cb8c37..f79a0fb 100644 --- a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher.java +++ b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher.java @@ -1,6 +1,5 @@ package org.jenkinsci.plugins.googleplayandroidpublisher; -import com.google.common.annotations.VisibleForTesting; import com.google.jenkins.plugins.credentials.oauth.GoogleRobotCredentials; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; @@ -16,6 +15,8 @@ import hudson.util.ComboBoxModel; import hudson.util.FormValidation; import net.dongliu.apk.parser.exception.ParserException; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.AppFileFormat; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.UploadFile; @@ -40,9 +41,9 @@ import static hudson.Util.fixEmptyAndTrim; import static hudson.Util.join; +import static hudson.Util.tryParseNumber; import static org.jenkinsci.plugins.googleplayandroidpublisher.Constants.OBB_FILE_REGEX; import static org.jenkinsci.plugins.googleplayandroidpublisher.Constants.OBB_FILE_TYPE_MAIN; -import static org.jenkinsci.plugins.googleplayandroidpublisher.Constants.PERCENTAGE_FORMATTER; import static org.jenkinsci.plugins.googleplayandroidpublisher.ReleaseTrack.fromConfigValue; import static org.jenkinsci.plugins.googleplayandroidpublisher.Util.REGEX_LANGUAGE; import static org.jenkinsci.plugins.googleplayandroidpublisher.Util.REGEX_VARIABLE; @@ -52,16 +53,19 @@ /** Uploads Android application files to the Google Play Developer Console. */ public class ApkPublisher extends GooglePlayPublisher { - @VisibleForTesting String filesPattern; + private String filesPattern; private String deobfuscationFilesPattern; private String expansionFilesPattern; private boolean usePreviousExpansionFilesIfMissing; - @VisibleForTesting String trackName; - private Double rolloutPercent; + private String trackName; + private String rolloutPercentage; private RecentChanges[] recentChangeList; + // This field was used before AAB support was introduced; it will be migrated to `filesPattern` for Freestyle jobs @Deprecated private transient String apkFilesPattern; - @Deprecated private transient String rolloutPercentage; + // This can still be used in Pipeline jobs as a convenience (i.e. defining rollout as a number instead of a string); + // but the `rolloutPercentage` field will take precedence, if defined + @Deprecated private transient Double rolloutPercent; @DataBoundConstructor public ApkPublisher() { @@ -70,30 +74,34 @@ public ApkPublisher() { @SuppressWarnings("unused") protected Object readResolve() { - // Migrate from old `apkFilesPattern` to `filesPattern` + // Migrate Freestyle jobs from old `apkFilesPattern` field to `filesPattern` if (apkFilesPattern != null) { setFilesPattern(apkFilesPattern); + apkFilesPattern = null; } - // Migrate from `rolloutPercentage` string to numeric `rolloutPercent` - if (rolloutPercentage != null) { - setRolloutPercentage(rolloutPercentage); + // Migrate Freestyle jobs back from `rolloutPercent` number to `rolloutPercentage` string + if (rolloutPercent != null) { + // Call the old setter, which updates `rolloutPercentage` if necessary + setRolloutPercent(rolloutPercent); + rolloutPercent = null; } return this; } - // Required for Pipeline builds still using `apkFilesPattern` + // Required for Pipeline builds using the deprecated `apkFilesPattern` option @Deprecated @DataBoundSetter public void setApkFilesPattern(String value) { setFilesPattern(value); } - // Required for the Snippet Generator, since the field has a @DataBoundSetter + // Since the `apkFilesPattern` field has a @DataBoundSetter, this method needs to exist in order + // for the Snippet Generator to work for this publisher, even although this method is otherwise unused @Deprecated public String getApkFilesPattern() { - return getFilesPattern(); + return null; } @DataBoundSetter @@ -106,37 +114,53 @@ public String getFilesPattern() { return fixEmptyAndTrim(filesPattern) == null ? DescriptorImpl.defaultFilesPattern : filesPattern; } - // Required for Pipeline builds still using `rolloutPercentage` - @Deprecated @DataBoundSetter - public void setRolloutPercentage(String percentage) { - String input = percentage.replace("%", "").trim(); - double value; - try { - value = Double.parseDouble(input); - } catch (NumberFormatException ignore) { - value = DescriptorImpl.defaultRolloutPercent; + @SuppressWarnings("ConstantConditions") + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") + public void setRolloutPercentage(@Nonnull String percentage) { + // If the value is an expression, just store it directly + if (percentage.matches(REGEX_VARIABLE)) { + this.rolloutPercentage = percentage; + return; + } + + // Otherwise try and parse it as a number + String pctStr = percentage.replace("%", "").trim(); + Number pct = tryParseNumber(pctStr, Double.NaN); + + // If it can't be parsed, save it, and we'll show an error at build time + if (Double.isNaN(pct.doubleValue())) { + this.rolloutPercentage = percentage; + return; } - setRolloutPercent(value); + this.rolloutPercentage = pct.intValue() == DescriptorImpl.defaultRolloutPercent ? null : pctStr; } - // Required for the Snippet Generator, since the field has a @DataBoundSetter @Nonnull - @Deprecated public String getRolloutPercentage() { - double value = rolloutPercent == null ? DescriptorImpl.defaultRolloutPercent : rolloutPercent; - return String.valueOf(value); + if (fixEmptyAndTrim(rolloutPercentage) == null) { + return String.valueOf(DescriptorImpl.defaultRolloutPercent); + } + return rolloutPercentage; } + // Required for Pipeline builds using the deprecated `rolloutPercent` option + @Deprecated @DataBoundSetter public void setRolloutPercent(Double percent) { - this.rolloutPercent = (percent == null || percent.intValue() == DescriptorImpl.defaultRolloutPercent) ? null : percent; + // If a job somehow has both `rolloutPercent` and `rolloutPercentage` defined, + // let the latter, non-deprecated field take precedence, i.e. don't overwrite it + if (rolloutPercentage != null || percent == null) { + return; + } + setRolloutPercentage(percent.toString()); } - @Nonnull - @SuppressFBWarnings("BX_UNBOXING_IMMEDIATELY_REBOXED") + // Since the `rolloutPercent` field has a @DataBoundSetter, this method needs to exist in order + // for the Snippet Generator to work for this publisher, even although this method is otherwise unused + @Deprecated public Double getRolloutPercent() { - return rolloutPercent == null ? DescriptorImpl.defaultRolloutPercent : rolloutPercent; + return null; } @DataBoundSetter @@ -211,6 +235,19 @@ private String getExpandedExpansionFilesPattern() throws IOException, Interrupte return expand(getExpansionFilesPattern()); } + private String getExpandedRolloutPercentageString() throws IOException, InterruptedException { + return expand(getRolloutPercentage()); + } + + private double getExpandedRolloutPercentage() throws IOException, InterruptedException { + try { + String value = getExpandedRolloutPercentageString().replace("%", "").trim(); + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return Double.NaN; + } + } + private RecentChanges[] getExpandedRecentChangesList() throws IOException, InterruptedException { if (recentChangeList == null) { return null; @@ -240,9 +277,9 @@ private boolean isConfigValid(PrintStream logger) throws IOException, Interrupte errors.add(String.format("'%s' is not a valid release track", trackName)); } else { // Check for valid rollout percentage - double pct = getRolloutPercent(); - if (Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { - errors.add(String.format("%s%% is not a valid rollout percentage", PERCENTAGE_FORMATTER.format(pct))); + double pct = getExpandedRolloutPercentage(); + if (Double.isNaN(pct) || Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { + errors.add(String.format("'%s' is not a valid rollout percentage", getExpandedRolloutPercentageString())); } } @@ -351,7 +388,7 @@ private boolean publishApk(@Nonnull Run run, @Nonnull FilePath workspace, List relativeMappingPaths = workspace.act(new FindFilesTask(mappingFilesPattern)); if (relativeMappingPaths.isEmpty()) { logger.println(String.format("No obfuscation mapping files matching the pattern '%s' could be found; " + - "no files will be uploaded", filesPattern)); + "no files will be uploaded", mappingFilesPattern)); return false; } @@ -463,7 +500,7 @@ private boolean publishApk(@Nonnull Run run, @Nonnull FilePath workspace, GoogleRobotCredentials credentials = getCredentialsHandler().getServiceAccountCredentials(run.getParent()); return workspace.act(new ApkUploadTask(listener, credentials, applicationId, workspace, validFiles, expansionFiles, usePreviousExpansionFilesIfMissing, fromConfigValue(getCanonicalTrackName()), - getRolloutPercent(), getExpandedRecentChangesList())); + getExpandedRolloutPercentage(), getExpandedRecentChangesList())); } catch (UploadException e) { logger.println(String.format("Upload failed: %s", getPublisherErrorMessage(e))); logger.println("No changes have been applied to the Google Play account"); @@ -512,6 +549,30 @@ public RecentChanges(String language, String text) { this.text = text; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecentChanges that = (RecentChanges) o; + return new EqualsBuilder() + .append(language, that.language) + .append(text, that.text) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(language) + .append(text) + .toHashCode(); + } + + @Override + public String toString() { + return String.format("RecentChanges[language='%s', text='%s']", language, text); + } + @Extension public static class DescriptorImpl extends Descriptor { diff --git a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/GooglePlayBuildStepDescriptor.java b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/GooglePlayBuildStepDescriptor.java index c70571b..4971fd2 100644 --- a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/GooglePlayBuildStepDescriptor.java +++ b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/GooglePlayBuildStepDescriptor.java @@ -99,14 +99,14 @@ public FormValidation doCheckTrackName(@QueryParameter String value) { } @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") - public FormValidation doCheckRolloutPercent(@QueryParameter String value) { + public FormValidation doCheckRolloutPercentage(@QueryParameter String value) { value = fixEmptyAndTrim(value); if (value == null || value.matches(REGEX_VARIABLE)) { return FormValidation.ok(); } - double pct = tryParseNumber(value.replace("%", ""), 100).doubleValue(); - if (Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { + double pct = tryParseNumber(value.replace("%", "").trim(), Double.NaN).doubleValue(); + if (Double.isNaN(pct) || Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { return FormValidation.error("Percentage value must be between 0 and 100%"); } return FormValidation.ok(); @@ -116,7 +116,7 @@ public ComboBoxModel doFillTrackNameItems() { return new ComboBoxModel(getConfigValues()); } - public ComboBoxModel doFillRolloutPercentItems() { + public ComboBoxModel doFillRolloutPercentageItems() { return null; } diff --git a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder.java b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder.java index ac3b2f9..e16175d 100644 --- a/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder.java +++ b/src/main/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder.java @@ -1,6 +1,5 @@ package org.jenkinsci.plugins.googleplayandroidpublisher; -import com.google.common.annotations.VisibleForTesting; import com.google.jenkins.plugins.credentials.oauth.GoogleRobotCredentials; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; @@ -29,21 +28,24 @@ import static hudson.Util.fixEmptyAndTrim; import static hudson.Util.tryParseNumber; -import static org.jenkinsci.plugins.googleplayandroidpublisher.Constants.PERCENTAGE_FORMATTER; import static org.jenkinsci.plugins.googleplayandroidpublisher.ReleaseTrack.fromConfigValue; +import static org.jenkinsci.plugins.googleplayandroidpublisher.Util.REGEX_VARIABLE; import static org.jenkinsci.plugins.googleplayandroidpublisher.Util.getPublisherErrorMessage; public class ReleaseTrackAssignmentBuilder extends GooglePlayBuilder { - @VisibleForTesting Boolean fromVersionCode; - @VisibleForTesting String applicationId; - @VisibleForTesting String versionCodes; + private Boolean fromVersionCode; + private String applicationId; + private String versionCodes; private String filesPattern; - @VisibleForTesting String trackName; - @VisibleForTesting Double rolloutPercent; + private String trackName; + private String rolloutPercentage; + // This field was used before AAB support was introduced; it will be migrated to `filesPattern` for Freestyle jobs @Deprecated private transient String apkFilesPattern; - @Deprecated private transient String rolloutPercentage; + // This can still be used in Pipeline jobs as a convenience (i.e. defining rollout as a number instead of a string); + // but the `rolloutPercentage` field will take precedence, if defined + @Deprecated private transient Double rolloutPercent; @DataBoundConstructor public ReleaseTrackAssignmentBuilder() { @@ -52,14 +54,17 @@ public ReleaseTrackAssignmentBuilder() { @SuppressWarnings("unused") protected Object readResolve() { - // Migrate from old `apkFilesPattern` to `filesPattern` + // Migrate Freestyle jobs from old `apkFilesPattern` field to `filesPattern` if (apkFilesPattern != null) { setFilesPattern(apkFilesPattern); + apkFilesPattern = null; } - // Migrate from `rolloutPercentage` string to numeric `rolloutPercent` - if (rolloutPercentage != null) { - setRolloutPercentage(rolloutPercentage); + // Migrate Freestyle jobs back from `rolloutPercent` number to `rolloutPercentage` string + if (rolloutPercent != null) { + // Call the old setter, which updates `rolloutPercentage` if necessary + setRolloutPercent(rolloutPercent); + rolloutPercent = null; } return this; @@ -119,37 +124,53 @@ public String getFilesPattern() { return fixEmptyAndTrim(filesPattern) == null ? DescriptorImpl.defaultFilesPattern : filesPattern; } - // Required for Pipeline builds still using `rolloutPercentage` - @Deprecated @DataBoundSetter - public void setRolloutPercentage(String percentage) { - String input = percentage.replace("%", "").trim(); - double value; - try { - value = Double.parseDouble(input); - } catch (NumberFormatException ignore) { - value = DescriptorImpl.defaultRolloutPercent; + @SuppressWarnings("ConstantConditions") + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") + public void setRolloutPercentage(@Nonnull String percentage) { + // If the value is an expression, just store it directly + if (percentage.matches(REGEX_VARIABLE)) { + this.rolloutPercentage = percentage; + return; + } + + // Otherwise try and parse it as a number + String pctStr = percentage.replace("%", "").trim(); + Number pct = tryParseNumber(pctStr, Double.NaN); + + // If it can't be parsed, save it, and we'll show an error at build time + if (Double.isNaN(pct.doubleValue())) { + this.rolloutPercentage = percentage; + return; } - setRolloutPercent(value); + this.rolloutPercentage = pct.intValue() == ApkPublisher.DescriptorImpl.defaultRolloutPercent ? null : pctStr; } - // Required for the Snippet Generator, since the field has a @DataBoundSetter @Nonnull - @Deprecated public String getRolloutPercentage() { - double value = rolloutPercent == null ? DescriptorImpl.defaultRolloutPercent : rolloutPercent; - return String.valueOf(value); + if (fixEmptyAndTrim(rolloutPercentage) == null) { + return String.valueOf(ApkPublisher.DescriptorImpl.defaultRolloutPercent); + } + return rolloutPercentage; } + // Required for Pipeline builds using the deprecated `rolloutPercent` option + @Deprecated @DataBoundSetter public void setRolloutPercent(Double percent) { - this.rolloutPercent = (percent == null || percent.intValue() == DescriptorImpl.defaultRolloutPercent) ? null : percent; + // If a job somehow has both `rolloutPercent` and `rolloutPercentage` defined, + // let the latter, non-deprecated field take precedence, i.e. don't overwrite it + if (rolloutPercentage != null || percent == null) { + return; + } + setRolloutPercentage(percent.toString()); } - @Nonnull - @SuppressFBWarnings("BX_UNBOXING_IMMEDIATELY_REBOXED") + // Since the `rolloutPercent` field has a @DataBoundSetter, this method needs to exist in order + // for the Snippet Generator to work for this publisher, even although this method is otherwise unused + @Deprecated public Double getRolloutPercent() { - return rolloutPercent == null ? DescriptorImpl.defaultRolloutPercent : rolloutPercent; + return null; } @DataBoundSetter @@ -182,6 +203,19 @@ private String getCanonicalTrackName() throws IOException, InterruptedException return name.toLowerCase(Locale.ENGLISH); } + private String getExpandedRolloutPercentageString() throws IOException, InterruptedException { + return expand(getRolloutPercentage()); + } + + private double getExpandedRolloutPercentage() throws IOException, InterruptedException { + try { + String value = getExpandedRolloutPercentageString().replace("%", "").trim(); + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return Double.NaN; + } + } + private boolean isConfigValid(PrintStream logger) throws IOException, InterruptedException { final List errors = new ArrayList<>(); @@ -206,9 +240,9 @@ private boolean isConfigValid(PrintStream logger) throws IOException, Interrupte errors.add(String.format("'%s' is not a valid release track", trackName)); } else { // Check for valid rollout percentage - double pct = getRolloutPercent(); - if (Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { - errors.add(String.format("%s%% is not a valid rollout percentage", PERCENTAGE_FORMATTER.format(pct))); + double pct = getExpandedRolloutPercentage(); + if (Double.isNaN(pct) || Double.compare(pct, 0) < 0 || Double.compare(pct, 100) > 0) { + errors.add(String.format("'%s' is not a valid rollout percentage", getExpandedRolloutPercentageString())); } } @@ -270,7 +304,7 @@ private boolean assignAppFiles(@Nonnull Run run, @Nonnull FilePath workspa try { GoogleRobotCredentials credentials = getCredentialsHandler().getServiceAccountCredentials(run.getParent()); return workspace.act(new TrackAssignmentTask(listener, credentials, applicationId, versionCodeList, - fromConfigValue(getCanonicalTrackName()), getRolloutPercent())); + fromConfigValue(getCanonicalTrackName()), getExpandedRolloutPercentage())); } catch (UploadException e) { logger.println(String.format("Assignment failed: %s", getPublisherErrorMessage(e))); logger.println("No changes have been applied to the Google Play account"); diff --git a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/config.jelly b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/config.jelly index 5aabcad..c961bef 100644 --- a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/config.jelly @@ -25,7 +25,7 @@ - diff --git a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/help-rolloutPercent.html b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/help-rolloutPercentage.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/help-rolloutPercent.html rename to src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisher/help-rolloutPercentage.html diff --git a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/config.jelly b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/config.jelly index 9929516..9549e48 100644 --- a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/config.jelly @@ -33,7 +33,7 @@ - diff --git a/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/help-rolloutPercent.html b/src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/help-rolloutPercentage.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/help-rolloutPercent.html rename to src/main/resources/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilder/help-rolloutPercentage.html diff --git a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisherTest.java b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisherTest.java index f8d2f9d..da5e4dd 100644 --- a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisherTest.java +++ b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ApkPublisherTest.java @@ -1,23 +1,18 @@ package org.jenkinsci.plugins.googleplayandroidpublisher; import com.cloudbees.hudson.plugins.folder.Folder; +import com.cloudbees.plugins.credentials.CredentialsParameterDefinition; import com.google.api.services.androidpublisher.AndroidPublisher; +import com.google.jenkins.plugins.credentials.oauth.GoogleRobotPrivateKeyCredentials; import hudson.FilePath; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; +import hudson.model.ParametersDefinitionProperty; import hudson.model.Result; import hudson.model.Slave; +import hudson.model.StringParameterDefinition; import hudson.model.queue.QueueTaskFuture; import hudson.slaves.DumbSlave; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.Arrays; -import java.util.Collections; - import org.jenkinsci.plugins.googleplayandroidpublisher.internal.JenkinsUtil; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.TestHttpTransport; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.TestUtilImpl; @@ -31,6 +26,8 @@ import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakePutBundleResponse; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakeUploadApkResponse; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakeUploadBundleResponse; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -39,13 +36,22 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Stream; + import static hudson.Util.join; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; import static org.jenkinsci.plugins.googleplayandroidpublisher.internal.TestConstants.DEFAULT_APK; import static org.jenkinsci.plugins.googleplayandroidpublisher.internal.TestConstants.DEFAULT_BUNDLE; -import static org.jenkinsci.plugins.googleplayandroidpublisher.internal.TestsHelper.setUpCredentials; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; @@ -83,12 +89,44 @@ public void tearDown() { Util.setJenkinsUtil(null); } + @Test + public void configRoundtripWorks() throws Exception { + // Given that a few credentials have been set up + TestsHelper.setUpCredentials("credential-a"); + TestsHelper.setUpCredentials("credential-b"); + TestsHelper.setUpCredentials("credential-c"); + + // And we have a job configured with the APK publisher, which includes all possible configuration options + FreeStyleProject project = j.createFreeStyleProject(); + ApkPublisher publisher = new ApkPublisher(); + // Choose the second credential, so that when the config page loads, we can differentiate between the dropdown + // working as expected vs just appearing to work because the first credential would be selected by default + publisher.setGoogleCredentialsId("credential-b"); + publisher.setFilesPattern("**/builds/*.apk, *.aab"); + publisher.setDeobfuscationFilesPattern("**/proguard/*.txt"); + publisher.setExpansionFilesPattern("**/exp/*.obb"); + publisher.setUsePreviousExpansionFilesIfMissing(true); + publisher.setTrackName("alpha"); + publisher.setRolloutPercentage("${ROLLOUT}"); + publisher.setRecentChangeList(new ApkPublisher.RecentChanges[] { + new ApkPublisher.RecentChanges("en", "Hello!"), + new ApkPublisher.RecentChanges("de", "Hallo!"), + }); + project.getPublishersList().add(publisher); + + // When we open and save the configuration page for this job + project = j.configRoundtrip(project); + + // Then the publisher object should have been serialised and deserialised, without any changes + j.assertEqualDataBoundBeans(publisher, project.getPublishersList().get(0)); + } + @Test public void whenApkFileMissing_buildFails() throws Exception { FreeStyleProject p = j.createFreeStyleProject("uploadApks"); ApkPublisher publisher = new ApkPublisher(); - publisher.trackName = "production"; + publisher.setTrackName("production"); p.getPublishersList().add(publisher); QueueTaskFuture scheduled = p.scheduleBuild2(0); @@ -110,8 +148,8 @@ public void uploadingExistingApkFails() throws Exception { FreeStyleProject p = j.createFreeStyleProject(); ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*.apk"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.apk"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); TestsHelper.setUpCredentials("test-credentials"); @@ -138,8 +176,8 @@ public void uploadSingleApk_succeeds() throws Exception { ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*.apk"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.apk"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); @@ -182,8 +220,8 @@ public void uploadSingleApk_inFolder_succeeds() throws Exception { FreeStyleProject p = folder.createProject(FreeStyleProject.class, "some-job-in-a-folder"); ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("folder-credentials"); - publisher.filesPattern = "**/*.apk"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.apk"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); setUpApkFile(p); @@ -198,6 +236,114 @@ public void uploadSingleApk_inFolder_succeeds() throws Exception { ); } + @Test + public void uploadingApkWithParametersSucceeds() throws Exception { + // Given a job with various parameters + FreeStyleProject p = j.createFreeStyleProject(); + + ParametersDefinitionProperty pdp = new ParametersDefinitionProperty( + new CredentialsParameterDefinition( + "GP_CREDENTIAL", null, "test-credentials", + GoogleRobotPrivateKeyCredentials.class.getName(), true + ), + new StringParameterDefinition("APK_PATTERN", "**/app.apk"), + new StringParameterDefinition("TRACK_NAME", "production"), + new StringParameterDefinition("ROLLOUT_PCT", "12.5%") + ); + p.addProperty(pdp); + + // And a publisher which uses those parameters + ApkPublisher publisher = new ApkPublisher(); + publisher.setGoogleCredentialsId("${GP_CREDENTIAL}"); + publisher.setFilesPattern("${APK_PATTERN}"); + publisher.setTrackName("${TRACK_NAME}"); + publisher.setRolloutPercentage("${ROLLOUT_PCT}"); + p.getPublishersList().add(publisher); + + // And the prerequisites are in place + TestsHelper.setUpCredentials("test-credentials"); + setUpTransportForApk(); + setUpApkFile(p); + + // When a build occurs, it should apply the default parameter values + TestsHelper.assertResultWithLogLines(j, p, Result.SUCCESS, + "- Credential: test-credentials", + "Setting rollout to target 12.5% of production track users", + "Changes were successfully applied to Google Play" + ); + } + + @Test + public void uploadSingleApkWithPipeline_succeeds() throws Exception { + // Given a Pipeline with only the required parameters + String stepDefinition = "androidApkUpload googleCredentialsId: 'test-credentials'"; + + uploadApkWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 100% of production track users" + ); + } + + @Test + public void uploadSingleApkWithPipeline_withRolloutPercentage() throws Exception { + // Given a step with a `rolloutPercentage` value + String stepDefinition = "androidApkUpload googleCredentialsId: 'test-credentials',\n" + + " rolloutPercentage: '56.789'"; + + // When a build occurs, it should roll out to that percentage + uploadApkWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 56.789% of production track users" + ); + } + + @Test + public void uploadSingleApkWithPipeline_withRolloutPercent() throws Exception { + // Given a step with a deprecated `rolloutPercent` value + String stepDefinition = "androidApkUpload googleCredentialsId: 'test-credentials',\n" + + " rolloutPercent: 12.34"; + + // When a build occurs, it should roll out to that percentage + uploadApkWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 12.34% of production track users" + ); + } + + @Test + public void uploadSingleApkWithPipeline_withBothRolloutFormats_usesRolloutPercentage() throws Exception { + // Given a step with both the deprecated `rolloutPercent`, and a verbose `rolloutPercentage` value + String stepDefinition = "androidApkUpload googleCredentialsId: 'test-credentials',\n" + + " rolloutPercent: 12.3456,\n" + + " rolloutPercentage: '56.789%'"; + + // When a build occurs, it should prefer the string `rolloutPercentage` value + uploadApkWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 56.789% of production track users" + ); + } + + private void uploadApkWithPipelineAndAssertSuccess(String stepDefinition, String... expectedLines) throws Exception { + WorkflowJob p = j.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("" + + "node {\n" + + " writeFile text: 'this-is-a-dummy-apk', file: 'build/outputs/apk/app.apk'\n" + + " " + stepDefinition + "\n" + + "}", true + )); + + TestsHelper.setUpCredentials("test-credentials"); + setUpTransportForApk(); + + String[] commonLines = { + "Uploading 1 file(s) with application ID: org.jenkins.appId", + "APK file: " + join(Arrays.asList("build", "outputs", "apk", "app.apk"), File.separator), + "versionCode: 42", + "The production release track will now contain the version code(s): 42", + "Changes were successfully applied to Google Play" + }; + String[] allExpectedLogLines = Stream.concat(Arrays.stream(commonLines), Arrays.stream(expectedLines)) + .toArray(String[]::new); + TestsHelper.assertResultWithLogLines(j, p, Result.SUCCESS, allExpectedLogLines); + } + @Test public void uploadingExistingBundleFails() throws Exception { // Given that some version codes already exist on Google Play @@ -211,8 +357,8 @@ public void uploadingExistingBundleFails() throws Exception { FreeStyleProject p = j.createFreeStyleProject(); ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*.aab"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.aab"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); TestsHelper.setUpCredentials("test-credentials"); @@ -229,7 +375,7 @@ public void uploadingExistingBundleFails() throws Exception { } @Test - public void uploadSingleBundle_succeeds() throws Exception { + public void uploadBundle_succeeds() throws Exception { setUpTransportForBundle(); FreeStyleProject p = j.createFreeStyleProject("uploadBundles"); @@ -239,8 +385,8 @@ public void uploadSingleBundle_succeeds() throws Exception { ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*.aab"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.aab"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); @@ -278,8 +424,8 @@ public void givenMultipleFileTypesBundlesArePreferred() throws Exception { FreeStyleProject p = j.createFreeStyleProject(); ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); TestsHelper.setUpCredentials("test-credentials"); @@ -304,6 +450,31 @@ public void givenMultipleFileTypesBundlesArePreferred() throws Exception { ); } + @Test + public void uploadBundleWithPipeline_succeeds() throws Exception { + // Given a Pipeline with only the required parameters + WorkflowJob p = j.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("" + + "node {\n" + + " writeFile text: 'this-is-a-dummy-bundle', file: 'build/outputs/bundle/release/bundle.aab'\n" + + " androidApkUpload googleCredentialsId: 'test-credentials'\n" + + "}", true + )); + + TestsHelper.setUpCredentials("test-credentials"); + setUpTransportForBundle(); + + // When a build occurs, it should succeed + TestsHelper.assertResultWithLogLines(j, p, Result.SUCCESS, + "Uploading 1 file(s) with application ID: org.jenkins.bundleAppId", + "AAB file: " + join(Arrays.asList("build", "outputs", "bundle", "release", "bundle.aab"), File.separator), + "versionCode: 43", + "Setting rollout to target 100% of production track users", + "The production release track will now contain the version code(s): 43", + "Changes were successfully applied to Google Play" + ); + } + @Test @WithoutJenkins public void responsesCanBeSerialized() throws IOException, ClassNotFoundException { @@ -345,8 +516,8 @@ public void uploadSingleApk_fromSlave_succeeds() throws Exception { ApkPublisher publisher = new ApkPublisher(); publisher.setGoogleCredentialsId("test-credentials"); - publisher.filesPattern = "**/*.apk"; - publisher.trackName = "production"; + publisher.setFilesPattern("**/*.apk"); + publisher.setTrackName("production"); p.getPublishersList().add(publisher); diff --git a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentTest.java b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilderTest.java similarity index 53% rename from src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentTest.java rename to src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilderTest.java index 3e5362b..4e97f4b 100644 --- a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentTest.java +++ b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/ReleaseTrackAssignmentBuilderTest.java @@ -16,19 +16,25 @@ import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakeListApksResponse; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakeListBundlesResponse; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.responses.FakePostEditsResponse; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; + +import java.util.Arrays; +import java.util.stream.Stream; + import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -public class ReleaseTrackAssignmentTest { +public class ReleaseTrackAssignmentBuilderTest { @Rule public JenkinsRule j = new JenkinsRule(); @@ -55,6 +61,35 @@ public void tearDown() throws Exception { transport.dumpRequests(); } + @Test + public void configRoundtripWorks() throws Exception { + // Given that a few credentials have been set up + TestsHelper.setUpCredentials("credential-a"); + TestsHelper.setUpCredentials("credential-b"); + TestsHelper.setUpCredentials("credential-c"); + + // And we have a job configured with the builder, which includes all possible configuration options + FreeStyleProject project = j.createFreeStyleProject(); + + ReleaseTrackAssignmentBuilder builder = new ReleaseTrackAssignmentBuilder(); + // Choose the second credential, so that when the config page loads, we can differentiate between the dropdown + // working as expected vs just appearing to work because the first credential would be selected by default + builder.setGoogleCredentialsId("credential-b"); + builder.setFromVersionCode(false); + builder.setApplicationId("org.jenkins.appId"); + builder.setVersionCodes("42"); + builder.setFilesPattern("**/*.apk"); + builder.setTrackName("production"); + builder.setRolloutPercentage("5"); + project.getBuildersList().add(builder); + + // When we open and save the configuration page for this job + project = j.configRoundtrip(project); + + // Then the publisher object should have been serialised and deserialised, without any changes + j.assertEqualDataBoundBeans(builder, project.getBuildersList().get(0)); + } + @Test public void moveApkTrack_whenVersionCodeDoesNotExist_buildFails() throws Exception { transport @@ -78,18 +113,7 @@ public void moveApkTrack_whenVersionCodeDoesNotExist_buildFails() throws Excepti @Test public void moveApkTrack_succeeds() throws Exception { - transport - .withResponse("/edits", - new FakePostEditsResponse().setEditId("the-edit-id")) - .withResponse("/edits/the-edit-id/apks", - new FakeListApksResponse().setApks(42)) - .withResponse("/edits/the-edit-id/bundles", - new FakeListBundlesResponse().setEmptyBundles()) - .withResponse("/edits/the-edit-id/tracks/production", - new FakeAssignTrackResponse().success("production", 42)) - .withResponse("/edits/the-edit-id:commit", - new FakeCommitResponse().success()) - ; + setUpTransportForSuccess(); FreeStyleProject p = j.createFreeStyleProject("moveReleaseTrack"); @@ -118,6 +142,89 @@ public void moveApkTrack_succeeds() throws Exception { ); } + @Test + public void moveApkTrackWithPipeline_succeeds() throws Exception { + String stepDefinition = + " androidApkMove googleCredentialsId: 'test-credentials',\n" + + " fromVersionCode: true,\n" + + " applicationId: 'org.jenkins.appId',\n" + + " versionCodes: '42'"; + + moveApkTrackWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 100% of production track users" + ); + } + + @Test + public void moveApkTrackWithPipeline_withRolloutPercentage() throws Exception { + // Given a step with a `rolloutPercentage` value + String stepDefinition = + "androidApkMove googleCredentialsId: 'test-credentials',\n" + + " fromVersionCode: true,\n" + + " applicationId: 'org.jenkins.appId',\n" + + " versionCodes: '42',\n" + + " rolloutPercentage: '56.789'"; + + // When a build occurs, it should roll out to that percentage + moveApkTrackWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 56.789% of production track users" + ); + } + + @Test + public void moveApkTrackWithPipeline_withRolloutPercent() throws Exception { + // Given a step with a deprecated `rolloutPercent` value + String stepDefinition = + "androidApkMove googleCredentialsId: 'test-credentials',\n" + + " fromVersionCode: true,\n" + + " applicationId: 'org.jenkins.appId',\n" + + " versionCodes: '42',\n" + + " rolloutPercent: 12.34"; + + // When a build occurs, it should roll out to that percentage + moveApkTrackWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 12.34% of production track users" + ); + } + + @Test + public void moveApkTrackWithPipeline_withBothRolloutFormats_usesRolloutPercentage() throws Exception { + // Given a step with both the deprecated `rolloutPercent`, and a verbose `rolloutPercentage` value + String stepDefinition = + "androidApkMove googleCredentialsId: 'test-credentials',\n" + + " fromVersionCode: true,\n" + + " applicationId: 'org.jenkins.appId',\n" + + " versionCodes: '42',\n" + + " rolloutPercent: 12.3456,\n" + + " rolloutPercentage: '56.789%'"; + + // When a build occurs, it should prefer the string `rolloutPercentage` value + moveApkTrackWithPipelineAndAssertSuccess( + stepDefinition, "Setting rollout to target 56.789% of production track users" + ); + } + + private void moveApkTrackWithPipelineAndAssertSuccess(String stepDefinition, String... expectedLines) throws Exception { + WorkflowJob p = j.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("" + + "node {\n" + + " " + stepDefinition + "\n" + + "}", true + )); + + TestsHelper.setUpCredentials("test-credentials"); + setUpTransportForSuccess(); + + String[] commonLines = { + "Assigning 1 version(s) with application ID org.jenkins.appId to production release track", + "The production release track will now contain the version code(s): 42", + "Changes were successfully applied to Google Play" + }; + String[] allExpectedLogLines = Stream.concat(Arrays.stream(commonLines), Arrays.stream(expectedLines)) + .toArray(String[]::new); + TestsHelper.assertResultWithLogLines(j, p, Result.SUCCESS, allExpectedLogLines); + } + @Test @Ignore("Test does not work on a remote slave") public void moveApkTrack_fromSlave_succeeds() throws Exception { @@ -163,14 +270,29 @@ public void moveApkTrack_fromSlave_succeeds() throws Exception { ); } + private void setUpTransportForSuccess() { + transport + .withResponse("/edits", + new FakePostEditsResponse().setEditId("the-edit-id")) + .withResponse("/edits/the-edit-id/apks", + new FakeListApksResponse().setApks(42)) + .withResponse("/edits/the-edit-id/bundles", + new FakeListBundlesResponse().setEmptyBundles()) + .withResponse("/edits/the-edit-id/tracks/production", + new FakeAssignTrackResponse().success("production", 42)) + .withResponse("/edits/the-edit-id:commit", + new FakeCommitResponse().success()) + ; + } + private ReleaseTrackAssignmentBuilder createBuilder() throws Exception { ReleaseTrackAssignmentBuilder builder = new ReleaseTrackAssignmentBuilder(); TestsHelper.setUpCredentials("test-credentials"); builder.setGoogleCredentialsId("test-credentials"); - builder.applicationId = "org.jenkins.appId"; - builder.versionCodes = "42"; - builder.rolloutPercent = 5d; - builder.trackName = "production"; + builder.setApplicationId("org.jenkins.appId"); + builder.setVersionCodes("42"); + builder.setRolloutPercentage("5%"); + builder.setTrackName("production"); return builder; } } \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/internal/TestsHelper.java b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/internal/TestsHelper.java index 98243d0..4318797 100644 --- a/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/internal/TestsHelper.java +++ b/src/test/java/org/jenkinsci/plugins/googleplayandroidpublisher/internal/TestsHelper.java @@ -9,16 +9,17 @@ import com.google.api.client.googleapis.testing.auth.oauth2.MockGoogleCredential; import com.google.api.services.androidpublisher.AndroidPublisher; import com.google.jenkins.plugins.credentials.oauth.GoogleRobotCredentials; -import hudson.model.FreeStyleBuild; -import hudson.model.FreeStyleProject; import hudson.model.Result; +import hudson.model.Run; import hudson.model.queue.QueueTaskFuture; +import jenkins.model.ParameterizedJobMixIn; import org.jenkinsci.plugins.googleplayandroidpublisher.internal.oauth.TestCredentials; import org.jvnet.hudson.test.JenkinsRule; -import javax.annotation.Nonnull; import java.io.IOException; +import static org.junit.Assert.assertNotNull; + public class TestsHelper { public static void setUpCredentials(String name) { GoogleRobotCredentials fakeCredentials = new TestCredentials(name); @@ -39,17 +40,6 @@ public static void setUpCredentials(String name, Folder folder) throws IOExcepti throw new IllegalStateException("Credentials store does not exist for folder: " + folder.getFullName()); } - @Nonnull - private static CredentialsStore getFolderCredentialsStore(Folder folder) { - Iterable stores = CredentialsProvider.lookupStores(folder); - for (CredentialsStore store : stores) { - if (store.getProvider() instanceof FolderCredentialsProvider && store.getContext() == folder) { - return store; - } - } - throw new IllegalStateException("Credentials store does not exist for folder: " + folder.getFullName()); - } - /** * Create the {@link AndroidPublisher} with a {@link TestHttpTransport}. * @@ -73,11 +63,10 @@ public static AndroidPublisher createAndroidPublisher(TestHttpTransport transpor */ public static void assertLogLines( JenkinsRule jenkinsRule, - QueueTaskFuture scheduledBuild, + QueueTaskFuture scheduledBuild, String... lines) throws Exception { - FreeStyleBuild build = scheduledBuild.get(); for (String line : lines) { - jenkinsRule.assertLogContains(line, build); + jenkinsRule.assertLogContains(line, scheduledBuild.get()); } } @@ -92,10 +81,11 @@ public static void assertLogLines( */ public static void assertResultWithLogLines( JenkinsRule jenkinsRule, - FreeStyleProject project, + ParameterizedJobMixIn.ParameterizedJob project, Result result, String... lines) throws Exception { - QueueTaskFuture future = project.scheduleBuild2(0); + QueueTaskFuture future = project.scheduleBuild2(0); + assertNotNull(future); jenkinsRule.assertBuildStatus(result, future); assertLogLines(jenkinsRule, future, lines); }