Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,15 +573,17 @@ use `--app-id` (and optionally `--rid`) to scope down.
surge restore -i
```

By default this resolves the latest release for the manifest app/target and default channel, restores missing full
packages from storage into `.surge/packages`, and builds installers using artifacts from
By default this resolves the latest release for the manifest app/target on the app's default channel, restores missing
full packages from storage into `.surge/packages`, and builds installers using artifacts from
`.surge/artifacts/<app-id>/<rid>/<version>`. The generated installers are written to
`.surge/installers/<app-id>/<rid>`.
`.surge/installers/<app-id>/<rid>`. Use `--channel <name>` to rebuild installers for a non-default channel after a
promotion flow.

Explicit override example:

```bash
surge restore -i \
--channel production \
--version 1.2.3 \
--artifacts-dir ./publish \
--packages-dir .surge/packages
Expand Down
29 changes: 29 additions & 0 deletions crates/surge-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ pub(crate) enum Commands {
#[arg(long)]
version: Option<String>,

/// Channel to resolve releases from when using --installers
#[arg(long, requires = "installers")]
channel: Option<String>,

/// Build installers only (snapx-compatible restore mode)
#[arg(long, short = 'i')]
installers: bool,
Expand Down Expand Up @@ -449,6 +453,15 @@ mod tests {
assert!(err.to_string().contains("--installers"));
}

#[test]
fn restore_channel_requires_installers_flag() {
let Err(err) = Cli::try_parse_from(["surge", "restore", "--channel", "production"]) else {
panic!("channel should require installers mode");
};

assert!(err.to_string().contains("--installers"));
}

#[test]
fn restore_upload_installers_conflicts_with_package_file() {
let Err(err) = Cli::try_parse_from([
Expand All @@ -465,6 +478,22 @@ mod tests {
assert!(err.to_string().contains("--package-file"));
}

#[test]
fn restore_channel_parses_in_installers_mode() {
let cli = Cli::try_parse_from(["surge", "restore", "--installers", "--channel", "production"])
.expect("restore installers mode with channel should parse");

let Commands::Restore {
channel, installers, ..
} = cli.command
else {
panic!("expected restore command");
};

assert!(installers);
assert_eq!(channel.as_deref(), Some("production"));
}

#[test]
fn install_force_flag_parses() {
let cli = Cli::try_parse_from(["surge", "install", "tailscale", "my-node", "--force"])
Expand Down
20 changes: 7 additions & 13 deletions crates/surge-cli/src/commands/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -990,30 +990,30 @@ mod tests {
}

#[test]
fn plan_remote_published_installer_uses_default_channel_key() {
fn plan_remote_published_installer_uses_requested_channel_key() {
let manifest = remote_manifest("demo", "linux-arm64", &["test", "production"], &["online"]);
let mut entry = release("1.2.3", "test", "linux-arm64", "demo.tar.zst");
let mut entry = release("1.2.3", "production", "linux-arm64", "demo.tar.zst");
entry.installers = vec!["online".to_string()];

let plan = plan_remote_published_installer(
&manifest,
"demo",
"linux-arm64",
"test",
"production",
&entry,
RemoteInstallerMode::Online,
)
.expect("plan should resolve");

assert_eq!(
plan.candidate_keys,
vec!["installers/Setup-linux-arm64-demo-test-online.bin".to_string()]
vec!["installers/Setup-linux-arm64-demo-production-online.bin".to_string()]
);
assert!(plan.blockers.is_empty(), "unexpected blockers: {:?}", plan.blockers);
}

#[test]
fn plan_remote_published_installer_reports_channel_mismatch() {
fn plan_remote_published_installer_drops_default_channel_mismatch_blocker() {
let manifest = remote_manifest("demo", "linux-arm64", &["test", "production"], &["online"]);
let mut entry = release("1.2.3", "production", "linux-arm64", "demo.tar.zst");
entry.installers = vec!["online".to_string()];
Expand All @@ -1030,15 +1030,9 @@ mod tests {

assert_eq!(
plan.candidate_keys,
vec!["installers/Setup-linux-arm64-demo-test-online.bin".to_string()]
);
assert!(
plan.blockers
.iter()
.any(|blocker| blocker.contains("default channel 'test'")),
"missing channel mismatch blocker: {:?}",
plan.blockers
vec!["installers/Setup-linux-arm64-demo-production-online.bin".to_string()]
);
assert!(plan.blockers.is_empty(), "unexpected blockers: {:?}", plan.blockers);
}

#[tokio::test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,6 @@ fn remote_installer_extension_for_rid(rid: &str) -> &'static str {
}
}

fn default_channel_for_remote_installer(manifest: &SurgeManifest, app_id: &str) -> Result<String> {
let app = manifest
.apps
.iter()
.find(|candidate| candidate.id == app_id)
.ok_or_else(|| SurgeError::Config(format!("App '{app_id}' was not found in manifest")))?;
Ok(app
.channels
.first()
.cloned()
.or_else(|| manifest.channels.first().map(|channel| channel.name.clone()))
.unwrap_or_else(|| "stable".to_string()))
}

pub(crate) fn plan_remote_published_installer(
manifest: &SurgeManifest,
app_id: &str,
Expand All @@ -38,7 +24,6 @@ pub(crate) fn plan_remote_published_installer(
let (_app, target) = manifest
.find_app_with_target(app_id, rid)
.ok_or_else(|| SurgeError::Config(format!("App '{app_id}' with RID '{rid}' not found in manifest")))?;
let default_channel = default_channel_for_remote_installer(manifest, app_id)?;
let declared_installers = if release.installers.is_empty() {
&target.installers
} else {
Expand All @@ -49,8 +34,7 @@ pub(crate) fn plan_remote_published_installer(
RemoteInstallerMode::Offline => "offline",
};
let installer_ext = remote_installer_extension_for_rid(rid);
let candidate_key =
format!("installers/Setup-{rid}-{app_id}-{default_channel}-{desired_installer}.{installer_ext}");
let candidate_key = format!("installers/Setup-{rid}-{app_id}-{channel}-{desired_installer}.{installer_ext}");

let mut blockers = Vec::new();
if !declared_installers
Expand All @@ -66,11 +50,6 @@ pub(crate) fn plan_remote_published_installer(
"release does not declare a '{desired_installer}' installer (declared installers: {declared})"
));
}
if channel != default_channel {
blockers.push(format!(
"published installers are currently bound to app default channel '{default_channel}', but install requested '{channel}'"
));
}

Ok(RemotePublishedInstallerPlan {
candidate_keys: vec![candidate_key],
Expand Down
Loading