13 Commits
Author SHA1 Message Date
BillyOutlast 164e47b6f8 ci(ocr): point review job at org reusable workflow (pinned)
Fix 'failed to fetch workflow' parse error: ocr.yml referenced a local reusable workflow path (./.github/workflows/ocr-review.yml) that does not exist in this repo. Point at Heretek-AI/.github org reusable workflow pinned to main HEAD (a440b485).
2026-10-19 16:34:10 -04:00
BillyOutlast d510e51d41 ci: add reusable OpenCodeReview PR workflow 2026-10-19 15:49:48 -04:00
BillyOutlast 4c48da64ba Merge pull request #13 from Heretek-AI/ci/test-gate
Add CI test gate. Human-authorized. Refs #7.
2026-10-12 10:37:55 -04:00
John Smith 181fca9dd7 ci(test): gate on check, unit tests and engine GDScript parse (Refs #7) 2026-10-12 10:36:59 -04:00
BillyOutlast b75f9a1bef Merge pull request #12 from Heretek-AI/ci/oidc-node24
Fix OIDC: node 24 + token-free setup-node. Human-authorized. Refs #7.
2026-10-12 10:26:46 -04:00
John Smith 3125622fce ci(publish): fix workflow YAML (Refs #7) 2026-10-12 10:26:25 -04:00
John Smith 5b4fa9a5b0 ci(publish): node 24 + no setup-node registry auth for OIDC (Refs #7) 2026-10-12 10:26:15 -04:00
BillyOutlast 80b382cb99 Merge pull request #11 from Heretek-AI/release/1.0.1
1.0.1 bump. Human-authorized release. Refs #7.
2026-10-12 10:24:53 -04:00
John Smith d52336d20e chore(release): bump to 1.0.1 for engine-4.7 companion fixes (Refs #7) 2026-10-12 10:24:45 -04:00
BillyOutlast b0ea0581eb Merge pull request #10 from Heretek-AI/fix/engine-47-parse-errors
Engine-4.7 companion fixes, live-verified 13/13 + GUT. Human-authorized merge. Refs #7.
2026-10-12 10:24:15 -04:00
John Smith 759985f57e fix(engine-47): make companion addon load and serve on Godot 4.7 (Refs #7) 2026-10-12 10:08:55 -04:00
BillyOutlast c4cc12233a Merge pull request #9 from Heretek-AI/ci/trusted-publishing
Add OIDC trusted-publishing workflow. Human-authorized release work. Refs #5.
2026-10-12 01:12:23 -04:00
John Smith beffb53378 ci(publish): OIDC trusted-publishing workflow for npm releases (Refs #5) 2026-10-12 01:12:17 -04:00
13 changed files with 252 additions and 23 deletions

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
name: Test gate
# Every push to main and every PR: lint/typecheck, unit + harness tests, and
# the engine GDScript parse gate. The parse gate downloads a pinned Godot
# binary (same major line the release was live-verified on); without it the
# mock-only suite cannot catch GDScript parse errors (see #10).
# Binary install failures must fail this workflow loudly — never skip silent.
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
GODOT_VERSION: 4.7.2-stable
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Lint + typecheck
run: bun run check
- name: Unit + harness tests (explicit paths; never bare sweeps of review/)
run: bun test ./packages/plugin/test/ ./companion/tests/
- name: Install Godot engine
run: |
curl -sSL -o /tmp/godot.zip "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_linux.x86_64.zip"
unzip -q /tmp/godot.zip -d /tmp/godot-bin
echo "/tmp/godot-bin" >> "$GITHUB_PATH"
- name: Engine GDScript parse gate (all 13 .gd files)
run: GODOT_BIN="Godot_v${GODOT_VERSION}_linux.x86_64" scripts/check-gdscript.sh
+41
View File
@@ -0,0 +1,41 @@
name: OpenCodeReview
concurrency:
group: >-
${{
(
github.event_name == 'pull_request_target'
|| (
github.event_name == 'issue_comment'
&& github.event.issue.pull_request
&& github.event.comment.user.type != 'Bot'
&& (
github.event.comment.author_association == 'MEMBER'
|| github.event.comment.author_association == 'OWNER'
|| github.event.comment.author_association == 'COLLABORATOR'
)
&& (
startsWith(github.event.comment.body, '/open-code-review')
|| startsWith(github.event.comment.body, '@open-code-review')
)
)
)
&& format('ocr-{0}', github.event.pull_request.number || github.event.issue.number)
|| format('noop-{0}', github.run_id)
}}
cancel-in-progress: true
on:
pull_request_target:
types: [opened, synchronize, reopened]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
jobs:
review:
uses: Heretek-AI/.github/.github/workflows/ocr-review.yml@a440b485d779b9177b1602c4d4f7f58cff0483a2 # main
secrets: inherit
+55
View File
@@ -0,0 +1,55 @@
name: Publish to npm (OIDC trusted publishing)
# Publishes @heretek-ai/godot-opencode + @heretek-ai/godot-testkit whenever a
# GitHub Release is published. No long-lived npm tokens: authentication uses
# npm trusted publishing (OIDC), so this workflow must NOT set NODE_AUTH_TOKEN.
# One-time npm-side setup (per package, on npmjs.com):
# Package Settings > Trusted Publisher > Manage Trusted Publishers >
# Add GitHub Actions publisher: org Heretek-AI, repo godot-opencode,
# workflow publish.yml (no environment restriction).
# Tag/version contract: release tag must be v<version> matching
# packages/*/package.json (e.g. tag v1.0.1 for version 1.0.1).
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Check out release tag
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
# No registry-url here: setup-node would inject NODE_AUTH_TOKEN and
# block OIDC minting. The default registry + id-token permission is enough.
- name: Set up Node for OIDC publish
uses: actions/setup-node@v4
with:
node-version: 24
- name: Require npm with OIDC support
run: npm install --global npm@latest
- name: Guard tag matches package versions
run: |
TAG="${{ github.event.release.tag_name }}"
EXPECTED="${TAG#v}"
PLUGIN="$(node -p "require('./packages/plugin/package.json').version")"
TESTKIT="$(node -p "require('./packages/testkit/package.json').version")"
test "$PLUGIN" = "$EXPECTED" || { echo "plugin $PLUGIN != tag $TAG"; exit 1; }
test "$TESTKIT" = "$EXPECTED" || { echo "testkit $TESTKIT != tag $TAG"; exit 1; }
- name: Publish @heretek-ai/godot-opencode
run: npm publish --provenance --access public
working-directory: packages/plugin
- name: Publish @heretek-ai/godot-testkit
run: npm publish --provenance --access public
working-directory: packages/testkit
@@ -37,7 +37,10 @@ var dispatcher = null
func _ready() -> void:
set_process(false)
# NOTE: _ready runs AFTER the parent plugin's _enter_tree (which calls
# startup()), so this must not force processing off — derive it from the
# actual listen state instead.
set_process(_server.is_listening())
## Start listening. `capability_path` points at the 9500.json record.
@@ -51,7 +54,7 @@ func startup(capability_path: String) -> int:
if not _is_ws_token(_ws_capability):
_ws_capability = ""
return ERR_FILE_CORRUPT
var err := _server.listen(BRIDGE_PORT, "127.0.0.1")
var err: int = _server.listen(BRIDGE_PORT, "127.0.0.1")
if err != OK:
return err
set_process(true)
@@ -98,7 +101,7 @@ func _poll_peers() -> void:
if not (peer is WebSocketPeer):
continue
peer.poll()
var state := peer.get_ready_state()
var state: int = peer.get_ready_state()
if state == WebSocketPeer.STATE_CLOSED:
_authenticated.erase(peer.get_instance_id())
continue
@@ -151,7 +154,9 @@ func _forward_command_frame(peer_id: int, raw: String) -> void:
## Route one dispatcher response to the peer that issued its request_id.
## Returns false when the peer is gone (response dropped, never requeued).
func send_response(response: Dictionary) -> bool: var peer := _peer_for_request(str(response.get("request_id", "")))
## (Multiline body: Godot 4.7 rejects a same-line suite after the colon here.)
func send_response(response: Dictionary) -> bool:
var peer := _peer_for_request(str(response.get("request_id", "")))
if peer == null:
return false
if peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
@@ -19,19 +19,20 @@ var _pending_queries: Dictionary = {}
func _setup_session(session_id: int) -> void:
var session := get_session(session_id)
if session == null:
return
session.register_message_capture(
CAPTURE_CHANNEL,
func(message: StringName, data: Array) -> bool:
return _on_capture(message, data)
)
_sessions.append(session_id)
if not _sessions.has(session_id):
_sessions.append(session_id)
func _has_capture() -> void:
_sessions.clear()
## Engine capture entry point: the editor routes channel traffic to this
## plugin when _has_capture reports the channel as handled.
func _capture(message: String, data: Array, _session_id: int) -> bool:
return _on_capture(message, data)
## EditorDebuggerPlugin virtual (parent: `_has_capture(String) -> bool`):
## report whether this plugin captures the given channel.
func _has_capture(capture: String) -> bool:
return capture == CAPTURE_CHANNEL
## Engine capture callback. Returns true when the message was consumed.
@@ -62,7 +62,7 @@ func instantiate_scene(params: Dictionary) -> Dictionary:
var node_name := str(params.get("name", ""))
if not node_name.is_empty():
instance.name = node_name
var result := _undo.add_node_by_path(scene_root, str(params.get("parent_path", "/")), instance)
var result: Dictionary = _undo.add_node_by_path(scene_root, str(params.get("parent_path", "/")), instance)
if result.has("data"):
result["data"]["scene_path"] = scene_path
return result
@@ -24,7 +24,7 @@ func search_resources(params: Dictionary) -> Dictionary:
var directory := queue.pop_back()
seen += 1
for entry in DirAccess.get_files_at(directory):
var full := directory.path_join(entry)
var full: String = directory.path_join(entry)
if not full.match("*" + pattern + "*"):
continue
if not type_hint.is_empty() and not _type_matches(full, type_hint):
+8 -1
View File
@@ -23,6 +23,12 @@ const BRIDGE_PORT := 9500
var _connection: Node = null
var _dispatcher: RefCounted = null
var _debugger_plugin: EditorDebuggerPlugin = null
## Retain every RefCounted handler for the plugin's lifetime. Callables bound
## to a RefCounted method do NOT extend its lifetime: once the constructing
## local goes out of scope the object is freed and every dispatch fails with
## `null::<method> (Callable)` (proven live on Godot 4.7). Nodes (_connection)
## are kept alive by the scene tree; RefCounteds need this explicit anchor.
var _handler_refs: Array = []
func _enter_tree() -> void:
@@ -34,6 +40,7 @@ func _enter_tree() -> void:
var shaders := ShaderHandler.new()
var themes := ThemeHandler.new(undo_manager)
var viewport := ViewportHandler.new()
_handler_refs = [undo_manager, nodes, signals, resources, shaders, themes, viewport]
_register_node_commands(nodes)
_register_signal_commands(signals)
_register_resource_commands(resources)
@@ -47,7 +54,7 @@ func _enter_tree() -> void:
_debugger_plugin = DebuggerPlugin.new()
_debugger_plugin.forward_target = _connection
add_debugger_plugin(_debugger_plugin)
var err := _connection.startup(_capability_record_path())
var err: int = _connection.startup(_capability_record_path())
if err != OK:
push_warning("GodotOpenCode: bridge not listening (%s)" % error_string(err))
@@ -66,8 +66,21 @@ func _undo_reinsert_by_path(scene_root: Node, parent_path: String, node: Node, i
## Rename the node at `node_path`. Undo resolves the node by its NEW path
## (post-rename) so a rename-then-reorder sequence still finds it.
## Node-name rule (verified against Godot 4.7): non-empty, and none of the
## characters the engine silently rewrites on add_child (":", "/", ".",
## "@", '"', "%"). Rejected names fail closed with INVALID_PARAMS instead
## of being stored mangled.
static func is_valid_node_name(value: String) -> bool:
if value.is_empty():
return false
for ch in [":", "/", ".", "@", "\"", "%"]:
if ch in value:
return false
return true
func rename_node_by_path(scene_root: Node, node_path: String, new_name: String) -> Dictionary:
if new_name.is_empty() or not new_name.is_valid_node_name():
if not is_valid_node_name(new_name):
return {"error": {"code": "INVALID_PARAMS", "message": "Invalid node name: %s" % new_name}}
var target := _resolve_by_path(scene_root, node_path)
if target == null:
+5 -1
View File
@@ -94,7 +94,11 @@ describe("ticket 2.6: out-of-band debugger and engine bridge", () => {
expect(existsSync(PLUGIN)).toBe(true);
const src = readFileSync(PLUGIN, "utf8");
expect(src).toContain("extends EditorDebuggerPlugin");
expect(src).toContain("register_message_capture(");
// Live-verified on Godot 4.7: captures route via the _has_capture/_capture
// virtuals. (register_message_capture does not exist on
// EditorDebuggerSession — asserting it here pinned the bug.)
expect(src).toContain("func _has_capture(capture: String) -> bool");
expect(src).toContain("func _capture(");
expect(src).toContain('CAPTURE_CHANNEL := "opencode"');
expect(src).toContain("game_exception");
expect(src).toContain("stack");
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@heretek-ai/godot-opencode",
"version": "1.0.0",
"description": "Godot Engine OpenCode v2 native plugin — zero-Python transport, verification, studio swarm and vision (Bun/TypeScript)",
"version": "1.0.1",
"description": "Godot Engine OpenCode v2 native plugin \u2014 zero-Python transport, verification, studio swarm and vision (Bun/TypeScript)",
"license": "MIT",
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@heretek-ai/godot-testkit",
"version": "1.0.0",
"version": "1.0.1",
"description": "Shared test helpers for godot-opencode (mock transports, fixtures)",
"license": "MIT",
"type": "module",
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Engine GDScript parse gate — regression pin for the v1.0.0 finding that the
# companion addon shipped with parse errors no mock harness could catch
# (same-line suite in connection.gd, String.is_valid_node_name, wrong
# _has_capture override, uninferrable :=). Stages companion/ into a throwaway
# Godot project and --check-only compiles every .gd file.
#
# Usage:
# scripts/check-gdscript.sh [--godot-bin "<bin>"]
# GODOT_BIN env is honored when the flag is absent. The binary may be a
# multi-word launcher, e.g.:
# scripts/check-gdscript.sh --godot-bin "flatpak run --branch=stable --arch=x86_64 --command=godot org.godotengine.Godot"
# No binary on PATH -> prints "unavailable" and exits 0 (CI-tolerated,
# mirroring the verify/ unavailable legs). Any SCRIPT ERROR -> exit 1.
set -u
BIN="${GODOT_BIN:-godot}"
if [ "${1:-}" = "--godot-bin" ]; then
BIN="${2:?missing binary after --godot-bin}"
fi
# shellcheck disable=SC2086
if ! command -v ${BIN%% *} >/dev/null 2>&1; then
echo "check-gdscript: unavailable (no Godot binary) — skipped"
exit 0
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Stage under $HOME: Flatpak-sandboxed engines cannot see /tmp, so mktemp(1)'s
# default glimpse outside the sandbox. A cache dir in $HOME is visible both
# inside and outside the sandbox.
STAGE_BASE="${XDG_CACHE_HOME:-$HOME/.cache}/godot-parse-gate"
mkdir -p "$STAGE_BASE"
STAGE="$(mktemp -d "$STAGE_BASE/stage.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
mkdir -p "$STAGE/addons"
cp -r "$ROOT/companion/addons/godot_opencode" "$STAGE/addons/godot_opencode"
cp "$ROOT/companion/validate_scene.gd" "$STAGE/validate_scene.gd"
printf 'config_version=5\n\n[application]\nconfig/name="ParseGate"\n' > "$STAGE/project.godot"
count=0
fail=0
while IFS= read -r f; do
count=$((count + 1))
rel="res://${f#$STAGE/}"
# shellcheck disable=SC2086
if ! out=$($BIN --headless --path "$STAGE" --check-only --script "$rel" 2>&1); then
echo "FAIL: $rel"
printf '%s\n' "$out" | grep "SCRIPT ERROR" | head -n 3
fail=1
fi
done < <(find "$STAGE" -name "*.gd")
if [ "$fail" -ne 0 ]; then
echo "check-gdscript: FAIL ($count files)"
exit 1
fi
echo "check-gdscript: ok ($count files)"