Skip to content

Commit c835b59

Browse files
committed
Put the commit first in the build stamp, and compare only that
#809 gave the stamp a second half and left the comparison reading the whole string. A canary reports `JingMatrix-Vector-93d66473` and no release SHA starts with that, so `divergesFrom` was true for the very release the reader had just flashed: the install bar warned "same number, other build", the versions sheet coloured that row divergent, and since installed is sameNumber && !diverged, no row was ever marked as installed. The `-dirty` branch it still tested had become unreachable in the same commit. The stamp now leads with the commit -- `93d66473-JingMatrix-Vector` -- so reading it back is a prefix and nothing more, whatever hyphens a repository or a machine turns out to have in its name. A modified tree is marked `+` rather than `-`, in semver's sense of build metadata: after a `-` is a repository holding this exact commit, after a `+` are changes no repository holds. Without that distinction a hand-built framework would claim to be the release it was merely started from. `buildStamp` takes one apart, and `isCommit` compares as a prefix in either direction, since a stamp carries git's short form and a release the full SHA. Where a build was made is deliberately not compared: a fork building the same commit builds the same code. A stamp that names no commit -- "unknown", or the shape published between #809 and here -- is "I cannot tell" rather than divergence, so the canaries already flashed claim neither. On the status page the commit keeps the size it is read at and the rest is set smaller and muted, since it is two thirds of the characters and almost never what the row is looked up for. Copying the page still yields the whole stamp; de-emphasising it must not shorten it.
1 parent b1e44f2 commit c835b59

7 files changed

Lines changed: 188 additions & 38 deletions

File tree

build.gradle.kts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,26 @@ abstract class GitLatestTagValueSource : ValueSource<String, ValueSourceParamete
6161
}
6262

