Menu
AkurAI-Build
publicLatest change 1488f88860de09bbb6791f4c67d3a8a8a3e0765f - Replace the JSON CI CLI with a full read/write akurai-build MCP tool set (repo add/host/sync/rename/remove/visibility/branches/tree, run queue/wait/retry/promote, artifact get, doctor, init); keep keygen/migrate/serve as plain CLI subcommands; update skill and deploy.md to the MCP tool set by Ólafur Búi Ólafsson
#[path = "support/mcp.rs"]
mod support;
use std::{fs, path::Path, process::Command};
use anyhow::{Context, Result, ensure};
use serde_json::{Value, json};
use support::McpSession;
#[test]
fn git_to_artifact_to_approved_deployment() -> Result<()> {
let temporary = tempfile::tempdir()?;
let repository = temporary.path().join("repository");
let data = temporary.path().join("data");
let key = temporary.path().join("database.key");
fs::create_dir(&repository)?;
git(&repository, &["init", "-b", "main"])?;
git(&repository, &["config", "user.name", "AkurAI Test"])?;
git(
&repository,
&["config", "user.email", "test@example.invalid"],
)?;
fs::write(
repository.join(".akurai.yml"),
r#"version: 1
jobs:
- name: build
executor: native
run: mkdir -p out && printf hello > out/artifact.txt
artifacts: [out/artifact.txt]
- name: deploy
needs: [build]
executor: native
environment: test
approval: true
run: test "$(cat out/artifact.txt)" = hello
"#,
)?;
git(&repository, &["add", ".akurai.yml"])?;
git(&repository, &["commit", "-m", "pipeline"])?;
cli(&data, &key, &["keygen", "--out", support::str(&key)?])?;
cli(&data, &key, &["migrate"])?;
let mut native_session = McpSession::start(&data, &key, true)?;
native_session.call(
"akurai_repo_add",
json!({"name": "fixture", "url": support::str(&repository)?, "branch": "main"}),
)?;
let first = native_session.call(
"akurai_run_queue",
json!({"repository": "fixture", "wait": true}),
)?;
ensure!(
first["status"] == "waiting",
"run should wait for approval: {first}"
);
let id = first["id"].as_i64().context("run has no id")?;
assert!(id >= 1, "first run id should be positive");
ensure!(
first["repository"] == "fixture",
"run should reference the correct repository"
);
ensure!(
first["trigger"] == "manual",
"run should record trigger as manual"
);
let promoted = native_session.call(
"akurai_run_promote",
json!({"id": id, "environment": "test", "wait": true}),
)?;
ensure!(
promoted["status"] == "succeeded",
"promotion failed: {promoted}"
);
let deployments = promoted["deployments"]
.as_array()
.context("deployments should be an array")?;
assert!(
!deployments.is_empty(),
"successful run should have deployments"
);
let deployment = &deployments[0];
ensure!(
deployment["status"] == "succeeded",
"deployment was not persisted as succeeded"
);
ensure!(
deployment["environment"] == "test",
"deployment environment should be 'test'"
);
let waited = native_session.call("akurai_run_wait", json!({"id": id, "timeout": 1}))?;
ensure!(waited["status"] == "succeeded", "wait returned early");
// Verify artifact metadata.
let artifacts = promoted["artifacts"]
.as_array()
.context("artifacts should be an array")?;
assert!(
!artifacts.is_empty(),
"successful run should produce artifacts"
);
let artifact = &artifacts[0];
let artifact_id = artifact["id"].as_i64().context("artifact has no id")?;
assert!(artifact_id >= 1, "artifact id should be positive");
ensure!(
artifact["name"] == "out/artifact.txt",
"artifact should have the correct name: {}",
artifact["name"]
);
ensure!(
artifact["bytes"].as_i64().is_some_and(|s| s > 0),
"artifact should have a positive byte count: {}",
artifact["bytes"]
);
let artifact_path = artifact["path"].as_str().context("artifact has no path")?;
let output = temporary.path().join("download.txt");
native_session.call(
"akurai_artifact_get",
json!({"id": artifact_id, "output": support::str(&output)?}),
)?;
ensure!(
fs::read_to_string(&output)? == "hello",
"downloaded artifact bytes differ"
);
// Tamper detection.
let stored = data.join(artifact_path);
fs::write(&stored, "tampered")?;
let rejected = native_session.call_error(
"akurai_artifact_get",
json!({
"id": artifact_id,
"output": support::str(&temporary.path().join("tampered.txt"))?
}),
)?;
ensure!(
rejected.contains("integrity"),
"tampered artifact was not rejected: {rejected}"
);
drop(native_session);
// Running without native allowance should fail closed.
let mut denied_session = McpSession::start(&data, &key, false)?;
let denied = denied_session.call(
"akurai_run_queue",
json!({"repository": "fixture", "wait": true}),
)?;
ensure!(
denied["status"] == "failed",
"native execution did not fail closed: {}",
denied["error"]
);
// Runs listing should reflect the two runs.
let runs_json = denied_session.call("akurai_runs", json!({"repo": "fixture", "limit": 10}))?;
let runs: &Vec<Value> = runs_json.as_array().context("runs should be an array")?;
assert_eq!(runs.len(), 2, "should have exactly 2 runs for fixture");
let statuses: Vec<&str> = runs.iter().filter_map(|r| r["status"].as_str()).collect();
assert!(
statuses.contains(&"succeeded") && statuses.contains(&"failed"),
"runs should contain succeeded and failed statuses: {statuses:?}"
);
Ok(())
}
fn cli(data: &Path, key: &Path, arguments: &[&str]) -> Result<Value> {
let output = Command::new(env!("CARGO_BIN_EXE_akurai"))
.args([
"--data",
support::str(data)?,
"--key-file",
support::str(key)?,
])
.args(arguments)
.output()?;
let value: Value = serde_json::from_slice(&output.stdout)
.with_context(|| format!("invalid JSON: {}", String::from_utf8_lossy(&output.stdout)))?;
ensure!(
output.status.success(),
"CLI failed: {value}\n{}",
String::from_utf8_lossy(&output.stderr)
);
ensure!(value["ok"] == true, "CLI envelope failed: {value}");
Ok(value)
}
fn git(directory: &Path, arguments: &[&str]) -> Result<()> {
let output = Command::new("git")
.current_dir(directory)
.args(arguments)
.output()?;
ensure!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}