Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
9 changes: 9 additions & 0 deletions .github/workflows/api-parity-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ jobs:
- name: Test the audit
run: python -m unittest scripts/test_audit_api_coverage.py

- name: Verify rolling PR credential
env:
REGEN_PR_TOKEN: ${{ secrets.REGEN_PR_TOKEN }}
run: |
if [ -z "$REGEN_PR_TOKEN" ]; then
echo "::error::REGEN_PR_TOKEN is missing or empty. Replace it with a token that has Contents and Pull requests read/write access to coval-ai/cli, then rerun this workflow. A secret name in repository settings does not prove that Actions receives a value."
exit 1
fi

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- name: Refresh the deterministic parity report
run: >-
python scripts/audit_api_coverage.py
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ jobs:
with:
python-version: "3.12"

- name: Verify Homebrew publishing credential
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
set -euo pipefail
if [ -z "$GH_TOKEN" ]; then
echo "::error::HOMEBREW_TAP_TOKEN is missing or empty."
exit 1
fi
if ! can_push="$(gh api repos/coval-ai/homebrew-tap --jq '.permissions.push // false')"; then
echo "::error::HOMEBREW_TAP_TOKEN could not access coval-ai/homebrew-tap."
exit 1
fi
if [ "$can_push" != "true" ]; then
echo "::error::HOMEBREW_TAP_TOKEN cannot push to coval-ai/homebrew-tap. Replace it with a token that has Contents read/write access."
exit 1
fi

- name: Match Cargo manifests and release tag
run: python scripts/release_version.py --expected-tag "$RELEASE_TAG"

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "coval"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
description = "CLI for Coval AI agent evaluation platform"
license = "MIT"
Expand Down
76 changes: 74 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ Command-line interface for the [Coval](https://coval.dev) AI evaluation platform
brew install coval-ai/tap/coval
```

### Cargo
### Build from source

```bash
cargo install coval
cargo install --git https://github.com/coval-ai/cli --locked
```

The CLI is not currently published to crates.io. Official versioned artifacts
are the GitHub release binaries and the `coval-ai/tap/coval` Homebrew formula.

### Binary

Download pre-built binaries from [Releases](https://github.com/coval-ai/cli/releases).
Expand Down Expand Up @@ -221,6 +224,40 @@ until the command implementation or an explicitly reviewed manifest exception
reconciles the drift. A GitHub issue is used only if the automation itself
fails before it can create or update that PR.

The schedule is Monday 2:00 AM PST (10:00 UTC; 3:00 AM during daylight saving
time). GitHub Actions schedules can start later during busy periods. The audit
compares the live public OpenAPI catalog with the checked-in manifest and
first-class Rust command surface. It does not synthesize command UX or publish
a CLI release. A maintainer must implement newly reported commands, regenerate
the report, include the appropriate version bump, and merge the green PR.

To run or recover the workflow:

```bash
# Run the same audit locally.
python3 -m venv .venv
.venv/bin/python -m pip install --requirement scripts/requirements-audit.txt
.venv/bin/python -m unittest scripts/test_audit_api_coverage.py
.venv/bin/python scripts/audit_api_coverage.py \
--write-markdown api-coverage-report.md

# Confirm the secret record and recent runs. This cannot verify the secret value.
gh secret list --repo coval-ai/cli
gh run list --repo coval-ai/cli --workflow api-parity-audit.yml --limit 5

# Replace a missing, empty, expired, or revoked token without putting it in shell history.
gh secret set REGEN_PR_TOKEN --repo coval-ai/cli

# Prove the replacement by dispatching the workflow and inspecting the run.
gh workflow run api-parity-audit.yml --repo coval-ai/cli --ref main
gh run watch --repo coval-ai/cli --exit-status
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
```

`REGEN_PR_TOKEN` must be a fine-grained token limited to `coval-ai/cli` with
Contents and Pull requests read/write access. A visible secret name is not
proof that its stored value is non-empty or usable; only a successful workflow
run proves that. Do not print the token or pass it as a command-line argument.

The SDK regeneration workflow can open deterministic codegen PRs because its
published clients are generated from OpenAPI. The CLI command surface is still
hand-written, so this repository does not present an automated audit as command
Expand All @@ -245,6 +282,41 @@ python3 scripts/bump_version.py minor
python3 scripts/bump_version.py patch
```

Publishing a new version:

1. Include the version bump in the implementation PR. Use `minor` for new
first-class commands and `patch` for backward-compatible fixes.
2. Run `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`,
`cargo test`, and the API coverage commands above. Confirm that
`Cargo.toml`, `Cargo.lock`, and `api-coverage-report.md` are all updated.
3. Merge only after current-head CI and review are green. The successful CI run
for that exact `main` commit triggers `Release on version bump`.
4. Verify the `v*` tag, all five binary artifacts, `SHA256SUMS`, and the GitHub
release. Then verify that `coval-ai/homebrew-tap` contains the same version
and that `brew update && brew upgrade coval-ai/tap/coval` installs it.

If the automatic release needs a retry, dispatch `Release on version bump` from
the `main` branch. It reuses an existing matching tag safely. Do not hand-create
a tag unless intentionally using the lower-level `Release` workflow; a tag must
exactly match the Cargo version, and the lower-level workflow still requires
all release credentials. A successful GitHub release is not complete publishing
proof until the Homebrew formula reports the same version.

To recover Homebrew publishing, replace the token interactively, dispatch the
retry from `main`, and verify both destinations:

```bash
gh secret set HOMEBREW_TAP_TOKEN --repo coval-ai/cli
gh workflow run release-on-version-bump.yml --repo coval-ai/cli --ref main
gh release view --repo coval-ai/cli
gh api -H 'Accept: application/vnd.github.raw+json' \
repos/coval-ai/homebrew-tap/contents/Formula/coval.rb | grep 'version '
```

The release validation checks that `HOMEBREW_TAP_TOKEN` can push to
`coval-ai/homebrew-tap` before building artifacts. As with `REGEN_PR_TOKEN`, a
secret record alone does not prove that the credential is present or authorized.

The release workflow validates tag/version consistency, builds all five target
binaries, creates or updates the GitHub release, and updates
`coval-ai/homebrew-tap`. A manual `Release on version bump` dispatch safely
Expand Down
6 changes: 3 additions & 3 deletions api-coverage-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ opens or updates a PR only when coverage actually changes.
| Metric | Value |
| --- | ---: |
| Reconciliation status | PASS |
| Published operations | 174 |
| First-class CLI operations | 118 |
| Published operations | 175 |
| First-class CLI operations | 119 |
| Reviewed gaps | 56 |
| Client operations | 126 |
| Client operations | 127 |

Catalog: https://api.coval.dev/v1/openapi

Expand Down
6 changes: 3 additions & 3 deletions api-coverage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

[snapshot]
catalog_url = "https://api.coval.dev/v1/openapi"
reviewed_at = "2026-08-04"
published_operations = 174
cli_supported_operations = 118
reviewed_at = "2026-08-12"
published_operations = 175
cli_supported_operations = 119

[[allowed_extra]]
operation = "POST /test-cases/{test_case_id}/media:upload-url"
Expand Down
6 changes: 6 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,12 @@ impl MetricsClient<'_> {
Ok(resp.metric)
}

