Skip to main content

flowey_hvlite/pipelines/
build_docs.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! See [`BuildDocsCli`]
5
6use flowey::node::prelude::FlowPlatformLinuxDistro;
7use flowey::node::prelude::GhPermission;
8use flowey::node::prelude::GhPermissionValue;
9use flowey::node::prelude::ReadVar;
10use flowey::pipeline::prelude::*;
11use flowey_lib_common::git_checkout::RepoSource;
12use flowey_lib_hvlite::common::CommonTriple;
13
14#[derive(Copy, Clone, clap::ValueEnum)]
15enum PipelineConfig {
16    /// Run on all PRs targeting the OpenVMM `main` branch.
17    Pr,
18    /// Run on all commits that land in OpenVMM's `main` branch.
19    ///
20    /// The CI pipeline also publishes the guide to openvmm.dev.
21    Ci,
22}
23
24/// A pipeline defining documentation CI and PR jobs.
25#[derive(clap::Args)]
26pub struct BuildDocsCli {
27    #[clap(long)]
28    config: PipelineConfig,
29
30    #[clap(flatten)]
31    local_run_args: Option<crate::pipelines_shared::cfg_common_params::LocalRunArgs>,
32}
33
34impl IntoPipeline for BuildDocsCli {
35    fn into_pipeline(self, backend_hint: PipelineBackendHint) -> anyhow::Result<Pipeline> {
36        let Self {
37            config,
38            local_run_args,
39        } = self;
40
41        let mut pipeline = Pipeline::new();
42
43        // The docs pipeline should only run on the main branch, and only when
44        // the Guide directory or the docs workflow/pipeline definitions are modified.
45        {
46            let branches = vec!["main".into()];
47            match config {
48                PipelineConfig::Ci => {
49                    pipeline
50                        .gh_set_ci_triggers(GhCiTriggers {
51                            branches,
52                            ..Default::default()
53                        })
54                        .gh_set_name("OpenVMM Docs CI");
55                }
56                PipelineConfig::Pr => {
57                    pipeline
58                        .gh_set_pr_triggers(GhPrTriggers {
59                            branches,
60                            ..GhPrTriggers::new_draftable()
61                        })
62                        .gh_set_name("OpenVMM Docs PR");
63                }
64            }
65        }
66
67        let openvmm_repo_source = {
68            if matches!(backend_hint, PipelineBackendHint::Local) {
69                RepoSource::ExistingClone(ReadVar::from_static(crate::repo_root()))
70            } else if matches!(backend_hint, PipelineBackendHint::Github) {
71                RepoSource::GithubSelf
72            } else {
73                anyhow::bail!(
74                    "Unsupported backend: Docs Pipeline only supports Local and GitHub backends"
75                );
76            }
77        };
78
79        if let RepoSource::GithubSelf = &openvmm_repo_source {
80            pipeline.gh_set_flowey_bootstrap_template(
81                crate::pipelines_shared::gh_flowey_bootstrap_template::get_template(),
82            );
83        }
84
85        let cfg_common_params = crate::pipelines_shared::cfg_common_params::get_cfg_common_params(
86            &mut pipeline,
87            backend_hint,
88            local_run_args,
89        )?;
90
91        pipeline.inject_all_jobs_with(move |job| {
92            job.dep_on(&cfg_common_params)
93                .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init)
94                .dep_on(
95                    |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params {
96                        hvlite_repo_source: openvmm_repo_source.clone(),
97                    },
98                )
99                .gh_grant_permissions::<flowey_lib_common::git_checkout::Node>([(
100                    GhPermission::Contents,
101                    GhPermissionValue::Read,
102                )])
103                .gh_grant_permissions::<flowey_lib_common::gh_task_azure_login::Node>([(
104                    GhPermission::IdToken,
105                    GhPermissionValue::Write,
106                )])
107        });
108
109        // We need to maintain a list of all jobs, so we can hang the "all good"
110        // job off of them. This is requires because github status checks only allow
111        // specifying jobs, and not workflows.
112        // There's more info in the following discussion:
113        // <https://github.com/orgs/community/discussions/12395>
114        let mut all_jobs = Vec::new();
115
116        // emit mdbook guide build job
117        let (pub_guide, use_guide) = pipeline.new_typed_artifact("guide");
118        let job = pipeline
119            .new_job(
120                FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu),
121                FlowArch::X86_64,
122                "build mdbook guide",
123            )
124            .gh_set_pool(crate::pipelines_shared::gh_pools::linux_x64_gh())
125            .publish(pub_guide, |built_guide| {
126                flowey_lib_hvlite::build_guide::Request { built_guide }
127            })
128            .finish();
129
130        all_jobs.push(job);
131
132        // emit rustdoc jobs
133        let (pub_rustdoc_linux, use_rustdoc_linux) =
134            pipeline.new_typed_artifact("x64-linux-rustdoc");
135        let (pub_rustdoc_win, use_rustdoc_win) = pipeline.new_typed_artifact("x64-windows-rustdoc");
136        for (target, platform, pool, pub_rustdoc) in [
137            (
138                CommonTriple::X86_64_WINDOWS_MSVC,
139                FlowPlatform::Windows,
140                crate::pipelines_shared::gh_pools::windows_x64_gh(),
141                pub_rustdoc_win,
142            ),
143            (
144                CommonTriple::X86_64_LINUX_GNU,
145                FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu),
146                crate::pipelines_shared::gh_pools::linux_x64_gh(),
147                pub_rustdoc_linux,
148            ),
149        ] {
150            let job = pipeline
151                .new_job(
152                    platform,
153                    FlowArch::X86_64,
154                    format!("build and check docs [x64-{platform}]"),
155                )
156                .gh_set_pool(pool)
157                .publish(pub_rustdoc, |docs| {
158                    flowey_lib_hvlite::build_rustdoc::Request {
159                        target_triple: target.as_triple(),
160                        docs,
161                    }
162                })
163                .finish();
164
165            all_jobs.push(job);
166        }
167
168        // emit consolidated gh pages publish job
169        if matches!(config, PipelineConfig::Ci) {
170            let pub_artifact = if matches!(backend_hint, PipelineBackendHint::Local) {
171                let (publish, _use) = pipeline.new_typed_artifact("gh-pages");
172                Some(publish)
173            } else {
174                None
175            };
176
177            let job = pipeline
178                .new_job(FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu), FlowArch::X86_64, "publish openvmm.dev")
179                .gh_set_pool(crate::pipelines_shared::gh_pools::linux_x64_gh())
180                .gh_set_concurrency_group(GhConcurrencyGroup {
181                    name: "publish-openvmm.dev".into(),
182                    cancel_in_progress: false,
183                })
184                .dep_on(
185                    |ctx| flowey_lib_hvlite::_jobs::consolidate_and_publish_gh_pages::Params {
186                        rustdoc_linux: ctx.use_typed_artifact(&use_rustdoc_linux),
187                        rustdoc_windows: ctx.use_typed_artifact(&use_rustdoc_win),
188                        guide: ctx.use_typed_artifact(&use_guide),
189                        output: if let Some(pub_artifact) = pub_artifact {
190                            ctx.publish_typed_artifact(pub_artifact)
191                        } else {
192                            ctx.new_done_handle().discard_result()
193                        }
194                    },
195                )
196                .gh_grant_permissions::<flowey_lib_hvlite::_jobs::consolidate_and_publish_gh_pages::Node>([
197                    (GhPermission::IdToken, GhPermissionValue::Write),
198                    (GhPermission::Pages, GhPermissionValue::Write),
199                ])
200                .finish();
201
202            all_jobs.push(job);
203        }
204
205        if matches!(config, PipelineConfig::Pr) {
206            // Add a job that depends on all others as a workaround for
207            // https://github.com/orgs/community/discussions/12395.
208            //
209            // This workaround then itself requires _another_ workaround, requiring
210            // the use of `gh_dangerous_override_if`, and some additional custom job
211            // logic, to deal with https://github.com/actions/runner/issues/2566.
212            //
213            // TODO: Add a way for this job to skip flowey setup and become a true
214            // no-op.
215            let all_good_job = pipeline
216                .new_job(
217                    FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu),
218                    FlowArch::X86_64,
219                    "openvmm build docs gates",
220                )
221                .gh_set_pool(crate::pipelines_shared::gh_pools::linux_x64_gh())
222                // always run this job, regardless whether or not any previous jobs failed
223                .gh_dangerous_override_if("always() && github.event.pull_request.draft == false")
224                .gh_dangerous_global_env_var("ANY_JOBS_FAILED", "${{ contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }}")
225                .side_effect(|done| flowey_lib_hvlite::_jobs::all_good_job::Params {
226                    did_fail_env_var: "ANY_JOBS_FAILED".into(),
227                    done,
228                })
229                .finish();
230
231            for job in all_jobs.iter() {
232                pipeline.non_artifact_dep(&all_good_job, job);
233            }
234        }
235
236        Ok(pipeline)
237    }
238}