Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
828a1de
[jrs] layout skeleton for coast-ssg
Apr 19, 2026
1f8b405
[jrs] implemented the TOML parser for Coastfile.shared_service_groups
Apr 19, 2026
d011709
[jrs] add consumer-side from_group shared_services field
Apr 19, 2026
9700f8f
[jrs] add SSG protocol request/response types
Apr 19, 2026
f577a24
[jrs] ssg build + ps, state DB
Apr 19, 2026
bcad400
[jrs] ssg run/stop/start/restart/rm + logs/exec/ports with ssg_mutex
Apr 20, 2026
99b6d0a
[jrs] ssg auto-start hook in coast run
Apr 20, 2026
efd5b0a
[jrs] ssg consumer wiring (routing + compose_rewrite)
Apr 20, 2026
d02ded2
[jrs] ssg remote reverse tunnels + stop/rm --force shadow gate
Apr 20, 2026
f626186
[jrs] ssg auto_create_db + inject for inline and SSG paths
Apr 20, 2026
0722207
[jrs] ssg checkout / uncheckout with coast-holder displacement
Apr 20, 2026
557a590
[jrs] ssg drift detection in coast build manifest
Apr 20, 2026
0779994
[jrs] SSG docs section + coast ssg doctor
Apr 20, 2026
7567d10
[jrs] added new integration test zip
Apr 20, 2026
87ae049
[jrs] audit fixes and test coverage
Apr 20, 2026
80ea281
[jrs] update following phases
Apr 20, 2026
93709ef
[jrs] refresh consumer socat forwarders after ssg run/start/restart
Apr 21, 2026
90a8313
[jrs] migrate lifecycle + images to SsgDockerOps trait
Apr 21, 2026
63bd206
[jrs] file inject via secret-file exec path
Apr 21, 2026
1767a0e
[jrs] auto_create_db=false override lost across artifact serializer
Apr 22, 2026
24cf98d
[jrs] import-host-volume command with --apply + .bak backup
Apr 22, 2026
5738959
[jrs] pin consumers to specific SSG builds
Apr 22, 2026
8f90fb5
[jrs] add ssg Coastfile inheritance
Apr 22, 2026
9aefa8c
[jrs] symmetric remote shared-service routing
Apr 22, 2026
3238ac2
[jrs] flip to per-project SSG
Apr 24, 2026
90e15f1
[jrs] derive SSG container and compose names from project
Apr 24, 2026
6ef497c
[jrs] add `coast ssg ls`
Apr 24, 2026
17d859e
[jrs] refactor dispatch_import_host_volume
Apr 24, 2026
ff2be6b
[jrs] scope consumer from_group resolution to own project
Apr 24, 2026
0775ea4
[jrs] remote SSG routes per-(project, remote)
Apr 24, 2026
004f745
[jrs] ssg lint fix
Apr 24, 2026
a713085
[jrs] integration test sweep backfill for per-project SSG
Apr 24, 2026
fcedaf4
[jrs] virtual-port allocator + ssg_virtual_ports table
Apr 24, 2026
cd35682
[jrs] host socat supervisor
Apr 24, 2026
d4abb6a
[jrs] remove runtime drift audit; add static service-missing check
Apr 24, 2026
581e1f1
[jrs] wire host socat supervisor into lifecycle; delete consumer_refresh
Apr 24, 2026
7271a89
[jrs] symmetric remote tunnels on virtual port; coalesce per (project…
Apr 25, 2026
f078452
[jrs] expose virtual_port via `coast ssg ports`
Apr 25, 2026
9bc4bdd
[jrs] refresh host socats on AlreadyRunning
Apr 25, 2026
4aee607
[jrs] frontend for ssg part 1
Apr 26, 2026
56a8413
[jrs] finished initial ssg frontend
Apr 26, 2026
7c37aff
[jrs] updated docs for ssg
Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
/target-cov
lcov.info
.env

Expand Down
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"coast-i18n",
"coast-secrets",
"coast-docker",
"coast-ssg",
"coast-daemon",
"coast-cli",
"coast-update",
Expand Down
91 changes: 77 additions & 14 deletions coast-cli/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,26 +378,49 @@ impl ProgressDisplay {
}
}

/// Validate a user-supplied `--type <coastfile_type>` argument.
///
/// Rejects:
/// - `default` / `toml` — reserved degenerate names.
/// - SSG variants (`shared_service_groups`) — built via `coast ssg build`,
/// not through this command. See `coast-ssg/DESIGN.md`.
///
/// Remote variants (`remote*`) are intentionally NOT rejected here:
/// `coast build --type remote.foo` is a valid invocation that the
/// downstream pipeline handles. Only SSG is fundamentally incompatible
/// with `coast build`.
pub(crate) fn validate_coastfile_type_arg(t: &str) -> Result<()> {
if t == "default" {
bail!(
"'--type default' is not allowed. \
The base 'Coastfile' is the default type. \
Run 'coast build' without --type."
);
}
if t == "toml" {
bail!(
"'--type toml' is not allowed. \
'toml' is a reserved name. Use 'Coastfile.toml' for the default type \
with syntax highlighting, or choose a different type name."
);
}
if coast_core::coastfile::Coastfile::is_ssg_type(Some(t)) {
bail!(
"'--type {t}' is a Shared Service Group variant; \
build it with 'coast ssg build' instead of 'coast build --type {t}'. \
SSGs are a separate build product (see coast-ssg/DESIGN.md)."
);
}
Ok(())
}

