release_utils/
github.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Tools for working with the Github API.
10
11use crate::cmd::{run_cmd, RunCommandError};
12use std::path::PathBuf;
13use std::process::Command;
14
15/// Wrapper for the [`gh`] tool.
16///
17/// This tool is already available and authenticated when running
18/// running code in a Github Actions workflow.
19///
20/// [`gh`]: https://cli.github.com/
21#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
22pub struct Gh {
23    exe: PathBuf,
24}
25
26impl Gh {
27    /// Create a new `Gh`.
28    pub fn new() -> Self {
29        Self::with_exe(PathBuf::from("gh"))
30    }
31
32    /// Create a new `Gh` using `exe` as the path to the `gh` executable.
33    pub fn with_exe(exe: PathBuf) -> Self {
34        Self { exe }
35    }
36
37    /// Create a new release.
38    pub fn create_release(
39        &self,
40        opt: CreateRelease,
41    ) -> Result<(), RunCommandError> {
42        let mut cmd = Command::new(&self.exe);
43        cmd.args([
44            "release",
45            "create",
46            // Abort if tag does not exist.
47            "--verify-tag",
48        ]);
49
50        if let Some(title) = &opt.title {
51            cmd.args(["--title", title]);
52        }
53
54        if let Some(notes) = &opt.notes {
55            cmd.args(["--notes", notes]);
56        }
57
58        // Tag from which to create the release.
59        cmd.arg(&opt.tag);
60
61        // Add files to upload with the release.
62        cmd.args(&opt.files);
63
64        run_cmd(cmd)
65    }
66
67    /// Check if a release for the given `tag` exists.
68    pub fn does_release_exist(
69        &self,
70        tag: &str,
71    ) -> Result<bool, RunCommandError> {
72        let mut cmd = Command::new(&self.exe);
73        cmd.args(["release", "view", tag]);
74        match run_cmd(cmd) {
75            Ok(()) => Ok(true),
76            Err(err @ RunCommandError::Launch { .. }) => Err(err),
77            Err(err @ RunCommandError::Wait { .. }) => Err(err),
78            Err(err @ RunCommandError::NonUtf8 { .. }) => Err(err),
79            Err(RunCommandError::NonZeroExit { cmd, status }) => {
80                // There are probably other ways this could fail, but
81                // checking for code 1 should be close enough.
82                if status.code() == Some(1) {
83                    Ok(false)
84                } else {
85                    Err(RunCommandError::NonZeroExit { cmd, status })
86                }
87            }
88        }
89    }
90}
91
92impl Default for Gh {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98/// Inputs for creating a Github release.
99#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
100pub struct CreateRelease {
101    /// Tag to create the release for. This tag must already exist
102    /// before calling `execute`.
103    pub tag: String,
104
105    /// Release title.
106    pub title: Option<String>,
107
108    /// Release notes.
109    pub notes: Option<String>,
110
111    /// Files to upload and attach to the release.
112    pub files: Vec<PathBuf>,
113}