flowey_lib_common/
publish_gh_release.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Publish a github release
5
6use flowey::node::prelude::*;
7
8flowey_request! {
9    pub struct Request(pub GhReleaseParams);
10}
11
12#[derive(Serialize, Deserialize)]
13pub struct GhReleaseParams<C = VarNotClaimed> {
14    /// First component of a github repo path
15    ///
16    /// e.g: the "foo" in "github.com/foo/bar"
17    pub repo_owner: String,
18    /// Second component of a github repo path
19    ///
20    /// e.g: the "bar" in "github.com/foo/bar"
21    pub repo_name: String,
22    /// Tag associated with the release artifact.
23    pub tag: ReadVar<String, C>,
24    /// Title associated with the release artifact.
25    pub title: ReadVar<String, C>,
26    /// Files to upload.
27    pub files: ReadVar<Vec<(PathBuf, Option<String>)>, C>,
28    /// Whether the release should be created as a draft
29    pub draft: bool,
30
31    pub done: WriteVar<SideEffect, C>,
32}
33
34impl GhReleaseParams {
35    pub fn claim(self, ctx: &mut StepCtx<'_>) -> GhReleaseParams<VarClaimed> {
36        let GhReleaseParams {
37            repo_owner,
38            repo_name,
39            tag,
40            title,
41            files,
42            draft,
43            done,
44        } = self;
45
46        GhReleaseParams {
47            repo_owner,
48            repo_name,
49            tag: tag.claim(ctx),
50            title: title.claim(ctx),
51            files: files.claim(ctx),
52            draft,
53            done: done.claim(ctx),
54        }
55    }
56}
57
58new_flow_node!(struct Node);
59
60impl FlowNode for Node {
61    type Request = Request;
62
63    fn imports(ctx: &mut ImportCtx<'_>) {
64        ctx.import::<crate::cache::Node>();
65        ctx.import::<crate::use_gh_cli::Node>();
66    }
67
68    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
69        if requests.is_empty() {
70            return Ok(());
71        }
72
73        let gh_cli = ctx.reqv(crate::use_gh_cli::Request::Get);
74
75        ctx.emit_rust_step("publish github releases", |ctx| {
76            let requests = requests
77                .into_iter()
78                .map(|r| r.0.claim(ctx))
79                .collect::<Vec<_>>();
80            let gh_cli = gh_cli.claim(ctx);
81
82            move |rt| {
83                let gh_cli = rt.read(gh_cli);
84
85                for req in requests {
86                    let GhReleaseParams {
87                        repo_owner,
88                        repo_name,
89                        tag,
90                        title,
91                        files,
92                        draft,
93                        done: _,
94                    } = req;
95
96                    let repo = format!("{repo_owner}/{repo_name}");
97                    let tag = rt.read(tag);
98
99                    // check if the release already exists
100                    //
101                    // xshell doesn't give us the exit code, so we have to
102                    // use the raw process API instead.
103                    let mut command = std::process::Command::new(&gh_cli);
104                    command
105                        .arg("release").arg("view").arg(&tag).arg("--repo").arg(&repo);
106                    let mut child = command.spawn().context(
107                       "failed to spawn gh cli"
108                    )?;
109                    let status = child.wait()?;
110
111                    // success means the release already exists, so skip publishing this release
112                    if status.success() {
113                        log::info!("GitHub release with tag {tag} already exists in repo {repo}. Skipping...");
114                        continue;
115                    };
116
117                    let title = rt.read(title);
118                    let files = rt.read(files)
119                        .into_iter()
120                        .map(|(path, label)| {
121                            let path = path.to_string_lossy().to_string();
122                            if let Some(label) = label {
123                                format!("{path}#{label}")
124                            } else {
125                                path
126                            }
127                        })
128                        .collect::<Vec<_>>();
129                    let draft = draft.then_some("--draft");
130
131                    flowey::shell_cmd!(rt, "{gh_cli} release create --repo {repo} {tag} --title {title} --notes TODO {draft...} {files...}").run()?;
132                }
133
134                Ok(())
135            }
136        });
137
138        Ok(())
139    }
140}