/// Execute the `coast build` command.
///
/// The project name is derived from the Coastfile, not from the `--project` flag,
/// since the Coastfile itself defines the project name.
pub async fn execute(args: &BuildArgs, global_working_dir: &Option<PathBuf>) -> Result<()> {
if let Some(ref t) = args.coastfile_type {
if t == "default" {
bail!(
"'--type default' is not allowed. \
The base 'Coastfile' is the default type. \
Run 'coast build' without --type."
);
}
if t == "toml" {
bail!(
"'--type toml' is not allowed. \
'toml' is a reserved name. Use 'Coastfile.toml' for the default type \
with syntax highlighting, or choose a different type name."
);
}
validate_coastfile_type_arg(t.as_str())?;
}

let has_inline_flags = args.project_name.is_some()
Expand Down Expand Up @@ -759,6 +782,46 @@ mod tests {
assert_eq!(cli.args.coastfile_path, PathBuf::from("my/Coastfile"));
}

// -------------------------------------------------------------------
// validate_coastfile_type_arg tests
// -------------------------------------------------------------------

#[test]
fn test_validate_coastfile_type_arg_rejects_default() {
let err = validate_coastfile_type_arg("default").unwrap_err();
assert!(err.to_string().contains("'--type default' is not allowed"));
}

#[test]
fn test_validate_coastfile_type_arg_rejects_toml() {
let err = validate_coastfile_type_arg("toml").unwrap_err();
assert!(err.to_string().contains("'--type toml' is not allowed"));
}

#[test]
fn test_validate_coastfile_type_arg_rejects_shared_service_groups() {
let err = validate_coastfile_type_arg("shared_service_groups").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Shared Service Group"),
"expected SSG mention, got: {msg}"
);
assert!(
msg.contains("coast ssg build"),
"expected pointer to 'coast ssg build', got: {msg}"
);
}

#[test]
fn test_validate_coastfile_type_arg_accepts_user_variants() {
assert!(validate_coastfile_type_arg("light").is_ok());
assert!(validate_coastfile_type_arg("e2e").is_ok());
// Remote variants pass through `validate_coastfile_type_arg` —
// they're handled downstream by the remote-build pipeline.
assert!(validate_coastfile_type_arg("remote").is_ok());
assert!(validate_coastfile_type_arg("remote.light").is_ok());
}

// -------------------------------------------------------------------
// ProgressDisplay state-machine tests
// -------------------------------------------------------------------
Expand Down
73 changes: 73 additions & 0 deletions coast-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub mod search_docs;
pub mod secret;
pub mod shared;
pub mod skills_prompt;
pub mod ssg;
pub mod start;
pub mod stop;
pub mod ui;
Expand Down Expand Up @@ -301,13 +302,85 @@ async fn send_build_request_to(
Response::RerunExtractorsProgress(ref event) => {
on_progress(event);
}
Response::SsgProgress(ref event) => {
on_progress(event);
}
_ => {
return Ok(response);
}
}
}
}

/// Stream `coast ssg logs --follow` output chunks to a callback.
///
/// Connects to the daemon, sends the request, then reads `SsgLogChunk`
/// responses in a loop, invoking `on_chunk` for each one. The final
/// `Ssg(SsgResponse)` (or `Error`) closes the stream.
pub async fn stream_ssg_log_chunks(request: Request, mut on_chunk: impl FnMut(&str)) -> Result<()> {
let sock = socket_path();
let stream = UnixStream::connect(&sock).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::ConnectionRefused
|| e.kind() == std::io::ErrorKind::NotFound
{
anyhow::anyhow!("{}", t!("error.daemon_not_running"))
} else {
anyhow::anyhow!(
"{}",
t!(
"error.daemon_connect_failed",
path = sock.display(),
message = e
)
)
}
})?;

let (reader, mut writer) = stream.into_split();

let encoded = protocol::encode_request(&request).context("Failed to encode request")?;
writer
.write_all(&encoded)
.await
.context("Failed to send request to coastd")?;
writer
.shutdown()
.await
.context("Failed to flush request to coastd")?;

let mut buf_reader = BufReader::new(reader);

loop {
let mut line = String::new();
let bytes = buf_reader
.read_line(&mut line)
.await
.context("Failed to read response from coastd")?;

if bytes == 0 {
return Ok(());
}

let response = protocol::decode_response(line.trim_end().as_bytes())
.context("Failed to decode response from coastd")?;

match response {
Response::SsgLogChunk(chunk) => {
on_chunk(&chunk.chunk);
}
Response::Ssg(_) => {
return Ok(());
}
Response::Error(e) => {
bail!("{}", e.error);
}
other => {
bail!("unexpected response from daemon while tailing SSG logs: {other:?}");
}
}
}
}

/// Send a run request and stream progress events to a callback.
///
/// Connects to the daemon, sends the request, then reads JSON lines in a loop.
Expand Down
Loading