Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f28e37f
Make tri build from a clean checkout, add rtl check, build it in CI (…
gHashTag Aug 17, 2026
743ac10
Add tri gates: find workflows that have never once succeeded (Refs #2…
gHashTag Aug 17, 2026
61a86f9
Add tri red: what is failing on the default branch right now (Refs #2…
gHashTag Aug 17, 2026
c6326c9
feat(tri): add `tri mutate` — find constants a checker never checks
gHashTag Aug 18, 2026
ea616b5
fix(tri): verify the restore instead of assuming it
gHashTag Aug 18, 2026
b7b73ae
fix(tri): back the file up instead of demanding a clean tree
gHashTag Aug 18, 2026
3d6702e
fix(tri): clear derived bytecode caches, not just the source
gHashTag Aug 18, 2026
eaca4ba
feat(tri): add `tri pr ready` — is this PR actually safe to merge?
gHashTag Aug 18, 2026
cf3431b
feat(tri): add `tri synth area` — area with the instrument named
gHashTag Aug 18, 2026
ea0ac65
fix(tri): let `pr ready` wait, because the verdict raced the checks
gHashTag Aug 18, 2026
9d350d7
fix(tri): split the wait-loop jq, which errored on first use
gHashTag Aug 18, 2026
bfb0aba
feat(tri): add `tri sweep area` — catch a synthesis fold by its shape
gHashTag Aug 18, 2026
206793c
fix(tri): let the wait loop survive a transient API failure
gHashTag Aug 18, 2026
d700480
fix(tri): give each command back its own description
gHashTag Aug 18, 2026
515c572
feat(tri): let `pr ready --merge` perform the merge itself
gHashTag Aug 18, 2026
5c09cfe
chore(tri): merge master into the CLI wave (Closes #2222)
gHashTag Aug 19, 2026
2975041
docs(now): the CLI wave's NOW entry (Closes #2222)
gHashTag Aug 19, 2026
7dfa115
ci(tri): build --all-targets -- the plain build hid two master test-b…
gHashTag Aug 19, 2026
e93ca83
chore(tri): merge master with the restored test build (Closes #2222)
gHashTag Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/cli-tri.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# This repository has 35 workflows and, until this one, not a single one built
# the `tri` crate. The consequence was exactly what you would expect: `cargo
# build -p tri` on main failed because dlc10 embeds a bitstream with
# `include_bytes!` that was committed to a feature branch and never to main, so
# nobody could build the CLI from a clean checkout. An autonomous loop went on
# committing into cli/tri/src/ the whole time.
#
# A crate nothing builds is a crate nobody can use, and the breakage is silent
# because there is no signal to be red.
name: cli-tri

on:
push:
branches: [main, master]
paths:
- 'cli/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/cli-tri.yml'
pull_request:
paths:
- 'cli/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/cli-tri.yml'
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable

# From a clean checkout, with nothing copied in by hand. That is the
# condition that was broken and the only one worth asserting.
- name: cargo build -p tri --all-targets
# Tests are targets too. The plain build stayed green twice while the
# test build was broken on master (#2227, #2236) -- every PR inherited
# a break no gate reported. --all-targets closes that gap.
run: cargo build -p tri --all-targets

- name: cargo test -p tri
run: cargo test -p tri

# `tri rtl check` reports numbers, and a binary that runs but reports
# nothing would pass the two steps above. yosys is installed so the
# command is exercised against a real design rather than assumed.
- name: the CLI actually produces a report
run: |
set -uo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq yosys
./target/debug/tri rtl check chips/phi --json > /tmp/r.json 2>/tmp/r.err || true
cat /tmp/r.err | head -5
N=$(python3 -c "import json;print(len(json.load(open('/tmp/r.json'))['checks']))" 2>/dev/null || echo 0)
echo "verdict lines: $N"
if [ "${N:-0}" -lt 5 ]; then
echo "::error::tri rtl check emitted $N verdicts; five checks should each emit one"
exit 1
fi
139 changes: 139 additions & 0 deletions cli/tri/src/gates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! `tri gates` — find workflows that have never once succeeded.
//!
//! A gate that has never been green carries no information: it is red before
//! your change and red after it, so nobody reads it — and after a while nobody
//! reads the others either. Eighteen such workflows were found across three of
//! these repositories, between them consuming 8182 runs and producing zero
//! green results.
//!
//! That is not an aesthetic complaint. It is the measured cause of nine
//! defects living undetected in a request path that had executed once in its
//! lifetime: when red is the normal colour, a real red says nothing.
//!
//! This was a hand-run loop of `gh api` calls three times before it became a
//! command. It reports, it does not disable anything — deciding between fix,
//! dispatch-only and delete belongs to whoever owns the workflow.

use anyhow::{Context, Result};
use clap::Subcommand;
use std::process::Command;

#[derive(Subcommand)]
pub enum GatesCmd {
/// List active workflows whose lifetime success count is zero.
Dead {
/// owner/repo, repeatable. Defaults to the three this fleet uses.
#[arg(long = "repo")]
repos: Vec<String>,
/// Ignore workflows with fewer lifetime runs than this, so a new or
/// rarely-triggered workflow is not reported as dead.
#[arg(long, default_value_t = 50)]
min_runs: u64,
},
}

pub fn run(cmd: &GatesCmd) -> Result<()> {
match cmd {
GatesCmd::Dead { repos, min_runs } => {
let list: Vec<String> = if repos.is_empty() {
["gHashTag/trinity", "gHashTag/trinity-fpga", "gHashTag/t27"]
.iter()
.map(|s| s.to_string())
.collect()
} else {
repos.clone()
};
dead(&list, *min_runs)
}
}
}

fn gh(args: &[&str]) -> Result<String> {
let out = Command::new("gh")
.args(args)
.output()
.context("gh is not installed or not on PATH")?;
if !out.status.success() {
anyhow::bail!(
"gh {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}

fn count(repo: &str, id: &str, success_only: bool) -> Result<u64> {
let path = if success_only {
format!("repos/{repo}/actions/workflows/{id}/runs?status=success&per_page=1")
} else {
format!("repos/{repo}/actions/workflows/{id}/runs?per_page=1")
};
let s = gh(&["api", &path, "--jq", ".total_count"])?;
Ok(s.parse().unwrap_or(0))
}

fn dead(repos: &[String], min_runs: u64) -> Result<()> {
let mut rows: Vec<(String, String, u64)> = Vec::new();
for repo in repos {
let listing = gh(&[
"api",
&format!("repos/{repo}/actions/workflows?per_page=100"),
"--jq",
r#".workflows[]|select(.state=="active")|"\(.id)\t\(.name)""#,
])?;
for line in listing.lines() {
let mut it = line.splitn(2, '\t');
let (id, name) = match (it.next(), it.next()) {
(Some(a), Some(b)) => (a, b),
_ => continue,
};
let total = count(repo, id, false)?;
// A workflow with few runs is not evidence of anything: it may be
// new, or triggered by a path nobody has touched.
if total < min_runs {
continue;
}
if count(repo, id, true)? == 0 {
rows.push((repo.clone(), name.to_string(), total));
}
}
}

rows.sort_by(|a, b| b.2.cmp(&a.2));
if rows.is_empty() {
println!("No active workflow with >= {min_runs} runs has a zero success count.");
return Ok(());
}

let total: u64 = rows.iter().map(|r| r.2).sum();
println!(
"{} workflow(s) have never succeeded, across {} run(s).\n",
rows.len(),
total
);
for (repo, name, runs) in &rows {
let short: String = name.chars().take(44).collect();
println!(" {runs:>6} {repo:<22} {short}");
}
println!();
println!("A gate that has never been green carries no information: red before");
println!("your change and red after it. Decide per workflow — fix it, make it");
println!("workflow_dispatch only, or delete it. Leaving it red is the one");
println!("option that costs every other gate in the repository.");
Ok(())
}

#[cfg(test)]
mod tests {
/// The `--min-runs` floor exists because "0 successes" over 2 runs is not
/// evidence of a dead gate, and reporting it as one would make this
/// command the thing it is written to find: an alarm nobody reads.
#[test]
fn the_floor_is_what_makes_a_zero_meaningful() {
let below = 2u64;
let at = 50u64;
assert!(below < 50, "2 runs is not evidence");
assert!(at >= 50, "50 runs with no success is");
}
}
50 changes: 50 additions & 0 deletions cli/tri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ use std::process::Command;

mod depin;
mod fpga;
mod gates;
mod hooks;
mod mutate;
mod prcheck;
mod sweep;
mod synth;
mod red;
mod rtl;

#[derive(Parser)]
#[command(name = "tri", about = "PHI LOOP CLI wrapper")]
Expand Down Expand Up @@ -58,6 +65,42 @@ enum Commands {
#[command(subcommand)]
action: fpga::FpgaCmd,
},
/// Find the constants in a checker that nothing actually checks.
Mutate {
#[command(subcommand)]
action: mutate::MutateCmd,
},
/// Is this pull request actually safe to merge?
Pr {
#[command(subcommand)]
action: prcheck::PrCmd,
},
/// Synthesise across a parameter and check the area actually moves.
Sweep {
#[command(subcommand)]
action: sweep::SweepCmd,
},
/// Synthesise a top module and report area, with the instrument named.
Synth {
#[command(subcommand)]
action: synth::SynthCmd,
},
/// What is failing on the default branch right now, and since when.
Red {
#[command(subcommand)]
action: red::RedCmd,
},
/// Find workflows that have never once succeeded.
Gates {
#[command(subcommand)]
action: gates::GatesCmd,
},
/// The structural check t27.ai offers, run locally: five verdicts, the
/// yosys version beside the numbers, and no claim about correctness.
Rtl {
#[command(subcommand)]
action: rtl::RtlCmd,
},
/// Pure-Rust ports of repository commit / push gates.
Hooks {
#[command(subcommand)]
Expand Down Expand Up @@ -655,6 +698,13 @@ fn main() -> Result<()> {
}
Commands::Serve { addr } => cmd_serve(addr)?,
Commands::Fpga { action } => fpga::run(action)?,
Commands::Mutate { action } => mutate::run(action)?,
Commands::Pr { action } => prcheck::run(action)?,
Commands::Sweep { action } => sweep::run(action)?,
Commands::Synth { action } => synth::run(action)?,
Commands::Red { action } => red::run(action)?,
Commands::Gates { action } => gates::run(action)?,
Commands::Rtl { action } => rtl::run(action)?,
Commands::Hooks { action } => hooks::run(action)?,
}

Expand Down
Loading
Loading