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}