in after_release

Ben Kunkle created

Change summary

.github/workflows/after_release.yml                | 111 ++++++++
.github/workflows/deploy_docs.yml                  |  27 -
.github/workflows/release.yml                      |  14 -
PLAN.md                                            | 219 ----------------
tooling/xtask/src/tasks/workflows/after_release.rs |   6 
tooling/xtask/src/tasks/workflows/deploy_docs.rs   | 181 +++++-------
tooling/xtask/src/tasks/workflows/release.rs       |  51 ---
7 files changed, 199 insertions(+), 410 deletions(-)

Detailed changes

.github/workflows/after_release.yml 🔗

@@ -34,6 +34,116 @@ jobs:
       run: ./script/redeploy-vercel
       env:
         VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+  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:
+    - id: resolve-channel
+      name: deploy_docs::resolve_channel_step
+      run: |
+        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="/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"
+      env:
+        CHANNEL: ${{ (github.event.release.prerelease || inputs.prerelease) && 'preview' || 'stable' }}
+    - name: steps::checkout_repo
+      uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
+      with:
+        clean: false
+        ref: ${{ github.event.release.tag_name || inputs.tag_name }}
+    - name: steps::setup_cargo_config
+      run: |
+        mkdir -p ./../.cargo
+        cp ./.cargo/ci-config.toml ./../.cargo/config.toml
+    - name: steps::cache_rust_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
+      with:
+        cache: rust
+        path: ~/.rustup
+    - name: steps::setup_linux
+      run: ./script/linux
+    - name: steps::download_wasi_sdk
+      run: ./script/download-wasi-sdk
+    - name: ./script/generate-action-metadata
+      run: ./script/generate-action-metadata
+    - name: deploy_docs::lychee_link_check
+      uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332
+      with:
+        args: --no-progress --exclude '^http' './docs/src/**/*'
+        fail: true
+        jobSummary: false
+    - name: deploy_docs::install_mdbook
+      uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
+      with:
+        mdbook-version: 0.4.37
+    - name: deploy_docs::build_docs_book
+      run: |
+        mkdir -p target/deploy
+        mdbook build ./docs --dest-dir=../target/deploy/docs/
+    - name: deploy_docs::lychee_link_check
+      uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332
+      with:
+        args: --no-progress --exclude '^http' 'target/deploy/docs'
+        fail: true
+        jobSummary: false
+    - name: deploy_docs::docs_deploy_steps::deploy_to_cf_pages
+      uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65
+      with:
+        apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+        accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+        command: pages deploy target/deploy --project-name=${{ steps.resolve-channel.outputs.project_name }}
+    - name: deploy_docs::docs_deploy_steps::upload_install_script
+      uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65
+      with:
+        apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+        accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+        command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
+    - name: deploy_docs::docs_deploy_steps::deploy_docs_worker
+      uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65
+      with:
+        apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+        accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+        command: deploy .cloudflare/docs-proxy/src/worker.js
+    - name: deploy_docs::docs_deploy_steps::upload_wrangler_logs
+      if: always()
+      uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+      with:
+        name: wrangler_logs
+        path: /home/runner/.config/.wrangler/logs/
+    timeout-minutes: 60
   post_to_discord:
     needs:
     - rebuild_releases_page
@@ -124,6 +234,7 @@ jobs:
   notify_on_failure:
     needs:
     - rebuild_releases_page
+    - deploy_docs
     - post_to_discord
     - publish_winget
     - create_sentry_release

.github/workflows/deploy_docs.yml 🔗

@@ -2,29 +2,6 @@
 # Rebuild with `cargo xtask workflows`.
 name: deploy_docs
 on:
-  push:
-    branches:
-    - main
-  workflow_call:
-    inputs:
-      channel:
-        description: channel
-        type: string
-        default: ''
-      commit_sha:
-        description: commit_sha
-        type: string
-        default: ''
-    secrets:
-      DOCS_AMPLITUDE_API_KEY:
-        description: DOCS_AMPLITUDE_API_KEY
-        required: true
-      CLOUDFLARE_API_TOKEN:
-        description: CLOUDFLARE_API_TOKEN
-        required: true
-      CLOUDFLARE_ACCOUNT_ID:
-        description: CLOUDFLARE_ACCOUNT_ID
-        required: true
   workflow_dispatch:
     inputs:
       channel:
