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