Keep what a build recorded when a declaration is accepted
Accepting a project's declaration replaced its components with the declared ones and carried across two of the three things this machine knows about them -- the package read out of an APK and the mode somebody picked here -- while dropping `builtFrom`. So every card of that project went back to saying it had never been built here: no freshness, nothing flagged, and Update with nothing to do, which is exactly what "I switched branches on ai-app and nothing was flagged" looks like from the phone. Silent, too, because "never built here" is a true sentence about plenty of projects. The same field went missing from the built-in row's startup reconciliation last week; two carry-across pairs remembered separately is what produced both, so there is now one `CarriedOver` for all three and both callers take it whole. The built-in card's own update no longer goes through the self-update dialog. Its Update pulls, builds and installs this app like any other card's, downloading over the frozen /self/apk route -- the build that produced the APK rebuilt this server too, so the one download that has to survive the server changing underneath it takes the route nothing can rename. It waits for the server to be answering again both before fetching and before handing anything to the installer, since the restart lands somewhere in that window either way. The dialog stays for what it was written for: it is offered when the built-in card is missing from the list, or when there is no list at all because this app is too old to read what the server now sends. That is the one case where nothing on screen can offer the update itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f359cd3edf
commit
a8f75be190
3 files changed
+201
-91
No files matched your search
+102
-76
@@ -319,58 +319,58 @@ fn component_id(component: &Component) -> ComponentId {
|
||||
)
|
||||
}
|
||||
|
||||
/// What this machine chose about each component, for carrying across a
|
||||
/// rewrite of the components list.
|
||||
/// Everything about a component that this machine either measured or was
|
||||
/// told here, for carrying across a rewrite of the components list.
|
||||
///
|
||||
/// The mirror of [`measured_packages`], and here for the same reason:
|
||||
/// both acceptance and the self entry's startup reconciliation replace
|
||||
/// `components` wholesale with what the checkout declares, which is right
|
||||
/// for everything the project asked for and wrong for everything somebody
|
||||
/// chose here. Without it, choosing to build this server in `debug`
|
||||
/// would last exactly until the next restart -- and restarting is how
|
||||
/// this server is updated, so it would never last at all.
|
||||
/// There are two places that rewrite one -- accepting a declaration, and
|
||||
/// the built-in row's reconciliation at every startup -- and both replace
|
||||
/// `components` wholesale with what the checkout declares. That is right
|
||||
/// for everything the project asked for and wrong for everything this
|
||||
/// side knows: the package read out of an APK, the mode somebody picked,
|
||||
/// the commit a build recorded. One type for all three rather than a
|
||||
/// collect-and-restore pair each, because the pairs were remembered
|
||||
/// individually and one of them was not -- acceptance carried the package
|
||||
/// and the chosen mode and dropped `builtFrom`, so accepting a project's
|
||||
/// declaration told every one of its cards it had never been built here.
|
||||
/// Silent, because that is a true sentence about plenty of projects and
|
||||
/// so reads as an answer rather than as a loss. Adding a field to a
|
||||
/// component now means adding it here once, and both callers get it.
|
||||
///
|
||||
/// Keyed the same way for the same reason: once a component's directory
|
||||
/// decides which builds are its own, a reused name is a different
|
||||
/// component, and handing it the old one's mode would build something
|
||||
/// nobody asked for.
|
||||
fn chosen_settings(components: &[Component]) -> HashMap<ComponentId, crate::config::Choices> {
|
||||
components
|
||||
.iter()
|
||||
.map(|component| (component_id(component), component.choices()))
|
||||
.collect()
|
||||
/// Keyed by name *and* directory like everything else here: once a
|
||||
/// component's directory decides which builds are its own, a reused name
|
||||
/// is a different component, and handing it the old one's package or
|
||||
/// commit would be wrong until that component happened to be built.
|
||||
#[derive(Default)]
|
||||
struct CarriedOver {
|
||||
packages: HashMap<ComponentId, String>,
|
||||
chosen: HashMap<ComponentId, crate::config::Choices>,
|
||||
built: HashMap<ComponentId, (Option<String>, Option<String>)>,
|
||||
}
|
||||
|
||||
fn restore_chosen_settings(
|
||||
components: &mut [Component],
|
||||
chosen: &HashMap<ComponentId, crate::config::Choices>,
|
||||
) {
|
||||
for component in components {
|
||||
if let Some(choices) = chosen.get(&component_id(component)) {
|
||||
component.restore(choices.clone());
|
||||
}
|
||||
fn carried_over(components: &[Component]) -> CarriedOver {
|
||||
CarriedOver {
|
||||
packages: measured_packages(components),
|
||||
chosen: components
|
||||
.iter()
|
||||
.map(|component| (component_id(component), component.choices()))
|
||||
.collect(),
|
||||
built: components
|
||||
.iter()
|
||||
.map(|component| (component_id(component), component.built()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a build recorded about each component, keyed the way every other
|
||||
/// carry-across here is: by name *and* directory, so a reused name for a
|
||||
/// component that now builds somewhere else does not inherit the old one's
|
||||
/// commit.
|
||||
fn built_records(
|
||||
components: &[Component],
|
||||
) -> HashMap<ComponentId, (Option<String>, Option<String>)> {
|
||||
components
|
||||
.iter()
|
||||
.map(|component| (component_id(component), component.built()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn restore_built(
|
||||
components: &mut [Component],
|
||||
recorded: &HashMap<ComponentId, (Option<String>, Option<String>)>,
|
||||
) {
|
||||
fn restore_carried_over(components: &mut [Component], carried: &CarriedOver) {
|
||||
for component in components {
|
||||
if let Some(built) = recorded.get(&component_id(component)) {
|
||||
let id = component_id(component);
|
||||
if let Some(package) = carried.packages.get(&id) {
|
||||
component.set_package(package.clone());
|
||||
}
|
||||
if let Some(choices) = carried.chosen.get(&id) {
|
||||
component.restore(choices.clone());
|
||||
}
|
||||
if let Some(built) = carried.built.get(&id) {
|
||||
component.set_built(built.clone());
|
||||
}
|
||||
}
|
||||
@@ -793,11 +793,14 @@ impl AppState {
|
||||
/// thing that rewrites that file is a pull the same person just
|
||||
/// performed.
|
||||
///
|
||||
/// Measured values are carried across rather than reset. A declaration
|
||||
/// says nothing about the package a build produces -- that is read from
|
||||
/// the APK -- so accepting one must not throw away what was already
|
||||
/// read, or every acceptance would blank the installed-version check
|
||||
/// until the next build.
|
||||
/// What this machine measured or was told is carried across rather
|
||||
/// than reset, through [`CarriedOver`]. A declaration says nothing
|
||||
/// about the package a build produces, the mode somebody picked here,
|
||||
/// or the commit a build was made from -- so accepting one must not
|
||||
/// throw any of them away. Dropping the package would blank the
|
||||
/// installed-version check until the next build; dropping the commit
|
||||
/// did worse, because a component with nothing recorded reads as one
|
||||
/// this server has never built.
|
||||
pub fn approve_declaration(&self, key: &str) -> Result<()> {
|
||||
let entry = self
|
||||
.entry(key)
|
||||
@@ -816,8 +819,7 @@ impl AppState {
|
||||
.iter_mut()
|
||||
.find(|project| project.key == key)
|
||||
.with_context(|| format!("{key} is built in and has nothing to accept"))?;
|
||||
let measured = measured_packages(&project.components);
|
||||
let chosen = chosen_settings(&project.components);
|
||||
let carried = carried_over(&project.components);
|
||||
project.git_pull = declared.git_pull;
|
||||
// Everything `matches_accepted` compares has to be written
|
||||
// here, or accepting cannot clear the gate. `resources` joined
|
||||
@@ -828,17 +830,12 @@ impl AppState {
|
||||
// nothing. Add a field to the gate and add it here.
|
||||
project.resources = declared.resources;
|
||||
project.components = declared.components;
|
||||
for component in &mut project.components {
|
||||
if let Some(package) = measured.get(&component_id(component)) {
|
||||
component.set_package(package.clone());
|
||||
}
|
||||
}
|
||||
// Accepting is about what the project asks to have run. It is
|
||||
// not somebody withdrawing the mode they picked or the strip
|
||||
// they turned off, so those survive it -- exactly as the
|
||||
// measured package does, and for the same reason: neither is
|
||||
// part of what was being agreed to.
|
||||
restore_chosen_settings(&mut project.components, &chosen);
|
||||
// they turned off, and it is not this machine forgetting what
|
||||
// it built -- none of those is part of what was being agreed
|
||||
// to.
|
||||
restore_carried_over(&mut project.components, &carried);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -920,29 +917,28 @@ fn reconcile_self(config: &mut Config, self_project: &Path) -> bool {
|
||||
"app".to_string()
|
||||
}
|
||||
};
|
||||
set_measured_package(&mut components, &apk_name, SELF_PACKAGE.to_string());
|
||||
let existing = config
|
||||
.projects
|
||||
.iter_mut()
|
||||
.find(|project| project.key == SELF_KEY);
|
||||
// Carried across the row being rebuilt, like `gitIpv4` below: what is
|
||||
// *derived* here is what the checkout declares, and a mode chosen for
|
||||
// this server's own component is not that. Restarting is how this
|
||||
// server takes an update, so a choice that did not survive a restart
|
||||
// would not survive being acted on.
|
||||
// *derived* here is what the checkout declares, and neither a mode
|
||||
// chosen for this server's own component nor the commit its last
|
||||
// build was made from is that. It matters more here than at an
|
||||
// acceptance, because this row is re-derived at *every* startup and
|
||||
// restarting is how this server takes its own update -- so a choice
|
||||
// that did not survive a restart would not survive being acted on,
|
||||
// and a commit that did not left this server's own card with no
|
||||
// freshness for ever after: never behind, never current, and its
|
||||
// Update with nothing to do, since both readings need a commit to
|
||||
// compare against.
|
||||
if let Some(existing) = existing.as_ref() {
|
||||
restore_chosen_settings(&mut components, &chosen_settings(&existing.components));
|
||||
// And what a build here measured, for the same reason and with
|
||||
// more force: this row is re-derived at *every* startup, and
|
||||
// restarting is how this server takes its own update -- so a
|
||||
// commit recorded by the build that produced the new binary was
|
||||
// forgotten by the process it started. Its own card then had no
|
||||
// freshness for ever after: never behind, never current, and its
|
||||
// Update with nothing to do, because both readings need a commit
|
||||
// to compare against. Carried across rather than re-derived,
|
||||
// because nothing but a build can know it.
|
||||
restore_built(&mut components, &built_records(&existing.components));
|
||||
restore_carried_over(&mut components, &carried_over(&existing.components));
|
||||
}
|
||||
// After the carry-across, so what is derived wins: this is the one
|
||||
// project whose package this server states rather than reads, and an
|
||||
// older row carrying something else must not override it.
|
||||
set_measured_package(&mut components, &apk_name, SELF_PACKAGE.to_string());
|
||||
let derived = ProjectConfig {
|
||||
key: SELF_KEY.to_string(),
|
||||
label: declared.label.unwrap_or_else(|| SELF_LABEL.to_string()),
|
||||
@@ -1379,6 +1375,36 @@ mod tests {
|
||||
assert_eq!(component.build().to_line(), "d");
|
||||
}
|
||||
|
||||
/// The commit a build recorded is a measurement of this machine, not
|
||||
/// something the project's declaration has an opinion about -- so it
|
||||
/// survives that declaration being accepted again, exactly as the
|
||||
/// chosen mode and the measured package do. It did not, and the cost
|
||||
/// was invisible: with nothing recorded a component reads as one this
|
||||
/// server has never built, which is a true sentence about plenty of
|
||||
/// projects and so looks like an answer rather than a loss. Every
|
||||
/// card of a project whose declaration had ever been accepted said it.
|
||||
#[test]
|
||||
fn what_a_build_recorded_survives_the_declaration_being_accepted_again() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
write_moded_request(dir.path());
|
||||
let (_home, state) = state_with(dir.path());
|
||||
state.approve_declaration("demo").expect("approve");
|
||||
state.record_built("demo", "app", "c0ffee".to_string());
|
||||
|
||||
state.approve_declaration("demo").expect("approve again");
|
||||
let component = &state.entry("demo").expect("entry").components[0];
|
||||
assert_eq!(
|
||||
component.built_from(),
|
||||
Some("c0ffee"),
|
||||
"accepting must not tell the card this component was never built here",
|
||||
);
|
||||
assert_eq!(
|
||||
component.built_mode(),
|
||||
Some("release"),
|
||||
"and the mode goes with the commit, or the record describes a build nobody made",
|
||||
);
|
||||
}
|
||||
|
||||
/// A mode this component does not declare is refused rather than
|
||||
/// stored: `ByMode::get` falls back to the first, so storing it would
|
||||
/// leave the card naming a mode nothing is built in -- which reads
|
||||
|
||||
Reference in new issue
Block a user