fix: reframe and relight the two newest heroes for card-scale readability #84
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 -- |