Skip to content

feat(workflows): degrade gracefully when a status body fails to render#1306

Merged
gregmagolan merged 6 commits into
mainfrom
feat/status-render-graceful-degradation
Jul 1, 2026
Merged

feat(workflows): degrade gracefully when a status body fails to render#1306
gregmagolan merged 6 commits into
mainfrom
feat/status-render-graceful-degradation

Conversation

@gregmagolan

@gregmagolan gregmagolan commented Jul 1, 2026

Copy link
Copy Markdown
Member

Makes a template render error in any CI status surface degrade gracefully instead of aborting the whole aspect process.

Lifecycle handler dispatch calls each status surface's renderer in a bare loop, so an error raised while rendering one surface's body unwinds the entire invocation. AXL has no try/except, so the fix starts at the template-expansion layer.

Changes:

  • New non-throwing runtime builtins ctx.template.try_jinja2 / try_handlebars / try_liquid — like their raising counterparts but return a (ok, text_or_error) tuple, so a render failure becomes a value the caller can branch on.
  • Shared AXL helpers render_summary_or_fallback / render_details_or_fallback (in bazel_results.axl): on render failure they log a WARNING: to CLI output with the error and return a minimal, macro-free fallback body embedding the same message.
  • All six status renderers (build/test, lint, format, gazelle, delivery, warming), both summary and details bodies, routed through the helpers.

The status, title, and conclusion are computed in AXL outside the template, so the check still posts with the correct verdict — only the body text degrades. The fallback body is far under the size limit, so each surface's existing trim cascade is a no-op on it. Templates can be user-supplied overrides, so the fallback attributes nothing to aspect-cli.

Example CLI output when a template fails to render:

WARNING: Status render: lint details body failed to render: unknown function: not_a_real_macro is unknown (in template:2)

Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: yes
  • Breaking change (forces users to change their own code or config): no
  • Suggested release notes appear below: yes

Suggested release notes

  • A template render error in one CI status surface (lint/format/gazelle/delivery/warming/build/test) no longer aborts the whole aspect run — the affected check posts a fallback body with the correct status and a WARNING: is logged.

Test plan

  • Rust unit tests: per-engine success + error-path coverage for jinja2/handlebars/liquid render, pinning the contract the try_* variants rely on.
  • AXL unit tests in bazel_results_test: drive all three try_* engines (success + malformed), and exercise render_details_or_fallback / render_summary_or_fallback directly (passthrough + fallback for both bodies).
  • AXL end-to-end test in the lint suite renders a broken details template through render_check_output and asserts the fallback body appears and the status/title survive.
  • All status-surface snapshot suites pass against a from-source CLI build.

A template render error in one status surface previously propagated out of
the lifecycle handler dispatch and aborted the whole `aspect` invocation —
that's how the missing `failure_snippets` macro (fixed in #1305) crashed
the process instead of just skipping the surface.

Add a non-throwing `ctx.template.try_jinja2()` runtime builtin that returns
`(ok, text_or_error)` instead of raising, then route every status renderer
(bazel/lint/format/gazelle/delivery/warming, summary and details bodies)
through a shared `render_{summary,details}_or_fallback` helper. On a render
error it logs a `WARNING:` line to CLI output with the message and emits a
minimal, macro-free fallback body embedding the same error. The status,
title, and conclusion are computed in AXL outside the template, so the check
still posts with the correct verdict — only the body degrades.

Summary templates carry no macros today and can't hit that failure class;
guarding them too is defense-in-depth so the renderer is categorically
unable to crash the invocation on a future template edit.

Tests: a Rust unit test pins `jinja2_render`'s error surfacing (the contract
try_jinja2 relies on), and an AXL test renders a deliberately-broken details
template and asserts the fallback body appears, the WARNING is emitted, and
the status/title survive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a8dc43563

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/aspect-cli/src/builtins/aspect/private/lib/bazel_results.axl Outdated
@aspect-workflows

aspect-workflows Bot commented Jul 1, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Wed Jul 1 20:31:03 UTC 2026

⚠️ 2 flagged tasks

  • ⚠️ delivery-gha-debug [delivery] · ⏱ 27.9s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 3 skipped)
  • ⚠️ delivery-gha [delivery] · ⏱ 37s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 3 skipped)

