deploy_docs.rs

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