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