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}