refactor: rename sh_events table to sh (migration 0008)
Simple ALTER TABLE rename. Rust struct/field names (ShEvent, sh_events)
are left as-is. SQL strings and doc references updated throughout.
https://claude.ai/code/session_01SpFto4X7xN2xnqgHbP62eB
diff --git a/docs/CI-STATE.md b/docs/CI-STATE.md
index bf448a3..0791cb5 100644
--- a/docs/CI-STATE.md
+++ b/docs/CI-STATE.md
@@ -179,7 +179,7 @@ sequenceDiagram
CI-->>Run: exit status
Run->>Run: ingest_events(events.jsonl)
Run->>DB: INSERT jobs (pass 1)
- Run->>DB: INSERT sh_events (pass 2)
+ Run->>DB: INSERT sh (pass 2)
alt RunFinished(Success) + exit 0
Run->>DB: UPDATE runs SET state='complete'
else RunFinished(PipelineFailure) + exit 0
@@ -195,7 +195,7 @@ Wire events (`quire-core/src/ci/event.rs`):
* `JobFinished { job_id, outcome: complete | failed }` — `JobOutcome` is the closed set, not the full job-state enum.
* `ShStarted { job_id, cmd }` / `ShFinished { job_id, exit_code }`
-`Run::ingest_events` reads the file in two passes (jobs first to satisfy the FK on `(run_id, job_id)`, then sh_events). Ingest failures are logged but never demote the run's own outcome — a partial DB write is preferable to losing the pass/fail signal.
+`Run::ingest_events` reads the file in two passes (jobs first to satisfy the FK on `(run_id, job_id)`, then sh). Ingest failures are logged but never demote the run's own outcome — a partial DB write is preferable to losing the pass/fail signal.
## Orchestration today
@@ -255,7 +255,7 @@ All six columns (`run_id`, `job_id`, `state`, `exit_code`, `started_at_ms`, `fin
The schema permits six states (`pending`, `active`, `complete`, `failed`, `skipped`, `aborted`) but `ingest_events` only writes `complete` and `failed`. The other four states have no producer today — see Gaps below.
-### `sh_events` table
+### `sh` table
All columns (`run_id`, `job_id`, `started_at_ms`, `finished_at_ms`, `exit_code`, `cmd`) are written by `Run::ingest_events` (pass 2) and read by the web detail view. All live.
diff --git a/docs/PLAN.md b/docs/PLAN.md
index ecca0cf..1293490 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -210,7 +210,7 @@ Keyboard navigation in the web UI. Atom feeds for recent commits (public, subjec
- **CI network policy.** Default on (you'll want it for `cargo`, `npm`), with a per-pipeline `(network false)` opt-out. Or default off with explicit `(network true)`? Default on is more ergonomic; default off is more principled.
- **Artifact size limits.** Probably want a per-run cap (1 GB?) and a per-repo cap (10 GB?). Values TBD after real use.
- **Push-time feedback for CI.** When post-receive kicks off CI, should the push block until the run starts (not completes)? Probably yes, so the client sees "CI run #42 queued" in push output.
-- **Secrets for CI.** Declared in the global `:secrets` map, exposed to jobs via `(runtime.secret :name)`. Each value is either a plain string or a `{:file "/run/secrets/<name>"}` reference (Docker-secrets convention; one trailing newline stripped on read). Resolved values are redacted from CI output surfaces — run logs, recorded command strings, the `sh_events.cmd` column — by a per-run registry that replaces matches with `{{ name }}`. Values shorter than 8 bytes are not registered (false-positive risk; a `WARN` trace event names the skip). Tracing/application logs are not covered in v1 — audit existing trace call sites instead. Encrypted-at-rest for the secrets file is deferred until there's a reason.
+- **Secrets for CI.** Declared in the global `:secrets` map, exposed to jobs via `(runtime.secret :name)`. Each value is either a plain string or a `{:file "/run/secrets/<name>"}` reference (Docker-secrets convention; one trailing newline stripped on read). Resolved values are redacted from CI output surfaces — run logs, recorded command strings, the `sh.cmd` column — by a per-run registry that replaces matches with `{{ name }}`. Values shorter than 8 bytes are not registered (false-positive risk; a `WARN` trace event names the skip). Tracing/application logs are not covered in v1 — audit existing trace call sites instead. Encrypted-at-rest for the secrets file is deferred until there's a reason.
- **Backup story.** `tar` the data volume. Deploy keys are in the volume, so they travel with the backup — convenient but also means the backup is sensitive. Worth thinking about encryption-at-rest for the backup, not just the source volume. Defer, but don't forget.
- **`docker exec` performance.** Each git push spawns a new `docker exec`. Container startup is not involved (the container is already running), but there's still some latency — tens to hundreds of milliseconds. Probably fine for interactive use, possibly noticeable if something scripts many pushes. Measure, don't optimize preemptively.
- **Reverse-proxy auth scheme.** Which auth mechanism does the proxy actually run? Candidates:
diff --git a/docs/config.md b/docs/config.md
index fd21568..720b30e 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -66,7 +66,7 @@ for the run; later appearances in `(sh ...)` stdout, stderr, or
recorded command strings are replaced with `{{ name }}` in:
- The CRI log files written under each run's workspace.
-- The `sh_events.cmd` column.
+- The `sh.cmd` column.
- Any other `ShOutput`-derived persistence.
Limits worth knowing:
diff --git a/docs/plans/2026-05-07-secret-redaction.md b/docs/plans/2026-05-07-secret-redaction.md
index 3af360e..6dd3633 100644
--- a/docs/plans/2026-05-07-secret-redaction.md
+++ b/docs/plans/2026-05-07-secret-redaction.md
@@ -64,14 +64,14 @@ The `Runtime::sh` method records `ShOutput` into `self.outputs`. Redact `stdout`
No code changes needed. Since Task 3 redacts the `ShOutput` before it reaches `write_cri_log` and the DB insert path, both surfaces are covered.
Schema audit — text columns that could carry user-derived text:
-- `sh_events.cmd` — covered by Task 3 (redacted before insert)
+- `sh.cmd` — covered by Task 3 (redacted before insert)
- `runs.repo` — git repo name, system-generated
- `runs.ref_name` — git ref, system-generated
- `runs.sha` — commit hash, system-generated
- `runs.failure_kind` — enum tag, not user text
- `runs.container_id`, `image_tag`, `workspace_path` — system-generated
-No additional redaction sites needed beyond `sh_events.cmd`.
+No additional redaction sites needed beyond `sh.cmd`.
- [x] **Step 1: Verify CRI logs and DB receive redacted content**
- [x] **Step 2: Commit**
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 6ac0f77..6dcba35 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -98,7 +98,7 @@ enum Commands {
/// `stdout` — write JSONL to stdout.
/// `<path>` — write JSONL to this file. The orchestrator
/// reads the file post-run to populate `jobs`
- /// and `sh_events` database rows.
+ /// and `sh` database rows.
#[facet(args::named, default = "null")]
events: String,
diff --git a/quire-server/migrations/0008_rename_sh.sql b/quire-server/migrations/0008_rename_sh.sql
new file mode 100644
index 0000000..0e70f1e
--- /dev/null
+++ b/quire-server/migrations/0008_rename_sh.sql
@@ -0,0 +1 @@
+ALTER TABLE sh_events RENAME TO sh;
diff --git a/quire-server/src/bin/quire/commands/dev.rs b/quire-server/src/bin/quire/commands/dev.rs
index 0add0df..ba780c9 100644
--- a/quire-server/src/bin/quire/commands/dev.rs
+++ b/quire-server/src/bin/quire/commands/dev.rs
@@ -167,7 +167,7 @@ impl Seeder {
let finished_at = started_at + event.duration_ms;
self.db
.execute(
- "INSERT INTO sh_events (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd)
+ "INSERT INTO sh (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
run_id,
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 262cd53..38a04d7 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -247,7 +247,7 @@ impl Run {
/// Layout under the run dir on disk:
/// * `quire-ci.log` — combined stdout+stderr of the subprocess.
/// * `events.jsonl` — structured event stream (one JSON object per
- /// line). Ingested into `jobs` and `sh_events` after the
+ /// line). Ingested into `jobs` and `sh` after the
/// subprocess exits.
/// * `jobs/<job>/sh-<n>.log` — per-sh CRI logs, written by quire-ci
/// via `--out-dir`.
@@ -332,7 +332,7 @@ impl Run {
tracing::warn!(
run_id = %self.id,
error = %e,
- "failed to ingest quire-ci events; jobs/sh_events rows may be incomplete"
+ "failed to ingest quire-ci events; jobs/sh rows may be incomplete"
);
None
}
@@ -367,7 +367,7 @@ impl Run {
/// Read `events.jsonl` and replay it into the database.
///
- /// Done in two passes because `sh_events` has a foreign key on
+ /// Done in two passes because `sh` has a foreign key on
/// `(run_id, job_id)` in `jobs`, and the wire format interleaves
/// sh events with their owning job. Pass 1 inserts every job row
/// (paired by `job_id`); pass 2 inserts sh events.
@@ -418,7 +418,7 @@ impl Run {
}
}
- // Pass 2: sh_events rows. Pair ShStarted with ShFinished by job_id
+ // Pass 2: sh rows. Pair ShStarted with ShFinished by job_id
// (sequential within a run-fn, so a single buffer slot per job
// is enough).
let mut pending_sh: HashMap<&str, (i64, &str)> = HashMap::new();
@@ -432,7 +432,7 @@ impl Run {
continue;
};
db.execute(
- "INSERT INTO sh_events (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd) \
+ "INSERT INTO sh (run_id, job_id, started_at_ms, finished_at_ms, exit_code, cmd) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![&self.id, job_id, started_at, event.at_ms, exit_code, cmd],
)?;
@@ -1170,7 +1170,7 @@ mod tests {
let sh_events: Vec<(String, i64, i64, i32, String)> = db
.prepare(
- "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd FROM sh_events \
+ "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd FROM sh \
WHERE run_id = ?1 ORDER BY started_at_ms",
)
.unwrap()
diff --git a/quire-server/src/db.rs b/quire-server/src/db.rs
index e26819d..3f08a20 100644
--- a/quire-server/src/db.rs
+++ b/quire-server/src/db.rs
@@ -19,6 +19,7 @@ static MIGRATIONS: std::sync::LazyLock<Migrations<'static>> = std::sync::LazyLoc
M::up(include_str!("../migrations/0005_rename_run_token.sql")),
M::up(include_str!("../migrations/0006_traceparent.sql")),
M::up(include_str!("../migrations/0007_schema_cleanup.sql")),
+ M::up(include_str!("../migrations/0008_rename_sh.sql")),
])
});
diff --git a/quire-server/src/quire/web/db.rs b/quire-server/src/quire/web/db.rs
index 20198f8..a395469 100644
--- a/quire-server/src/quire/web/db.rs
+++ b/quire-server/src/quire/web/db.rs
@@ -110,7 +110,7 @@ pub fn load_run_detail(quire: &Quire, repo: &str, run_id: &str) -> Result<RunDet
let mut sh_stmt = db.prepare(
"SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
- FROM sh_events WHERE run_id = ?1
+ FROM sh WHERE run_id = ?1
ORDER BY job_id, started_at_ms",
)?;