@@ -46,7 +23,7 @@ jobs:
       DOCS_CHANNEL: ${{ steps.resolve-channel.outputs.channel }}
     steps:
     - id: resolve-channel
-      name: deploy_docs::deploy_docs_job::resolve_channel_step
+      name: deploy_docs::resolve_channel_step
       run: |
         if [ -z "$CHANNEL" ]; then
             if [ "$GITHUB_REF" = "refs/heads/main" ]; then
@@ -80,7 +57,7 @@ jobs:
         echo "site_url=$SITE_URL" >> "$GITHUB_OUTPUT"
         echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
       env:
-        CHANNEL: ${ inputs.channel }
+        CHANNEL: inputs.channel
     - name: steps::checkout_repo
       uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
       with:

.github/workflows/release.yml 🔗

@@ -725,20 +725,6 @@ jobs:
       run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
       env:
         GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
-  deploy_docs:
-    needs:
-    - validate_release_assets
-    - create_draft_release
-    permissions:
-      contents: read
-    uses: zed-industries/zed/.github/workflows/deploy_docs.yml@main
-    secrets:
-      DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
-      CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-      CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
-    with:
-      channel: ${{ endsWith(github.ref_name, '-pre') && 'preview' || 'stable' }}
-      commit_sha: ${{ github.sha }}
   push_release_update_notification:
     needs:
     - create_draft_release

PLAN.md 🔗

