-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add Latesttag location resolver for init-time tag discovery #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a66aa34
a3613aa
57af83a
28211b8
6049096
5894a65
967a3b7
93c5332
7e562ab
83b2bd6
880c442
e48ac15
56423bf
535fa10
dbdb38d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,15 @@ | ||
| //! Configuration generation utilities for foc-devnet initialization. | ||
| //! | ||
| //! This module handles the generation of default configuration files | ||
| //! and application of location overrides. | ||
| //! and application of location overrides. Dynamic location variants | ||
| //! (`LatestTag`) is resolved to a concrete value at init | ||
| //! time via [`super::latest_resolver`], ensuring the stored config always | ||
| //! records the exact tag that was used. | ||
|
|
||
| use std::fs; | ||
| use tracing::{info, warn}; | ||
|
|
||
| use super::latest_resolver::resolve_location; | ||
| use crate::config::{Config, Location}; | ||
| use crate::paths::foc_devnet_config; | ||
|
|
||
|
|
@@ -67,6 +71,12 @@ pub fn generate_default_config( | |
| "https://github.com/FilOzone/filecoin-services.git", | ||
| )?; | ||
|
|
||
| // Resolve any dynamic variants (LatestTag) by querying the remote. | ||
| // The resolved concrete tag is stored in config.toml for reproducibility. | ||
| config.lotus = resolve_location(config.lotus)?; | ||
| config.curio = resolve_location(config.curio)?; | ||
| config.filecoin_services = resolve_location(config.filecoin_services)?; | ||
|
||
|
|
||
| // Override yugabyte URL if provided | ||
| if let Some(url) = yugabyte_url { | ||
| config.yugabyte_download_url = url; | ||
|
|
@@ -103,6 +113,7 @@ pub fn apply_location_override( | |
| Location::GitTag { ref url, .. } => url.clone(), | ||
| Location::GitCommit { ref url, .. } => url.clone(), | ||
| Location::GitBranch { ref url, .. } => url.clone(), | ||
| Location::LatestTag { ref url, .. } => url.clone(), | ||
| Location::LocalSource { .. } => default_url.to_string(), | ||
| }; | ||
| *location = Location::parse_with_default(&loc_str, &url) | ||
|
|
||
|
redpanda-f marked this conversation as resolved.
Outdated
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| //! Resolves `LatestTag` → `GitTag` at init time. | ||
| //! | ||
| //! When a user specifies `--lotus latesttag:master` (or similar), we need to | ||
| //! figure out which concrete tag that maps to. This module: | ||
| //! | ||
| //! 1. Creates a temporary bare git repo (no working tree, no blobs). | ||
| //! 2. Fetches only the requested branch + tags from the remote. | ||
| //! 3. Picks the newest tag reachable from that branch. | ||
| //! 4. Returns a `GitTag` so the rest of the system works with a pinned version. | ||
| //! | ||
| //! The temp repo is automatically cleaned up when it goes out of scope. | ||
|
|
||
| use crate::config::Location; | ||
| use std::process::Command; | ||
| use tracing::info; | ||
|
|
||
| /// Temporary bare git repo that deletes itself on drop. | ||
| /// | ||
| /// We use a bare repo (no checkout) so we never download actual file content — | ||
| /// only refs and tag metadata. The `--filter=blob:none` fetch flag ensures | ||
| /// this stays lightweight even for large repositories. | ||
| struct TempBareRepo(std::path::PathBuf); | ||
|
redpanda-f marked this conversation as resolved.
Outdated
|
||
|
|
||
| impl TempBareRepo { | ||
| fn create() -> Result<Self, Box<dyn std::error::Error>> { | ||
| let dir = std::env::temp_dir().join(format!( | ||
| "foc-devnet-tag-probe-{}", | ||
| std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .unwrap_or_default() | ||
| .as_nanos() | ||
| )); | ||
| let status = Command::new("git") | ||
| .args(["init", "--bare", dir.to_str().unwrap()]) | ||
| .env("GIT_TERMINAL_PROMPT", "0") | ||
| .status()?; | ||
| if !status.success() { | ||
| return Err("git init --bare failed".into()); | ||
| } | ||
| Ok(Self(dir)) | ||
|
redpanda-f marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| fn path(&self) -> &std::path::Path { | ||
| &self.0 | ||
| } | ||
| } | ||
|
|
||
| impl Drop for TempBareRepo { | ||
| fn drop(&mut self) { | ||
| let _ = std::fs::remove_dir_all(&self.0); | ||
| } | ||
| } | ||
|
|
||
| /// If `location` is `LatestTag`, resolve it to a concrete `GitTag`. | ||
| /// All other variants pass through unchanged. | ||
| /// | ||
| /// Example: `LatestTag { url: "…lotus.git", branch: "master" }` | ||
| /// → `GitTag { url: "…lotus.git", tag: "v1.35.0" }` | ||
| pub fn resolve_location(location: Location) -> Result<Location, Box<dyn std::error::Error>> { | ||
| match location { | ||
| Location::LatestTag { url, branch } => { | ||
| let tag = fetch_latest_tag(&url, &branch)?; | ||
| info!("Resolved latesttag: {} (branch {}) → {}", url, branch, tag); | ||
| Ok(Location::GitTag { url, tag }) | ||
| } | ||
| other => Ok(other), | ||
| } | ||
| } | ||
|
|
||
| /// Fetch the newest tag on `branch` from the remote at `url`. | ||
| /// | ||
| /// Steps: | ||
| /// 1. `git fetch --tags --filter=blob:none <url> refs/heads/<branch>` | ||
| /// — pulls the branch ref and all tags without downloading any file blobs. | ||
| /// 2. `git tag --merged <branch> --sort=-creatordate` | ||
| /// — lists tags reachable from that branch, newest first. | ||
| /// 3. Take the first line → that's the latest tag. | ||
| fn fetch_latest_tag(url: &str, branch: &str) -> Result<String, Box<dyn std::error::Error>> { | ||
| info!("Fetching newest tag on branch '{}' from {}", branch, url); | ||
|
|
||
| let repo = TempBareRepo::create()?; | ||
| let refspec = format!("refs/heads/{b}:refs/heads/{b}", b = branch); | ||
|
|
||
| // Fetch branch + tags (no blobs — keeps it fast) | ||
| let fetch = Command::new("git") | ||
| .args(["fetch", "--tags", "--filter=blob:none", url, &refspec]) | ||
| .current_dir(repo.path()) | ||
| .env("GIT_TERMINAL_PROMPT", "0") | ||
| .status()?; | ||
| if !fetch.success() { | ||
| return Err(format!("git fetch failed for {} (branch {})", url, branch).into()); | ||
|
redpanda-f marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // List tags reachable from the branch, newest first | ||
| let tags = Command::new("git") | ||
| .args(["tag", "--merged", branch, "--sort=-creatordate"]) | ||
|
redpanda-f marked this conversation as resolved.
Outdated
|
||
| .current_dir(repo.path()) | ||
| .output()?; | ||
| if !tags.status.success() { | ||
| return Err(format!( | ||
| "git tag --merged {} failed: {}", | ||
| branch, | ||
| String::from_utf8_lossy(&tags.stderr).trim() | ||
| ) | ||
| .into()); | ||
| } | ||
|
|
||
| // First non-empty line is the newest tag | ||
| let stdout = String::from_utf8_lossy(&tags.stdout); | ||
| stdout | ||
| .lines() | ||
| .map(str::trim) | ||
| .find(|l| !l.is_empty()) | ||
| .map(str::to_string) | ||
| .ok_or_else(|| format!("No tags found on branch '{}' for {}", branch, url).into()) | ||
| } | ||
|
redpanda-f marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.