Try gh-workflow (#41155)

Experimenting with not writing YAML by hand...

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin
2025-10-27 13:39:01 -06:00
committed by GitHub
parent db0f7a8b23
commit 58f07ff709
13 changed files with 514 additions and 90 deletions
+3
View File
@@ -10,9 +10,12 @@ workspace = true
[dependencies]
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
toml_edit.workspace = true
gh-workflow.workspace = true
+2
View File
@@ -20,6 +20,7 @@ enum CliCommand {
PackageConformity(tasks::package_conformity::PackageConformityArgs),
/// Publishes GPUI and its dependencies to crates.io.
PublishGpui(tasks::publish_gpui::PublishGpuiArgs),
Workflows(tasks::workflows::GenerateWorkflowArgs),
}
fn main() -> Result<()> {
@@ -32,5 +33,6 @@ fn main() -> Result<()> {
tasks::package_conformity::run_package_conformity(args)
}
CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args),
CliCommand::Workflows(args) => tasks::workflows::run_workflows(args),
}
}
+1
View File
@@ -2,3 +2,4 @@ pub mod clippy;
pub mod licenses;
pub mod package_conformity;
pub mod publish_gpui;
pub mod workflows;
+33
View File
@@ -0,0 +1,33 @@
use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use std::path::Path;
mod runners;
mod steps;
mod vars;
mod workflows;
use workflows::*;
#[derive(Parser)]
pub struct GenerateWorkflowArgs {}
pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
let dir = Path::new(".github/workflows");
let workflows = vec![("danger.yml", danger()), ("nix.yml", nix())];
fs::create_dir_all(dir)
.with_context(|| format!("Failed to create directory: {}", dir.display()))?;
for (filename, workflow) in workflows {
let content = workflow
.to_string()
.map_err(|e| anyhow::anyhow!("{}: {:?}", filename, e))?;
let content = format!("# generated `cargo xtask workflows`. Do not edit.\n{content}");
let file_path = dir.join(filename);
fs::write(&file_path, content)?;
}
Ok(())
}
@@ -0,0 +1,11 @@
pub const LINUX_CHEAP: Runner = Runner("namespace-profile-2x4-ubuntu-2404");
pub const LINUX_DEFAULT: Runner = Runner("namespace-profile-16x32-ubuntu-2204");
pub const MAC_DEFAULT: Runner = Runner("self-mini-macos");
pub struct Runner(&'static str);
impl Into<gh_workflow::RunsOn> for Runner {
fn into(self) -> gh_workflow::RunsOn {
self.0.into()
}
}
+142
View File
@@ -0,0 +1,142 @@
use gh_workflow::*;
pub fn checkout_repo() -> Step<Use> {
named::uses(
"actions",
"checkout",
"11bd71901bbe5b1630ceea73d27597364c9af683", // v4
)
}
pub fn setup_pnpm() -> Step<Use> {
named::uses(
"pnpm",
"action-setup",
"fe02b34f77f8bc703788d5817da081398fad5dd2", // v4.0.0
)
.add_with(("version", "9"))
}
pub mod danger {
use super::*;
pub fn setup_node() -> Step<Use> {
named::uses(
"actions",
"setup-node",
"49933ea5288caeca8642d1e84afbd3f7d6820020", // v4
)
.add_with(("node-version", "20"))
.add_with(("cache", "pnpm"))
.add_with(("cache-dependency-path", "script/danger/pnpm-lock.yaml"))
}
pub fn install_deps() -> Step<Run> {
named::run("pnpm install --dir script/danger")
}
pub fn run() -> Step<Run> {
named::run("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 an instance of
// https://github.com/maxdeviant/danger-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.fly.dev/github",
))
}
}
pub mod nix {
use indoc::indoc;
use crate::tasks::workflows::vars;
use super::*;
// on our macs we manually install nix. for some reason the cachix action is running
// under a non-login /bin/bash shell which doesn't source the proper script to add the
// nix profile to PATH, so we manually add them here
pub fn set_path() -> Step<Run> {
named::run(indoc! {r#"
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH"
"#})
}
pub fn install_nix() -> Step<Use> {
named::uses(
"cachix",
"install-nix-action",
"02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31
)
.add_with(("github_access_token", vars::GITHUB_TOKEN))
}
pub fn cachix_action(cachix_filter: &str) -> Step<Use> {
named::uses(
"cachix",
"cachix-action",
"0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16
)
.add_with(("name", "zed"))
.add_with(("authToken", vars::CACHIX_AUTH_TOKEN))
.add_with(("pushFilter", cachix_filter))
.add_with(("cachixArgs", "-v"))
}
pub fn build(flake_output: &str) -> Step<Run> {
named::run(&format!(
"nix build .#{} -L --accept-flake-config",
flake_output
))
}
pub fn limit_store() -> Step<Run> {
named::run(indoc! {r#"
if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then
nix-collect-garbage -d || true
fi"#
})
}
}
// (janky) helpers to generate steps with a name that corresponds
// to the name of the calling function.
mod named {
use gh_workflow::*;
pub(super) fn uses(owner: &str, repo: &str, ref_: &str) -> Step<Use> {
Step::new(function_name(1)).uses(owner, repo, ref_)
}
pub(super) fn run(script: &str) -> Step<Run> {
Step::new(function_name(1))
.run(script)
.shell("bash -euxo pipefail {0}")
}
fn function_name(i: usize) -> String {
let mut name = "<unknown>".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 != &"steps")
.collect::<Vec<_>>()
.join("::")
}
}
+17
View File
@@ -0,0 +1,17 @@
use gh_workflow::WorkflowCallInput;
macro_rules! secret {
($secret_name:ident) => {
pub const $secret_name: &str = concat!("${{ secrets.", stringify!($secret_name), " }}");
};
}
secret!(GITHUB_TOKEN);
secret!(CACHIX_AUTH_TOKEN);
secret!(ZED_CLIENT_CHECKSUM_SEED);
secret!(ZED_MINIDUMP_ENDPOINT);
secret!(ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON);
pub fn input(name: &str, input: WorkflowCallInput) -> (String, (&str, WorkflowCallInput)) {
return (format!("${{{{ inputs.{name} }}}}"), (name, input));
}
@@ -0,0 +1,105 @@
use gh_workflow::*;
use indexmap::IndexMap;
use super::{runners, steps, vars};
/// Generates the danger.yml workflow
pub fn danger() -> Workflow {
Workflow::default()
.name("Danger")
.on(
Event::default().pull_request(PullRequest::default().add_branch("main").types([
PullRequestType::Opened,
PullRequestType::Synchronize,
PullRequestType::Reopened,
PullRequestType::Edited,
])),
)
.add_job(
"danger",
Job::default()
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.runs_on(runners::LINUX_CHEAP)
.add_step(steps::checkout_repo())
.add_step(steps::setup_pnpm())
.add_step(steps::danger::setup_node())
.add_step(steps::danger::install_deps())
.add_step(steps::danger::run()),
)
}
/// Generates the nix.yml workflow
pub fn nix() -> Workflow {
let env: IndexMap<_, _> = [
("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED),
("ZED_MINIDUMP_ENDPOINT", vars::ZED_MINIDUMP_ENDPOINT),
(
"ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON",
vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON,
),
("GIT_LFS_SKIP_SMUDGE", "1"), // breaks the livekit rust sdk examples which we don't actually depend on
]
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect();
// todo(ci) instead of having these as optional YAML inputs,
// should we just generate two copies of the job (one for release-nightly
// and one for CI?)
let (input_flake_output, flake_output) = vars::input(
"flake-output",
WorkflowCallInput {
input_type: "string".into(),
default: Some("default".into()),
..Default::default()
},
);
let (input_cachix_filter, cachix_filter) = vars::input(
"cachix-filter",
WorkflowCallInput {
input_type: "string".into(),
..Default::default()
},
);
Workflow::default()
.name("Nix build")
.on(Event::default().workflow_call(
WorkflowCall::default()
.add_input(flake_output.0, flake_output.1)
.add_input(cachix_filter.0, cachix_filter.1),
))
.add_job(
"nix-build-linux-x86",
Job::default()
.timeout_minutes(60u32)
.continue_on_error(true)
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.runs_on(runners::LINUX_DEFAULT)
.env(env.clone())
.add_step(steps::checkout_repo().add_with(("clean", "false")))
.add_step(steps::nix::install_nix())
.add_step(steps::nix::cachix_action(&input_cachix_filter))
.add_step(steps::nix::build(&input_flake_output)),
)
.add_job(
"nix-build-mac-arm",
Job::default()
.timeout_minutes(60u32)
.continue_on_error(true)
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.runs_on(runners::MAC_DEFAULT)
.env(env)
.add_step(steps::checkout_repo().add_with(("clean", "false")))
.add_step(steps::nix::set_path())
.add_step(steps::nix::cachix_action(&input_cachix_filter))
.add_step(steps::nix::build(&input_flake_output))
.add_step(steps::nix::limit_store()),
)
}