P3.5: release promote / rollback over the persisted store
promote points an (app, channel) at a release id — cross-channel promotion allowed, missing channel created, manual policy gate stubbed. rollback moves the pointer to the previous PUBLISHED release in the channel's publish-order lineage (cross-promoted pointer falls back to the channel's own latest; at the earliest release it refuses with rollback.no_previous). Both append a cli-actor audit event and re-persist db.json; failures follow the P3.4b contract (dotted-code JSON error, exit 1, store untouched). Acceptance pinned in tests/release_ops.sx; cli_dispatch reworked off the removed stubs.
This commit is contained in:
@@ -8,13 +8,15 @@
|
||||
//
|
||||
// 1. no args → human help/usage on STDERR + EX_USAGE (64).
|
||||
// 2. unknown command → human error on STDERR + EX_USAGE (64).
|
||||
// 3. `release promote --json` → STDOUT is a SINGLE valid JSON object
|
||||
// (parses via std.json with no trailing junk); the human acknowledgement
|
||||
// is on STDERR, never stdout. (`release promote` is still a stub; the
|
||||
// real `ci publish` json output is exercised by publish_happy.sx.)
|
||||
// 3. a fully-flagged `release promote --json` against a store that was
|
||||
// never published → exit 1 (command failed, NOT usage), and STDOUT is
|
||||
// a SINGLE valid JSON error object (parses via std.json with no
|
||||
// trailing junk; status "error", code "store.load"); the human
|
||||
// sentence is on STDERR, never stdout. (Success-path json output is
|
||||
// exercised by publish_happy.sx / release_ops.sx.)
|
||||
// 4. `--help` → lists the `ci` / `release` groups, exits 0.
|
||||
// 5. `ci publish --json` with NO required flags → EX_USAGE (64), error on
|
||||
// stderr (the --manifest / --local-store contract).
|
||||
// 5. `ci publish --json` / `release promote --json` with NO required
|
||||
// flags → EX_USAGE (64), error on stderr (the required-flag contract).
|
||||
//
|
||||
// `make test` depends on `build`, so `build/dist` exists before this runs;
|
||||
// the relative path resolves from the repo root (the `make test` cwd).
|
||||
@@ -65,27 +67,31 @@ main :: () -> s32 {
|
||||
proc.assert(false, "spawn build/dist bogus failed");
|
||||
}
|
||||
|
||||
// ── 3a. `--json` stdout purity: a single valid JSON object, nothing
|
||||
// else. `2>/dev/null` drops the human note so the pipe carries
|
||||
// ONLY stdout; std.json.parse rejects trailing junk. ─────────
|
||||
if r := proc.run("build/dist release promote --json 2>/dev/null") {
|
||||
proc.assert(r.exit_code == 0, "stub --json command must succeed (EX_OK)");
|
||||
// ── 3a. `--json` stdout purity on a FAILURE path: a single valid
|
||||
// JSON error object, nothing else. The store dir was never
|
||||
// published into, so the command fails with `store.load` and
|
||||
// exit 1 (command failed — distinct from usage's 64).
|
||||
// `2>/dev/null` drops the human note so the pipe carries ONLY
|
||||
// stdout; std.json.parse rejects trailing junk. ──────────────
|
||||
PROMOTE :: "build/dist release promote --app x --channel beta --release rel-x --local-store .sx-tmp/cli_dispatch_nostore --json";
|
||||
if r := proc.run(concat(PROMOTE, " 2>/dev/null")) {
|
||||
proc.assert(r.exit_code == 1, "failed command must exit 1 (not EX_USAGE)");
|
||||
v, e := json.parse(r.stdout, xx gpa);
|
||||
proc.assert(!e, "stdout in --json mode must be a single valid JSON object (parse failed / trailing junk)");
|
||||
if !e {
|
||||
o := v.object;
|
||||
proc.assert(o.len == 3, "stub json object carries command/status/stub");
|
||||
proc.assert(o.items[0].key == "command" and o.items[0].val.str == "release promote",
|
||||
"stub json names the dispatched command");
|
||||
proc.assert(o.items[1].key == "status" and o.items[1].val.str == "ok",
|
||||
"stub json reports status ok");
|
||||
proc.assert(o.items[0].key == "status" and o.items[0].val.str == "error",
|
||||
"failure json reports status error");
|
||||
eo := o.items[1].val.object;
|
||||
proc.assert(eo.items[0].key == "code" and eo.items[0].val.str == "store.load",
|
||||
"failure json names the store.load code");
|
||||
}
|
||||
} else {
|
||||
proc.assert(false, "spawn build/dist release promote --json failed");
|
||||
}
|
||||
|
||||
// ── 3b. `--json` mode keeps human text on STDERR (not stdout) ──────
|
||||
if r := proc.run("build/dist release promote --json 2>&1 1>/dev/null") {
|
||||
if r := proc.run(concat(PROMOTE, " 2>&1 1>/dev/null")) {
|
||||
proc.assert(r.stdout.len > 0, "--json mode must still emit human text to stderr");
|
||||
} else {
|
||||
proc.assert(false, "spawn build/dist release promote --json (stderr) failed");
|
||||
@@ -100,7 +106,7 @@ main :: () -> s32 {
|
||||
proc.assert(false, "spawn build/dist --help failed");
|
||||
}
|
||||
|
||||
// ── 5. `ci publish` requires --manifest / --local-store ───────────
|
||||
// ── 5. required flags: ci publish AND release promote ─────────────
|
||||
// Missing a required flag is a usage error: EX_USAGE (64), human
|
||||
// diagnostic on stderr (`2>&1 1>/dev/null` captures the stderr text).
|
||||
if r := proc.run("build/dist ci publish --json 2>&1 1>/dev/null") {
|
||||
@@ -109,6 +115,12 @@ main :: () -> s32 {
|
||||
} else {
|
||||
proc.assert(false, "spawn build/dist ci publish (no flags) failed");
|
||||
}
|
||||
if r := proc.run("build/dist release promote --json 2>&1 1>/dev/null") {
|
||||
proc.assert(r.exit_code == 64, "release promote without required flags must exit EX_USAGE (64)");
|
||||
proc.assert(contains(r.stdout, "missing required flag"), "missing-flag error names the failure on stderr");
|
||||
} else {
|
||||
proc.assert(false, "spawn build/dist release promote (no flags) failed");
|
||||
}
|
||||
|
||||
print("cli_dispatch: ok\n");
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user