@@ -1,219 +0,0 @@
-# Staged Docs Releases Plan
-
-## Background & Current State
-
-| Component | Status | Location |
-|---|---|---|
-| Docs build (CI check) | **xtask-generated** (in `run_tests.rs`) | `check_docs()` at L514–560 |
-| Docs deploy workflow | **Hand-written YAML** | `.github/workflows/deploy_cloudflare.yml` |
-| Composite build action | **Hand-written YAML** | `.github/actions/build_docs/action.yml` |
-| Docs-proxy worker | **Runtime JS** | `.cloudflare/docs-proxy/src/worker.js` |
-| book.toml | **Config** | `docs/book.toml` (site-url = `/docs/`) |
-
-There's already a TODO in the codebase acknowledging the duplication:
-
-```
-// todo(ci): un-inline build_docs/action.yml here
-```
-(`tooling/xtask/src/tasks/workflows/run_tests.rs` L549)
-
----
-
-## Step 1: Move Docs Build & Deploy into xtask Workflow Generation
-
-### 1.1 Create `deploy_docs.rs` with shared build helpers and deploy workflow
-
-Create `tooling/xtask/src/tasks/workflows/deploy_docs.rs`. This module owns all docs build and deploy logic and exports shared helpers used by both the new deploy workflow and `check_docs()` in `run_tests.rs`.
-
-**Shared helpers** — export as `pub(crate)`:
-
-```rust
-pub(crate) fn lychee_link_check(dir: &str) -> Step<Use> { ... }
-pub(crate) fn install_mdbook() -> Step<Use> { ... }
-pub(crate) fn build_docs_book() -> Step<Run> { ... }
-```
-
-Copy each implementation verbatim from the inline private functions currently inside `check_docs()` in `run_tests.rs`.
-
-**Build job** — mirror `.github/actions/build_docs/action.yml` step-for-step, using the patterns from `check_docs()` in `run_tests.rs` as the reference for how shared steps are expressed in xtask. Steps in order:
-
-1. `steps::checkout_repo()`
-2. A step that runs `cp ./.cargo/collab-config.toml ./.cargo/config.toml` — the deploy workflow uses `collab-config.toml` (not `ci-config.toml`), so this is distinct from `steps::setup_cargo_config`
-3. `steps::cache_rust_dependencies_namespace()`
-4. `steps::install_linux_dependencies` (covers `./script/linux`, `./script/install-mold`, `./script/download-wasi-sdk`)
-5. `steps::script("./script/generate-action-metadata")`
-6. `lychee_link_check("./docs/src/**/*")` — check Markdown links
-7. `install_mdbook()`
-8. `build_docs_book()`
-9. `lychee_link_check("target/deploy/docs")` — check links in generated HTML
-
-Set `DOCS_AMPLITUDE_API_KEY` on the build job (currently passed via `env:` to the composite action in `deploy_cloudflare.yml`).
-
-### 1.1.1
-**Deploy job** — copy each step verbatim from the `deploy-docs` job in `deploy_cloudflare.yml`. Steps in order:
-
-1. Wrangler: `pages deploy target/deploy --project-name=docs`
-2. Wrangler: `r2 object put -f script/install.sh zed-open-source-website-assets/install.sh`
-3. Wrangler: `deploy .cloudflare/docs-proxy/src/worker.js`
-4. Upload Wrangler logs artifact (`always()`, path `~/.config/.wrangler/logs/`, name `wrangler_logs`)
-
-Note: steps "Deploy Docs Workers" and "Deploy Install Workers" in `deploy_cloudflare.yml` run the identical `wrangler deploy .cloudflare/docs-proxy/src/worker.js` command — this is a copy-paste bug. Include it only once (step 3 above).
-
-Each Wrangler step uses `cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65` with `apiToken` and `accountId` from `vars::CLOUDFLARE_API_TOKEN` and `vars::CLOUDFLARE_ACCOUNT_ID`.
-
-**Update `check_docs()`** — once the module exists, replace the inline private `lychee_link_check`, `install_mdbook`, and `build_docs` functions in `run_tests.rs` with calls to `deploy_docs::lychee_link_check`, `deploy_docs::install_mdbook`, and `deploy_docs::build_docs_book`. This resolves the TODO at L549.
-
-### 1.2 Register the new workflow
-
-In `tooling/xtask/src/tasks/workflows.rs`:
-- Add `mod deploy_docs;`
-- Add `WorkflowFile::zed(deploy_docs::deploy_docs)` to the workflows array
-
-### 1.3 Add Cloudflare secrets to `vars.rs`
-
-Add typed secret references:
-
-```rust
-secret!(CLOUDFLARE_API_TOKEN);
-secret!(CLOUDFLARE_ACCOUNT_ID);
-secret!(DOCS_AMPLITUDE_API_KEY);
-```
-
-### 1.4 Verify generated output matches the hand-written workflow
-
-- Run `cargo xtask workflows` to generate `.github/workflows/deploy_docs.yml`
-- Diff the generated file against `.github/workflows/deploy_cloudflare.yml` and `.github/actions/build_docs/action.yml` to confirm all steps, env vars, secrets, triggers, and job dependencies are equivalent
-- Pay attention to: step ordering, `if:` conditions, artifact names, the Wrangler action hash, and that the duplicate worker deploy step is now deduplicated
-
-### 1.5 Ask the user to approve the generated files
-
-**Stop and show the generated `.github/workflows/deploy_docs.yml` to the user.** Ask them to confirm the output looks correct before proceeding to delete the hand-written files. Do not continue to step 1.6 without explicit approval.
-
-### 1.6 Delete hand-written files
-
-- Delete `.github/workflows/deploy_cloudflare.yml` (replaced by the generated workflow)
-- Delete `.github/actions/build_docs/action.yml` (steps are now inlined in xtask)
-
-### 1.7 Final check
-
-The CI self-check (`check_xtask_workflows`) will enforce that the generated file stays in sync going forward.
-
----
-
-## Step 2: Split Deployments into Nightly, Preview, and Stable
-
-### 2.1 Introduce `DocsChannel` and parameterize the build
-
-Add a `DocsChannel` enum to `deploy_docs.rs`:
-
-```rust
-pub(crate) enum DocsChannel {
-    Nightly,  // site-url = "/docs/nightly/", project = "docs-nightly"
-    Preview,  // site-url = "/docs/preview/", project = "docs-preview"
-    Stable,   // site-url = "/docs/",          project = "docs"
-}
-```
-
-mdBook supports overriding `book.toml` values at build time via `MDBOOK_`-prefixed environment variables, using `__` for TOML key nesting. The `site-url` field lives under `[book]`, so setting `MDBOOK_BOOK__SITE_URL=/docs/nightly/` before `mdbook build` overrides it without touching the file. The `book.toml` in the repository remains unchanged at `site-url = "/docs/"`.
-
-The `build_docs_book()` step stays as-is; the channel env var is applied to the build job:
-
-```rust
-fn build_job(channel: DocsChannel, deps: &[&NamedJob]) -> NamedJob {
-    // ...
-    .add_env(("MDBOOK_BOOK__SITE_URL", channel.site_url()))
-}
-```
-
-The deploy step uses `--project-name` matching the channel:
-
-```rust
-fn pages_deploy_step(channel: &DocsChannel) -> Step<Use> {
-    // wrangler: pages deploy target/deploy --project-name=<channel.project_name()>
-}
-```
-
-Export `pub(crate) fn deploy_docs_job(channel: DocsChannel, deps: &[&NamedJob]) -> NamedJob` for use in `release.rs`.
-
-### 2.2 Create `deploy_docs_nightly.rs`
-
-Create `tooling/xtask/src/tasks/workflows/deploy_docs_nightly.rs`. This is the standalone workflow for nightly docs, triggered on push to `main`. It calls `deploy_docs::deploy_docs_job(DocsChannel::Nightly, &[])`. The deploy job includes the `install.sh` R2 upload and docs-proxy worker deploy (these are `main`-push operations, currently bundled in `deploy_cloudflare.yml`).
-
-Register it in `workflows.rs`: `WorkflowFile::zed(deploy_docs_nightly::deploy_docs_nightly)`.
-
-### 2.3 Add preview and stable deploy jobs to `release.rs`
-
-In `release.rs`, call `deploy_docs::deploy_docs_job` twice after `validate_release_assets` completes:
-
-```rust
-let deploy_docs_preview = deploy_docs::deploy_docs_job(
-    DocsChannel::Preview,
-    &[&validate_release_assets],
-);
-let deploy_docs_stable = deploy_docs::deploy_docs_job(
-    DocsChannel::Stable,
-    &[&validate_release_assets],
-);
-```
-
-Apply an `if:` condition to each job:
-- Preview: `startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-pre')`
-- Stable: `startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-pre')`
-
-Add both jobs to the release workflow with `.add_job(...)`.
-
-### 2.4 Update the docs-proxy worker
-
-Modify `.cloudflare/docs-proxy/src/worker.js` to route all three channels:
-
-```javascript
-export default {
-  async fetch(request, _env, _ctx) {
-    const url = new URL(request.url);
-
-    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) {
-      res = await fetch("https://zed.dev/404");
-    }
-
-    return res;
-  },
-};
-```
-
-The `docs-nightly` and `docs-preview` Pages project hostnames will be auto-assigned by Cloudflare on first deploy — verify the actual `*.pages.dev` hostnames and update accordingly.
-
-**Important:** Confirm no existing stable docs pages have a path starting with `nightly` or `preview` — grep `docs/src/SUMMARY.md` to verify.
-
-### 2.5 Wire install.sh and worker deploys
-
-- **install.sh R2 upload** → stays in the nightly workflow (runs on push to `main`, matching current behavior)
-- **docs-proxy worker deploy** → stays in the nightly workflow (worker routes all three channels, deploying once on `main` push is sufficient)
-
----
-
-### 2.6 Add `noindex` meta tag for nightly and preview
-
-Also set a `DOCS_CHANNEL` env var (`nightly`, `preview`, or `stable`) on the build job alongside `MDBOOK_BOOK__SITE_URL` in step 2.1. Add a `channel_name() -> &'static str` method to `DocsChannel` returning the appropriate string.
-
-In `docs/theme/index.hbs`, add a `#noindex#` placeholder in `<head>` directly after the existing `{{#if is_print}}` noindex block:
-
-```html
-#noindex#
-```
-
-In `crates/docs_preprocessor/src/main.rs`'s `handle_postprocessing()`, read `DOCS_CHANNEL` and replace the placeholder — following the same pattern as the existing `#amplitude_key#` and `#description#` replacements:
-
-- `nightly` or `preview`: replace `#noindex#` with `<meta name="robots" content="noindex, nofollow">`
-- anything else: replace `#noindex#` with an empty string

