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