# Scenario Godot Pipeline Automation

> Use when Godot 4.7 work must run without a mouse or at scale: batch import of hundreds of GLB pro…

Use when Godot 4.7 work must run without a mouse or at scale: batch import of hundreds of GLB props or an AI image-to-3D art drop, naming rules, import presets and post-import scripts, editor plugins, EditorScript and @tool, EditorImportPlugin, GUT or gdUnit4 tests in CI, a GitHub Actions export workflow, GDExtension basics, or a Godot MCP server; also when --import is slow, textures stay uncompressed, tests pass locally but CI fails, or an export ships test folders.

- Page: https://ss33.ai/skills/scenario-labs/scenario-godot-pipeline-automation-17825999838
- Publisher: scenario-labs
- Category: [Software Development](https://ss33.ai/skills/category/software-development-t44z8z) › [Languages & frameworks](https://ss33.ai/skills/category/software-development-t44z8z/languages-and-frameworks-rgvrso)
- License: MIT
- Source: https://github.com/scenario-labs/skills/blob/ccc6c5c623a4f96423df0d157b8ef174f14edfd8/skills/game-engines/godot/scenario-godot-pipeline-automation/SKILL.md

## Skill

SKILL.md: https://ss33.ai/api/v1/ss33/download/scenario-labs/scenario-godot-pipeline-automation-17825999838

# Godot pipeline automation (tools and build engineer)

Target: Godot 4.7.2.stable, macOS Apple Silicon, gdUnit4 6.2.1.

Expert level here means every step a human would click is a command that CI can run twice with the
same result, and every result is checked by a test that has been seen failing. The tools engineer
owns the path from an art drop or a commit to a tested export: ingest, import, editor tooling,
tests, CI, native extensions and agent channels. If a sibling skill named here is missing from your
available skills, ask the user to install it (`npx skills add scenario-labs/skills --skill <name>`);
unattended, proceed from tool schemas and flag the gap.

**REQUIRED BACKGROUND:** scenario-godot-expert (channels, `gd_run` and `gd_env`, the review loop,
4.7 traps). This skill adds [`scripts/gd_pipeline.py`](scripts/gd_pipeline.py) and the AgentKit
module [`scripts/agentkit/pipeline/`](scripts/agentkit/pipeline/); it never starts Godot itself,
every run goes through `gd_run`.

**Status (2026-10-02):** 43 live checks in 9 test files, the round-2 checks (procedures 15) and 8
offline tests, all pass, on a 200-GLB art drop that includes 12 real image-to-3D files
([`references/procedures.md`](references/procedures.md)). Not run: the workflow on GitHub runners,
godot-cpp and Rust builds (no scons or cargo here).

## Stance (the expert delta)

1. **Write the import settings before the first import.** A `.glb.import` sidecar holding only
   `[remap] importer="scene"` and `[params]` is honored by headless `--import`, and Godot adds the
   uid [added, verified M2]. Ingest therefore decides scale, pivot, collision and texture mode per
   file, then imports once. Rewriting `.import` files after the fact doubles the import time and
   dirties the repo.
2. **Pass data to the importer, not code paths.** An `EditorScenePostImportPlugin` declares per-file
   options (`pipeline/fit_height`, `pipeline/unit_scale`, `pipeline/fix_pivot`,
   `pipeline/collision`) that sit in each sidecar, appear in the Import dock, and run under headless
   `--import` once the plugin is listed in `project.godot` [added, verified M6, M10]. One plugin
   serves the whole project; files without `pipeline/prop_id` are untouched.
3. **AI image-to-3D files are not DCC files.** The 12 real files here call themselves "Khronos glTF
   Blender I/O", so a generator check cannot find them; only the manifest can. They arrived at
   arbitrary scale (a toy robot 1.96 m tall) with 2 to 35 materials; 8 of 12 use clearcoat,
   transmission, ior or specular, which 4.7.2 drops without a warning [added, verified M8]. Fit them
   to a manifest height; keep DCC props at authored size after a unit fix.
4. **Parse bytes, not paths.** Dinoleaf's rule for import code: `parse(bytes: PackedByteArray)` with
   the source injected, so the same parser serves `res://`, `user://`, zips and tests with literal
   bytes (sSurjCgebjw [00:04:46]). The GLB probe here works this way, and its tests build GLBs in
   memory.
5. **A test counts only once you have seen it fail.** Butch Wesley, author of GUT: watch red, then
   green; break the function body to retrofit (ImqhHLlPfZg [00:13:40]). Every suite here has a
   planted failure that must exit 100.
6. **Judge an export by its artifact, not its exit code.** Headless `--export-pack` crashed at
   shutdown with signal 11 after "[ DONE ] savezip" in 4 of 5 runs while gdUnit4 was in the pack,
   and the pack was complete [added, observation]. Check the file, its size and its contents;
   exclude dev folders from every preset.
7. **Headless output is the truth for agents** (Fennara, 2vSYP7GyA5U [00:02:51]). Output-panel
   scrapers report errors for lines no longer on disk, and the panel does not list every project
   error [00:06:20]: re-run headless before believing either. MCP servers add scene edits and
   screenshots, never error truth; call only their headless tools on a shared machine [added].
8. **Test behavior through signals, with bounded waits.** Godotneers and Mike Schulze (gdUnit4's
   author) assert on signals and public results, never private state; call `monitor_signals()`
   before the action, since a signal emitted earlier is missed (CreugthdgJ0 [00:36:34]); every wait
   has a timeout. Headless has no input device, so GUI-input tests are skipped in CI (Butch Wesley
   [00:48:33]; gdUnit4 6.2.1 prints the same warning).

## Establish first

| Input                                                                     | Changes                                                                   | Default                                                                                                                               |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Art drop: count, formats, manifest (`file,category,name,height_m,source`) | ingest rules, AI fit                                                      | GLB only, manifest required for AI files                                                                                              |
| Category budgets (triangles, materials, texture side)                     | warn and reject lines                                                     | [`prop_rules.gd`](scripts/agentkit/pipeline/prop_rules.gd) defaults, set from the frame budget with scenario-godot-performance-export |
| Texture path: extract plus VRAM pass, or Basis Universal embed            | import time, disk, Import dock control                                    | extract                                                                                                                               |
| Target platforms                                                          | VRAM formats (`import_etc2_astc` for mobile and macOS universal), presets | Linux and macOS desktop                                                                                                               |
| Test framework                                                            | runner, CI command                                                        | gdUnit4 for JUnit reports, GUT if the project already has it; not both                                                                |
| CI host                                                                   | workflow file, caches                                                     | GitHub Actions, official binaries checked by SHA-512                                                                                  |

## Workflow

**1. Probe and decide (no import).** `prop_ingest.gd:ingest` reads every file as bytes, probes it
([`glb_probe.gd`](scripts/agentkit/pipeline/glb_probe.gd): header, chunks, triangles, materials,
images with their glTF role, extensions, world bounds), applies `prop_rules.gd`, copies accepted
files to `res://props/<category>/<id>/<id>.glb` with a sidecar, and writes `props_manifest.json`.
Writes happen only when content changes. Put an empty `.gdignore` in a raw drop inside the project,
or the editor imports it too (verified, procedures 15). Collision naming uses Godot's node suffixes,
stripped on import (verified, procedures 15): `-col` keeps the mesh and adds a StaticBody3D with a
trimesh shape, `-convcol` a convex one, `-colonly` keeps only the body, `-noimp` drops the node.
GATE: counts add up (accepted + warned + rejected + duplicates = files), every reject has a reason,
zero engine errors.

**2. Import.** `gd_run.import_project(P)`. GATE: no `ERROR:` lines except the exit noise 4.7.2
prints after the work is done (`ERROR: N resources still in use at exit`,
`WARNING: N ObjectDB instances were leaked at exit`, `WARNING: N RIDs of type ... were leaked`;
filter these three, procedures 15); every accepted prop has a `.scn` in `.godot/imported`.

**3. Texture pass (extract mode).** `prop_textures.gd:fix` sets VRAM compression, mipmaps, the
normal-map flag by glTF role and the size limit by category in each extracted texture's `.import`,
then import again. Headless never draws a frame, so `detect_3d` never flips and extracted textures
stay lossless without this pass [added, verified M7]. GATE: every extracted texture lands as
`.s3tc.ctex` (plus ETC2/ASTC when mobile is a target).

**4. Audit and test.** `prop_audit.gd:audit` loads every prop and checks meta version, root name,
pivot, fitted height, height range, triangles against three times the budget, the `Collision` body,
no `AnimationPlayer`, VRAM textures. The same `check_prop()` runs inside one gdUnit4 case over all
props. GATE: 0 failures, JUnit parsed, the planted failure exits 100.

**5. Look.** `prop_thumbs.gd:thumbs` (windowed, 160x90 main window, SubViewport renders) frames each
prop at three-quarter view next to a red 1 m post; `gd_review.contact_sheet` builds sheets of 100 at
1500 px. Open them. GATE: no black or missing prop, sizes plausible against the post, nothing
floating or sunk.

**6. Rerun and change one file.** Run ingest and import again: 0 copies, 0 sidecars, nothing
reimported. Change one source: only that prop and its textures reimport. GATE: both.

**7. Export.** Templates: the `.tpz` holds everything under `templates/` (35 files) plus
`version.txt` reading `4.7.2.stable`, the folder name; move the contents into
`export_templates/4.7.2.stable/` under the data dir (`~/.local/share/godot` on Linux; Voylin
AbMESM0UEHk [00:11:34]). C# projects run `--build-solutions` first (listed in the 4.7.2 `--help`,
implies `--editor`). Then `gd_run.ensure_preset`, `gp.exclude_from_export(P, preset)` (test
frameworks, tests, agent tooling, editor-only plugins), `gd_run.export`. GATE: artifact exists and
non-empty, no dev files in the pack listing, `verify_pack` loads a prop.

