1use gh_workflow::{Event, Expression, Push, Run, Step, Use, Workflow};
2use indoc::formatdoc;
3
4use crate::tasks::workflows::{
5 run_bundling::{bundle_linux, bundle_mac, bundle_windows},
6 run_tests,
7 runners::{self, Arch, Platform},
8 steps::{self, FluentBuilder, NamedJob, dependant_job, named, release_job},
9 vars::{self, StepOutput, assets},
10};
11
12const CURRENT_ACTION_RUN_URL: &str =
13 "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}";
14
15pub(crate) fn release() -> Workflow {
16 let macos_tests = run_tests::run_platform_tests(Platform::Mac);
17 let linux_tests = run_tests::run_platform_tests(Platform::Linux);
18 let windows_tests = run_tests::run_platform_tests(Platform::Windows);
19 let macos_clippy = run_tests::clippy(Platform::Mac);
20 let linux_clippy = run_tests::clippy(Platform::Linux);
21 let windows_clippy = run_tests::clippy(Platform::Windows);
22 let check_scripts = run_tests::check_scripts();
23
24 let create_draft_release = create_draft_release();
25
26 let bundle = ReleaseBundleJobs {
27 linux_aarch64: bundle_linux(
28 Arch::AARCH64,
29 None,
30 &[&linux_tests, &linux_clippy, &check_scripts],
31 ),
32 linux_x86_64: bundle_linux(
33 Arch::X86_64,
34 None,
35 &[&linux_tests, &linux_clippy, &check_scripts],
36 ),
37 mac_aarch64: bundle_mac(
38 Arch::AARCH64,
39 None,
40 &[&macos_tests, &macos_clippy, &check_scripts],
41 ),
42 mac_x86_64: bundle_mac(
43 Arch::X86_64,
44 None,
45 &[&macos_tests, &macos_clippy, &check_scripts],
46 ),
47 windows_aarch64: bundle_windows(
48 Arch::AARCH64,
49 None,
50 &[&windows_tests, &windows_clippy, &check_scripts],
51 ),
52 windows_x86_64: bundle_windows(
53 Arch::X86_64,
54 None,
55 &[&windows_tests, &windows_clippy, &check_scripts],
56 ),
57 };
58
59 let upload_release_assets = upload_release_assets(&[&create_draft_release], &bundle);
60 let validate_release_assets = validate_release_assets(&[&upload_release_assets]);
61
62 let auto_release_preview = auto_release_preview(&[&validate_release_assets]);
63 let push_slack_notification = push_release_update_notification(
64 &create_draft_release,
65 &upload_release_assets,
66 &validate_release_assets,
67 &auto_release_preview,
68 );
69
70 named::workflow()
71 .on(Event::default().push(Push::default().tags(vec!["v*".to_string()])))
72 .concurrency(vars::one_workflow_per_non_main_branch())
73 .add_env(("CARGO_TERM_COLOR", "always"))
74 .add_env(("RUST_BACKTRACE", "1"))
75 .add_job(macos_tests.name, macos_tests.job)
76 .add_job(linux_tests.name, linux_tests.job)
77 .add_job(windows_tests.name, windows_tests.job)
78 .add_job(macos_clippy.name, macos_clippy.job)
79 .add_job(linux_clippy.name, linux_clippy.job)
80 .add_job(windows_clippy.name, windows_clippy.job)
81 .add_job(check_scripts.name, check_scripts.job)
82 .add_job(create_draft_release.name, create_draft_release.job)
83 .map(|mut workflow| {
84 for job in bundle.into_jobs() {
85 workflow = workflow.add_job(job.name, job.job);
86 }
87 workflow
88 })
89 .add_job(upload_release_assets.name, upload_release_assets.job)
90 .add_job(validate_release_assets.name, validate_release_assets.job)
91 .add_job(auto_release_preview.name, auto_release_preview.job)
92 .add_job(push_slack_notification.name, push_slack_notification.job)
93}
94
95pub(crate) struct ReleaseBundleJobs {
96 pub linux_aarch64: NamedJob,
97 pub linux_x86_64: NamedJob,
98 pub mac_aarch64: NamedJob,
99 pub mac_x86_64: NamedJob,
100 pub windows_aarch64: NamedJob,
101 pub windows_x86_64: NamedJob,
102}
103
104impl ReleaseBundleJobs {
105 pub fn jobs(&self) -> Vec<&NamedJob> {
106 vec![
107 &self.linux_aarch64,
108 &self.linux_x86_64,
109 &self.mac_aarch64,
110 &self.mac_x86_64,
111 &self.windows_aarch64,
112 &self.windows_x86_64,
113 ]
114 }
115
116 pub fn into_jobs(self) -> Vec<NamedJob> {
117 vec![
118 self.linux_aarch64,
119 self.linux_x86_64,
120 self.mac_aarch64,
121 self.mac_x86_64,
122 self.windows_aarch64,
123 self.windows_x86_64,
124 ]
125 }
126}
127
128pub(crate) fn create_sentry_release() -> Step<Use> {
129 named::uses(
130 "getsentry",
131 "action-release",
132 "526942b68292201ac6bbb99b9a0747d4abee354c", // v3
133 )
134 .add_env(("SENTRY_ORG", "zed-dev"))
135 .add_env(("SENTRY_PROJECT", "zed"))
136 .add_env(("SENTRY_AUTH_TOKEN", vars::SENTRY_AUTH_TOKEN))
137 .add_with(("environment", "production"))
138}
139
140fn validate_release_assets(deps: &[&NamedJob]) -> NamedJob {
141 let expected_assets: Vec<String> = assets::all().iter().map(|a| format!("\"{a}\"")).collect();
142 let expected_assets_json = format!("[{}]", expected_assets.join(", "));
143
144 let validation_script = formatdoc! {r#"
145 EXPECTED_ASSETS='{expected_assets_json}'
146 TAG="$GITHUB_REF_NAME"
147
148 ACTUAL_ASSETS=$(gh release view "$TAG" --repo=zed-industries/zed --json assets -q '[.assets[].name]')
149
150 MISSING_ASSETS=$(echo "$EXPECTED_ASSETS" | jq -r --argjson actual "$ACTUAL_ASSETS" '. - $actual | .[]')
151
152 if [ -n "$MISSING_ASSETS" ]; then
153 echo "Error: The following assets are missing from the release:"
154 echo "$MISSING_ASSETS"
155 exit 1
156 fi
157
158 echo "All expected assets are present in the release."
159 "#,
160 };
161
162 named::job(
163 dependant_job(deps).runs_on(runners::LINUX_SMALL).add_step(
164 named::bash(&validation_script).add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
165 ),
166 )
167}
168
169fn auto_release_preview(deps: &[&NamedJob]) -> NamedJob {
170 let (authenticate, token) = steps::authenticate_as_zippy();
171
172 named::job(
173 dependant_job(deps)
174 .runs_on(runners::LINUX_SMALL)
175 .cond(Expression::new(indoc::indoc!(
176 r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"#
177 )))
178 .add_step(authenticate)
179 .add_step(
180 steps::script(
181 r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#,
182 )
183 .add_env(("GITHUB_TOKEN", &token)),
184 )
185 )
186}
187
188pub(crate) fn download_workflow_artifacts() -> Step<Use> {
189 named::uses(
190 "actions",
191 "download-artifact",
192 "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0
193 )
194 .add_with(("path", "./artifacts/"))
195}
196
197pub(crate) fn prep_release_artifacts() -> Step<Run> {
198 let mut script_lines = vec!["mkdir -p release-artifacts/\n".to_string()];
199 for asset in assets::all() {
200 let mv_command = format!("mv ./artifacts/{asset}/{asset} release-artifacts/{asset}");
201 script_lines.push(mv_command)
202 }
203
204 named::bash(&script_lines.join("\n"))
205}
206
207fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> NamedJob {
208 let mut deps = deps.to_vec();
209 deps.extend(bundle.jobs());
210
211 named::job(
212 dependant_job(&deps)
213 .runs_on(runners::LINUX_MEDIUM)
214 .add_step(download_workflow_artifacts())
215 .add_step(steps::script("ls -lR ./artifacts"))
216 .add_step(prep_release_artifacts())
217 .add_step(
218 steps::script("gh release upload \"$GITHUB_REF_NAME\" --repo=zed-industries/zed release-artifacts/*")
219 .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
220 ),
221 )
222}
223
224fn create_draft_release() -> NamedJob {
225 fn generate_release_notes() -> Step<Run> {
226 named::bash(
227 r#"node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md"#,
228 )
229 }
230
231 fn create_release() -> Step<Run> {
232 named::bash("script/create-draft-release target/release-notes.md")
233 .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
234 }
235
236 named::job(
237 release_job(&[])
238 .runs_on(runners::LINUX_SMALL)
239 // We need to fetch more than one commit so that `script/draft-release-notes`
240 // is able to diff between the current and previous tag.
241 //
242 // 25 was chosen arbitrarily.
243 .add_step(
244 steps::checkout_repo()
245 .add_with(("fetch-depth", 25))
246 .add_with(("clean", false))
247 .add_with(("ref", "${{ github.ref }}")),
248 )
249 .add_step(steps::script("script/determine-release-channel"))
250 .add_step(steps::script("mkdir -p target/"))
251 .add_step(generate_release_notes())
252 .add_step(create_release()),
253 )
254}
255
256pub(crate) fn push_release_update_notification(
257 create_draft_release_job: &NamedJob,
258 upload_assets_job: &NamedJob,
259 validate_assets_job: &NamedJob,
260 auto_release_preview: &NamedJob,
261) -> NamedJob {
262 let notification_script = formatdoc! {r#"
263 DRAFT_RESULT="${{{{ needs.{draft_job}.result }}}}"
264 UPLOAD_RESULT="${{{{ needs.{upload_job}.result }}}}"
265 VALIDATE_RESULT="${{{{ needs.{validate_job}.result }}}}"
266 AUTO_RELEASE_RESULT="${{{{ needs.{auto_release_job}.result }}}}"
267 TAG="$GITHUB_REF_NAME"
268 RUN_URL="{run_url}"
269
270 if [ "$DRAFT_RESULT" == "failure" ]; then
271 echo "❌ Draft release creation failed for $TAG: $RUN_URL"
272 else
273 RELEASE_URL=$(gh release view "$TAG" --repo=zed-industries/zed --json url -q '.url')
274 if [ "$UPLOAD_RESULT" == "failure" ]; then
275 echo "❌ Release asset upload failed for $TAG: $RELEASE_URL"
276 elif [ "$VALIDATE_RESULT" == "failure" ]; then
277 echo "❌ Release asset validation failed for $TAG (missing assets): $RUN_URL"
278 elif [ "$AUTO_RELEASE_RESULT" == "success" ]; then
279 echo "✅ Release $TAG was auto-released successfully: $RELEASE_URL"
280 elif [ "$AUTO_RELEASE_RESULT" == "failure" ]; then
281 echo "❌ Auto release failed for $TAG: $RUN_URL"
282 else
283 echo "👀 Release $TAG sitting freshly baked in the oven and waiting to be published: $RELEASE_URL"
284 fi
285 fi
286 "#,
287 draft_job = create_draft_release_job.name,
288 upload_job = upload_assets_job.name,
289 validate_job = validate_assets_job.name,
290 auto_release_job = auto_release_preview.name,
291 run_url = CURRENT_ACTION_RUN_URL,
292 };
293
294 let mut job = dependant_job(&[
295 create_draft_release_job,
296 upload_assets_job,
297 validate_assets_job,
298 auto_release_preview,
299 ])
300 .runs_on(runners::LINUX_SMALL)
301 .cond(Expression::new("always()"));
302
303 for step in notify_slack(MessageType::Evaluated(notification_script)) {
304 job = job.add_step(step);
305 }
306 named::job(job)
307}
308
309pub(crate) fn notify_on_failure(deps: &[&NamedJob]) -> NamedJob {
310 let failure_message = format!("❌ ${{{{ github.workflow }}}} failed: {CURRENT_ACTION_RUN_URL}");
311
312 let mut job = dependant_job(deps)
313 .runs_on(runners::LINUX_SMALL)
314 .cond(Expression::new("failure()"));
315
316 for step in notify_slack(MessageType::Static(failure_message)) {
317 job = job.add_step(step);
318 }
319 named::job(job)
320}
321
322pub(crate) enum MessageType {
323 Static(String),
324 Evaluated(String),
325}
326
327fn notify_slack(message: MessageType) -> Vec<Step<Run>> {
328 match message {
329 MessageType::Static(message) => vec![send_slack_message(message)],
330 MessageType::Evaluated(expression) => {
331 let (generate_step, generated_message) = generate_slack_message(expression);
332
333 vec![
334 generate_step,
335 send_slack_message(generated_message.to_string()),
336 ]
337 }
338 }
339}
340
341fn generate_slack_message(expression: String) -> (Step<Run>, StepOutput) {
342 let script = formatdoc! {r#"
343 MESSAGE=$({expression})
344 echo "message=$MESSAGE" >> "$GITHUB_OUTPUT"
345 "#
346 };
347 let generate_step = named::bash(&script).id("generate-webhook-message");
348
349 let output = StepOutput::new(&generate_step, "message");
350
351 (generate_step, output)
352}
353
354fn send_slack_message(message: String) -> Step<Run> {
355 let script = formatdoc! {r#"
356 curl -X POST -H 'Content-type: application/json'\
357 --data '{{"text":"{message}"}}' "$SLACK_WEBHOOK"
358 "#
359 };
360 named::bash(&script).add_env(("SLACK_WEBHOOK", vars::SLACK_WEBHOOK_WORKFLOW_FAILURES))
361}