1use gh_workflow::{Event, Expression, Job, Push, Run, Step, Use, Workflow, WorkflowDispatch};
2
3use crate::tasks::workflows::{
4 runners,
5 steps::{self, FluentBuilder as _, NamedJob, named, release_job},
6 vars::{self, StepOutput, WorkflowInput},
7};
8
9pub(crate) enum DocsChannel {
10 Nightly,
11 Preview,
12 Stable,
13}
14
15impl DocsChannel {
16 pub(crate) fn site_url(&self) -> &'static str {
17 match self {
18 Self::Nightly => "/docs/nightly/",
19 Self::Preview => "/docs/preview/",
20 Self::Stable => "/docs/",
21 }
22 }
23
24 pub(crate) fn project_name(&self) -> &'static str {
25 match self {
26 Self::Nightly => "docs-nightly",
27 Self::Preview => "docs-preview",
28 Self::Stable => "docs",
29 }
30 }
31
32 pub(crate) fn channel_name(&self) -> &'static str {
33 match self {
34 Self::Nightly => "nightly",
35 Self::Preview => "preview",
36 Self::Stable => "stable",
37 }
38 }
39}
40
41pub(crate) fn lychee_link_check(dir: &str) -> Step<Use> {
42 named::uses(
43 "lycheeverse",
44 "lychee-action",
45 "82202e5e9c2f4ef1a55a3d02563e1cb6041e5332",
46 ) // v2.4.1
47 .add_with(("args", format!("--no-progress --exclude '^http' '{dir}'")))
48 .add_with(("fail", true))
49 .add_with(("jobSummary", false))
50}
51
52pub(crate) fn install_mdbook() -> Step<Use> {
53 named::uses(
54 "peaceiris",
55 "actions-mdbook",
56 "ee69d230fe19748b7abf22df32acaa93833fad08", // v2
57 )
58 .with(("mdbook-version", "0.4.37"))
59}
60
61pub(crate) fn build_docs_book() -> Step<Run> {
62 named::bash(indoc::indoc! {r#"
63 mkdir -p target/deploy
64 mdbook build ./docs --dest-dir=../target/deploy/docs/
65 "#})
66}
67
68fn pages_deploy_step(project_name: &StepOutput) -> Step<Use> {
69 named::uses(
70 "cloudflare",
71 "wrangler-action",
72 "da0e0dfe58b7a431659754fdf3f186c529afbe65",
73 ) // v3
74 .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))
75 .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))
76 .add_with((
77 "command",
78 format!(
79 "pages deploy target/deploy --project-name=${{{{ {} }}}}",
80 project_name.expr()
81 ),
82 ))
83}
84
85fn deploy_install_script() -> Step<Use> {
86 named::uses(
87 "cloudflare",
88 "wrangler-action",
89 "da0e0dfe58b7a431659754fdf3f186c529afbe65",
90 ) // v3
91 .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))
92 .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))
93 .add_with((
94 "command",
95 "r2 object put -f script/install.sh zed-open-source-website-assets/install.sh",
96 ))
97}
98
99fn deploy_docs_worker() -> Step<Use> {
100 named::uses(
101 "cloudflare",
102 "wrangler-action",
103 "da0e0dfe58b7a431659754fdf3f186c529afbe65",
104 ) // v3
105 .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))
106 .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))
107 .add_with(("command", "deploy .cloudflare/docs-proxy/src/worker.js"))
108}
109
110fn upload_wrangler_logs() -> Step<Use> {
111 named::uses(
112 "actions",
113 "upload-artifact",
114 "ea165f8d65b6e75b540449e92b4886f43607fa02",
115 ) // v4
116 .if_condition(Expression::new("always()"))
117 .add_with(("name", "wrangler_logs"))
118 .add_with(("path", "/home/runner/.config/.wrangler/logs/"))
119}
120
121fn resolve_channel_step(
122 channel_input: &WorkflowInput,
123) -> (Step<Run>, StepOutput, StepOutput, StepOutput) {
124 let step = named::bash(format!(
125 indoc::indoc! {r#"
126 if [ "${{{{ github.event_name }}}}" = "workflow_dispatch" ]; then
127 CHANNEL="${{{{ {dispatch_channel} }}}}"
128 else
129 case "${{{{ github.ref }}}}" in
130 "refs/heads/main")
131 CHANNEL="nightly"
132 ;;
133 "refs/heads/preview")
134 CHANNEL="preview"
135 ;;
136 "refs/heads/stable")
137 CHANNEL="stable"
138 ;;
139 *)
140 echo "::error::Unsupported ref for docs deploy: ${{{{ github.ref }}}}"
141 exit 1
142 ;;
143 esac
144 fi
145
146 case "$CHANNEL" in
147 "nightly")
148 SITE_URL="{nightly_site_url}"
149 PROJECT_NAME="{nightly_project_name}"
150 ;;
151 "preview")
152 SITE_URL="{preview_site_url}"
153 PROJECT_NAME="{preview_project_name}"
154 ;;
155 "stable")
156 SITE_URL="{stable_site_url}"
157 PROJECT_NAME="{stable_project_name}"
158 ;;
159 *)
160 echo "::error::Invalid docs channel '$CHANNEL'. Expected one of: nightly, preview, stable."
161 exit 1
162 ;;
163 esac
164
165 echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
166 echo "site_url=$SITE_URL" >> "$GITHUB_OUTPUT"
167 echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
168 "#},
169 dispatch_channel = channel_input.expr(),
170 nightly_site_url = DocsChannel::Nightly.site_url(),
171 preview_site_url = DocsChannel::Preview.site_url(),
172 stable_site_url = DocsChannel::Stable.site_url(),
173 nightly_project_name = DocsChannel::Nightly.project_name(),
174 preview_project_name = DocsChannel::Preview.project_name(),
175 stable_project_name = DocsChannel::Stable.project_name(),
176 ))
177 .id("resolve-channel");
178
179 let channel = StepOutput::new(&step, "channel");
180 let site_url = StepOutput::new(&step, "site_url");
181 let project_name = StepOutput::new(&step, "project_name");
182 (step, channel, site_url, project_name)
183}
184
185fn static_channel_resolution_step(
186 channel: DocsChannel,
187) -> (Step<Run>, StepOutput, StepOutput, StepOutput) {
188 let (channel_name, site_url, project_name) = match channel {
189 DocsChannel::Nightly => (
190 DocsChannel::Nightly.channel_name(),
191 DocsChannel::Nightly.site_url(),
192 DocsChannel::Nightly.project_name(),
193 ),
194 DocsChannel::Preview => (
195 DocsChannel::Preview.channel_name(),
196 DocsChannel::Preview.site_url(),
197 DocsChannel::Preview.project_name(),
198 ),
199 DocsChannel::Stable => (
200 DocsChannel::Stable.channel_name(),
201 DocsChannel::Stable.site_url(),
202 DocsChannel::Stable.project_name(),
203 ),
204 };
205
206 let step = named::bash(format!(
207 indoc::indoc! {r#"
208 echo "channel={channel_name}" >> "$GITHUB_OUTPUT"
209 echo "site_url={site_url}" >> "$GITHUB_OUTPUT"
210 echo "project_name={project_name}" >> "$GITHUB_OUTPUT"
211 "#},
212 channel_name = channel_name,
213 site_url = site_url,
214 project_name = project_name,
215 ))
216 .id("resolve-channel");
217
218 let channel = StepOutput::new(&step, "channel");
219 let site_url = StepOutput::new(&step, "site_url");
220 let project_name = StepOutput::new(&step, "project_name");
221 (step, channel, site_url, project_name)
222}
223
224fn docs_build_steps(
225 job: Job,
226 resolved_channel_step: Step<Run>,
227 channel: &StepOutput,
228 site_url: &StepOutput,
229 project_name: &StepOutput,
230 include_deploy_steps: bool,
231) -> Job {
232 let mut job = job
233 .add_env(("DOCS_AMPLITUDE_API_KEY", vars::DOCS_AMPLITUDE_API_KEY))
234 .add_step(steps::checkout_repo())
235 .add_step(resolved_channel_step)
236 .add_env(("MDBOOK_BOOK__SITE_URL", site_url.to_string()))
237 .add_env(("DOCS_CHANNEL", channel.to_string()))
238 .runs_on(runners::LINUX_XL)
239 .add_step(steps::setup_cargo_config(runners::Platform::Linux))
240 .add_step(steps::cache_rust_dependencies_namespace())
241 .map(steps::install_linux_dependencies)
242 .add_step(steps::script("./script/generate-action-metadata"))
243 .add_step(lychee_link_check("./docs/src/**/*"))
244 .add_step(install_mdbook())
245 .add_step(build_docs_book())
246 .add_step(lychee_link_check("target/deploy/docs"));
247
248 if include_deploy_steps {
249 job = job
250 .add_step(pages_deploy_step(project_name))
251 .add_step(deploy_install_script())
252 .add_step(deploy_docs_worker())
253 .add_step(upload_wrangler_logs());
254 }
255
256 job
257}
258
259pub(crate) fn check_docs() -> NamedJob {
260 let (resolve_step, channel, site_url, project_name) =
261 static_channel_resolution_step(DocsChannel::Stable);
262
263 NamedJob {
264 name: "check_docs".to_owned(),
265 job: docs_build_steps(
266 release_job(&[]),
267 resolve_step,
268 &channel,
269 &site_url,
270 &project_name,
271 false,
272 ),
273 }
274}
275
276pub(crate) fn deploy_docs_job(channel_input: &WorkflowInput) -> NamedJob {
277 let (resolve_step, channel, site_url, project_name) = resolve_channel_step(channel_input);
278
279 NamedJob {
280 name: "deploy_docs".to_owned(),
281 job: docs_build_steps(
282 release_job(&[])
283 .name("Build and Deploy Docs")
284 .cond(Expression::new(
285 "github.repository_owner == 'zed-industries'",
286 )),
287 resolve_step,
288 &channel,
289 &site_url,
290 &project_name,
291 true,
292 ),
293 }
294}
295
296pub(crate) fn deploy_docs() -> Workflow {
297 let channel = WorkflowInput::string("channel", Some("nightly".to_string()))
298 .description("Docs channel to deploy: nightly, preview, or stable");
299
300 let deploy_docs = deploy_docs_job(&channel);
301
302 named::workflow()
303 .on(Event::default()
304 .push(
305 Push::default()
306 .add_branch("main")
307 .add_branch("preview")
308 .add_branch("stable"),
309 )
310 .workflow_dispatch(
311 WorkflowDispatch::default().add_input(channel.name, channel.input()),
312 ))
313 .add_job(deploy_docs.name, deploy_docs.job)
314}