**8. CI.** `gp.write_workflow(root, "game", presets=(...))` writes `.github/workflows/godot-ci.yml`
and `tools/ci/godot_ci.sh`. Run the script locally first, plant a failing test, lint with
`gp.lint_workflow`, HEAD-check the URLs with `gp.check_urls`. GATE: local run exit 0, planted
failure exit 11, actionlint clean, URLs 200.

**9. Editor tooling.** An EditorScript's `_run()` is a plain method: a
`gd_run.run_script(..., editor=True)` job opens the scene (`EditorInterface.open_scene_from_path`),
waits a few frames, calls `_run()`, saves with `EditorInterface.save_scene()`. Route changes through
`EditorInterface.get_editor_undo_redo()` so Ctrl+Z works later (Queble, nW7YtSSJzbQ [00:04:41]); set
`owner` on new nodes or they vanish on save (docs). Call an `@export_tool_button` Callable and
`_get_configuration_warnings()` directly. GATE: undo and redo restore the child count, the saved
`.tscn` contains the new nodes, warnings match good and bad inputs.

**GridMap.** A GridMap level wants a `MeshLibrary`: `create_item(id)`, `set_item_mesh(id, mi.mesh)`
per imported mesh, saved as `.res` (verified, procedures 15).

**10. Custom formats.** An `EditorImportPlugin` registered by an enabled plugin runs under headless
`--import`; a parse error returns `ERR_PARSE_ERROR`, leaves `valid=false` in the sidecar and logs
the bad line. GATE: good files load as their resource type, bad files fail with a readable message,
a format-version bump reimports through `reimport_files()`.

