Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -22,14 +25,19 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Set up uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5

- name: Install dependencies
run: pip install -e . -r requirements-dev.txt
run: |
uv venv
uv pip install -r requirements.txt -r requirements-dev.txt -e .

- name: Check release versions
run: python scripts/check_release_versions.py

- name: Run tests
run: pytest -q
run: uv run pytest -q

highlighter:
runs-on: ubuntu-latest
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Release

on:
push:
tags:
- "v*"

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5

- name: Install dependencies
run: |
uv venv
uv pip install -r requirements.txt -r requirements-dev.txt -e .

- name: Check release versions
run: python scripts/check_release_versions.py --tag "${{ github.ref_name }}"

- name: Run tests
run: uv run pytest -q

publish-npm:
needs: verify
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Verify npm package version matches tag
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
NPM_VERSION="$(node -p "require('./npm/package.json').version")"
if [ "$TAG_VERSION" != "$NPM_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match npm/package.json ($NPM_VERSION)"
exit 1
fi

- name: Publish to npm
working-directory: npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

github-release:
needs: [verify, publish-npm]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
.venv/
twine_upload
setup.py
test_file.py
Expand Down
17 changes: 11 additions & 6 deletions contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This fork is maintained at [SpyC0der77/Minecraft-Script](https://github.com/SpyC
```commandline
git clone https://github.com/SpyC0der77/Minecraft-Script.git
cd Minecraft-Script
pip install -e . -r requirements-dev.txt
uv venv
uv pip install -r requirements.txt -r requirements-dev.txt -e .
```

2. Confirm the CLI works:
Expand All @@ -33,7 +34,7 @@ Open `highlighter/` in VS Code and press `F5` to launch an Extension Development
From the repository root:

```commandline
pytest -q
uv run pytest -q
```

CI runs the same suite on Python 3.10 and 3.12, plus a highlighter build check.
Expand Down Expand Up @@ -82,7 +83,11 @@ git push origin main
git push origin v0.3.5
```

Create a GitHub release from the tag on [SpyC0der77/Minecraft-Script/releases](https://github.com/SpyC0der77/Minecraft-Script/releases). Use the tag name as the release title (for example `v0.3.5`).
Pushing a `v*` tag triggers the [Release workflow](.github/workflows/release.yml). It verifies aligned versions, runs tests, creates a GitHub release with generated notes, and publishes the npm package.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Add an `NPM_TOKEN` repository secret (npm access token with publish rights) before the first automated publish.

You can still create or edit releases manually on [SpyC0der77/Minecraft-Script/releases](https://github.com/SpyC0der77/Minecraft-Script/releases) if needed.

The npm wrapper installs the matching Python package with:

Expand All @@ -94,7 +99,7 @@ So the GitHub tag must exist before users install that npm version.

### Publish to npm

From the `npm/` directory:
The Release workflow publishes automatically when a matching `v*` tag is pushed. To publish manually from the `npm/` directory:

```commandline
cd npm
Expand All @@ -115,8 +120,8 @@ npm install -g minecraft-script
- [ ] `pyproject.toml` and `npm/package.json` versions match
- [ ] `python scripts/check_release_versions.py` passes
- [ ] Tag pushed (`v<version>`)
- [ ] GitHub release created from the tag
- [ ] `npm publish` from `npm/` succeeded
- [ ] Release workflow succeeded (GitHub release + npm publish)
- [ ] Or, if publishing manually: GitHub release created and `npm publish` from `npm/` succeeded

The VS Code extension (`highlighter/`) uses publisher **`SpyC0der77`** and is published to the Marketplace separately — see [Publishing the VS Code extension](#publishing-the-vs-code-extension).

Expand Down
10 changes: 10 additions & 0 deletions minecraft_script/compiler/builtin_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def raycast_block(interpreter, args, context) -> function_output:
raycast_range: mcs_type = args[1]
raycast_loop_function: MCSFunction | None = args[2] if len(args) > 2 else None

interpreter.schedule_function_generation(raycast_function)
if raycast_loop_function is not None:
interpreter.schedule_function_generation(raycast_loop_function)

interpreter.add_commands(
local_context.mcfunction_name,
version.render_lines(
Expand Down Expand Up @@ -165,6 +169,10 @@ def raycast_entity(interpreter, args, context) -> function_output:
raycast_range: mcs_type = args[1]
raycast_loop_function: MCSFunction | None = args[2] if len(args) > 2 else None

interpreter.schedule_function_generation(raycast_function)
if raycast_loop_function is not None:
interpreter.schedule_function_generation(raycast_loop_function)

interpreter.add_commands(
local_context.mcfunction_name,
version.render_lines(
Expand Down Expand Up @@ -330,6 +338,8 @@ def give_clickable_item(interpreter, args, context) -> function_output:
name: MCSString = args[1] if len(args) > 1 else None
custom_model_data: MCSNumber = args[2] if len(args) > 2 else None

interpreter.schedule_function_generation(click_function)

click_function_id = interpreter.click_item_lookup.get(click_function.name)
if click_function_id is None:
click_function_id = (
Expand Down
43 changes: 35 additions & 8 deletions minecraft_script/compiler/compile_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,37 @@ def __init__(self, datapack_id):
self.version = get_version_context()
self.commands = CompileCommands()
self.used_context_ids = set()
self.functions_to_generate = set()
self.defined_functions: dict[str, MCSFunction] = {}
self.generated_functions: set[MCSFunction] = set()
self.pending_function_generation: list[MCSFunction] = []
self.click_item_lookup = dict()
self.used_math_builtins = set()
self.used_builtin_functions = set()
self.scoreboard_event_functions = []

def schedule_function_generation(self, function: MCSFunction) -> None:
if function.body is None:
return
if function in self.generated_functions:
return
if function not in self.pending_function_generation:
self.pending_function_generation.append(function)

def generate_scheduled_functions(self) -> None:
for entry_name in ("init", "main", "kill"):
function = self.defined_functions.get(entry_name)
if function is not None:
self.schedule_function_generation(function)
for function in self.scoreboard_event_functions:
self.schedule_function_generation(function)

while self.pending_function_generation:
function = self.pending_function_generation.pop(0)
if function in self.generated_functions:
continue
self.generated_functions.add(function)
function.generate_function(self)

def get_scoreboard_event_hooks(self) -> list[dict[str, str]]:
hooks = []
for index, function in enumerate(self.scoreboard_event_functions):
Expand Down Expand Up @@ -206,9 +231,10 @@ def visit_DefineFunctionNode(self, node, context: CompileContext) -> CompileResu
if SCOREBOARD_CRITERIA_PATTERN.fullmatch(event_criteria) is None:
raise ValueError(f"Invalid scoreboard criteria {event_criteria!r}")
function = MCSFunction(context.get_function_name(fnc_name), fnc_body, fnc_parameter_names, context, event_criteria)
self.functions_to_generate.add(function)
self.defined_functions[function.name] = function
if event_criteria is not None:
self.scoreboard_event_functions.append(function)
self.schedule_function_generation(function)
context.declare(fnc_name, function)
return CompileResult(function)
def visit_VariableDeclareNode(self, node, context: CompileContext) -> CompileResult:
Expand Down Expand Up @@ -459,11 +485,13 @@ def visit_ImportNode(self, node, context: CompileContext) -> CompileResult:
def visit_FunctionCallNode(self, node, context: CompileContext) -> CompileResult:
fnc: MCSFunction = self.visit(node.get_root(), context).get_value()
arguments: tuple[mcs_type, ...] = tuple(map(lambda x: self.visit(x, context).get_value(), node.get_arguments()))
commands, return_value = fnc.call(self, arguments, context)
is_builtin = (
hasattr(fnc.call, "__module__") and
fnc.call.__module__.endswith(".builtin_functions")
is_builtin = (
hasattr(fnc.call, "__module__")
and fnc.call.__module__.endswith(".builtin_functions")
)
if not is_builtin and fnc.body is not None:
self.schedule_function_generation(fnc)
commands, return_value = fnc.call(self, arguments, context)
Comment on lines +492 to +494

@cubic-dev-ai cubic-dev-ai Bot Jun 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Tree-shaken function scheduling misses callback functions passed into builtins like raycast_block/raycast_entity, producing runtime calls to non-generated mcfunctions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At minecraft_script/compiler/compile_interpreter.py, line 492:

<comment>Tree-shaken function scheduling misses callback functions passed into builtins like `raycast_block`/`raycast_entity`, producing runtime calls to non-generated mcfunctions.</comment>

<file context>
@@ -459,11 +485,13 @@ def visit_ImportNode(self, node, context: CompileContext) -> CompileResult:
+            hasattr(fnc.call, "__module__")
+            and fnc.call.__module__.endswith(".builtin_functions")
         )
+        if not is_builtin and fnc.body is not None:
+            self.schedule_function_generation(fnc)
+        commands, return_value = fnc.call(self, arguments, context)
</file context>
Suggested change
if not is_builtin and fnc.body is not None:
self.schedule_function_generation(fnc)
commands, return_value = fnc.call(self, arguments, context)
if not is_builtin and fnc.body is not None:
self.schedule_function_generation(fnc)
elif is_builtin:
for argument in arguments:
if isinstance(argument, MCSFunction):
self.schedule_function_generation(argument)
commands, return_value = fnc.call(self, arguments, context)
Fix with cubic

if is_builtin:
self.used_builtin_functions.add(fnc.call.__name__)
if commands is not None:
Expand Down Expand Up @@ -533,8 +561,7 @@ def _mcs_compile(ast, functions_dir: str, datapack_id):
context = CompileContext('init', top_level=True)
interpreter = CompileInterpreter(datapack_id)
interpreter.visit(ast, context)
for mcs_fnc in interpreter.functions_to_generate:
mcs_fnc.generate_function(interpreter)
interpreter.generate_scheduled_functions()
version = get_version_context()
for context_id in interpreter.used_context_ids:
commands = []
Expand Down
33 changes: 32 additions & 1 deletion scripts/check_release_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,26 @@ def read_npm_version() -> str:
return data["version"]


def main() -> int:
def normalize_tag_version(tag: str) -> str:
if not tag.startswith("v"):
raise SystemExit(f"Release tag must start with 'v', got {tag!r}")
return tag[1:]


def main(argv: list[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
tag = None
if "--tag" in args:
index = args.index("--tag")
try:
tag = args[index + 1]
except IndexError:
raise SystemExit("Missing value for --tag") from None
del args[index:index + 2]

if args:
raise SystemExit(f"Unknown arguments: {' '.join(args)}")

pyproject_version = read_pyproject_version()
npm_version = read_npm_version()

Expand All @@ -49,6 +68,18 @@ def main() -> int:
)
return 1

if tag is not None:
tag_version = normalize_tag_version(tag)
if tag_version != pyproject_version:
print(
"Tag version mismatch:\n"
f" git tag: {tag_version}\n"
f" pyproject.toml: {pyproject_version}\n"
f" npm/package.json: {npm_version}",
file=sys.stderr,
)
return 1

print(f"Release versions aligned at {pyproject_version}")
return 0

Expand Down
Loading
Loading