workflows.rs

  1use anyhow::{Context, Result};
  2use clap::Parser;
  3use gh_workflow::Workflow;
  4use std::fs;
  5use std::path::{Path, PathBuf};
  6
  7use crate::tasks::workflow_checks::{self};
  8
  9mod after_release;
 10mod autofix_pr;
 11mod bump_patch_version;
 12mod cherry_pick;
 13mod compare_perf;
 14mod compliance_check;
 15mod danger;
 16mod deploy_collab;
 17mod deploy_docs;
 18mod deploy_docs_nightly_pr;
 19mod extension_auto_bump;
 20mod extension_bump;
 21mod extension_tests;
 22mod extension_workflow_rollout;
 23mod extensions;
 24mod nix_build;
 25mod publish_extension_cli;
 26mod release_nightly;
 27mod run_bundling;
 28
 29mod release;
 30mod run_agent_evals;
 31mod run_tests;
 32mod runners;
 33mod steps;
 34mod vars;
 35
 36#[derive(Clone)]
 37pub(crate) struct GitSha(String);
 38
 39impl AsRef<str> for GitSha {
 40    fn as_ref(&self) -> &str {
 41        &self.0
 42    }
 43}
 44
 45#[allow(
 46    clippy::disallowed_methods,
 47    reason = "This runs only in a CLI environment"
 48)]
 49fn parse_ref(value: &str) -> Result<GitSha, String> {
 50    const GIT_SHA_LENGTH: usize = 40;
 51    (value.len() == GIT_SHA_LENGTH)
 52        .then_some(value)
 53        .ok_or_else(|| {
 54            format!(
 55                "Git SHA has wrong length! \
 56                Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.",
 57                len = value.len()
 58            )
 59        })
 60        .and_then(|value| {
 61            let mut tmp = [0; 4];
 62            value
 63                .chars()
 64                .all(|char| u16::from_str_radix(char.encode_utf8(&mut tmp), 16).is_ok()).then_some(value)
 65                .ok_or_else(|| "Not a valid Git SHA".to_owned())
 66        })
 67        .and_then(|sha| {
 68           std::process::Command::new("git")
 69               .args([
 70                   "rev-parse",
 71                   "--quiet",
 72                   "--verify",
 73                   &format!("{sha}^{{commit}}")
 74               ])
 75               .output()
 76               .map_err(|_| "Failed to spawn Git command to verify SHA".to_owned())
 77               .and_then(|output|
 78                   output
 79                       .status.success()
 80                       .then_some(sha)
 81                       .ok_or_else(|| format!("SHA {sha} is not a valid Git SHA within this repository!")))
 82        }).map(|sha| GitSha(sha.to_owned()))
 83}
 84
 85#[derive(Parser)]
 86pub(crate) struct GenerateWorkflowArgs {
 87    #[arg(value_parser = parse_ref)]
 88    /// The Git SHA to use when invoking this
 89    pub(crate) sha: Option<GitSha>,
 90}
 91
 92enum WorkflowSource {
 93    Contextless(fn() -> Workflow),
 94    WithContext(fn(&GenerateWorkflowArgs) -> Workflow),
 95}
 96
 97struct WorkflowFile {
 98    source: WorkflowSource,
 99    r#type: WorkflowType,
100}
101
102impl WorkflowFile {
103    fn zed(f: fn() -> Workflow) -> WorkflowFile {
104        WorkflowFile {
105            source: WorkflowSource::Contextless(f),
106            r#type: WorkflowType::Zed,
107        }
108    }
109
110    fn extension(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile {
111        WorkflowFile {
112            source: WorkflowSource::WithContext(f),
113            r#type: WorkflowType::ExtensionCi,
114        }
115    }
116
117    fn extension_shared(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile {
118        WorkflowFile {
119            source: WorkflowSource::WithContext(f),
120            r#type: WorkflowType::ExtensionsShared,
121        }
122    }
123
124    fn generate_file(&self, workflow_args: &GenerateWorkflowArgs) -> Result<()> {
125        let workflow = match &self.source {
126            WorkflowSource::Contextless(f) => f(),
127            WorkflowSource::WithContext(f) => f(workflow_args),
128        };
129        let workflow_folder = self.r#type.folder_path();
130
131        fs::create_dir_all(&workflow_folder).with_context(|| {
132            format!("Failed to create directory: {}", workflow_folder.display())
133        })?;
134
135        let workflow_name = workflow
136            .name
137            .as_ref()
138            .expect("Workflow must have a name at this point");
139        let filename = format!(
140            "{}.yml",
141            workflow_name.rsplit("::").next().unwrap_or(workflow_name)
142        );
143
144        let workflow_path = workflow_folder.join(filename);
145
146        let content = workflow
147            .to_string()
148            .map_err(|e| anyhow::anyhow!("{:?}: {:?}", workflow_path, e))?;
149
150        let disclaimer = self.r#type.disclaimer(workflow_name);
151
152        let content = [disclaimer, content].join("\n");
153        fs::write(&workflow_path, content).map_err(Into::into)
154    }
155}
156
157#[derive(PartialEq, Eq, strum::EnumIter)]
158pub enum WorkflowType {
159    /// Workflows living in the Zed repository
160    Zed,
161    /// Workflows living in the `zed-extensions/workflows` repository that are
162    /// required workflows for PRs to the extension organization
163    ExtensionCi,
164    /// Workflows living in each of the extensions to perform checks and version
165    /// bumps until a better, more centralized system for that is in place.
166    ExtensionsShared,
167}
168
169impl WorkflowType {
170    fn disclaimer(&self, workflow_name: &str) -> String {
171        format!(
172            concat!(
173                "# Generated from xtask::workflows::{}{}\n",
174                "# Rebuild with `cargo xtask workflows`.",
175            ),
176            workflow_name,
177            (*self != WorkflowType::Zed)
178                .then_some(" within the Zed repository.")
179                .unwrap_or_default(),
180        )
181    }
182
183    pub fn folder_path(&self) -> PathBuf {
184        match self {
185            WorkflowType::Zed => PathBuf::from(".github/workflows"),
186            WorkflowType::ExtensionCi => PathBuf::from("extensions/workflows"),
187            WorkflowType::ExtensionsShared => PathBuf::from("extensions/workflows/shared"),
188        }
189    }
190}
191
192pub fn run_workflows(args: GenerateWorkflowArgs) -> Result<()> {
193    if !Path::new("crates/zed/").is_dir() {
194        anyhow::bail!("xtask workflows must be ran from the project root");
195    }
196
197    let workflows = [
198        WorkflowFile::zed(after_release::after_release),
199        WorkflowFile::zed(autofix_pr::autofix_pr),
200        WorkflowFile::zed(bump_patch_version::bump_patch_version),
201        WorkflowFile::zed(cherry_pick::cherry_pick),
202        WorkflowFile::zed(compare_perf::compare_perf),
203        WorkflowFile::zed(compliance_check::compliance_check),
204        WorkflowFile::zed(danger::danger),
205        WorkflowFile::zed(deploy_collab::deploy_collab),
206        WorkflowFile::zed(deploy_docs::deploy_docs),
207        WorkflowFile::zed(deploy_docs_nightly_pr::deploy_docs_nightly_pr),
208        WorkflowFile::zed(extension_bump::extension_bump),
209        WorkflowFile::zed(extension_auto_bump::extension_auto_bump),
210        WorkflowFile::zed(extension_tests::extension_tests),
211        WorkflowFile::zed(extension_workflow_rollout::extension_workflow_rollout),
212        WorkflowFile::zed(publish_extension_cli::publish_extension_cli),
213        WorkflowFile::zed(release::release),
214        WorkflowFile::zed(release_nightly::release_nightly),
215        WorkflowFile::zed(run_agent_evals::run_cron_unit_evals),
216        WorkflowFile::zed(run_agent_evals::run_unit_evals),
217        WorkflowFile::zed(run_bundling::run_bundling),
218        WorkflowFile::zed(run_tests::run_tests),
219        /* workflows used for CI/CD in extension repositories */
220        WorkflowFile::extension(extensions::run_tests::run_tests),
221        WorkflowFile::extension_shared(extensions::bump_version::bump_version),
222    ];
223
224    for workflow_file in workflows {
225        workflow_file.generate_file(&args)?;
226    }
227
228    workflow_checks::validate(Default::default())
229}