**11. Native code.** Only for a profiled hotspot or a native library (Snopek, 4R0uoBJ5XSk
[00:03:16]). Smoke-test the chain: dump `gdextension_interface.h`, build, write the `.gdextension`,
`--import`, then `ClassDB.class_exists` and one bound call. Production uses godot-cpp with a renamed
`entry_symbol` (Snopek [00:13:22]); not yet run (no scons here). GATE: class registered, method
answers, no load errors.

**12. Agent channels.** For live scene inspection, clone and build one MCP server in the project's
tooling folder, pass `GODOT_PATH` in its environment only, and call it through
`gp.mcp_stdio(..., hold_project=P)` (shares the `GD_MAX` lock). Never call tools that launch the
editor or game on a shared machine. GATE: tools listed, one headless edit verified by loading the
result through `gd_run`.

## Numbers

Measured here (Godot 4.7.2, 2026-10-02), 200-file drop, two fresh runs: relative to that drop.
Thumbnail, pack, CI and time-factor numbers are in procedures.md.

| Measure                                                    | Value                                                                                                                                                   |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Full pipeline, 200 files (190 imported)                    | 21.8 to 24.2 s: ingest 0.6 to 1.4, import 17.4 to 19.7, texture pass 0.4 to 0.5, import 1.9 to 2.7, audit 0.6 to 0.7                                    |
| Rerun, nothing changed                                     | ingest 0.6 to 0.7 s, import 1.4 s, 0 files reimported                                                                                                   |
| One file changed                                           | import 1.4 to 1.6 s, 4 imported files touched (scene + texture)                                                                                         |
| Basis Universal embed instead                              | 13.8 to 14.4 s total; `.godot/imported` 80.6 MB vs 154.5 MB                                                                                             |
| Reject line                                                | more than 3x the category triangle budget (3 of 12 AI files, 69k to 181k triangles)                                                                     |
| gdUnit4 6.2.1 exit codes                                   | 0 pass, 100 failures or errors, 101 orphans only, 103 headless refused, 104 Godot version unsupported, 105 script errors (`GdUnitTestSessionRunner.gd`) |
| [`godot_ci.sh`](scripts/ci/godot_ci.sh) exit codes [added] | 10 import, 11 tests, 12 export produced no file                                                                                                         |

