Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .aspect/config.axl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ def config(ctx: ConfigContext):
# assert we can call a lambda with a global bind from another module in a config script
lambda_with_global_bind()("assert")

is_ci = bool(ctx.std.env.var("CI"))

if is_ci:
ctx.traits[BazelTrait].extra_flags.extend([
"--config=ci",
])

# Dogfood the telemetry.exporters API: when our GitHub Actions workflow
# injects DASH0_ENDPOINT / DASH0_TOKEN, ship aspect-cli's own traces and
# logs to Dash0 so the team can see real instrumentation data from CI
Expand Down
5 changes: 4 additions & 1 deletion .buildkite/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ steps:
limit: 1
command: |
echo "--- :bazel: aspect build //crates/aspect-launcher //crates/aspect-cli"
ASPECT_DEBUG=1 aspect build --task-key build-bk-axl-tests-bootstrap //crates/aspect-launcher //crates/aspect-cli
ASPECT_DEBUG=1 aspect build \
--task-key build-bk-axl-tests-bootstrap \
--bazel-flag=--remote_download_outputs=toplevel \
//crates/aspect-launcher //crates/aspect-cli
# Use cquery to get the actual output paths (handles platform transitions on Linux)
WORKSPACE_ROOT=$$(pwd)
LAUNCHER=$$WORKSPACE_ROOT/$$(bazel $$BAZEL_STARTUP_OPTS cquery $$BAZEL_BUILD_OPTS --output=files //crates/aspect-launcher)
Expand Down
65 changes: 29 additions & 36 deletions crates/aspect-cli/src/builtins/aspect/lib/bazel_runner.axl
Original file line number Diff line number Diff line change
Expand Up @@ -227,36 +227,13 @@ def run_bazel_task(ctx: TaskContext, command: str) -> TaskConclusion:

data = init_data()

# Mark the `setup` phase BEFORE `task_started` so the time spent in
# CI-platform handlers (GitHub check-run create, Buildkite annotate)
# is tracked as a named phase rather than absorbed into init. CI-only
# gate; locally it's a no-op (handlers are fast). See setup_phase().
setup_phase(ctx, lifecycle)

# Fire task_started FIRST so any subsequent failure (pre_health_check
# throwing, rc parse error) still surfaces as a failed check run
# rather than a silent no-op. This matches the lint / format /
# gazelle / delivery convention.
for handler in lifecycle.task_started:
handler(ctx, " ".join(ctx.args.targets))

preflight_phase(ctx, lifecycle, hc_trait)

for hook in hc_trait.post_health_check:
result = hook(ctx)
if result != None:
_emit_terminal(ctx, lifecycle, command, data, 1)
fail(result)

# Flags: accumulate data, then optionally transform.
# Resolve all bazelrc references before any lifecycle hooks fire. An rc-
# resolution error (e.g. undefined `--config=ci`) then aborts with no CI
# side effects — no status checks created, no API quota burned, no stuck
# "running" state from a Starlark error bypassing the terminal lifecycle.
flags = ["--isatty=" + str(int(ctx.std.io.stdout.is_tty))]
flags.extend(ctx.args.bazel_flags)
flags.extend(bazel_trait.extra_flags)

# Task-time flag contributors — features register hooks that need
# ctx.task (e.g. --build_metadata=ASPECT_TASK_NAME=...) which features
# can't produce at activation time because FeatureContext has no task.
# See Workflows in feature/workflows.axl for the metadata registration.
for hook in bazel_trait.task_flags:
flags.extend(hook(ctx))
if bazel_trait.flags:
Expand All @@ -269,6 +246,31 @@ def run_bazel_task(ctx: TaskContext, command: str) -> TaskConclusion:
if bazel_trait.startup_flags:
startup_flags = bazel_trait.startup_flags(startup_flags)

rc = ctx.bazel.parse_rc(
startup_flags = startup_flags,
flags = flags,
)
trace.event("build.flags", fields = {"flags": flags})
rc_startup_flags, flags = rc.expand_all(command = command)
ctx.bazel.startup_flags.extend(startup_flags + rc_startup_flags + ["--ignore_all_rc_files"])

# Mark the `setup` phase before `task_started` so the time spent in
# CI-platform handlers (GitHub check-run create, Buildkite annotate) is
# tracked as a named phase rather than absorbed into init. CI-only gate;
# locally it's a no-op (handlers are fast). See setup_phase().
setup_phase(ctx, lifecycle)

for handler in lifecycle.task_started:
handler(ctx, " ".join(ctx.args.targets))

preflight_phase(ctx, lifecycle, hc_trait)

for hook in hc_trait.post_health_check:
result = hook(ctx)
if result != None:
_emit_terminal(ctx, lifecycle, command, data, 1)
fail(result)

bes_sinks = _collect_bes_from_args(ctx)
if bazel_trait.build_event_sinks:
bes_sinks.extend(bazel_trait.build_event_sinks)
Expand All @@ -282,15 +284,6 @@ def run_bazel_task(ctx: TaskContext, command: str) -> TaskConclusion:
cancellation = ctx.bazel.cancel_invocation()
cancellation.wait()

rc = ctx.bazel.parse_rc(
startup_flags = startup_flags,
flags = flags,
)

trace.event("build.flags", fields = {"flags": flags})
rc_startup_flags, flags = rc.expand_all(command = command)
ctx.bazel.startup_flags.extend(startup_flags + rc_startup_flags + ["--ignore_all_rc_files"])

# Parsed-rc disclosure is verbose; print on user opt-in via
# --announce-rc, otherwise gate behind trace.log so it only
# surfaces under ASPECT_DEBUG.
Expand Down
11 changes: 9 additions & 2 deletions crates/aspect-cli/src/builtins/aspect/lib/environment.axl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def color_enabled(std):
True when stdout is a real TTY, or when a recognized CI host is
detected (GitHub Actions, Buildkite, CircleCI, GitLab) — those CIs
render ANSI in their log viewers even though stdout is captured into
a non-TTY pipe.
a non-TTY pipe. `NO_COLOR` (https://no-color.org/) opts out
regardless when present and non-empty.

This is the "should I emit color?" predicate. Use it everywhere
styled output / `rc.announce(ansi=...)` decisions are made.
Expand All @@ -93,9 +94,15 @@ def color_enabled(std):
breaks line-oriented log viewers, but plain ANSI color
codes render fine. Don't conflate the two.
"""
env = std.env

# NO_COLOR spec: disable iff present *and non-empty*. `NO_COLOR= my-cmd`
# is the standard shell idiom for clearing it for one invocation; honor
# it by treating empty as unset.
if env.var("NO_COLOR"):
return False
if std.io.stdout.is_tty:
return True
env = std.env
return bool(
env.var("GITHUB_ACTIONS") or
env.var("BUILDKITE") or
Expand Down
14 changes: 12 additions & 2 deletions crates/aspect-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::time::Duration;

use aspect_telemetry::{cargo_pkg_short_version, do_not_track, send_telemetry};
use axl_runtime::bazel_live;
use axl_runtime::eval::{Loader, ModuleEnv, MultiPhaseEval};
use axl_runtime::eval::{EvalError, Loader, ModuleEnv, MultiPhaseEval};
use axl_runtime::module::{AXL_ROOT_MODULE_NAME, Mod};
use axl_runtime::module::{DiskStore, ModEvaluator};
use tokio::task;
Expand Down Expand Up @@ -207,7 +207,17 @@ fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(err) => {
eprintln!("error: {err:?}");
// Pre-shaped user-facing errors (a `bazelrc:` block, etc.) use Display
// — `EvalError`'s custom impl strips the AXL call-stack traceback. Other
// errors use Debug so AXL/Aspect authors keep the full diagnostic.
if err
.downcast_ref::<EvalError>()
.is_some_and(EvalError::is_pre_shaped_user_error)
{
eprintln!("error: {err}");
} else {
eprintln!("error: {err:?}");
}
ExitCode::FAILURE
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/axl-runtime/src/engine/bazel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,8 @@ pub(crate) fn bazel_methods(registry: &mut MethodsBuilder) {
.map(|v| unpack_rc_option(*v))
.collect::<anyhow::Result<_>>()?;
let inner = bazelrc::BazelRC::new(root, &startup_flags_vec, &flags_vec)
.map_err(|e| anyhow::anyhow!("{}", e))?;
.map_err(|e| anyhow::anyhow!("{}", e))?
.with_ansi_errors(crate::term::color_enabled());
Ok(rc::StarlarkBazelRC {
inner,
skip_config_if_missing: skip_config_if_missing.items,
Expand Down
4 changes: 2 additions & 2 deletions crates/axl-runtime/src/engine/bazel/rc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn bazelrc_methods(registry: &mut MethodsBuilder) {
let opts = this
.inner
.expand_configs(&command, &ignore)
.map_err(|e| anyhow::anyhow!("{}", e))?;
.map_err(anyhow::Error::from)?;
let mut result = Vec::with_capacity(opts.len());
for opt in &opts {
let v = match &opt.version_condition {
Expand Down Expand Up @@ -114,7 +114,7 @@ fn bazelrc_methods(registry: &mut MethodsBuilder) {
let opts = this
.inner
.expand_configs(&command, &ignore)
.map_err(|e| anyhow::anyhow!("{}", e))?;
.map_err(anyhow::Error::from)?;

let mut startup_flags: Vec<Value<'v>> = Vec::new();

Expand Down
49 changes: 48 additions & 1 deletion crates/axl-runtime/src/eval/error.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::fmt;

use thiserror::Error;

/// Enum representing possible errors during evaluation, including Starlark-specific errors,
/// missing symbols, and wrapped anyhow or IO errors.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum EvalError {
#[error("{0}")]
#[error("{}", StarlarkErrorDisplay(.0))]
StarlarkError(starlark::Error),

#[error("{0:?}")]
Expand All @@ -21,6 +23,51 @@ pub enum EvalError {
IOError(#[from] std::io::Error),
}

impl EvalError {
/// True when this error is a pre-shaped user-facing block that should be
/// rendered without the AXL call-stack traceback. Currently any
/// [`bazelrc::BazelRcError`] qualifies — those messages already name the
/// subsystem, the source of the bad flag, the loaded rc files, and the
/// fix; the call stack only adds noise.
///
/// The `Display` impl uses this same check to strip the traceback inline.
/// The CLI's main loop calls this from outside to decide between Display
/// (`{err}`, no traceback) and Debug (`{err:?}`, full diagnostic).
pub fn is_pre_shaped_user_error(&self) -> bool {
match self {
EvalError::StarlarkError(s) => starlark_wraps_user_error(s),
_ => false,
}
}
}

/// True when `s` carries an [`anyhow::Error`] that downcasts to a
/// [`bazelrc::BazelRcError`]. Only `ErrorKind::Native` and `ErrorKind::Other`
/// carry an inner `anyhow::Error` we can downcast; everything else is internal
/// to starlark and never wraps our errors.
fn starlark_wraps_user_error(s: &starlark::Error) -> bool {
let anyhow_err = match s.kind() {
starlark::ErrorKind::Native(e) | starlark::ErrorKind::Other(e) => e,
_ => return false,
};
anyhow_err.downcast_ref::<bazelrc::BazelRcError>().is_some()
}

/// `Display` for [`EvalError::StarlarkError`] that strips the AXL call-stack
/// traceback when the error wraps a pre-shaped user-facing block. See
/// [`EvalError::is_pre_shaped_user_error`].
struct StarlarkErrorDisplay<'a>(&'a starlark::Error);

impl<'a> fmt::Display for StarlarkErrorDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if starlark_wraps_user_error(self.0) {
write!(f, "{}", self.0.without_diagnostic())
} else {
write!(f, "{}", self.0)
}
}
}

// Custom From implementation since starlark::Error doesn't implement std::error::Error.
impl From<starlark::Error> for EvalError {
fn from(value: starlark::Error) -> Self {
Expand Down
23 changes: 5 additions & 18 deletions crates/axl-runtime/src/eval/multi_phase.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::io::IsTerminal;
use std::path::{Path, PathBuf};

use anyhow::anyhow;
Expand Down Expand Up @@ -45,25 +44,13 @@ const SGR_RESET: &str = "\x1b[0m";
const DEFAULT_HIGHLIGHT_COLOR: &str = "1;36";
const HIGHLIGHT_COLOR_ENV: &str = "ASPECT_CLI_HIGHLIGHT_COLOR";

/// True on any recognized CI host (Buildkite / GitHub Actions / CircleCI /
/// GitLab CI / generic `CI=...`). Used to gate task-key suffix on the
/// running-task header and to force-enable color when stderr is piped to
/// a CI log viewer that renders ANSI.
fn on_recognized_ci() -> bool {
std::env::var_os("BUILDKITE").is_some()
|| std::env::var_os("CI").is_some()
|| std::env::var_os("GITHUB_ACTIONS").is_some()
|| std::env::var_os("CIRCLECI").is_some()
|| std::env::var_os("GITLAB_CI").is_some()
}

/// Return `(verb_color_prefix, reset)` for the "Running" verb on the
/// task-starting line. `verb_color_prefix` is empty when color is
/// disabled or `ASPECT_CLI_HIGHLIGHT_COLOR=""`; `reset` is empty when
/// color is disabled. Color is enabled when stderr is a TTY or we're on
/// a recognized CI host.
/// color is disabled. The color decision is delegated to
/// [`crate::term::color_enabled`].
fn running_verb_color() -> (String, &'static str) {
let color = std::io::stderr().is_terminal() || on_recognized_ci();
let color = crate::term::color_enabled();
let highlight = std::env::var(HIGHLIGHT_COLOR_ENV)
.ok()
.unwrap_or_else(|| DEFAULT_HIGHLIGHT_COLOR.to_string());
Expand Down Expand Up @@ -374,7 +361,7 @@ impl<'v, 'l> MultiPhaseEval<'v, 'l> {
EvalError::UnknownError(anyhow!("task index {} out of range", task_index))
})?;
let (verb_seq, reset) = running_verb_color();
let key_suffix = if on_recognized_ci() {
let key_suffix = if crate::term::on_recognized_ci() {
format!(" [{}]", task_key)
} else {
String::new()
Expand Down Expand Up @@ -501,7 +488,7 @@ impl<'v, 'l> MultiPhaseEval<'v, 'l> {
// the task body are still announced from AXL via
// `lib/lifecycle.axl::announce`. Color setup mirrors the helper
// so the verdict glyph and labels match.
let color = std::io::stderr().is_terminal() || on_recognized_ci();
let color = crate::term::color_enabled();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its sort of sad that CI specific code is creeping into the rust code. ideally, this should be knob on the parse_bazelrc call.

let sgr = |params: &str| {
if color {
format!("\x1b[{}m", params)
Expand Down
1 change: 1 addition & 0 deletions crates/axl-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod docs;
pub mod engine;
pub mod eval;
pub mod module;
pub mod term;
pub mod trace;

/// Bazel subprocess live-tracking. Re-exported so `aspect-cli`'s
Expand Down
Loading
Loading