373 lines · 12.7 KB · Rust
RawDownload
use gh_workflow::{    Event, Expression, Input, Job, Level, Permissions, Push, Run, Step, Use, UsesJob, Workflow,    WorkflowCall, WorkflowCallSecret, WorkflowDispatch,}; use crate::tasks::workflows::{    runners,    steps::{        self, CommonJobConditions, CommonPermissionSets, FluentBuilder as _, NamedJob,        UploadArtifactStep, named, release_job,    },    vars::{self, StepOutput, WorkflowInput},}; const BUILD_OUTPUT_DIR: &str = "target/deploy"; pub(crate) enum DocsChannel {    Nightly,    Preview,    Stable,} impl DocsChannel {    pub(crate) fn site_url(&self) -> &'static str {        match self {            Self::Nightly => "/docs/nightly/",            Self::Preview => "/docs/preview/",            Self::Stable => "/docs/",        }    }     pub(crate) fn project_name(&self) -> &'static str {        match self {            Self::Nightly => "docs-nightly",            Self::Preview => "docs-preview",            Self::Stable => "docs",        }    }     pub(crate) fn channel_name(&self) -> &'static str {        match self {            Self::Nightly => "nightly",            Self::Preview => "preview",            Self::Stable => "stable",        }    }} pub(crate) fn lychee_link_check(dir: &str) -> Step<Use> {    named::uses(        "lycheeverse",        "lychee-action",        "82202e5e9c2f4ef1a55a3d02563e1cb6041e5332",    ) // v2.4.1    .add_with(("args", format!("--no-progress --exclude '^http' '{dir}'")))    .add_with(("fail", true))    .add_with(("jobSummary", false))} pub(crate) fn install_mdbook() -> Step<Use> {    named::uses(        "peaceiris",        "actions-mdbook",        "ee69d230fe19748b7abf22df32acaa93833fad08", // v2    )    .with(("mdbook-version", "0.4.37"))} pub(crate) fn build_docs_book(docs_channel: String, site_url: String) -> Step<Run> {    named::bash(indoc::formatdoc! {r#"        mkdir -p {BUILD_OUTPUT_DIR}        mdbook build ./docs --dest-dir=../{BUILD_OUTPUT_DIR}/docs/    "#})    .add_env(("DOCS_CHANNEL", docs_channel))    .add_env(("MDBOOK_BOOK__SITE_URL", site_url))} fn docs_build_steps(    job: Job,    checkout_ref: Option<String>,    docs_channel: impl Into<String>,    site_url: impl Into<String>,) -> Job {    let docs_channel = docs_channel.into();    let site_url = site_url.into();     steps::use_clang(        job.add_env(("DOCS_AMPLITUDE_API_KEY", vars::DOCS_AMPLITUDE_API_KEY))            .add_env(("DOCS_CONSENT_IO_INSTANCE", vars::DOCS_CONSENT_IO_INSTANCE))            .add_step(                steps::checkout_repo().when_some(checkout_ref, |step, checkout_ref| {                    step.with_ref(checkout_ref)                }),            )            .runs_on(runners::LINUX_XL)            .add_step(steps::setup_cargo_config(runners::Platform::Linux))            .add_step(steps::cache_rust_dependencies_namespace())            .map(steps::install_linux_dependencies)            .add_step(steps::script("./script/generate-action-metadata"))            .add_step(lychee_link_check("./docs/src/**/*"))            .add_step(install_mdbook())            .add_step(build_docs_book(docs_channel, site_url))            .add_step(lychee_link_check(&format!("{BUILD_OUTPUT_DIR}/docs"))),    )} fn docs_deploy_steps(job: Job, project_name: &StepOutput) -> Job {    fn deploy_to_cf_pages(project_name: &StepOutput) -> Step<Use> {        named::uses(            "cloudflare",            "wrangler-action",            "da0e0dfe58b7a431659754fdf3f186c529afbe65",        ) // v3        .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))        .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))        .add_with((            "command",            format!(                "pages deploy {BUILD_OUTPUT_DIR} --project-name=${{{{ {} }}}} --branch main",                project_name.expr()            ),        ))    }     fn upload_install_script() -> Step<Use> {        named::uses(            "cloudflare",            "wrangler-action",            "da0e0dfe58b7a431659754fdf3f186c529afbe65",        ) // v3        .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))        .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))        .add_with((            "command",            "r2 object put -f script/install.sh zed-open-source-website-assets/install.sh",        ))    }     fn deploy_docs_worker() -> Step<Use> {        named::uses(            "cloudflare",            "wrangler-action",            "da0e0dfe58b7a431659754fdf3f186c529afbe65",        ) // v3        .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN))        .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID))        .add_with(("command", "deploy .cloudflare/docs-proxy/src/worker.js"))    }     fn upload_wrangler_logs() -> UploadArtifactStep {        steps::upload_artifact("wrangler_logs", "/home/runner/.config/.wrangler/logs/")            .if_condition(Expression::new("always()"))    }     job.add_step(deploy_to_cf_pages(project_name))        .add_step(upload_install_script())        .add_step(deploy_docs_worker())        .add_step(upload_wrangler_logs())} pub(crate) fn check_docs() -> NamedJob {    NamedJob {        name: "check_docs".to_owned(),        job: docs_build_steps(            release_job(&[]).add_step(steps::harden_runner()),            None,            DocsChannel::Stable.channel_name(),            DocsChannel::Stable.site_url(),        ),    }} fn resolve_channel_step(    channel_expr: impl Into<String>,) -> (Step<Run>, StepOutput, StepOutput, StepOutput) {    let step = Step::new("deploy_docs::resolve_channel_step").run(format!(        indoc::indoc! {r#"            if [ -z "$CHANNEL" ]; then                if [ "$GITHUB_REF" = "refs/heads/main" ]; then                    CHANNEL="nightly"                else                    echo "::error::channel input is required when ref is not main."                    exit 1                fi            fi             case "$CHANNEL" in                "nightly")                    SITE_URL="{nightly_site_url}"                    PROJECT_NAME="{nightly_project_name}"                    ;;                "preview")                    SITE_URL="{preview_site_url}"                    PROJECT_NAME="{preview_project_name}"                    ;;                "stable")                    SITE_URL="{stable_site_url}"                    PROJECT_NAME="{stable_project_name}"                    ;;                *)                    echo "::error::Invalid docs channel '$CHANNEL'. Expected one of: nightly, preview, stable."                    exit 1                    ;;            esac             {{                echo "channel=$CHANNEL"                echo "site_url=$SITE_URL"                echo "project_name=$PROJECT_NAME"            }} >> "$GITHUB_OUTPUT"        "#},        nightly_site_url = DocsChannel::Nightly.site_url(),        preview_site_url = DocsChannel::Preview.site_url(),        stable_site_url = DocsChannel::Stable.site_url(),        nightly_project_name = DocsChannel::Nightly.project_name(),        preview_project_name = DocsChannel::Preview.project_name(),        stable_project_name = DocsChannel::Stable.project_name(),    ))    .id("resolve-channel")    .add_env(("CHANNEL", channel_expr.into()));     let channel = StepOutput::new(&step, "channel");    let site_url = StepOutput::new(&step, "site_url");    let project_name = StepOutput::new(&step, "project_name");    (step, channel, site_url, project_name)} fn docs_job(channel_expr: impl Into<String>, checkout_ref: Option<String>) -> NamedJob {    let (resolve_step, channel, site_url, project_name) = resolve_channel_step(channel_expr);     NamedJob {        name: "deploy_docs".to_owned(),        job: docs_deploy_steps(            docs_build_steps(                release_job(&[])                    .cond(Expression::new(                        "github.repository_owner == 'zed-industries'",                    ))                    .name("Build and Deploy Docs")                    .add_step(resolve_step),                checkout_ref,                channel.to_string(),                site_url.to_string(),            ),            &project_name,        ),    }} pub(crate) fn deploy_docs_workflow_call(    channel: impl Into<String>,    checkout_ref: impl Into<String>,) -> NamedJob<UsesJob> {    let job = Job::default()        .with_repository_owner_guard()        .permissions(Permissions::default().contents(Level::Read))        .uses(            "zed-industries",            "zed",            ".github/workflows/deploy_docs.yml",            // Pinned to a commit rather than the mutable `main` ref (supply-chain hardening).            // Same-repo reusable workflow; bump via Dependabot or alongside deploy_docs.yml changes.            "3f16f7b9082f8828e4d6ae207d2349b1ef932517",        )        .with(            Input::default()                .add("channel", channel.into())                .add("checkout_ref", checkout_ref.into()),        )        .secrets(indexmap::IndexMap::from([            (                "DOCS_AMPLITUDE_API_KEY".to_owned(),                vars::DOCS_AMPLITUDE_API_KEY.to_owned(),            ),            (                "DOCS_CONSENT_IO_INSTANCE".to_owned(),                vars::DOCS_CONSENT_IO_INSTANCE.to_owned(),            ),            (                "CLOUDFLARE_API_TOKEN".to_owned(),                vars::CLOUDFLARE_API_TOKEN.to_owned(),            ),            (                "CLOUDFLARE_ACCOUNT_ID".to_owned(),                vars::CLOUDFLARE_ACCOUNT_ID.to_owned(),            ),        ]));     NamedJob {        name: "deploy_docs".to_owned(),        job,    }} pub(crate) fn deploy_docs_job(    channel_input: &WorkflowInput,    checkout_ref_input: &WorkflowInput,) -> NamedJob {    docs_job(        channel_input.to_string(),        Some(format!(            "${{{{ {} != '' && {} || github.sha }}}}",            checkout_ref_input.expr(),            checkout_ref_input.expr()        )),    )} pub(crate) fn deploy_docs() -> Workflow {    let channel = WorkflowInput::string("channel", Some(String::new()))        .description("Docs channel to deploy: nightly, preview, or stable");    let checkout_ref = WorkflowInput::string("checkout_ref", Some(String::new()))        .description("Git ref to checkout and deploy. Defaults to event SHA when omitted.");    let deploy_docs = deploy_docs_job(&channel, &checkout_ref);     named::workflow()        .with_minimal_permissions()        .add_event(            Event::default().workflow_dispatch(                WorkflowDispatch::default()                    .add_input(channel.name, channel.input())                    .add_input(checkout_ref.name, checkout_ref.input()),            ),        )        .add_event(            Event::default().workflow_call(                WorkflowCall::default()                    .add_input(channel.name, channel.call_input())                    .add_input(checkout_ref.name, checkout_ref.call_input())                    .secrets([                        (                            "DOCS_AMPLITUDE_API_KEY".to_owned(),                            WorkflowCallSecret {                                description: "DOCS_AMPLITUDE_API_KEY".to_owned(),                                required: true,                            },                        ),                        (                            "DOCS_CONSENT_IO_INSTANCE".to_owned(),                            WorkflowCallSecret {                                description: "DOCS_CONSENT_IO_INSTANCE".to_owned(),                                required: true,                            },                        ),                        (                            "CLOUDFLARE_API_TOKEN".to_owned(),                            WorkflowCallSecret {                                description: "CLOUDFLARE_API_TOKEN".to_owned(),                                required: true,                            },                        ),                        (                            "CLOUDFLARE_ACCOUNT_ID".to_owned(),                            WorkflowCallSecret {                                description: "CLOUDFLARE_ACCOUNT_ID".to_owned(),                                required: true,                            },                        ),                    ]),            ),        )        .add_job(deploy_docs.name, deploy_docs.job)} pub(crate) fn deploy_nightly_docs() -> Workflow {    let deploy_docs = deploy_docs_workflow_call("nightly", "${{ github.sha }}");     named::workflow()        .name("deploy_nightly_docs")        .permissions(Permissions::default())        .add_event(Event::default().push(Push::default().add_branch("main")))        .add_job(deploy_docs.name, deploy_docs.job)}