deploy_docs.rs

  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}