---
title: 'Mobile Continuous Delivery'
source: 'https://academia.sh/en/courses/cross-platform-production/mobile-continuous-delivery'
course: 'Cross-Platform Production Practice'
language: en
updated: '2026-10-21T05:02:51+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the role of signing, the certificate, and the provisioning profile in making a build ready for release'
    - 'Orders the build, upload, and test distribution steps in a delivery pipeline'
    - "Builds the condition of the staging stage and the production stage separately and measures the feature's operability"
    - 'Shows that a feature flag can turn a feature off even in a signed and uploaded build'
---

# Mobile Continuous Delivery

Mobile continuous delivery turns a code change into a signed build and passes it through a test distribution channel and store review. This lesson measures how well a feature works when the same signed build ships to production with a closed flag.

In the earlier lessons, the state of the device determined a feature's operability: the network was present or absent, and background work was throttled or not. This lesson moves the source of the condition from the device to the stage. Before an application reaches users, it passes through a pipeline; the pipeline compiles the build, signs it, uploads it, and distributes it through a channel. The same build can run in different stages with different feature flag values, and the value of a flag says which capability exists in that stage.

The question of the course does not change: how well does this feature work under this condition? In the opening lesson the nine records came out as five full, one degraded, two deferred, and one disabled. This lesson does not change those counts; it measures only its own slice, the condition of the build.

## Continuous Delivery on Mobile

Continuous delivery is the practice of turning every code change into a release-ready build through the same steps and without manual intervention. On the web, the end of such a pipeline updates a server, and everyone sees the new version. On mobile, the end of the pipeline is a build, and that build waits to be installed on the user's device. Until the user accepts the update, the old version keeps running on the device.

There is one more limit. The pipeline makes the build ready for release, but the final step of release goes through the store review of the app store or the Android store, and that stage is not in the pipeline's hands. For this reason mobile pipelines have two parts: the stages the team controls before the build enters the store, and the stages that wait after it enters. The team automates the first part and plans around the second.

M12/K05 asked whether a behavior reaches production. Operability asks something else. A feature flag can be off even in a build that reaches production; operability classifies not where the build went, but how well the feature works in the build that went there.

## Signing, Certificate, and Provisioning Profile

A build does not enter a store without being signed. Signing proves who produced the build and that it has not changed since. On iOS, the signature is made with a certificate; the build is also paired with a provisioning profile that says which application identifier, which entitlements, and which distribution type it belongs to. A certificate has a validity period, and an expired certificate prevents the next build from being signed. On Android, the signature is tied to a key; the signing identity is part of the application's identity. If the team keeps the signing key itself, losing the key cannot be undone, and a leaked key concerns not a single build but the whole release history of the application. If the store keeps the signing key, a lost upload key held by the team can be renewed by applying to the store.

The rule is the same on both platforms: signing uses a secret, and a secret is not committed to the repository. The pipeline reads the certificate, the provisioning profile, and the key from a secret store and uses them only for the duration of the signing step. If signing material stays on one developer's computer, the pipeline depends on that person, which is the very problem continuous delivery tries to solve.

## Build, Upload, and Test Distribution

A pipeline is usually divided into three stages. The build stage produces the build from the code in a clean environment and increments the build number on every run. The signing stage signs the build. The upload stage sends the signed build to the publishing console of the store or to a test distribution channel. A test distribution channel delivers a build to selected users without passing it through full store review. On iOS, when testers from outside the team are invited, the first build goes through a separate test review; the team tries the build that will go to production first on its own devices and with test users.

The fragment below shows what these stages look like in a configuration. The fragment is not measured; the field names are examples.

```yaml
# Platform code: delivery pipeline stages
pipeline:
  - stage: build
    steps: [compile, sign]
    signing:
      certificate: from-secret-store
      profile: store-distribution
  - stage: upload
    steps: [upload-to-test-channel]
  - stage: release
    needs-approval: true
    steps: [upload-to-store, submit-for-review]
```

The `needs-approval` line in the last stage shows where the pipeline stops on purpose. If no problem appeared while the build circulated in the test channel, a person gives approval and the build is submitted to store review. Automation carries the load; the decision stays with a person.

## Habits That Make the Pipeline Reliable

The value of a pipeline comes less from its speed than from its repeatability. A few habits protect that.

First, the build tried in the test channel and the build sent to the store must be the same. If one build is compiled for staging and a second build for production, a difference can arise between what was tried and what was published. For this reason the pipeline produces the build once and carries the same file from one stage to the next. The difference between the stages lies not in the content of the build but in the configuration of the stage.

Second, the build number never goes backward and is never the same in two builds. Stores do not accept a second upload with the same build number, and managing the number by hand is one of the places where pipelines get stuck most often. The pipeline increments the number, and a person decides only the version number. The difference between the two is the subject of the next lesson.

Third, if a stage fails, the pipeline stops and a half-finished build is not uploaded. If the signing step reports an error, the upload step does not run; only a signed and complete build reaches the test channel. The log of a failed run is the first place the team looks, and no secret should appear in that log.

Fourth, a store review rejection is part of the plan. The fix for a rejected build gets a new build number, and the pipeline runs again from the start. Automation shortens this cycle, but waiting for the store's decision is not something the team can manage.

## Measuring the Condition of the Build

From the point of view of operability, the build the pipeline produces takes a single piece of information from the configuration of the stage: which feature flags are set and with which values. In the configuration of the staging stage, the flag of the new payment flow is on, because the team wants to try it. In the configuration of the production stage it is off, because the flow will not yet be given to users. The core below is familiar from the first lesson and is given again in the same form in every lesson; the lesson does not touch it.

```javascript
// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}
```

After the core comes the lesson's helper file. `configFor(stage)` produces a condition from a pipeline stage: the network is present in every stage, while a flag capability is added only for the stage's open flags. The capability string is written with the `flag:` prefix. There are three features. `NewCheckout` is the new payment flow and needs the network and the `flag:new-checkout` capability; the flag has no fallback. `LegacyCheckout` is the old payment flow and needs only the network. `Catalog` is the product list and also needs only the network.

```javascript
// delivery.mjs — produces a condition from a pipeline stage
import { feature, condition } from "./operability.mjs";

export function configFor(stage) {
  const flags = stage.flags.map((name) => `flag:${name}`);
  return condition(stage.name, ["network", ...flags]);
}

export const checkoutFeatures = [
  feature("NewCheckout", ["network", "flag:new-checkout"]),
  feature("LegacyCheckout", ["network"]),
  feature("Catalog", ["network"]),
];
```

**RO1.** The model builds the flag as a capability: a flag is either in the configuration of the stage or not. In a real application, the value of a flag can be embedded in the build or can come from remote configuration; the rule does not see this difference and looks only at the `has` list. The model builds this operability's rule; the model is not the framework itself.

The first measurement puts two stages side by side with the same build number. The build was compiled and signed once; the only difference between staging and production is the list of open flags. The build lines are the lesson's local output; the record lines are the `assess` output.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { configFor, checkoutFeatures } from "./delivery.mjs";

const build = { number: 42, signed: true, uploaded: true };
const stages = [
  { name: "staging", flags: ["new-checkout"] },
  { name: "production", flags: [] },
];
for (const stage of stages) {
  console.log(`build: ${build.number} stage=${stage.name} signed=${build.signed ? "yes" : "no"}`);
}
printLevels(assess(checkoutFeatures, stages.map(configFor)));
```

```text
build: 42 stage=staging signed=yes
build: 42 stage=production signed=yes
full: NewCheckout @ staging missing=-
full: LegacyCheckout @ staging missing=-
full: Catalog @ staging missing=-
disabled: NewCheckout @ production missing=flag:new-checkout
full: LegacyCheckout @ production missing=-
full: Catalog @ production missing=-
```

In staging all three features are full. In production, `NewCheckout` comes out **disabled**: the missing capability is `flag:new-checkout`, and it has no fallback defined. `LegacyCheckout` and `Catalog` stay full in the same production build. The work of the disabled feature is not deferred either; the user pays through the old flow. The old flow being full does not mean it is the new flow's fallback. They are two separate features and get separate records.

## The Build Stays Fixed, the Flag Changes

A flag can be turned on in production, and this may require no new build. If the value of the flag is read from remote configuration, the team changes the value, and the application picks up the new value on its next read. This is one of the most valuable distinctions in mobile continuous delivery: sending a build to the store and opening a behavior to users are two separate acts. The first is subject to store review; the second is not.

The second measurement compares the same production build with the flag closed and open. The condition names differ, because two conditions cannot share a name; the build and the build number are the same.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { configFor, checkoutFeatures } from "./delivery.mjs";

const closed = { name: "production", flags: [] };
const opened = { name: "production-flag-on", flags: ["new-checkout"] };
printLevels(assess(checkoutFeatures, [closed, opened].map(configFor)));
```

