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