6363
/**
64-
* Which build this is, in one string: where it was built and from what commit.
64+
* Which build this is, in one string: what commit it came from and where it was built.
6565
*
6666
* The version code is the commit count on origin/master, so every branch build carries master's
6767
* number: a build flashed from a feature branch and one flashed from master both report "v2.0
6868
* (3052)" and cannot be told apart on the device. That is not hypothetical — it cost a real
6969
* investigation to establish which of the two was installed.
7070
*
71-
* Two shapes, because "where did this binary come from" has two different answers:
71+
* **The commit always comes first, and what follows always says where.** Both readers of this string
72+
* want the commit and nothing else — the manager compares it against the SHA a release was cut from
73+
* to answer "am I running this build", and the status page sets it in a type the rest is not given
74+
* — and for both it is now simply the head of the string. No rule has to work out which part is
75+
* which, so none can get it wrong when a repository or a machine turns out to have a hyphen in its
76+
* name.
7277
*
73-
* *On GitHub Actions*, `JingMatrix-Vector-93d66473` — the repository the code came from, then the
74-
* commit. Forks build the same code from the same commits, so the hash alone identifies a
75-
* *revision* and not a *build*, and a fork's artifact was indistinguishable from ours.
78+
* The separator says which kind of "where" follows, because they mean opposite things about whether
79+
* the commit describes the binary:
80+
*
81+
* *On GitHub Actions*, `93d66473-JingMatrix-Vector` — the commit, then the repository the code came
82+
* from. Forks build the same code from the same commits, so the hash alone identifies a *revision*
83+
* and not a *build*, and a fork's artifact was indistinguishable from ours.
7684
*
7785
* Both halves are the *head* ones, and neither is read from the environment GitHub sets by default,
7886
* because on a pull request both defaults name something other than the code that was built:
@@ -91,11 +99,17 @@ abstract class GitLatestTagValueSource : ValueSource<String, ValueSourceParamete
9199
* when there is not — outside a pull request the two agree anyway. Absent either, this falls back to
92100
* what the environment says and to `HEAD`, which covers a workflow too old to set them.
93101
*
94-
* *Locally*, the bare `93d66473`, or `93d66473-thinkpad` when the tree has uncommitted changes.
95-
* That build corresponds to no commit at all, so naming the commit alone would name something the
96-
* binary does not match — and whoever is looking at it is far better served by the machine it was
97-
* built on than by the word "dirty", since a device that has a hand-built framework on it usually
98-
* has exactly one candidate author.
102+
* *Locally*, the bare `93d66473`, or `93d66473+thinkpad` when the tree has uncommitted changes. That
103+
* build corresponds to no commit at all, so naming the commit alone would name something the binary
104+
* does not match — and whoever is looking at it is far better served by the machine it was built on
105+
* than by the word "dirty", since a device that has a hand-built framework on it usually has exactly
106+
* one candidate author.
107+
*
108+
* `+` rather than `-` for that one, in semver's sense of build metadata: after a `-` is a repository
109+
* that holds this exact commit, and after a `+` is a change that is in no repository at all. That is
110+
* the one distinction a reader of the stamp cannot afford to lose — a modified build matches no
111+
* release, and read as a CI build it would claim to be the release it was merely started from.
112+
* Neither character can occur inside a host name or an `owner/repo`, so the two shapes stay apart.
99113
*
100114
* The dirty check is deliberately not applied on CI. The workflow's own "Write key" step appends
101115
* the signing credentials to the tracked `gradle.properties` before Gradle starts, so every master
@@ -165,7 +179,7 @@ abstract class GitCommitHashValueSource : ValueSource<String, GitCommitHashValue
165179
val repository = parameters.buildRepository.getOrElse("")
166180
if (repository.isBlank()) {
167181
val dirty = capture("git", "status", "--porcelain", "--untracked-files=no") != null
168-
return if (dirty) "$head-${hostname()}" else head
182+
return if (dirty) "$head+${hostname()}" else head
169183
}
170184

171185
// The pushed commit is an ancestor of the merge that was checked out, so it is in the
@@ -174,7 +188,7 @@ abstract class GitCommitHashValueSource : ValueSource<String, GitCommitHashValue
174188
val pushed = parameters.buildCommit.getOrElse("").takeIf { it.isNotBlank() }
175189
val short =
176190
pushed?.let { capture("git", "rev-parse", "--short", it) ?: it.take(head.length) } ?: head
177-
return repository.replace('/', '-') + "-" + short
191+
return short + "-" + repository.replace('/', '-')
178192
}
179193
}
180194

manager/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ asserting the old meaning until someone notices, which can be a long time.
8686
```
8787

8888
The version code is `git rev-list --count refs/remotes/origin/master`, so a branch build and a
89-
master build can share one; `module.prop` carries the commit, marked `-dirty` when the tree was not
90-
clean, which is often the only way to tell two builds apart on a device.
89+
master build can share one; `module.prop` and the status page carry a build stamp, which is often
90+
the only way to tell two builds apart on a device. It names where the build came from as well as
91+
what it was built from. The commit leads and what follows says where: `93d66473-JingMatrix-Vector`
92+
for a CI build, the bare `93d66473` for a local one, and `93d66473+thinkpad` — the machine that made
93+
it — when the tree was not clean.
9194

9295
Debug builds add a second launcher activity — a demo mode with scripted device states, in
9396
`src/debug` and absent from release builds, so it cannot be used to make a release report a healthy
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.matrix.vector.manager.data.model
2+
3+
/**
4+
* A build stamp, taken apart: which commit a build came from, and where it was built.
5+
*
6+
* The framework and the manager both report one — `BuildConfig.VERSION_HASH`, and
7+
* `getFrameworkCommit()` across the binder — because the version code cannot tell two builds apart:
8+
* it is the commit count on origin/master, so every branch build at the same depth wears the same
9+
* number as the official build it was never made from.
10+
*
11+
* Only the commit is taken out. Where a build was made is for a person to read, and it is exactly
12+
* the rest of the string — a caller that wants it takes what follows the commit, separator and all.
13+
*
14+
* @property commit the commit the build was made from, abbreviated as git abbreviated it, or null
15+
* when the stamp names none — "unknown" from a build made outside a git checkout, and anything
16+
* else this does not recognise. That is a third answer, distinct from "these differ".
17+
* @property modified whether the tree had uncommitted changes. Such a build was made from no commit
18+
* at all: [commit] names the one it departed from, not one the binary corresponds to.
19+
*/
20+
data class BuildStamp(val commit: String?, val modified: Boolean) {
21+
22+
/**
23+
* Whether this build and [other] were made from the same commit.
24+
*
25+
* A modified tree matches nothing, including itself. Otherwise it is a prefix test in either
26+
* direction, because the two sides need not be abbreviated to the same length: a stamp carries
27+
* git's short form and a GitHub release carries the full SHA.
28+
*
29+
* **Where the build was made is deliberately not compared.** A fork building the same commit
30+
* builds the same code; the repository is in the stamp to identify a *binary* to someone
31+
* reading a bug report, which is a different question from "is this the build that release
32+
* published".
33+
*/
34+
fun isCommit(other: String?): Boolean {
35+
if (modified || other == null) return false
36+
val ours = commit ?: return false
37+
return other.startsWith(ours) || ours.startsWith(other)
38+
}
39+
}
40+
41+
/**
42+
* Reads a build stamp.
43+
*
44+
* The commit leads, always, and the separator says what follows it — `-` a repository that holds
45+
* this exact commit, `+` a machine that holds changes no repository does. So the commit is the head
46+
* and there is nothing to guess: neither character can occur inside a host name or an `owner/repo`,
47+
* and a hyphen inside either is harmless because everything after the first one is the origin.
48+
*
49+
* - `93d66473` — a local build of a clean tree.
50+
* - `93d66473-JingMatrix-Vector` — CI, from `JingMatrix/Vector`.
51+
* - `93d66473+thinkpad` — a local build with uncommitted changes, named by the machine that made it.
52+
*
53+
* A stamp that does not start with a commit yields a null one, which everywhere it is asked means
54+
* "I cannot tell" rather than "these differ". That covers the shape shipped between #809 and this
55+
* change, `JingMatrix-Vector-93d66473`, which is what the canaries already on people's devices
56+
* carry: they are shown as they were recorded and claimed to be neither installed nor divergent,
57+
* which is the truthful answer for a build whose stamp this cannot read.
58+
*/
59+
fun buildStamp(reported: String): BuildStamp {
60+
val stamp = reported.trim()
61+
val head = stamp.takeWhile { it != '-' && it != '+' }
62+
if (!head.isAbbreviatedSha()) return BuildStamp(null, modified = false)
63+
return BuildStamp(commit = head, modified = stamp.getOrNull(head.length) == '+')
64+
}
65+
66+
/**
67+
* Whether this could be a commit as git abbreviates one.
68+
*
69+
* Seven is git's own floor for an abbreviation and forty is a full SHA. Lower case only, which git
70+
* emits and which is what keeps the word "unknown" — a build made where git could not be asked —
71+
* from being mistaken for one.
72+
*/
73+
private fun String.isAbbreviatedSha(): Boolean =
74+
length in 7..40 && all { it.isDigit() || it in 'a'..'f' }

manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkUpdateRepository.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.StateFlow
55
import kotlinx.coroutines.flow.asStateFlow
66
import org.matrix.vector.manager.data.github.FrameworkRelease
77
import org.matrix.vector.manager.data.github.GitHubRepository
8+
import org.matrix.vector.manager.data.model.buildStamp
89

910
/**
1011
* Whether a newer build of the framework exists, and which one this reader may be offered.
@@ -70,7 +71,12 @@ class FrameworkUpdateRepository(private val github: GitHubRepository) {
7071
*/
7172
data class FrameworkUpdateState(
7273
val installedVersionCode: Long = 0,
73-
/** The commit the running daemon was built from, when it recorded one. */
74+
/**
75+
* The build stamp the running daemon reports, when it recorded one.
76+
*
77+
* Not a bare hash: it names where the build came from as well as what commit it was made from,
78+
* in one of the shapes [buildStamp] reads. Nothing here compares it as a string.
79+
*/
7480
val installedCommit: String? = null,
7581
val available: FrameworkRelease? = null,
7682
/** Every release on this channel, newest first — including ones older than the installed one. */
@@ -93,12 +99,16 @@ enum class ReleaseDirection {
9399
* Only askable when both sides recorded a commit: the canaries carry a SHA, a hand-made release
94100
* carries a branch name, and a build made before this existed carries nothing. "I cannot tell" is a
95101
* third answer and is reported as false rather than as divergence.
102+
*
103+
* What the framework reports is a *build stamp*, not a bare hash, so the commit is read out of it
104+
* before anything is compared. Comparing the whole stamp is what #809 left behind: a CI stamp
105+
* carries the repository as well, no release SHA matches that, and so every canary reader was told
106+
* they were running "same number, other build" against the very release they had flashed, with no
107+
* row anywhere marked as installed.
96108
*/
97109
fun FrameworkUpdateState.divergesFrom(release: FrameworkRelease?): Boolean {
98110
if (release == null || release.versionCode != installedVersionCode) return false
99-
val mine = installedCommit ?: return false
100-
val theirs = release.commit ?: return false
101-
// A dirty build matches nothing by definition — it was not built from any commit.
102-
if (mine.endsWith("-dirty")) return true
103-
return !theirs.startsWith(mine)
111+
val mine = buildStamp(installedCommit ?: return false)
112+
if (mine.commit == null || release.commit == null) return false
113+
return !mine.isCommit(release.commit)
104114
}

manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ data class FrameworkStatus(
4747
val sepolicyLoaded: Boolean = false,
4848
val systemServerInjected: Boolean = false,
4949
/**
50-
* Which build the running framework is: where it came from and from what commit.
50+
* Which build the running framework is: what commit it came from and where it was built.
5151
*
5252
* The version code is a commit count, so it cannot tell a branch build from the official build
5353
* of the same depth — and the framework and the manager are flashed separately, so they are
5454
* not always the same build. Naming both is the difference between a bug report that can be
55-
* placed and one that cannot. A CI build reads `JingMatrix-Vector-93d66473`, a clean local one
56-
* the bare hash, and a local build from a modified tree adds the machine that made it.
55+
* placed and one that cannot. The commit leads: a CI build reads `93d66473-JingMatrix-Vector`,
56+
* a clean local one the bare hash, and a local build from a modified tree marks the machine
57+
* that made it with a `+`. Taken apart by `buildStamp`; nothing compares it as a string.
5758
*/
5859
val commit: String? = null,
5960
) {

0 commit comments

Comments
 (0)