From d4e8cf60ced673741ff105b48bd7e2babac1221e Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 25 Feb 2026 17:58:25 -0500 Subject: [PATCH] Step 2: Split Deployments into Nightly, Preview, and Stable --- .cloudflare/docs-proxy/src/worker.js | 11 +- .github/workflows/deploy_docs.yml | 62 ++++- .github/workflows/run_tests.yml | 8 + crates/docs_preprocessor/src/main.rs | 7 + docs/theme/index.hbs | 1 + .../xtask/src/tasks/workflows/deploy_docs.rs | 235 ++++++++++++++++-- 6 files changed, 292 insertions(+), 32 deletions(-) diff --git a/.cloudflare/docs-proxy/src/worker.js b/.cloudflare/docs-proxy/src/worker.js index f9f441883ad9b86fe1156277489cc6451c2ff316..60b4a67054d69501f05d3048809cc42a76a38efb 100644 --- a/.cloudflare/docs-proxy/src/worker.js +++ b/.cloudflare/docs-proxy/src/worker.js @@ -1,8 +1,17 @@ export default { async fetch(request, _env, _ctx) { const url = new URL(request.url); - url.hostname = "docs-anw.pages.dev"; + let hostname; + if (url.pathname.startsWith("/docs/nightly")) { + hostname = "docs-nightly.pages.dev"; + } else if (url.pathname.startsWith("/docs/preview")) { + hostname = "docs-preview.pages.dev"; + } else { + hostname = "docs-anw.pages.dev"; + } + + url.hostname = hostname; let res = await fetch(url, request); if (res.status === 404) { diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 0920fe4da5c3d06a03f9d32889f3f68c3095c058..61db90aee4fce70842808f18adf3a4898247bd3e 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -5,19 +5,73 @@ on: push: branches: - main - - staged-docs-releases + - preview + - stable + workflow_dispatch: + inputs: + channel: + description: 'Docs channel to deploy: nightly, preview, or stable' + type: string + default: nightly jobs: - deploy_docs_job: + deploy_docs: if: github.repository_owner == 'zed-industries' name: Build and Deploy Docs runs-on: namespace-profile-16x32-ubuntu-2204 env: DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} + MDBOOK_BOOK__SITE_URL: ${{ steps.resolve-channel.outputs.site_url }} + DOCS_CHANNEL: ${{ steps.resolve-channel.outputs.channel }} steps: - name: steps::checkout_repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: clean: false + - id: resolve-channel + name: deploy_docs::resolve_channel_step + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + CHANNEL="${{ inputs.channel }}" + else + case "${{ github.ref }}" in + "refs/heads/main") + CHANNEL="nightly" + ;; + "refs/heads/preview") + CHANNEL="preview" + ;; + "refs/heads/stable") + CHANNEL="stable" + ;; + *) + echo "::error::Unsupported ref for docs deploy: ${{ github.ref }}" + exit 1 + ;; + esac + fi + + case "$CHANNEL" in + "nightly") + SITE_URL="/docs/nightly/" + PROJECT_NAME="docs-nightly" + ;; + "preview") + SITE_URL="/docs/preview/" + PROJECT_NAME="docs-preview" + ;; + "stable") + SITE_URL="/docs/" + PROJECT_NAME="docs" + ;; + *) + echo "::error::Invalid docs channel '$CHANNEL'. Expected one of: nightly, preview, stable." + exit 1 + ;; + esac + + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "site_url=$SITE_URL" >> "$GITHUB_OUTPUT" + echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT" - name: steps::setup_cargo_config run: | mkdir -p ./../.cargo @@ -55,12 +109,12 @@ jobs: args: --no-progress --exclude '^http' 'target/deploy/docs' fail: true jobSummary: false - - name: deploy_docs::deploy_docs_to_pages + - name: deploy_docs::pages_deploy_step uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy target/deploy --project-name=docs + command: pages deploy target/deploy --project-name=${{ steps.resolve-channel.outputs.project_name }} - name: deploy_docs::deploy_install_script uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 with: diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index af47904c7ade90aed2bc9ac1297363152aafed2b..32de9f5d589254e9aa79fe2f66c99d1242804a83 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -505,11 +505,19 @@ jobs: runs-on: namespace-profile-16x32-ubuntu-2204 env: DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} + MDBOOK_BOOK__SITE_URL: ${{ steps.resolve-channel.outputs.site_url }} + DOCS_CHANNEL: ${{ steps.resolve-channel.outputs.channel }} steps: - name: steps::checkout_repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: clean: false + - id: resolve-channel + name: deploy_docs::static_channel_resolution_step + run: | + echo "channel=stable" >> "$GITHUB_OUTPUT" + echo "site_url=/docs/" >> "$GITHUB_OUTPUT" + echo "project_name=docs" >> "$GITHUB_OUTPUT" - name: steps::setup_cargo_config run: | mkdir -p ./../.cargo diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6ef599542a5b2f511915d7435af192162a5dbd3b..78b96b532b042623f83792769f08e33ed05524a6 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -578,6 +578,12 @@ fn handle_postprocessing() -> Result<()> { .expect("Default title not a string") .to_string(); let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default(); + let docs_channel = std::env::var("DOCS_CHANNEL").unwrap_or_else(|_| "stable".to_string()); + let noindex = if docs_channel == "nightly" || docs_channel == "preview" { + "" + } else { + "" + }; output.insert("html".to_string(), zed_html); mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?; @@ -647,6 +653,7 @@ fn handle_postprocessing() -> Result<()> { zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); let contents = contents.replace("#description#", meta_description); let contents = contents.replace("#amplitude_key#", &litude_key); + let contents = contents.replace("#noindex#", noindex); let contents = title_regex() .replace(&contents, |_: ®ex::Captures| { format!("{}", meta_title) diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs index 98f64d41c3eb86dfb335ecf0964f434c50fad0bb..31b22ccfe263063958ae51dec664c77d29d75bcf 100644 --- a/docs/theme/index.hbs +++ b/docs/theme/index.hbs @@ -30,6 +30,7 @@ {{#if is_print }} {{/if}} + #noindex# {{#if base_url}} {{/if}} diff --git a/tooling/xtask/src/tasks/workflows/deploy_docs.rs b/tooling/xtask/src/tasks/workflows/deploy_docs.rs index b215f59f64b8ee3461c83bb494afdbbfa788f767..1711cac707f6de9d7bb80a375493492fbfea249e 100644 --- a/tooling/xtask/src/tasks/workflows/deploy_docs.rs +++ b/tooling/xtask/src/tasks/workflows/deploy_docs.rs @@ -1,11 +1,43 @@ -use gh_workflow::{Event, Expression, Job, Push, Run, Step, Use, Workflow}; +use gh_workflow::{Event, Expression, Job, Push, Run, Step, Use, Workflow, WorkflowDispatch}; use crate::tasks::workflows::{ - runners::{self, Platform}, + runners, steps::{self, FluentBuilder as _, NamedJob, named, release_job}, - vars, + vars::{self, StepOutput, WorkflowInput}, }; +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 { named::uses( "lycheeverse", @@ -33,7 +65,7 @@ pub(crate) fn build_docs_book() -> Step { "#}) } -fn deploy_docs_to_pages() -> Step { +fn pages_deploy_step(project_name: &StepOutput) -> Step { named::uses( "cloudflare", "wrangler-action", @@ -41,7 +73,13 @@ fn deploy_docs_to_pages() -> Step { ) // v3 .add_with(("apiToken", vars::CLOUDFLARE_API_TOKEN)) .add_with(("accountId", vars::CLOUDFLARE_ACCOUNT_ID)) - .add_with(("command", "pages deploy target/deploy --project-name=docs")) + .add_with(( + "command", + format!( + "pages deploy target/deploy --project-name=${{{{ {} }}}}", + project_name.expr() + ), + )) } fn deploy_install_script() -> Step { @@ -80,54 +118,197 @@ fn upload_wrangler_logs() -> Step { .add_with(("path", "/home/runner/.config/.wrangler/logs/")) } -fn docs_build_steps(job: Job) -> Job { - job.add_env(("DOCS_AMPLITUDE_API_KEY", vars::DOCS_AMPLITUDE_API_KEY)) - .runs_on(runners::LINUX_XL) +fn resolve_channel_step( + channel_input: &WorkflowInput, +) -> (Step, StepOutput, StepOutput, StepOutput) { + let step = named::bash(format!( + indoc::indoc! {r#" + if [ "${{{{ github.event_name }}}}" = "workflow_dispatch" ]; then + CHANNEL="${{{{ {dispatch_channel} }}}}" + else + case "${{{{ github.ref }}}}" in + "refs/heads/main") + CHANNEL="nightly" + ;; + "refs/heads/preview") + CHANNEL="preview" + ;; + "refs/heads/stable") + CHANNEL="stable" + ;; + *) + echo "::error::Unsupported ref for docs deploy: ${{{{ github.ref }}}}" + exit 1 + ;; + esac + 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" >> "$GITHUB_OUTPUT" + echo "site_url=$SITE_URL" >> "$GITHUB_OUTPUT" + echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT" + "#}, + dispatch_channel = channel_input.expr(), + 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"); + + 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 static_channel_resolution_step( + channel: DocsChannel, +) -> (Step, StepOutput, StepOutput, StepOutput) { + let (channel_name, site_url, project_name) = match channel { + DocsChannel::Nightly => ( + DocsChannel::Nightly.channel_name(), + DocsChannel::Nightly.site_url(), + DocsChannel::Nightly.project_name(), + ), + DocsChannel::Preview => ( + DocsChannel::Preview.channel_name(), + DocsChannel::Preview.site_url(), + DocsChannel::Preview.project_name(), + ), + DocsChannel::Stable => ( + DocsChannel::Stable.channel_name(), + DocsChannel::Stable.site_url(), + DocsChannel::Stable.project_name(), + ), + }; + + let step = named::bash(format!( + indoc::indoc! {r#" + echo "channel={channel_name}" >> "$GITHUB_OUTPUT" + echo "site_url={site_url}" >> "$GITHUB_OUTPUT" + echo "project_name={project_name}" >> "$GITHUB_OUTPUT" + "#}, + channel_name = channel_name, + site_url = site_url, + project_name = project_name, + )) + .id("resolve-channel"); + + 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_build_steps( + job: Job, + resolved_channel_step: Step, + channel: &StepOutput, + site_url: &StepOutput, + project_name: &StepOutput, + include_deploy_steps: bool, +) -> Job { + let mut job = job + .add_env(("DOCS_AMPLITUDE_API_KEY", vars::DOCS_AMPLITUDE_API_KEY)) .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) + .add_step(resolved_channel_step) + .add_env(("MDBOOK_BOOK__SITE_URL", site_url.to_string())) + .add_env(("DOCS_CHANNEL", channel.to_string())) + .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()) - .add_step(lychee_link_check("target/deploy/docs")) + .add_step(lychee_link_check("target/deploy/docs")); + + if include_deploy_steps { + job = job + .add_step(pages_deploy_step(project_name)) + .add_step(deploy_install_script()) + .add_step(deploy_docs_worker()) + .add_step(upload_wrangler_logs()); + } + + job } pub(crate) fn check_docs() -> NamedJob { + let (resolve_step, channel, site_url, project_name) = + static_channel_resolution_step(DocsChannel::Stable); + NamedJob { name: "check_docs".to_owned(), - job: docs_build_steps(release_job(&[])), + job: docs_build_steps( + release_job(&[]), + resolve_step, + &channel, + &site_url, + &project_name, + false, + ), } } -pub(crate) fn deploy_docs_job() -> NamedJob { - named::job( - docs_build_steps( +pub(crate) fn deploy_docs_job(channel_input: &WorkflowInput) -> NamedJob { + let (resolve_step, channel, site_url, project_name) = resolve_channel_step(channel_input); + + NamedJob { + name: "deploy_docs".to_owned(), + job: docs_build_steps( release_job(&[]) .name("Build and Deploy Docs") .cond(Expression::new( "github.repository_owner == 'zed-industries'", )), - ) - .add_step(deploy_docs_to_pages()) - .add_step(deploy_install_script()) - .add_step(deploy_docs_worker()) - .add_step(upload_wrangler_logs()), - ) + resolve_step, + &channel, + &site_url, + &project_name, + true, + ), + } } pub(crate) fn deploy_docs() -> Workflow { - let deploy_docs = deploy_docs_job(); + let channel = WorkflowInput::string("channel", Some("nightly".to_string())) + .description("Docs channel to deploy: nightly, preview, or stable"); + + let deploy_docs = deploy_docs_job(&channel); named::workflow() - .add_event( - Event::default().push( + .on(Event::default() + .push( Push::default() .add_branch("main") - // todo! remove - .add_branch("staged-docs-releases"), - ), - ) + .add_branch("preview") + .add_branch("stable"), + ) + .workflow_dispatch( + WorkflowDispatch::default().add_input(channel.name, channel.input()), + )) .add_job(deploy_docs.name, deploy_docs.job) }