## Quality gates

- Measurable: counts reconcile; 0 engine errors per stage; audit 0 failures; gdUnit4 exit 0 and the
  planted test exit 100; rerun reimports 0 files; pack listing has no `addons/gdUnit4`, `test/`,
  `addons/agentkit`, `addons/prop_pipeline`; `project.godot` checked for
  `physics/3d/physics_engine="Jolt Physics"`, the renderer and the plugin line; actionlint clean.
- Visual: contact sheets opened and read for black or untextured props, scale against the 1 m post,
  pivots, missing parts.

## Common mistakes

| Mistake                                               | What it looks like                                                          | Fix                                                                                                             |
| ----------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Flag AI assets by `asset.generator`                   | AI files pass as DCC and keep arbitrary scale (a toy robot 1.96 m tall)     | manifest `source` column                                                                                        |
| Scale every prop to the manifest height               | authored size variation is lost (every lamp becomes 2.2 m)                  | unit fix for DCC, fit only AI                                                                                   |
| Trust headless to compress extracted textures         | lossless `.ctex`, big packs                                                 | texture pass or `[importer_defaults] texture` (project-wide, also hits UI)                                      |
| `-rd /abs/path` for gdUnit4                           | reports land under `<project>/Users/...`, CI finds no JUnit                 | `-rd res://...`                                                                                                 |
| `ResourceLoader.exists()` as an import check          | a failed import "exists"                                                    | read `valid=false` in the `.import`                                                                             |
| Bump `_get_format_version()` and run `--import`       | nothing reimports                                                           | `-e` job: `scan()`, wait, `reimport_files()`                                                                    |
| Ship with default export filter                       | gdUnit4, tests, agent tools in the pack                                     | `exclude_from_export`                                                                                           |
| `upload-artifact` the Linux binary                    | executable bit lost                                                         | tar first (Voylin, AbMESM0UEHk [00:20:10])                                                                      |
| Loose checksum grep                                   | matches the mono build line too                                             | exact names, expect 2 lines                                                                                     |
| Toggle plugins from a `--script -e` job               | contradictory `is_plugin_enabled`, engine errors                            | edit `[editor_plugins]` in `project.godot`                                                                      |
| `pause_before_teardown()` left in a GUT test          | a windowed run hangs (killed at 25 s); headless GUT 9.7.1 does not pause    | `-gignore_pause` (Butch Wesley [00:32:08], procedures 15)                                                       |
| GUI-input test in CI                                  | fails: headless has no input device                                         | `should_skip_script()` returns a reason; GUT counts it Risky, exit 0 (Butch Wesley [00:48:33], procedures 15)   |
| Ignoring GUT's orphan count                           | a leak ships (3 leaked nodes print "3 orphans")                             | a jump from 1 to 80 is a real leak (Butch Wesley [00:57:00])                                                    |
| Trusting a merged `.tscn`                             | conflict markers break the scene; `ResourceLoader.exists()` still says true | load every `.tscn` after a merge (null plus a Parse Error, procedures 15; Lilith Duncan CAJ_iIedx_I [00:01:36]) |
| Trust the first `--import` after adding a GDExtension | shutdown crash (2 of 3 fresh projects), exit code bad                       | check `extension_list.cfg`, run the smoke job                                                                   |

