feat(cli): env-scoped overlays — --env merges picloud.<env>.toml
The §4.1 base+overlay model for the single-app world: a base `picloud.toml`
holds the shared scripts/routes/triggers; a sparse `picloud.<env>.toml`
carries the per-environment slug (+ extra secret names). `pic plan/apply
--env <E>` merges the overlay onto the base before diffing/applying, so an
environment deploys to its own app ("an environment is an app", §2)
without duplicating the shared definitions.
Merge is sparse: overlay `app.slug`/`name` replace the base's; secret
names union. Scripts/routes/triggers always come from the base. Rich
per-key `vars` resolution stays Phase 3.
Tested: unit tests for the merge + overlay-path derivation; a journey
proving `apply --env staging` deploys to the staging app and leaves the
base app untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ mod config;
|
||||
mod dead_letters;
|
||||
mod email_queue;
|
||||
mod enabled;
|
||||
mod env_overlay;
|
||||
mod init;
|
||||
mod invoke;
|
||||
mod logs;
|
||||
|
||||
76
crates/picloud-cli/tests/env_overlay.rs
Normal file
76
crates/picloud-cli/tests/env_overlay.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! Env-scoped overlays (§4.1): `pic apply --env <E>` merges the sparse
|
||||
//! `picloud.<env>.toml` (per-env slug) onto the base and deploys to that
|
||||
//! environment's app — leaving the base app untouched.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::common;
|
||||
use crate::common::cleanup::AppGuard;
|
||||
|
||||
fn scripts_ls(env: &common::TestEnv, slug: &str) -> String {
|
||||
String::from_utf8(
|
||||
common::pic_as(env)
|
||||
.args(["scripts", "ls", "--app", slug])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn apply_env_overlay_targets_the_env_app() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let base_slug = common::unique_slug("ovl");
|
||||
let staging_slug = format!("{base_slug}-staging");
|
||||
for s in [&base_slug, &staging_slug] {
|
||||
common::pic_as(&env)
|
||||
.args(["apps", "create", s])
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
let _g1 = AppGuard::new(&env.url, &env.token, &base_slug);
|
||||
let _g2 = AppGuard::new(&env.url, &env.token, &staging_slug);
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
fs::create_dir_all(dir.path().join("scripts")).unwrap();
|
||||
fs::write(dir.path().join("scripts/hello.rhai"), "\"hi\"").unwrap();
|
||||
let base = dir.path().join("picloud.toml");
|
||||
fs::write(
|
||||
&base,
|
||||
format!(
|
||||
"[app]\nslug = \"{base_slug}\"\nname = \"Ovl\"\n\n\
|
||||
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
// Overlay redirects to the staging app.
|
||||
fs::write(
|
||||
dir.path().join("picloud.staging.toml"),
|
||||
format!("[app]\nslug = \"{staging_slug}\"\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Apply to staging only.
|
||||
common::pic_as(&env)
|
||||
.args(["apply", "--file"])
|
||||
.arg(&base)
|
||||
.args(["--env", "staging"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(
|
||||
scripts_ls(&env, &staging_slug).contains("hello"),
|
||||
"overlay apply must deploy to the staging app"
|
||||
);
|
||||
assert!(
|
||||
!scripts_ls(&env, &base_slug).contains("hello"),
|
||||
"the base app must be untouched by an --env apply"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user