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 ...
This commit is contained in:
Ben Kunkle 2026-04-30 06:10:14 -05:00 committed by GitHub
parent c499a95218
commit 62f7eaba0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 639 additions and 199 deletions

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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:

View file

@ -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/

153
.github/workflows/deploy_docs.yml vendored Normal file
View file

@ -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}

View file

@ -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}

View file

@ -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'

View file

@ -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" {
"<meta name=\"robots\" content=\"noindex, nofollow\">"
} 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#", &amplitude_key);
let contents = contents.replace("#consent_io_instance#", &consent_io_instance);
let contents = contents.replace("#noindex#", noindex);
let contents = title_regex()
.replace(&contents, |_: &regex::Captures| {
format!("<title>{}</title>", meta_title)

View file

@ -30,6 +30,7 @@
{{#if is_print }}
<meta name="robots" content="noindex">
{{/if}}
#noindex#
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}

View file

@ -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),

View file

@ -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<Run> {
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<Run> {
@ -66,16 +78,16 @@ fn rebuild_releases_page() -> NamedJob {
fn post_to_discord(deps: &[&NamedJob]) -> NamedJob {
fn get_release_url() -> Step<Run> {
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<Run>, 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)

View file

@ -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<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_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() -> Step<Use> {
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<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",
"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)
}

View file

@ -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<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))
}
fn install_mdbook() -> Step<Use> {
named::uses(
"peaceiris",
"actions-mdbook",
"ee69d230fe19748b7abf22df32acaa93833fad08", // v2
)
.with(("mdbook-version", "0.4.37"))
}
fn build_docs() -> Step<Run> {
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<Run> {
named::bash(

View file

@ -137,6 +137,8 @@ impl From<CheckoutStep> for Step<Use> {
}
}
impl FluentBuilder for CheckoutStep {}
pub fn checkout_repo() -> CheckoutStep {
CheckoutStep::default()
}

View file

@ -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);