Skip to content

fix: reframe and relight the two newest heroes for card-scale readability #84

fix: reframe and relight the two newest heroes for card-scale readability

fix: reframe and relight the two newest heroes for card-scale readability #84

Workflow file for this run

name: Blender Smoke Test
# Executes the snippets' and skills' headline examples inside REAL Blender, headless,
# on the current stable (5.1.x) and the active LTS (4.5.x), and fails on any error or
# empty-output assertion. py_compile (in validate.yml) cannot catch API-level regressions
# like the EEVEE-id inversion, the slotted-actions boundary, the driver TypeError, or the
# dead SDF link -- this gate runs the code so those surface in CI, not in users' files.
on:
workflow_dispatch: {}
schedule:
- cron: "0 7 * * 1" # weekly, Monday 07:00 UTC
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: blender-smoke-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke:
name: Blender ${{ matrix.series }} smoke
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- series: "5.1" # current stable
- series: "4.5" # active LTS
steps:
- uses: actions/checkout@v7
- name: Install Blender runtime libraries
run: |
set -euo pipefail
sudo apt-get update
# Blender needs these shared libs even in --background (GPU/GL module init);
# xvfb provides a virtual display so GL/EGL init does not abort the process.
sudo apt-get install -y --no-install-recommends \
xvfb libgl1 libegl1 libxrender1 libxxf86vm1 libxfixes3 libxi6 \
libxkbcommon0 libsm6 libice6
- name: Resolve and download Blender ${{ matrix.series }}
run: |
set -euo pipefail
series="${{ matrix.series }}"
base="https://download.blender.org/release/Blender${series}/"
echo "Listing $base"
# pick the highest point release for this series (linux x64 portable)
file=$(curl -fsSL "$base" \
| grep -oE "blender-${series}\.[0-9]+-linux-x64\.tar\.xz" \
| sort -V | uniq | tail -1)
if [ -z "$file" ]; then
echo "::error::Could not resolve a linux-x64 build for Blender ${series} at $base"
exit 1
fi
url="${base}${file}"
echo "Downloading $url"
mkdir -p "$RUNNER_TEMP/bl"
curl -fSL --retry 3 -o "$RUNNER_TEMP/bl.tar.xz" "$url"
tar -xf "$RUNNER_TEMP/bl.tar.xz" -C "$RUNNER_TEMP/bl"
bl=$(find "$RUNNER_TEMP/bl" -maxdepth 2 -type f -name blender | head -1)
if [ -z "$bl" ]; then
echo "::error::blender binary not found after extraction"
exit 1
fi
echo "BLENDER=$bl" >> "$GITHUB_ENV"
- name: Print Blender version
run: |
set -euo pipefail
"$BLENDER" --version | head -1
# series guard: confirm we actually got the matrix series
"$BLENDER" --version | head -1 | grep -q "Blender ${{ matrix.series }}\." \
|| { echo "::error::version does not match series ${{ matrix.series }}"; exit 1; }
- name: Run in-Blender smoke driver
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/out"
xvfb-run -a "$BLENDER" --background --python tests/smoke/run_smoke.py -- "$RUNNER_TEMP/out"
- name: Build template input scene
run: |
set -euo pipefail
xvfb-run -a "$BLENDER" --background --python tests/smoke/make_input.py -- "$RUNNER_TEMP/out/input.blend"
- name: Headless glTF template runs (exit 0, .glb produced)
run: |
set -euo pipefail
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
--python tests/smoke/tmpl_gltf.py -- \
--output "$RUNNER_TEMP/out/out.glb" --apply-modifier SUBSURF
test -s "$RUNNER_TEMP/out/out.glb" || { echo "::error::glTF output missing/empty"; exit 1; }
head -c4 "$RUNNER_TEMP/out/out.glb" | grep -q "glTF" || { echo "::error::not a glTF binary"; exit 1; }
- name: Headless template no-mesh path returns exit 2
run: |
set +e
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/empty.blend" \
--python tests/smoke/tmpl_gltf.py -- --output "$RUNNER_TEMP/out/none.glb"
code=$?
set -e
[ "$code" -eq 2 ] || { echo "::error::expected exit 2 for no-mesh input, got $code"; exit 1; }
echo "no-mesh exit code = $code (correct)"
- name: Headless render template runs (exit 0, PNG produced)
run: |
set -euo pipefail
# Cycles (CPU) so this is reliable on GPU-less runners; the EEVEE-id regression
# itself is gated in run_smoke.py via engine assignment.
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
--python tests/smoke/tmpl_render.py -- \
--output "$RUNNER_TEMP/out/render.png" --engine CYCLES
test -s "$RUNNER_TEMP/out/render.png" || { echo "::error::render PNG missing/empty"; exit 1; }
- name: Shipped example - swatch grid renders and self-verifies
run: |
set -euo pipefail
# Run the SHIPPED example file (not a copy). It asserts non-black AND
# distinct-region-count == material-count internally and exits non-zero on
# failure. Cycles (CPU) for pixels on the GPU-less runner; the example still
# asserts the version-correct EEVEE engine id before rendering. Small/low-sample
# for speed -- region detection holds at this size.
xvfb-run -a "$BLENDER" --background \
--python examples/swatch-grid/swatch_grid.py -- \
--output "$RUNNER_TEMP/out/swatch.png" --engine cycles --samples 8 --width 640
test -s "$RUNNER_TEMP/out/swatch.png" || { echo "::error::swatch grid output missing/empty"; exit 1; }
- name: Shipped example - turntable correctness (slotted actions)
run: |
set -euo pipefail
# Frame-independent check only (no render): asserts the rotation keys drive
# playback via the cross-version channelbag path; exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/turntable/turntable.py --
- name: Shipped example - GN SDF remesh correctness (GridToMesh)
run: |
set -euo pipefail
# Frame-independent check only (no render): asserts the SDF->GridToMesh remesh
# yields geometry (eval vcount > 0 and != base); exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/gn-sdf-remesh/gn_sdf_remesh.py --
- name: Shipped example - depsgraph-evaluated export
run: |
set -euo pipefail
# Frame-independent check only (no render): asserts wm.obj_export ships the
# depsgraph-evaluated (modifier-applied) geometry -- exported vcount == evaluated
# > base. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/depsgraph-export/depsgraph_export.py --
- name: Shipped example - wave displace (foreach bulk IO)
run: |
set -euo pipefail
# Frame-independent check only (no render): 9409 verts displaced via one
# foreach_get + one foreach_set; asserts the Z span matches the amplitude
# and every vertex matches the closed-form wave. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/wave-displace/wave_displace.py --
- name: Shipped example - driver wave (driver_namespace + depsgraph)
run: |
set -euo pipefail
# Frame-independent check only (no render): sixteen SCRIPTED drivers call a
# driver_namespace function; asserts the driven values on the evaluated copy
# AND the flushed-back original both match the closed form. Exits non-zero
# on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/driver-wave/driver_wave.py --
- name: Shipped example - bmesh gear (ownership + watertight topology)
run: |
set -euo pipefail
# Frame-independent check only (no render): parametric gear via bmesh with
# bm.free() in try/finally; asserts closed-form vert/edge/face counts and
# that every edge borders exactly two faces. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/bmesh-gear/bmesh_gear.py --
- name: Shipped example - shader node group (interface sockets + sharing)
run: |
set -euo pipefail
# Frame-independent check only (no render): TintedGloss group declared via
# tree.interface.new_socket, instanced by two materials; asserts the sockets
# exist, the group datablock is shared (users == 2), and the per-instance
# Tint values differ. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/shader-node-group/shader_node_group.py --
- name: Shipped example - temp-override join (context override)
run: |
set -euo pipefail
# Frame-independent check only (no render): three-step staircase joined under
# bpy.context.temp_override; asserts one mesh remains, sources are gone,
# topology verts=24 faces=18, and local Z spans all steps. Exits non-zero
# on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/temp-override-join/temp_override_join.py --
- name: Shipped example - GN instance grid (Instance on Points)
run: |
set -euo pipefail
# Frame-independent check only (no render): generative GN tree instances a
# cube on a 3x3 grid and realizes; asserts eval verts=72 faces=54, Set
# Material carries Lime, and corner center matches the grid. Exits non-zero
# on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/gn-instance-grid/gn_instance_grid.py --
- name: Shipped example - shape-key blend (data API + evaluated mesh)
run: |
set -euo pipefail
# Frame-independent check only (no render): relative Tall key lifts+flares
# at value=0.5; asserts undeformed mesh stays at Basis and every evaluated
# vert matches basis + value*(key-basis). Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/shape-key-blend/shape_key_blend.py --
- name: Shipped example - curve bevel arc (Bezier + bevel_depth)
run: |
set -euo pipefail
# Frame-independent check only (no render): beveled Bezier semicircle via
# curve data API; asserts 8 points, bevel_depth=0.15, fill caps, eval
# verts=1044 faces=1028, tube rests on floor. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/curve-bevel-arc/curve_bevel_arc.py --
- name: Shipped example - compositor glare (Fog Glow + 5.x node group)
run: |
set -euo pipefail
# Two tiny 1-sample Cycles renders (compositor on vs off, ~seconds each):
# asserts the cross-version compositor plumbing (compositing_node_group
# on 5.x, scene.node_tree on 4.x), the Glare Fog Glow configuration, a
# strictly-falling halo with the compositor on, and exactly-zero halo
# pixels with it off. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/compositor-glare/compositor_glare.py --
- name: Shipped example - damped-track aim (DAMPED_TRACK data API)
run: |
set -euo pipefail
# Frame-independent check only (no render): twelve needles aim at an
# emissive core via Object.constraints.new('DAMPED_TRACK') on TRACK_Z;
# asserts one unmuted constraint per needle and evaluated local +Z
# aligns toward the core (dot ≥ 0.998). Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/damped-track-aim/damped_track_aim.py --
- name: Shipped example - color attribute wheel (color_attributes CORNER domain)
run: |
set -euo pipefail
# Frame-independent check only (no render): an HSV wheel disc colored via
# mesh.color_attributes.new() on the CORNER domain; asserts the attribute
# is sized to loop count (not vertex count), is active_color, and probe
# loops match the closed-form HSV for their vertex. Exits non-zero on
# failure.
xvfb-run -a "$BLENDER" --background \
--python examples/color-attribute-wheel/color_attribute_wheel.py --
- name: Shipped example - parent inverse orrery (matrix_parent_inverse)
run: |
set -euo pipefail
# Frame-independent check only (no render): a data-API orrery whose arms,
# planets, and moon are parented with the keep-world idiom; asserts bare
# `.parent =` teleports a probe, matrix_parent_inverse restores it exactly,
# matrix_world is stale until view_layer.update(), and every orbit lands
# on its closed form. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/parent-inverse-orrery/parent_inverse_orrery.py --
- name: Shipped example - grease pencil rosette (GPv3 attribute API)
run: |
set -euo pipefail
# Frame-independent check only (no render): five nested rose curves drawn
# as GPv3 strokes; asserts the version-gated datablock address (4.5:
# grease_pencils_v3 with grease_pencils still legacy; 5.x: grease_pencils
# is GPv3, legacy names gone), lazy attribute-layer materialization from
# point writes, and a closed-form round-trip of every position through
# the raw POINT attribute buffer. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/grease-pencil-rosette/grease_pencil_rosette.py --
- name: Shipped example - armature bend (edit_bones + LBS closed form)
run: |
set -euo pipefail
# Check only (no render): a four-bone chain skins a tapered tube via
# name-bound vertex groups; asserts edit_bones is empty outside edit
# mode and holds the closed-form chain inside it, then compares every
# depsgraph-evaluated vertex against re-implemented linear blend
# skinning (root ring pinned, tip deflected — a straight tube fails).
# Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/armature-bend/armature_bend.py --
- name: Shipped example - text version stamp (TextCurve solids + version string)
run: |
set -euo pipefail
# Check only (no render): a TextCurve whose body is the live
# bpy.app.version_string; asserts the Curve subclass and built-in
# font contracts, that flat text is filled but planar, the closed
# form z-extent = 2*(extrude+bevel_depth), body edits regenerating
# geometry, that version_string starts with the dotted version tuple
# ("4.5.11 LTS" is not bare semver), and that a Mesh reference dies
# at to_mesh_clear(). Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/text-version-stamp/text_version_stamp.py --
- name: Shipped example - image pixels testcard (flat RGBA buffer + save lifecycle)
run: |
set -euo pipefail
# Check only (no render): a procedural broadcast test card written with
# one pixels.foreach_set; asserts the buffer is always flat RGBA
# (channels == 4 even with alpha=False, RGB-stride writes raise), the
# byte round-trip error is <= 0.5/255 and strictly > 0 (storage really
# is 8-bit) while float_buffer=True round-trips at float32 precision,
# scale() reallocates so stale-size bulk reads raise, and save() flips
# source to FILE and drops the buffer (pixels silently re-load from
# disk — proven with an imposter file) while save_render() preserves
# it. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/image-pixels-testcard/image_pixels_testcard.py --
- name: Shipped example - uv layer grid (calc_uvs silent no-op)
run: |
set -euo pipefail
# Check only (no render): create_grid(..., calc_uvs=True) without a
# pre-existing UV layer leaves 0 layers (silent no-op); pre-create +
# calc_uvs fills loops to the closed-form grid UVs
# ((x/size+1)/2, (y/size+1)/2) within 1e-6 including mesh persistence;
# explicit loop assignment is the calc_uvs-free fallback. Exits
# non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/uv-layer-grid/uv_layer_grid.py --
- name: Shipped example - png vs exr alpha (float PNG false unpremul)
run: |
set -euo pipefail
# Check only (no render): float_buffer=True Image.save() to PNG writes
# RGBA16 and unpremultiplies as if associated-alpha, so straight-authored
# dark values at low alpha clamp to white (closed-form err 0.98 at
# RGB 0.02 / a=1/255); OpenEXR round-trips within 1e-5; byte→PNG is
# straight RGBA8; EXR color_mode=RGB drops alpha to opaque. Exits
# non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/png-exr-alpha/png_exr_alpha.py --
- name: Shipped example - vse cut list (sequencer 4.5 vs 5.x rename)
run: |
set -euo pipefail
# Check only (no render): asserts the accessor rename (.sequences
# gone on 5.x, bridged on 4.5), the new_effect end-kwarg rename
# (frame_end on 4.5, length on 5.x — the wrong kwarg TypeErrors),
# closed-form spans on each version's canonical accessors
# (frame_final_* vs left_handle/right_handle/duration with the
# deprecation bridge), GC wiring and clamping, scene-strip span,
# and a save/reload round-trip. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/vse-cut-list/vse_cut_list.py --
# Compositing witness on a tiny Cycles render: mosaic cells carry
# their strip colors, the cross cell is a true mid blend, and a
# consumed cross source never composites independently.
xvfb-run -a "$BLENDER" --background \
--python examples/vse-cut-list/vse_cut_list.py -- \
--check-pixels --engine cycles
- name: Shipped example - glTF export round-trip (+Y-up, applied modifiers, UVs)
run: |
set -euo pipefail
# Check only (no render): a 35-part supply crate exported GLTF_SEPARATE
# and re-imported; asserts the exporter/importer kwarg set still exists
# (RNA guard), the on-disk +Y-up conversion ((x,y,z)->(x,z,-y) baked
# into POSITION bounds, no node rotation), disk vertex count ==
# evaluated loop count (export_apply), V-flipped TEXCOORD_0, and the
# full round-trip of positions/normals/UVs/per-triangle material
# bindings within float tolerances. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/gltf-export-roundtrip/gltf_export_roundtrip.py --
- name: Shipped example - LOD decimate chain (depsgraph + ratio bounds)
run: |
set -euo pipefail
# Check only (no render): a lathed retro rocket evaluated at LOD0/1/2
# through the Decimate modifier; asserts closed-form base topology and
# bbox, evaluated-vs-original non-destructiveness (obj.data unchanged),
# evaluated triangle counts within 5% of ratio*base, and silhouette
# (bbox) preservation within 1e-3. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/lod-decimate-chain/lod_decimate_chain.py --
- name: Shipped example - vertex weight limit (4-influence cap + LBS)
run: |
set -euo pipefail
# Check only (no render): a five-bone mech arm with rich 5-influence
# boots, pruned to the 4-influence engine cap via the data API;
# asserts pre/post-limit influence counts, unit weight sums, pose
# preservation (pre vs post pruning), exact LBS from the mesh's own
# deform layer, and a pinned Root mount. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/vertex-weight-limit/vertex_weight_limit.py --
- name: Shipped example - triangulate + tangents (mikktspace contract)
run: |
set -euo pipefail
# Check only (no render): a lathed buckler with polar/strip/planar
# UV charts; asserts closed-form loop-triangle count, unit and
# orthogonal tangent frames, bitangent == sign*(n x t), tangents
# matching the independent edge/UV-delta formula on smooth-field
# triangles, zero flips off seams, and the re-fetched UV layer
# surviving calc_tangents. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/triangulate-tangents/triangulate_tangents.py --
- name: Shipped example - glTF skin round-trip (skins, weights, deform)
run: |
set -euo pipefail
# Check only (no render): a 7-bone mech scorpion exported with
# export_skins and re-imported; asserts skins[0] joints, JOINTS_0/
# WEIGHTS_0 unit sums, bone/parent/rest-matrix round-trip, bit-exact
# weights via position-key lookup, and identical deformation of the
# re-imported rig. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py --
- name: Shipped example - VSE gamma cross (blend-curve closed form)
run: |
set -euo pipefail
# Check only: renders tiny frames across a GAMMA_CROSS and asserts
# every sample against the gamma-0.5 closed form ((1-t)*sqrt(A) +
# t*sqrt(B))^2 with t = (frame-start)/duration, plus the material
# lerp deviation at mid. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/vse-gamma-cross/vse_gamma_cross.py --
- name: Shipped example - light link studio (receiver collections in pixels)
run: |
set -euo pipefail
# Check only (two tiny Cycles renders): a key linked to the hero
# collection must light only the hero (luminance ratio >= 3x at
# projected centers), and unlinking must raise the decoy >= 50%
# with hero drift <= 5%. Also guards the Object-level API path
# (ld.light_linking is an AttributeError). Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/light-link-studio/light_link_studio.py --