///|
suberror ReviewFailure {
ReviewFailure(String)
}
///|
fn read_utf8(path : String) -> String raise {
@utf8.decode(@fs.read_file_to_bytes(path)) catch {
e => raise ReviewFailure("Cannot read UTF-8 input \{path}: \{Repr(e)}")
}
}
///|
fn save(output : String, name : String, text : String) -> Unit raise {
@fs.write_string_to_file(output + "/" + name, text) catch {
e => raise ReviewFailure("Cannot write \{name}: \{Repr(e)}")
}
}
///|
fn review(
project : String,
manifest_path : String,
output : String,
baseline : String?,
) -> (Int32, Int32) raise {
let manifest = match @manifest.parse_entries(read_utf8(manifest_path)) {
Ok(manifest) => manifest
Err(e) => raise ReviewFailure("Manifest: " + e)
}
let inputs = []
for entry in manifest.entries {
inputs.push(
@lens.Migration::new(
entry.path,
read_utf8(entry.path),
execution=entry.execution,
initial_transaction=entry.initial_transaction,
require_lock_timeout=entry.require_lock_timeout,
max_lock_timeout_ms?=entry.maximum,
),
)
}
let archive = match
@lens.ReviewArchive::review(
project,
inputs[:],
initial_lock_timeout_ms?=manifest.initial_lock_timeout_ms,
) {
Ok(archive) => archive
Err(e) => raise ReviewFailure("Review configuration: " + e)
}
// Save absolute evidence before any baseline failure or finding can gate CI.
save(output, "archive.json", archive.to_json().stringify())
save(output, "report.txt", archive.to_text())
let sarif = match archive.to_sarif() {
Ok(sarif) => sarif
Err(e) => raise ReviewFailure("SARIF export: " + e)
}
save(output, "report.sarif.json", sarif.stringify())
let absolute = archive.exit_code()
let gate = if baseline is Some(path) {
let previous = match @lens.ReviewArchive::parse_json(read_utf8(path)) {
Ok(previous) => previous
Err(e) => raise ReviewFailure("Baseline: " + e)
}
let delta = match @lens.compare_archives(previous, archive) {
Ok(delta) => delta
Err(e) => raise ReviewFailure("Baseline comparison: " + e)
}
save(output, "delta.json", delta.to_json().stringify())
save(output, "delta.txt", delta.to_text())
delta.exit_code()
} else {
absolute
}
(absolute, gate)
}
///|
fn write_status(
output : String,
code : Int32,
absolute : Int32?,
baseline_used : Bool,
message : String,
) -> Unit raise {
let data : Json = {
"format_version": 1,
"exit_code": code,
"absolute_exit_code": match absolute {
Some(value) => Json(value)
None => Json::null()
},
"baseline_used": baseline_used,
"message": message,
}
save(output, "status.json", data.stringify())
}
///|
fn main {
let args = @env.args()[argument_prefix():].to_owned()
if args.length() < 3 || args.length() > 4 {
println(
"usage: migration-lens-ci [trusted-v2-baseline.json]",
)
@sys.exit(2)
return
}
let output = args[2]
if @fs.path_exists(output) {
println("Output already exists; choose a fresh directory: " + output)
@sys.exit(2)
return
}
// A fresh directory prevents stale successful reports from being uploaded
// after a failed run. Existing directories and files are never overwritten.
@fs.create_dir(output) catch {
e => {
println("Cannot create fresh output directory \{output}: \{Repr(e)}")
@sys.exit(2)
return
}
}
let baseline = args.get(3)
let code = try {
write_status(output, 2, None, baseline is Some(_), "Review did not finish")
let (absolute, gate) = review(args[0], args[1], output, baseline)
write_status(
output,
gate,
Some(absolute),
baseline is Some(_),
"Review finished; inspect full archive for absolute findings",
)
println(
"MigrationLens CI: absolute=\{absolute}; gate=\{gate}; reports=\{output}",
)
gate
} catch {
e => {
let message = "Review failed: \{Repr(e)}"
println(message)
write_status(output, 2, None, baseline is Some(_), message) catch {
io => println("Cannot save failure status: \{Repr(io)}")
}
2
}
}
@sys.exit(code)
}
///|
#cfg(target="js")
fn argument_prefix() -> Int32 {
2
}
///|
#cfg(target="wasm")
fn argument_prefix() -> Int32 {
1
}
///|
#cfg(target="wasm-gc")
fn argument_prefix() -> Int32 {
1
}
///|
#cfg(target="native")
fn argument_prefix() -> Int32 {
1
}