Slide 1

Slide 1 text

DevOps sur Android D'un git push à une release sur le Play Store @JeremMartinez www.jeremie-martinez.com

Slide 2

Slide 2 text

Nous recrutons !

Slide 3

Slide 3 text

Les processes dépendent du contexte

Slide 4

Slide 4 text

DevOps n.pr [dɛvops] Fluidifier le processus de mise en production en rassemblant les équipes de développement et les équipes opérationnelles. Play Store

Slide 5

Slide 5 text

Intégration continue Livraison continue Déploiement continu Tests unitaires Automatisation Release Packaging Build Tests d’intégration Test d’instrumentation Quality

Slide 6

Slide 6 text

Intégration continue Build automatique Fail fast Testé constamment Packaging constant Release plus facile

Slide 7

Slide 7 text

Base d’un process solide

Slide 8

Slide 8 text

Packaging Quality Tests Build Push

Slide 9

Slide 9 text

Push dev master v19 v20

Slide 10

Slide 10 text

Push dev master v19 v20 feature

Slide 11

Slide 11 text

Build Push Packaging Quality Tests

Slide 12

Slide 12 text

Build 2 builds: debug et release

Slide 13

Slide 13 text

Build

Slide 14

Slide 14 text

Packaging Quality Tests Build Push

Slide 15

Slide 15 text

Tests Oui, même sur Android

Slide 16

Slide 16 text

Tests unitaires Valider des fonctionnalités Garantir des comportements Empêcher les regressions

Slide 17

Slide 17 text

JUnit4 public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Tests unitaires

Slide 18

Slide 18 text

Robolectric public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Tests unitaires

Slide 19

Slide 19 text

@RunWith(RobolectricTestRunner.class)
 @Config(manifest = Config.NONE) Tests unitaires Robolectric public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } }

Slide 20

Slide 20 text

@Test
 public void shouldRestoreFromParcelable() { // Given Character item = new Character("Frodo", "Baggins", "Hobbit"); 
 // When
 Parcel parcel = Parcel.obtain();
 item.writeToParcel(parcel, 0);
 parcel.setDataPosition(0); // Then Character fromParcel = Character.CREATOR.createFromParcel(parcel); assertThat(fromParcel.firstname).isEqualTo("Frodo"); assertThat(fromParcel.lastname).isEqualTo("Baggins");
 assertThat(fromParcel.race).isEqualTo("Hobbit"); } Tests unitaires

Slide 21

Slide 21 text

Ne testez pas Android Tests unitaires

Slide 22

Slide 22 text

Utilisez un framework d’assertions Truth ou AssertJ par exemple Tests unitaires

Slide 23

Slide 23 text

Tests d’intégration Espresso ou Robotium

Slide 24

Slide 24 text

Tests d’intégration Espresso ou Robotium

Slide 25

Slide 25 text

Tests d’intégration Testez uniquement les user-flows principaux

Slide 26

Slide 26 text

Tests d’intégration Recherche Panier Paiement Billets Exemple pour Captain Train

Slide 27

Slide 27 text

Tests

Slide 28

Slide 28 text

Quality Tests Build Push Packaging

Slide 29

Slide 29 text

Quality

Slide 30

Slide 30 text

Quality Lint

Slide 31

Slide 31 text

Quality Lint Custom

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Quality // Only XML files
 @Override
 public boolean appliesTo(@NonNull Context context,
 @NonNull File file) {
 return LintUtils.isXmlFile(file);
 }

Slide 34

Slide 34 text

Quality // Only values folder
 @Override
 public boolean appliesTo(ResourceFolderType folderType) {
 return ResourceFolderType.VALUES == folderType;
 }

Slide 35

Slide 35 text

Quality // Only attr tag
 @Override
 public Collection getApplicableElements() {
 return Collections.singletonList(TAG_ATTR);
 }

Slide 36

Slide 36 text