✅ 26 successful tasks

  • ✅ axl-smoke-gha-bootstrap [build] · ⏱ 23.8s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ run-axl-smoke [run] · ⏱ 16.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:py_deliverable
  • ✅ run-axl-smoke-2 [run] · ⏱ 14.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:sh_deliverable
  • ✅ axl-tests-gha-bootstrap [build] · ⏱ 21.6s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ build-gha-debug [build] · ⏱ 1m 5s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (166 built)
  • ✅ build-gha [build] · ⏱ 1m 9s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (166 built)
  • ✅ build-gha-ephemeral [build] · ⏱ 50.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (9 built)
  • ✅ buildifier-gha-debug [buildifier] · ⏱ 35s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ buildifier-gha [buildifier] · ⏱ 38.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha-debug [format] · ⏱ 1m 32s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task [format] · ⏱ 1m 22s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-2 [format] · ⏱ 15.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-3 [format] · ⏱ 14.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-4 [format] · ⏱ 14s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha [format] · ⏱ 1m 39s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle-gha-debug [gazelle] · ⏱ 1m 8s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha-debug [gazelle] · ⏱ 2m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha [gazelle] · ⏱ 2m 7s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ init-shell [build] · ⏱ 1m 18s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ lint-gha-debug [lint] · ⏱ 1m 4s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ lint-gha [lint] · ⏱ 1m 34s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ test-gha-debug [test] · ⏱ 56.6s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 26 cached)
  • ✅ test-gha-coverage [test] · ⏱ 16.7s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha-target-pattern-file [test] · ⏱ 15.5s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha [test] · ⏱ 1m 17s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 25 cached)
  • ✅ test-gha-ephemeral [test] · ⏱ 45.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)

🔁 Reproduce

⚠️ delivery (delivery-gha-debug · delivery-gha)

# --mode=always --track-state=false for off-runner with no state backend.
aspect delivery \
  --commit-sha=722ec705357d6a5d312829f8a5529be95a75084e \
  --mode=always \
  --track-state=false \
  --dry-run=true

Install aspect: aspect.build/docs/cli/install


⏱ Last updated Wed Jul 1 20:34:26 UTC 2026 · 📊 GitHub API quota 1,263/15,000 (8% used, resets in 43m)
🚀 Powered by Aspect CLI (v0.0.0-dev)  |  Aspect Build · X · LinkedIn · YouTube

gregmagolan and others added 5 commits July 1, 2026 15:29
- Extract template_data_to_json in the runtime, collapsing the dict→JSON
  loop duplicated across handlebars/jinja2/try_jinja2/liquid.
- Unit-test the shared render_{details,summary}_or_fallback helpers directly
  in bazel_results_test (passthrough + fallback for both), covering the
  summary-fallback path that only had transitive coverage.
- Correct the helper docstrings: the size guard makes the trim cascade a
  no-op on a fallback body; callers don't branch on the returned `ok`.
- Trim stale/duplicated inline comments now covered by the helper docstrings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The details override in `templates` can be a user template, so a render
failure isn't necessarily an aspect-cli bug. Drop the "This is a bug in
aspect-cli" line; keep the neutral "status above is still accurate."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All three template engines now have a non-throwing try_* variant returning
(ok, text). Factor the shared render-Result → tuple mapping into
render_result_to_tuple so each try_* is a one-liner.

Tests: per-engine ok/error Rust unit tests pin the *_render contract, and an
AXL test drives all three try_* methods (success + malformed) through the
real evaluator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses Codex review: the details trim cascade re-rendered with the
throwing ctx.template.jinja2 after the first render. A template that only
errors on a trimmed shape (e.g. after a section is emptied) would still
abort the invocation. Each surface now re-renders via a local _render_details
closure over render_details_or_fallback, so a mid-cascade render error also
degrades to the fallback body.

Also fixes a pre-existing crash the new regression test surfaced: the lint
cascade computed `g["count"] - len(kept)`, but count is stringified for the
template — `string - int` panics once row-halving runs. Parse it back and
re-stringify overflow to match the rendered field shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gregmagolan gregmagolan merged commit 81b9d87 into main Jul 1, 2026
69 checks passed
@gregmagolan gregmagolan deleted the feat/status-render-graceful-degradation branch July 1, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant