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