tooling/xtask/src/tasks/workflows/after_release.rs 🔗

@@ -1,6 +1,7 @@
 use gh_workflow::*;
 
 use crate::tasks::workflows::{
+    deploy_docs,
     release::{self, notify_on_failure},
     runners,
     steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named},
@@ -10,6 +11,8 @@ use crate::tasks::workflows::{
 const TAG_NAME: &str = "${{ github.event.release.tag_name || inputs.tag_name }}";
 const IS_PRERELEASE: &str = "${{ github.event.release.prerelease || inputs.prerelease }}";
 const RELEASE_BODY: &str = "${{ github.event.release.body || inputs.body }}";
+const DOCS_CHANNEL: &str =
+    "${{ (github.event.release.prerelease || inputs.prerelease) && 'preview' || 'stable' }}";
 
 pub fn after_release() -> Workflow {
     let tag_name = WorkflowInput::string("tag_name", None);
@@ -17,11 +20,13 @@ pub fn after_release() -> Workflow {
     let body = WorkflowInput::string("body", Some(String::new()));
 
     let refresh_zed_dev = rebuild_releases_page();
+    let deploy_docs = deploy_docs::release_docs_job(DOCS_CHANNEL, TAG_NAME);
     let post_to_discord = post_to_discord(&[&refresh_zed_dev]);
     let publish_winget = publish_winget();
     let create_sentry_release = create_sentry_release();
     let notify_on_failure = notify_on_failure(&[
         &refresh_zed_dev,
+        &deploy_docs,
         &post_to_discord,
         &publish_winget,
         &create_sentry_release,
@@ -37,6 +42,7 @@ pub fn after_release() -> Workflow {
                     .add_input(body.name, body.input()),
             ))
         .add_job(refresh_zed_dev.name, refresh_zed_dev.job)
+        .add_job(deploy_docs.name, deploy_docs.job)
         .add_job(post_to_discord.name, post_to_discord.job)
         .add_job(publish_winget.name, publish_winget.job)
         .add_job(create_sentry_release.name, create_sentry_release.job)

tooling/xtask/src/tasks/workflows/deploy_docs.rs 🔗

@@ -1,7 +1,4 @@
-use gh_workflow::{
-    Event, Expression, Job, Push, Run, Step, Use, Workflow, WorkflowCall, WorkflowCallSecret,
-    WorkflowDispatch,
-};
+use gh_workflow::{Event, Expression, Job, Run, Step, Use, Workflow, WorkflowDispatch};
 
 use crate::tasks::workflows::{
     runners,
@@ -167,64 +164,61 @@ pub(crate) fn check_docs() -> NamedJob {
     }
 }
 
-pub(crate) fn deploy_docs_job(
-    channel_input: &WorkflowInput,
-    commit_sha_input: &WorkflowInput,
-) -> NamedJob {
-    fn resolve_channel_step(
-        channel_input: &WorkflowInput,
-    ) -> (Step<Run>, StepOutput, StepOutput, StepOutput) {
-        let step = named::bash(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
+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
+            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"
-            "#},
-            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_input.expr()))
-        ;
+            echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
+            echo "site_url=$SITE_URL" >> "$GITHUB_OUTPUT"
+            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)
-    }
-    let (resolve_step, channel, site_url, project_name) = resolve_channel_step(channel_input);
+    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(),
@@ -236,11 +230,7 @@ pub(crate) fn deploy_docs_job(
                         "github.repository_owner == 'zed-industries'",
                     ))
                     .add_step(resolve_step),
-                Some(format!(
-                    "${{{{ {} != '' && {} || github.sha }}}}",
-                    commit_sha_input.expr(),
-                    commit_sha_input.expr()
-                )),
+                checkout_ref,
                 channel.to_string(),
                 site_url.to_string(),
             ),
@@ -249,6 +239,27 @@ pub(crate) fn deploy_docs_job(
     }
 }
 
