Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ pedantic clean, snapshot-tested output. Concrete rules follow.
Stays dependency-light and **clap-free**.
- `mergify-cli` — the binary: clap tree, dispatch, `run_native`, `self_update`,
`cli_schema`.
- `mergify-stack` / `mergify-ci` / `mergify-queue` / `mergify-freeze` /
`mergify-config` — one crate per command group.
- `mergify-stack` / `mergify-ci` / `mergify-queue` / `mergify-events` /
`mergify-freeze` / `mergify-config` — one crate per command group.
`mergify-events` also hosts the shared `/logs` client the `queue`
crate's dequeue diagnosis consumes.
- `mergify-test-support` — shared test scaffolding (not published).

## Error handling
Expand Down Expand Up @@ -236,6 +238,7 @@ Doc updates ship in the **same** commit/PR as the change, never a follow-up.
| `ci`, `tests` | `mergify-ci` |
| `config` | `mergify-config` |
| `queue` | `mergify-merge-queue` |
| `events` | `mergify-events` |
| `freeze` | `mergify-merge-protections` |

3. **Crate `//!` module docs** — keep the purpose/invariant header accurate when
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Every command group maps to a section of the
[Docs](https://docs.mergify.com/stacks/)
- **`mergify queue`** — Inspect and control the merge queue.
[Docs](https://docs.mergify.com/merge-queue/)
- **`mergify events`** — Browse the events Mergify recorded for the
repository or one pull request, as a timeline or JSON.
- **`mergify ci`** — Send JUnit results and pull request scopes from any CI
provider. [Docs](https://docs.mergify.com/ci-insights/)
- **`mergify tests`** — Look up test health and manage the flaky-test
Expand Down
1 change: 1 addition & 0 deletions crates/mergify-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ clap_mangen = { workspace = true }
mergify-ci = { path = "../mergify-ci" }
mergify-config = { path = "../mergify-config" }
mergify-core = { path = "../mergify-core" }
mergify-events = { path = "../mergify-events" }
mergify-freeze = { path = "../mergify-freeze" }
mergify-queue = { path = "../mergify-queue" }
mergify-stack = { path = "../mergify-stack" }
Expand Down
159 changes: 159 additions & 0 deletions crates/mergify-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::io::IsTerminal;
use std::path::PathBuf;
use std::process::ExitCode;

use chrono::TimeDelta;
use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
Expand All @@ -29,6 +30,7 @@ use mergify_config::simulate::SimulateOptions;
use mergify_core::OutputMode;
use mergify_core::StdioOutput;
use mergify_core::pull_request::PullRequestRef;
use mergify_events::list::ListOptions as EventsListOptions;
use mergify_freeze::common::parse_naive_datetime;
use mergify_freeze::create::CreateOptions as FreezeCreateOptions;
use mergify_freeze::delete::DeleteOptions as FreezeDeleteOptions;
Expand Down Expand Up @@ -180,6 +182,7 @@ enum NativeCommand {
QueueUnpause(QueueUnpauseOpts),
QueueStatus(QueueStatusOpts),
QueueShow(QueueShowOpts),
Events(EventsOpts),
FreezeList(FreezeListOpts),
FreezeCreate(FreezeCreateOpts),
FreezeUpdate(FreezeUpdateOpts),
Expand Down Expand Up @@ -547,6 +550,17 @@ struct QueueShowOpts {
output_json: bool,
}

struct EventsOpts {
repository: Option<String>,
token: Option<String>,
api_url: Option<String>,
pr_number: Option<u64>,
since: Option<TimeDelta>,
event_types: Vec<String>,
limit: Option<usize>,
output_json: bool,
}

struct FreezeListOpts {
repository: Option<String>,
token: Option<String>,
Expand Down Expand Up @@ -1028,6 +1042,25 @@ fn dispatch_from_parsed(parsed: CliRoot) -> Dispatch {
verbose: parsed.verbose > 0,
output_json: json,
})),
Subcommands::Events(EventsCliArgs {
repository,
token,
api_url,
pr,
since,
r#type,
limit,
json,
}) => Dispatch::Native(NativeCommand::Events(EventsOpts {
repository,
token,
api_url,
pr_number: pr,
since,
event_types: r#type,
limit,
output_json: json,
})),
Subcommands::Freeze(FreezeArgs {
repository,
token,
Expand Down Expand Up @@ -1665,6 +1698,21 @@ fn run_native(cmd: NativeCommand) -> ExitCode {
)
.await
.map(|()| mergify_core::ExitCode::Success),
NativeCommand::Events(opts) => mergify_events::list::run(
EventsListOptions {
repository: opts.repository.as_deref(),
token: opts.token.as_deref(),
api_url: opts.api_url.as_deref(),
pr_number: opts.pr_number,
since: opts.since,
event_types: opts.event_types,
limit: opts.limit,
output_json: opts.output_json,
},
&mut output,
)
.await
.map(|()| mergify_core::ExitCode::Success),
NativeCommand::FreezeList(opts) => mergify_freeze::list::run(
FreezeListOptions {
repository: opts.repository.as_deref(),
Expand Down Expand Up @@ -2682,6 +2730,14 @@ enum Subcommands {
/// pull request's queue state, and pause or resume merging for a
/// repository.
Queue(QueueArgs),
/// Browse the events Mergify recorded for the repository.
///
/// List the activity log as a timeline — queue enters and leaves,
/// merges, commands, CI Insights, every event type — for the
/// whole repository or one pull request (`--pr`). The output
/// always states the time window it covers: the last 24 hours by
/// default, up to the 90 days the log retains (`--since 90d`).
Events(EventsCliArgs),
/// Schedule and manage merge freezes.
///
/// Create, list, update, and delete freezes that temporarily stop
Expand Down Expand Up @@ -4204,6 +4260,52 @@ struct ShowCliArgs {
json: bool,
}

#[derive(clap::Args)]
struct EventsCliArgs {
/// Mergify or GitHub token. Falls back to ``MERGIFY_TOKEN`` and
/// then ``GITHUB_TOKEN`` env vars.
#[arg(long, short = 't')]
token: Option<String>,

/// Mergify API URL. Falls back to ``MERGIFY_API_URL`` env var,
/// then to the default.
#[arg(long = "api-url", short = 'u')]
api_url: Option<String>,

/// Repository full name (owner/repo). Falls back to
/// ``GITHUB_REPOSITORY`` env var.
#[arg(long, short = 'r')]
repository: Option<String>,

/// Only events for this pull request; omit to cover the whole
/// repository.
#[arg(long, value_name = "PR_NUMBER")]
pr: Option<u64>,

/// How far back to look: an integer with a unit — s, m, h, d or w
/// (e.g. 30m, 12h, 7d). Defaults to 24h; the log retains 90 days
/// (--since 90d is the widest useful window). The window always
/// ends now and is stated in the output.
#[arg(long, value_name = "DURATION", value_parser = mergify_events::list::parse_since)]
since: Option<TimeDelta>,

/// Only events of this type (e.g. action.queue.leave). Repeat the
/// flag to match several types; values pass to the API verbatim,
/// so types newer than this CLI work too.
#[arg(long = "type", value_name = "EVENT_TYPE")]
r#type: Vec<String>,

/// Stop after the newest N events instead of fetching the whole
/// window. The output says so when it takes effect.
#[arg(long, value_name = "N")]
limit: Option<usize>,

/// Emit a single JSON document (the raw events, newest first,
/// with the queried window echoed) instead of the timeline.
#[arg(long, default_value_t = false)]
json: bool,
}

#[derive(clap::Args)]
struct FreezeArgs {
/// Mergify or GitHub token. Falls back to ``MERGIFY_TOKEN`` and
Expand Down Expand Up @@ -4376,6 +4478,7 @@ mod tests {
"ci",
"tests",
"queue",
"events",
"freeze",
"stack",
"self-update",
Expand Down Expand Up @@ -4614,6 +4717,62 @@ mod tests {
assert_eq!(opts.files, vec!["report.xml"]);
}

#[test]
fn events_dispatches_natively_with_every_flag() {
let parsed = parse(&[
"events",
"-r",
"owner/repo",
"--pr",
"1740",
"--since",
"7d",
"--type",
"action.queue.leave",
"--type",
"command.queue",
"--limit",
"50",
"--json",
]);
let Dispatch::Native(NativeCommand::Events(opts)) = dispatch_from_parsed(parsed) else {
panic!("events must dispatch to the native Events variant");
};
assert_eq!(opts.repository.as_deref(), Some("owner/repo"));
assert_eq!(opts.pr_number, Some(1740));
assert_eq!(opts.since, Some(TimeDelta::days(7)));
assert_eq!(
opts.event_types,
vec!["action.queue.leave", "command.queue"],
);
assert_eq!(opts.limit, Some(50));
assert!(opts.output_json);
}

#[test]
fn events_defaults_to_repo_wide_last_24h() {
// No --pr, no --since: the command covers the repository and
// the run applies (and states) the 24h default itself.
let parsed = parse(&["events"]);
let Dispatch::Native(NativeCommand::Events(opts)) = dispatch_from_parsed(parsed) else {
panic!("events must dispatch to the native Events variant");
};
assert_eq!(opts.pr_number, None);
assert_eq!(opts.since, None);
assert!(opts.event_types.is_empty());
assert!(!opts.output_json);
}

#[test]
fn events_rejects_a_since_past_the_retention_cap() {
// The impossible window dies as a usage error carrying the
// fix, not as an API 422 later.
let Err(err) = CliRoot::try_parse_from(["mergify", "events", "--since", "94d"]) else {
panic!("a 94d window must be a usage error");
};
assert!(err.to_string().contains("90d"), "got: {err}");
}

#[test]
fn tests_quarantines_add_dispatches_natively() {
let parsed = parse(&[
Expand Down
Loading