1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Tools for working with the Github API.

use crate::cmd::{run_cmd, RunCommandError};
use std::path::PathBuf;
use std::process::Command;

/// Wrapper for the [`gh`] tool.
///
/// This tool is already available and authenticated when running
/// running code in a Github Actions workflow.
///
/// [`gh`]: https://cli.github.com/
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Gh {
    exe: PathBuf,
}

impl Gh {
    /// Create a new `Gh`.
    pub fn new() -> Self {
        Self::with_exe(PathBuf::from("gh"))
    }

    /// Create a new `Gh` using `exe` as the path to the `gh` executable.
    pub fn with_exe(exe: PathBuf) -> Self {
        Self { exe }
    }

    /// Create a new release.
    pub fn create_release(
        &self,
        opt: CreateRelease,
    ) -> Result<(), RunCommandError> {
        let mut cmd = Command::new(&self.exe);
        cmd.args([
            "release",
            "create",
            // Abort if tag does not exist.
            "--verify-tag",
        ]);

        if let Some(title) = &opt.title {
            cmd.args(["--title", title]);
        }

        if let Some(notes) = &opt.notes {
            cmd.args(["--notes", notes]);
        }

        // Tag from which to create the release.
        cmd.arg(&opt.tag);

        // Add files to upload with the release.
        cmd.args(&opt.files);

        run_cmd(cmd)
    }

    /// Check if a release for the given `tag` exists.
    pub fn does_release_exist(
        &self,
        tag: &str,
    ) -> Result<bool, RunCommandError> {
        let mut cmd = Command::new(&self.exe);
        cmd.args(["release", "view", tag]);
        match run_cmd(cmd) {
            Ok(()) => Ok(true),
            Err(err @ RunCommandError::Launch { .. }) => Err(err),
            Err(err @ RunCommandError::Wait { .. }) => Err(err),
            Err(err @ RunCommandError::NonUtf8 { .. }) => Err(err),
            Err(RunCommandError::NonZeroExit { cmd, status }) => {
                // There are probably other ways this could fail, but
                // checking for code 1 should be close enough.
                if status.code() == Some(1) {
                    Ok(false)
                } else {
                    Err(RunCommandError::NonZeroExit { cmd, status })
                }
            }
        }
    }
}

impl Default for Gh {
    fn default() -> Self {
        Self::new()
    }
}

/// Inputs for creating a Github release.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct CreateRelease {
    /// Tag to create the release for. This tag must already exist
    /// before calling `execute`.
    pub tag: String,

    /// Release title.
    pub title: Option<String>,

    /// Release notes.
    pub notes: Option<String>,

    /// Files to upload and attach to the release.
    pub files: Vec<PathBuf>,
}