flowey_lib_common/
gh_workflow_id.rs1use flowey::node::prelude::*;
7
8#[derive(Serialize, Deserialize)]
9pub enum GhRunStatus {
10 Completed,
11 Success,
12}
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct GithubWorkflow {
16 pub id: String,
17 pub commit: String,
18}
19
20#[derive(Serialize, Deserialize)]
21pub enum GitCommitOrBranch {
22 Commit(ReadVar<String>),
23 Branch(ReadVar<String>),
24}
25
26flowey_request! {
27 pub struct Request {
28 pub repo_owner: String,
30 pub repo_name: String,
32 pub commit_or_branch: GitCommitOrBranch,
34 pub pipeline_name: String,
36 pub require_run_status: Option<GhRunStatus>,
38 pub require_successful_job_with_name: Option<String>,
40 pub gh_workflow: WriteVar<GithubWorkflow>,
42 }
43}
44
45new_flow_node!(struct Node);
46
47impl FlowNode for Node {
48 type Request = Request;
49
50 fn imports(ctx: &mut ImportCtx<'_>) {
51 ctx.import::<crate::use_gh_cli::Node>();
52 }
53
54 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
55 for request in requests {
56 let Request {
57 repo_owner,
58 repo_name,
59 commit_or_branch,
60 pipeline_name,
61 require_run_status,
62 require_successful_job_with_name,
63 gh_workflow,
64 } = request;
65
66 let pipeline_name = pipeline_name.clone();
67 let gh_cli = ctx.reqv(crate::use_gh_cli::Request::Get);
68
69 let repo = format!("{repo_owner}/{repo_name}");
70 let commit_hash = match commit_or_branch {
71 GitCommitOrBranch::Commit(commit) => commit,
72 GitCommitOrBranch::Branch(branch) => {
73 ctx.emit_rust_stepv("get latest commit by branch", |ctx| {
74 let branch = branch.claim(ctx);
75 let gh_cli = gh_cli.clone().claim(ctx);
76 let repo = repo.clone();
77
78 move |rt| {
79 let branch = rt.read(branch);
80 let gh_cli = rt.read(gh_cli);
81
82 let commit_hash = flowey::shell_cmd!(
83 rt,
84 "{gh_cli} api repos/{repo}/commits/{branch} --jq .sha"
85 )
86 .read()?
87 .trim()
88 .to_string();
89 Ok(commit_hash)
90 }
91 })
92 }
93 };
94
95 ctx.emit_rust_step("get action id by commit", |ctx| {
96 let gh_workflow = gh_workflow.claim(ctx);
97 let commit_hash = commit_hash.claim(ctx);
98 let pipeline_name = pipeline_name.clone();
99 let gh_cli = gh_cli.claim(ctx);
100
101 move |rt| {
102 let commit_hash = rt.read(commit_hash);
103 let gh_cli = rt.read(gh_cli);
104
105 let workflow = get_action_id_by_commit(
106 rt,
107 commit_hash,
108 gh_cli,
109 repo,
110 pipeline_name,
111 require_run_status,
112 require_successful_job_with_name,
113 )?;
114
115 println!("Got action id {}, commit {}", workflow.id, workflow.commit);
116 rt.write(gh_workflow, &workflow);
117
118 Ok(())
119 }
120 });
121 }
122
123 Ok(())
124 }
125}
126
127fn get_action_id_by_commit(
128 rt: &mut RustRuntimeServices<'_>,
129 mut commit_hash: String,
130 gh_cli: PathBuf,
131 repo: String,
132 pipeline_name: String,
133 require_run_status: Option<GhRunStatus>,
134 require_successful_job_with_name: Option<String>,
135) -> anyhow::Result<GithubWorkflow> {
136 let (run_status_flag, run_status_value) = require_run_status
137 .map(|s| {
138 (
139 "-s",
140 match s {
141 GhRunStatus::Completed => "completed",
142 GhRunStatus::Success => "success",
143 },
144 )
145 })
146 .unzip();
147
148 let handle_output =
149 |output: Result<String, xshell::Error>, error_msg: &str| -> Option<String> {
150 match output {
151 Ok(output) if output.trim().is_empty() => None,
152 Ok(output) => Some(output.trim().to_string()),
153 Err(e) => {
154 println!("{}: {}", error_msg, e);
155 None
156 }
157 }
158 };
159
160 let get_action_id_for_commit = |commit: &str| -> Option<String> {
162 let output = flowey::shell_cmd!(
163 rt,
164 "{gh_cli} run list
165 -R {repo}
166 --commit {commit}
167 -w {pipeline_name}
168 {run_status_flag...} {run_status_value...}
169 -L 1
170 --json databaseId
171 --jq .[].databaseId"
172 )
173 .read();
174
175 handle_output(
176 output,
177 &format!("Failed to get action id for commit {}", commit),
178 )
179 };
180
181 let verify_job_exists = |action_id: &str, job_name: &str| -> Option<String> {
183 let select = format!(
186 ".jobs[] | select(.name == \"{job_name}\" and .conclusion == \"success\") | .url"
187 );
188 let output = flowey::shell_cmd!(
189 rt,
190 "{gh_cli} run view {action_id}
191 -R {repo}
192 --json jobs
193 --jq={select}"
194 )
195 .read();
196
197 handle_output(
198 output,
199 &format!("Failed to get job {} for action id {}", job_name, action_id),
200 )
201 };
202
203 let get_action_id = |commit: &str| -> Option<String> {
205 let action_id = get_action_id_for_commit(commit)?;
206
207 if let Some(job_name) = &require_successful_job_with_name {
209 verify_job_exists(&action_id, job_name)?;
210 }
211
212 Some(action_id)
213 };
214
215 let mut action_id = get_action_id(&commit_hash);
216 let mut loop_count = 0;
217
218 while action_id.is_none() {
221 println!(
222 "Unable to get action id for commit {}, trying again",
223 commit_hash
224 );
225
226 if loop_count > 4 {
227 anyhow::bail!("Failed to get action id after 5 attempts");
228 }
229
230 commit_hash = flowey::shell_cmd!(
231 rt,
232 "{gh_cli} api repos/{repo}/commits/{commit_hash} --jq .parents[0].sha"
233 )
234 .read()?
235 .trim()
236 .to_string();
237 action_id = get_action_id(&commit_hash);
238
239 loop_count += 1;
240 }
241
242 let id = action_id.context("failed to get action id")?;
244
245 Ok(GithubWorkflow {
246 id,
247 commit: commit_hash,
248 })
249}