DevOps sur Android : from one git push to a Play Store release

DevOps sur Android : from one git push to a Play Store release

Slides in English.

Talk given à Devoxx Belgium and Droidcon Vienna

05dd369062f7c4e450e1e08d1471da5b?s=128

Jeremie Martinez

November 10, 2016
Tweet

Transcript

  1. DevOps on Android From one git push to a Play

    Store release @JeremMartinez www.jeremie-martinez.com
  2. None
  3. None
  4. Processes depend on context

  5. DevOps n.pr [dɛvops] Promotes a release process which bring together

    through communication and collaboration both development and operational teams. Play Store
  6. Continuous integration Automated build Fail fast Constantly tested Constant packaging

    Easier release
  7. Source of a solid process

  8. Packaging Quality Tests Build Push

  9. Push dev master v21 v22

  10. Push feature dev master v21 v22

  11. Push feature dev master v21 v22

  12. Merge request mandatory At least one reviewer Merge always by

    the reviewer Branch must always be rebased Push
  13. Build Push Packaging Quality Tests

  14. Build 2 builds: debug and release

  15. Build

  16. Build Isolated builds Different architectures possible Empower developers

  17. Packaging Quality Tests Build Push

  18. Tests Yes, even on Android

  19. JUnit4 public final class LordOfTheRingsTest {
 
 @Before
 public void

    setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Unit tests
  20. Robolectric public final class LordOfTheRingsTest {
 
 @Before
 public void

    setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Unit tests
  21. @RunWith(RobolectricTestRunner.class)
 @Config(manifest = Config.NONE) Unit tests Robolectric public final class

    LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } }
  22. Integration tests Espresso or Robotium

  23. Integration tests Espresso or Robotium

  24. Integration tests Only test main user-flows

  25. Integration tests Search Cart Payment Tickets Example for Captain Train

  26. Tests

  27. Quality Tests Build Push Packaging

  28. Quality

  29. Quality Lint

  30. Quality Lint Custom

  31. Quality public class AttrPrefixDetector extends ResourceXmlDetector {
 
 public static

    final Issue ISSUE = Issue.create("AttrNotPrefixed", //
 "You must prefix your custom attr by `ct`", //
 "We prefix all our attrs to avoid clashes.", //
 Category.TYPOGRAPHY, //
 5, // Priority
 Severity.WARNING, //
 new Implementation(AttrPrefixDetector.class, // Scope.RESOURCE_FILE_SCOPE) // );
  32. Quality // Only XML files
 @Override
 public boolean appliesTo(@NonNull Context

    context,
 @NonNull File file) {
 return LintUtils.isXmlFile(file);
 }
  33. Quality // Only values folder
 @Override
 public boolean appliesTo(ResourceFolderType folderType)

    {
 return ResourceFolderType.VALUES == folderType;
 }
  34. Quality // Only attr tag
 @Override
 public Collection<String> getApplicableElements() {


    return Collections.singletonList(TAG_ATTR);
 }
  35. Quality // Only name attribute
 @Override
 public Collection<String> getApplicableAttributes() {


    return Collections.singletonList(ATTR_NAME);
 }
  36. Quality @Override
 public void visitElement(XmlContext context, Element element) {
 final

    Attr attributeNode = element.getAttributeNode(ATTR_NAME);
 if (attributeNode != null) {
 final String val = attributeNode.getValue();
 if (!val.startsWith("android:") && !val.startsWith("ct")) {
 context.report(ISSUE,
 attributeNode,
 context.getLocation(attributeNode),
 "You must prefix your custom attr by `ct`");
 }
 }
 }
  37. Quality public final class CaptainRegistry extends IssueRegistry {
 @Override
 public

    List<Issue> getIssues() {
 return Collections.singletonList(AttrPrefixDetector.ISSUE);
 }
 }
  38. Quality

  39. Quality Do not underestimate Lint rules

  40. Packaging Quality Tests Build Push

  41. Packaging Filter your resources Proguard Assemble Sign your APK Zipalign

    your APK zipalign -z 4 in.apk out.apk
  42. Packaging Quality Tests Build Push Continuous integration

  43. Packaging Quality Tests Build Push Continuous integration

  44. Continuous integration

  45. Automate releases in order to deliver quickly, easily and reliably

    Continuous integration Continuous delivery
  46. Continuous deployment

  47. « Too many updates »

  48. Not enough content

  49. Can ≠ Want

  50. But … What is a release ?

  51. Beta Rollout 5% Rollout 50% Release 100% 1 week 1

    week
  52. Beta Rollout 5% Rollout 50% Release 100% Every 6 weeks

    On Tuesday
  53. Continuous integration Continuous delivery dev master v21

  54. Publish Screenshots Continuous delivery

  55. Screenshots Don’t neglect screenshots

  56. Screenshots Automate screenshots

  57. Home-made with UiAutomator Spoon by Square Screengrab by Fastlane Screenshots

  58. Publish Continuous delivery Screenshots

  59. Avoid bad surprises Publish

  60. Check your permissions Publish

  61. private static final String[] EXPECTED_PERMISSIONS = { … } @Test


    public void shouldMatchPermissions() throws Exception {
 final AndroidManifest androidManifest = new AndroidManifest( //
 Fs.fileFromPath("build/intermediates/manifests/full/debug/AndroidManifest.xml"), //
 null, //
 null //
 );
 final Set<String> requestedPermissions = new HashSet<>(androidManifest.getUsedPermissions());
 
 assertThat(requestedPermissions).containsOnly(EXPECTED_PERMISSIONS);
 } Publish
  62. Tests database upgrades Publish

  63. testCompile 'org.xerial:sqlite-jdbc:3.8.10.1'
 Publish

  64. @Test
 public void upgradeShouldBeTheSameAsCreate() throws Exception {
 DbOpenHelper helper =

    new DbOpenHelper(RuntimeEnvironment.application);
 
 helper.onCreate(newDatabase());
 helper.onUpgrade(originDatabase(), 1, CURRENT_VERSION);
 
 Set<String> newSchema = extractSchema(newFile.getAbsolutePath());
 Set<String> upgradedSchema = extractSchema(upgradedFile.getAbsolutePath());
 
 assertThat(upgradedSchema).isEqualTo(newSchema);
 } Publish
  65. connection = DriverManager.getConnection(JDBC_SQLITE + url);
 
 tables = connection.getMetaData().getTables(null, null,

    null, null);
 while (tables.next()) {
 
 final String tableType = tables.getString("TABLE_TYPE");
 final String tableName = tables.getString("TABLE_NAME");
 schema.add(tableType + " " + tableName);
 Publish
  66. columns = connection.getMetaData().getColumns(null, null, tableName, null);
 while (columns.next()) {
 


    final String columnName = columns.getString("COLUMN_NAME");
 final String columnType = columns.getString("TYPE_NAME");
 final String columnNullable = columns.getString("IS_NULLABLE");
 final String columnDefault = columns.getString("COLUMN_DF");
 schema.add("TABLE " + tableName +
 " COLUMN " + columnName + " " + columnType +
 " NULLABLE=" + columnNullable +
 " DEFAULT=" + columnDefault);
 } Publish
  67. Tables / Views Columns Primary Keys Cross-references Indexes Publish

  68. Publish

  69. Publish

  70. Publish

  71. Publish

  72. Publish

  73. Publish

  74. Choose your client HTTP - Java - Ruby - Python

    Publish
  75. compile 'com.google.apis:google-api-services-androidpublisher:v2-rev20-1.21.0'
 Publish

  76. http = GoogleNetHttpTransport.newTrustedTransport();
 json = JacksonFactory.getDefaultInstance();
 
 GoogleCredential credential =

    new GoogleCredential.Builder().
 setTransport(http).
 setJsonFactory(json).
 setServiceAccountPrivateKeyId(KEY_ID).
 setServiceAccountId(SERVICE_ACCOUNT_EMAIL).
 setServiceAccountScopes(AndroidPublisherScopes.ANDROIDPUBLISHER).
 setServiceAccountPrivateKeyFromPemFile(secretFile).
 build();
 
 publisher = new AndroidPublisher.Builder(http, json, credential).
 setApplicationName(PACKAGE).
 build(); Publish
  77. AndroidPublisher.Edits edits = publisher.edits();
 
 AppEdit edit = edits.insert(PACKAGE, null).execute();


    String id = edit.getId(); Publish
  78. Listings listings = edits.listings(); Listing listing = new Listing().
 setFullDescription(description).


    setShortDescription(shortDescription).
 setTitle(title);
 
 listings.update(PACKAGE, id, "en_US", listing).execute(); Publish
  79. Images images = edits.images();
 
 FileContent content = new FileContent(PNG_MIME_TYPE,

    file);
 
 images.upload(PACKAGE, id, "en_US", "phone5", content).execute(); Publish "tablet7" "tablet9"
 "wear"
  80. 
 Apks apks = edits.apks(); 
 FileContent apkContent = new

    FileContent(APK_MIME_TYPE, apkFile);
 Apk apk = apks.upload(PACKAGE, id, apkContent).execute(); 
 int version = apk.getVersionCode();
 Publish
  81. 
 // Assign APK to Track
 Tracks tracks = edits.tracks();

    
 Track track = new Track().setVersionCodes(version); 
 tracks.update(PACKAGE, id, "production", track).execute(); Publish "rollout" "beta" "alpha"
  82. 
 // Update APK listing
 Apklistings apklistings = edits.apklistings(); 


    ApkListing whatsnew = new ApkListing().setRecentChanges(changes); 
 apklistings.update(PACKAGE, id, version, "en_US", whatsnew).execute(); Publish
  83. edits.validate(PACKAGE, id).execute(); 
 edits.commit(PACKAGE, id).execute(); Publish

  84. Listings Screenshots APK What’s new Track Publish

  85. Home-made Gradle plugin Jenkins plugin Publish

  86. Screenshots Continuous delivery Publish

  87. Publish Screenshots Continuous delivery Packaging Quality Tests Build Push Continuous

    integration
  88. Questions ? @JeremMartinez www.jeremie-martinez.com