From 62f7eaba0e660d43739ccd93f6e1d73ec620239e Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 30 Apr 2026 06:10:14 -0500 Subject: [PATCH] Staged docs releases (#50136) Closes #ISSUE Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A *or* Added/Fixed/Improved ... --- .cloudflare/README.md | 4 +- .cloudflare/docs-proxy/src/worker.js | 17 +- .github/actions/build_docs/action.yml | 46 --- .github/workflows/after_release.yml | 26 +- .github/workflows/deploy_cloudflare.yml | 64 ---- .github/workflows/deploy_docs.yml | 153 ++++++++ .github/workflows/deploy_nightly_docs.yml | 23 ++ .github/workflows/run_tests.yml | 24 +- crates/docs_preprocessor/src/main.rs | 7 + docs/theme/index.hbs | 1 + tooling/xtask/src/tasks/workflows.rs | 3 + .../src/tasks/workflows/after_release.rs | 54 +-- .../xtask/src/tasks/workflows/deploy_docs.rs | 360 ++++++++++++++++++ .../xtask/src/tasks/workflows/run_tests.rs | 51 +-- tooling/xtask/src/tasks/workflows/steps.rs | 2 + tooling/xtask/src/tasks/workflows/vars.rs | 3 + 16 files changed, 639 insertions(+), 199 deletions(-) delete mode 100644 .github/actions/build_docs/action.yml delete mode 100644 .github/workflows/deploy_cloudflare.yml create mode 100644 .github/workflows/deploy_docs.yml create mode 100644 .github/workflows/deploy_nightly_docs.yml create mode 100644 tooling/xtask/src/tasks/workflows/deploy_docs.rs diff --git a/.cloudflare/README.md b/.cloudflare/README.md index d21377ddffd..8da1a129fee 100644 --- a/.cloudflare/README.md +++ b/.cloudflare/README.md @@ -4,11 +4,11 @@ from Cloudflare. - `open-source-website-assets` is used for `install.sh` - `docs-proxy` is used for `https://zed.dev/docs` -On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare. +During docs deployments, both of these (and the files they depend on) are uploaded to Cloudflare. ### Deployment -These functions are deployed on push to main by the deploy_cloudflare.yml workflow. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers. +These functions are deployed by the docs deployment workflows. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers. ### Testing diff --git a/.cloudflare/docs-proxy/src/worker.js b/.cloudflare/docs-proxy/src/worker.js index f9f441883ad..08b0265fafb 100644 --- a/.cloudflare/docs-proxy/src/worker.js +++ b/.cloudflare/docs-proxy/src/worker.js @@ -1,7 +1,22 @@ export default { async fetch(request, _env, _ctx) { const url = new URL(request.url); - url.hostname = "docs-anw.pages.dev"; + + if (url.pathname === "/docs/nightly") { + url.hostname = "docs-nightly.pages.dev"; + url.pathname = "/docs/"; + } else if (url.pathname.startsWith("/docs/nightly/")) { + url.hostname = "docs-nightly.pages.dev"; + url.pathname = url.pathname.replace("/docs/nightly/", "/docs/"); + } else if (url.pathname === "/docs/preview") { + url.hostname = "docs-preview-5xd.pages.dev"; + url.pathname = "/docs/"; + } else if (url.pathname.startsWith("/docs/preview/")) { + url.hostname = "docs-preview-5xd.pages.dev"; + url.pathname = url.pathname.replace("/docs/preview/", "/docs/"); + } else { + url.hostname = "docs-anw.pages.dev"; + } let res = await fetch(url, request); diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml deleted file mode 100644 index 002f6f4653f..00000000000 --- a/.github/actions/build_docs/action.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "Build docs" -description: "Build the docs" - -runs: - using: "composite" - steps: - - name: Setup mdBook - uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2 - with: - mdbook-version: "0.4.37" - - - name: Cache dependencies - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" - - - name: Install Linux dependencies - shell: bash -euxo pipefail {0} - run: ./script/linux - - - name: Download WASI SDK - shell: bash -euxo pipefail {0} - run: ./script/download-wasi-sdk - - - name: Generate action metadata - shell: bash -euxo pipefail {0} - run: ./script/generate-action-metadata - - - name: Check for broken links (in MD) - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 - with: - args: --no-progress --exclude '^http' './docs/src/**/*' - fail: true - - - name: Build book - shell: bash -euxo pipefail {0} - run: | - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - - - name: Check for broken links (in HTML) - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 - with: - args: --no-progress --exclude '^http' 'target/deploy/docs/' - fail: true diff --git a/.github/workflows/after_release.yml b/.github/workflows/after_release.yml index ab222076486..f6777aa2c00 100644 --- a/.github/workflows/after_release.yml +++ b/.github/workflows/after_release.yml @@ -1,6 +1,9 @@ # Generated from xtask::workflows::after_release # Rebuild with `cargo xtask workflows`. name: after_release +env: + TAG_NAME: ${{ github.event.release.tag_name || inputs.tag_name }} + IS_PRERELEASE: ${{ github.event.release.prerelease || inputs.prerelease }} on: release: types: @@ -25,7 +28,7 @@ jobs: runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: after_release::rebuild_releases_page::refresh_cloud_releases - run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name || inputs.tag_name }} + run: curl -fX POST "https://cloud.zed.dev/releases/refresh?expect_tag=$TAG_NAME" - name: steps::checkout_repo uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: @@ -34,6 +37,18 @@ jobs: run: ./script/redeploy-vercel env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + deploy_docs: + if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') + 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: ${{ (github.event.release.prerelease || inputs.prerelease) && 'preview' || 'stable' }} + checkout_ref: ${{ github.event.release.tag_name || inputs.tag_name }} post_to_discord: needs: - rebuild_releases_page @@ -43,7 +58,7 @@ jobs: - id: get-release-url name: after_release::post_to_discord::get_release_url run: | - if [ "${{ github.event.release.prerelease || inputs.prerelease }}" == "true" ]; then + if [ "$IS_PRERELEASE" == "true" ]; then URL="https://zed.dev/releases/preview" else URL="https://zed.dev/releases/stable" @@ -55,7 +70,7 @@ jobs: uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 with: stringToTruncate: | - 📣 Zed [${{ github.event.release.tag_name || inputs.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released! + 📣 Zed [${{ env.TAG_NAME }}](<${{ steps.get-release-url.outputs.URL }}>) was just released! ${{ github.event.release.body || inputs.body }} maxLength: 2000 @@ -90,7 +105,7 @@ jobs: - id: set-package-name name: after_release::publish_winget::set_package_name run: | - if ("${{ github.event.release.prerelease || inputs.prerelease }}" -eq "true") { + if ($env:IS_PRERELEASE -eq "true") { $PACKAGE_NAME = "ZedIndustries.Zed.Preview" } else { $PACKAGE_NAME = "ZedIndustries.Zed" @@ -102,7 +117,7 @@ jobs: uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f with: identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }} - release-tag: ${{ github.event.release.tag_name || inputs.tag_name }} + release-tag: ${{ env.TAG_NAME }} max-versions-to-keep: 5 token: ${{ secrets.WINGET_TOKEN }} create_sentry_release: @@ -127,6 +142,7 @@ jobs: - post_to_discord - publish_winget - create_sentry_release + - deploy_docs if: failure() runs-on: namespace-profile-2x4-ubuntu-2404 steps: diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml deleted file mode 100644 index 4e029c63ccd..00000000000 --- a/.github/workflows/deploy_cloudflare.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Deploy Docs - -on: - push: - branches: - - main - -jobs: - deploy-docs: - name: Deploy Docs - if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-16x32-ubuntu-2204 - - steps: - - name: Checkout repo - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - clean: false - - - name: Set up default .cargo/config.toml - run: cp ./.cargo/collab-config.toml ./.cargo/config.toml - - - name: Build docs - uses: ./.github/actions/build_docs - env: - CC: clang - CXX: clang++ - DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} - DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }} - - - name: Deploy Docs - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy target/deploy --project-name=docs - - - name: Deploy Install - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - 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 Workers - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy .cloudflare/docs-proxy/src/worker.js - - - name: Deploy Install Workers - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy .cloudflare/docs-proxy/src/worker.js - - - name: Preserve Wrangler logs - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - if: always() - with: - name: wrangler_logs - path: /home/runner/.config/.wrangler/logs/ diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000000..1739b6b257a --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,153 @@ +# Generated from xtask::workflows::deploy_docs +# Rebuild with `cargo xtask workflows`. +name: deploy_docs +on: + workflow_call: + inputs: + channel: + description: channel + type: string + default: '' + checkout_ref: + description: checkout_ref + 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: + description: 'Docs channel to deploy: nightly, preview, or stable' + type: string + default: '' + checkout_ref: + description: Git ref to checkout and deploy. Defaults to event SHA when omitted. + type: string + default: '' +jobs: + 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 }} + CC: clang + CXX: clang++ + 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" + echo "site_url=$SITE_URL" + echo "project_name=$PROJECT_NAME" + } >> "$GITHUB_OUTPUT" + env: + CHANNEL: ${{ inputs.channel }} + - name: steps::checkout_repo + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + clean: false + ref: ${{ inputs.checkout_ref != '' && inputs.checkout_ref || github.sha }} + - 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/ + env: + DOCS_CHANNEL: ${{ steps.resolve-channel.outputs.channel }} + MDBOOK_BOOK__SITE_URL: ${{ steps.resolve-channel.outputs.site_url }} + - 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 }} --branch main + - 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 +defaults: + run: + shell: bash -euxo pipefail {0} diff --git a/.github/workflows/deploy_nightly_docs.yml b/.github/workflows/deploy_nightly_docs.yml new file mode 100644 index 00000000000..340713e0a41 --- /dev/null +++ b/.github/workflows/deploy_nightly_docs.yml @@ -0,0 +1,23 @@ +# Generated from xtask::workflows::deploy_nightly_docs +# Rebuild with `cargo xtask workflows`. +name: deploy_nightly_docs +on: + push: + branches: + - main +jobs: + deploy_docs: + if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') + 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: nightly + checkout_ref: ${{ github.sha }} +defaults: + run: + shell: bash -euxo pipefail {0} diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 2051fa567b5..4ce9b3cc1d6 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -637,8 +637,9 @@ jobs: needs: - orchestrate if: needs.orchestrate.outputs.run_docs == 'true' && github.event_name != 'merge_group' - runs-on: namespace-profile-8x16-ubuntu-2204 + runs-on: namespace-profile-16x32-ubuntu-2204 env: + DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} CC: clang CXX: clang++ steps: @@ -655,27 +656,30 @@ jobs: with: cache: rust path: ~/.rustup - - name: run_tests::check_docs::lychee_link_check - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 - with: - args: --no-progress --exclude '^http' './docs/src/**/*' - fail: true - jobSummary: false - 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: run_tests::check_docs::install_mdbook + - 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: run_tests::check_docs::build_docs + - name: deploy_docs::build_docs_book run: | mkdir -p target/deploy mdbook build ./docs --dest-dir=../target/deploy/docs/ - - name: run_tests::check_docs::lychee_link_check + env: + DOCS_CHANNEL: stable + MDBOOK_BOOK__SITE_URL: /docs/ + - name: deploy_docs::lychee_link_check uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 with: args: --no-progress --exclude '^http' 'target/deploy/docs' diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6655d86d25c..5b860ba16cd 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -679,6 +679,12 @@ fn handle_postprocessing() -> Result<()> { .to_string(); let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default(); let consent_io_instance = std::env::var("DOCS_CONSENT_IO_INSTANCE").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)?; @@ -749,6 +755,7 @@ fn handle_postprocessing() -> Result<()> { let contents = contents.replace("#description#", meta_description); let contents = contents.replace("#amplitude_key#", &litude_key); let contents = contents.replace("#consent_io_instance#", &consent_io_instance); + 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 24378bcca69..2c7786817aa 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.rs b/tooling/xtask/src/tasks/workflows.rs index 2e21363bb65..b275503f34d 100644 --- a/tooling/xtask/src/tasks/workflows.rs +++ b/tooling/xtask/src/tasks/workflows.rs @@ -15,6 +15,7 @@ mod compare_perf; mod compliance_check; mod danger; mod deploy_collab; +mod deploy_docs; mod extension_auto_bump; mod extension_bump; mod extension_tests; @@ -203,6 +204,8 @@ pub fn run_workflows(args: GenerateWorkflowArgs) -> Result<()> { WorkflowFile::zed(compliance_check::compliance_check), WorkflowFile::zed(danger::danger), WorkflowFile::zed(deploy_collab::deploy_collab), + WorkflowFile::zed(deploy_docs::deploy_docs), + WorkflowFile::zed(deploy_docs::deploy_nightly_docs), WorkflowFile::zed(extension_bump::extension_bump), WorkflowFile::zed(extension_auto_bump::extension_auto_bump), WorkflowFile::zed(extension_tests::extension_tests), diff --git a/tooling/xtask/src/tasks/workflows/after_release.rs b/tooling/xtask/src/tasks/workflows/after_release.rs index 07ff1fba0d4..fe96325c7fd 100644 --- a/tooling/xtask/src/tasks/workflows/after_release.rs +++ b/tooling/xtask/src/tasks/workflows/after_release.rs @@ -1,15 +1,19 @@ use gh_workflow::*; use crate::tasks::workflows::{ + deploy_docs::deploy_docs_workflow_call, release::{self, notify_on_failure}, runners, steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named}, vars::{self, StepOutput, WorkflowInput}, }; -const TAG_NAME: &str = "${{ github.event.release.tag_name || inputs.tag_name }}"; -const IS_PRERELEASE: &str = "${{ github.event.release.prerelease || inputs.prerelease }}"; +const TAG_NAME_ENV: &str = "${{ github.event.release.tag_name || inputs.tag_name }}"; +const IS_PRERELEASE_ENV: &str = "${{ github.event.release.prerelease || inputs.prerelease }}"; +const TAG_NAME: &str = "${{ env.TAG_NAME }}"; 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,17 +21,26 @@ 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_workflow_call(DOCS_CHANNEL, TAG_NAME_ENV); 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, - &post_to_discord, - &publish_winget, - &create_sentry_release, - ]); + let notify_on_failure = { + let notify_on_failure = notify_on_failure(&[ + &refresh_zed_dev, + &post_to_discord, + &publish_winget, + &create_sentry_release, + ]); + NamedJob { + name: notify_on_failure.name, + job: notify_on_failure.job.add_need(deploy_docs.name.clone()), + } + }; named::workflow() + .add_env(("TAG_NAME", TAG_NAME_ENV)) + .add_env(("IS_PRERELEASE", IS_PRERELEASE_ENV)) .on(Event::default() .release(Release::default().types(vec![ReleaseType::Published])) .workflow_dispatch( @@ -37,6 +50,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) @@ -45,9 +59,7 @@ pub fn after_release() -> Workflow { fn rebuild_releases_page() -> NamedJob { fn refresh_cloud_releases() -> Step { - named::bash(format!( - "curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag={TAG_NAME}" - )) + named::bash("curl -fX POST \"https://cloud.zed.dev/releases/refresh?expect_tag=$TAG_NAME\"") } fn redeploy_zed_dev() -> Step { @@ -66,16 +78,16 @@ fn rebuild_releases_page() -> NamedJob { fn post_to_discord(deps: &[&NamedJob]) -> NamedJob { fn get_release_url() -> Step { - named::bash(format!( - r#"if [ "{IS_PRERELEASE}" == "true" ]; then + named::bash( + r#"if [ "$IS_PRERELEASE" == "true" ]; then URL="https://zed.dev/releases/preview" else URL="https://zed.dev/releases/stable" fi echo "URL=$URL" >> "$GITHUB_OUTPUT" -"# - )) +"#, + ) .id("get-release-url") } @@ -136,17 +148,15 @@ fn publish_winget() -> NamedJob { } fn set_package_name() -> (Step, StepOutput) { - let script = format!( - r#"if ("{IS_PRERELEASE}" -eq "true") {{ + let script = r#"if ($env:IS_PRERELEASE -eq "true") { $PACKAGE_NAME = "ZedIndustries.Zed.Preview" -}} else {{ +} else { $PACKAGE_NAME = "ZedIndustries.Zed" -}} +} echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT -"# - ); - let step = named::pwsh(&script).id("set-package-name"); +"#; + let step = named::pwsh(script).id("set-package-name"); let output = StepOutput::new(&step, "PACKAGE_NAME"); (step, output) diff --git a/tooling/xtask/src/tasks/workflows/deploy_docs.rs b/tooling/xtask/src/tasks/workflows/deploy_docs.rs new file mode 100644 index 00000000000..c2cb6331816 --- /dev/null +++ b/tooling/xtask/src/tasks/workflows/deploy_docs.rs @@ -0,0 +1,360 @@ +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, FluentBuilder as _, NamedJob, 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 { + 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 { + 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 { + 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, + docs_channel: impl Into, + site_url: impl Into, +) -> 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_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 { + 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 { + 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 { + 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() -> Step { + named::uses( + "actions", + "upload-artifact", + "ea165f8d65b6e75b540449e92b4886f43607fa02", + ) // v4 + .if_condition(Expression::new("always()")) + .add_with(("name", "wrangler_logs")) + .add_with(("path", "/home/runner/.config/.wrangler/logs/")) + } + + 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(&[]), + None, + DocsChannel::Stable.channel_name(), + DocsChannel::Stable.site_url(), + ), + } +} + +fn resolve_channel_step( + channel_expr: impl Into, +) -> (Step, 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, checkout_ref: Option) -> 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, + checkout_ref: impl Into, +) -> NamedJob { + let job = Job::default() + .with_repository_owner_guard() + .permissions(Permissions::default().contents(Level::Read)) + .uses( + "zed-industries", + "zed", + ".github/workflows/deploy_docs.yml", + "main", + ) + .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(), + ), + ( + "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() + .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, + }, + ), + ( + "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") + .add_event(Event::default().push(Push::default().add_branch("main"))) + .add_job(deploy_docs.name, deploy_docs.job) +} diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index 644c033f681..0bbff446a75 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -15,6 +15,7 @@ use crate::tasks::workflows::{ }; use super::{ + deploy_docs, runners::{self, Arch, Platform}, steps::{self, FluentBuilder, NamedJob, named, release_job}, }; @@ -82,7 +83,7 @@ pub(crate) fn run_tests() -> Workflow { .then(check_dependencies()), // could be more specific here? should_check_docs .and_not_in_merge_queue() - .then(check_docs()), + .then(deploy_docs::check_docs()), should_check_licences .and_not_in_merge_queue() .then(check_licenses()), @@ -713,54 +714,6 @@ fn check_licenses() -> NamedJob { ) } -fn check_docs() -> NamedJob { - fn lychee_link_check(dir: &str) -> Step { - 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)) - } - - fn install_mdbook() -> Step { - named::uses( - "peaceiris", - "actions-mdbook", - "ee69d230fe19748b7abf22df32acaa93833fad08", // v2 - ) - .with(("mdbook-version", "0.4.37")) - } - - fn build_docs() -> Step { - named::bash(indoc::indoc! {r#" - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - "#}) - } - - named::job(use_clang( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - // todo(ci): un-inline build_docs/action.yml here - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step( - lychee_link_check("./docs/src/**/*"), // check markdown links - ) - .map(steps::install_linux_dependencies) - .add_step(steps::script("./script/generate-action-metadata")) - .add_step(install_mdbook()) - .add_step(build_docs()) - .add_step( - lychee_link_check("target/deploy/docs"), // check links in generated html - ), - )) -} - pub(crate) fn check_scripts() -> NamedJob { fn download_actionlint() -> Step { named::bash( diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs index df0f6aababd..2e13427d603 100644 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ b/tooling/xtask/src/tasks/workflows/steps.rs @@ -137,6 +137,8 @@ impl From for Step { } } +impl FluentBuilder for CheckoutStep {} + pub fn checkout_repo() -> CheckoutStep { CheckoutStep::default() } diff --git a/tooling/xtask/src/tasks/workflows/vars.rs b/tooling/xtask/src/tasks/workflows/vars.rs index 87010b9b79a..6f6fca5e2ab 100644 --- a/tooling/xtask/src/tasks/workflows/vars.rs +++ b/tooling/xtask/src/tasks/workflows/vars.rs @@ -51,6 +51,9 @@ secret!(SLACK_WEBHOOK_WORKFLOW_FAILURES); secret!(R2_ACCOUNT_ID); secret!(R2_ACCESS_KEY_ID); secret!(R2_SECRET_ACCESS_KEY); +secret!(CLOUDFLARE_API_TOKEN); +secret!(CLOUDFLARE_ACCOUNT_ID); +secret!(DOCS_AMPLITUDE_API_KEY); // todo(ci) make these secrets too... var!(AZURE_SIGNING_ACCOUNT_NAME);