+pub(crate) fn release_docs_job(
+    channel_expr: impl Into<String>,
+    checkout_ref: impl Into<String>,
+) -> NamedJob {
+    docs_job(channel_expr, Some(checkout_ref.into()))
+}
+
+pub(crate) fn deploy_docs_job(
+    channel_input: &WorkflowInput,
+    commit_sha_input: &WorkflowInput,
+) -> NamedJob {
+    docs_job(
+        channel_input.expr(),
+        Some(format!(
+            "${{{{ {} != '' && {} || github.sha }}}}",
+            commit_sha_input.expr(),
+            commit_sha_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");
@@ -258,44 +269,10 @@ pub(crate) fn deploy_docs() -> Workflow {
     let deploy_docs = deploy_docs_job(&channel, &commit_sha);
 
     named::workflow()
-        .add_event(
-            Event::default()
-                .push(Push::default().add_branch("main"))
-                .workflow_dispatch(
-                    WorkflowDispatch::default()
-                        .add_input(channel.name, channel.input())
-                        .add_input(commit_sha.name, commit_sha.input()),
-                ),
-        )
-        .add_event(
-            Event::default().workflow_call(
-                WorkflowCall::default()
-                    .add_input(channel.name, channel.call_input())
-                    .add_input(commit_sha.name, commit_sha.call_input())
-                    .secrets([
-                        (
-                            "DOCS_AMPLITUDE_API_KEY".to_owned(),
-                            WorkflowCallSecret {
-                                description: "DOCS_AMPLITUDE_API_KEY".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,
-                            },
-                        ),
-                    ]),
-            ),
-        )
+        .on(Event::default().workflow_dispatch(
+            WorkflowDispatch::default()
+                .add_input(channel.name, channel.input())
+                .add_input(commit_sha.name, commit_sha.input()),
+        ))
         .add_job(deploy_docs.name, deploy_docs.job)
 }

tooling/xtask/src/tasks/workflows/release.rs 🔗

@@ -1,7 +1,4 @@
-use gh_workflow::{
-    Event, Expression, Input, Job, Level, Permissions, Push, Run, Step, Use, UsesJob, Workflow,
-    ctx::Context,
-};
+use gh_workflow::{Event, Expression, Push, Run, Step, Use, Workflow, ctx::Context};
 use indoc::formatdoc;
 
 use crate::tasks::workflows::{
@@ -67,7 +64,6 @@ pub(crate) fn release() -> Workflow {
     );
 
     let auto_release_preview = auto_release_preview(&[&validate_release_assets]);
-    let deploy_docs = deploy_docs(&[&validate_release_assets], &create_draft_release);
 
     let test_jobs = [
         &macos_tests,
@@ -113,7 +109,6 @@ pub(crate) fn release() -> Workflow {
         .add_job(upload_release_assets.name, upload_release_assets.job)
         .add_job(validate_release_assets.name, validate_release_assets.job)
         .add_job(auto_release_preview.name, auto_release_preview.job)
-        .add_job(deploy_docs.name, deploy_docs.job)
         .add_job(push_slack_notification.name, push_slack_notification.job)
 }
 
@@ -375,50 +370,6 @@ fn auto_release_preview(deps: &[&NamedJob]) -> NamedJob {
     )
 }
 
-fn deploy_docs(deps: &[&NamedJob], create_draft_release: &NamedJob) -> NamedJob<UsesJob> {
-    let job = Job::default()
-        .needs(
-            deps.iter()
-                .map(|job| job.name.clone())
-                .chain(std::iter::once(create_draft_release.name.clone()))
-                .collect::<Vec<_>>(),
-        )
-        .permissions(Permissions::default().contents(Level::Read))
-        .uses(
-            "zed-industries",
-            "zed",
-            ".github/workflows/deploy_docs.yml",
-            "main",
-        )
-        .with(
-            Input::default()
-                .add(
-                    "channel",
-                    "${{ endsWith(github.ref_name, '-pre') && 'preview' || 'stable' }}",
-                )
-                .add("commit_sha", "${{ github.sha }}"),
-        )
-        .secrets(indexmap::IndexMap::from([
-            (
-                "DOCS_AMPLITUDE_API_KEY".to_owned(),
-                vars::DOCS_AMPLITUDE_API_KEY.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 download_workflow_artifacts() -> Step<Use> {
     named::uses(
         "actions",