Lesson 14 / 19
Version Management
The version code is the integer the platform compares, while the version name is the string shown to the user. The lesson compares how far the installed version codes of eight devices are from the latest release with a budget and shows that changing the version name does not change the distance.
Published Updated
In this lesson
- Distinguishes the roles of the version code and the version name and which one is used for comparison
- Explains why the version code must increase monotonically and cannot be lowered
- Measures the distance of a device's installed version code from the latest release as the number of versions left behind
- Derives the budget from the lowest supported version and classifies the overrun of eight samples
Contents
The previous lesson looked at the output of the build: how many files land on a configuration. This lesson deals with giving that output an identity. Devices do not all receive every build at the same time; some stay on the latest version and some on an old one. For this reason every build needs a number that says which build is newer than which. The question is again the course’s question: in how many samples does this measurement exceed its budget? The measurement is how many versions a device has fallen behind the latest version, the budget is the number of versions back that are supported, and each device is a sample.
// Overrun.kt — classifies in how many samples a measurement exceeds its budget
class Budget(val name: String, val limit: Int)
class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
val total: Int = samples.size
val over: Int = samples.count { it > budget.limit }
val verdict: String = when {
over == 0 -> "within"
over == total -> "all"
over * 2 < total -> "minority"
else -> "majority"
}
fun line() {
println("$label: budget=${budget.name}<=${budget.limit} | over=$over/$total -> $verdict")
}
}
fun countOverruns(overruns: List<Overrun>) {
for (name in listOf("within", "minority", "majority", "all")) {
println("$name: ${overruns.count { it.verdict == name }}")
}
}
Overrun counts the samples that strictly exceed the limit and gives how widespread the overrun is with four names: within, minority, majority, all. The rule sees the number; whether a device is one version behind or ten versions behind, it counts the same for every sample that overruns.
Version Code and Version Name
Android gives every app version two separate values. The version code is an integer; the platform and the app store look at this number to understand which of two versions is newer. The version name is the string shown to the user, and the platform does not compare it. 2.2.1 is a version name; for the platform it carries no meaning about whether it is newer than the version before it.
// Platform code: module build script
android {
defaultConfig {
versionCode = 12
versionName = "2.2.1"
}
}
This fragment is platform code; we claim no output here. The model sets up the rule of this measurement; the model is not the tool itself. In the model a release is a Release carrying two fields: a code and a name. Only the code does the comparison.
The two values stand side by side in the script and both are written by hand, but they have different readers. versionCode tells the machine: this build comes after the previous one. versionName tells the human: this is which version the build is. A team can increase both at the same time, can increase only one, or can compute the code from the name; which one is done directly affects the distance we will measure throughout the lesson. This is why the model puts the code and the name in separate fields: when one changes, the other does not have to.
Two rules govern the version code. The first is monotonic increase: the code of every new release must be greater than that of the one before it. The second is that the code cannot be lowered: the platform does not accept an update with a package whose code is lower than the installed one. Therefore, rolling back a faulty release does not mean republishing the old code; it means publishing a new build that carries the old behavior but a higher code.
The Number of Versions Left Behind
The model gathers the release history, the installed devices and a distance function in a helper file. isMonotonic checks that the codes strictly increase. versionsBehind counts how many distinct codes are greater than a device’s installed code; two releases that carry the same code count as one, because the platform treats them as the same version.
// VersionModel.kt — models the version code and name, computes the number of versions left behind
class Release(val code: Int, val name: String)
class InstalledDevice(val label: String, val installedCode: Int)
fun isMonotonic(releases: List<Release>): Boolean =
releases.zipWithNext().all { (previous, next) -> next.code > previous.code }
fun latestCode(releases: List<Release>): Int = releases.maxOf { it.code }
fun versionsBehind(releases: List<Release>, device: InstalledDevice): Int =
releases.map { it.code }.distinct().count { it > device.installedCode }
fun releaseHistory(): List<Release> = listOf(
Release(5, "1.8.0"),
Release(6, "1.8.1"),
Release(7, "1.9.0"),
Release(8, "2.0.0"),
Release(9, "2.0.1"),
Release(10, "2.1.0"),
Release(11, "2.2.0"),
Release(12, "2.2.1"),
)
fun installedDevices(): List<InstalledDevice> = listOf(
InstalledDevice("device-a", 12),
InstalledDevice("device-b", 11),
InstalledDevice("device-c", 10),
InstalledDevice("device-d", 9),
InstalledDevice("device-e", 8),
InstalledDevice("device-f", 6),
InstalledDevice("device-g", 5),
InstalledDevice("device-h", 12),
)
The eight releases carry eight different codes and two of the eight devices are on the latest version. We derive the budget as the result of a decision: the team decided not to support devices whose installed code is less than 10. If the lowest supported version code is 10 and the latest code is 12, the number of supported versions back is two.
// Main.kt — the budget is derived from the lowest supported version, eight devices are measured
fun main() {
val releases = releaseHistory()
println("codes monotonic: ${isMonotonic(releases)}")
val minSupported = 10
val budget = Budget("version", latestCode(releases) - minSupported)
val samples = installedDevices().map { versionsBehind(releases, it) }
println("versions behind: $samples")
Overrun("version-distance", budget, samples).line()
}
codes monotonic: true versions behind: [0, 1, 2, 3, 4, 6, 7, 0] version-distance: budget=version<=2 | over=4/8 -> majority
Four devices are more than two versions behind: three, four, six and seven. Because half of the eight exceed the limit, the answer is majority; exactly half counts as majority, not minority. This is the lesson’s overrun. The rule does not see that device-g is seven versions behind and device-d is three; both are samples that overrun.
DI3. The model measures the distance as a number of versions and weighs every version equally. In reality one version can carry a small fix and another a large change; the model does not set up this difference. The model also takes the devices’ installed codes as an input; in reality, when updates are downloaded depends on the user’s settings and the network.
The Budget Is a Decision
If the lowest supported version changes, the budget changes, and the same eight devices give a different overrun. We set three separate budgets: the team supports seven versions back, four versions back, two versions back.
// Main.kt — the same eight devices give three different overruns under three budgets
fun main() {
val releases = releaseHistory()
val samples = installedDevices().map { versionsBehind(releases, it) }
val wide = Overrun("seven-versions", Budget("version", 7), samples)
val middle = Overrun("four-versions", Budget("version", 4), samples)
val narrow = Overrun("two-versions", Budget("version", 2), samples)
listOf(wide, middle, narrow).forEach { it.line() }
countOverruns(listOf(wide, middle, narrow))
}
seven-versions: budget=version<=7 | over=0/8 -> within four-versions: budget=version<=4 | over=2/8 -> minority two-versions: budget=version<=2 | over=4/8 -> majority within: 1 minority: 1 majority: 1 all: 0
With the budget at seven, no device strictly exceeds the limit; a device seven versions behind is equal to the limit, not an overrun. When the budget drops to four, only those six and seven versions behind overrun. When it drops to two, four devices overrun. Three of the four answers came out of the same measurement; all never appeared, because two devices are on the latest version and zero exceeds no valid budget.
These three results are not a contradiction; they are the nature of the measurement. The number of supported versions back is a product decision. The longer the team supports old versions, the heavier the support load becomes; the more it supports, the wider the budget gets and the smaller the overrun. The lesson does not impose a budget; it only shows how the budget determines the overrun.
The Version Name Does Not Affect the Rule
The version name belongs to the user: calling a version 2.3.0 is a marketing or communication decision. The platform reads only the code. We imagine the team changes the version name to 2.3.0 and leaves the code at 12; in a second scenario the same version name is given but the code is increased to 13.
The Bounding Measurement
The bounding measurement asks, before the lesson’s question is tied to one of the four answers of the overrun, which change moves the category. We measure three scenarios with the same eight devices and the same budget: the baseline, only the name changed, and both the name and the code changed. In the first scenario a release with code 12 but the name 2.3.0 is added to the release history.
// Main.kt — if the version name changes and the code does not increase, the distance stays the same
fun main() {
val devices = installedDevices()
val budget = Budget("version", 2)
val before = releaseHistory()
val renamed = before + Release(12, "2.3.0")
val bumped = before + Release(13, "2.3.0")
val baseline = Overrun("baseline", budget, devices.map { versionsBehind(before, it) })
val nameOnly = Overrun("name-only", budget, devices.map { versionsBehind(renamed, it) })
val nameAndCode = Overrun("name-and-code", budget, devices.map { versionsBehind(bumped, it) })
listOf(baseline, nameOnly, nameAndCode).forEach { it.line() }
println("codes monotonic: ${isMonotonic(renamed)} ${isMonotonic(bumped)}")
}
baseline: budget=version<=2 | over=4/8 -> majority name-only: budget=version<=2 | over=4/8 -> majority name-and-code: budget=version<=2 | over=5/8 -> majority codes monotonic: false true
The scenario in which only the name changed gave exactly the same line as the baseline: four samples overrun, the answer is majority. The rule does not see the version name; versionsBehind counts only the codes and the codes did not change. When the code was increased to 13, the number of samples that overrun rose to five and the answer still stayed majority. Here the rule’s second limit appears: the number of overruns increased but the category stayed constant, because the rule reduces the number of samples that exceed the limit to one of four answers.
The result is read in two directions. A name change tells the user that there is a new version but does not make a device up to date; the update decision is made by the code. A code increase, on the other hand, leaves the same eight devices one version further behind: when the latest version is republished, everyone drops back a step. Every release pushes the budget in one direction.
DI4. The model accepts a second release with the same code and counts it as the existing code. In reality the publishing console does not accept a second upload with the same version code; the model does not set up the rejection, because the lesson wants to show that the rule does not look at the version name. In this scenario isMonotonic returns false in the first case: this return is the model’s counterpart of the rejection.
Deriving the Code from the Name
Many teams do not increase the code by hand; they compute it from the version name. A common form is to add the major, minor and patch values with weights. This rule has a silent trap: if the weights are chosen narrow, different names fall onto the same code. We measure the trap. The input is nine version names; this time the list also contains 1.10.0. The budget is zero: every release that shares a code is an overrun. For each release, the number of other releases carrying the same code is the sample.
// Main.kt — in deriving the code from the name, narrow multipliers collapse different names onto the same code
fun foldCode(name: String, minorWeight: Int, majorWeight: Int): Int {
val (major, minor, patch) = name.split(".").map { it.toInt() }
return major * majorWeight + minor * minorWeight + patch
}
fun collisions(names: List<String>, minorWeight: Int, majorWeight: Int): List<Int> {
val codes = names.map { foldCode(it, minorWeight, majorWeight) }
return codes.map { code -> codes.count { it == code } - 1 }
}
fun main() {
val names = listOf("1.8.0", "1.8.1", "1.9.0", "1.10.0", "2.0.0", "2.0.1", "2.1.0", "2.2.0", "2.2.1")
val budget = Budget("collision", 0)
val narrow = collisions(names, 10, 100)
val wide = collisions(names, 100, 10000)
println("narrow multiplier: $narrow")
println("wide multiplier: $wide")
Overrun("narrow-multiplier", budget, narrow).line()
Overrun("wide-multiplier", budget, wide).line()
}
narrow multiplier: [0, 0, 0, 1, 1, 0, 0, 0, 0] wide multiplier: [0, 0, 0, 0, 0, 0, 0, 0, 0] narrow-multiplier: budget=collision<=0 | over=2/9 -> minority wide-multiplier: budget=collision<=0 | over=0/9 -> within
With the narrow multiplier, 1.10.0 and 2.0.0 both produce 200. Because two of the nine releases collide, the answer is minority; with the wide multiplier there is no collision and the answer is within. A collision is not only a table defect: when two releases carry the same code, the platform cannot tell them apart, that is, it cannot see the second as an update. The rule does not see the cause of the collision; it only counts how many releases are affected.
DI5. The model folds the code with a three-field name and two weights. Real teams also use other schemes such as a build number or a date; the model does not set these up and does not say which scheme should be chosen. The only thing measured is the number of collisions.
Common Misconceptions
“If the version name grew, the version is newer.” For the platform, the newer one is the one whose code is greater. A package with a large name and a small code is counted as older than the installed one.
“The version code has a form like 2.2.1.” The version code is an integer. A dotted notation is the job of the version name; teams that split the code into several fields do it by their own rule, folding it into a single integer.
“Deriving the code from the name is always safe.” The derivation seems to preserve monotonicity, but if the weights are chosen narrow it collides. In the measurement above, 1.10.0 and 2.0.0 gave the same code. The derivation rule should be tried against all the names in the release history before it changes.
“Changing only the version name publishes an update.” For the platform, an update is a package whose code increased. A package whose name alone changed is counted as the same version; in this lesson the number of samples that overrun stayed at four.
“A rolled-back version’s code is rolled back.” The code cannot be lowered. A rollback is done with a new build with a higher code; by this lesson’s measurement, the budget is pushed one more version.
“Skipping a version code is harmless.” The code must only increase; skipped numbers do not break the ordering but they go to waste. In a derived code, skipping also appears as a side effect: if the weights are chosen wide, the difference between two consecutive releases grows. The measurement looks only at the ordering between two releases; it does not see the gap in between, so whether the gap has a cost is something the team decides by its own rule.
“A collision only produces the same code.” The real harm of a collision is that two separate releases are counted as the same release for the platform. The second package cannot be installed over the first, because its code is not greater. This is why the collision budget is zero and even a single collision is an overrun.
Summary
- This lesson’s overrun: majority. Four of eight devices fell more than two versions behind: because half exceeded the limit, the answer is majority, not minority.
- The version code is the integer the platform compares and it increases monotonically; the version name is the string shown to the user and does not enter the comparison.
- The budget was derived from the lowest supported version: the difference between 10 and 12 was two. The same eight devices came out within under the budget of seven, minority under the budget of four, majority under the budget of two.
- When only the version name changed, the number of samples that overrun stayed at four; the rule does not see the version name.
- A rule that derives the code from the name collapsed the names
1.10.0and2.0.0onto the same code with a narrow weight: two overruns out of nine releases, the answer minority; with a wide weight, within. - When the version code was increased to 13, the number of samples that overrun rose to five but the answer stayed majority; the rule counts the number and does not see the size.
Next Step
In this lesson the version’s identity was written by hand: versionCode = 12 in a script fragment. In a real team, someone has to increase this number on every change and someone has to produce every build. The next lesson, Continuous Integration and Delivery, measures how many jobs run when a change arrives.
To keep your progress and take notes, Log in
My notes
Log in to take notes.