1use gh_workflow::*;
2
3use crate::tasks::workflows::{
4 release::{self, notify_on_failure},
5 runners,
6 steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named},
7 vars::{self, StepOutput, WorkflowInput},
8};
9
10const TAG_NAME_ENV: &str = "${{ github.event.release.tag_name || inputs.tag_name }}";
11const IS_PRERELEASE_ENV: &str = "${{ github.event.release.prerelease || inputs.prerelease }}";
12const TAG_NAME: &str = "${{ env.TAG_NAME }}";
13const RELEASE_BODY: &str = "${{ github.event.release.body || inputs.body }}";
14const DOCS_CHANNEL: &str = "${{ env.IS_PRERELEASE == 'true' && 'preview' || 'stable' }}";
15
16pub fn after_release() -> Workflow {
17 let tag_name = WorkflowInput::string("tag_name", None);
18 let prerelease = WorkflowInput::bool("prerelease", None);
19 let body = WorkflowInput::string("body", Some(String::new()));
20
21 let refresh_zed_dev = rebuild_releases_page();
22 let deploy_docs = deploy_docs();
23 let post_to_discord = post_to_discord(&[&refresh_zed_dev]);
24 let publish_winget = publish_winget();
25 let create_sentry_release = create_sentry_release();
26 let notify_on_failure = {
27 let notify_on_failure = notify_on_failure(&[
28 &refresh_zed_dev,
29 &post_to_discord,
30 &publish_winget,
31 &create_sentry_release,
32 ]);
33 NamedJob {
34 name: notify_on_failure.name,
35 job: notify_on_failure.job.add_need(deploy_docs.name.clone()),
36 }
37 };
38
39 named::workflow()
40 .add_env(("TAG_NAME", TAG_NAME_ENV))
41 .add_env(("IS_PRERELEASE", IS_PRERELEASE_ENV))
42 .on(Event::default()
43 .release(Release::default().types(vec![ReleaseType::Published]))
44 .workflow_dispatch(
45 WorkflowDispatch::default()
46 .add_input(tag_name.name, tag_name.input())
47 .add_input(prerelease.name, prerelease.input())
48 .add_input(body.name, body.input()),
49 ))
50 .add_job(refresh_zed_dev.name, refresh_zed_dev.job)
51 .add_job(deploy_docs.name, deploy_docs.job)
52 .add_job(post_to_discord.name, post_to_discord.job)
53 .add_job(publish_winget.name, publish_winget.job)
54 .add_job(create_sentry_release.name, create_sentry_release.job)
55 .add_job(notify_on_failure.name, notify_on_failure.job)
56}
57
58fn deploy_docs() -> NamedJob<UsesJob> {
59 let job = Job::default()
60 .cond(Expression::new(
61 "github.repository_owner == 'zed-industries'",
62 ))
63 .permissions(Permissions::default().contents(Level::Read))
64 .uses(
65 "zed-industries",
66 "zed",
67 ".github/workflows/deploy_docs.yml",
68 "main",
69 )
70 .with(
71 Input::default()
72 .add("channel", DOCS_CHANNEL)
73 .add("checkout_ref", TAG_NAME),
74 )
75 .secrets(indexmap::IndexMap::from([
76 (
77 "DOCS_AMPLITUDE_API_KEY".to_owned(),
78 vars::DOCS_AMPLITUDE_API_KEY.to_owned(),
79 ),
80 (
81 "CLOUDFLARE_API_TOKEN".to_owned(),
82 vars::CLOUDFLARE_API_TOKEN.to_owned(),
83 ),
84 (
85 "CLOUDFLARE_ACCOUNT_ID".to_owned(),
86 vars::CLOUDFLARE_ACCOUNT_ID.to_owned(),
87 ),
88 ]));
89
90 NamedJob {
91 name: "deploy_docs".to_owned(),
92 job,
93 }
94}
95
96fn rebuild_releases_page() -> NamedJob {
97 fn refresh_cloud_releases() -> Step<Run> {
98 named::bash("curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=$TAG_NAME")
99 }
100
101 fn redeploy_zed_dev() -> Step<Run> {
102 named::bash("./script/redeploy-vercel").add_env(("VERCEL_TOKEN", vars::VERCEL_TOKEN))
103 }
104
105 named::job(
106 Job::default()
107 .runs_on(runners::LINUX_SMALL)
108 .with_repository_owner_guard()
109 .add_step(refresh_cloud_releases())
110 .add_step(checkout_repo())
111 .add_step(redeploy_zed_dev()),
112 )
113}
114
115fn post_to_discord(deps: &[&NamedJob]) -> NamedJob {
116 fn get_release_url() -> Step<Run> {
117 named::bash(
118 r#"if [ "$IS_PRERELEASE" == "true" ]; then
119 URL="https://zed.dev/releases/preview"
120else
121 URL="https://zed.dev/releases/stable"
122fi
123
124echo "URL=$URL" >> "$GITHUB_OUTPUT"
125"#,
126 )
127 .id("get-release-url")
128 }
129
130 fn get_content() -> Step<Use> {
131 named::uses(
132 "2428392",
133 "gh-truncate-string-action",
134 "b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1
135 )
136 .id("get-content")
137 .add_with((
138 "stringToTruncate",
139 format!(
140 "📣 Zed [{TAG_NAME}](<${{{{ steps.get-release-url.outputs.URL }}}}>) was just released!\n\n{RELEASE_BODY}\n"
141 ),
142 ))
143 .add_with(("maxLength", 2000))
144 .add_with(("truncationSymbol", "..."))
145 }
146
147 fn discord_webhook_action() -> Step<Use> {
148 named::uses(
149 "tsickert",
150 "discord-webhook",
151 "c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0
152 )
153 .add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES))
154 .add_with(("content", "${{ steps.get-content.outputs.string }}"))
155 }
156 let job = dependant_job(deps)
157 .runs_on(runners::LINUX_SMALL)
158 .with_repository_owner_guard()
159 .add_step(get_release_url())
160 .add_step(get_content())
161 .add_step(discord_webhook_action());
162 named::job(job)
163}
164
165fn publish_winget() -> NamedJob {
166 fn sync_winget_pkgs_fork() -> Step<Run> {
167 named::pwsh(indoc::indoc! {r#"
168 $headers = @{
169 "Authorization" = "Bearer $env:WINGET_TOKEN"
170 "Accept" = "application/vnd.github+json"
171 "X-GitHub-Api-Version" = "2022-11-28"
172 }
173 $body = @{ branch = "master" } | ConvertTo-Json
174 $uri = "https://api.github.com/repos/$env:GITHUB_REPOSITORY_OWNER/winget-pkgs/merge-upstream"
175 try {
176 Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json"
177 Write-Host "Successfully synced winget-pkgs fork"
178 } catch {
179 Write-Host "Fork sync response: $_"
180 Write-Host "Continuing anyway - fork may already be up to date"
181 }
182 "#})
183 .add_env(("WINGET_TOKEN", vars::WINGET_TOKEN))
184 }
185
186 fn set_package_name() -> (Step<Run>, StepOutput) {
187 let script = r#"if ($env:IS_PRERELEASE -eq "true") {
188 $PACKAGE_NAME = "ZedIndustries.Zed.Preview"
189} else {
190 $PACKAGE_NAME = "ZedIndustries.Zed"
191}
192
193echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
194"#;
195 let step = named::pwsh(script).id("set-package-name");
196
197 let output = StepOutput::new(&step, "PACKAGE_NAME");
198 (step, output)
199 }
200
201 fn winget_releaser(package_name: &StepOutput) -> Step<Use> {
202 named::uses(
203 "vedantmgoyal9",
204 "winget-releaser",
205 "19e706d4c9121098010096f9c495a70a7518b30f", // v2
206 )
207 .add_with(("identifier", package_name.to_string()))
208 .add_with(("release-tag", TAG_NAME))
209 .add_with(("max-versions-to-keep", 5))
210 .add_with(("token", vars::WINGET_TOKEN))
211 }
212
213 let (set_package_name, package_name) = set_package_name();
214
215 named::job(
216 Job::default()
217 .runs_on(runners::WINDOWS_DEFAULT)
218 .add_step(sync_winget_pkgs_fork())
219 .add_step(set_package_name)
220 .add_step(winget_releaser(&package_name)),
221 )
222}
223
224fn create_sentry_release() -> NamedJob {
225 let job = Job::default()
226 .runs_on(runners::LINUX_SMALL)
227 .with_repository_owner_guard()
228 .add_step(checkout_repo())
229 .add_step(release::create_sentry_release());
230 named::job(job)
231}