max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
1 file changed,
+51 insertions,
-7 deletions
| @@ -722,6 +722,50 @@ | |||
| 722 | 722 | ); | |
| 723 | 723 | resolve_artifact_match(&tail, glob, required) | |
| 724 | 724 | } | |
| 725 | + | ||
| 726 | + | // --- Run-state accessors. | |
| 727 | + | // | |
| 728 | + | // The four fields below are private and `Mutex`-wrapped, and the modules | |
| 729 | + | // that read them are siblings of this one rather than descendants, so a | |
| 730 | + | // sibling cannot reach the field. Each accessor hands out a value and never | |
| 731 | + | // the guard: a returned guard invites a lock held across a sibling's await. | |
| 732 | + | ||
| 733 | + | /// The steps finalized as `Failed` so far, as a snapshot. | |
| 734 | + | /// | |
| 735 | + | /// Cloned rather than borrowed because `PublishAuthority::prove` wants a | |
| 736 | + | /// slice and the caller slices this, which is the shape that keeps the lock | |
| 737 | + | /// out of the caller's scope. | |
| 738 | + | pub(super) fn failed_steps_snapshot(&self) -> Vec<Step> { | |
| 739 | + | self.failed_steps.lock().unwrap().clone() | |
| 740 | + | } | |
| 741 | + | ||
| 742 | + | /// Record the digest `collect` computed for one artifact, by file name. | |
| 743 | + | pub(super) fn record_artifact_hash(&self, name: String, digest: String) { | |
| 744 | + | self.artifact_hashes.lock().unwrap().insert(name, digest); | |
| 745 | + | } | |
| 746 | + | ||
| 747 | + | /// One artifact's recorded digest, if `collect` hashed it. | |
| 748 | + | /// | |
| 749 | + | /// The targeted read beside [`RecipeCtx::artifact_hashes`], which clones the | |
| 750 | + | /// whole map; `publish` wants exactly one entry. | |
| 751 | + | pub(super) fn artifact_hash(&self, name: &str) -> Option<String> { | |
| 752 | + | self.artifact_hashes.lock().unwrap().get(name).cloned() | |
| 753 | + | } | |
| 754 | + | ||
| 755 | + | /// The all-targets-green publish gate, if this app declares one. | |
| 756 | + | pub(super) fn all_green_required(&self) -> Option<Vec<Target>> { | |
| 757 | + | self.all_green_required.clone() | |
| 758 | + | } | |
| 759 | + | ||
| 760 | + | /// The gatekeeper verdict: `None` = never run, `Some(false)` = rejected. | |
| 761 | + | pub(super) fn gatekeeper_ok(&self) -> Option<bool> { | |
| 762 | + | *self.gatekeeper_ok.lock().unwrap() | |
| 763 | + | } | |
| 764 | + | ||
| 765 | + | /// Record what the gatekeeper said about this artifact. | |
| 766 | + | pub(super) fn set_gatekeeper_ok(&self, accepted: bool) { | |
| 767 | + | *self.gatekeeper_ok.lock().unwrap() = Some(accepted); | |
| 768 | + | } | |
| 725 | 769 | } | |
| 726 | 770 | ||
| 727 | 771 | // ----- error bridging: anyhow -> Rhai runtime error ----- | |
| @@ -1972,7 +2016,7 @@ | |||
| 1972 | 2016 | // A failed earlier step bars a deploy exactly as it bars a publish. An | |
| 1973 | 2017 | // artifact that failed its gates must not reach a production host just | |
| 1974 | 2018 | // because the recipe kept running. | |
| 1975 | - | let failed = self.failed_steps.lock().unwrap().clone(); | |
| 2019 | + | let failed = self.failed_steps_snapshot(); | |
| 1976 | 2020 | anyhow::ensure!( | |
| 1977 | 2021 | failed.is_empty(), | |
| 1978 | 2022 | "refusing to deploy {} {}: {} failed earlier in this run", | |
| @@ -2132,7 +2176,7 @@ | |||
| 2132 | 2176 | .map_or_else(|| rel.clone(), |n| n.to_string_lossy().into_owned()); | |
| 2133 | 2177 | assert_artifact_version(&name, &self.version)?; | |
| 2134 | 2178 | let digest = sha256_file(&path)?; | |
| 2135 | - | self.artifact_hashes.lock().unwrap().insert(rel, digest); | |
| 2179 | + | self.record_artifact_hash(rel, digest); | |
| 2136 | 2180 | } | |
| 2137 | 2181 | // Deposit at the archive path, so this target's bytes have one address | |
| 2138 | 2182 | // whichever host produced them. A no-op when no archive is configured. | |
| @@ -2230,7 +2274,7 @@ | |||
| 2230 | 2274 | // or still building. The publishing target itself is the last mile (it | |
| 2231 | 2275 | // reached publish, so its steps passed) and is not required to be green | |
| 2232 | 2276 | // in the ledger yet. | |
| 2233 | - | if let Some(declared) = self.all_green_required.clone() { | |
| 2277 | + | if let Some(declared) = self.all_green_required() { | |
| 2234 | 2278 | self.assert_siblings_green(&declared)?; | |
| 2235 | 2279 | } | |
| 2236 | 2280 | let backend = self | |
| @@ -2286,8 +2330,8 @@ | |||
| 2286 | 2330 | // called without one, so the unverified/post-failure ship path is sealed | |
| 2287 | 2331 | // at the type level rather than guarded by a separate runtime check. | |
| 2288 | 2332 | let authority = { | |
| 2289 | - | let failed = self.failed_steps.lock().unwrap(); | |
| 2290 | - | let gatekeeper = *self.gatekeeper_ok.lock().unwrap(); | |
| 2333 | + | let failed = self.failed_steps_snapshot(); | |
| 2334 | + | let gatekeeper = self.gatekeeper_ok(); | |
| 2291 | 2335 | PublishAuthority::prove(target, failed.as_slice(), gatekeeper)? | |
| 2292 | 2336 | }; | |
| 2293 | 2337 | let notes = meta | |
| @@ -2328,7 +2372,7 @@ | |||
| 2328 | 2372 | let artifact_hash: Option<String> = artifact_path | |
| 2329 | 2373 | .file_name() | |
| 2330 | 2374 | .and_then(|n| n.to_str()) | |
| 2331 | - | .and_then(|n| self.artifact_hashes.lock().unwrap().get(n).cloned()) | |
| 2375 | + | .and_then(|n| self.artifact_hash(n)) | |
| 2332 | 2376 | .or_else(|| sha256_file(&artifact_path).ok()); | |
| 2333 | 2377 | let me = self.clone(); | |
| 2334 | 2378 | let (app_s, target_s, ver_s, chan_s) = ( | |
| @@ -2408,7 +2452,7 @@ | |||
| 2408 | 2452 | // Record the verdict for the publish gate. A rejection also | |
| 2409 | 2453 | // fails the step, so the matrix shows red and `publish` is barred | |
| 2410 | 2454 | // even if the recipe ignores the returned bool. | |
| 2411 | - | *ctx.gatekeeper_ok.lock().unwrap() = Some(accepted); | |
| 2455 | + | ctx.set_gatekeeper_ok(accepted); | |
| 2412 | 2456 | if !accepted { | |
| 2413 | 2457 | ctx.fail_current_step(); | |
| 2414 | 2458 | } |