1use gh_workflow::*;
2
3use crate::tasks::workflows::{runners::Platform, vars, vars::StepOutput};
4
5const BASH_SHELL: &str = "bash -euxo pipefail {0}";
6// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell
7pub const PWSH_SHELL: &str = "pwsh";
8
9pub(crate) struct Nextest(Step<Run>);
10
11pub(crate) fn cargo_nextest(platform: Platform) -> Nextest {
12 Nextest(named::run(
13 platform,
14 "cargo nextest run --workspace --no-fail-fast",
15 ))
16}
17
18impl Nextest {
19 pub(crate) fn with_target(mut self, target: &str) -> Step<Run> {
20 if let Some(nextest_command) = self.0.value.run.as_mut() {
21 nextest_command.push_str(&format!(r#" --target "{target}""#));
22 }
23 self.into()
24 }
25
26 #[allow(dead_code)]
27 pub(crate) fn with_filter_expr(mut self, filter_expr: &str) -> Self {
28 if let Some(nextest_command) = self.0.value.run.as_mut() {
29 nextest_command.push_str(&format!(r#" -E "{filter_expr}""#));
30 }
31 self
32 }
33
34 pub(crate) fn with_changed_packages_filter(mut self, orchestrate_job: &str) -> Self {
35 if let Some(nextest_command) = self.0.value.run.as_mut() {
36 nextest_command.push_str(&format!(
37 r#"${{{{ needs.{orchestrate_job}.outputs.changed_packages && format(' -E "{{0}}"', needs.{orchestrate_job}.outputs.changed_packages) || '' }}}}"#
38 ));
39 }
40 self
41 }
42}
43
44impl From<Nextest> for Step<Run> {
45 fn from(value: Nextest) -> Self {
46 value.0
47 }
48}
49
50pub fn restore_mtime() -> Step<Use> {
51 named::uses(
52 "zed-industries",
53 "git-restore-mtime-action",
54 "72cefcf20b50bf4fcd74ea8a0d2927b0680e2114",
55 )
56}
57
58pub fn checkout_repo() -> Step<Use> {
59 named::uses(
60 "actions",
61 "checkout",
62 "11bd71901bbe5b1630ceea73d27597364c9af683", // v4
63 )
64 // prevent checkout action from running `git clean -ffdx` which
65 // would delete the target directory
66 .add_with(("clean", false))
67}
68
69pub fn checkout_repo_with_token(token: &StepOutput) -> Step<Use> {
70 named::uses(
71 "actions",
72 "checkout",
73 "11bd71901bbe5b1630ceea73d27597364c9af683", // v4
74 )
75 .add_with(("clean", false))
76 .add_with(("token", token.to_string()))
77}
78
79pub fn setup_pnpm() -> Step<Use> {
80 named::uses(
81 "pnpm",
82 "action-setup",
83 "fe02b34f77f8bc703788d5817da081398fad5dd2", // v4.0.0
84 )
85 .add_with(("version", "9"))
86}
87
88pub fn setup_node() -> Step<Use> {
89 named::uses(
90 "actions",
91 "setup-node",
92 "49933ea5288caeca8642d1e84afbd3f7d6820020", // v4
93 )
94 .add_with(("node-version", "20"))
95}
96
97pub fn setup_sentry() -> Step<Use> {
98 named::uses(
99 "matbour",
100 "setup-sentry-cli",
101 "3e938c54b3018bdd019973689ef984e033b0454b",
102 )
103 .add_with(("token", vars::SENTRY_AUTH_TOKEN))
104}
105
106pub fn prettier() -> Step<Run> {
107 named::bash("./script/prettier")
108}
109
110pub fn cargo_fmt() -> Step<Run> {
111 named::bash("cargo fmt --all -- --check")
112}
113
114pub fn cargo_install_nextest() -> Step<Use> {
115 named::uses("taiki-e", "install-action", "nextest")
116}
117
118pub fn setup_cargo_config(platform: Platform) -> Step<Run> {
119 match platform {
120 Platform::Windows => named::pwsh(indoc::indoc! {r#"
121 New-Item -ItemType Directory -Path "./../.cargo" -Force
122 Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
123 "#}),
124
125 Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#"
126 mkdir -p ./../.cargo
127 cp ./.cargo/ci-config.toml ./../.cargo/config.toml
128 "#}),
129 }
130}
131
132pub fn cleanup_cargo_config(platform: Platform) -> Step<Run> {
133 let step = match platform {
134 Platform::Windows => named::pwsh(indoc::indoc! {r#"
135 Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
136 "#}),
137 Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#"
138 rm -rf ./../.cargo
139 "#}),
140 };
141
142 step.if_condition(Expression::new("always()"))
143}
144
145pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
146 match platform {
147 Platform::Windows => named::pwsh("./script/clear-target-dir-if-larger-than.ps1 250"),
148 Platform::Linux => named::bash("./script/clear-target-dir-if-larger-than 250"),
149 Platform::Mac => named::bash("./script/clear-target-dir-if-larger-than 300"),
150 }
151}
152
153pub fn clippy(platform: Platform) -> Step<Run> {
154 match platform {
155 Platform::Windows => named::pwsh("./script/clippy.ps1"),
156 _ => named::bash("./script/clippy"),
157 }
158}
159
160pub fn cache_rust_dependencies_namespace() -> Step<Use> {
161 named::uses("namespacelabs", "nscloud-cache-action", "v1")
162 .add_with(("cache", "rust"))
163 .add_with(("path", "~/.rustup"))
164}
165
166pub fn cache_nix_dependencies_namespace() -> Step<Use> {
167 named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "nix"))
168}
169
170pub fn cache_nix_store_macos() -> Step<Use> {
171 // On macOS, `/nix` is on a read-only root filesystem so nscloud's `cache: nix`
172 // cannot mount or symlink there. Instead we cache a user-writable directory and
173 // use nix-store --import/--export in separate steps to transfer store paths.
174 named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("path", "~/nix-cache"))
175}
176
177pub fn setup_linux() -> Step<Run> {
178 named::bash("./script/linux")
179}
180
181fn install_mold() -> Step<Run> {
182 named::bash("./script/install-mold")
183}
184
185fn download_wasi_sdk() -> Step<Run> {
186 named::bash("./script/download-wasi-sdk")
187}
188
189pub(crate) fn install_linux_dependencies(job: Job) -> Job {
190 job.add_step(setup_linux())
191 .add_step(install_mold())
192 .add_step(download_wasi_sdk())
193}
194
195pub fn script(name: &str) -> Step<Run> {
196 if name.ends_with(".ps1") {
197 Step::new(name).run(name).shell(PWSH_SHELL)
198 } else {
199 Step::new(name).run(name)
200 }
201}
202
203pub struct NamedJob<J: JobType = RunJob> {
204 pub name: String,
205 pub job: Job<J>,
206}
207
208// impl NamedJob {
209// pub fn map(self, f: impl FnOnce(Job) -> Job) -> Self {
210// NamedJob {
211// name: self.name,
212// job: f(self.job),
213// }
214// }
215// }
216
217pub(crate) const DEFAULT_REPOSITORY_OWNER_GUARD: &str =
218 "(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')";
219
220pub fn repository_owner_guard_expression(trigger_always: bool) -> Expression {
221 Expression::new(format!(
222 "{}{}",
223 DEFAULT_REPOSITORY_OWNER_GUARD,
224 trigger_always.then_some(" && always()").unwrap_or_default()
225 ))
226}
227
228pub trait CommonJobConditions: Sized {
229 fn with_repository_owner_guard(self) -> Self;
230}
231
232impl CommonJobConditions for Job {
233 fn with_repository_owner_guard(self) -> Self {
234 self.cond(repository_owner_guard_expression(false))
235 }
236}
237
238pub(crate) fn release_job(deps: &[&NamedJob]) -> Job {
239 dependant_job(deps)
240 .with_repository_owner_guard()
241 .timeout_minutes(60u32)
242}
243
244pub(crate) fn dependant_job(deps: &[&NamedJob]) -> Job {
245 let job = Job::default();
246 if deps.len() > 0 {
247 job.needs(deps.iter().map(|j| j.name.clone()).collect::<Vec<_>>())
248 } else {
249 job
250 }
251}
252
253impl FluentBuilder for Job {}
254impl FluentBuilder for Workflow {}
255impl FluentBuilder for Input {}
256
257/// A helper trait for building complex objects with imperative conditionals in a fluent style.
258/// Copied from GPUI to avoid adding GPUI as dependency
259/// todo(ci) just put this in gh-workflow
260#[allow(unused)]
261pub trait FluentBuilder {
262 /// Imperatively modify self with the given closure.
263 fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
264 where
265 Self: Sized,
266 {
267 f(self)
268 }
269
270 /// Conditionally modify self with the given closure.
271 fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
272 where
273 Self: Sized,
274 {
275 self.map(|this| if condition { then(this) } else { this })
276 }
277
278 /// Conditionally modify self with the given closure.
279 fn when_else(
280 self,
281 condition: bool,
282 then: impl FnOnce(Self) -> Self,
283 else_fn: impl FnOnce(Self) -> Self,
284 ) -> Self
285 where
286 Self: Sized,
287 {
288 self.map(|this| if condition { then(this) } else { else_fn(this) })
289 }
290
291 /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
292 fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
293 where
294 Self: Sized,
295 {
296 self.map(|this| {
297 if let Some(value) = option {
298 then(this, value)
299 } else {
300 this
301 }
302 })
303 }
304 /// Conditionally unwrap and modify self with the given closure, if the given option is None.
305 fn when_none<T>(self, option: &Option<T>, then: impl FnOnce(Self) -> Self) -> Self
306 where
307 Self: Sized,
308 {
309 self.map(|this| if option.is_some() { this } else { then(this) })
310 }
311}
312
313// (janky) helper to generate steps with a name that corresponds
314// to the name of the calling function.
315pub mod named {
316 use super::*;
317
318 /// Returns a uses step with the same name as the enclosing function.
319 /// (You shouldn't inline this function into the workflow definition, you must
320 /// wrap it in a new function.)
321 pub fn uses(owner: &str, repo: &str, ref_: &str) -> Step<Use> {
322 Step::new(function_name(1)).uses(owner, repo, ref_)
323 }
324
325 /// Returns a bash-script step with the same name as the enclosing function.
326 /// (You shouldn't inline this function into the workflow definition, you must
327 /// wrap it in a new function.)
328 pub fn bash(script: impl AsRef<str>) -> Step<Run> {
329 Step::new(function_name(1)).run(script.as_ref())
330 }
331
332 /// Returns a pwsh-script step with the same name as the enclosing function.
333 /// (You shouldn't inline this function into the workflow definition, you must
334 /// wrap it in a new function.)
335 pub fn pwsh(script: &str) -> Step<Run> {
336 Step::new(function_name(1)).run(script).shell(PWSH_SHELL)
337 }
338
339 /// Runs the command in either powershell or bash, depending on platform.
340 /// (You shouldn't inline this function into the workflow definition, you must
341 /// wrap it in a new function.)
342 pub fn run(platform: Platform, script: &str) -> Step<Run> {
343 match platform {
344 Platform::Windows => Step::new(function_name(1)).run(script).shell(PWSH_SHELL),
345 Platform::Linux | Platform::Mac => Step::new(function_name(1)).run(script),
346 }
347 }
348
349 /// Returns a Workflow with the same name as the enclosing module with default
350 /// set for the running shell.
351 pub fn workflow() -> Workflow {
352 Workflow::default()
353 .name(
354 named::function_name(1)
355 .split("::")
356 .collect::<Vec<_>>()
357 .into_iter()
358 .rev()
359 .skip(1)
360 .rev()
361 .collect::<Vec<_>>()
362 .join("::"),
363 )
364 .defaults(Defaults::default().run(RunDefaults::default().shell(BASH_SHELL)))
365 }
366
367 /// Returns a Job with the same name as the enclosing function.
368 /// (note job names may not contain `::`)
369 pub fn job<J: JobType>(job: Job<J>) -> NamedJob<J> {
370 NamedJob {
371 name: function_name(1).split("::").last().unwrap().to_owned(),
372 job,
373 }
374 }
375
376 /// Returns the function name N callers above in the stack
377 /// (typically 1).
378 /// This only works because xtask always runs debug builds.
379 pub fn function_name(i: usize) -> String {
380 let mut name = "<unknown>".to_string();
381 let mut count = 0;
382 backtrace::trace(|frame| {
383 if count < i + 3 {
384 count += 1;
385 return true;
386 }
387 backtrace::resolve_frame(frame, |cb| {
388 if let Some(s) = cb.name() {
389 name = s.to_string()
390 }
391 });
392 false
393 });
394
395 name.split("::")
396 .skip_while(|s| s != &"workflows")
397 .skip(1)
398 .collect::<Vec<_>>()
399 .join("::")
400 }
401}
402
403pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
404 named::bash(&format!(
405 "git fetch origin {ref_name} && git checkout {ref_name}"
406 ))
407}
408
409pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
410 let step = named::uses(
411 "actions",
412 "create-github-app-token",
413 "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
414 )
415 .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
416 .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
417 .id("get-app-token");
418 let output = StepOutput::new(&step, "token");
419 (step, output)
420}