ci: add gitflow — dev/stage/main branches with CI workflows
Dev — Build & local smoke test / build-smoke (push) Successful in 2m51s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m52s
Deploy marketing to Cloud Run / deploy (push) Successful in 3m37s

- dev.yaml: build + local docker smoke test only (no push, no deploy)
- stage.yaml: build + push + deploy to marketing-stage + smoke test (stops here)
- deploy.yaml: add HTML placeholder touch step before docker build

Proper human gate between stage and prod: the stage→main merge decision.
This commit is contained in:
Will Anderson
2026-05-03 11:28:43 -05:00
parent 102343c8fe
commit c75d8a9563
6 changed files with 399 additions and 3 deletions
+86 -2
View File
@@ -38,6 +38,7 @@
#include <arpa/inet.h>
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
@@ -1853,6 +1854,84 @@ el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) {
return 1;
}
// exec_command — run a shell command, return exit code (0 = success).
// Used by elb and other El tooling to invoke subprocesses.
el_val_t exec_command(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd) return (el_val_t)(int64_t)-1;
int ret = system(cmd);
return (el_val_t)(int64_t)ret;
}
// exec_capture — run a shell command, capture stdout, return as String.
// Returns "" on failure.
el_val_t exec_capture(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd) return el_wrap_str(el_strdup(""));
FILE* f = popen(cmd, "r");
if (!f) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
char buf[4096];
while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf);
pclose(f);
return el_wrap_str(b.buf);
}
// exec — run a shell command via /bin/sh, capture stdout, return as String.
// Times out after 30 seconds. Returns "" on any error.
// El name: exec(cmd) -> String
el_val_t exec(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
/* Build a time-limited command: wrap with timeout(1) if available,
* otherwise rely on the 30s read loop guard below. We use the simple
* popen approach with a deadline measured by wall clock so the caller
* is never blocked indefinitely. */
FILE* f = popen(cmd, "r");
if (!f) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
char buf[4096];
/* 30-second wall-clock deadline */
time_t deadline = time(NULL) + 30;
while (time(NULL) < deadline) {
if (fgets(buf, sizeof(buf), f) == NULL) break;
jb_puts(&b, buf);
}
pclose(f);
return el_wrap_str(b.buf);
}
// exec_bg — run a shell command in background, return PID as String.
// The child process runs independently; the caller is not blocked.
// Returns "" on fork failure.
// El name: exec_bg(cmd) -> String
el_val_t exec_bg(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
pid_t pid = fork();
if (pid < 0) {
/* fork failed */
return el_wrap_str(el_strdup(""));
}
if (pid == 0) {
/* child: detach from parent's stdio, exec via shell */
setsid();
int devnull = open("/dev/null", O_RDWR);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
_exit(127);
}
/* parent: convert pid to string and return immediately */
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
return el_wrap_str(el_strdup(pidbuf));
}
el_val_t fs_list(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
el_val_t lst = el_list_empty();
@@ -2916,8 +2995,13 @@ static int looks_like_string(el_val_t v) {
const unsigned char* s = (const unsigned char*)p;
for (int i = 0; i < 16; i++) {
unsigned char c = s[i];
if (c == '\0') return i > 0; /* terminated string */
if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0;
if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */
/* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes.
* 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK)
* 0x20-0x7e = printable ASCII (OK)
* 0x7f = DEL (reject)
* 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */
if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0;
}
return 1; /* 16+ printable bytes — call it a string */
}
+6
View File
@@ -739,6 +739,12 @@ el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
/* See bottom of el_runtime.c for the implementation.
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
/* ── Subprocess execution ────────────────────────────────────────────────── */
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
el_val_t trace_span_start(el_val_t name);