Quality // Only name attribute
 @Override
 public Collection getApplicableAttributes() {
 return Collections.singletonList(ATTR_NAME);
 }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Quality public final class CaptainRegistry extends IssueRegistry {
 @Override
 public List getIssues() {
 return Collections.singletonList(AttrPrefixDetector.ISSUE);
 }
 }

Slide 39

Slide 39 text

Quality

Slide 40

Slide 40 text

Quality Ne sous-estimez pas les règles Lint

Slide 41

Slide 41 text

Packaging Quality Tests Build Push

Slide 42

Slide 42 text

Packaging Filtrez vos ressources Proguard Assemble Signez votre APK Zipalign votre APK zipalign -z 4 in.apk out.apk

Slide 43

Slide 43 text

Packaging Quality Tests Build Push Intégration continue

Slide 44

Slide 44 text

Packaging Quality Tests Build Push Intégration continue

Slide 45

Slide 45 text

Intégration continue

Slide 46

Slide 46 text

Automatiser les livraisons afin de pouvoir livrer rapidement et facilement Intégration continue Livraison continue

Slide 47

Slide 47 text

Déploiement continu

Slide 48

Slide 48 text

« Trop de mises à jour »

Slide 49

Slide 49 text

Pas assez de contenu

Slide 50

Slide 50 text

Pouvoir ≠ Vouloir

Slide 51

Slide 51 text

Une release, c’est quoi ?

Slide 52

Slide 52 text

Beta Rollout 5% Rollout 50% Release 100% 1 semaine 1 semaine

Slide 53

Slide 53 text

Beta Rollout 5% Rollout 50% Release 100% Toutes les 6 semaines Le mardi

Slide 54

Slide 54 text

Intégration continue Livraison continue dev master v21

Slide 55

Slide 55 text

Publish Screenshots Livraison continue

Slide 56

Slide 56 text

Screenshots Ne négligez pas les screenshots

Slide 57

Slide 57 text

Screenshots Automatisez les screenshots

Slide 58

Slide 58 text

Outil maison avec UiAutomator Spoon par Square Screengrab par Fastlane Screenshots

Slide 59

Slide 59 text

Publish Livraison continue Screenshots

Slide 60

Slide 60 text

Éviter les surprises Publish

Slide 61

Slide 61 text

Vérifiez vos permissions Publish

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Testez vos upgrades de database Publish

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

@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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Tables / Views Columns Primary Keys Cross-references Indexes Publish

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

Publish

Slide 71

Slide 71 text

Publish

Slide 72

Slide 72 text

Publish

Slide 73

Slide 73 text

Choisissez votre client HTTP - Java - Ruby - Python Publish

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

AndroidPublisher.Edits edits = publisher.edits();
 
 AppEdit edit = edits.insert(PACKAGE, null).execute();
 String id = edit.getId(); Publish

Slide 77

Slide 77 text

Listings listings = edits.listings(); Listing listing = new Listing().
 setFullDescription(description).
 setShortDescription(shortDescription).
 setTitle(title);
 
 listings.update(PACKAGE, id, "en_US", listing).execute(); Publish

Slide 78

Slide 78 text

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"

Slide 79

Slide 79 text


 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

Slide 80

Slide 80 text


 // 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"

Slide 81

Slide 81 text


 // Update APK listing
 Apklistings apklistings = edits.apklistings(); 
 ApkListing whatsnew = new ApkListing().setRecentChanges(changes); 
 apklistings.update(PACKAGE, id, version, "en_US", whatsnew).execute(); Publish

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Listings Screenshots APK Nouveautés Track Publish

Slide 84

Slide 84 text

Outil maison Plugin Gradle Plugin Jenkins Publish

Slide 85

Slide 85 text

Screenshots Livraison continue Publish

Slide 86

Slide 86 text

Publish Screenshots Livraison continue Packaging Quality Tests Build Push Intégration continue

Slide 87

Slide 87 text

Questions ? @JeremMartinez www.jeremie-martinez.com