apiforge 0.1.1

Production-grade API release automation CLI. From merged code to healthy pods in production — one command.
Documentation
use crate::error::Result;
use crate::integrations::git::GitRepo;
use crate::steps::{Step, StepContext, StepOutput};
use crate::utils::format_version;
use async_trait::async_trait;
use semver::Version;

pub struct GitPushStep {
    version: Version,
}

impl GitPushStep {
    pub fn new(version: Version) -> Self {
        Self { version }
    }
}

#[async_trait]
impl Step for GitPushStep {
    fn name(&self) -> &str {
        "git-push"
    }

    fn description(&self) -> &str {
        "Push commits and tags to remote"
    }

    async fn validate(&self, ctx: &StepContext) -> Result<()> {
        let repo = GitRepo::open()?;
        if !repo.remote_exists(&ctx.config.git.remote) {
            return Err(
                crate::error::GitError::RemoteNotFound(ctx.config.git.remote.clone()).into(),
            );
        }
        Ok(())
    }

    async fn execute(&self, ctx: &StepContext) -> Result<StepOutput> {
        let repo = GitRepo::open()?;
        let remote = &ctx.config.git.remote;
        let branch = repo.current_branch()?;

        repo.push(remote, &format!("refs/heads/{}", branch))?;

        let tag_name = format_version(&self.version, &ctx.config.git.tag_format);
        repo.push(remote, &format!("refs/tags/{}", tag_name))?;

        Ok(StepOutput::ok(format!(
            "Pushed to {} (branch + tag)",
            remote
        )))
    }

    async fn dry_run(&self, ctx: &StepContext) -> Result<StepOutput> {
        let repo = GitRepo::open()?;
        let branch = repo.current_branch()?;
        let tag_name = format_version(&self.version, &ctx.config.git.tag_format);

        Ok(StepOutput::ok(format!(
            "Would push branch '{}' and tag '{}' to '{}'",
            branch, tag_name, ctx.config.git.remote
        )))
    }
}