Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Jeremie Martinez

November 10, 2016
Tweet

More Decks by Jeremie Martinez

Other Decks in Technology

Transcript

  1. DevOps on Android
    From one git push to a Play Store release
    @JeremMartinez
    www.jeremie-martinez.com

    View Slide

  2. View Slide

  3. View Slide

  4. Processes
    depend on context

    View Slide

  5. DevOps
    n.pr [dɛvops]
    Promotes a release process which bring together through communication
    and collaboration both development and operational teams.
    Play Store

    View Slide

  6. Continuous integration
    Automated build
    Fail fast
    Constantly tested
    Constant packaging
    Easier release

    View Slide

  7. Source of a
    solid process

    View Slide

  8. Packaging
    Quality
    Tests
    Build
    Push

    View Slide

  9. Push
    dev
    master
    v21 v22

    View Slide

  10. Push
    feature
    dev
    master
    v21 v22

    View Slide

  11. Push
    feature
    dev
    master
    v21 v22

    View Slide

  12. Merge request mandatory
    At least one reviewer
    Merge always by the reviewer
    Branch must always be rebased
    Push

    View Slide

  13. Build
    Push Packaging
    Quality
    Tests

    View Slide

  14. Build
    2 builds: debug and release

    View Slide

  15. Build

    View Slide

  16. Build
    Isolated builds
    Different architectures possible
    Empower developers

    View Slide

  17. Packaging
    Quality
    Tests
    Build
    Push

    View Slide

  18. Tests
    Yes, even on Android

    View Slide

  19. JUnit4
    public final class LordOfTheRingsTest {


    @Before

    public void setup() { … }


    @Test

    public void shouldBringThePrecious() { … }
    @After

    public void tearDown() { … }
    }
    Unit tests

    View Slide

  20. Robolectric
    public final class LordOfTheRingsTest {


    @Before

    public void setup() { … }


    @Test

    public void shouldBringThePrecious() { … }
    @After

    public void tearDown() { … }
    }
    Unit tests

    View Slide

  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() { … }
    }

    View Slide

  22. Integration tests
    Espresso or Robotium

    View Slide

  23. Integration tests
    Espresso or Robotium

    View Slide

  24. Integration tests
    Only test
    main user-flows

    View Slide

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

    View Slide

  26. Tests

    View Slide

  27. Quality
    Tests
    Build
    Push Packaging

    View Slide

  28. Quality

    View Slide

  29. Quality
    Lint

    View Slide

  30. Quality
    Lint
    Custom

    View Slide

  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) //
    );

    View Slide

  32. Quality
    // Only XML files

    @Override

    public boolean appliesTo(@NonNull Context context,

    @NonNull File file) {

    return LintUtils.isXmlFile(file);

    }

    View Slide

  33. Quality
    // Only values folder

    @Override

    public boolean appliesTo(ResourceFolderType folderType) {

    return ResourceFolderType.VALUES == folderType;

    }

    View Slide

  34. Quality
    // Only attr tag

    @Override

    public Collection getApplicableElements() {

    return Collections.singletonList(TAG_ATTR);

    }

    View Slide

  35. Quality
    // Only name attribute

    @Override

    public Collection getApplicableAttributes() {

    return Collections.singletonList(ATTR_NAME);

    }

    View Slide

  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`");

    }

    }

    }

    View Slide

  37. Quality
    public final class CaptainRegistry extends IssueRegistry {

    @Override

    public List getIssues() {

    return Collections.singletonList(AttrPrefixDetector.ISSUE);

    }

    }

    View Slide

  38. Quality

    View Slide

  39. Quality
    Do not underestimate
    Lint rules

    View Slide

  40. Packaging
    Quality
    Tests
    Build
    Push

    View Slide

  41. Packaging
    Filter your resources
    Proguard
    Assemble
    Sign your APK
    Zipalign your APK
    zipalign -z 4 in.apk out.apk

    View Slide

  42. Packaging
    Quality
    Tests
    Build
    Push
    Continuous integration

    View Slide

  43. Packaging
    Quality
    Tests
    Build
    Push
    Continuous integration

    View Slide

  44. Continuous integration

    View Slide

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

    View Slide

  46. Continuous deployment

    View Slide

  47. « Too many
    updates »

    View Slide

  48. Not enough
    content

    View Slide

  49. Can ≠ Want

    View Slide

  50. But …
    What is a release ?

    View Slide

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

    View Slide

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

    View Slide

  53. Continuous integration Continuous delivery
    dev
    master
    v21

    View Slide

  54. Publish
    Screenshots
    Continuous delivery

    View Slide

  55. Screenshots
    Don’t neglect
    screenshots

    View Slide

  56. Screenshots
    Automate
    screenshots

    View Slide

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

    View Slide

  58. Publish
    Continuous delivery
    Screenshots

    View Slide

  59. Avoid bad surprises
    Publish

    View Slide

  60. Check your
    permissions
    Publish

    View Slide

  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 requestedPermissions = new HashSet<>(androidManifest.getUsedPermissions());


    assertThat(requestedPermissions).containsOnly(EXPECTED_PERMISSIONS);

    }
    Publish

    View Slide

  62. Tests database
    upgrades
    Publish

    View Slide

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

    Publish

    View Slide

  64. @Test

    public void upgradeShouldBeTheSameAsCreate() throws Exception {

    DbOpenHelper helper = new DbOpenHelper(RuntimeEnvironment.application);


    helper.onCreate(newDatabase());

    helper.onUpgrade(originDatabase(), 1, CURRENT_VERSION);


    Set newSchema = extractSchema(newFile.getAbsolutePath());

    Set upgradedSchema = extractSchema(upgradedFile.getAbsolutePath());


    assertThat(upgradedSchema).isEqualTo(newSchema);

    }
    Publish

    View Slide

  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

    View Slide

  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

    View Slide

  67. Tables / Views
    Columns
    Primary Keys
    Cross-references
    Indexes
    Publish

    View Slide

  68. Publish

    View Slide

  69. Publish

    View Slide

  70. Publish

    View Slide

  71. Publish

    View Slide

  72. Publish

    View Slide

  73. Publish

    View Slide

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

    View Slide

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

    Publish

    View Slide

  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

    View Slide

  77. AndroidPublisher.Edits edits = publisher.edits();


    AppEdit edit = edits.insert(PACKAGE, null).execute();

    String id = edit.getId();
    Publish

    View Slide

  78. Listings listings = edits.listings();
    Listing listing = new Listing().

    setFullDescription(description).

    setShortDescription(shortDescription).

    setTitle(title);


    listings.update(PACKAGE, id, "en_US", listing).execute();
    Publish

    View Slide

  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"

    View Slide


  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

    View Slide


  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"

    View Slide


  82. // Update APK listing

    Apklistings apklistings = edits.apklistings();

    ApkListing whatsnew = new ApkListing().setRecentChanges(changes);

    apklistings.update(PACKAGE, id, version, "en_US", whatsnew).execute();
    Publish

    View Slide

  83. edits.validate(PACKAGE, id).execute();

    edits.commit(PACKAGE, id).execute();
    Publish

    View Slide

  84. Listings
    Screenshots
    APK
    What’s new
    Track
    Publish

    View Slide

  85. Home-made
    Gradle plugin
    Jenkins plugin
    Publish

    View Slide

  86. Screenshots
    Continuous delivery
    Publish

    View Slide

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

    View Slide

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

    View Slide