flowey_lib_hvlite/_jobs/
publish_openvmm_gh_release.rs1use crate::assemble_openvmm_vendor_release::{VendorReleaseOutput, read_vendor_identity};
15use flowey::node::prelude::*;
16
17flowey_request! {
18 pub struct Request {
19 pub release: ReadVar<VendorReleaseOutput>,
20 pub done: WriteVar<SideEffect>,
21 }
22}
23
24new_simple_flow_node!(struct Node);
25
26impl SimpleFlowNode for Node {
27 type Request = Request;
28
29 fn imports(ctx: &mut ImportCtx<'_>) {
30 ctx.import::<flowey_lib_common::publish_gh_release::Node>();
31 ctx.import::<flowey_lib_common::use_gh_cli::Node>();
32 }
33
34 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
35 let Request { release, done } = request;
36
37 let files = ctx.emit_rust_stepv("enumerate release files", |ctx| {
38 let release = release.clone().claim(ctx);
39 move |rt| {
40 let release = rt.read(release);
41 vendor_release_files(&release.assets)
42 }
43 });
44
45 let identity = ctx.emit_rust_stepv("resolve source release identity", |ctx| {
46 let release = release.claim(ctx);
47 move |rt| {
48 let release = rt.read(release);
49 read_vendor_identity(&release.assets)
50 }
51 });
52
53 let target = identity.map(ctx, |identity| identity.revision);
54 let tag = identity.map(ctx, |identity| format!("openvmm-v{}", identity.version));
55 let title = identity.map(ctx, |identity| format!("OpenVMM v{}", identity.version));
56
57 let gh_cli = ctx.reqv(flowey_lib_common::use_gh_cli::Request::Get);
62 let no_existing_release = ctx.emit_rust_step("ensure no existing source release", |ctx| {
63 let gh_cli = gh_cli.clone().claim(ctx);
64 let tag = tag.clone().claim(ctx);
65 move |rt| {
66 let gh_cli = rt.read(gh_cli);
67 let tag = rt.read(tag);
68
69 let output =
70 flowey::shell_cmd!(rt, "{gh_cli} release view {tag} --repo microsoft/openvmm")
71 .ignore_status()
72 .output()
73 .context("failed to query the OpenVMM release")?;
74
75 if output.status.success() {
76 anyhow::bail!(
77 "a GitHub release already exists for tag {tag}. It may already have \
78 been reviewed or published, so this run will not pin a tag it could \
79 adopt. Delete it and rerun if it should be regenerated."
80 );
81 }
82
83 let stderr = String::from_utf8_lossy(&output.stderr);
84 if !stderr.contains("release not found") {
85 anyhow::bail!(
86 "failed to query the OpenVMM release for tag {tag}: {}",
87 stderr.trim()
88 );
89 }
90
91 Ok(())
92 }
93 });
94
95 let tag_is_pinned = ctx.emit_rust_step("pin source release tag", |ctx| {
99 no_existing_release.claim(ctx);
103 let _files = files.clone().claim(ctx);
107 let gh_cli = gh_cli.claim(ctx);
108 let tag = tag.clone().claim(ctx);
109 let target = target.clone().claim(ctx);
110 move |rt| {
111 let gh_cli = rt.read(gh_cli);
112 let tag = rt.read(tag);
113 let target = rt.read(target);
114
115 let ref_name = format!("refs/tags/{tag}");
116 let create = flowey::shell_cmd!(
117 rt,
118 "{gh_cli} api --method POST repos/microsoft/openvmm/git/refs -f ref={ref_name} -f sha={target}"
119 )
120 .ignore_status()
121 .output()
122 .context("failed to create the OpenVMM release tag")?;
123 if create.status.success() {
124 log::info!("created release tag {tag} at {target}");
125 return Ok(());
126 }
127
128 let existing = flowey::shell_cmd!(
129 rt,
130 "{gh_cli} api repos/microsoft/openvmm/git/ref/tags/{tag}"
131 )
132 .ignore_status()
133 .output()
134 .context("failed to query the existing OpenVMM release tag")?;
135 if !existing.status.success() {
136 anyhow::bail!(
137 "failed to create release tag {tag}: {}; querying the existing tag also \
138 failed: {}",
139 String::from_utf8_lossy(&create.stderr).trim(),
140 String::from_utf8_lossy(&existing.stderr).trim()
141 );
142 }
143
144 let existing: serde_json::Value = serde_json::from_slice(&existing.stdout)
145 .context("failed to parse the existing OpenVMM release tag")?;
146 let tag_type = existing["object"]["type"].as_str();
147 let tag_target = existing["object"]["sha"].as_str();
148 if tag_type == Some("tag") {
149 anyhow::bail!(
152 "release tag {tag} already exists as an annotated tag (object {}), but \
153 this pipeline publishes lightweight tags naming commit {target}",
154 tag_target.unwrap_or("<unknown>")
155 );
156 }
157 if tag_type != Some("commit") || tag_target != Some(target.as_str()) {
158 anyhow::bail!(
159 "release tag {tag} already exists at {} ({}) instead of commit {target}",
160 tag_target.unwrap_or("<unknown>"),
161 tag_type.unwrap_or("unknown object type")
162 );
163 }
164
165 log::info!("reusing release tag {tag} at {target}");
166 Ok(())
167 }
168 });
169
170 ctx.req(flowey_lib_common::publish_gh_release::Request(
171 flowey_lib_common::publish_gh_release::GhReleaseParams {
172 repo_owner: "microsoft".into(),
173 repo_name: "openvmm".into(),
174 target,
175 tag,
176 title,
177 files,
178 notes: flowey_lib_common::publish_gh_release::GhReleaseNotes::Empty,
182 draft: true,
183 verify_tag: true,
184 on_existing: flowey_lib_common::publish_gh_release::OnExistingRelease::Fail,
188 prerequisites: vec![tag_is_pinned],
189 done,
190 },
191 ));
192
193 Ok(())
194 }
195}
196
197fn vendor_release_files(assets: &Path) -> anyhow::Result<Vec<(PathBuf, Option<String>)>> {
198 let identity = read_vendor_identity(assets)?;
199 let archive = assets.join(identity.archive_name());
200 if !archive.is_file() {
201 anyhow::bail!("missing vendor archive {}", archive.display());
202 }
203
204 Ok(vec![(archive, None)])
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::assemble_openvmm_vendor_release::{IDENTITY_FILE, VendorReleaseIdentity};
211
212 #[test]
213 fn identity_stays_private_when_enumerating_release_assets() {
214 let dir = tempfile::tempdir().unwrap();
215 let identity = VendorReleaseIdentity {
216 version: "0.12.3".into(),
217 revision: "0123456789abcdef0123456789abcdef01234567".into(),
218 };
219 let archive = dir.path().join(identity.archive_name());
220
221 fs_err::write(
222 dir.path().join(IDENTITY_FILE),
223 serde_json::to_vec(&identity).unwrap(),
224 )
225 .unwrap();
226 fs_err::write(&archive, b"archive").unwrap();
227
228 assert_eq!(
229 vendor_release_files(dir.path()).unwrap(),
230 vec![(archive, None)]
231 );
232 }
233}