## Handoffs

- Receives GLB, textures and audio with a manifest (blender-expert, `scenario-*`), budgets
  (scenario-godot-performance-export), scene conventions (scenario-godot-architecture,
  scenario-godot-3d-world, which owns how a prop looks and plays).
- Delivers audited props, a showroom and contact sheets to scenario-godot-3d-world; suites and CI to
  everyone; presets to scenario-godot-performance-export (signing, stores). Sends decimation to
  blender-expert, material fixes to scenario-godot-rendering-lighting.

## Godot 4.7 notes

- In an `EditorScenePostImportPlugin`, `_pre_process` sees `ImporterMeshInstance3D` nodes and
  `_post_process` sees `MeshInstance3D` (verified, procedures 15); collision suffixes are already
  applied in both.
- `EditorScript.get_scene()` is deprecated: use `EditorInterface.get_edited_scene_root()` (verified:
  the job logs the deprecation).
- `EditorPlugin.add_dock(EditorDock)` replaces `add_control_to_dock` (4.6, still present).
- Supported glTF extensions in 4.7.2 (M8): basisu, webp, texture_transform, emissive_strength,
  unlit, pbrSpecularGlossiness, lights_punctual, node_visibility, animation_pointer, OMI physics. No
  Draco, meshopt, mesh quantization, clearcoat, transmission, ior, specular.
- GDExtension: `classdb_register_extension_class4` and `classdb_construct_object2` still load in
  4.7.2 but are deprecated; godot-cpp is the production path. A project loads an extension only
  after `--import` writes `.godot/extension_list.cfg`.
- 4.4+ `.uid` sidecars move with their scripts; 4.6 `.tscn` files carry `unique_id`.

## References

- [`references/procedures.md`](references/procedures.md) (every procedure with code, live test and
  result; round 2 is section 15), [`expert-notes.md`](references/expert-notes.md) (judgment by
  source, timestamps), [`critique.md`](references/critique.md) (rubric),
  [`gui-paths.md`](references/gui-paths.md) (editor GUI), [`sources.md`](references/sources.md).
- Code: [`scripts/agentkit/prop_pipeline_addon/`](scripts/agentkit/prop_pipeline_addon/) (the
  `addons/prop_pipeline` plugin), [`scripts/ci/`](scripts/ci/) (workflow and CI script),
  [`scripts/gdunit/`](scripts/gdunit/) (gdUnit4 tests), [`scripts/examples/`](scripts/examples/)
  (EditorScript tools, a level importer, a C GDExtension).
