Remove mirroring from CI
Server-side mirroring already runs on every push, so a pipeline `(mirror ...)` job pushes the same refs twice — the CI path is a duplicate, not a mechanism. Drops M.mirror and its helpers from quire.stdlib, the .quire/ci.fnl job, the docs section, and the tests, plus the git_dir plumbing through Bootstrap, the runtime push table, and store_bootstrap_data that only existed to feed it.
Assisted-by: GLM-5.2 via pi
diff --git a/.quire/ci.fnl b/.quire/ci.fnl
index d5943dd..0bf5ff9 100644
--- a/.quire/ci.fnl
+++ b/.quire/ci.fnl
@@ -1,15 +1,3 @@
(local {: job} (require :quire.ci))
-(local {: mirror} (require :quire.stdlib))
; (job :test [:quire/push] (fn [{: sh}] (sh [:cargo :test])))
-
-; (job :mirror [:quire/push]
-; (fn [{: jobs : secret}]
-; (let [push (jobs :quire/push)]
-; (when (= push.ref :refs/heads/main)
-; (mirror {:url "https://github.com/kejadlen/quire.git"
-; :auth-header (secret :github_auth_header)
-; :sha push.sha
-; :tag (.. :v (os.date "!%Y-%m-%d") "-" (push.sha:sub 1 8))
-; :git-dir push.git-dir
-; :refs [:refs/heads/main]})))))
diff --git a/docs/CI-FENNEL.md b/docs/CI-FENNEL.md
index 9cbcae1..6d3ea86 100644
--- a/docs/CI-FENNEL.md
+++ b/docs/CI-FENNEL.md
@@ -221,33 +221,6 @@ The execute VM is sandboxed (no `io`/`os`/`debug`), so `runtime.sh` is the docum
`runtime` is also reachable as a module: `(let [{: sh : secret} (require :quire.runtime)] …)`. Same table, same closures — useful for library code that wants its dependencies explicit.
-## Stdlib (`quire.stdlib`)
-
-Helpers that compose runtime primitives into common recipes. Embedded into the binary; available via `(require :quire.stdlib)` from any run-fn.
-
-The kernel (`sh`/`secret`/`jobs`) stays small. Higher-level operations like tag-and-push live in Fennel where they're easier to read and evolve.
-
-```
-(local {: mirror} (require :quire.stdlib))
-
-(ci.job :mirror [:quire/push :test]
- (fn [{: jobs : secret}]
- (let [push (jobs :quire/push)
- auth (secret :github_auth_header)]
- (mirror {:url "https://github.com/example/repo.git"
- :auth-header auth
- :sha push.sha
- :tag (.. "quire-" (string.sub push.sha 1 8))
- :git-dir (. push :git-dir)
- :refs ["refs/heads/main"]}))))
-```
-
-Available helpers:
-
-* `(mirror opts)` — tag a commit and push it (plus optional refs) to a remote. `opts.url`, `opts.auth-header`, `opts.sha`, `opts.tag`, and `opts.git-dir` are required; `opts.refs` defaults to `[]`. The caller resolves the credential (typically via `runtime.secret`) and passes the full HTTP header line as `:auth-header`; mirror passes it to git via `GIT_CONFIG_*` env vars rather than `-c http.extraHeader=…` in argv, so it doesn't appear in `ps` listings. Returns `{:tag :pushed_refs}`. Raises on missing required opts or non-zero git exits.
-
-Use the stdlib form to mirror conditionally or as part of a larger run-fn.
-
## A worked example
```
diff --git a/docs/CI-STATE.md b/docs/CI-STATE.md
index bd74d04..6faadb2 100644
--- a/docs/CI-STATE.md
+++ b/docs/CI-STATE.md
@@ -160,7 +160,7 @@ sequenceDiagram
Run->>CI: spawn (QUIRE__SERVER_URL, QUIRE__RUN_TOKEN, --events, --out-dir)
CI->>Bootstrap: GET /api/run/bootstrap (bearer token)
Bootstrap->>DB: UPDATE runs SET state='active', started_at_ms=now
- Bootstrap-->>CI: git_dir, meta, sentry_trace_id
+ Bootstrap-->>CI: meta, sentry_trace_id
CI->>CI: compile .quire/ci.fnl
loop per job in topo order
CI->>CI: enter_job / run-fn / leave_job
@@ -234,7 +234,6 @@ Two things in `CI.md` that the code does *not* yet implement at this layer:
| `started_at_ms` | `transition(Active)`, also stamped as fallback in `Succeeded/Failed/Canceled` | `read_started_at`, web handlers |
| `finished_at_ms` | `transition(Succeeded/Failed/Canceled)` | `read_finished_at`, web handlers |
| `run_token` | `Runs::create` (API sessions only) | `verify_run_token` middleware |
-| `git_dir` | `Run::store_bootstrap_data` (API sessions only) | bootstrap endpoint |
| `traceparent` | `Run::store_bootstrap_data` (API sessions only) | bootstrap endpoint |
Migration 0007 dropped eight columns that carried no live data with the Process executor: `container_id`, `workspace_path`, `image_tag`, `build_started_at_ms`, `build_finished_at_ms`, `container_started_at_ms`, `container_stopped_at_ms`, and `sentry_trace_id`. The first five were Docker-executor placeholders; `workspace_path` was written at create time but reconstructable from `<base_dir>/<run_id>/workspace`; `sentry_trace_id` was added in migration 0004 and superseded by `traceparent` before it was ever used.
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index b66093e..b2739ed 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -240,12 +240,11 @@ impl RunClient {
///
/// One-shot: the server marks the bootstrap as fetched after the first
/// successful call and returns 410 on any subsequent call.
- fn fetch_bootstrap(&self) -> Result<(PathBuf, RunMeta, TelemetryContext)> {
+ fn fetch_bootstrap(&self) -> Result<(RunMeta, TelemetryContext)> {
let bootstrap: Bootstrap =
(|| -> reqwest::Result<_> { self.get("bootstrap")?.error_for_status()?.json() })()
.into_diagnostic()?;
Ok((
- bootstrap.git_dir,
bootstrap.meta,
TelemetryContext {
traceparent: bootstrap.traceparent,
@@ -352,7 +351,7 @@ fn main() -> Result<()> {
// before the Sentry client flushes to the server.
let _guard = telemetry::init_telemetry(miette_layer, FmtMode::Plain, None, VERSION)?;
- let (git_dir, meta, sentry_ctx) = if local {
+ let (meta, sentry_ctx) = if local {
let Some(git_dir) = git_dir else {
bail!("--git-dir is required for local runs");
};
@@ -363,7 +362,7 @@ fn main() -> Result<()> {
r#ref: git_ref,
pushed_at: jiff::Timestamp::now(),
};
- (git_dir, meta, TelemetryContext::default())
+ (meta, TelemetryContext::default())
} else {
client.fetch_bootstrap().inspect_err(|e| {
tracing::error!(error = %e, "bootstrap fetch failed");
@@ -386,7 +385,7 @@ fn main() -> Result<()> {
let registry = SecretRegistry::new(move |name| client.fetch_secret(name));
- run_pipeline(workspace, sink, log_dir, git_dir, meta, registry)
+ run_pipeline(workspace, sink, log_dir, meta, registry)
}
}
}
@@ -493,7 +492,6 @@ fn run_pipeline(
workspace: PathBuf,
mut sink: Box<dyn EventSink>,
log_dir: PathBuf,
- git_dir: PathBuf,
meta: RunMeta,
registry: SecretRegistry,
) -> Result<()> {
@@ -535,9 +533,7 @@ fn run_pipeline(
let sink: Rc<RefCell<Box<dyn EventSink>>> = Rc::new(RefCell::new(sink));
- let runtime = Rc::new(Runtime::new(
- pipeline, registry, &meta, &git_dir, workspace, log_dir,
- ));
+ let runtime = Rc::new(Runtime::new(pipeline, registry, &meta, workspace, log_dir));
// Active job pointer, shared between the main loop and the
// runtime callback. The callback translates RuntimeEvent into
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index 56bbc81..9f254dd 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -5,23 +5,19 @@
//! token. Local runs pass `--local --git-dir <path>` and derive the
//! commit SHA and ref directly from the git dir.
-use std::path::PathBuf;
-
use serde::{Deserialize, Serialize};
use crate::ci::run::RunMeta;
/// Inputs the orchestrator supplies to a quire-ci subprocess.
///
-/// `git_dir` is the bare repo the run is scoped to. quire-ci surfaces
-/// it via `(jobs :quire/push).git-dir`, which the mirror job's run-fn
-/// passes to git as `GIT_DIR`. The materialized workspace is a flat
-/// `git archive` extract with no `.git` inside, so quire-ci has no
-/// way to recover this path on its own.
+/// Carries push facts (`meta`), run identity (`repo`, `run_id`), and
+/// the trace context (`traceparent`). The bare repo the run is scoped
+/// to stays server-side — quire-ci runs against the materialized
+/// workspace and has no need for it.
#[derive(Debug, Serialize, Deserialize)]
pub struct Bootstrap {
pub meta: RunMeta,
- pub git_dir: PathBuf,
/// The repo this run is scoped to (matches the `runs.repo`
/// column). quire-ci tags Sentry events with it.
pub repo: String,
diff --git a/quire-core/src/ci/runtime.rs b/quire-core/src/ci/runtime.rs
index 4aa0410..aea1808 100644
--- a/quire-core/src/ci/runtime.rs
+++ b/quire-core/src/ci/runtime.rs
@@ -142,7 +142,6 @@ impl Runtime {
pipeline: Pipeline,
registry: SecretRegistry,
meta: &RunMeta,
- git_dir: &std::path::Path,
workspace: std::path::PathBuf,
log_dir: std::path::PathBuf,
) -> Self {
@@ -155,11 +154,6 @@ impl Runtime {
push.set("ref", meta.r#ref.as_str()).expect("set ref");
push.set("pushed-at", meta.pushed_at.to_string().as_str())
.expect("set pushed-at");
- // `git-dir` is environmental rather than a fact about the push;
- // it may belong on an ambient context alongside `sh`/`secret`
- // instead of on this table.
- push.set("git-dir", git_dir.to_string_lossy().as_ref())
- .expect("set git-dir");
let push_value = push.into_lua(lua).expect("push table to value");
// Build per-job input views from transitive reachability.
@@ -1007,115 +1001,4 @@ mod tests {
"expected type error, got: {msg}"
);
}
-
- // --- quire.stdlib mirror tests ---
-
- /// Run `git` once with the standard test env. Asserts success and
- /// returns stdout. Used by the mirror fixture below to set up the
- /// source and target bare repos.
- fn git(args: &[&str], cwd: &std::path::Path) -> String {
- let env_vars: [(&str, &str); 6] = [
- ("GIT_AUTHOR_NAME", "test"),
- ("GIT_AUTHOR_EMAIL", "test@test"),
- ("GIT_COMMITTER_NAME", "test"),
- ("GIT_COMMITTER_EMAIL", "test@test"),
- ("GIT_CONFIG_GLOBAL", "/dev/null"),
- ("GIT_CONFIG_SYSTEM", "/dev/null"),
- ];
- let out = std::process::Command::new("git")
- .args(args)
- .current_dir(cwd)
- .envs(env_vars)
- .output()
- .expect("git");
- assert!(
- out.status.success(),
- "git {:?} failed: {}",
- args,
- String::from_utf8_lossy(&out.stderr)
- );
- String::from_utf8(out.stdout).expect("utf8")
- }
-
- /// Build a source bare repo with one commit and an empty target
- /// bare repo in the same tempdir. Returns (tempdir, source bare,
- /// target bare, head sha).
- fn bare_repo_with_target() -> (
- tempfile::TempDir,
- std::path::PathBuf,
- std::path::PathBuf,
- String,
- ) {
- let dir = tempfile::tempdir().expect("tempdir");
- let work = dir.path().join("work");
- let bare = dir.path().join("repo.git");
- let target = dir.path().join("target.git");
-
- fs_err::create_dir_all(&work).expect("mkdir work");
- git(&["init", "-b", "main"], &work);
- git(&["commit", "--allow-empty", "-m", "initial"], &work);
- let sha = git(&["rev-parse", "HEAD"], &work).trim().to_string();
- git(
- &[
- "clone",
- "--bare",
- work.to_str().unwrap(),
- bare.to_str().unwrap(),
- ],
- dir.path(),
- );
- git(&["init", "--bare", target.to_str().unwrap()], dir.path());
-
- (dir, bare, target, sha)
- }
-
- #[test]
- fn stdlib_mirror_tags_and_pushes() {
- let (_dir, bare, target, sha) = bare_repo_with_target();
-
- let mut secrets = HashMap::new();
- secrets.insert(
- "github_token".to_string(),
- SecretString::from("Authorization: Bearer test-token"),
- );
-
- let source = format!(
- r#"(local {{: job : runtime}} (require :quire.ci))
-(local {{: mirror}} (require :quire.stdlib))
-(job :go [:quire/push]
- (fn []
- (let [auth (runtime.secret :github_token)]
- (mirror {{:url "{url}"
- :auth-header auth
- :sha "{sha}"
- :tag "v1"
- :git-dir "{git_dir}"}}))))"#,
- url = format!("file://{}", target.display()),
- sha = sha,
- git_dir = bare.display(),
- );
-
- let (runtime, run_fn, _guard) = rt(&source, secrets);
- let _: mlua::Value = call_fn(&runtime, &run_fn).expect("mirror should succeed");
-
- // Tag landed in the target repo, pointing at the head SHA.
- let resolved = git(&["rev-parse", "refs/tags/v1"], &target);
- assert_eq!(resolved.trim(), sha);
- }
-
- #[test]
- fn stdlib_mirror_errors_on_missing_required_opt() {
- let source = r#"(local {: job} (require :quire.ci))
-(local {: mirror} (require :quire.stdlib))
-(job :go [:quire/push]
- (fn []
- (mirror {:auth-header "x" :sha "x" :tag "v1" :git-dir "/tmp"})))"#;
- let (_runtime, run_fn, _guard) = rt(source, HashMap::new());
- let err = run_fn.call::<mlua::Value>(()).unwrap_err();
- let msg = err.to_string();
- assert!(
- msg.contains("Missing argument url"),
- "expected missing-:url error, got: {msg}"
- );
- }
}
diff --git a/quire-core/src/ci/stdlib.fnl b/quire-core/src/ci/stdlib.fnl
index c851c79..744dbf1 100644
--- a/quire-core/src/ci/stdlib.fnl
+++ b/quire-core/src/ci/stdlib.fnl
@@ -5,55 +5,4 @@
(local M {})
-(fn trim [s]
- (string.gsub s "%s+$" ""))
-
-(fn cat [...]
- ;; Concatenate sequence tables into a fresh sequence.
- (let [out []]
- (each [_ t (ipairs [...])]
- (each [_ x (ipairs t)]
- (table.insert out x)))
- out))
-
-;; (mirror opts)
-;;
-;; Tag a commit and push the tag (plus optional refs) to a remote.
-;;
-;; opts: {:url — remote URL (required)
-;; :auth-header — full HTTP header line passed to git as
-;; `http.extraHeader`; resolve via
-;; `runtime.secret` at the call site (required)
-;; :sha — commit to tag (required)
-;; :tag — tag name (required)
-;; :git-dir — bare git directory the run is scoped to (required)
-;; :refs — extra refs to push alongside the tag
-;; (optional, default [])}
-;;
-;; Returns {:tag :pushed_refs}. Raises on missing required opts or
-;; non-zero git exits. `lambda` checks the required bindings for nil
-;; at the call site.
-(λ M.mirror [{: url : auth-header : sha : tag : git-dir :refs ?refs}]
- (let [{: sh} (. (require :quire.ci) :runtime)
- refs (or ?refs [])
- ;; Pass http.extraHeader via GIT_CONFIG_* env (git 2.31+)
- ;; instead of `-c http.extraHeader=…` in argv. Keeps the auth
- ;; header out of `ps` and out of any argv logging we add
- ;; later; runtime.sh's redact pass on stdout/stderr remains as
- ;; defense in depth.
- sh-opts {:env {:GIT_DIR git-dir
- :GIT_CONFIG_COUNT :1
- :GIT_CONFIG_KEY_0 :http.extraHeader
- :GIT_CONFIG_VALUE_0 auth-header}}
- tag-result (sh [:git :tag tag sha] sh-opts)]
- (when (not= 0 tag-result.exit)
- (error (.. "git tag failed: " (trim tag-result.stderr))))
- (let [push-args (cat [:git :push :--porcelain url]
- refs
- [(.. :refs/tags/ tag)])
- push-result (sh push-args sh-opts)]
- (when (not= 0 push-result.exit)
- (error (.. "git push failed: " (trim push-result.stderr))))
- {: tag :pushed_refs refs})))
-
M
diff --git a/quire-core/src/fennel.rs b/quire-core/src/fennel.rs
index aceb5ad..52fe7d5 100644
--- a/quire-core/src/fennel.rs
+++ b/quire-core/src/fennel.rs
@@ -537,12 +537,14 @@ mod tests {
#[test]
fn stdlib_module_preloaded_at_construction() {
let f = fennel();
- let module: mlua::Table = f
+ // `quire.stdlib` is the home for run-fn helpers composed from
+ // runtime primitives. The module loads even when it ships no
+ // helpers, so a future addition needs no loader changes.
+ let _: mlua::Table = f
.lua()
.load(r#"return require("quire.stdlib")"#)
.eval()
.expect("require quire.stdlib");
- let _: mlua::Function = module.get("mirror").expect("mirror should be a function");
}
#[test]
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index bbd1806..5b81bf8 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -278,7 +278,7 @@ impl Run {
cmd.arg("--local").arg("--git-dir").arg(git_dir);
}
Some(s) => {
- self.store_bootstrap_data(git_dir, traceparent)?;
+ self.store_bootstrap_data(traceparent)?;
cmd.env("QUIRE__SERVER_URL", &s.server_url);
cmd.env("QUIRE__RUN_TOKEN", &s.run_token);
}
@@ -425,17 +425,11 @@ impl Run {
/// Called by `execute` when the API transport is active, before spawning
/// quire-ci. quire-ci fetches this via `GET /api/runs/:id/bootstrap`
/// instead of reading a file.
- fn store_bootstrap_data(&self, git_dir: &Path, traceparent: Option<&str>) -> Result<()> {
- let git_dir_str = git_dir.to_str().ok_or_else(|| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- "git_dir path is not valid UTF-8",
- )
- })?;
+ fn store_bootstrap_data(&self, traceparent: Option<&str>) -> Result<()> {
let db = crate::db::open(&self.db_path)?;
db.execute(
- "UPDATE runs SET git_dir = ?1, traceparent = ?2 WHERE id = ?3",
- rusqlite::params![git_dir_str, traceparent, &self.id],
+ "UPDATE runs SET traceparent = ?1 WHERE id = ?2",
+ rusqlite::params![traceparent, &self.id],
)?;
Ok(())
}
diff --git a/quire-server/src/quire/web/api.rs b/quire-server/src/quire/web/api.rs
index 79695a0..95ad4c6 100644
--- a/quire-server/src/quire/web/api.rs
+++ b/quire-server/src/quire/web/api.rs
@@ -5,8 +5,6 @@
//! is created and stored in `runs.run_token`. The bearer token itself
//! identifies the run — no run ID appears in the path.
-use std::path::PathBuf;
-
use serde::Deserialize;
use axum::extract::{FromRequestParts, State};
@@ -137,11 +135,8 @@ async fn verify_run_token(
///
/// Returns the bootstrap payload for a run. One-shot: the server marks
/// bootstrap as fetched on the first successful read and returns 410 on
-/// any subsequent call. Auth is handled by [`verify_run_token`] middleware.
-///
-/// Returns 404 if the run does not have API bootstrap data (e.g. the run
-/// was created with filesystem transport and `store_bootstrap_data` was
-/// never called).
+/// any subsequent call. Auth is handled by [`verify_run_token`] middleware,
+/// which resolves the run from the bearer token before this handler runs.
async fn get_bootstrap(
State(quire): State<Quire>,
axum::Extension(run_id): axum::Extension<String>,
@@ -155,7 +150,6 @@ async fn get_bootstrap(
sha: String,
ref_name: String,
pushed_at_ms: i64,
- git_dir: Option<String>,
traceparent: Option<String>,
dispatched_at: Option<i64>,
repo: String,
@@ -163,7 +157,7 @@ async fn get_bootstrap(
let row: RunRow = db
.prepare(
- "SELECT sha, ref_name, pushed_at_ms, git_dir, traceparent, dispatched_at, repo
+ "SELECT sha, ref_name, pushed_at_ms, traceparent, dispatched_at, repo
FROM runs WHERE id = ?1",
)?
.query_and_then(rusqlite::params![run_id], serde_rusqlite::from_row)?
@@ -174,8 +168,6 @@ async fn get_bootstrap(
return Err(ApiError::Gone);
}
- let git_dir: PathBuf = row.git_dir.map(PathBuf::from).ok_or(ApiError::NotFound)?;
-
let meta = RunMeta {
sha: row.sha,
r#ref: row.ref_name,
@@ -191,7 +183,6 @@ async fn get_bootstrap(
Ok(Bootstrap {
meta,
- git_dir,
repo: row.repo,
run_id,
traceparent: row.traceparent,
@@ -295,7 +286,6 @@ mod tests {
async fn create_run_with_bootstrap(
env: &TestEnv,
session: &ApiSession,
- git_dir: &str,
traceparent: Option<&str>,
) -> String {
let run = env
@@ -306,8 +296,8 @@ mod tests {
let db = crate::db::open(&env.quire.db_path()).expect("db open");
db.execute(
- "UPDATE runs SET git_dir = ?1, traceparent = ?2 WHERE id = ?3",
- rusqlite::params![git_dir, traceparent, &run_id],
+ "UPDATE runs SET traceparent = ?1 WHERE id = ?2",
+ rusqlite::params![traceparent, &run_id],
)
.expect("update bootstrap data");
run_id
@@ -317,7 +307,7 @@ mod tests {
async fn bootstrap_returns_401_without_auth() {
let env = TestEnv::new();
let session = ApiSession::new(3000);
- create_run_with_bootstrap(&env, &session, "/repos/test.git", None).await;
+ create_run_with_bootstrap(&env, &session, None).await;
let resp = get(env.app(), "/run/bootstrap", None).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
@@ -335,7 +325,7 @@ mod tests {
async fn bootstrap_returns_payload_on_first_fetch() {
let env = TestEnv::new();
let session = ApiSession::new(3000);
- let run_id = create_run_with_bootstrap(&env, &session, "/repos/test.git", None).await;
+ let run_id = create_run_with_bootstrap(&env, &session, None).await;
let resp = get(env.app(), "/run/bootstrap", Some(&session.run_token)).await;
assert_eq!(resp.status(), StatusCode::OK);
@@ -343,7 +333,6 @@ mod tests {
use http_body_util::BodyExt;
let body = resp.into_body().collect().await.unwrap().to_bytes();
let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
- assert_eq!(parsed["git_dir"], "/repos/test.git");
assert_eq!(parsed["repo"], "test.git");
assert_eq!(parsed["run_id"], run_id);
}
@@ -352,7 +341,7 @@ mod tests {
async fn bootstrap_returns_410_on_second_fetch() {
let env = TestEnv::new();
let session = ApiSession::new(3000);
- create_run_with_bootstrap(&env, &session, "/repos/test.git", None).await;
+ create_run_with_bootstrap(&env, &session, None).await;
let token = &session.run_token;
let first = get(env.app(), "/run/bootstrap", Some(token)).await;