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
38 changes: 37 additions & 1 deletion completions/bash/forge-cli
Original file line number Diff line number Diff line change
Expand Up @@ -1348,12 +1348,48 @@ _forge-cli() {
return 0
;;
forge__cli__pr__deliver)
opts="-h --format --remote --provider --repo --dry-run --help"
opts="-h --kind --title --body --body-file --head --base --method --reviewer --timeout --no-merge --allow-non-default-base --format --remote --provider --repo --dry-run --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--kind)
COMPREPLY=($(compgen -W "feature bug" -- "${cur}"))
return 0
;;
--title)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--body)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--body-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--head)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--base)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--method)
COMPREPLY=($(compgen -W "squash merge rebase" -- "${cur}"))
return 0
;;
--reviewer)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--timeout)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--format)
COMPREPLY=($(compgen -W "text json" -- "${cur}"))
return 0
Expand Down
11 changes: 11 additions & 0 deletions completions/zsh/_forge-cli
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,22 @@ json\:"Single-record JSON envelope (snake_case)"))' \
;;
(deliver)
_arguments "${_arguments_options[@]}" : \
'--kind=[PR / MR kind (selects branch-prefix rule). Required]:KIND:(feature bug)' \
'--title=[PR / MR title (required)]:TITLE:_default' \
'(--body-file)--body=[PR / MR body / description text. Mutually exclusive with \`--body-file\`]:BODY:_default' \
'--body-file=[Read PR / MR body from a file. Use \`-\` to read from stdin]:PATH:_default' \
'--head=[Source branch (defaults to the current branch)]:HEAD:_default' \
'--base=[Target / base branch (defaults to the repo'\''s default branch)]:BASE:_default' \
'--method=[Merge method (default\: squash)]:METHOD:(squash merge rebase)' \
'*--reviewer=[Add a reviewer (repeatable)]:USER:_default' \
'--timeout=[CI-wait budget before declaring \`checks_timeout\` (default \`30m\`)]:TIMEOUT:_default' \
'--format=[Output format (defaults to text)]:FORMAT:((text\:"Human-readable text output (default)"
json\:"Single-record JSON envelope (snake_case)"))' \
'--remote=[Git remote whose URL feeds provider detection (default\: \`origin\`)]:REMOTE:_default' \
'--provider=[Override auto-detected provider]:PROVIDER:(github gitlab)' \
'--repo=[Override the repo slug (\`owner/name\`). When absent it is derived from the remote URL]:owner/name:_default' \
'--no-merge[Stop after \`pr.wait-checks\` — do not promote to ready or merge]' \
'--allow-non-default-base[Allow merges where the PR'\''s base is not the repo'\''s default branch]' \
'--dry-run[Render the backend command that would run, without invoking it. The envelope'\''s \`data.plan\` carries the exact argv]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
Expand Down
63 changes: 54 additions & 9 deletions crates/forge-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::time::Duration;

use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use nils_common::cli_contract::{OutputFormat, emit_parse_error, exit, schema_version_for};
use nils_common::cli_contract::{OutputFormat, emit_parse_error, exit};

use crate::error::ForgeError;
use crate::ops;
Expand Down Expand Up @@ -161,6 +161,13 @@ impl PrKindFlag {
PrKindFlag::Bug => crate::validations::PrKind::Bug,
}
}

pub fn as_str(self) -> &'static str {
match self {
PrKindFlag::Feature => "feature",
PrKindFlag::Bug => "bug",
}
}
}

/// `--state` filter shared by `pr list` and the close payload normaliser.
Expand Down Expand Up @@ -421,7 +428,46 @@ pub enum PrCommand {
/// Block until every required check reaches a terminal state.
WaitChecks(PrWaitChecksArgs),
/// End-to-end "open draft → CI green → ready → merge" macro.
Deliver,
Deliver(PrDeliverArgs),
}

/// `pr deliver` arguments. Maps to
/// `forge-cli-ops-v1.yaml::operations.pr.deliver` inputs.
#[derive(Args, Debug, Clone)]
pub struct PrDeliverArgs {
/// PR / MR kind (selects branch-prefix rule). Required.
#[arg(long, value_enum)]
pub kind: PrKindFlag,
/// PR / MR title (required).
#[arg(long)]
pub title: String,
/// PR / MR body / description text. Mutually exclusive with `--body-file`.
#[arg(long, conflicts_with = "body_file")]
pub body: Option<String>,
/// Read PR / MR body from a file. Use `-` to read from stdin.
#[arg(long = "body-file", value_name = "PATH")]
pub body_file: Option<String>,
/// Source branch (defaults to the current branch).
#[arg(long)]
pub head: Option<String>,
/// Target / base branch (defaults to the repo's default branch).
#[arg(long)]
pub base: Option<String>,
/// Merge method (default: squash).
#[arg(long, value_enum, default_value_t = MergeMethodFlag::Squash)]
pub method: MergeMethodFlag,
/// Add a reviewer (repeatable).
#[arg(long = "reviewer", value_name = "USER")]
pub reviewers: Vec<String>,
/// CI-wait budget before declaring `checks_timeout` (default `30m`).
#[arg(long, value_parser = parse_duration, default_value = "30m")]
pub timeout: Duration,
/// Stop after `pr.wait-checks` — do not promote to ready or merge.
#[arg(long = "no-merge", action = ArgAction::SetTrue)]
pub no_merge: bool,
/// Allow merges where the PR's base is not the repo's default branch.
#[arg(long = "allow-non-default-base", action = ArgAction::SetTrue)]
pub allow_non_default_base: bool,
}

/// `issue` subtree.
Expand Down Expand Up @@ -584,6 +630,9 @@ pub fn dispatch(args: Vec<OsString>) -> i32 {
Some(Command::Pr(PrArgs {
command: Some(PrCommand::Merge(args)),
})) => ops::pr_merge::run(&global, args, format),
Some(Command::Pr(PrArgs {
command: Some(PrCommand::Deliver(args)),
})) => crate::macros::pr_deliver::run(&global, args, format),
Some(Command::Issue(IssueArgs {
command: Some(IssueCommand::Create(args)),
})) => ops::issue_create::run(&global, args, format),
Expand Down Expand Up @@ -613,13 +662,6 @@ pub fn dispatch(args: Vec<OsString>) -> i32 {
let _ = <Cli as clap::CommandFactory>::command().print_help();
return exit::USAGE;
}
// Every other subcommand declared above is part of the v1 surface but
// not implemented in Sprint 1. The structured failure keeps callers on
// the contract instead of panicking on `todo!()`.
_ => Err(ForgeError::not_implemented(
schema_version_for(BINARY, "error", 1),
"subcommand not implemented in this sprint",
)),
};

match result {
Expand Down Expand Up @@ -818,6 +860,9 @@ mod tests {
"create" => {
argv.extend(["--title", "demo", "--kind", "feature", "--body", "x"]);
}
"deliver" => {
argv.extend(["--kind", "feature", "--title", "demo"]);
}
_ => {}
}
let result = parse(&argv);
Expand Down
1 change: 1 addition & 0 deletions crates/forge-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod config;
pub mod envelope;
pub mod error;
pub mod glab_version;
pub mod macros;
pub mod ops;
pub mod provider;
pub mod validations;
Expand Down
6 changes: 6 additions & 0 deletions crates/forge-cli/src/macros/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Macro orchestrators that compose multiple atoms into a single envelope.
//!
//! Spec / ops: `cli.forge-cli.pr.deliver.v1` is the only macro in v1; future
//! macros (e.g. `issue deliver`) will live here too.

pub mod pr_deliver;
Loading