use gh_workflow::{
Concurrency, Container, Event, Expression, Job, Port, PullRequest, Push, Run, Step, Use,
Workflow,
};
use indexmap::IndexMap;
use crate::tasks::workflows::{
steps::{CommonJobConditions, repository_owner_guard_expression},
vars::{self, PathCondition},
};
use super::{
runners::{self, 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)
let should_run_tests = PathCondition::inverted(
"run_tests",
r"^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))",
);
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)),
should_run_tests.guard(clippy(Platform::Linux)),
should_run_tests.guard(clippy(Platform::Mac)),
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_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 tests_pass = tests_pass(&jobs);
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(tests_pass.name, tests_pass.job)
}
// 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, true)
}
pub fn orchestrate_without_package_filter(rules: &[&PathCondition]) -> NamedJob {
orchestrate_impl(rules, false)
}
fn orchestrate_impl(rules: &[&PathCondition], include_package_filter: bool) -> 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 }})"
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 include_package_filter {
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
));
}
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]) -> 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
}
"#});
script.push_str(
&jobs
.iter()
.map(|job| {
format!(
"check_result \"{}\" \"${{{{ needs.{}.result }}}}\"",
job.name, job.name
)
})
.collect::>()
.join("\n"),
);
script.push_str("\n\nexit $EXIT_CODE\n");
let job = Job::default()
.runs_on(runners::LINUX_SMALL)
.needs(
jobs.iter()
.map(|j| j.name.to_string())
.collect::>(),
)
.cond(repository_owner_guard_expression(true))
.add_step(named::bash(&script));
named::job(job)
}
fn check_style() -> NamedJob {
fn check_for_typos() -> Step