pub async fn duplicate(&self, id: &str) -> Result<models::Metric, ApiError> {
let url = self.0.url(&format!("/v1/metrics/{id}/duplicate"));
let resp: models::GetMetricResponse = self.0.post_empty(url).await?;
Ok(resp.metric)
}

pub async fn update(
&self,
id: &str,
Expand Down
18 changes: 18 additions & 0 deletions src/commands/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum MetricCommands {
List(ListArgs),
Get(GetArgs),
Create(Box<CreateArgs>),
Duplicate(DuplicateArgs),
Update(Box<UpdateArgs>),
Delete(DeleteArgs),
Test(TestArgs),
Expand Down Expand Up @@ -60,6 +61,7 @@ impl MetricCommands {
Self::List(_) => "list",
Self::Get(_) => "get",
Self::Create(_) => "create",
Self::Duplicate(_) => "duplicate",
Self::Update(_) => "update",
Self::Delete(_) => "delete",
Self::Test(_) => "test",
Expand Down Expand Up @@ -103,6 +105,12 @@ pub struct GetArgs {
metric_id: String,
}

#[derive(Args)]
pub struct DuplicateArgs {
/// ID of the metric to duplicate
metric_id: String,
}

#[derive(Args)]
#[command(
after_help = "Required fields by metric type:\n llm-binary --prompt\n categorical --prompt --categories\n numerical --prompt --min-value --max-value\n audio-binary --prompt\n audio-categorical --prompt --categories\n audio-numerical --prompt --min-value --max-value\n toolcall --prompt\n metadata --metadata-field-type --metadata-field-key\n regex --regex-pattern\n pause --min-pause-duration\n composite --criteria-source test_case --criteria-path expected_behaviors\n or --criteria-source metric_metadata --criteria \"a,b,c\""
Expand Down Expand Up @@ -432,6 +440,16 @@ pub async fn execute(cmd: MetricCommands, client: &CovalClient, ctx: &OutputCont
next_actions::item_result("metrics", &metric.id),
);
}
MetricCommands::Duplicate(args) => {
let metric = client.metrics().duplicate(&args.metric_id).await?;
emit_one_with_actions(
ctx,
"metrics",
operation,
&metric,
next_actions::item_result("metrics", &metric.id),
);
}
MetricCommands::Update(args) => {
let args = *args;
let mut input = args.input_json.object()?;
Expand Down
33 changes: 33 additions & 0 deletions tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,39 @@ async fn test_metrics_create_composite_test_case() {
.stdout(predicate::str::contains("comp1"));
}

#[tokio::test]
async fn test_metrics_duplicate() {
let mock_server = MockServer::start().await;

Mock::given(method("POST"))
.and(path("/v1/metrics/met_source/duplicate"))
.and(header("X-API-Key", "test_key"))
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
"metric": {
"name": "metrics/met_copy",
"id": "met_copy",
"metric_name": "Copied metric",
"description": "A duplicated metric",
"metric_type": "METRIC_LLM",
"create_time": "2026-08-12T12:00:00Z"
}
})))
.mount(&mock_server)
.await;

coval()
.arg("--api-key")
.arg("test_key")
.arg("--api-url")
.arg(mock_server.uri())
.arg("metrics")
.arg("duplicate")
.arg("met_source")
.assert()
.success()
.stdout(predicate::str::contains("met_copy"));
}

#[test]
fn test_metrics_create_composite_test_case_requires_criteria_path() {
coval()
Expand Down