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}