```text
disabled: NewCheckout @ production missing=flag:new-checkout
full: LegacyCheckout @ production missing=-
full: Catalog @ production missing=-
full: NewCheckout @ production-flag-on missing=-
full: LegacyCheckout @ production-flag-on missing=-
full: Catalog @ production-flag-on missing=-
```

When the flag opens, `NewCheckout` moves from disabled to full; the other two features do not change at all. The rule does not see how the flag was opened: remote configuration, a new build, or a manually changed value all yield the same capability. What the rule looks at is whether the capability exists at the end of the opening.

**RO2.** The model does not show the spread of a flag's opening. In a real application, remote configuration does not reach every device at the same moment; devices read the value at different times. This distribution is the subject of the next lessons, and in this lesson the condition of a single application instance is measured.

## The Bounding Measurement

The focus record of this lesson is `NewCheckout @ production`. The bounding measurement shows two things together. First, the focus record is full at the staging stage and disabled at the production stage, and only one of six records is affected. Second, in the model the signing and uploading of the build do not enter the record: a signed and uploaded build and a build not yet signed produce the same record under the same production condition. This is not an observation, it is the definition of the model: the `configFor` function that builds the condition never reads the `build` field.

```javascript
// main.mjs
import { assess, printLevels, countLevels } from "./operability.mjs";
import { configFor, checkoutFeatures } from "./delivery.mjs";

const stages = [
  { name: "staging", flags: ["new-checkout"] },
  { name: "production", flags: [] },
];
const records = assess(checkoutFeatures, stages.map(configFor));
printLevels(records.filter((r) => r.feature === "NewCheckout"));
countLevels(records);

const signed = { number: 42, signed: true, uploaded: true };
const unsigned = { number: 43, signed: false, uploaded: false };
const withBuild = (build) =>
  assess(checkoutFeatures, [{ name: "production", flags: [], build }].map(configFor));
const same = JSON.stringify(withBuild(signed)) === JSON.stringify(withBuild(unsigned));
console.log(`signing does not enter the record: ${same ? "yes" : "no"}`);
```

```text
full: NewCheckout @ staging missing=-
disabled: NewCheckout @ production missing=flag:new-checkout
full: 5
degraded: 0
deferred: 0
disabled: 1
affected: 1
signing does not enter the record: yes
```

The first two lines carry the lesson's operability: the same feature, the same code, the same build number; only whether the flag is in the configuration of the stage changed, and the record moved from full to disabled. The count lines say that of six records, five are full and one is disabled; affected is one.

The last line shows the limit of the model plainly. The signature, the certificate, and the upload are conditions a build must meet to enter a store, but they are not capabilities the feature needs. A build that is not signed will never enter the store; but the rule classifies the feature inside a build that has entered the store. The success of the pipeline and the working of the feature answer two separate questions. The pipeline can be green, the signature valid, the upload complete, and the user can still fail to see the new payment flow.

This distinction changes the workflow as well. When a feature is found not to work after a production release, the first place to look is not the pipeline log; it is the flag value in the configuration of the stage.

## Summary

- This lesson's operability: disabled. `NewCheckout @ production` came out disabled because the configuration of the production stage lacked the `flag:new-checkout` capability and the flag has no fallback; in the same stage `LegacyCheckout` and `Catalog` stayed full.
- A mobile pipeline compiles, signs, and uploads the build and gives it to a test distribution channel; the last stage is store review, and it is not in the pipeline's hands.
- Signing depends on secrets such as the certificate, the provisioning profile, and the key; these values live in the pipeline's secret store, not in the repository.
- In staging three features were full and in production one was disabled; of six records, one was affected. When the flag opened, the record returned to full in the same production build.
- In the model, a signed and uploaded build and an unsigned build give the same record; this equality is not a measured finding. A build being ready does not determine operability.

## Next Step

In this lesson the build was referred to by a single build number, and no meaning was attached to it. The next lesson, `Version Management`, covers the difference between the version number and the build number, semantic versioning, and release notes. It then ties the contract between client and server to the version, and measures how well order history works when it asks for the new contract on a server that offers only the old one.
