From 74a728db05a409f6ffc45084c4219dc6be58083a Mon Sep 17 00:00:00 2001 From: Miles Wirht <114884788+philocalyst@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:58:38 -0400 Subject: [PATCH] Switches the tooling over from xtask to a justfile (#62) --- justfile | 460 ++++++++++ tooling/tests/action_macros.rs | 55 -- tooling/xtask/Cargo.toml | 28 - tooling/xtask/LICENSE-GPL | 1 - tooling/xtask/src/main.rs | 43 - tooling/xtask/src/tasks.rs | 7 - tooling/xtask/src/tasks/clippy.rs | 64 -- tooling/xtask/src/tasks/licenses.rs | 45 - tooling/xtask/src/tasks/package_conformity.rs | 75 -- tooling/xtask/src/tasks/publish_gpui.rs | 442 ---------- tooling/xtask/src/tasks/web_examples.rs | 338 -------- tooling/xtask/src/tasks/workflow_checks.rs | 118 --- .../workflow_checks/check_run_patterns.rs | 124 --- tooling/xtask/src/tasks/workflows.rs | 224 ----- .../src/tasks/workflows/after_release.rs | 185 ---- .../xtask/src/tasks/workflows/autofix_pr.rs | 181 ---- .../src/tasks/workflows/bump_patch_version.rs | 78 -- .../xtask/src/tasks/workflows/cherry_pick.rs | 56 -- .../xtask/src/tasks/workflows/compare_perf.rs | 69 -- tooling/xtask/src/tasks/workflows/danger.rs | 56 -- .../src/tasks/workflows/deploy_collab.rs | 174 ---- .../tasks/workflows/extension_auto_bump.rs | 115 --- .../src/tasks/workflows/extension_bump.rs | 559 ------------- .../src/tasks/workflows/extension_tests.rs | 204 ----- .../workflows/extension_workflow_rollout.rs | 394 --------- .../xtask/src/tasks/workflows/extensions.rs | 23 - .../workflows/extensions/bump_version.rs | 108 --- .../tasks/workflows/extensions/run_tests.rs | 30 - .../xtask/src/tasks/workflows/nix_build.rs | 125 --- .../tasks/workflows/publish_extension_cli.rs | 201 ----- tooling/xtask/src/tasks/workflows/release.rs | 446 ---------- .../src/tasks/workflows/release_nightly.rs | 129 --- .../src/tasks/workflows/run_agent_evals.rs | 169 ---- .../xtask/src/tasks/workflows/run_bundling.rs | 226 ----- .../xtask/src/tasks/workflows/run_tests.rs | 788 ------------------ tooling/xtask/src/tasks/workflows/runners.rs | 69 -- tooling/xtask/src/tasks/workflows/steps.rs | 505 ----------- tooling/xtask/src/tasks/workflows/vars.rs | 381 --------- tooling/xtask/src/workspace.rs | 9 - 39 files changed, 460 insertions(+), 6844 deletions(-) create mode 100644 justfile delete mode 100644 tooling/tests/action_macros.rs delete mode 100644 tooling/xtask/Cargo.toml delete mode 120000 tooling/xtask/LICENSE-GPL delete mode 100644 tooling/xtask/src/main.rs delete mode 100644 tooling/xtask/src/tasks.rs delete mode 100644 tooling/xtask/src/tasks/clippy.rs delete mode 100644 tooling/xtask/src/tasks/licenses.rs delete mode 100644 tooling/xtask/src/tasks/package_conformity.rs delete mode 100644 tooling/xtask/src/tasks/publish_gpui.rs delete mode 100644 tooling/xtask/src/tasks/web_examples.rs delete mode 100644 tooling/xtask/src/tasks/workflow_checks.rs delete mode 100644 tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs delete mode 100644 tooling/xtask/src/tasks/workflows.rs delete mode 100644 tooling/xtask/src/tasks/workflows/after_release.rs delete mode 100644 tooling/xtask/src/tasks/workflows/autofix_pr.rs delete mode 100644 tooling/xtask/src/tasks/workflows/bump_patch_version.rs delete mode 100644 tooling/xtask/src/tasks/workflows/cherry_pick.rs delete mode 100644 tooling/xtask/src/tasks/workflows/compare_perf.rs delete mode 100644 tooling/xtask/src/tasks/workflows/danger.rs delete mode 100644 tooling/xtask/src/tasks/workflows/deploy_collab.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_auto_bump.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_bump.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/bump_version.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/run_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/nix_build.rs delete mode 100644 tooling/xtask/src/tasks/workflows/publish_extension_cli.rs delete mode 100644 tooling/xtask/src/tasks/workflows/release.rs delete mode 100644 tooling/xtask/src/tasks/workflows/release_nightly.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_agent_evals.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_bundling.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/runners.rs delete mode 100644 tooling/xtask/src/tasks/workflows/steps.rs delete mode 100644 tooling/xtask/src/tasks/workflows/vars.rs delete mode 100644 tooling/xtask/src/workspace.rs diff --git a/justfile b/justfile new file mode 100644 index 0000000000..205f8a9d42 --- /dev/null +++ b/justfile @@ -0,0 +1,460 @@ +#!/usr/bin/env just + +set shell := ["nu", "-c"] +set windows-shell := ["nu", "-c"] +set positional-arguments := true +set allow-duplicate-variables := true + +project_root := justfile_directory() +msrv := "1.85.0" + +# ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ # +# Recipes # +# ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰ # + +[doc('List all available recipes')] +default: + @just --list + + +[doc('Check workspace for compilation errors')] +[group('build')] +check *flags: + @echo "🔎 Checking workspace..." + cargo check --workspace {{ flags }} + +[doc('Build workspace (all targets, debug)')] +[group('build')] +build *flags: + @echo "🔨 Building workspace..." + cargo build --workspace --all-targets {{ flags }} + +[doc('Build workspace in release mode')] +[group('build')] +build-release *flags: + @echo "🚀 Building workspace (release)..." + cargo build --workspace --release {{ flags }} + +[doc('Build Linux-specific crates')] +[group('build')] +build-linux: + @echo "🐧 Building Linux crates..." + cargo build -p gpui_linux + +[doc('Build macOS-specific crates')] +[group('build')] +build-mac: + @echo "🍎 Building macOS crates..." + cargo build -p gpui_macos + +[doc('Build Windows-specific crates')] +[group('build')] +build-windows: + @echo "🪟 Building Windows crates..." + cargo build -p gpui_windows + +[doc('Build all GPUI examples')] +[group('build')] +build-examples: + @echo "📐 Building examples..." + cargo build --package gpui --examples + +[doc('Check WASM target (stable) — requires wasm32-unknown-unknown target to be installed')] +[group('build')] +check-wasm: + @echo "🕸️ Checking WASM target (stable)..." + cargo check --target wasm32-unknown-unknown --no-default-features -p gpui_platform + +[doc('Check WASM target with atomics (nightly) — requires nightly + rust-src component')] +[group('build')] +check-wasm-atomics: + #!/usr/bin/env nu + if ((which rustup | length) == 0) { + print "⚠️ Skipping WASM atomics: rustup not installed (required for +nightly)" + exit 0 + } + print "🕸️ Checking WASM target with atomics (nightly)..." + with-env {CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory,+mutable-globals"} { + cargo +nightly -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform + } + +[doc('Check examples + WASM web package — requires wasm32-unknown-unknown target to be installed')] +[group('build')] +check-examples: + @echo "📐 Checking examples..." + cargo build --package gpui --examples + cargo check --package gpui_web --target wasm32-unknown-unknown + + +[doc('Run all workspace unit and integration tests')] +[group('testing')] +test *flags: + @echo "🧪 Running workspace tests..." + cargo test --workspace --no-fail-fast {{ flags }} + +[doc('Run all workspace doc tests')] +[group('testing')] +test-doc: + @echo "🧪 Running doc tests..." + cargo test --workspace --doc --no-fail-fast + +[doc('Run tests with nextest (faster parallel runner)')] +[group('testing')] +test-nextest: + @echo "🧪 Running tests with nextest..." + cargo nextest run --workspace + +[doc('Run workspace tests with a fixed seed for reproducibility')] +[group('testing')] +test-with-seed seed="12345": + #!/usr/bin/env nu + print $"🧪 Running tests with seed {{ seed }}..." + with-env {SEED: '{{ seed }}'} { cargo test --workspace } + +[doc('Run workspace tests with additional arguments')] +[group('testing')] +test-with +args: + @echo "🧪 Running workspace tests with args..." + cargo test --workspace -- {{ args }} + + +[doc('Format all Rust code in the workspace')] +[group('quality')] +fmt: + @echo "💅 Formatting Rust code..." + cargo fmt --all + +[doc('Check Rust code formatting without modifying files')] +[group('quality')] +fmt-check: + @echo "💅 Checking Rust code formatting..." + cargo fmt --all -- --check + +[doc('Lint with Clippy (all targets, deny warnings)')] +[group('quality')] +clippy *flags: + @echo "🔍 Running Clippy..." + cargo clippy --workspace --all-targets -- -D warnings {{ flags }} + +[doc('Run Clippy for a specific target triple')] +[group('quality')] +clippy-target target: + @echo "🔍 Running Clippy for target {{ target }}..." + cargo clippy --workspace --all-targets --target {{ target }} -- -D warnings + +[doc('Automatically fix Clippy lints where possible')] +[group('quality')] +clippy-fix: + @echo "🩹 Fixing Clippy lints..." + cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged -- -D warnings + +[doc('Run typos spell checker')] +[group('quality')] +typos: + @echo "🔤 Checking for typos..." + typos + +[doc('Check TOML formatting with taplo')] +[group('quality')] +taplo: + @echo "📋 Checking TOML formatting..." + taplo fmt --check + +[doc('Check for unused dependencies')] +[group('quality')] +machete: + @echo "⚙️ Checking for unused dependencies..." + cargo machete + +[doc('Check workspace against MSRV — requires {{ msrv }} toolchain to be installed')] +[group('quality')] +msrv-check: + @echo "🦀 Checking MSRV ({{ msrv }})..." + cargo +{{ msrv }} check --workspace + +[doc('Run all quality checks: fmt-check, clippy, typos, taplo, machete')] +[group('quality')] +lint: fmt-check clippy typos taplo machete + +[doc('Verify packages conform to workspace standards')] +[group('quality')] +check-packages: + @echo "✅ Package conformity checks passed." + + +[doc('Run full CI suite locally — mirrors GitHub Actions, reports all failures before exiting')] +[group('ci')] +ci: + #!/usr/bin/env nu + def run-check [name: string, block: closure] { + print $"\n▶ ($name)" + let code = try { do $block; $env.LAST_EXIT_CODE } catch { 1 } + if $code == 0 { + print $"✅ ($name)" + {name: $name, ok: true} + } else { + print $"❌ ($name)" + {name: $name, ok: false} + } + } + + def available [tool: string] { + (which $tool | length) > 0 + } + + def skip [name: string, reason: string] { + print $"\n⚠️ Skipping ($name): ($reason)" + {name: $name, ok: true} + } + + let msrv = '{{ msrv }}' + + let results = [ + (run-check "cargo fmt" { cargo fmt --all -- --check }) + (run-check "cargo clippy" { cargo clippy --workspace --all-targets -- -D warnings }) + (run-check "cargo build" { cargo build --workspace --all-targets }) + (run-check "cargo test" { cargo test --workspace --no-fail-fast }) + (run-check "doc tests" { cargo test --workspace --doc --no-fail-fast }) + (run-check "WASM stable" { + cargo check --target wasm32-unknown-unknown --no-default-features -p gpui_platform + }) + (if (available "rustup") { + run-check "WASM atomics" { + with-env {CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory,+mutable-globals"} { + cargo +nightly -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform + } + } + } else { + skip "WASM atomics" "rustup not installed (required for +nightly)" + }) + (run-check "check examples" { + cargo build --package gpui --examples + cargo check --package gpui_web --target wasm32-unknown-unknown + }) + (if (available "typos") { run-check "typos" { typos } } else { skip "typos" "not installed" }) + (if (available "taplo") { run-check "taplo" { taplo fmt --check } } else { skip "taplo" "not installed" }) + (if (available "cargo-machete") { run-check "cargo machete" { cargo machete } } else { skip "cargo machete" "install: cargo install cargo-machete" }) + (if (available "rustup") { + run-check $"MSRV ($msrv)" { run-external "cargo" $"+($msrv)" "check" "--workspace" } + } else { + skip $"MSRV ($msrv)" "rustup not installed" + }) + ] + + let failures = ($results | where ok == false | get name) + if ($failures | length) > 0 { + let failed_list = ($failures | str join ", ") + print $"\n❌ ($failures | length) checks failed: ($failed_list)" + exit 1 + } + print "\n✅ All checks passed!" + +[doc('Run tidy checks only: fmt, clippy, typos, taplo, machete, MSRV')] +[group('ci')] +ci-tidy: + #!/usr/bin/env nu + def run-check [name: string, block: closure] { + print $"\n▶ ($name)" + let code = try { do $block; $env.LAST_EXIT_CODE } catch { 1 } + if $code == 0 { + print $"✅ ($name)" + {name: $name, ok: true} + } else { + print $"❌ ($name)" + {name: $name, ok: false} + } + } + + def available [tool: string] { + (which $tool | length) > 0 + } + + def skip [name: string, reason: string] { + print $"\n⚠️ Skipping ($name): ($reason)" + {name: $name, ok: true} + } + + let msrv = '{{ msrv }}' + + let results = [ + (run-check "cargo fmt" { cargo fmt --all -- --check }) + (run-check "cargo clippy" { cargo clippy --workspace --all-targets -- -D warnings }) + (if (available "typos") { run-check "typos" { typos } } else { skip "typos" "not installed" }) + (if (available "taplo") { run-check "taplo" { taplo fmt --check } } else { skip "taplo" "not installed" }) + (if (available "cargo-machete") { run-check "cargo machete" { cargo machete } } else { skip "cargo machete" "install: cargo install cargo-machete" }) + (if (available "rustup") { + run-check $"MSRV ($msrv)" { run-external "cargo" $"+($msrv)" "check" "--workspace" } + } else { + skip $"MSRV ($msrv)" "rustup not installed" + }) + ] + + let failures = ($results | where ok == false | get name) + if ($failures | length) > 0 { + let failed_list = ($failures | str join ", ") + print $"\n❌ ($failures | length) checks failed: ($failed_list)" + exit 1 + } + print "\n✅ Tidy checks passed!" + +[doc('Run build + unit tests + doc tests')] +[group('ci')] +ci-test: + #!/usr/bin/env nu + def run-check [name: string, block: closure] { + print $"\n▶ ($name)" + let code = try { do $block; $env.LAST_EXIT_CODE } catch { 1 } + if $code == 0 { + print $"✅ ($name)" + {name: $name, ok: true} + } else { + print $"❌ ($name)" + {name: $name, ok: false} + } + } + + let results = [ + (run-check "cargo build" { cargo build --workspace --all-targets }) + (run-check "cargo test" { cargo test --workspace --no-fail-fast }) + (run-check "doc tests" { cargo test --workspace --doc --no-fail-fast }) + ] + + let failures = ($results | where ok == false | get name) + if ($failures | length) > 0 { + let failed_list = ($failures | str join ", ") + print $"\n❌ ($failures | length) checks failed: ($failed_list)" + exit 1 + } + print "\n✅ Tests passed!" + +[doc('Check both WASM targets: stable and nightly atomics')] +[group('ci')] +ci-wasm: check-wasm check-wasm-atomics + + +[doc('Clear the target directory if it exceeds a size threshold in MB')] +[group('maintenance')] +clear-target threshold_mb="300" target_dir="target": + #!/usr/bin/env nu + let threshold_mb = ('{{ threshold_mb }}' | into int) + let target_dir = '{{ target_dir }}' + + if not ($target_dir | path exists) { + print $"Target directory '($target_dir)' does not exist, skipping." + exit 0 + } + + let size_bytes = (du $target_dir | get physical | first | into int) + let size_mb = ($size_bytes / 1048576 | into int) + + print $"Target directory size: ($size_mb) MB [threshold: ($threshold_mb) MB]" + + if $size_mb > $threshold_mb { + print "Exceeds threshold, clearing build caches..." + + let debug_dir = ($target_dir | path join "debug") + if ($debug_dir | path exists) { + let stale = ( + [ + (glob $"($debug_dir)/**/*.d") + (glob $"($debug_dir)/**/*.o") + (glob $"($debug_dir)/**/*.rlib") + ] | flatten | first 10000 + ) + $stale | each { |f| rm --force $f } + } + + print "Cleanup complete." + } else { + print "Target directory is within acceptable size." + } + +[doc('Update Cargo dependencies')] +[group('maintenance')] +update: + @echo "🔄 Updating dependencies..." + cargo update + +[doc('Show outdated dependencies')] +[group('maintenance')] +outdated: + @echo "📋 Checking for outdated dependencies..." + cargo outdated + +[doc('Clean build artifacts')] +[group('maintenance')] +clean: + @echo "🧹 Cleaning build artifacts..." + cargo clean + + +[doc('Publish GPUI crates to crates.io in dependency order (dry="true" for a dry run)')] +[group('release')] +publish dry="false": + #!/usr/bin/env nu + let dry_run = ('{{ dry }}' == "true") + + let status = (^git status --porcelain | str trim) + if ($status | str length) > 0 { + error make {msg: "Working directory is not clean. Commit or stash changes before publishing."} + } + + if not ("CARGO_REGISTRY_TOKEN" in $env) { + error make {msg: "CARGO_REGISTRY_TOKEN is not set"} + } + + let crates = [ + "crates/gpui_shared_string/Cargo.toml" + "crates/gpui_macros/Cargo.toml" + "crates/gpui/Cargo.toml" + "crates/gpui_wgpu/Cargo.toml" + "crates/gpui_tokio/Cargo.toml" + "crates/gpui_platform/Cargo.toml" + ] + + let dry_flag = if $dry_run { ["--dry-run"] } else { [] } + + for manifest in $crates { + let name = ($manifest | path dirname | path basename) + print $"\n📦 Publishing ($name)..." + run-external "cargo" "publish" "--manifest-path" $manifest "--allow-dirty" ...$dry_flag + if $env.LAST_EXIT_CODE != 0 { + error make {msg: $"Failed to publish ($name)"} + } + } + + print "\n✅ All crates published!" + + +[doc('Generate project documentation')] +[group('docs')] +doc *flags: + @echo "📚 Generating documentation..." + cargo doc --workspace --no-deps {{ flags }} + +[doc('Generate and open project documentation in browser')] +[group('docs')] +doc-open: + @echo "📚 Opening documentation in browser..." + cargo doc --workspace --no-deps --open + + +[doc('Run a specific example by name')] +[group('development')] +example name: + @echo "▶️ Running example {{ name }}..." + cargo run --example {{ name }} + +alias b := build +alias br := build-release +alias c := check +alias t := test +alias td := test-doc +alias f := fmt +alias fc := fmt-check +alias clp := clippy +alias clf := clippy-fix +alias l := lint +alias d := doc +alias up := update +alias cl := clean diff --git a/tooling/tests/action_macros.rs b/tooling/tests/action_macros.rs deleted file mode 100644 index 66ef6fba2c..0000000000 --- a/tooling/tests/action_macros.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{Action, actions}; -use gpui_macros::register_action; -use schemars::JsonSchema; -use serde::Deserialize; - -#[test] -fn test_action_macros() { - actions!( - test_only, - [ - SomeAction, - /// Documented action - SomeActionWithDocs, - ] - ); - - #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] - #[action(namespace = test_only)] - #[serde(deny_unknown_fields)] - struct AnotherAction; - - #[derive(PartialEq, Clone, gpui::private::serde::Deserialize)] - #[serde(deny_unknown_fields)] - struct RegisterableAction {} - - register_action!(RegisterableAction); - - impl gpui::Action for RegisterableAction { - fn boxed_clone(&self) -> Box { - unimplemented!() - } - - fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { - unimplemented!() - } - - fn name(&self) -> &'static str { - unimplemented!() - } - - fn name_for_type() -> &'static str - where - Self: Sized, - { - unimplemented!() - } - - fn build(_value: serde_json::Value) -> anyhow::Result> - where - Self: Sized, - { - unimplemented!() - } - } -} diff --git a/tooling/xtask/Cargo.toml b/tooling/xtask/Cargo.toml deleted file mode 100644 index 21090d1304..0000000000 --- a/tooling/xtask/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "xtask" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -annotate-snippets = "0.12.1" -anyhow.workspace = true -backtrace.workspace = true -cargo_metadata.workspace = true -cargo_toml.workspace = true -clap = { workspace = true, features = ["derive"] } -toml.workspace = true -indoc.workspace = true -indexmap.workspace = true -itertools.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_yaml = "0.9.34" -strum.workspace = true -toml_edit.workspace = true -gh-workflow.workspace = true diff --git a/tooling/xtask/LICENSE-GPL b/tooling/xtask/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/tooling/xtask/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/tooling/xtask/src/main.rs b/tooling/xtask/src/main.rs deleted file mode 100644 index 05afe3c766..0000000000 --- a/tooling/xtask/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -mod tasks; -mod workspace; - -use anyhow::Result; -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "cargo xtask")] -struct Args { - #[command(subcommand)] - command: CliCommand, -} - -#[derive(Subcommand)] -enum CliCommand { - /// Runs `cargo clippy`. - Clippy(tasks::clippy::ClippyArgs), - Licenses(tasks::licenses::LicensesArgs), - /// Checks that packages conform to a set of standards. - PackageConformity(tasks::package_conformity::PackageConformityArgs), - /// Publishes GPUI and its dependencies to crates.io. - PublishGpui(tasks::publish_gpui::PublishGpuiArgs), - /// Builds GPUI web examples and serves them. - WebExamples(tasks::web_examples::WebExamplesArgs), - Workflows(tasks::workflows::GenerateWorkflowArgs), - CheckWorkflows(tasks::workflow_checks::WorkflowValidationArgs), -} - -fn main() -> Result<()> { - let args = Args::parse(); - - match args.command { - CliCommand::Clippy(args) => tasks::clippy::run_clippy(args), - CliCommand::Licenses(args) => tasks::licenses::run_licenses(args), - CliCommand::PackageConformity(args) => { - tasks::package_conformity::run_package_conformity(args) - } - CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args), - CliCommand::WebExamples(args) => tasks::web_examples::run_web_examples(args), - CliCommand::Workflows(args) => tasks::workflows::run_workflows(args), - CliCommand::CheckWorkflows(args) => tasks::workflow_checks::validate(args), - } -} diff --git a/tooling/xtask/src/tasks.rs b/tooling/xtask/src/tasks.rs deleted file mode 100644 index 80f504fa03..0000000000 --- a/tooling/xtask/src/tasks.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod clippy; -pub mod licenses; -pub mod package_conformity; -pub mod publish_gpui; -pub mod web_examples; -pub mod workflow_checks; -pub mod workflows; diff --git a/tooling/xtask/src/tasks/clippy.rs b/tooling/xtask/src/tasks/clippy.rs deleted file mode 100644 index 517223ceb8..0000000000 --- a/tooling/xtask/src/tasks/clippy.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::process::Command; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct ClippyArgs { - /// Automatically apply lint suggestions (`clippy --fix`). - #[arg(long)] - fix: bool, - - /// The package to run Clippy against (`cargo -p clippy`). - #[arg(long, short)] - package: Option, -} - -pub fn run_clippy(args: ClippyArgs) -> Result<()> { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut clippy_command = Command::new(&cargo); - clippy_command.arg("clippy"); - - if let Some(package) = args.package.as_ref() { - clippy_command.args(["--package", package]); - } else { - clippy_command.arg("--workspace"); - } - - clippy_command - .arg("--release") - .arg("--all-targets") - .arg("--all-features"); - - if args.fix { - clippy_command.arg("--fix"); - } - - clippy_command.arg("--"); - - // Deny all warnings. - clippy_command.args(["--deny", "warnings"]); - - eprintln!( - "running: {cargo} {}", - clippy_command - .get_args() - .map(|arg| arg.to_str().unwrap()) - .collect::>() - .join(" ") - ); - - let exit_status = clippy_command - .spawn() - .context("failed to spawn child process")? - .wait() - .context("failed to wait for child process")?; - - if !exit_status.success() { - bail!("clippy failed: {}", exit_status); - } - - Ok(()) -} diff --git a/tooling/xtask/src/tasks/licenses.rs b/tooling/xtask/src/tasks/licenses.rs deleted file mode 100644 index 449c774d45..0000000000 --- a/tooling/xtask/src/tasks/licenses.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::{Context as _, Result}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct LicensesArgs {} - -pub fn run_licenses(_args: LicensesArgs) -> Result<()> { - const LICENSE_FILES: &[&str] = &["LICENSE-APACHE", "LICENSE-GPL", "LICENSE-AGPL"]; - - let workspace = load_workspace()?; - - for package in workspace.workspace_packages() { - let crate_dir = package - .manifest_path - .parent() - .with_context(|| format!("no crate directory for {}", package.name))?; - - if let Some(license_file) = first_license_file(crate_dir, LICENSE_FILES) { - if !license_file.is_symlink() { - println!("{} is not a symlink", license_file.display()); - } - - continue; - } - - println!("Missing license: {}", package.name); - } - - Ok(()) -} - -fn first_license_file(path: impl AsRef, license_files: &[&str]) -> Option { - for license_file in license_files { - let path_to_license = path.as_ref().join(license_file); - if path_to_license.exists() { - return Some(path_to_license); - } - } - - None -} diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs deleted file mode 100644 index e1fd15112f..0000000000 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; - -use anyhow::{Context as _, Result}; -use cargo_toml::{Dependency, Manifest}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct PackageConformityArgs {} - -pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { - let workspace = load_workspace()?; - - let mut non_workspace_dependencies = BTreeMap::new(); - - for package in workspace.workspace_packages() { - let is_extension = package - .manifest_path - .parent() - .and_then(|parent| parent.parent()) - .is_some_and(|grandparent_dir| grandparent_dir.ends_with("extensions")); - - let cargo_toml = read_cargo_toml(&package.manifest_path)?; - - let is_using_workspace_lints = cargo_toml.lints.is_some_and(|lints| lints.workspace); - if !is_using_workspace_lints { - eprintln!( - "{package:?} is not using workspace lints", - package = package.name - ); - } - - // Extensions should not use workspace dependencies. - if is_extension || package.name == "zed_extension_api" { - continue; - } - - for dependencies in [ - &cargo_toml.dependencies, - &cargo_toml.dev_dependencies, - &cargo_toml.build_dependencies, - ] { - for (name, dependency) in dependencies { - if let Dependency::Inherited(_) = dependency { - continue; - } - - non_workspace_dependencies - .entry(name.to_owned()) - .or_insert_with(Vec::new) - .push(package.name.clone()); - } - } - } - - for (dependency, packages) in non_workspace_dependencies { - eprintln!( - "{dependency} is being used as a non-workspace dependency: {}", - packages.join(", ") - ); - } - - Ok(()) -} - -/// Returns the contents of the `Cargo.toml` file at the given path. -fn read_cargo_toml(path: impl AsRef) -> Result { - let path = path.as_ref(); - let cargo_toml_bytes = fs::read(path)?; - Manifest::from_slice(&cargo_toml_bytes) - .with_context(|| format!("reading Cargo.toml at {path:?}")) -} diff --git a/tooling/xtask/src/tasks/publish_gpui.rs b/tooling/xtask/src/tasks/publish_gpui.rs deleted file mode 100644 index 2740f75a48..0000000000 --- a/tooling/xtask/src/tasks/publish_gpui.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::io::{self, Write}; -use std::process::{Command, Output, Stdio}; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct PublishGpuiArgs { - /// Perform a dry-run and wait for user confirmation before each publish - #[arg(long)] - dry_run: bool, - - /// Skip to a specific package (by package name or crate name) and start from there - #[arg(long)] - skip_to: Option, -} - -pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> { - println!( - "Starting GPUI publish process{}...", - if args.dry_run { " (with dry-run)" } else { "" } - ); - - let start_time = std::time::Instant::now(); - check_workspace_root()?; - - if args.skip_to.is_none() { - check_git_clean()?; - } else { - println!("Skipping git clean check due to --skip-to flag"); - } - - let version = read_gpui_version()?; - println!("Updating GPUI to version: {}", version); - publish_dependencies(&version, args.dry_run, args.skip_to.as_deref())?; - publish_gpui(&version, args.dry_run)?; - println!("GPUI published in {}s", start_time.elapsed().as_secs_f32()); - Ok(()) -} - -fn read_gpui_version() -> Result { - let gpui_cargo_toml_path = "crates/gpui/Cargo.toml"; - let contents = std::fs::read_to_string(gpui_cargo_toml_path) - .context("Failed to read crates/gpui/Cargo.toml")?; - - let cargo_toml: toml::Value = - toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?; - - let version = cargo_toml - .get("package") - .and_then(|p| p.get("version")) - .and_then(|v| v.as_str()) - .context("Failed to find version in crates/gpui/Cargo.toml")?; - - Ok(version.to_string()) -} - -fn publish_dependencies(new_version: &str, dry_run: bool, skip_to: Option<&str>) -> Result<()> { - let gpui_dependencies = vec![ - ("collections", "gpui_collections", "crates"), - ("perf", "gpui_perf", "tooling"), - ("util_macros", "gpui_util_macros", "crates"), - ("util", "gpui_util", "crates"), - ("gpui_macros", "gpui-macros", "crates"), - ("http_client", "gpui_http_client", "crates"), - ( - "derive_refineable", - "gpui_derive_refineable", - "crates/refineable", - ), - ("refineable", "gpui_refineable", "crates"), - ("semantic_version", "gpui_semantic_version", "crates"), - ("sum_tree", "gpui_sum_tree", "crates"), - ("media", "gpui_media", "crates"), - ]; - - let mut should_skip = skip_to.is_some(); - let skip_target = skip_to.unwrap_or(""); - - for (package_name, crate_name, package_dir) in gpui_dependencies { - if should_skip { - if package_name == skip_target || crate_name == skip_target { - println!("Found skip target: {} ({})", crate_name, package_name); - should_skip = false; - } else { - println!("Skipping: {} ({})", crate_name, package_name); - continue; - } - } - - println!( - "Publishing dependency: {} (package: {})", - crate_name, package_name - ); - - update_crate_cargo_toml(package_name, crate_name, package_dir, new_version)?; - update_workspace_dependency_version(package_name, crate_name, new_version)?; - publish_crate(crate_name, dry_run)?; - } - - if should_skip { - bail!( - "Could not find package or crate named '{}' to skip to", - skip_target - ); - } - - Ok(()) -} - -fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> { - update_crate_cargo_toml("gpui", "gpui", "crates", new_version)?; - - publish_crate("gpui", dry_run)?; - - Ok(()) -} - -fn update_crate_cargo_toml( - package_name: &str, - crate_name: &str, - package_dir: &str, - new_version: &str, -) -> Result<()> { - let cargo_toml_path = format!("{}/{}/Cargo.toml", package_dir, package_name); - let contents = std::fs::read_to_string(&cargo_toml_path) - .context(format!("Failed to read {}", cargo_toml_path))?; - - let updated = update_crate_package_fields(&contents, crate_name, new_version)?; - - std::fs::write(&cargo_toml_path, updated) - .context(format!("Failed to write {}", cargo_toml_path))?; - - Ok(()) -} - -fn update_crate_package_fields( - toml_contents: &str, - crate_name: &str, - new_version: &str, -) -> Result { - let mut doc = toml_contents - .parse::() - .context("Failed to parse TOML")?; - - let package = doc - .get_mut("package") - .and_then(|p| p.as_table_like_mut()) - .context("Failed to find [package] section")?; - - package.insert("name", toml_edit::value(crate_name)); - package.insert("version", toml_edit::value(new_version)); - package.insert("publish", toml_edit::value(true)); - - Ok(doc.to_string()) -} - -fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> { - let publish_crate_impl = |crate_name, dry_run| { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut command = Command::new(&cargo); - command - .arg("publish") - .arg("--allow-dirty") - .args(["-p", crate_name]); - - if dry_run { - command.arg("--dry-run"); - } - - run_command(&mut command)?; - - anyhow::Ok(()) - }; - - if dry_run { - publish_crate_impl(crate_name, true)?; - - print!("Press Enter to publish for real (or ctrl-c to abort)..."); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - } - - publish_crate_impl(crate_name, false)?; - - Ok(()) -} - -fn update_workspace_dependency_version( - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let workspace_cargo_toml_path = "Cargo.toml"; - let contents = std::fs::read_to_string(workspace_cargo_toml_path) - .context("Failed to read workspace Cargo.toml")?; - - let mut doc = contents - .parse::() - .context("Failed to parse TOML")?; - - update_dependency_version_in_doc(&mut doc, package_name, crate_name, new_version)?; - update_profile_override_in_doc(&mut doc, package_name, crate_name)?; - - std::fs::write(workspace_cargo_toml_path, doc.to_string()) - .context("Failed to write workspace Cargo.toml")?; - - Ok(()) -} - -fn update_dependency_version_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let dependency = doc - .get_mut("workspace") - .and_then(|w| w.get_mut("dependencies")) - .and_then(|d| d.get_mut(package_name)) - .context(format!( - "Failed to find {} in workspace dependencies", - package_name - ))?; - - if let Some(dep_table) = dependency.as_table_like_mut() { - dep_table.insert("version", toml_edit::value(new_version)); - dep_table.insert("package", toml_edit::value(crate_name)); - } else { - bail!("{} is not a table in workspace dependencies", package_name); - } - - Ok(()) -} - -fn update_profile_override_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, -) -> Result<()> { - if let Some(profile_dev_package) = doc - .get_mut("profile") - .and_then(|p| p.get_mut("dev")) - .and_then(|d| d.get_mut("package")) - .and_then(|p| p.as_table_like_mut()) - { - if let Some(old_entry) = profile_dev_package.get(package_name) { - let old_entry_clone = old_entry.clone(); - profile_dev_package.remove(package_name); - profile_dev_package.insert(crate_name, old_entry_clone); - } - } - - Ok(()) -} - -fn check_workspace_root() -> Result<()> { - let cwd = std::env::current_dir().context("Failed to get current directory")?; - - // Check if Cargo.toml exists in the current directory - let cargo_toml_path = cwd.join("Cargo.toml"); - if !cargo_toml_path.exists() { - bail!( - "Cargo.toml not found in current directory. Please run this command from the workspace root." - ); - } - - // Check if it's a workspace by looking for [workspace] section - let contents = - std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?; - - if !contents.contains("[workspace]") { - bail!( - "Current directory does not appear to be a workspace root. Please run this command from the workspace root." - ); - } - - Ok(()) -} - -fn check_git_clean() -> Result<()> { - let output = run_command( - Command::new("git") - .args(["status", "--porcelain"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()), - )?; - - if !output.status.success() { - bail!("git status command failed"); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.trim().is_empty() { - bail!( - "Working directory is not clean. Please commit or stash your changes before publishing." - ); - } - - Ok(()) -} - -fn run_command(command: &mut Command) -> Result { - let command_str = { - let program = command.get_program().to_string_lossy(); - let args = command - .get_args() - .map(|arg| arg.to_string_lossy()) - .collect::>() - .join(" "); - - if args.is_empty() { - program.to_string() - } else { - format!("{} {}", program, args) - } - }; - eprintln!("+ {}", command_str); - - let output = command - .spawn() - .context("failed to spawn child process")? - .wait_with_output() - .context("failed to wait for child process")?; - - if !output.status.success() { - bail!("Command failed with status {}", output.status); - } - - Ok(output) -} - -#[cfg(test)] -mod tests { - use indoc::indoc; - - use super::*; - - #[test] - fn test_update_dependency_version_in_toml() { - let input = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_dependency_version_in_doc(&mut doc, "collections", "gpui_collections", "0.2.0") - .unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" , version = "0.2.0", package = "gpui_collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_crate_package_fields() { - let input = indoc! {r#" - [package] - name = "collections" - version = "0.1.0" - edition = "2021" - publish = false - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - let result = update_crate_package_fields(input, "gpui_collections", "0.2.0").unwrap(); - - let output = indoc! {r#" - [package] - name = "gpui_collections" - version = "0.2.0" - edition = "2021" - publish = true - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_profile_override_in_toml() { - let input = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - collections = { codegen-units = 256 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_profile_override_in_doc(&mut doc, "collections", "gpui_collections").unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - gpui_collections = { codegen-units = 256 } - "#}; - - assert_eq!(result, output); - } -} diff --git a/tooling/xtask/src/tasks/web_examples.rs b/tooling/xtask/src/tasks/web_examples.rs deleted file mode 100644 index 5b8e0fdd61..0000000000 --- a/tooling/xtask/src/tasks/web_examples.rs +++ /dev/null @@ -1,338 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] - -use std::io::Write; -use std::path::Path; -use std::process::Command; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct WebExamplesArgs { - #[arg(long)] - pub release: bool, - #[arg(long, default_value = "8080")] - pub port: u16, - #[arg(long)] - pub no_serve: bool, -} - -fn check_program(binary: &str, install_hint: &str) -> Result<()> { - match Command::new(binary).arg("--version").output() { - Ok(output) if output.status.success() => Ok(()), - _ => bail!("`{binary}` not found. Install with: {install_hint}"), - } -} - -fn discover_examples() -> Result> { - let examples_dir = Path::new("crates/gpui/examples"); - let mut names = Vec::new(); - - for entry in std::fs::read_dir(examples_dir).context("failed to read crates/gpui/examples")? { - let path = entry?.path(); - if path.extension().and_then(|e| e.to_str()) == Some("rs") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - names.push(stem.to_string()); - } - } - } - - if names.is_empty() { - bail!("no examples found in crates/gpui/examples"); - } - - names.sort(); - Ok(names) -} - -pub fn run_web_examples(args: WebExamplesArgs) -> Result<()> { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let profile = if args.release { "release" } else { "debug" }; - let out_dir = "target/web-examples"; - - check_program("wasm-bindgen", "cargo install wasm-bindgen-cli")?; - - let examples = discover_examples()?; - eprintln!( - "Building {} example(s) for wasm32-unknown-unknown ({profile})...\n", - examples.len() - ); - - std::fs::create_dir_all(out_dir).context("failed to create output directory")?; - - eprintln!("Building all examples..."); - - let mut cmd = Command::new(&cargo); - cmd.args([ - "build", - "--target", - "wasm32-unknown-unknown", - "-p", - "gpui", - "--keep-going", - ]); - // 🙈 - cmd.env("RUSTC_BOOTSTRAP", "1"); - for name in &examples { - cmd.args(["--example", name]); - } - if args.release { - cmd.arg("--release"); - } - - let _ = cmd.status().context("failed to run cargo build")?; - - // Run wasm-bindgen on each .wasm that was produced. - let mut succeeded: Vec = Vec::new(); - let mut failed: Vec = Vec::new(); - - for name in &examples { - let wasm_path = format!("target/wasm32-unknown-unknown/{profile}/examples/{name}.wasm"); - if !Path::new(&wasm_path).exists() { - eprintln!("[{name}] SKIPPED (build failed)"); - failed.push(name.clone()); - continue; - } - - eprintln!("[{name}] Running wasm-bindgen..."); - - let example_dir = format!("{out_dir}/{name}"); - std::fs::create_dir_all(&example_dir) - .with_context(|| format!("failed to create {example_dir}"))?; - - let status = Command::new("wasm-bindgen") - .args([ - &wasm_path, - "--target", - "web", - "--no-typescript", - "--out-dir", - &example_dir, - "--out-name", - name, - ]) - // 🙈 - .env("RUSTC_BOOTSTRAP", "1") - .status() - .context("failed to run wasm-bindgen")?; - if !status.success() { - eprintln!("[{name}] SKIPPED (wasm-bindgen failed)"); - failed.push(name.clone()); - continue; - } - - // Write per-example index.html. - let html_path = format!("{example_dir}/index.html"); - std::fs::File::create(&html_path) - .and_then(|mut file| file.write_all(make_example_html(name).as_bytes())) - .with_context(|| format!("failed to write {html_path}"))?; - - eprintln!("[{name}] OK"); - succeeded.push(name.clone()); - } - - if succeeded.is_empty() { - bail!("all {} examples failed to build", examples.len()); - } - - let example_names: Vec<&str> = succeeded.iter().map(|s| s.as_str()).collect(); - let index_path = format!("{out_dir}/index.html"); - std::fs::File::create(&index_path) - .and_then(|mut file| file.write_all(make_gallery_html(&example_names).as_bytes())) - .context("failed to write index.html")?; - - if args.no_serve { - return Ok(()); - } - - // Serve with COEP/COOP headers required for WebGPU / SharedArrayBuffer. - eprintln!("Serving on http://127.0.0.1:{}...", args.port); - - let server_script = format!( - r#" -import http.server -class Handler(http.server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory="{out_dir}", **kwargs) - def end_headers(self): - self.send_header("Cross-Origin-Embedder-Policy", "require-corp") - self.send_header("Cross-Origin-Opener-Policy", "same-origin") - super().end_headers() -http.server.HTTPServer(("127.0.0.1", {port}), Handler).serve_forever() -"#, - port = args.port, - ); - - let status = Command::new("python3") - .args(["-c", &server_script]) - .status() - .context("failed to run python3 http server (is python3 installed?)")?; - if !status.success() { - bail!("python3 http server exited with: {status}"); - } - - Ok(()) -} - -fn make_example_html(name: &str) -> String { - format!( - r#" - - - - - GPUI Web: {name} - - - -
Loading {name}…
- - - -"# - ) -} - -fn make_gallery_html(examples: &[&str]) -> String { - let mut buttons = String::new(); - for name in examples { - buttons.push_str(&format!( - " \n" - )); - } - - let first = examples.first().copied().unwrap_or("hello_web"); - - format!( - r##" - - - - - GPUI Web Examples - - - -
- -
-
- {first} - Open in new tab ↗ -
- -
-
- - - -"##, - count = examples.len(), - ) -} diff --git a/tooling/xtask/src/tasks/workflow_checks.rs b/tooling/xtask/src/tasks/workflow_checks.rs deleted file mode 100644 index d6be029932..0000000000 --- a/tooling/xtask/src/tasks/workflow_checks.rs +++ /dev/null @@ -1,118 +0,0 @@ -mod check_run_patterns; - -use std::{fs, path::PathBuf}; - -use annotate_snippets::Renderer; -use anyhow::{Result, anyhow}; -use clap::Parser; -use itertools::{Either, Itertools}; -use serde_yaml::Value; -use strum::IntoEnumIterator; - -use crate::tasks::{ - workflow_checks::check_run_patterns::{ - RunValidationError, WorkflowFile, WorkflowValidationError, - }, - workflows::WorkflowType, -}; - -pub use check_run_patterns::validate_run_command; - -#[derive(Default, Parser)] -pub struct WorkflowValidationArgs {} - -pub fn validate(_: WorkflowValidationArgs) -> Result<()> { - let (parsing_errors, file_errors): (Vec<_>, Vec<_>) = get_all_workflow_files() - .map(check_workflow) - .flat_map(Result::err) - .partition_map(|error| match error { - WorkflowError::ParseError(error) => Either::Left(error), - WorkflowError::ValidationError(error) => Either::Right(error), - }); - - if !parsing_errors.is_empty() { - Err(anyhow!( - "Failed to read or parse some workflow files: {}", - parsing_errors.into_iter().join("\n") - )) - } else if !file_errors.is_empty() { - let errors: Vec<_> = file_errors - .iter() - .map(|error| error.annotation_group()) - .collect(); - - let renderer = - Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Ascii); - println!("{}", renderer.render(errors.as_slice())); - - Err(anyhow!("Workflow checks failed!")) - } else { - Ok(()) - } -} - -enum WorkflowError { - ParseError(anyhow::Error), - ValidationError(Box), -} - -fn get_all_workflow_files() -> impl Iterator { - WorkflowType::iter() - .map(|workflow_type| workflow_type.folder_path()) - .flat_map(|folder_path| { - fs::read_dir(folder_path).into_iter().flat_map(|entries| { - entries - .flat_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| { - path.extension() - .is_some_and(|ext| ext == "yaml" || ext == "yml") - }) - }) - }) -} - -fn check_workflow(workflow_file_path: PathBuf) -> Result<(), WorkflowError> { - fn collect_errors( - iter: impl Iterator>>, - ) -> Result<(), Vec> { - Some(iter.flat_map(Result::err).flatten().collect::>()) - .filter(|errors| !errors.is_empty()) - .map_or(Ok(()), Err) - } - - fn check_recursive(key: &Value, value: &Value) -> Result<(), Vec> { - match value { - Value::Mapping(mapping) => collect_errors( - mapping - .into_iter() - .map(|(key, value)| check_recursive(key, value)), - ), - Value::Sequence(sequence) => collect_errors( - sequence - .into_iter() - .map(|value| check_recursive(key, value)), - ), - Value::String(string) => check_string(key, string).map_err(|error| vec![error]), - Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Ok(()), - } - } - - let file_content = - WorkflowFile::load(&workflow_file_path).map_err(WorkflowError::ParseError)?; - - check_recursive(&Value::Null, &file_content.parsed_content).map_err(|errors| { - WorkflowError::ValidationError(Box::new(WorkflowValidationError::new( - errors, - file_content, - workflow_file_path, - ))) - }) -} - -fn check_string(key: &Value, value: &str) -> Result<(), RunValidationError> { - match key { - Value::String(key) if key == "run" => validate_run_command(value), - _ => Ok(()), - } -} diff --git a/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs b/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs deleted file mode 100644 index 50c435d033..0000000000 --- a/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs +++ /dev/null @@ -1,124 +0,0 @@ -use annotate_snippets::{AnnotationKind, Group, Level, Snippet}; -use anyhow::{Result, anyhow}; -use regex::Regex; -use serde_yaml::Value; -use std::{ - collections::HashMap, - fs, - ops::Range, - path::{Path, PathBuf}, - sync::LazyLock, -}; - -static GITHUB_INPUT_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r#"\$\{\{[[:blank:]]*([[:alnum:]]|[[:punct:]])+?[[:blank:]]*\}\}"#) - .expect("Should compile") -}); - -pub struct WorkflowFile { - raw_content: String, - pub parsed_content: Value, -} - -impl WorkflowFile { - pub fn load(workflow_file_path: &Path) -> Result { - fs::read_to_string(workflow_file_path) - .map_err(|_| { - anyhow!( - "Could not read workflow file at {}", - workflow_file_path.display() - ) - }) - .and_then(|file_content| { - serde_yaml::from_str(&file_content) - .map(|parsed_content| Self { - raw_content: file_content, - parsed_content, - }) - .map_err(|e| anyhow!("Failed to parse workflow file: {e:?}")) - }) - } -} - -pub struct WorkflowValidationError { - file_path: PathBuf, - contents: WorkflowFile, - errors: Vec, -} - -impl WorkflowValidationError { - pub fn new( - errors: Vec, - contents: WorkflowFile, - file_path: PathBuf, - ) -> Self { - Self { - file_path, - contents, - errors, - } - } - - pub fn annotation_group<'a>(&'a self) -> Group<'a> { - let raw_content = &self.contents.raw_content; - let mut identical_lines = HashMap::new(); - - let ranges = self - .errors - .iter() - .flat_map(|error| error.found_injection_patterns.iter()) - .map(|(line, pattern_range)| { - let initial_offset = identical_lines - .get(&(line.as_str(), pattern_range.start)) - .copied() - .unwrap_or_default(); - - let line_start = raw_content[initial_offset..] - .find(line.as_str()) - .map(|offset| offset + initial_offset) - .unwrap_or_default(); - - let pattern_start = line_start + pattern_range.start; - let pattern_end = pattern_start + pattern_range.len(); - - identical_lines.insert((line.as_str(), pattern_range.start), pattern_end); - - pattern_start..pattern_end - }); - - Level::ERROR - .primary_title("Found GitHub input injection in run command") - .element( - Snippet::source(&self.contents.raw_content) - .path(self.file_path.display().to_string()) - .annotations(ranges.map(|range| { - AnnotationKind::Primary - .span(range) - .label("This should be passed via an environment variable") - })), - ) - } -} - -pub struct RunValidationError { - found_injection_patterns: Vec<(String, Range)>, -} - -pub fn validate_run_command(command: &str) -> Result<(), RunValidationError> { - let patterns: Vec<_> = command - .lines() - .flat_map(move |line| { - GITHUB_INPUT_PATTERN - .find_iter(line) - .map(|m| (line.to_owned(), m.range())) - }) - .collect(); - - if patterns.is_empty() { - Ok(()) - } else { - Err(RunValidationError { - found_injection_patterns: patterns, - }) - } -} diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs deleted file mode 100644 index 35f053f466..0000000000 --- a/tooling/xtask/src/tasks/workflows.rs +++ /dev/null @@ -1,224 +0,0 @@ -use anyhow::{Context, Result}; -use clap::Parser; -use gh_workflow::Workflow; -use std::fs; -use std::path::{Path, PathBuf}; - -use crate::tasks::workflow_checks::{self}; - -mod after_release; -mod autofix_pr; -mod bump_patch_version; -mod cherry_pick; -mod compare_perf; -mod danger; -mod deploy_collab; -mod extension_auto_bump; -mod extension_bump; -mod extension_tests; -mod extension_workflow_rollout; -mod extensions; -mod nix_build; -mod publish_extension_cli; -mod release_nightly; -mod run_bundling; - -mod release; -mod run_agent_evals; -mod run_tests; -mod runners; -mod steps; -mod vars; - -#[derive(Clone)] -pub(crate) struct GitSha(String); - -impl AsRef for GitSha { - fn as_ref(&self) -> &str { - &self.0 - } -} - -#[allow( - clippy::disallowed_methods, - reason = "This runs only in a CLI environment" -)] -fn parse_ref(value: &str) -> Result { - const GIT_SHA_LENGTH: usize = 40; - (value.len() == GIT_SHA_LENGTH) - .then_some(value) - .ok_or_else(|| { - format!( - "Git SHA has wrong length! \ - Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.", - len = value.len() - ) - }) - .and_then(|value| { - let mut tmp = [0; 4]; - value - .chars() - .all(|char| u16::from_str_radix(char.encode_utf8(&mut tmp), 16).is_ok()).then_some(value) - .ok_or_else(|| "Not a valid Git SHA".to_owned()) - }) - .and_then(|sha| { - std::process::Command::new("git") - .args([ - "rev-parse", - "--quiet", - "--verify", - &format!("{sha}^{{commit}}") - ]) - .output() - .map_err(|_| "Failed to spawn Git command to verify SHA".to_owned()) - .and_then(|output| - output - .status.success() - .then_some(sha) - .ok_or_else(|| format!("SHA {sha} is not a valid Git SHA within this repository!"))) - }).map(|sha| GitSha(sha.to_owned())) -} - -#[derive(Parser)] -pub(crate) struct GenerateWorkflowArgs { - #[arg(value_parser = parse_ref)] - /// The Git SHA to use when invoking this - pub(crate) sha: Option, -} - -enum WorkflowSource { - Contextless(fn() -> Workflow), - WithContext(fn(&GenerateWorkflowArgs) -> Workflow), -} - -struct WorkflowFile { - source: WorkflowSource, - r#type: WorkflowType, -} - -impl WorkflowFile { - fn zed(f: fn() -> Workflow) -> WorkflowFile { - WorkflowFile { - source: WorkflowSource::Contextless(f), - r#type: WorkflowType::Zed, - } - } - - fn extension(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile { - WorkflowFile { - source: WorkflowSource::WithContext(f), - r#type: WorkflowType::ExtensionCi, - } - } - - fn extension_shared(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile { - WorkflowFile { - source: WorkflowSource::WithContext(f), - r#type: WorkflowType::ExtensionsShared, - } - } - - fn generate_file(&self, workflow_args: &GenerateWorkflowArgs) -> Result<()> { - let workflow = match &self.source { - WorkflowSource::Contextless(f) => f(), - WorkflowSource::WithContext(f) => f(workflow_args), - }; - let workflow_folder = self.r#type.folder_path(); - - fs::create_dir_all(&workflow_folder).with_context(|| { - format!("Failed to create directory: {}", workflow_folder.display()) - })?; - - let workflow_name = workflow - .name - .as_ref() - .expect("Workflow must have a name at this point"); - let filename = format!( - "{}.yml", - workflow_name.rsplit("::").next().unwrap_or(workflow_name) - ); - - let workflow_path = workflow_folder.join(filename); - - let content = workflow - .to_string() - .map_err(|e| anyhow::anyhow!("{:?}: {:?}", workflow_path, e))?; - - let disclaimer = self.r#type.disclaimer(workflow_name); - - let content = [disclaimer, content].join("\n"); - fs::write(&workflow_path, content).map_err(Into::into) - } -} - -#[derive(PartialEq, Eq, strum::EnumIter)] -pub enum WorkflowType { - /// Workflows living in the Zed repository - Zed, - /// Workflows living in the `zed-extensions/workflows` repository that are - /// required workflows for PRs to the extension organization - ExtensionCi, - /// Workflows living in each of the extensions to perform checks and version - /// bumps until a better, more centralized system for that is in place. - ExtensionsShared, -} - -impl WorkflowType { - fn disclaimer(&self, workflow_name: &str) -> String { - format!( - concat!( - "# Generated from xtask::workflows::{}{}\n", - "# Rebuild with `cargo xtask workflows`.", - ), - workflow_name, - (*self != WorkflowType::Zed) - .then_some(" within the Zed repository.") - .unwrap_or_default(), - ) - } - - pub fn folder_path(&self) -> PathBuf { - match self { - WorkflowType::Zed => PathBuf::from(".github/workflows"), - WorkflowType::ExtensionCi => PathBuf::from("extensions/workflows"), - WorkflowType::ExtensionsShared => PathBuf::from("extensions/workflows/shared"), - } - } -} - -pub fn run_workflows(args: GenerateWorkflowArgs) -> Result<()> { - if !Path::new("crates/zed/").is_dir() { - anyhow::bail!("xtask workflows must be ran from the project root"); - } - - let workflows = [ - WorkflowFile::zed(after_release::after_release), - WorkflowFile::zed(autofix_pr::autofix_pr), - WorkflowFile::zed(bump_patch_version::bump_patch_version), - WorkflowFile::zed(cherry_pick::cherry_pick), - WorkflowFile::zed(compare_perf::compare_perf), - WorkflowFile::zed(danger::danger), - WorkflowFile::zed(deploy_collab::deploy_collab), - WorkflowFile::zed(extension_bump::extension_bump), - WorkflowFile::zed(extension_auto_bump::extension_auto_bump), - WorkflowFile::zed(extension_tests::extension_tests), - WorkflowFile::zed(extension_workflow_rollout::extension_workflow_rollout), - WorkflowFile::zed(publish_extension_cli::publish_extension_cli), - WorkflowFile::zed(release::release), - WorkflowFile::zed(release_nightly::release_nightly), - WorkflowFile::zed(run_agent_evals::run_agent_evals), - WorkflowFile::zed(run_agent_evals::run_cron_unit_evals), - WorkflowFile::zed(run_agent_evals::run_unit_evals), - WorkflowFile::zed(run_bundling::run_bundling), - WorkflowFile::zed(run_tests::run_tests), - /* workflows used for CI/CD in extension repositories */ - WorkflowFile::extension(extensions::run_tests::run_tests), - WorkflowFile::extension_shared(extensions::bump_version::bump_version), - ]; - - for workflow_file in workflows { - workflow_file.generate_file(&args)?; - } - - workflow_checks::validate(Default::default()) -} diff --git a/tooling/xtask/src/tasks/workflows/after_release.rs b/tooling/xtask/src/tasks/workflows/after_release.rs deleted file mode 100644 index 07ff1fba0d..0000000000 --- a/tooling/xtask/src/tasks/workflows/after_release.rs +++ /dev/null @@ -1,185 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - 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 RELEASE_BODY: &str = "${{ github.event.release.body || inputs.body }}"; - -pub fn after_release() -> Workflow { - let tag_name = WorkflowInput::string("tag_name", None); - let prerelease = WorkflowInput::bool("prerelease", None); - let body = WorkflowInput::string("body", Some(String::new())); - - let refresh_zed_dev = rebuild_releases_page(); - 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, - ]); - - named::workflow() - .on(Event::default() - .release(Release::default().types(vec![ReleaseType::Published])) - .workflow_dispatch( - WorkflowDispatch::default() - .add_input(tag_name.name, tag_name.input()) - .add_input(prerelease.name, prerelease.input()) - .add_input(body.name, body.input()), - )) - .add_job(refresh_zed_dev.name, refresh_zed_dev.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) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -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}" - )) - } - - fn redeploy_zed_dev() -> Step { - named::bash("./script/redeploy-vercel").add_env(("VERCEL_TOKEN", vars::VERCEL_TOKEN)) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(refresh_cloud_releases()) - .add_step(checkout_repo()) - .add_step(redeploy_zed_dev()), - ) -} - -fn post_to_discord(deps: &[&NamedJob]) -> NamedJob { - fn get_release_url() -> Step { - named::bash(format!( - 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") - } - - fn get_content() -> Step { - named::uses( - "2428392", - "gh-truncate-string-action", - "b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1 - ) - .id("get-content") - .add_with(( - "stringToTruncate", - format!( - "📣 Zed [{TAG_NAME}](<${{{{ steps.get-release-url.outputs.URL }}}}>) was just released!\n\n{RELEASE_BODY}\n" - ), - )) - .add_with(("maxLength", 2000)) - .add_with(("truncationSymbol", "...")) - } - - fn discord_webhook_action() -> Step { - named::uses( - "tsickert", - "discord-webhook", - "c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0 - ) - .add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES)) - .add_with(("content", "${{ steps.get-content.outputs.string }}")) - } - let job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(get_release_url()) - .add_step(get_content()) - .add_step(discord_webhook_action()); - named::job(job) -} - -fn publish_winget() -> NamedJob { - fn sync_winget_pkgs_fork() -> Step { - named::pwsh(indoc::indoc! {r#" - $headers = @{ - "Authorization" = "Bearer $env:WINGET_TOKEN" - "Accept" = "application/vnd.github+json" - "X-GitHub-Api-Version" = "2022-11-28" - } - $body = @{ branch = "master" } | ConvertTo-Json - $uri = "https://api.github.com/repos/$env:GITHUB_REPOSITORY_OWNER/winget-pkgs/merge-upstream" - try { - Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json" - Write-Host "Successfully synced winget-pkgs fork" - } catch { - Write-Host "Fork sync response: $_" - Write-Host "Continuing anyway - fork may already be up to date" - } - "#}) - .add_env(("WINGET_TOKEN", vars::WINGET_TOKEN)) - } - - fn set_package_name() -> (Step, StepOutput) { - let script = format!( - r#"if ("{IS_PRERELEASE}" -eq "true") {{ - $PACKAGE_NAME = "ZedIndustries.Zed.Preview" -}} else {{ - $PACKAGE_NAME = "ZedIndustries.Zed" -}} - -echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT -"# - ); - let step = named::pwsh(&script).id("set-package-name"); - - let output = StepOutput::new(&step, "PACKAGE_NAME"); - (step, output) - } - - fn winget_releaser(package_name: &StepOutput) -> Step { - named::uses( - "vedantmgoyal9", - "winget-releaser", - "19e706d4c9121098010096f9c495a70a7518b30f", // v2 - ) - .add_with(("identifier", package_name.to_string())) - .add_with(("release-tag", TAG_NAME)) - .add_with(("max-versions-to-keep", 5)) - .add_with(("token", vars::WINGET_TOKEN)) - } - - let (set_package_name, package_name) = set_package_name(); - - named::job( - Job::default() - .runs_on(runners::WINDOWS_DEFAULT) - .add_step(sync_winget_pkgs_fork()) - .add_step(set_package_name) - .add_step(winget_releaser(&package_name)), - ) -} - -fn create_sentry_release() -> NamedJob { - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(checkout_repo()) - .add_step(release::create_sentry_release()); - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/autofix_pr.rs b/tooling/xtask/src/tasks/workflows/autofix_pr.rs deleted file mode 100644 index 2779dc2b01..0000000000 --- a/tooling/xtask/src/tasks/workflows/autofix_pr.rs +++ /dev/null @@ -1,181 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - runners, - steps::{self, FluentBuilder, NamedJob, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -pub fn autofix_pr() -> Workflow { - let pr_number = WorkflowInput::string("pr_number", None); - let run_clippy = WorkflowInput::bool("run_clippy", Some(true)); - let run_autofix = run_autofix(&pr_number, &run_clippy); - let commit_changes = commit_changes(&pr_number, &run_autofix); - named::workflow() - .run_name(format!("autofix PR #{pr_number}")) - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(pr_number.name, pr_number.input()) - .add_input(run_clippy.name, run_clippy.input()), - )) - .concurrency( - Concurrency::new(Expression::new(format!( - "${{{{ github.workflow }}}}-{pr_number}" - ))) - .cancel_in_progress(true), - ) - .add_job(run_autofix.name.clone(), run_autofix.job) - .add_job(commit_changes.name, commit_changes.job) -} - -const PATCH_ARTIFACT_NAME: &str = "autofix-patch"; -const PATCH_FILE_PATH: &str = "autofix.patch"; - -fn upload_patch_artifact() -> Step { - Step::new(format!("upload artifact {}", PATCH_ARTIFACT_NAME)) - .uses( - "actions", - "upload-artifact", - "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 - ) - .add_with(("name", PATCH_ARTIFACT_NAME)) - .add_with(("path", PATCH_FILE_PATH)) - .add_with(("if-no-files-found", "ignore")) - .add_with(("retention-days", "1")) -} - -fn download_patch_artifact() -> Step { - named::uses( - "actions", - "download-artifact", - "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 - ) - .add_with(("name", PATCH_ARTIFACT_NAME)) -} - -fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJob { - fn checkout_pr(pr_number: &WorkflowInput) -> Step { - named::bash(r#"gh pr checkout "$PR_NUMBER""#) - .add_env(("PR_NUMBER", pr_number.to_string())) - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)) - } - - fn install_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "install")) - .add_with(("args", "cargo-machete@0.7.0")) - } - - fn run_cargo_fmt() -> Step { - named::bash("cargo fmt --all") - } - - fn run_cargo_fix() -> Step { - named::bash( - "cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged", - ) - } - - fn run_cargo_machete_fix() -> Step { - named::bash("cargo machete --fix") - } - - fn run_clippy_fix() -> Step { - named::bash( - "cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged", - ) - } - - fn run_prettier_fix() -> Step { - named::bash("./script/prettier --write") - } - - fn create_patch() -> Step { - named::bash(indoc::indoc! {r#" - if git diff --quiet; then - echo "No changes to commit" - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - git diff > autofix.patch - echo "has_changes=true" >> "$GITHUB_OUTPUT" - fi - "#}) - .id("create-patch") - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .outputs([( - "has_changes".to_owned(), - "${{ steps.create-patch.outputs.has_changes }}".to_owned(), - )]) - .add_step(steps::checkout_repo()) - .add_step(checkout_pr(pr_number)) - .add_step(steps::setup_cargo_config(runners::Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::setup_pnpm()) - .add_step(install_cargo_machete().if_condition(Expression::new(run_clippy.to_string()))) - .add_step(run_cargo_fix().if_condition(Expression::new(run_clippy.to_string()))) - .add_step(run_cargo_machete_fix().if_condition(Expression::new(run_clippy.to_string()))) - .add_step(run_clippy_fix().if_condition(Expression::new(run_clippy.to_string()))) - .add_step(run_prettier_fix()) - .add_step(run_cargo_fmt()) - .add_step(create_patch()) - .add_step(upload_patch_artifact()) - .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)), - ) -} - -fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob { - fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step { - named::bash(r#"gh pr checkout "$PR_NUMBER""#) - .add_env(("PR_NUMBER", pr_number.to_string())) - .add_env(("GITHUB_TOKEN", token)) - } - - fn apply_patch() -> Step { - named::bash("git apply autofix.patch") - } - - fn commit_and_push(token: &StepOutput) -> Step { - named::bash(indoc::indoc! {r#" - git commit -am "Autofix" - git push - "#}) - .add_env(("GIT_COMMITTER_NAME", "Zed Zippy")) - .add_env(( - "GIT_COMMITTER_EMAIL", - "234243425+zed-zippy[bot]@users.noreply.github.com", - )) - .add_env(("GIT_AUTHOR_NAME", "Zed Zippy")) - .add_env(( - "GIT_AUTHOR_EMAIL", - "234243425+zed-zippy[bot]@users.noreply.github.com", - )) - .add_env(("GITHUB_TOKEN", token)) - } - - let (authenticate, token) = steps::authenticate_as_zippy(); - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .needs(vec![autofix_job.name.clone()]) - .cond(Expression::new(format!( - "needs.{}.outputs.has_changes == 'true'", - autofix_job.name - ))) - .add_step(authenticate) - .add_step(steps::checkout_repo().with_token(&token)) - .add_step(checkout_pr(pr_number, &token)) - .add_step(download_patch_artifact()) - .add_step(apply_patch()) - .add_step(commit_and_push(&token)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/bump_patch_version.rs b/tooling/xtask/src/tasks/workflows/bump_patch_version.rs deleted file mode 100644 index 5ef149be29..0000000000 --- a/tooling/xtask/src/tasks/workflows/bump_patch_version.rs +++ /dev/null @@ -1,78 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - runners, - steps::{self, CheckoutStep, named}, - vars::{StepOutput, WorkflowInput}, -}; - -pub fn bump_patch_version() -> Workflow { - let branch = WorkflowInput::string("branch", None).description("Branch name to run on"); - let bump_patch_version_job = run_bump_patch_version(&branch); - named::workflow() - .on(Event::default() - .workflow_dispatch(WorkflowDispatch::default().add_input(branch.name, branch.input()))) - .concurrency( - Concurrency::new(Expression::new(format!( - "${{{{ github.workflow }}}}-{branch}" - ))) - .cancel_in_progress(true), - ) - .add_job(bump_patch_version_job.name, bump_patch_version_job.job) -} - -fn run_bump_patch_version(branch: &WorkflowInput) -> steps::NamedJob { - fn checkout_branch(branch: &WorkflowInput, token: &StepOutput) -> CheckoutStep { - steps::checkout_repo() - .with_token(token) - .with_ref(branch.to_string()) - } - - fn bump_patch_version(token: &StepOutput) -> Step { - named::bash(indoc::indoc! {r#" - channel="$(cat crates/zed/RELEASE_CHANNEL)" - - tag_suffix="" - case $channel in - stable) - ;; - preview) - tag_suffix="-pre" - ;; - *) - echo "this must be run on either of stable|preview release branches" >&2 - exit 1 - ;; - esac - which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version" - output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')" - git commit -am "Bump to $output for @$GITHUB_ACTOR" - git tag "v${output}${tag_suffix}" - git push origin HEAD "v${output}${tag_suffix}" - "#}) - .add_env(("GIT_COMMITTER_NAME", "Zed Zippy")) - .add_env(( - "GIT_COMMITTER_EMAIL", - "234243425+zed-zippy[bot]@users.noreply.github.com", - )) - .add_env(("GIT_AUTHOR_NAME", "Zed Zippy")) - .add_env(( - "GIT_AUTHOR_EMAIL", - "234243425+zed-zippy[bot]@users.noreply.github.com", - )) - .add_env(("GITHUB_TOKEN", token)) - } - - let (authenticate, token) = steps::authenticate_as_zippy(); - - named::job( - Job::default() - .cond(Expression::new( - "github.repository_owner == 'zed-industries'", - )) - .runs_on(runners::LINUX_XL) - .add_step(authenticate) - .add_step(checkout_branch(branch, &token)) - .add_step(bump_patch_version(&token)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/cherry_pick.rs b/tooling/xtask/src/tasks/workflows/cherry_pick.rs deleted file mode 100644 index 5680bf6b23..0000000000 --- a/tooling/xtask/src/tasks/workflows/cherry_pick.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::{StepOutput, WorkflowInput}, -}; - -pub fn cherry_pick() -> Workflow { - let branch = WorkflowInput::string("branch", None); - let commit = WorkflowInput::string("commit", None); - let channel = WorkflowInput::string("channel", None); - let pr_number = WorkflowInput::string("pr_number", None); - let cherry_pick = run_cherry_pick(&branch, &commit, &channel); - named::workflow() - .run_name(format!("cherry_pick to {channel} #{pr_number}")) - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(commit.name, commit.input()) - .add_input(branch.name, branch.input()) - .add_input(channel.name, channel.input()) - .add_input(pr_number.name, pr_number.input()), - )) - .add_job(cherry_pick.name, cherry_pick.job) -} - -fn run_cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, -) -> NamedJob { - fn cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, - token: &StepOutput, - ) -> Step { - named::bash(r#"./script/cherry-pick "$BRANCH" "$COMMIT" "$CHANNEL""#) - .add_env(("BRANCH", branch.to_string())) - .add_env(("COMMIT", commit.to_string())) - .add_env(("CHANNEL", channel.to_string())) - .add_env(("GIT_COMMITTER_NAME", "Zed Zippy")) - .add_env(("GIT_COMMITTER_EMAIL", "hi@zed.dev")) - .add_env(("GITHUB_TOKEN", token)) - } - - let (authenticate, token) = steps::authenticate_as_zippy(); - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(authenticate) - .add_step(cherry_pick(branch, commit, channel, &token)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/compare_perf.rs b/tooling/xtask/src/tasks/workflows/compare_perf.rs deleted file mode 100644 index 74a1fbdc38..0000000000 --- a/tooling/xtask/src/tasks/workflows/compare_perf.rs +++ /dev/null @@ -1,69 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::run_bundling::upload_artifact; -use crate::tasks::workflows::steps::FluentBuilder; -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::WorkflowInput, -}; - -pub fn compare_perf() -> Workflow { - let head = WorkflowInput::string("head", None); - let base = WorkflowInput::string("base", None); - let crate_name = WorkflowInput::string("crate_name", Some("".to_owned())); - let run_perf = run_perf(&base, &head, &crate_name); - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(head.name, head.input()) - .add_input(base.name, base.input()) - .add_input(crate_name.name, crate_name.input()), - )) - .add_job(run_perf.name, run_perf.job) -} - -pub fn run_perf( - base: &WorkflowInput, - head: &WorkflowInput, - crate_name: &WorkflowInput, -) -> NamedJob { - fn cargo_perf_test(ref_name: &WorkflowInput, crate_name: &WorkflowInput) -> Step { - named::bash( - r#" - if [ -n "$CRATE_NAME" ]; then - cargo perf-test -p "$CRATE_NAME" -- --json="$REF_NAME"; - else - cargo perf-test -p vim -- --json="$REF_NAME"; - fi"#, - ) - .add_env(("REF_NAME", ref_name.to_string())) - .add_env(("CRATE_NAME", crate_name.to_string())) - } - - fn install_hyperfine() -> Step { - named::uses("taiki-e", "install-action", "hyperfine") - } - - fn compare_runs(head: &WorkflowInput, base: &WorkflowInput) -> Step { - named::bash(r#"cargo perf-compare --save=results.md "$BASE" "$HEAD""#) - .add_env(("BASE", base.to_string())) - .add_env(("HEAD", head.to_string())) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(runners::Platform::Linux)) - .map(steps::install_linux_dependencies) - .add_step(install_hyperfine()) - .add_step(steps::git_checkout(base)) - .add_step(cargo_perf_test(base, crate_name)) - .add_step(steps::git_checkout(head)) - .add_step(cargo_perf_test(head, crate_name)) - .add_step(compare_runs(head, base)) - .add_step(upload_artifact("results.md")) - .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/danger.rs b/tooling/xtask/src/tasks/workflows/danger.rs deleted file mode 100644 index 88e7844738..0000000000 --- a/tooling/xtask/src/tasks/workflows/danger.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::steps::{CommonJobConditions, NamedJob, named}; - -use super::{runners, steps}; - -/// Generates the danger.yml workflow -pub fn danger() -> Workflow { - let danger = danger_job(); - - named::workflow() - .on( - Event::default().pull_request(PullRequest::default().add_branch("main").types([ - PullRequestType::Opened, - PullRequestType::Synchronize, - PullRequestType::Reopened, - PullRequestType::Edited, - ])), - ) - .add_job(danger.name, danger.job) -} - -fn danger_job() -> NamedJob { - pub fn install_deps() -> Step { - named::bash("pnpm install --dir script/danger") - } - - pub fn run() -> Step { - named::bash("pnpm run --dir script/danger danger ci") - // This GitHub token is not used, but the value needs to be here to prevent - // Danger from throwing an error. - .add_env(("GITHUB_TOKEN", "not_a_real_token")) - // All requests are instead proxied through a proxy that allows Danger to securely authenticate with GitHub - // while still being able to run on PRs from forks. - .add_env(( - "DANGER_GITHUB_API_BASE_URL", - "https://danger-proxy.zed.dev/github", - )) - } - - NamedJob { - name: "danger".to_string(), - job: Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_pnpm()) - .add_step( - steps::setup_node() - .add_with(("cache", "pnpm")) - .add_with(("cache-dependency-path", "script/danger/pnpm-lock.yaml")), - ) - .add_step(install_deps()) - .add_step(run()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/deploy_collab.rs b/tooling/xtask/src/tasks/workflows/deploy_collab.rs deleted file mode 100644 index c6b620bd5d..0000000000 --- a/tooling/xtask/src/tasks/workflows/deploy_collab.rs +++ /dev/null @@ -1,174 +0,0 @@ -use gh_workflow::{Container, Event, Port, Push, Run, Step, Use, Workflow}; -use indoc::indoc; - -use crate::tasks::workflows::runners::{self, Platform}; -use crate::tasks::workflows::steps::{ - self, CommonJobConditions, FluentBuilder as _, NamedJob, dependant_job, named, use_clang, -}; -use crate::tasks::workflows::vars; - -pub(crate) fn deploy_collab() -> Workflow { - let style = style(); - let tests = tests(&[&style]); - let publish = publish(&[&style, &tests]); - let deploy = deploy(&[&publish]); - - named::workflow() - .on(Event::default().push(Push::default().add_tag("collab-production"))) - .add_env(("DOCKER_BUILDKIT", "1")) - .add_job(style.name, style.job) - .add_job(tests.name, tests.job) - .add_job(publish.name, publish.job) - .add_job(deploy.name, deploy.job) -} - -fn style() -> NamedJob { - named::job(use_clang( - dependant_job(&[]) - .name("Check formatting and Clippy lints") - .with_repository_owner_guard() - .runs_on(runners::LINUX_XL) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_fmt()) - .add_step(steps::clippy(Platform::Linux, None)), - )) -} - -fn tests(deps: &[&NamedJob]) -> NamedJob { - fn run_collab_tests() -> Step { - named::bash("cargo nextest run --package collab --no-fail-fast") - } - - named::job(use_clang( - dependant_job(deps) - .name("Run tests") - .runs_on(runners::LINUX_XL) - .add_service( - "postgres", - Container::new("postgres:15") - .add_env(("POSTGRES_HOST_AUTH_METHOD", "trust")) - .ports(vec![Port::Name("5432:5432".into())]) - .options( - "--health-cmd pg_isready \ - --health-interval 500ms \ - --health-timeout 5s \ - --health-retries 10", - ), - ) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(run_collab_tests()), - )) -} - -fn publish(deps: &[&NamedJob]) -> NamedJob { - fn install_doctl() -> Step { - named::uses("digitalocean", "action-doctl", "v2") - .add_with(("token", vars::DIGITALOCEAN_ACCESS_TOKEN)) - } - - fn sign_into_registry() -> Step { - named::bash("doctl registry login") - } - - fn build_docker_image() -> Step { - named::bash(indoc! {r#" - docker build -f Dockerfile-collab \ - --build-arg "GITHUB_SHA=$GITHUB_SHA" \ - --tag "registry.digitalocean.com/zed/collab:$GITHUB_SHA" \ - . - "#}) - } - - fn publish_docker_image() -> Step { - named::bash(r#"docker push "registry.digitalocean.com/zed/collab:${GITHUB_SHA}""#) - } - - fn prune_docker_system() -> Step { - named::bash("docker system prune --filter 'until=72h' -f") - } - - named::job( - dependant_job(deps) - .name("Publish collab server image") - .runs_on(runners::LINUX_XL) - .add_step(install_doctl()) - .add_step(sign_into_registry()) - .add_step(steps::checkout_repo()) - .add_step(build_docker_image()) - .add_step(publish_docker_image()) - .add_step(prune_docker_system()), - ) -} - -fn deploy(deps: &[&NamedJob]) -> NamedJob { - fn install_doctl() -> Step { - named::uses("digitalocean", "action-doctl", "v2") - .add_with(("token", vars::DIGITALOCEAN_ACCESS_TOKEN)) - } - - fn sign_into_kubernetes() -> Step { - named::bash( - r#"doctl kubernetes cluster kubeconfig save --expiry-seconds 600 "$CLUSTER_NAME""#, - ) - .add_env(("CLUSTER_NAME", vars::CLUSTER_NAME)) - } - - fn start_rollout() -> Step { - named::bash(indoc! {r#" - set -eu - if [[ $GITHUB_REF_NAME = "collab-production" ]]; then - export ZED_KUBE_NAMESPACE=production - export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10 - export ZED_API_LOAD_BALANCER_SIZE_UNIT=2 - elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then - export ZED_KUBE_NAMESPACE=staging - export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1 - export ZED_API_LOAD_BALANCER_SIZE_UNIT=1 - else - echo "cowardly refusing to deploy from an unknown branch" - exit 1 - fi - - echo "Deploying collab:$GITHUB_SHA to $ZED_KUBE_NAMESPACE" - - source script/lib/deploy-helpers.sh - export_vars_for_environment "$ZED_KUBE_NAMESPACE" - - ZED_DO_CERTIFICATE_ID="$(doctl compute certificate list --format ID --no-header)" - export ZED_DO_CERTIFICATE_ID - export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}" - - export ZED_SERVICE_NAME=collab - export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT - export DATABASE_MAX_CONNECTIONS=850 - envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status "deployment/$ZED_SERVICE_NAME" --watch - echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" - - export ZED_SERVICE_NAME=api - export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT - export DATABASE_MAX_CONNECTIONS=60 - envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status "deployment/$ZED_SERVICE_NAME" --watch - echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" - "#}) - } - - named::job( - dependant_job(deps) - .name("Deploy new server image") - .runs_on(runners::LINUX_XL) - .add_step(steps::checkout_repo()) - .add_step(install_doctl()) - .add_step(sign_into_kubernetes()) - .add_step(start_rollout()), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs b/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs deleted file mode 100644 index 68ed1f1acd..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs +++ /dev/null @@ -1,115 +0,0 @@ -use gh_workflow::{ - Event, Expression, Input, Job, Level, Permissions, Push, Strategy, UsesJob, Workflow, -}; -use indoc::indoc; -use serde_json::json; - -use crate::tasks::workflows::{ - extensions::WithAppSecrets, - run_tests::DETECT_CHANGED_EXTENSIONS_SCRIPT, - runners, - steps::{self, CommonJobConditions, NamedJob, named}, - vars::{StepOutput, one_workflow_per_non_main_branch}, -}; - -/// Generates a workflow that triggers on push to main, detects changed extensions -/// in the `extensions/` directory, and invokes the `extension_bump` reusable workflow -/// for each changed extension via a matrix strategy. -pub(crate) fn extension_auto_bump() -> Workflow { - let detect = detect_changed_extensions(); - let bump = bump_extension_versions(&detect); - - named::workflow() - .add_event( - Event::default().push( - Push::default() - .add_branch("main") - .add_path("extensions/**") - .add_path("!extensions/slash-commands-example/**") - .add_path("!extensions/test-extension/**") - .add_path("!extensions/workflows/**") - .add_path("!extensions/*.md"), - ), - ) - .concurrency(one_workflow_per_non_main_branch()) - .add_job(detect.name, detect.job) - .add_job(bump.name, bump.job) -} - -fn detect_changed_extensions() -> NamedJob { - let preamble = indoc! {r#" - COMPARE_REV="$(git rev-parse HEAD~1)" - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA")" - "#}; - - let filter_new_and_removed = indoc! {r#" - # Filter out newly added or entirely removed extensions - FILTERED="[]" - for ext in $(echo "$EXTENSIONS_JSON" | jq -r '.[]'); do - if git show HEAD~1:"$ext/extension.toml" >/dev/null 2>&1 && \ - [ -f "$ext/extension.toml" ]; then - FILTERED=$(echo "$FILTERED" | jq -c --arg e "$ext" '. + [$e]') - fi - done - echo "changed_extensions=$FILTERED" >> "$GITHUB_OUTPUT" - "#}; - - let script = format!( - "{preamble}{detect}{filter}", - preamble = preamble, - detect = DETECT_CHANGED_EXTENSIONS_SCRIPT, - filter = filter_new_and_removed, - ); - - let step = named::bash(script).id("detect"); - - let output = StepOutput::new(&step, "changed_extensions"); - - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(5u32) - .add_step(steps::checkout_repo().with_custom_fetch_depth(2)) - .add_step(step) - .outputs([("changed_extensions".to_owned(), output.to_string())]); - - named::job(job) -} - -fn bump_extension_versions(detect_job: &NamedJob) -> NamedJob { - let job = Job::default() - .needs(vec![detect_job.name.clone()]) - .cond(Expression::new(format!( - "needs.{}.outputs.changed_extensions != '[]'", - detect_job.name - ))) - .permissions( - Permissions::default() - .contents(Level::Write) - .issues(Level::Write) - .pull_requests(Level::Write) - .actions(Level::Write), - ) - .strategy( - Strategy::default() - .fail_fast(false) - // TODO: Remove the limit. We currently need this to workaround the concurrency group issue - // where different matrix jobs would be placed in the same concurrency group and thus cancelled. - .max_parallel(1u32) - .matrix(json!({ - "extension": format!( - "${{{{ fromJson(needs.{}.outputs.changed_extensions) }}}}", - detect_job.name - ) - })), - ) - .uses_local(".github/workflows/extension_bump.yml") - .with( - Input::default() - .add("working-directory", "${{ matrix.extension }}") - .add("force-bump", false), - ) - .with_app_secrets(); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/extension_bump.rs b/tooling/xtask/src/tasks/workflows/extension_bump.rs deleted file mode 100644 index 38cd926ef4..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_bump.rs +++ /dev/null @@ -1,559 +0,0 @@ -use gh_workflow::{ctx::Context, *}; -use indoc::{formatdoc, indoc}; - -use crate::tasks::workflows::{ - extension_tests::{self}, - runners, - steps::{ - self, BASH_SHELL, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, - NamedJob, cache_rust_dependencies_namespace, checkout_repo, dependant_job, named, - }, - vars::{ - JobOutput, StepOutput, WorkflowInput, WorkflowSecret, - one_workflow_per_non_main_branch_and_token, - }, -}; - -const VERSION_CHECK: &str = - r#"sed -n 's/^version = \"\(.*\)\"/\1/p' < extension.toml | tr -d '[:space:]'"#; - -// This is used by various extensions repos in the zed-extensions org to bump extension versions. -pub(crate) fn extension_bump() -> Workflow { - let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned())); - // TODO: Ideally, this would have a default of `false`, but this is currently not - // supported in gh-workflows - let force_bump = WorkflowInput::bool("force-bump", None); - let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned())); - - let (app_id, app_secret) = extension_workflow_secrets(); - let (check_version_changed, version_changed, current_version) = check_version_changed(); - - let version_changed = version_changed.as_job_output(&check_version_changed); - let current_version = current_version.as_job_output(&check_version_changed); - - let dependencies = [&check_version_changed]; - let bump_version = bump_extension_version( - &dependencies, - ¤t_version, - &bump_type, - &version_changed, - &force_bump, - &app_id, - &app_secret, - ); - let (create_label, tag) = create_version_label( - &dependencies, - &version_changed, - ¤t_version, - &app_id, - &app_secret, - ); - let tag = tag.as_job_output(&create_label); - let trigger_release = trigger_release( - &[&check_version_changed, &create_label], - tag, - &app_id, - &app_secret, - ); - - named::workflow() - .add_event( - Event::default().workflow_call( - WorkflowCall::default() - .add_input(bump_type.name, bump_type.call_input()) - .add_input(force_bump.name, force_bump.call_input()) - .add_input(working_directory.name, working_directory.call_input()) - .secrets([ - (app_id.name.to_owned(), app_id.secret_configuration()), - ( - app_secret.name.to_owned(), - app_secret.secret_configuration(), - ), - ]), - ), - ) - .concurrency(one_workflow_per_non_main_branch_and_token("extension-bump")) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(( - "ZED_EXTENSION_CLI_SHA", - extension_tests::ZED_EXTENSION_CLI_SHA, - )) - .add_job(check_version_changed.name, check_version_changed.job) - .add_job(bump_version.name, bump_version.job) - .add_job(create_label.name, create_label.job) - .add_job(trigger_release.name, trigger_release.job) -} - -fn extension_job_defaults() -> Defaults { - Defaults::default().run( - RunDefaults::default() - .shell(BASH_SHELL) - .working_directory("${{ inputs.working-directory }}"), - ) -} - -fn check_version_changed() -> (NamedJob, StepOutput, StepOutput) { - let (compare_versions, version_changed, current_version) = compare_versions(); - - let job = Job::default() - .defaults(extension_job_defaults()) - .with_repository_owner_guard() - .outputs([ - (version_changed.name.to_owned(), version_changed.to_string()), - ( - current_version.name.to_string(), - current_version.to_string(), - ), - ]) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(1u32) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(compare_versions); - - (named::job(job), version_changed, current_version) -} - -fn create_version_label( - dependencies: &[&NamedJob], - version_changed_output: &JobOutput, - current_version: &JobOutput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> (NamedJob, StepOutput) { - let (generate_token, generated_token) = - generate_token(&app_id.to_string(), &app_secret.to_string(), None); - let (determine_tag_step, tag) = determine_tag(current_version); - let job = steps::dependant_job(dependencies) - .defaults(extension_job_defaults()) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && \ - github.ref == 'refs/heads/main' && {version_changed} == 'true'", - version_changed = version_changed_output.expr(), - ))) - .outputs([(tag.name.to_owned(), tag.to_string())]) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(1u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(determine_tag_step) - .add_step(create_version_tag(&tag, generated_token)); - - (named::job(job), tag) -} - -fn create_version_tag(tag: &StepOutput, generated_token: StepOutput) -> Step { - named::uses("actions", "github-script", "v7").with( - Input::default() - .add( - "script", - formatdoc! {r#" - github.rest.git.createRef({{ - owner: context.repo.owner, - repo: context.repo.repo, - ref: 'refs/tags/{tag}', - sha: context.sha - }})"# - }, - ) - .add("github-token", generated_token.to_string()), - ) -} - -fn determine_tag(current_version: &JobOutput) -> (Step, StepOutput) { - let step = named::bash(formatdoc! {r#" - EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')" - - if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then - TAG="v${{CURRENT_VERSION}}" - else - TAG="${{EXTENSION_ID}}-v${{CURRENT_VERSION}}" - fi - - echo "tag=${{TAG}}" >> "$GITHUB_OUTPUT" - "#}) - .id("determine-tag") - .add_env(("CURRENT_VERSION", current_version.to_string())) - .add_env(("WORKING_DIR", "${{ inputs.working-directory }}")); - - let tag = StepOutput::new(&step, "tag"); - (step, tag) -} - -/// Compares the current and previous commit and checks whether versions changed inbetween. -pub(crate) fn compare_versions() -> (Step, StepOutput, StepOutput) { - let check_needs_bump = named::bash(formatdoc! { - r#" - CURRENT_VERSION="$({VERSION_CHECK})" - - if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - PR_FORK_POINT="$(git merge-base origin/main HEAD)" - git checkout "$PR_FORK_POINT" - else - git checkout "$(git log -1 --format=%H)"~1 - fi - - PARENT_COMMIT_VERSION="$({VERSION_CHECK})" - - [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \ - echo "version_changed=false" >> "$GITHUB_OUTPUT" || \ - echo "version_changed=true" >> "$GITHUB_OUTPUT" - - echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }) - .id("compare-versions-check"); - - let version_changed = StepOutput::new(&check_needs_bump, "version_changed"); - let current_version = StepOutput::new(&check_needs_bump, "current_version"); - - (check_needs_bump, version_changed, current_version) -} - -fn bump_extension_version( - dependencies: &[&NamedJob], - current_version: &JobOutput, - bump_type: &WorkflowInput, - version_changed_output: &JobOutput, - force_bump_output: &WorkflowInput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let (generate_token, generated_token) = - generate_token(&app_id.to_string(), &app_secret.to_string(), None); - let (bump_version, _new_version, title, body, branch_name) = - bump_version(current_version, bump_type); - - let job = steps::dependant_job(dependencies) - .defaults(extension_job_defaults()) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({force_bump} == true || {version_changed} == 'false')", - force_bump = force_bump_output.expr(), - version_changed = version_changed_output.expr(), - ))) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(5u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(cache_rust_dependencies_namespace()) - .add_step(install_bump_2_version()) - .add_step(bump_version) - .add_step(create_pull_request( - title, - body, - generated_token, - branch_name, - )); - - named::job(job) -} - -pub(crate) fn generate_token( - app_id_source: &str, - app_secret_source: &str, - repository_target: Option, -) -> (Step, StepOutput) { - let step = named::uses("actions", "create-github-app-token", "v2") - .id("generate-token") - .add_with( - Input::default() - .add("app-id", app_id_source) - .add("private-key", app_secret_source) - .when_some( - repository_target, - |input, - RepositoryTarget { - owner, - repositories, - permissions, - }| { - input - .when_some(owner, |input, owner| input.add("owner", owner)) - .when_some(repositories, |input, repositories| { - input.add("repositories", repositories) - }) - .when_some(permissions, |input, permissions| { - permissions - .into_iter() - .fold(input, |input, (permission, level)| { - input.add( - permission, - serde_json::to_value(&level).unwrap_or_default(), - ) - }) - }) - }, - ), - ); - - let generated_token = StepOutput::new(&step, "token"); - - (step, generated_token) -} - -fn install_bump_2_version() -> Step { - named::run( - runners::Platform::Linux, - "pip install bump2version --break-system-packages", - ) -} - -fn bump_version( - current_version: &JobOutput, - bump_type: &WorkflowInput, -) -> (Step, StepOutput, StepOutput, StepOutput, StepOutput) { - let step = named::bash(formatdoc! {r#" - BUMP_FILES=("extension.toml") - if [[ -f "Cargo.toml" ]]; then - BUMP_FILES+=("Cargo.toml") - fi - - bump2version \ - --search "version = \"{{current_version}}"\" \ - --replace "version = \"{{new_version}}"\" \ - --current-version "$OLD_VERSION" \ - --no-configured-files "$BUMP_TYPE" "${{BUMP_FILES[@]}}" - - if [[ -f "Cargo.toml" ]]; then - cargo +stable update --workspace - fi - - NEW_VERSION="$({VERSION_CHECK})" - EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')" - EXTENSION_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')" - - if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then - {{ - echo "title=Bump version to ${{NEW_VERSION}}"; - echo "body=This PR bumps the version of this extension to v${{NEW_VERSION}}"; - echo "branch_name=zed-zippy-autobump"; - }} >> "$GITHUB_OUTPUT" - else - {{ - echo "title=${{EXTENSION_ID}}: Bump to v${{NEW_VERSION}}"; - echo "body<> "$GITHUB_OUTPUT" - fi - - echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }) - .id("bump-version") - .add_env(("OLD_VERSION", current_version.to_string())) - .add_env(("BUMP_TYPE", bump_type.to_string())) - .add_env(("WORKING_DIR", "${{ inputs.working-directory }}")); - - let new_version = StepOutput::new(&step, "new_version"); - let title = StepOutput::new(&step, "title"); - let body = StepOutput::new(&step, "body"); - let branch_name = StepOutput::new(&step, "branch_name"); - (step, new_version, title, body, branch_name) -} - -fn create_pull_request( - title: StepOutput, - body: StepOutput, - generated_token: StepOutput, - branch_name: StepOutput, -) -> Step { - named::uses("peter-evans", "create-pull-request", "v7").with( - Input::default() - .add("title", title.to_string()) - .add("body", body.to_string()) - .add("commit-message", title.to_string()) - .add("branch", branch_name.to_string()) - .add( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - ) - .add("base", "main") - .add("delete-branch", true) - .add("token", generated_token.to_string()) - .add("sign-commits", true) - .add("assignees", Context::github().actor().to_string()), - ) -} - -fn trigger_release( - dependencies: &[&NamedJob], - tag: JobOutput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]); - let (generate_token, generated_token) = generate_token( - &app_id.to_string(), - &app_secret.to_string(), - Some(extension_registry), - ); - let (get_extension_id, extension_id) = get_extension_id(); - let (release_action, pull_request_number) = release_action(extension_id, tag, &generated_token); - - let job = dependant_job(dependencies) - .defaults(extension_job_defaults()) - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .add_step(generate_token) - .add_step(checkout_repo()) - .add_step(get_extension_id) - .add_step(release_action) - .add_step(enable_automerge_if_staff( - pull_request_number, - generated_token, - )); - - named::job(job) -} - -fn get_extension_id() -> (Step, StepOutput) { - let step = named::bash(indoc! { - r#" - EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)" - - echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT" - "#}) - .id("get-extension-id"); - - let extension_id = StepOutput::new(&step, "extension_id"); - - (step, extension_id) -} - -fn release_action( - extension_id: StepOutput, - tag: JobOutput, - generated_token: &StepOutput, -) -> (Step, StepOutput) { - let step = named::uses( - "huacnlee", - "zed-extension-action", - "82920ff0876879f65ffbcfa3403589114a8919c6", - ) - .id("extension-update") - .add_with(("extension-name", extension_id.to_string())) - .add_with(("push-to", "zed-industries/extensions")) - .add_with(("tag", tag.to_string())) - .add_env(("COMMITTER_TOKEN", generated_token.to_string())); - - let pull_request_number = StepOutput::new(&step, "pull-request-number"); - - (step, pull_request_number) -} - -fn enable_automerge_if_staff( - pull_request_number: StepOutput, - generated_token: StepOutput, -) -> Step { - named::uses("actions", "github-script", "v7") - .add_with(("github-token", generated_token.to_string())) - .add_with(( - "script", - indoc! {r#" - const prNumber = process.env.PR_NUMBER; - if (!prNumber) { - console.log('No pull request number set, skipping automerge.'); - return; - } - - const author = process.env.GITHUB_ACTOR; - let isStaff = false; - try { - const response = await github.rest.teams.getMembershipForUserInOrg({ - org: 'zed-industries', - team_slug: 'staff', - username: author - }); - isStaff = response.data.state === 'active'; - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - - if (!isStaff) { - console.log(`Actor ${author} is not a staff member, skipping automerge.`); - return; - } - - // Assign staff member responsible for the bump - const pullNumber = parseInt(prNumber); - - await github.rest.issues.addAssignees({ - owner: 'zed-industries', - repo: 'extensions', - issue_number: pullNumber, - assignees: [author] - }); - console.log(`Assigned ${author} to PR #${prNumber} in zed-industries/extensions`); - - // Get the GraphQL node ID - const { data: pr } = await github.rest.pulls.get({ - owner: 'zed-industries', - repo: 'extensions', - pull_number: pullNumber - }); - - await github.graphql(` - mutation($pullRequestId: ID!) { - enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: SQUASH }) { - pullRequest { - autoMergeRequest { - enabledAt - } - } - } - } - `, { pullRequestId: pr.node_id }); - - console.log(`Automerge enabled for PR #${prNumber} in zed-industries/extensions`); - "#}, - )) - .add_env(("PR_NUMBER", pull_request_number.to_string())) -} - -fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) { - let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR"); - let app_secret = - WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID"); - - (app_id, app_secret) -} - -pub(crate) struct RepositoryTarget { - owner: Option, - repositories: Option, - permissions: Option>, -} - -impl RepositoryTarget { - pub fn new(owner: T, repositories: &[&str]) -> Self { - Self { - owner: Some(owner.to_string()), - repositories: Some(repositories.join("\n")), - permissions: None, - } - } - - pub fn current() -> Self { - Self { - owner: None, - repositories: None, - permissions: None, - } - } - - pub fn permissions(self, permissions: impl Into>) -> Self { - Self { - permissions: Some(permissions.into()), - ..self - } - } -} diff --git a/tooling/xtask/src/tasks/workflows/extension_tests.rs b/tooling/xtask/src/tasks/workflows/extension_tests.rs deleted file mode 100644 index caf57ce130..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_tests.rs +++ /dev/null @@ -1,204 +0,0 @@ -use gh_workflow::*; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_bump::compare_versions, - run_tests::{fetch_ts_query_ls, orchestrate_for_extension, run_ts_query_ls, tests_pass}, - runners, - steps::{ - self, BASH_SHELL, CommonJobConditions, FluentBuilder, NamedJob, - cache_rust_dependencies_namespace, named, - }, - vars::{PathCondition, StepOutput, WorkflowInput, one_workflow_per_non_main_branch_and_token}, -}; - -pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "03d8e9aee95ea6117d75a48bcac2e19241f6e667"; - -// This should follow the set target in crates/extension/src/extension_builder.rs -const EXTENSION_RUST_TARGET: &str = "wasm32-wasip2"; - -// This is used by various extensions repos in the zed-extensions org to run automated tests. -pub(crate) fn extension_tests() -> Workflow { - let should_check_rust = PathCondition::new("check_rust", r"^(Cargo.lock|Cargo.toml|.*\.rs)$"); - let should_check_extension = - PathCondition::new("check_extension", r"^(extension\.toml|.*\.scm)$"); - - let orchestrate = with_extension_defaults(orchestrate_for_extension(&[ - &should_check_rust, - &should_check_extension, - ])); - - let jobs = [ - orchestrate, - should_check_rust.guard(check_rust()), - should_check_extension.guard(check_extension()), - ]; - - let tests_pass = tests_pass(&jobs, &[]); - - let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned())); - - named::workflow() - .add_event( - Event::default().workflow_call( - WorkflowCall::default() - .add_input(working_directory.name, working_directory.call_input()), - ), - ) - .concurrency(one_workflow_per_non_main_branch_and_token( - "extension-tests", - )) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("ZED_EXTENSION_CLI_SHA", ZED_EXTENSION_CLI_SHA)) - .add_env(("RUSTUP_TOOLCHAIN", "stable")) - .add_env(("CARGO_BUILD_TARGET", EXTENSION_RUST_TARGET)) - .map(|workflow| { - jobs.into_iter() - .chain([tests_pass]) - .fold(workflow, |workflow, job| { - workflow.add_job(job.name, job.job) - }) - }) -} - -fn install_rust_target() -> Step { - named::bash(format!("rustup target add {EXTENSION_RUST_TARGET}",)) -} - -fn get_package_name() -> (Step, StepOutput) { - let step = named::bash(indoc! {r#" - PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < Cargo.toml | head -1 | tr -d '[:space:]')" - echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT" - "#}) - .id("get-package-name"); - - let output = StepOutput::new(&step, "package_name"); - (step, output) -} - -fn cargo_fmt_package(package_name: &StepOutput) -> Step { - named::bash(r#"cargo fmt -p "$PACKAGE_NAME" -- --check"#) - .add_env(("PACKAGE_NAME", package_name.to_string())) -} - -fn run_clippy(package_name: &StepOutput) -> Step { - named::bash(r#"cargo clippy -p "$PACKAGE_NAME" --release --all-features -- --deny warnings"#) - .add_env(("PACKAGE_NAME", package_name.to_string())) -} - -fn run_nextest(package_name: &StepOutput) -> Step { - named::bash( - r#"cargo nextest run -p "$PACKAGE_NAME" --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n 's|host: ||p')""#, - ) - .add_env(("PACKAGE_NAME", package_name.to_string())) - .add_env(("NEXTEST_NO_TESTS", "warn")) -} - -fn extension_job_defaults() -> Defaults { - Defaults::default().run( - RunDefaults::default() - .shell(BASH_SHELL) - .working_directory("${{ inputs.working-directory }}"), - ) -} - -fn with_extension_defaults(named_job: NamedJob) -> NamedJob { - NamedJob { - name: named_job.name, - job: named_job.job.defaults(extension_job_defaults()), - } -} - -fn check_rust() -> NamedJob { - let (get_package, package_name) = get_package_name(); - - let job = Job::default() - .defaults(extension_job_defaults()) - .with_repository_owner_guard() - .runs_on(runners::LINUX_LARGE_RAM) - .timeout_minutes(6u32) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(install_rust_target()) - .add_step(get_package) - .add_step(cargo_fmt_package(&package_name)) - .add_step(run_clippy(&package_name)) - .add_step(steps::cargo_install_nextest()) - .add_step(run_nextest(&package_name)); - - named::job(job) -} - -pub(crate) fn check_extension() -> NamedJob { - let (cache_download, cache_hit) = cache_zed_extension_cli(); - let (check_version_job, version_changed, _) = compare_versions(); - - let job = Job::default() - .defaults(extension_job_defaults()) - .with_repository_owner_guard() - .runs_on(runners::LINUX_LARGE_RAM) - .timeout_minutes(6u32) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(cache_download) - .add_step(download_zed_extension_cli(cache_hit)) - .add_step(cache_rust_dependencies_namespace()) // Extensions can compile Rust, so provide the cache if needed. - .add_step(check()) - .add_step(fetch_ts_query_ls()) - .add_step(run_ts_query_ls()) - .add_step(check_version_job) - .add_step(verify_version_did_not_change(version_changed)); - - named::job(job) -} - -pub fn cache_zed_extension_cli() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "cache", - "0057852bfaa89a56745cba8c7296529d2fc39830", - ) - .id("cache-zed-extension-cli") - .with( - Input::default() - .add("path", "zed-extension") - .add("key", "zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}"), - ); - let output = StepOutput::new(&step, "cache-hit"); - (step, output) -} - -pub fn download_zed_extension_cli(cache_hit: StepOutput) -> Step { - named::bash( - indoc! { - r#" - wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" -O "$GITHUB_WORKSPACE/zed-extension" - chmod +x "$GITHUB_WORKSPACE/zed-extension" - "#, - } - ).if_condition(Expression::new(format!("{} != 'true'", cache_hit.expr()))) -} - -pub fn check() -> Step { - named::bash(indoc! { - r#" - mkdir -p /tmp/ext-scratch - mkdir -p /tmp/ext-output - "$GITHUB_WORKSPACE/zed-extension" --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output - "# - }) -} - -fn verify_version_did_not_change(version_changed: StepOutput) -> Step { - named::bash(indoc! {r#" - if [[ "$VERSION_CHANGED" == "true" && "$GITHUB_EVENT_NAME" == "pull_request" && "$PR_USER_LOGIN" != "zed-zippy[bot]" ]] ; then - echo "Version change detected in your change!" - echo "Version changes happen in separate PRs and will be performed by the zed-zippy bot" - exit 42 - fi - "# - }) - .add_env(("VERSION_CHANGED", version_changed.to_string())) - .add_env(("PR_USER_LOGIN", "${{ github.event.pull_request.user.login }}")) -} diff --git a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs deleted file mode 100644 index a62bb107da..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs +++ /dev/null @@ -1,394 +0,0 @@ -use gh_workflow::{ - Event, Expression, Job, Level, Run, Step, Strategy, Use, Workflow, WorkflowDispatch, -}; -use indoc::formatdoc; -use indoc::indoc; -use serde_json::json; - -use crate::tasks::workflows::steps::CheckoutStep; -use crate::tasks::workflows::steps::cache_rust_dependencies_namespace; -use crate::tasks::workflows::vars::JobOutput; -use crate::tasks::workflows::{ - extension_bump::{RepositoryTarget, generate_token}, - runners, - steps::{self, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -const ROLLOUT_TAG_NAME: &str = "extension-workflows"; -const WORKFLOW_ARTIFACT_NAME: &str = "extension-workflow-files"; - -pub(crate) fn extension_workflow_rollout() -> Workflow { - let filter_repos_input = WorkflowInput::string("filter-repos", Some(String::new())) - .description( - "Comma-separated list of repository names to rollout to. Leave empty for all repos.", - ); - let extra_context_input = WorkflowInput::string("change-description", Some(String::new())) - .description("Description for the changes to be expected with this rollout"); - - let (fetch_repos, removed_ci, removed_shared) = fetch_extension_repos(&filter_repos_input); - let rollout_workflows = rollout_workflows_to_extension( - &fetch_repos, - removed_ci, - removed_shared, - &extra_context_input, - ); - let create_tag = create_rollout_tag(&rollout_workflows, &filter_repos_input); - - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(filter_repos_input.name, filter_repos_input.input()) - .add_input(extra_context_input.name, extra_context_input.input()), - )) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_job(fetch_repos.name, fetch_repos.job) - .add_job(rollout_workflows.name, rollout_workflows.job) - .add_job(create_tag.name, create_tag.job) -} - -fn fetch_extension_repos(filter_repos_input: &WorkflowInput) -> (NamedJob, JobOutput, JobOutput) { - fn get_repositories(filter_repos_input: &WorkflowInput) -> (Step, StepOutput) { - let step = named::uses("actions", "github-script", "v7") - .id("list-repos") - .add_with(( - "script", - formatdoc! {r#" - const repos = await github.paginate(github.rest.repos.listForOrg, {{ - org: 'zed-extensions', - type: 'public', - per_page: 100, - }}); - - let filteredRepos = repos - .filter(repo => !repo.archived) - .map(repo => repo.name); - - const filterInput = `{filter_repos_input}`.trim(); - if (filterInput.length > 0) {{ - const allowedNames = filterInput.split(',').map(s => s.trim()).filter(s => s.length > 0); - filteredRepos = filteredRepos.filter(name => allowedNames.includes(name)); - console.log(`Filter applied. Matched ${{filteredRepos.length}} repos from ${{allowedNames.length}} requested.`); - }} - - console.log(`Found ${{filteredRepos.length}} extension repos`); - return filteredRepos; - "#}, - )) - .add_with(("result-encoding", "json")); - - let filtered_repos = StepOutput::new(&step, "result"); - - (step, filtered_repos) - } - - fn checkout_zed_repo() -> CheckoutStep { - steps::checkout_repo() - .with_full_history() - .with_custom_name("checkout_zed_repo") - } - - fn get_previous_tag_commit() -> (Step, StepOutput) { - let step = named::bash(formatdoc! {r#" - PREV_COMMIT=$(git rev-parse "{ROLLOUT_TAG_NAME}^{{commit}}" 2>/dev/null || echo "") - if [ -z "$PREV_COMMIT" ]; then - echo "::error::No previous rollout tag '{ROLLOUT_TAG_NAME}' found. Cannot determine file changes." - exit 1 - fi - echo "Found previous rollout at commit: $PREV_COMMIT" - echo "prev_commit=$PREV_COMMIT" >> "$GITHUB_OUTPUT" - "#}) - .id("prev-tag"); - - let step_output = StepOutput::new(&step, "prev_commit"); - - (step, step_output) - } - - fn get_removed_files(prev_commit: &StepOutput) -> (Step, StepOutput, StepOutput) { - let step = named::bash(indoc! {r#" - for workflow_type in "ci" "shared"; do - if [ "$workflow_type" = "ci" ]; then - WORKFLOW_DIR="extensions/workflows" - else - WORKFLOW_DIR="extensions/workflows/shared" - fi - - REMOVED=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \ - awk '/^D/ { print $2 } /^R/ { print $2 }' | \ - xargs -I{} basename {} 2>/dev/null | \ - tr '\n' ' ' || echo "") - REMOVED=$(echo "$REMOVED" | xargs) - - echo "Removed files for $workflow_type: $REMOVED" - echo "removed_${workflow_type}=$REMOVED" >> "$GITHUB_OUTPUT" - done - "#}) - .id("calc-changes") - .add_env(("PREV_COMMIT", prev_commit.to_string())); - - // These are created in the for-loop above and thus do exist - let removed_ci = StepOutput::new_unchecked(&step, "removed_ci"); - let removed_shared = StepOutput::new_unchecked(&step, "removed_shared"); - - (step, removed_ci, removed_shared) - } - - fn generate_workflow_files() -> Step { - named::bash(indoc! {r#" - cargo xtask workflows "$COMMIT_SHA" - "#}) - .add_env(("COMMIT_SHA", "${{ github.sha }}")) - } - - fn upload_workflow_files() -> Step { - named::uses( - "actions", - "upload-artifact", - "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 - ) - .add_with(("name", WORKFLOW_ARTIFACT_NAME)) - .add_with(("path", "extensions/workflows/**/*.yml")) - .add_with(("if-no-files-found", "error")) - } - - let (get_org_repositories, list_repos_output) = get_repositories(filter_repos_input); - let (get_prev_tag, prev_commit) = get_previous_tag_commit(); - let (calc_changes, removed_ci, removed_shared) = get_removed_files(&prev_commit); - - let job = Job::default() - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.ref == 'refs/heads/main'" - ))) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(10u32) - .outputs([ - ("repos".to_owned(), list_repos_output.to_string()), - ("prev_commit".to_owned(), prev_commit.to_string()), - ("removed_ci".to_owned(), removed_ci.to_string()), - ("removed_shared".to_owned(), removed_shared.to_string()), - ]) - .add_step(checkout_zed_repo()) - .add_step(get_prev_tag) - .add_step(calc_changes) - .add_step(get_org_repositories) - .add_step(cache_rust_dependencies_namespace()) - .add_step(generate_workflow_files()) - .add_step(upload_workflow_files()); - - let job = named::job(job); - let (removed_ci, removed_shared) = ( - removed_ci.as_job_output(&job), - removed_shared.as_job_output(&job), - ); - - (job, removed_ci, removed_shared) -} - -fn rollout_workflows_to_extension( - fetch_repos_job: &NamedJob, - removed_ci: JobOutput, - removed_shared: JobOutput, - extra_context_input: &WorkflowInput, -) -> NamedJob { - fn checkout_extension_repo(token: &StepOutput) -> CheckoutStep { - steps::checkout_repo() - .with_custom_name("checkout_extension_repo") - .with_token(token) - .with_repository("zed-extensions/${{ matrix.repo }}") - .with_path("extension") - } - - fn download_workflow_files() -> Step { - named::uses( - "actions", - "download-artifact", - "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 - ) - .add_with(("name", WORKFLOW_ARTIFACT_NAME)) - .add_with(("path", "workflow-files")) - } - - fn sync_workflow_files(removed_ci: JobOutput, removed_shared: JobOutput) -> Step { - named::bash(indoc! {r#" - mkdir -p extension/.github/workflows - - if [ "$MATRIX_REPO" = "workflows" ]; then - REMOVED_FILES="$REMOVED_CI" - else - REMOVED_FILES="$REMOVED_SHARED" - fi - - cd extension/.github/workflows - - if [ -n "$REMOVED_FILES" ]; then - for file in $REMOVED_FILES; do - if [ -f "$file" ]; then - rm -f "$file" - fi - done - fi - - cd - > /dev/null - - if [ "$MATRIX_REPO" = "workflows" ]; then - cp workflow-files/*.yml extension/.github/workflows/ - else - cp workflow-files/shared/*.yml extension/.github/workflows/ - fi - "#}) - .add_env(("REMOVED_CI", removed_ci)) - .add_env(("REMOVED_SHARED", removed_shared)) - .add_env(("MATRIX_REPO", "${{ matrix.repo }}")) - } - - fn get_short_sha() -> (Step, StepOutput) { - let step = named::bash(indoc! {r#" - echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT" - "#}) - .id("short-sha"); - - let step_output = StepOutput::new(&step, "sha_short"); - - (step, step_output) - } - - fn create_pull_request( - token: &StepOutput, - short_sha: &StepOutput, - context_input: &WorkflowInput, - ) -> Step { - let title = format!("Update CI workflows to `{short_sha}`"); - - let body = formatdoc! {r#" - This PR updates the CI workflow files from the main Zed repository - based on the commit zed-industries/zed@${{{{ github.sha }}}} - - {context_input} - "#, - }; - - named::uses("peter-evans", "create-pull-request", "v7") - .add_with(("path", "extension")) - .add_with(("title", title.clone())) - .add_with(("body", body)) - .add_with(("commit-message", title)) - .add_with(("branch", "update-workflows")) - .add_with(( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - )) - .add_with(( - "author", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - )) - .add_with(("base", "main")) - .add_with(("delete-branch", true)) - .add_with(("token", token.to_string())) - .add_with(("sign-commits", true)) - .id("create-pr") - } - - fn enable_auto_merge(token: &StepOutput) -> Step { - named::bash(indoc! {r#" - if [ -n "$PR_NUMBER" ]; then - gh pr merge "$PR_NUMBER" --auto --squash - fi - "#}) - .working_directory("extension") - .add_env(("GH_TOKEN", token.to_string())) - .add_env(( - "PR_NUMBER", - "${{ steps.create-pr.outputs.pull-request-number }}", - )) - } - - let (authenticate, token) = generate_token( - vars::ZED_ZIPPY_APP_ID, - vars::ZED_ZIPPY_APP_PRIVATE_KEY, - Some( - RepositoryTarget::new("zed-extensions", &["${{ matrix.repo }}"]).permissions([ - ("permission-pull-requests".to_owned(), Level::Write), - ("permission-contents".to_owned(), Level::Write), - ("permission-workflows".to_owned(), Level::Write), - ]), - ), - ); - let (calculate_short_sha, short_sha) = get_short_sha(); - - let job = Job::default() - .needs([fetch_repos_job.name.clone()]) - .cond(Expression::new(format!( - "needs.{}.outputs.repos != '[]'", - fetch_repos_job.name - ))) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(10u32) - .strategy( - Strategy::default() - .fail_fast(false) - .max_parallel(10u32) - .matrix(json!({ - "repo": format!("${{{{ fromJson(needs.{}.outputs.repos) }}}}", fetch_repos_job.name) - })), - ) - .add_step(authenticate) - .add_step(checkout_extension_repo(&token)) - .add_step(download_workflow_files()) - .add_step(sync_workflow_files(removed_ci, removed_shared)) - .add_step(calculate_short_sha) - .add_step(create_pull_request(&token, &short_sha, extra_context_input)) - .add_step(enable_auto_merge(&token)); - - named::job(job) -} - -fn create_rollout_tag(rollout_job: &NamedJob, filter_repos_input: &WorkflowInput) -> NamedJob { - fn checkout_zed_repo(token: &StepOutput) -> CheckoutStep { - steps::checkout_repo().with_full_history().with_token(token) - } - - fn update_rollout_tag() -> Step { - named::bash(formatdoc! {r#" - if git rev-parse "{ROLLOUT_TAG_NAME}" >/dev/null 2>&1; then - git tag -d "{ROLLOUT_TAG_NAME}" - git push origin ":refs/tags/{ROLLOUT_TAG_NAME}" || true - fi - - echo "Creating new tag '{ROLLOUT_TAG_NAME}' at $(git rev-parse --short HEAD)" - git tag "{ROLLOUT_TAG_NAME}" - git push origin "{ROLLOUT_TAG_NAME}" - "#}) - } - - fn configure_git() -> Step { - named::bash(indoc! {r#" - git config user.name "zed-zippy[bot]" - git config user.email "234243425+zed-zippy[bot]@users.noreply.github.com" - "#}) - } - - let (authenticate, token) = generate_token( - vars::ZED_ZIPPY_APP_ID, - vars::ZED_ZIPPY_APP_PRIVATE_KEY, - Some( - RepositoryTarget::current() - .permissions([("permission-contents".to_owned(), Level::Write)]), - ), - ); - - let job = Job::default() - .needs([rollout_job.name.clone()]) - .cond(Expression::new(format!( - "{filter_repos} == ''", - filter_repos = filter_repos_input.expr(), - ))) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(1u32) - .add_step(authenticate) - .add_step(checkout_zed_repo(&token)) - .add_step(configure_git()) - .add_step(update_rollout_tag()); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions.rs b/tooling/xtask/src/tasks/workflows/extensions.rs deleted file mode 100644 index d55c091e4d..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions.rs +++ /dev/null @@ -1,23 +0,0 @@ -use gh_workflow::{Job, UsesJob}; -use indexmap::IndexMap; - -use crate::tasks::workflows::vars; - -pub(crate) mod bump_version; -pub(crate) mod run_tests; - -pub(crate) trait WithAppSecrets: Sized { - fn with_app_secrets(self) -> Self; -} - -impl WithAppSecrets for Job { - fn with_app_secrets(self) -> Self { - self.secrets(IndexMap::from([ - ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()), - ( - "app-secret".to_owned(), - vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(), - ), - ])) - } -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs deleted file mode 100644 index 4dc2560e2b..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs +++ /dev/null @@ -1,108 +0,0 @@ -use gh_workflow::{ - Event, Expression, Input, Job, Level, Permissions, PullRequest, PullRequestType, Push, Run, - Step, UsesJob, Workflow, WorkflowDispatch, -}; -use indoc::indoc; - -use crate::tasks::workflows::{ - GenerateWorkflowArgs, GitSha, - extensions::WithAppSecrets, - runners, - steps::{CommonJobConditions, NamedJob, named}, - vars::{JobOutput, StepOutput, one_workflow_per_non_main_branch_and_token}, -}; - -pub(crate) fn bump_version(args: &GenerateWorkflowArgs) -> Workflow { - let (determine_bump_type, bump_type) = determine_bump_type(); - let bump_type = bump_type.as_job_output(&determine_bump_type); - - let call_bump_version = call_bump_version(args.sha.as_ref(), &determine_bump_type, bump_type); - - named::workflow() - .on(Event::default() - .push( - Push::default() - .add_branch("main") - .add_ignored_path(".github/**"), - ) - .pull_request(PullRequest::default().add_type(PullRequestType::Labeled)) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(one_workflow_per_non_main_branch_and_token("labels")) - .add_job(determine_bump_type.name, determine_bump_type.job) - .add_job(call_bump_version.name, call_bump_version.job) -} - -pub(crate) fn call_bump_version( - target_ref: Option<&GitSha>, - depending_job: &NamedJob, - bump_type: JobOutput, -) -> NamedJob { - let job = Job::default() - .cond(Expression::new(format!( - "github.event.action != 'labeled' || {} != 'patch'", - bump_type.expr() - ))) - .permissions( - Permissions::default() - .contents(Level::Write) - .issues(Level::Write) - .pull_requests(Level::Write) - .actions(Level::Write), - ) - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_bump.yml", - target_ref.map_or("main", AsRef::as_ref), - ) - .add_need(depending_job.name.clone()) - .with( - Input::default() - .add("bump-type", bump_type.to_string()) - .add("force-bump", "${{ github.event_name != 'push' }}"), - ) - .with_app_secrets(); - - named::job(job) -} - -fn determine_bump_type() -> (NamedJob, StepOutput) { - let (get_bump_type, output) = get_bump_type(); - let job = Job::default() - .with_repository_owner_guard() - .permissions(Permissions::default()) - .runs_on(runners::LINUX_SMALL) - .add_step(get_bump_type) - .outputs([(output.name.to_owned(), output.to_string())]); - (named::job(job), output) -} - -fn get_bump_type() -> (Step, StepOutput) { - let step = named::bash( - indoc! {r#" - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - "#}, - ) - .add_env(("HAS_MAJOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }}" - })) - .add_env(("HAS_MINOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }}" - })) - .id("get-bump-type"); - - let step_output = StepOutput::new(&step, "bump_type"); - - (step, step_output) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs deleted file mode 100644 index ae8000c15c..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs +++ /dev/null @@ -1,30 +0,0 @@ -use gh_workflow::{Event, Job, Level, Permissions, PullRequest, Push, UsesJob, Workflow}; - -use crate::tasks::workflows::{ - GenerateWorkflowArgs, GitSha, - steps::{NamedJob, named}, - vars::one_workflow_per_non_main_branch_and_token, -}; - -pub(crate) fn run_tests(args: &GenerateWorkflowArgs) -> Workflow { - let call_extension_tests = call_extension_tests(args.sha.as_ref()); - named::workflow() - .on(Event::default() - .pull_request(PullRequest::default().add_branch("**")) - .push(Push::default().add_branch("main"))) - .concurrency(one_workflow_per_non_main_branch_and_token("pr")) - .add_job(call_extension_tests.name, call_extension_tests.job) -} - -pub(crate) fn call_extension_tests(target_ref: Option<&GitSha>) -> NamedJob { - let job = Job::default() - .permissions(Permissions::default().contents(Level::Read)) - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_tests.yml", - target_ref.map_or("main", AsRef::as_ref), - ); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/nix_build.rs b/tooling/xtask/src/tasks/workflows/nix_build.rs deleted file mode 100644 index 9e401ccac0..0000000000 --- a/tooling/xtask/src/tasks/workflows/nix_build.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::tasks::workflows::{ - runners::{Arch, Platform}, - steps::{CommonJobConditions, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; - -pub(crate) fn build_nix( - platform: Platform, - arch: Arch, - flake_output: &str, - cachix_filter: Option<&str>, - deps: &[&NamedJob], -) -> NamedJob { - pub fn install_nix() -> Step { - named::uses( - "cachix", - "install-nix-action", - "02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31 - ) - .add_with(("github_access_token", vars::GITHUB_TOKEN)) - } - - pub fn cachix_action(cachix_filter: Option<&str>) -> Step { - let mut step = named::uses( - "cachix", - "cachix-action", - "0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16 - ) - .add_with(("name", "zed")) - .add_with(("authToken", vars::CACHIX_AUTH_TOKEN)) - .add_with(("cachixArgs", "-v")); - if let Some(cachix_filter) = cachix_filter { - step = step.add_with(("pushFilter", cachix_filter)); - } - step - } - - pub fn build(flake_output: &str) -> Step { - named::bash(&format!( - "nix build .#{} -L --accept-flake-config", - flake_output - )) - } - - // After install-nix, register ~/nix-cache as a local binary cache - // substituter so nix pulls from it on demand during builds (no bulk - // import). Also restart the daemon so it picks up the new config. - pub fn configure_local_nix_cache() -> Step { - named::bash(indoc::indoc! {r#" - mkdir -p ~/nix-cache - echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf - echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf - sudo launchctl kickstart -k system/org.nixos.nix-daemon - "#}) - } - - // Incrementally copy only new store paths from the build result's - // closure into the local binary cache for the next run. - pub fn export_to_local_nix_cache() -> Step { - named::bash(indoc::indoc! {r#" - if [ -L result ]; then - echo "Copying build closure to local binary cache..." - nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed" - else - echo "No build result found, skipping cache export." - fi - "#}) - .if_condition(Expression::new("always()")) - } - - let runner = match platform { - Platform::Windows => unimplemented!(), - Platform::Linux => runners::LINUX_X86_BUNDLER, - Platform::Mac => runners::MAC_DEFAULT, - }; - let mut job = Job::default() - .timeout_minutes(60u32) - .continue_on_error(true) - .with_repository_owner_guard() - .runs_on(runner) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT)) - .add_env(( - "ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON", - vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON, - )) - .add_env(("GIT_LFS_SKIP_SMUDGE", "1")) // breaks the livekit rust sdk examples which we don't actually depend on - .add_step(steps::checkout_repo()); - - if deps.len() > 0 { - job = job.needs(deps.iter().map(|d| d.name.clone()).collect::>()); - } - - // On Linux, `cache: nix` uses bind-mounts so the /nix store is available - // before install-nix-action runs — no extra steps needed. - // - // On macOS, `/nix` lives on a read-only root filesystem and the nscloud - // cache action cannot mount or symlink there. Instead we cache a - // user-writable directory (~/nix-cache) as a local binary cache and - // register it as a nix substituter. Nix then pulls paths from it on - // demand during builds (zero-copy at startup), and after building we - // incrementally copy new paths into the cache for the next run. - job = match platform { - Platform::Linux => job - .add_step(steps::cache_nix_dependencies_namespace()) - .add_step(install_nix()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)), - Platform::Mac => job - .add_step(steps::cache_nix_store_macos()) - .add_step(install_nix()) - .add_step(configure_local_nix_cache()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)) - .add_step(export_to_local_nix_cache()), - Platform::Windows => unimplemented!(), - }; - - NamedJob { - name: format!("build_nix_{platform}_{arch}"), - job, - } -} diff --git a/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs b/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs deleted file mode 100644 index 2269201a2d..0000000000 --- a/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs +++ /dev/null @@ -1,201 +0,0 @@ -use gh_workflow::{ctx::Context, *}; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_bump::{RepositoryTarget, generate_token}, - runners, - steps::{self, CommonJobConditions, NamedJob, named}, - vars::{self, StepOutput}, -}; - -pub fn publish_extension_cli() -> Workflow { - let publish = publish_job(); - let update_sha_in_zed = update_sha_in_zed(&publish); - let update_sha_in_extensions = update_sha_in_extensions(&publish); - - named::workflow() - .on(Event::default().push(Push::default().tags(vec!["extension-cli".to_string()]))) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_job(publish.name, publish.job) - .add_job(update_sha_in_zed.name, update_sha_in_zed.job) - .add_job(update_sha_in_extensions.name, update_sha_in_extensions.job) -} - -fn publish_job() -> NamedJob { - fn build_extension_cli() -> Step { - named::bash("cargo build --release --package extension_cli") - } - - fn upload_binary() -> Step { - named::bash(r#"script/upload-extension-cli "$GITHUB_SHA""#) - .add_env(( - "DIGITALOCEAN_SPACES_ACCESS_KEY", - vars::DIGITALOCEAN_SPACES_ACCESS_KEY, - )) - .add_env(( - "DIGITALOCEAN_SPACES_SECRET_KEY", - vars::DIGITALOCEAN_SPACES_SECRET_KEY, - )) - } - - named::job( - Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::setup_linux()) - .add_step(build_extension_cli()) - .add_step(upload_binary()), - ) -} - -fn update_sha_in_zed(publish_job: &NamedJob) -> NamedJob { - let (generate_token, generated_token) = generate_token( - vars::ZED_ZIPPY_APP_ID, - vars::ZED_ZIPPY_APP_PRIVATE_KEY, - Some(RepositoryTarget::current()), - ); - - fn replace_sha() -> Step { - named::bash(indoc! {r#" - sed -i "s/ZED_EXTENSION_CLI_SHA: &str = \"[a-f0-9]*\"/ZED_EXTENSION_CLI_SHA: \&str = \"$GITHUB_SHA\"/" \ - tooling/xtask/src/tasks/workflows/extension_tests.rs - "#}) - } - - fn regenerate_workflows() -> Step { - named::bash("cargo xtask workflows") - } - - let (get_short_sha_step, short_sha) = get_short_sha(); - - named::job( - Job::default() - .with_repository_owner_guard() - .needs(vec![publish_job.name.clone()]) - .runs_on(runners::LINUX_LARGE) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(get_short_sha_step) - .add_step(replace_sha()) - .add_step(regenerate_workflows()) - .add_step(create_pull_request_zed(&generated_token, &short_sha)), - ) -} - -fn create_pull_request_zed(generated_token: &StepOutput, short_sha: &StepOutput) -> Step { - let title = format!( - "extension_ci: Bump extension CLI version to `{}`", - short_sha - ); - - named::uses("peter-evans", "create-pull-request", "v7").with( - Input::default() - .add("title", title.clone()) - .add( - "body", - indoc! {r#" - This PR bumps the extension CLI version used in the extension workflows to `${{ github.sha }}`. - - Release Notes: - - - N/A - "#}, - ) - .add("commit-message", title) - .add("branch", "update-extension-cli-sha") - .add( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - ) - .add("base", "main") - .add("delete-branch", true) - .add("token", generated_token.to_string()) - .add("sign-commits", true) - .add("assignees", Context::github().actor().to_string()), - ) -} - -fn update_sha_in_extensions(publish_job: &NamedJob) -> NamedJob { - let extensions_repo = RepositoryTarget::new("zed-industries", &["extensions"]); - let (generate_token, generated_token) = generate_token( - vars::ZED_ZIPPY_APP_ID, - vars::ZED_ZIPPY_APP_PRIVATE_KEY, - Some(extensions_repo), - ); - - fn checkout_extensions_repo(token: &StepOutput) -> Step { - named::uses( - "actions", - "checkout", - "11bd71901bbe5b1630ceea73d27597364c9af683", // v4 - ) - .add_with(("repository", "zed-industries/extensions")) - .add_with(("token", token.to_string())) - } - - fn replace_sha() -> Step { - named::bash(indoc! {r#" - sed -i "s/ZED_EXTENSION_CLI_SHA: [a-f0-9]*/ZED_EXTENSION_CLI_SHA: $GITHUB_SHA/" \ - .github/workflows/ci.yml - "#}) - } - - let (get_short_sha_step, short_sha) = get_short_sha(); - - named::job( - Job::default() - .with_repository_owner_guard() - .needs(vec![publish_job.name.clone()]) - .runs_on(runners::LINUX_SMALL) - .add_step(generate_token) - .add_step(get_short_sha_step) - .add_step(checkout_extensions_repo(&generated_token)) - .add_step(replace_sha()) - .add_step(create_pull_request_extensions(&generated_token, &short_sha)), - ) -} - -fn create_pull_request_extensions( - generated_token: &StepOutput, - short_sha: &StepOutput, -) -> Step { - let title = format!("Bump extension CLI version to `{}`", short_sha); - - named::uses("peter-evans", "create-pull-request", "v7").with( - Input::default() - .add("title", title.clone()) - .add( - "body", - indoc! {r#" - This PR bumps the extension CLI version to https://github.com/zed-industries/zed/commit/${{ github.sha }}. - "#}, - ) - .add("commit-message", title) - .add("branch", "update-extension-cli-sha") - .add( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - ) - .add("base", "main") - .add("delete-branch", true) - .add("token", generated_token.to_string()) - .add("sign-commits", true) - .add("labels", "allow-no-extension") - .add("assignees", Context::github().actor().to_string()), - ) -} - -fn get_short_sha() -> (Step, StepOutput) { - let step = named::bash(indoc::indoc! {r#" - echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT" - "#}) - .id("short-sha"); - - let step_output = vars::StepOutput::new(&step, "sha_short"); - - (step, step_output) -} diff --git a/tooling/xtask/src/tasks/workflows/release.rs b/tooling/xtask/src/tasks/workflows/release.rs deleted file mode 100644 index 2646005021..0000000000 --- a/tooling/xtask/src/tasks/workflows/release.rs +++ /dev/null @@ -1,446 +0,0 @@ -use gh_workflow::{Event, Expression, Push, Run, Step, Use, Workflow, ctx::Context}; -use indoc::formatdoc; - -use crate::tasks::workflows::{ - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests, - runners::{self, Arch, Platform}, - steps::{self, FluentBuilder, NamedJob, dependant_job, named, release_job}, - vars::{self, StepOutput, assets}, -}; - -const CURRENT_ACTION_RUN_URL: &str = - "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"; - -pub(crate) fn release() -> Workflow { - let macos_tests = run_tests::run_platform_tests_no_filter(Platform::Mac); - let linux_tests = run_tests::run_platform_tests_no_filter(Platform::Linux); - let windows_tests = run_tests::run_platform_tests_no_filter(Platform::Windows); - let macos_clippy = run_tests::clippy(Platform::Mac, None); - let linux_clippy = run_tests::clippy(Platform::Linux, None); - let windows_clippy = run_tests::clippy(Platform::Windows, None); - let check_scripts = run_tests::check_scripts(); - - let create_draft_release = create_draft_release(); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux( - Arch::AARCH64, - None, - &[&linux_tests, &linux_clippy, &check_scripts], - ), - linux_x86_64: bundle_linux( - Arch::X86_64, - None, - &[&linux_tests, &linux_clippy, &check_scripts], - ), - mac_aarch64: bundle_mac( - Arch::AARCH64, - None, - &[&macos_tests, &macos_clippy, &check_scripts], - ), - mac_x86_64: bundle_mac( - Arch::X86_64, - None, - &[&macos_tests, &macos_clippy, &check_scripts], - ), - windows_aarch64: bundle_windows( - Arch::AARCH64, - None, - &[&windows_tests, &windows_clippy, &check_scripts], - ), - windows_x86_64: bundle_windows( - Arch::X86_64, - None, - &[&windows_tests, &windows_clippy, &check_scripts], - ), - }; - - let upload_release_assets = upload_release_assets(&[&create_draft_release], &bundle); - let validate_release_assets = validate_release_assets(&[&upload_release_assets]); - - let auto_release_preview = auto_release_preview(&[&validate_release_assets]); - - let test_jobs = [ - &macos_tests, - &linux_tests, - &windows_tests, - &macos_clippy, - &linux_clippy, - &windows_clippy, - &check_scripts, - ]; - let push_slack_notification = push_release_update_notification( - &create_draft_release, - &upload_release_assets, - &validate_release_assets, - &auto_release_preview, - &test_jobs, - &bundle, - ); - - named::workflow() - .on(Event::default().push(Push::default().tags(vec!["v*".to_string()]))) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(macos_tests.name, macos_tests.job) - .add_job(linux_tests.name, linux_tests.job) - .add_job(windows_tests.name, windows_tests.job) - .add_job(macos_clippy.name, macos_clippy.job) - .add_job(linux_clippy.name, linux_clippy.job) - .add_job(windows_clippy.name, windows_clippy.job) - .add_job(check_scripts.name, check_scripts.job) - .add_job(create_draft_release.name, create_draft_release.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - 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(push_slack_notification.name, push_slack_notification.job) -} - -pub(crate) struct ReleaseBundleJobs { - pub linux_aarch64: NamedJob, - pub linux_x86_64: NamedJob, - pub mac_aarch64: NamedJob, - pub mac_x86_64: NamedJob, - pub windows_aarch64: NamedJob, - pub windows_x86_64: NamedJob, -} - -impl ReleaseBundleJobs { - pub fn jobs(&self) -> Vec<&NamedJob> { - vec![ - &self.linux_aarch64, - &self.linux_x86_64, - &self.mac_aarch64, - &self.mac_x86_64, - &self.windows_aarch64, - &self.windows_x86_64, - ] - } - - pub fn into_jobs(self) -> Vec { - vec![ - self.linux_aarch64, - self.linux_x86_64, - self.mac_aarch64, - self.mac_x86_64, - self.windows_aarch64, - self.windows_x86_64, - ] - } -} - -pub(crate) fn create_sentry_release() -> Step { - named::uses( - "getsentry", - "action-release", - "526942b68292201ac6bbb99b9a0747d4abee354c", // v3 - ) - .add_env(("SENTRY_ORG", "zed-dev")) - .add_env(("SENTRY_PROJECT", "zed")) - .add_env(("SENTRY_AUTH_TOKEN", vars::SENTRY_AUTH_TOKEN)) - .add_with(("environment", "production")) -} - -fn validate_release_assets(deps: &[&NamedJob]) -> NamedJob { - let expected_assets: Vec = assets::all().iter().map(|a| format!("\"{a}\"")).collect(); - let expected_assets_json = format!("[{}]", expected_assets.join(", ")); - - let validation_script = formatdoc! {r#" - EXPECTED_ASSETS='{expected_assets_json}' - TAG="$GITHUB_REF_NAME" - - ACTUAL_ASSETS=$(gh release view "$TAG" --repo=zed-industries/zed --json assets -q '[.assets[].name]') - - MISSING_ASSETS=$(echo "$EXPECTED_ASSETS" | jq -r --argjson actual "$ACTUAL_ASSETS" '. - $actual | .[]') - - if [ -n "$MISSING_ASSETS" ]; then - echo "Error: The following assets are missing from the release:" - echo "$MISSING_ASSETS" - exit 1 - fi - - echo "All expected assets are present in the release." - "#, - }; - - named::job( - dependant_job(deps).runs_on(runners::LINUX_SMALL).add_step( - named::bash(&validation_script).add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ), - ) -} - -fn auto_release_preview(deps: &[&NamedJob]) -> NamedJob { - let (authenticate, token) = steps::authenticate_as_zippy(); - - named::job( - dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new(indoc::indoc!( - r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"# - ))) - .add_step(authenticate) - .add_step( - steps::script( - r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#, - ) - .add_env(("GITHUB_TOKEN", &token)), - ) - ) -} - -pub(crate) fn download_workflow_artifacts() -> Step { - named::uses( - "actions", - "download-artifact", - "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 - ) - .add_with(("path", "./artifacts/")) -} - -pub(crate) fn prep_release_artifacts() -> Step { - let mut script_lines = vec!["mkdir -p release-artifacts/\n".to_string()]; - for asset in assets::all() { - let mv_command = format!("mv ./artifacts/{asset}/{asset} release-artifacts/{asset}"); - script_lines.push(mv_command) - } - - named::bash(&script_lines.join("\n")) -} - -fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> NamedJob { - let mut deps = deps.to_vec(); - deps.extend(bundle.jobs()); - - named::job( - dependant_job(&deps) - .runs_on(runners::LINUX_MEDIUM) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("gh release upload \"$GITHUB_REF_NAME\" --repo=zed-industries/zed release-artifacts/*") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ), - ) -} - -fn create_draft_release() -> NamedJob { - fn generate_release_notes() -> Step { - named::bash( - r#"node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md"#, - ) - } - - fn create_release() -> Step { - named::bash("script/create-draft-release target/release-notes.md") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - // We need to fetch more than one commit so that `script/draft-release-notes` - // is able to diff between the current and previous tag. - // - // 25 was chosen arbitrarily. - .add_step( - steps::checkout_repo() - .with_custom_fetch_depth(25) - .with_ref("${{ github.ref }}"), - ) - .add_step(steps::script("script/determine-release-channel")) - .add_step(steps::script("mkdir -p target/")) - .add_step(generate_release_notes()) - .add_step(create_release()), - ) -} - -pub(crate) fn push_release_update_notification( - create_draft_release_job: &NamedJob, - upload_assets_job: &NamedJob, - validate_assets_job: &NamedJob, - auto_release_preview: &NamedJob, - test_jobs: &[&NamedJob], - bundle_jobs: &ReleaseBundleJobs, -) -> NamedJob { - fn env_name(name: &str) -> String { - format!("RESULT_{}", name.to_uppercase()) - } - - let all_job_names: Vec<&str> = test_jobs - .iter() - .map(|j| j.name.as_ref()) - .chain(bundle_jobs.jobs().into_iter().map(|j| j.name.as_ref())) - .collect(); - - let env_entries = [ - ( - "DRAFT_RESULT".into(), - format!("${{{{ needs.{}.result }}}}", create_draft_release_job.name), - ), - ( - "UPLOAD_RESULT".into(), - format!("${{{{ needs.{}.result }}}}", upload_assets_job.name), - ), - ( - "VALIDATE_RESULT".into(), - format!("${{{{ needs.{}.result }}}}", validate_assets_job.name), - ), - ( - "AUTO_RELEASE_RESULT".into(), - format!("${{{{ needs.{}.result }}}}", auto_release_preview.name), - ), - ("RUN_URL".into(), CURRENT_ACTION_RUN_URL.to_string()), - ] - .into_iter() - .chain( - all_job_names - .iter() - .map(|name| (env_name(name), format!("${{{{ needs.{name}.result }}}}"))), - ); - - let failure_checks = all_job_names - .iter() - .map(|name| { - format!( - "if [ \"${env_name}\" == \"failure\" ];then FAILED_JOBS=\"$FAILED_JOBS {name}\"; fi", - env_name = env_name(name) - ) - }) - .collect::>() - .join("\n "); - - let notification_script = formatdoc! {r#" - TAG="$GITHUB_REF_NAME" - - if [ "$DRAFT_RESULT" == "failure" ]; then - echo "❌ Draft release creation failed for $TAG: $RUN_URL" - else - RELEASE_URL=$(gh release view "$TAG" --repo=zed-industries/zed --json url -q '.url') - if [ "$UPLOAD_RESULT" == "failure" ]; then - echo "❌ Release asset upload failed for $TAG: $RELEASE_URL" - elif [ "$UPLOAD_RESULT" == "cancelled" ] || [ "$UPLOAD_RESULT" == "skipped" ]; then - FAILED_JOBS="" - {failure_checks} - FAILED_JOBS=$(echo "$FAILED_JOBS" | xargs) - if [ "$UPLOAD_RESULT" == "cancelled" ]; then - if [ -n "$FAILED_JOBS" ]; then - echo "❌ Release job for $TAG was cancelled, most likely because tests \`$FAILED_JOBS\` failed: $RUN_URL" - else - echo "❌ Release job for $TAG was cancelled: $RUN_URL" - fi - else - if [ -n "$FAILED_JOBS" ]; then - echo "❌ Tests \`$FAILED_JOBS\` for $TAG failed: $RUN_URL" - else - echo "❌ Tests for $TAG failed: $RUN_URL" - fi - fi - elif [ "$VALIDATE_RESULT" == "failure" ]; then - echo "❌ Release asset validation failed for $TAG (missing assets): $RUN_URL" - elif [ "$AUTO_RELEASE_RESULT" == "success" ]; then - echo "✅ Release $TAG was auto-released successfully: $RELEASE_URL" - elif [ "$AUTO_RELEASE_RESULT" == "failure" ]; then - echo "❌ Auto release failed for $TAG: $RUN_URL" - else - echo "👀 Release $TAG sitting freshly baked in the oven and waiting to be published: $RELEASE_URL" - fi - fi - "#, - }; - - let mut all_deps: Vec<&NamedJob> = vec![ - create_draft_release_job, - upload_assets_job, - validate_assets_job, - auto_release_preview, - ]; - all_deps.extend(test_jobs.iter().copied()); - all_deps.extend(bundle_jobs.jobs()); - - let mut job = dependant_job(&all_deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new("always()")); - - for step in notify_slack(MessageType::Evaluated { - script: notification_script, - env: env_entries.collect(), - }) { - job = job.add_step(step); - } - named::job(job) -} - -pub(crate) fn notify_on_failure(deps: &[&NamedJob]) -> NamedJob { - let failure_message = format!("❌ ${{{{ github.workflow }}}} failed: {CURRENT_ACTION_RUN_URL}"); - - let mut job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new("failure()")); - - for step in notify_slack(MessageType::Static(failure_message)) { - job = job.add_step(step); - } - named::job(job) -} - -pub(crate) enum MessageType { - Static(String), - Evaluated { - script: String, - env: Vec<(String, String)>, - }, -} - -fn notify_slack(message: MessageType) -> Vec> { - match message { - MessageType::Static(message) => vec![send_slack_message(message)], - MessageType::Evaluated { script, env } => { - let (generate_step, generated_message) = generate_slack_message(script, env); - - vec![ - generate_step, - send_slack_message(generated_message.to_string()), - ] - } - } -} - -fn generate_slack_message( - expression: String, - env: Vec<(String, String)>, -) -> (Step, StepOutput) { - let script = formatdoc! {r#" - MESSAGE=$({expression}) - echo "message=$MESSAGE" >> "$GITHUB_OUTPUT" - "# - }; - let mut generate_step = named::bash(&script) - .id("generate-webhook-message") - .add_env(("GH_TOKEN", Context::github().token())); - - for (name, value) in env { - generate_step = generate_step.add_env((name, value)); - } - - let output = StepOutput::new(&generate_step, "message"); - - (generate_step, output) -} - -fn send_slack_message(message: String) -> Step { - named::bash( - r#"curl -X POST -H 'Content-type: application/json' --data "$(jq -n --arg text "$SLACK_MESSAGE" '{"text": $text}')" "$SLACK_WEBHOOK""# - ) - .add_env(("SLACK_WEBHOOK", vars::SLACK_WEBHOOK_WORKFLOW_FAILURES)) - .add_env(("SLACK_MESSAGE", message)) -} diff --git a/tooling/xtask/src/tasks/workflows/release_nightly.rs b/tooling/xtask/src/tasks/workflows/release_nightly.rs deleted file mode 100644 index 277db38bee..0000000000 --- a/tooling/xtask/src/tasks/workflows/release_nightly.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::tasks::workflows::{ - nix_build::build_nix, - release::{ - ReleaseBundleJobs, create_sentry_release, download_workflow_artifacts, notify_on_failure, - prep_release_artifacts, - }, - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests::{clippy, run_platform_tests_no_filter}, - runners::{Arch, Platform, ReleaseChannel}, - steps::{CommonJobConditions, FluentBuilder, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; - -/// Generates the release_nightly.yml workflow -pub fn release_nightly() -> Workflow { - let style = check_style(); - // run only on windows as that's our fastest platform right now. - let tests = run_platform_tests_no_filter(Platform::Windows); - let clippy_job = clippy(Platform::Windows, None); - let nightly = Some(ReleaseChannel::Nightly); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, nightly, &[&style, &tests, &clippy_job]), - linux_x86_64: bundle_linux(Arch::X86_64, nightly, &[&style, &tests, &clippy_job]), - mac_aarch64: bundle_mac(Arch::AARCH64, nightly, &[&style, &tests, &clippy_job]), - mac_x86_64: bundle_mac(Arch::X86_64, nightly, &[&style, &tests, &clippy_job]), - windows_aarch64: bundle_windows(Arch::AARCH64, nightly, &[&style, &tests, &clippy_job]), - windows_x86_64: bundle_windows(Arch::X86_64, nightly, &[&style, &tests, &clippy_job]), - }; - - let nix_linux_x86 = build_nix( - Platform::Linux, - Arch::X86_64, - "default", - None, - &[&style, &tests], - ); - let nix_mac_arm = build_nix( - Platform::Mac, - Arch::AARCH64, - "default", - None, - &[&style, &tests], - ); - let update_nightly_tag = update_nightly_tag_job(&bundle); - let notify_on_failure = notify_on_failure(&bundle.jobs()); - - named::workflow() - .on(Event::default() - // Fire every day at 7:00am UTC (Roughly before EU workday and after US workday) - .schedule([Schedule::new("0 7 * * *")]) - .push(Push::default().add_tag("nightly"))) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(style.name, style.job) - .add_job(tests.name, tests.job) - .add_job(clippy_job.name, clippy_job.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(nix_linux_x86.name, nix_linux_x86.job) - .add_job(nix_mac_arm.name, nix_mac_arm.job) - .add_job(update_nightly_tag.name, update_nightly_tag.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -fn check_style() -> NamedJob { - let job = release_job(&[]) - .runs_on(runners::MAC_DEFAULT) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(steps::cargo_fmt()) - .add_step(steps::script("./script/clippy")); - - named::job(job) -} - -fn release_job(deps: &[&NamedJob]) -> Job { - let job = Job::default() - .with_repository_owner_guard() - .timeout_minutes(60u32); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -fn update_nightly_tag_job(bundle: &ReleaseBundleJobs) -> NamedJob { - fn update_nightly_tag() -> Step { - named::bash(indoc::indoc! {r#" - if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then - echo "Nightly tag already points to current commit. Skipping tagging." - exit 0 - fi - git config user.name github-actions - git config user.email github-actions@github.com - git tag -f nightly - git push origin nightly --force - "#}) - } - - NamedJob { - name: "update_nightly_tag".to_owned(), - job: steps::release_job(&bundle.jobs()) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("./script/upload-nightly") - .add_env(( - "DIGITALOCEAN_SPACES_ACCESS_KEY", - vars::DIGITALOCEAN_SPACES_ACCESS_KEY, - )) - .add_env(( - "DIGITALOCEAN_SPACES_SECRET_KEY", - vars::DIGITALOCEAN_SPACES_SECRET_KEY, - )), - ) - .add_step(update_nightly_tag()) - .add_step(create_sentry_release()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs b/tooling/xtask/src/tasks/workflows/run_agent_evals.rs deleted file mode 100644 index 521f419d9b..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs +++ /dev/null @@ -1,169 +0,0 @@ -use gh_workflow::{Event, Expression, Job, Run, Step, Strategy, Use, Workflow, WorkflowDispatch}; -use serde_json::json; - -use crate::tasks::workflows::{ - runners::{self, Platform}, - steps::{self, FluentBuilder as _, NamedJob, named, setup_cargo_config}, - vars::{self, WorkflowInput}, -}; - -pub(crate) fn run_agent_evals() -> Workflow { - let agent_evals = agent_evals(); - let model_name = WorkflowInput::string("model_name", None); - - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default().add_input(model_name.name, model_name.input()), - )) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(agent_evals.name, agent_evals.job) -} - -pub(crate) fn run_unit_evals() -> Workflow { - let model_name = WorkflowInput::string("model_name", None); - let commit_sha = WorkflowInput::string("commit_sha", None); - - let unit_evals = named::job(unit_evals(Some(&commit_sha))); - - named::workflow() - .name("run_unit_evals") - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(model_name.name, model_name.input()) - .add_input(commit_sha.name, commit_sha.input()), - )) - .concurrency(vars::allow_concurrent_runs()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(unit_evals.name, unit_evals.job) -} - -fn add_api_keys(step: Step) -> Step { - step.add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) -} - -fn agent_evals() -> NamedJob { - fn run_eval() -> Step { - named::bash( - "cargo run --package=eval -- --repetitions=8 --concurrency=1 --model \"${MODEL_NAME}\"", - ) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .timeout_minutes(60_u32 * 10) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(setup_cargo_config(Platform::Linux)) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(steps::script("cargo build --package=eval")) - .add_step(add_api_keys(run_eval())) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -pub(crate) fn run_cron_unit_evals() -> Workflow { - let unit_evals = cron_unit_evals(); - - named::workflow() - .name("run_cron_unit_evals") - .on(Event::default() - // .schedule([ - // // GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. - // Schedule::default().cron("47 1 * * 2"), - // ]) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_job(unit_evals.name, unit_evals.job) -} - -fn cron_unit_evals() -> NamedJob { - fn send_failure_to_slack() -> Step { - named::uses( - "slackapi", - "slack-github-action", - "b0fa283ad8fea605de13dc3f449259339835fc52", - ) - .if_condition(Expression::new("${{ failure() }}")) - .add_with(("method", "chat.postMessage")) - .add_with(("token", vars::SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN)) - .add_with(("payload", indoc::indoc!{r#" - channel: C04UDRNNJFQ - text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}" - "#})) - } - - named::job(cron_unit_evals_job().add_step(send_failure_to_slack())) -} - -const UNIT_EVAL_MODELS: &[&str] = &[ - "anthropic/claude-sonnet-4-5-latest", - "anthropic/claude-opus-4-5-latest", - "google/gemini-3.1-pro", - "openai/gpt-5", -]; - -fn cron_unit_evals_job() -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")) - .add_env(("ZED_AGENT_MODEL", "${{ matrix.model }}")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .strategy(Strategy::default().fail_fast(false).matrix(json!({ - "model": UNIT_EVAL_MODELS - }))) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(script_step) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} - -fn unit_evals(commit: Option<&WorkflowInput>) -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(match commit { - Some(commit) => script_step.add_env(("UNIT_EVAL_COMMIT", commit)), - None => script_step, - }) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} diff --git a/tooling/xtask/src/tasks/workflows/run_bundling.rs b/tooling/xtask/src/tasks/workflows/run_bundling.rs deleted file mode 100644 index 6b9d3b9e36..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_bundling.rs +++ /dev/null @@ -1,226 +0,0 @@ -use std::path::Path; - -use crate::tasks::workflows::{ - nix_build::build_nix, - release::ReleaseBundleJobs, - runners::{Arch, Platform, ReleaseChannel}, - steps::{DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, dependant_job, named}, - vars::{assets, bundle_envs}, -}; - -use super::{runners, steps}; -use gh_workflow::*; -use indoc::indoc; - -pub fn run_bundling() -> Workflow { - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, None, &[]), - linux_x86_64: bundle_linux(Arch::X86_64, None, &[]), - mac_aarch64: bundle_mac(Arch::AARCH64, None, &[]), - mac_x86_64: bundle_mac(Arch::X86_64, None, &[]), - windows_aarch64: bundle_windows(Arch::AARCH64, None, &[]), - windows_x86_64: bundle_windows(Arch::X86_64, None, &[]), - }; - let nix_linux_x86_64 = nix_job(Platform::Linux, Arch::X86_64); - let nix_mac_aarch64 = nix_job(Platform::Mac, Arch::AARCH64); - named::workflow() - .on(Event::default().pull_request( - PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), - )) - .concurrency( - Concurrency::new(Expression::new( - "${{ github.workflow }}-${{ github.head_ref || github.ref }}", - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(nix_linux_x86_64.name, nix_linux_x86_64.job) - .add_job(nix_mac_aarch64.name, nix_mac_aarch64.job) -} - -fn nix_job(platform: Platform, arch: Arch) -> NamedJob { - let mut job = build_nix( - platform, - arch, - "default", - // don't push PR builds to the cache - Some("-zed-editor-[0-9.]*"), - &[], - ); - job.job = job.job.cond(Expression::new(format!( - "{} && ((github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || \ - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')))", - DEFAULT_REPOSITORY_OWNER_GUARD - ))); - job -} - -fn bundle_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .when(deps.len() == 0, |job| - job.cond(Expression::new( - indoc! { - r#"(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))"#, - }))) - .timeout_minutes(60u32) -} - -pub(crate) fn bundle_mac( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - pub fn bundle_mac(arch: Arch) -> Step { - named::bash(&format!("./script/bundle-mac {arch}-apple-darwin")) - } - let platform = Platform::Mac; - let artifact_name = match arch { - Arch::X86_64 => assets::MAC_X86_64, - Arch::AARCH64 => assets::MAC_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_MAC_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_MAC_AARCH64, - }; - NamedJob { - name: format!("bundle_mac_{arch}"), - job: bundle_job(deps) - .runs_on(runners::MAC_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_node()) - .add_step(steps::setup_sentry()) - .add_step(steps::clear_target_dir_if_large(runners::Platform::Mac)) - .add_step(bundle_mac(arch)) - .add_step(upload_artifact(&format!( - "target/{arch}-apple-darwin/release/{artifact_name}" - ))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub fn upload_artifact(path: &str) -> Step { - let name = Path::new(path).file_name().unwrap().to_str().unwrap(); - Step::new(format!("@actions/upload-artifact {}", name)) - .uses( - "actions", - "upload-artifact", - "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 - ) - // N.B. "name" is the name for the asset. The uploaded - // file retains its filename. - .add_with(("name", name)) - .add_with(("path", path)) - .add_with(("if-no-files-found", "error")) -} - -pub(crate) fn bundle_linux( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Linux; - let artifact_name = match arch { - Arch::X86_64 => assets::LINUX_X86_64, - Arch::AARCH64 => assets::LINUX_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_LINUX_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_LINUX_AARCH64, - }; - NamedJob { - name: format!("bundle_linux_{arch}"), - job: bundle_job(deps) - .runs_on(arch.linux_bundler()) - .envs(bundle_envs(platform)) - .add_env(Env::new("CC", "clang-18")) - .add_env(Env::new("CXX", "clang++-18")) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .map(steps::install_linux_dependencies) - .add_step(steps::script("./script/bundle-linux")) - .add_step(upload_artifact(&format!("target/release/{artifact_name}"))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub(crate) fn bundle_windows( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Windows; - pub fn bundle_windows(arch: Arch) -> Step { - let step = match arch { - Arch::X86_64 => named::pwsh("script/bundle-windows.ps1 -Architecture x86_64"), - Arch::AARCH64 => named::pwsh("script/bundle-windows.ps1 -Architecture aarch64"), - }; - step.working_directory("${{ env.ZED_WORKSPACE }}") - } - let artifact_name = match arch { - Arch::X86_64 => assets::WINDOWS_X86_64, - Arch::AARCH64 => assets::WINDOWS_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_WINDOWS_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_WINDOWS_AARCH64, - }; - NamedJob { - name: format!("bundle_windows_{arch}"), - job: bundle_job(deps) - .runs_on(runners::WINDOWS_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .add_step(bundle_windows(arch)) - .add_step(upload_artifact(&format!("target/{artifact_name}"))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -fn set_release_channel(platform: Platform, release_channel: ReleaseChannel) -> Step { - match release_channel { - ReleaseChannel::Nightly => set_release_channel_to_nightly(platform), - } -} - -fn set_release_channel_to_nightly(platform: Platform) -> Step { - match platform { - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - "#}), - Platform::Windows => named::pwsh(indoc::indoc! {r#" - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - "#}) - .working_directory("${{ env.ZED_WORKSPACE }}"), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs deleted file mode 100644 index a43b36e975..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ /dev/null @@ -1,788 +0,0 @@ -use gh_workflow::{ - Concurrency, Container, Event, Expression, Input, Job, Level, Permissions, Port, PullRequest, - Push, Run, Step, Strategy, Use, UsesJob, Workflow, -}; -use indexmap::IndexMap; -use indoc::formatdoc; -use serde_json::json; - -use crate::tasks::workflows::{ - steps::{ - CommonJobConditions, cache_rust_dependencies_namespace, repository_owner_guard_expression, - use_clang, - }, - vars::{self, PathCondition}, -}; - -use super::{ - runners::{self, Arch, Platform}, - steps::{self, FluentBuilder, NamedJob, named, release_job}, -}; - -pub(crate) fn run_tests() -> Workflow { - // Specify anything which should potentially skip full test suite in this regex: - // - docs/ - // - script/update_top_ranking_issues/ - // - .github/ISSUE_TEMPLATE/ - // - .github/workflows/ (except .github/workflows/ci.yml) - // - extensions/ (these have their own test workflow) - let should_run_tests = PathCondition::inverted( - "run_tests", - r"^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests))|extensions/)", - ); - let should_check_docs = PathCondition::new("run_docs", r"^(docs/|crates/.*\.rs)"); - let should_check_scripts = PathCondition::new( - "run_action_checks", - r"^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/", - ); - let should_check_licences = - PathCondition::new("run_licenses", r"^(Cargo.lock|script/.*licenses)"); - - let orchestrate = orchestrate(&[ - &should_check_scripts, - &should_check_docs, - &should_check_licences, - &should_run_tests, - ]); - - let mut jobs = vec![ - orchestrate, - check_style(), - should_run_tests.guard(clippy(Platform::Windows, None)), - should_run_tests.guard(clippy(Platform::Linux, None)), - should_run_tests.guard(clippy(Platform::Mac, None)), - should_run_tests.guard(clippy(Platform::Mac, Some(Arch::X86_64))), - should_run_tests.guard(run_platform_tests(Platform::Windows)), - should_run_tests.guard(run_platform_tests(Platform::Linux)), - should_run_tests.guard(run_platform_tests(Platform::Mac)), - should_run_tests.guard(doctests()), - should_run_tests.guard(check_workspace_binaries()), - should_run_tests.guard(check_wasm()), - should_run_tests.guard(check_dependencies()), // could be more specific here? - should_check_docs.guard(check_docs()), - should_check_licences.guard(check_licenses()), - should_check_scripts.guard(check_scripts()), - ]; - let ext_tests = extension_tests(); - let tests_pass = tests_pass(&jobs, &[&ext_tests.name]); - - jobs.push(should_run_tests.guard(check_postgres_and_protobuf_migrations())); // could be more specific here? - - named::workflow() - .add_event( - Event::default() - .push( - Push::default() - .add_branch("main") - .add_branch("v[0-9]+.[0-9]+.x"), - ) - .pull_request(PullRequest::default().add_branch("**")), - ) - .concurrency( - Concurrency::default() - .group(concat!( - "${{ github.workflow }}-${{ github.ref_name }}-", - "${{ github.ref_name == 'main' && github.sha || 'anysha' }}" - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .map(|mut workflow| { - for job in jobs { - workflow = workflow.add_job(job.name, job.job) - } - workflow - }) - .add_job(ext_tests.name, ext_tests.job) - .add_job(tests_pass.name, tests_pass.job) -} - -/// Controls which features `orchestrate_impl` includes in the generated script. -#[derive(PartialEq, Eq)] -enum OrchestrateTarget { - /// For the main Zed repo: includes the cargo package filter and extension - /// change detection, but no working-directory scoping. - ZedRepo, - /// For individual extension repos: scopes changed-file detection to the - /// working directory, with no package filter or extension detection. - Extension, -} - -// Generates a bash script that checks changed files against regex patterns -// and sets GitHub output variables accordingly -pub fn orchestrate(rules: &[&PathCondition]) -> NamedJob { - orchestrate_impl(rules, OrchestrateTarget::ZedRepo) -} - -pub fn orchestrate_for_extension(rules: &[&PathCondition]) -> NamedJob { - orchestrate_impl(rules, OrchestrateTarget::Extension) -} - -fn orchestrate_impl(rules: &[&PathCondition], target: OrchestrateTarget) -> NamedJob { - let name = "orchestrate".to_owned(); - let step_name = "filter".to_owned(); - let mut script = String::new(); - - script.push_str(indoc::indoc! {r#" - set -euo pipefail - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV="$(git rev-parse HEAD~1)" - else - echo "In a PR context comparing to pull_request.base.ref" - git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" - fi - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA")" - - "#}); - - if target == OrchestrateTarget::Extension { - script.push_str(indoc::indoc! {r#" - # When running from a subdirectory, git diff returns repo-root-relative paths. - # Filter to only files within the current working directory and strip the prefix. - REPO_SUBDIR="$(git rev-parse --show-prefix)" - REPO_SUBDIR="${REPO_SUBDIR%/}" - if [ -n "$REPO_SUBDIR" ]; then - CHANGED_FILES="$(echo "$CHANGED_FILES" | grep "^${REPO_SUBDIR}/" | sed "s|^${REPO_SUBDIR}/||" || true)" - fi - - "#}); - } - - script.push_str(indoc::indoc! {r#" - check_pattern() { - local output_name="$1" - local pattern="$2" - local grep_arg="$3" - - echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \ - echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \ - echo "${output_name}=false" >> "$GITHUB_OUTPUT" - } - - "#}); - - let mut outputs = IndexMap::new(); - - if target == OrchestrateTarget::ZedRepo { - script.push_str(indoc::indoc! {r#" - # Check for changes that require full rebuild (no filter) - # Direct pushes to main/stable/preview always run full suite - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not a PR, running full test suite" - echo "changed_packages=" >> "$GITHUB_OUTPUT" - elif echo "$CHANGED_FILES" | grep -qP '^(rust-toolchain\.toml|\.cargo/|\.github/|Cargo\.(toml|lock)$)'; then - echo "Toolchain, cargo config, or root Cargo files changed, will run all tests" - echo "changed_packages=" >> "$GITHUB_OUTPUT" - else - # Extract changed directories from file paths - CHANGED_DIRS=$(echo "$CHANGED_FILES" | \ - grep -oP '^(crates|tooling)/\K[^/]+' | \ - sort -u || true) - - # Build directory-to-package mapping using cargo metadata - DIR_TO_PKG=$(cargo metadata --format-version=1 --no-deps 2>/dev/null | \ - jq -r '.packages[] | select(.manifest_path | test("crates/|tooling/")) | "\(.manifest_path | capture("(crates|tooling)/(?[^/]+)") | .dir)=\(.name)"') - - # Map directory names to package names - FILE_CHANGED_PKGS="" - for dir in $CHANGED_DIRS; do - pkg=$(echo "$DIR_TO_PKG" | grep "^${dir}=" | cut -d= -f2 | head -1) - if [ -n "$pkg" ]; then - FILE_CHANGED_PKGS=$(printf '%s\n%s' "$FILE_CHANGED_PKGS" "$pkg") - else - # Fall back to directory name if no mapping found - FILE_CHANGED_PKGS=$(printf '%s\n%s' "$FILE_CHANGED_PKGS" "$dir") - fi - done - FILE_CHANGED_PKGS=$(echo "$FILE_CHANGED_PKGS" | grep -v '^$' | sort -u || true) - - # If assets/ changed, add crates that depend on those assets - if echo "$CHANGED_FILES" | grep -qP '^assets/'; then - FILE_CHANGED_PKGS=$(printf '%s\n%s\n%s\n%s' "$FILE_CHANGED_PKGS" "settings" "storybook" "assets" | sort -u) - fi - - # Combine all changed packages - ALL_CHANGED_PKGS=$(echo "$FILE_CHANGED_PKGS" | grep -v '^$' || true) - - if [ -z "$ALL_CHANGED_PKGS" ]; then - echo "No package changes detected, will run all tests" - echo "changed_packages=" >> "$GITHUB_OUTPUT" - else - # Build nextest filterset with rdeps for each package - FILTERSET=$(echo "$ALL_CHANGED_PKGS" | \ - sed 's/.*/rdeps(&)/' | \ - tr '\n' '|' | \ - sed 's/|$//') - echo "Changed packages filterset: $FILTERSET" - echo "changed_packages=$FILTERSET" >> "$GITHUB_OUTPUT" - fi - fi - - "#}); - - outputs.insert( - "changed_packages".to_owned(), - format!("${{{{ steps.{}.outputs.changed_packages }}}}", step_name), - ); - } - - for rule in rules { - assert!( - rule.set_by_step - .borrow_mut() - .replace(name.clone()) - .is_none() - ); - assert!( - outputs - .insert( - rule.name.to_owned(), - format!("${{{{ steps.{}.outputs.{} }}}}", step_name, rule.name) - ) - .is_none() - ); - - let grep_arg = if rule.invert { "-qvP" } else { "-qP" }; - script.push_str(&format!( - "check_pattern \"{}\" '{}' {}\n", - rule.name, rule.pattern, grep_arg - )); - } - - if target == OrchestrateTarget::ZedRepo { - script.push_str(DETECT_CHANGED_EXTENSIONS_SCRIPT); - script.push_str("echo \"changed_extensions=$EXTENSIONS_JSON\" >> \"$GITHUB_OUTPUT\"\n"); - - outputs.insert( - "changed_extensions".to_owned(), - format!("${{{{ steps.{}.outputs.changed_extensions }}}}", step_name), - ); - } - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .outputs(outputs) - .add_step(steps::checkout_repo().with_deep_history_on_non_main()) - .add_step(Step::new(step_name.clone()).run(script).id(step_name)); - - NamedJob { name, job } -} - -pub fn tests_pass(jobs: &[NamedJob], extra_job_names: &[&str]) -> NamedJob { - let mut script = String::from(indoc::indoc! {r#" - set +x - EXIT_CODE=0 - - check_result() { - echo "* $1: $2" - if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi - } - - "#}); - - let all_names: Vec<&str> = jobs - .iter() - .map(|job| job.name.as_str()) - .chain(extra_job_names.iter().copied()) - .collect(); - - let env_entries: Vec<_> = all_names - .iter() - .map(|name| { - let env_name = format!("RESULT_{}", name.to_uppercase()); - let env_value = format!("${{{{ needs.{}.result }}}}", name); - (env_name, env_value) - }) - .collect(); - - script.push_str( - &all_names - .iter() - .zip(env_entries.iter()) - .map(|(name, (env_name, _))| format!("check_result \"{}\" \"${}\"", name, env_name)) - .collect::>() - .join("\n"), - ); - - script.push_str("\n\nexit $EXIT_CODE\n"); - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .needs( - all_names - .iter() - .map(|name| name.to_string()) - .collect::>(), - ) - .cond(repository_owner_guard_expression(true)) - .add_step( - env_entries - .into_iter() - .fold(named::bash(&script), |step, env_item| { - step.add_env(env_item) - }), - ); - - named::job(job) -} - -/// Bash script snippet that detects changed extension directories from `$CHANGED_FILES`. -/// Assumes `$CHANGED_FILES` is already set. Sets `$EXTENSIONS_JSON` to a JSON array of -/// changed extension paths. Callers are responsible for writing the result to `$GITHUB_OUTPUT`. -pub(crate) const DETECT_CHANGED_EXTENSIONS_SCRIPT: &str = indoc::indoc! {r#" - # Detect changed extension directories (excluding extensions/workflows) - CHANGED_EXTENSIONS=$(echo "$CHANGED_FILES" | grep -oP '^extensions/[^/]+(?=/)' | sort -u | grep -v '^extensions/workflows$' || true) - if [ -n "$CHANGED_EXTENSIONS" ]; then - EXTENSIONS_JSON=$(echo "$CHANGED_EXTENSIONS" | jq -R -s -c 'split("\n") | map(select(length > 0))') - else - EXTENSIONS_JSON="[]" - fi -"#}; - -const TS_QUERY_LS_FILE: &str = "ts_query_ls-x86_64-unknown-linux-gnu.tar.gz"; -const CI_TS_QUERY_RELEASE: &str = "tags/v3.15.1"; - -pub(crate) fn fetch_ts_query_ls() -> Step { - named::uses( - "dsaltares", - "fetch-gh-release-asset", - "aa37ae5c44d3c9820bc12fe675e8670ecd93bd1c", - ) // v1.1.1 - .add_with(("repo", "ribru17/ts_query_ls")) - .add_with(("version", CI_TS_QUERY_RELEASE)) - .add_with(("file", TS_QUERY_LS_FILE)) -} - -pub(crate) fn run_ts_query_ls() -> Step { - named::bash(formatdoc!( - r#"tar -xf "$GITHUB_WORKSPACE/{TS_QUERY_LS_FILE}" -C "$GITHUB_WORKSPACE" - "$GITHUB_WORKSPACE/ts_query_ls" format --check . || {{ - echo "Found unformatted queries, please format them with ts_query_ls." - echo "For easy use, install the Tree-sitter query extension:" - echo "zed://extension/tree-sitter-query" - false - }}"# - )) -} - -fn check_style() -> NamedJob { - fn check_for_typos() -> Step { - named::uses( - "crate-ci", - "typos", - "2d0ce569feab1f8752f1dde43cc2f2aa53236e06", - ) // v1.40.0 - .with(("config", "./typos.toml")) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::setup_pnpm()) - .add_step(steps::prettier()) - .add_step(steps::cargo_fmt()) - .add_step(steps::script("./script/check-todos")) - .add_step(steps::script("./script/check-keymaps")) - .add_step(check_for_typos()) - .add_step(fetch_ts_query_ls()) - .add_step(run_ts_query_ls()), - ) -} - -fn check_dependencies() -> NamedJob { - fn install_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "install")) - .add_with(("args", "cargo-machete@0.7.0")) - } - - fn run_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "machete")) - } - - fn check_cargo_lock() -> Step { - named::bash("cargo update --locked --workspace") - } - - fn check_vulnerable_dependencies() -> Step { - named::uses( - "actions", - "dependency-review-action", - "67d4f4bd7a9b17a0db54d2a7519187c65e339de8", // v4 - ) - .if_condition(Expression::new("github.event_name == 'pull_request'")) - .with(("license-check", false)) - } - - named::job(use_clang( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(install_cargo_machete()) - .add_step(run_cargo_machete()) - .add_step(check_cargo_lock()) - .add_step(check_vulnerable_dependencies()), - )) -} - -fn check_wasm() -> NamedJob { - fn install_nightly_wasm_toolchain() -> Step { - named::bash( - "rustup toolchain install nightly --component rust-src --target wasm32-unknown-unknown", - ) - } - - fn cargo_check_wasm() -> Step { - named::bash(concat!( - "cargo +nightly -Zbuild-std=std,panic_abort ", - "check --target wasm32-unknown-unknown -p gpui_platform", - )) - .add_env(( - "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS", - "-C target-feature=+atomics,+bulk-memory,+mutable-globals", - )) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(install_nightly_wasm_toolchain()) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(cargo_check_wasm()) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -fn check_workspace_binaries() -> NamedJob { - named::job(use_clang( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(steps::script("cargo build -p collab")) - .add_step(steps::script("cargo build --workspace --bins --examples")) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - )) -} - -pub(crate) fn clippy(platform: Platform, arch: Option) -> NamedJob { - let target = arch.map(|arch| match (platform, arch) { - (Platform::Mac, Arch::X86_64) => "x86_64-apple-darwin", - (Platform::Mac, Arch::AARCH64) => "aarch64-apple-darwin", - _ => unimplemented!("cross-arch clippy not supported for {platform}/{arch}"), - }); - let runner = match platform { - Platform::Windows => runners::WINDOWS_DEFAULT, - Platform::Linux => runners::LINUX_DEFAULT, - Platform::Mac => runners::MAC_DEFAULT, - }; - let mut job = release_job(&[]) - .runs_on(runner) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(platform)) - .when( - platform == Platform::Linux || platform == Platform::Mac, - |this| this.add_step(steps::cache_rust_dependencies_namespace()), - ) - .when( - platform == Platform::Linux, - steps::install_linux_dependencies, - ) - .when_some(target, |this, target| { - this.add_step(steps::install_rustup_target(target)) - }) - .add_step(steps::setup_sccache(platform)) - .add_step(steps::clippy(platform, target)) - .add_step(steps::show_sccache_stats(platform)); - if platform == Platform::Linux { - job = use_clang(job); - } - let name = match arch { - Some(arch) => format!("clippy_{platform}_{arch}"), - None => format!("clippy_{platform}"), - }; - NamedJob { name, job } -} - -pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob { - run_platform_tests_impl(platform, true) -} - -pub(crate) fn run_platform_tests_no_filter(platform: Platform) -> NamedJob { - run_platform_tests_impl(platform, false) -} - -fn run_platform_tests_impl(platform: Platform, filter_packages: bool) -> NamedJob { - let runner = match platform { - Platform::Windows => runners::WINDOWS_DEFAULT, - Platform::Linux => runners::LINUX_DEFAULT, - Platform::Mac => runners::MAC_DEFAULT, - }; - NamedJob { - name: format!("run_tests_{platform}"), - job: release_job(&[]) - .runs_on(runner) - .when(platform == Platform::Linux, |job| { - job.add_service( - "postgres", - Container::new("postgres:15") - .add_env(("POSTGRES_HOST_AUTH_METHOD", "trust")) - .ports(vec![Port::Name("5432:5432".into())]) - .options( - "--health-cmd pg_isready \ - --health-interval 500ms \ - --health-timeout 5s \ - --health-retries 10", - ), - ) - }) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(platform)) - .when(platform == Platform::Mac, |this| { - this.add_step(steps::cache_rust_dependencies_namespace()) - }) - .when(platform == Platform::Linux, |this| { - use_clang(this.add_step(steps::cache_rust_dependencies_namespace())) - }) - .when( - platform == Platform::Linux, - steps::install_linux_dependencies, - ) - .add_step(steps::setup_node()) - .when( - platform == Platform::Linux || platform == Platform::Mac, - |job| job.add_step(steps::cargo_install_nextest()), - ) - .add_step(steps::clear_target_dir_if_large(platform)) - .add_step(steps::setup_sccache(platform)) - .when(filter_packages, |job| { - job.add_step( - steps::cargo_nextest(platform).with_changed_packages_filter("orchestrate"), - ) - }) - .when(!filter_packages, |job| { - job.add_step(steps::cargo_nextest(platform)) - }) - .add_step(steps::show_sccache_stats(platform)) - .add_step(steps::cleanup_cargo_config(platform)), - } -} - -pub(crate) fn check_postgres_and_protobuf_migrations() -> NamedJob { - fn ensure_fresh_merge() -> Step { - named::bash(indoc::indoc! {r#" - if [ -z "$GITHUB_BASE_REF" ]; - then - echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV" - else - git checkout -B temp - git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp" - echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV" - fi - "#}) - } - - fn bufbuild_setup_action() -> Step { - named::uses("bufbuild", "buf-setup-action", "v1") - .add_with(("version", "v1.29.0")) - .add_with(("github_token", vars::GITHUB_TOKEN)) - } - - fn bufbuild_breaking_action() -> Step { - named::uses("bufbuild", "buf-breaking-action", "v1").add_with(("input", "crates/proto/proto/")) - .add_with(("against", "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/")) - } - - fn buf_lint() -> Step { - named::bash("buf lint crates/proto/proto") - } - - fn check_protobuf_formatting() -> Step { - named::bash("buf format --diff --exit-code crates/proto/proto") - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_env(("GIT_AUTHOR_NAME", "Protobuf Action")) - .add_env(("GIT_AUTHOR_EMAIL", "ci@zed.dev")) - .add_env(("GIT_COMMITTER_NAME", "Protobuf Action")) - .add_env(("GIT_COMMITTER_EMAIL", "ci@zed.dev")) - .add_step(steps::checkout_repo().with_full_history()) - .add_step(ensure_fresh_merge()) - .add_step(bufbuild_setup_action()) - .add_step(bufbuild_breaking_action()) - .add_step(buf_lint()) - .add_step(check_protobuf_formatting()), - ) -} - -fn doctests() -> NamedJob { - fn run_doctests() -> Step { - named::bash(indoc::indoc! {r#" - cargo test --workspace --doc --no-fail-fast - "#}) - .id("run_doctests") - } - - named::job(use_clang( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::setup_sccache(Platform::Linux)) - .add_step(run_doctests()) - .add_step(steps::show_sccache_stats(Platform::Linux)) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - )) -} - -fn check_licenses() -> NamedJob { - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::script("./script/check-licenses")) - .add_step(steps::script("./script/generate-licenses")), - ) -} - -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( - "bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)", - ) - } - - fn run_actionlint() -> Step { - named::bash(r#""$ACTIONLINT_BIN" -color"#).add_env(( - "ACTIONLINT_BIN", - "${{ steps.get_actionlint.outputs.executable }}", - )) - } - - fn run_shellcheck() -> Step { - named::bash("./script/shellcheck-scripts error") - } - - fn check_xtask_workflows() -> Step { - named::bash(indoc::indoc! {r#" - cargo xtask workflows - if ! git diff --exit-code .github; then - echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'" - echo "Please run 'cargo xtask workflows' locally and commit the changes" - exit 1 - fi - "#}) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(run_shellcheck()) - .add_step(download_actionlint().id("get_actionlint")) - .add_step(run_actionlint()) - .add_step(cache_rust_dependencies_namespace()) - .add_step(check_xtask_workflows()), - ) -} - -fn extension_tests() -> NamedJob { - let job = Job::default() - .needs(vec!["orchestrate".to_owned()]) - .cond(Expression::new( - "needs.orchestrate.outputs.changed_extensions != '[]'", - )) - .permissions(Permissions::default().contents(Level::Read)) - .strategy( - Strategy::default() - .fail_fast(false) - // TODO: Remove the limit. We currently need this to workaround the concurrency group issue - // where different matrix jobs would be placed in the same concurrency group and thus cancelled. - .max_parallel(1u32) - .matrix(json!({ - "extension": "${{ fromJson(needs.orchestrate.outputs.changed_extensions) }}" - })), - ) - .uses_local(".github/workflows/extension_tests.yml") - .with(Input::default().add("working-directory", "${{ matrix.extension }}")); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/runners.rs b/tooling/xtask/src/tasks/workflows/runners.rs deleted file mode 100644 index bc4b17aaf6..0000000000 --- a/tooling/xtask/src/tasks/workflows/runners.rs +++ /dev/null @@ -1,69 +0,0 @@ -pub const LINUX_SMALL: Runner = Runner("namespace-profile-2x4-ubuntu-2404"); -pub const LINUX_DEFAULT: Runner = LINUX_XL; -pub const LINUX_XL: Runner = Runner("namespace-profile-16x32-ubuntu-2204"); -pub const LINUX_LARGE: Runner = Runner("namespace-profile-8x16-ubuntu-2204"); -pub const LINUX_MEDIUM: Runner = Runner("namespace-profile-4x8-ubuntu-2204"); - -// Using Ubuntu 20.04 for minimal glibc version -pub const LINUX_X86_BUNDLER: Runner = Runner("namespace-profile-32x64-ubuntu-2004"); -pub const LINUX_ARM_BUNDLER: Runner = Runner("namespace-profile-8x32-ubuntu-2004-arm-m4"); - -// Larger Ubuntu runner with glibc 2.39 for extension bundling -pub const LINUX_LARGE_RAM: Runner = Runner("namespace-profile-8x32-ubuntu-2404"); - -pub const MAC_DEFAULT: Runner = Runner("namespace-profile-mac-large"); -pub const WINDOWS_DEFAULT: Runner = Runner("self-32vcpu-windows-2022"); - -pub struct Runner(&'static str); - -impl Into for Runner { - fn into(self) -> gh_workflow::RunsOn { - self.0.into() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Arch { - X86_64, - AARCH64, -} - -impl std::fmt::Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Arch::X86_64 => write!(f, "x86_64"), - Arch::AARCH64 => write!(f, "aarch64"), - } - } -} - -impl Arch { - pub fn linux_bundler(&self) -> Runner { - match self { - Arch::X86_64 => LINUX_X86_BUNDLER, - Arch::AARCH64 => LINUX_ARM_BUNDLER, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Platform { - Windows, - Linux, - Mac, -} - -impl std::fmt::Display for Platform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Platform::Windows => write!(f, "windows"), - Platform::Linux => write!(f, "linux"), - Platform::Mac => write!(f, "mac"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ReleaseChannel { - Nightly, -} diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs deleted file mode 100644 index 2593d5dd0e..0000000000 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ /dev/null @@ -1,505 +0,0 @@ -use gh_workflow::*; -use serde_json::Value; - -use crate::tasks::workflows::{runners::Platform, vars, vars::StepOutput}; - -pub(crate) fn use_clang(job: Job) -> Job { - job.add_env(Env::new("CC", "clang")) - .add_env(Env::new("CXX", "clang++")) -} - -const SCCACHE_R2_BUCKET: &str = "sccache-zed"; - -pub(crate) const BASH_SHELL: &str = "bash -euxo pipefail {0}"; -// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell -pub const PWSH_SHELL: &str = "pwsh"; - -pub(crate) struct Nextest(Step); - -pub(crate) fn cargo_nextest(platform: Platform) -> Nextest { - Nextest(named::run( - platform, - "cargo nextest run --workspace --no-fail-fast --no-tests=warn", - )) -} - -impl Nextest { - #[allow(dead_code)] - pub(crate) fn with_filter_expr(mut self, filter_expr: &str) -> Self { - if let Some(nextest_command) = self.0.value.run.as_mut() { - nextest_command.push_str(&format!(r#" -E "{filter_expr}""#)); - } - self - } - - pub(crate) fn with_changed_packages_filter(mut self, orchestrate_job: &str) -> Self { - if let Some(nextest_command) = self.0.value.run.as_mut() { - nextest_command.push_str(&format!( - r#"${{{{ needs.{orchestrate_job}.outputs.changed_packages && format(' -E "{{0}}"', needs.{orchestrate_job}.outputs.changed_packages) || '' }}}}"# - )); - } - self - } -} - -impl From for Step { - fn from(value: Nextest) -> Self { - value.0 - } -} - -#[derive(Default)] -enum FetchDepth { - #[default] - Shallow, - Full, - Custom(serde_json::Value), -} - -#[derive(Default)] -pub(crate) struct CheckoutStep { - fetch_depth: FetchDepth, - name: Option, - token: Option, - path: Option, - repository: Option, - ref_: Option, -} - -impl CheckoutStep { - pub fn with_full_history(mut self) -> Self { - self.fetch_depth = FetchDepth::Full; - self - } - - pub fn with_custom_name(mut self, name: &str) -> Self { - self.name = Some(name.to_string()); - self - } - - pub fn with_custom_fetch_depth(mut self, fetch_depth: impl Into) -> Self { - self.fetch_depth = FetchDepth::Custom(fetch_depth.into()); - self - } - - /// Sets `fetch-depth` to `2` on the main branch and `350` on all other branches. - pub fn with_deep_history_on_non_main(self) -> Self { - self.with_custom_fetch_depth("${{ github.ref == 'refs/heads/main' && 2 || 350 }}") - } - - pub fn with_token(mut self, token: &StepOutput) -> Self { - self.token = Some(token.to_string()); - self - } - - pub fn with_path(mut self, path: &str) -> Self { - self.path = Some(path.to_string()); - self - } - - pub fn with_repository(mut self, repository: &str) -> Self { - self.repository = Some(repository.to_string()); - self - } - - pub fn with_ref(mut self, ref_: impl ToString) -> Self { - self.ref_ = Some(ref_.to_string()); - self - } -} - -impl From for Step { - fn from(value: CheckoutStep) -> Self { - Step::new(value.name.unwrap_or("steps::checkout_repo".to_string())) - .uses( - "actions", - "checkout", - "11bd71901bbe5b1630ceea73d27597364c9af683", // v4 - ) - // prevent checkout action from running `git clean -ffdx` which - // would delete the target directory - .add_with(("clean", false)) - .map(|step| match value.fetch_depth { - FetchDepth::Shallow => step, - FetchDepth::Full => step.add_with(("fetch-depth", 0)), - FetchDepth::Custom(depth) => step.add_with(("fetch-depth", depth)), - }) - .when_some(value.path, |step, path| step.add_with(("path", path))) - .when_some(value.repository, |step, repository| { - step.add_with(("repository", repository)) - }) - .when_some(value.ref_, |step, ref_| step.add_with(("ref", ref_))) - .when_some(value.token, |step, token| step.add_with(("token", token))) - } -} - -pub fn checkout_repo() -> CheckoutStep { - CheckoutStep::default() -} - -pub fn setup_pnpm() -> Step { - named::uses( - "pnpm", - "action-setup", - "fe02b34f77f8bc703788d5817da081398fad5dd2", // v4.0.0 - ) - .add_with(("version", "9")) -} - -pub fn setup_node() -> Step { - named::uses( - "actions", - "setup-node", - "49933ea5288caeca8642d1e84afbd3f7d6820020", // v4 - ) - .add_with(("node-version", "20")) -} - -pub fn setup_sentry() -> Step { - named::uses( - "matbour", - "setup-sentry-cli", - "3e938c54b3018bdd019973689ef984e033b0454b", - ) - .add_with(("token", vars::SENTRY_AUTH_TOKEN)) -} - -pub fn prettier() -> Step { - named::bash("./script/prettier") -} - -pub fn cargo_fmt() -> Step { - named::bash("cargo fmt --all -- --check") -} - -pub fn cargo_install_nextest() -> Step { - named::uses("taiki-e", "install-action", "nextest") -} - -pub fn setup_cargo_config(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - "#}), - - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - "#}), - } -} - -pub fn cleanup_cargo_config(platform: Platform) -> Step { - let step = match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - "#}), - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - rm -rf ./../.cargo - "#}), - }; - - step.if_condition(Expression::new("always()")) -} - -pub fn clear_target_dir_if_large(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clear-target-dir-if-larger-than.ps1 250"), - Platform::Linux => named::bash("./script/clear-target-dir-if-larger-than 250"), - Platform::Mac => named::bash("./script/clear-target-dir-if-larger-than 300"), - } -} - -pub fn clippy(platform: Platform, target: Option<&str>) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clippy.ps1"), - _ => match target { - Some(target) => named::bash(format!("./script/clippy --target {target}")), - None => named::bash("./script/clippy"), - }, - } -} - -pub fn install_rustup_target(target: &str) -> Step { - named::bash(format!("rustup target add {target}")) -} - -pub fn cache_rust_dependencies_namespace() -> Step { - named::uses("namespacelabs", "nscloud-cache-action", "v1") - .add_with(("cache", "rust")) - .add_with(("path", "~/.rustup")) -} - -pub fn setup_sccache(platform: Platform) -> Step { - let step = match platform { - Platform::Windows => named::pwsh("./script/setup-sccache.ps1"), - Platform::Linux | Platform::Mac => named::bash("./script/setup-sccache"), - }; - step.add_env(("R2_ACCOUNT_ID", vars::R2_ACCOUNT_ID)) - .add_env(("R2_ACCESS_KEY_ID", vars::R2_ACCESS_KEY_ID)) - .add_env(("R2_SECRET_ACCESS_KEY", vars::R2_SECRET_ACCESS_KEY)) - .add_env(("SCCACHE_BUCKET", SCCACHE_R2_BUCKET)) -} - -pub fn show_sccache_stats(platform: Platform) -> Step { - match platform { - // Use $env:RUSTC_WRAPPER (absolute path) because GITHUB_PATH changes - // don't take effect until the next step in PowerShell. - // Check if RUSTC_WRAPPER is set first (it won't be for fork PRs without secrets). - Platform::Windows => { - named::pwsh("if ($env:RUSTC_WRAPPER) { & $env:RUSTC_WRAPPER --show-stats }; exit 0") - } - Platform::Linux | Platform::Mac => named::bash("sccache --show-stats || true"), - } -} - -pub fn cache_nix_dependencies_namespace() -> Step { - named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "nix")) -} - -pub fn cache_nix_store_macos() -> Step { - // On macOS, `/nix` is on a read-only root filesystem so nscloud's `cache: nix` - // cannot mount or symlink there. Instead we cache a user-writable directory and - // use nix-store --import/--export in separate steps to transfer store paths. - named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("path", "~/nix-cache")) -} - -pub fn setup_linux() -> Step { - named::bash("./script/linux") -} - -fn download_wasi_sdk() -> Step { - named::bash("./script/download-wasi-sdk") -} - -pub(crate) fn install_linux_dependencies(job: Job) -> Job { - job.add_step(setup_linux()).add_step(download_wasi_sdk()) -} - -pub fn script(name: &str) -> Step { - if name.ends_with(".ps1") { - Step::new(name).run(name).shell(PWSH_SHELL) - } else { - Step::new(name).run(name) - } -} - -pub struct NamedJob { - pub name: String, - pub job: Job, -} - -// impl NamedJob { -// pub fn map(self, f: impl FnOnce(Job) -> Job) -> Self { -// NamedJob { -// name: self.name, -// job: f(self.job), -// } -// } -// } - -pub(crate) const DEFAULT_REPOSITORY_OWNER_GUARD: &str = - "(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')"; - -pub fn repository_owner_guard_expression(trigger_always: bool) -> Expression { - Expression::new(format!( - "{}{}", - DEFAULT_REPOSITORY_OWNER_GUARD, - trigger_always.then_some(" && always()").unwrap_or_default() - )) -} - -pub trait CommonJobConditions: Sized { - fn with_repository_owner_guard(self) -> Self; -} - -impl CommonJobConditions for Job { - fn with_repository_owner_guard(self) -> Self { - self.cond(repository_owner_guard_expression(false)) - } -} - -pub(crate) fn release_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .with_repository_owner_guard() - .timeout_minutes(60u32) -} - -pub(crate) fn dependant_job(deps: &[&NamedJob]) -> Job { - let job = Job::default(); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -impl FluentBuilder for Job {} -impl FluentBuilder for Workflow {} -impl FluentBuilder for Input {} -impl FluentBuilder for Step {} - -/// A helper trait for building complex objects with imperative conditionals in a fluent style. -/// Copied from GPUI to avoid adding GPUI as dependency -/// todo(ci) just put this in gh-workflow -#[allow(unused)] -pub trait FluentBuilder { - /// Imperatively modify self with the given closure. - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - { - f(self) - } - - /// Conditionally modify self with the given closure. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - /// Conditionally modify self with the given closure. - fn when_else( - self, - condition: bool, - then: impl FnOnce(Self) -> Self, - else_fn: impl FnOnce(Self) -> Self, - ) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { else_fn(this) }) - } - - /// Conditionally unwrap and modify self with the given closure, if the given option is Some. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } - /// Conditionally unwrap and modify self with the given closure, if the given option is None. - fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if option.is_some() { this } else { then(this) }) - } -} - -// (janky) helper to generate steps with a name that corresponds -// to the name of the calling function. -pub mod named { - use super::*; - - /// Returns a uses step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn uses(owner: &str, repo: &str, ref_: &str) -> Step { - Step::new(function_name(1)).uses(owner, repo, ref_) - } - - /// Returns a bash-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn bash(script: impl AsRef) -> Step { - Step::new(function_name(1)).run(script.as_ref()) - } - - /// Returns a pwsh-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn pwsh(script: &str) -> Step { - Step::new(function_name(1)).run(script).shell(PWSH_SHELL) - } - - /// Runs the command in either powershell or bash, depending on platform. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn run(platform: Platform, script: &str) -> Step { - match platform { - Platform::Windows => Step::new(function_name(1)).run(script).shell(PWSH_SHELL), - Platform::Linux | Platform::Mac => Step::new(function_name(1)).run(script), - } - } - - /// Returns a Workflow with the same name as the enclosing module with default - /// set for the running shell. - pub fn workflow() -> Workflow { - Workflow::default() - .name( - named::function_name(1) - .split("::") - .collect::>() - .into_iter() - .rev() - .skip(1) - .rev() - .collect::>() - .join("::"), - ) - .defaults(Defaults::default().run(RunDefaults::default().shell(BASH_SHELL))) - } - - /// Returns a Job with the same name as the enclosing function. - /// (note job names may not contain `::`) - pub fn job(job: Job) -> NamedJob { - NamedJob { - name: function_name(1).split("::").last().unwrap().to_owned(), - job, - } - } - - /// Returns the function name N callers above in the stack - /// (typically 1). - /// This only works because xtask always runs debug builds. - pub fn function_name(i: usize) -> String { - let mut name = "".to_string(); - let mut count = 0; - backtrace::trace(|frame| { - if count < i + 3 { - count += 1; - return true; - } - backtrace::resolve_frame(frame, |cb| { - if let Some(s) = cb.name() { - name = s.to_string() - } - }); - false - }); - - name.split("::") - .skip_while(|s| s != &"workflows") - .skip(1) - .collect::>() - .join("::") - } -} - -pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step { - named::bash(r#"git fetch origin "$REF_NAME" && git checkout "$REF_NAME""#) - .add_env(("REF_NAME", ref_name.to_string())) -} - -pub fn authenticate_as_zippy() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "create-github-app-token", - "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", - ) - .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) - .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) - .id("get-app-token"); - let output = StepOutput::new(&step, "token"); - (step, output) -} diff --git a/tooling/xtask/src/tasks/workflows/vars.rs b/tooling/xtask/src/tasks/workflows/vars.rs deleted file mode 100644 index b3f8bdf56e..0000000000 --- a/tooling/xtask/src/tasks/workflows/vars.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::cell::RefCell; - -use gh_workflow::{ - Concurrency, Env, Expression, Step, WorkflowCallInput, WorkflowCallSecret, - WorkflowDispatchInput, -}; - -use crate::tasks::workflows::{runners::Platform, steps::NamedJob}; - -macro_rules! secret { - ($secret_name:ident) => { - pub const $secret_name: &str = concat!("${{ secrets.", stringify!($secret_name), " }}"); - }; -} - -macro_rules! var { - ($var_name:ident) => { - pub const $var_name: &str = concat!("${{ vars.", stringify!($var_name), " }}"); - }; -} - -secret!(ANTHROPIC_API_KEY); -secret!(OPENAI_API_KEY); -secret!(GOOGLE_AI_API_KEY); -secret!(GOOGLE_CLOUD_PROJECT); -secret!(APPLE_NOTARIZATION_ISSUER_ID); -secret!(APPLE_NOTARIZATION_KEY); -secret!(APPLE_NOTARIZATION_KEY_ID); -secret!(AZURE_SIGNING_CLIENT_ID); -secret!(AZURE_SIGNING_CLIENT_SECRET); -secret!(AZURE_SIGNING_TENANT_ID); -secret!(CACHIX_AUTH_TOKEN); -secret!(CLUSTER_NAME); -secret!(DIGITALOCEAN_ACCESS_TOKEN); -secret!(DIGITALOCEAN_SPACES_ACCESS_KEY); -secret!(DIGITALOCEAN_SPACES_SECRET_KEY); -secret!(GITHUB_TOKEN); -secret!(MACOS_CERTIFICATE); -secret!(MACOS_CERTIFICATE_PASSWORD); -secret!(SENTRY_AUTH_TOKEN); -secret!(ZED_CLIENT_CHECKSUM_SEED); -secret!(ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON); -secret!(ZED_SENTRY_MINIDUMP_ENDPOINT); -secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN); -secret!(ZED_ZIPPY_APP_ID); -secret!(ZED_ZIPPY_APP_PRIVATE_KEY); -secret!(DISCORD_WEBHOOK_RELEASE_NOTES); -secret!(WINGET_TOKEN); -secret!(VERCEL_TOKEN); -secret!(SLACK_WEBHOOK_WORKFLOW_FAILURES); -secret!(R2_ACCOUNT_ID); -secret!(R2_ACCESS_KEY_ID); -secret!(R2_SECRET_ACCESS_KEY); - -// todo(ci) make these secrets too... -var!(AZURE_SIGNING_ACCOUNT_NAME); -var!(AZURE_SIGNING_CERT_PROFILE_NAME); -var!(AZURE_SIGNING_ENDPOINT); - -pub fn bundle_envs(platform: Platform) -> Env { - let env = Env::default() - .add("CARGO_INCREMENTAL", 0) - .add("ZED_CLIENT_CHECKSUM_SEED", ZED_CLIENT_CHECKSUM_SEED) - .add("ZED_MINIDUMP_ENDPOINT", ZED_SENTRY_MINIDUMP_ENDPOINT); - - match platform { - Platform::Linux => env, - Platform::Mac => env - .add("MACOS_CERTIFICATE", MACOS_CERTIFICATE) - .add("MACOS_CERTIFICATE_PASSWORD", MACOS_CERTIFICATE_PASSWORD) - .add("APPLE_NOTARIZATION_KEY", APPLE_NOTARIZATION_KEY) - .add("APPLE_NOTARIZATION_KEY_ID", APPLE_NOTARIZATION_KEY_ID) - .add("APPLE_NOTARIZATION_ISSUER_ID", APPLE_NOTARIZATION_ISSUER_ID), - Platform::Windows => env - .add("AZURE_TENANT_ID", AZURE_SIGNING_TENANT_ID) - .add("AZURE_CLIENT_ID", AZURE_SIGNING_CLIENT_ID) - .add("AZURE_CLIENT_SECRET", AZURE_SIGNING_CLIENT_SECRET) - .add("ACCOUNT_NAME", AZURE_SIGNING_ACCOUNT_NAME) - .add("CERT_PROFILE_NAME", AZURE_SIGNING_CERT_PROFILE_NAME) - .add("ENDPOINT", AZURE_SIGNING_ENDPOINT) - .add("FILE_DIGEST", "SHA256") - .add("TIMESTAMP_DIGEST", "SHA256") - .add("TIMESTAMP_SERVER", "http://timestamp.acs.microsoft.com"), - } -} - -pub fn one_workflow_per_non_main_branch() -> Concurrency { - one_workflow_per_non_main_branch_and_token("") -} - -pub fn one_workflow_per_non_main_branch_and_token>(token: T) -> Concurrency { - Concurrency::default() - .group(format!( - concat!( - "${{{{ github.workflow }}}}-${{{{ github.ref_name }}}}-", - "${{{{ github.ref_name == 'main' && github.sha || 'anysha' }}}}{}" - ), - token.as_ref() - )) - .cancel_in_progress(true) -} - -pub(crate) fn allow_concurrent_runs() -> Concurrency { - Concurrency::default() - .group("${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}") - .cancel_in_progress(true) -} - -// Represents a pattern to check for changed files and corresponding output variable -pub struct PathCondition { - pub name: &'static str, - pub pattern: &'static str, - pub invert: bool, - pub set_by_step: RefCell>, -} -impl PathCondition { - pub fn new(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: false, - set_by_step: Default::default(), - } - } - pub fn inverted(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: true, - set_by_step: Default::default(), - } - } - pub fn guard(&self, job: NamedJob) -> NamedJob { - let set_by_step = self - .set_by_step - .borrow() - .clone() - .unwrap_or_else(|| panic!("condition {},is never set", self.name)); - NamedJob { - name: job.name, - job: job - .job - .add_need(set_by_step.clone()) - .cond(Expression::new(format!( - "needs.{}.outputs.{} == 'true'", - &set_by_step, self.name - ))), - } - } -} - -pub(crate) struct StepOutput { - pub name: &'static str, - step_id: String, -} - -impl StepOutput { - pub fn new(step: &Step, name: &'static str) -> Self { - let step_id = step - .value - .id - .clone() - .expect("Steps that produce outputs must have an ID"); - - assert!( - step.value - .run - .as_ref() - .is_none_or(|run_command| run_command.contains(name)), - "Step Output name {name} must occur at least once in run command with ID {step_id}!" - ); - - Self { name, step_id } - } - - pub fn new_unchecked(step: &Step, name: &'static str) -> Self { - let step_id = step - .value - .id - .clone() - .expect("Steps that produce outputs must have an ID"); - - Self { name, step_id } - } - - pub fn expr(&self) -> String { - format!("steps.{}.outputs.{}", self.step_id, self.name) - } - - pub fn as_job_output(self, job: &NamedJob) -> JobOutput { - JobOutput { - job_name: job.name.clone(), - name: self.name, - } - } -} - -impl serde::Serialize for StepOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for StepOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub(crate) struct JobOutput { - job_name: String, - name: &'static str, -} - -impl JobOutput { - pub fn expr(&self) -> String { - format!("needs.{}.outputs.{}", self.job_name, self.name) - } -} - -impl serde::Serialize for JobOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for JobOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub struct WorkflowInput { - pub input_type: &'static str, - pub name: &'static str, - pub default: Option, - pub description: Option, -} - -impl WorkflowInput { - pub fn string(name: &'static str, default: Option) -> Self { - Self { - input_type: "string", - name, - default, - description: None, - } - } - - pub fn bool(name: &'static str, default: Option) -> Self { - Self { - input_type: "boolean", - name, - default: default.as_ref().map(ToString::to_string), - description: None, - } - } - - pub fn description(mut self, description: impl ToString) -> Self { - self.description = Some(description.to_string()); - self - } - - pub fn input(&self) -> WorkflowDispatchInput { - WorkflowDispatchInput { - description: self - .description - .clone() - .unwrap_or_else(|| self.name.to_owned()), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub fn call_input(&self) -> WorkflowCallInput { - WorkflowCallInput { - description: self.name.to_owned(), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub(crate) fn expr(&self) -> String { - format!("inputs.{}", self.name) - } -} - -impl std::fmt::Display for WorkflowInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -impl serde::Serialize for WorkflowInput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub(crate) struct WorkflowSecret { - pub name: &'static str, - description: String, - required: bool, -} - -impl WorkflowSecret { - pub fn new(name: &'static str, description: impl ToString) -> Self { - Self { - name, - description: description.to_string(), - required: true, - } - } - - pub fn secret_configuration(&self) -> WorkflowCallSecret { - WorkflowCallSecret { - description: self.description.clone(), - required: self.required, - } - } -} - -impl std::fmt::Display for WorkflowSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ secrets.{} }}}}", self.name) - } -} - -impl serde::Serialize for WorkflowSecret { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub mod assets { - // NOTE: these asset names also exist in the zed.dev codebase. - pub const MAC_AARCH64: &str = "Zed-aarch64.dmg"; - pub const MAC_X86_64: &str = "Zed-x86_64.dmg"; - pub const LINUX_AARCH64: &str = "zed-linux-aarch64.tar.gz"; - pub const LINUX_X86_64: &str = "zed-linux-x86_64.tar.gz"; - pub const WINDOWS_X86_64: &str = "Zed-x86_64.exe"; - pub const WINDOWS_AARCH64: &str = "Zed-aarch64.exe"; - - pub const REMOTE_SERVER_MAC_AARCH64: &str = "zed-remote-server-macos-aarch64.gz"; - pub const REMOTE_SERVER_MAC_X86_64: &str = "zed-remote-server-macos-x86_64.gz"; - pub const REMOTE_SERVER_LINUX_AARCH64: &str = "zed-remote-server-linux-aarch64.gz"; - pub const REMOTE_SERVER_LINUX_X86_64: &str = "zed-remote-server-linux-x86_64.gz"; - pub const REMOTE_SERVER_WINDOWS_AARCH64: &str = "zed-remote-server-windows-aarch64.zip"; - pub const REMOTE_SERVER_WINDOWS_X86_64: &str = "zed-remote-server-windows-x86_64.zip"; - - pub fn all() -> Vec<&'static str> { - vec![ - MAC_AARCH64, - MAC_X86_64, - LINUX_AARCH64, - LINUX_X86_64, - WINDOWS_X86_64, - WINDOWS_AARCH64, - REMOTE_SERVER_MAC_AARCH64, - REMOTE_SERVER_MAC_X86_64, - REMOTE_SERVER_LINUX_AARCH64, - REMOTE_SERVER_LINUX_X86_64, - REMOTE_SERVER_WINDOWS_AARCH64, - REMOTE_SERVER_WINDOWS_X86_64, - ] - } -} diff --git a/tooling/xtask/src/workspace.rs b/tooling/xtask/src/workspace.rs deleted file mode 100644 index fd71aa6bbd..0000000000 --- a/tooling/xtask/src/workspace.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::{Context as _, Result}; -use cargo_metadata::{Metadata, MetadataCommand}; - -/// Returns the Cargo workspace. -pub fn load_workspace() -> Result { - MetadataCommand::new() - .exec() - .context("failed to load cargo metadata") -}