chore: bump com.IvanMurzak.McpPlugin 7.5.1 -> 7.5.2 (#330) #165
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
| # ┌──────────────────────────────────────────────────────────────────┐ | |
| # │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ | |
| # │ Repository: GitHub (https://github.com/IvanMurzak/Godot-MCP) │ | |
| # │ Copyright (c) 2026 Ivan Murzak │ | |
| # │ Licensed under the Apache License, Version 2.0. │ | |
| # │ See the LICENSE file in the project root for more information. │ | |
| # └──────────────────────────────────────────────────────────────────┘ | |
| # | |
| # Release pipeline (Unity-MCP parity: release.yml -> deploy.yml). | |
| # | |
| # Trigger: push to `main`. A `check-version-tag` job reads the release version | |
| # from addons/godot_mcp/plugin.cfg (the SINGLE source of truth for the release | |
| # version — see docs/RELEASING.md) and decides whether THIS push should cut a | |
| # release. The gate (`should_release`) is the conjunction of two conditions: | |
| # | |
| # 1. tag_exists == 'false' — no `v<version>` tag exists yet for the | |
| # version currently in plugin.cfg, AND | |
| # 2. version_changed == 'true' — this push actually modified plugin.cfg's | |
| # `version=` line (a deliberate bump). | |
| # | |
| # EVERY downstream release/publish job is gated on | |
| # `needs.check-version-tag.outputs.should_release == 'true'`, so: | |
| # | |
| # * A plain merge to main that does NOT touch plugin.cfg's version is a | |
| # guaranteed no-op (version_changed == 'false') — NO release, NO npm | |
| # publish — even on a virgin repo that has no tags yet. THIS is what makes | |
| # merging a workflow-only / feature PR inert. | |
| # * A merge that DOES bump plugin.cfg to a new (untagged) version satisfies | |
| # both conditions -> the gate opens, tests run, the GitHub Release (addon | |
| # zip) is cut on the new `v<version>` tag, and the cli is published to npm. | |
| # * Re-running on an already-released version is also a no-op (tag_exists | |
| # short-circuits even if plugin.cfg appears in the diff). | |
| # | |
| # The `version_changed` condition is deliberately STRICTER than Unity-MCP's | |
| # tag-only gate: Godot-MCP's current 0.1.0 has no tag yet, so a tag-only gate | |
| # would (wrongly) fire a v0.1.0 release on the very next merge. Requiring an | |
| # actual version-line bump prevents that and matches the safety contract that | |
| # the first real publish must be a deliberate maintainer action. | |
| # | |
| # The release is gated on the full test set as `needs:` — the .NET build+test, | |
| # the godot-mcp-cli node tests (test_cli.yml), and the Godot engine smoke matrix | |
| # (test_godot_plugin.yml for 4.3 → 4.7). The release proceeds only if all pass. | |
| # | |
| # Manual dispatch (workflow_dispatch) bypasses the version_changed check (there | |
| # is no push diff to inspect) but STILL honors tag_exists — a dispatch cuts a | |
| # release only when the current plugin.cfg version has no tag yet. This is the | |
| # escape hatch for re-running a release when a bump already landed on main. | |
| # | |
| # SAFETY: the npm publish (deploy.yml) runs ONLY on a real version bump AND | |
| # requires the owner to have configured an npm Trusted Publisher for the | |
| # godot-mcp-cli package (no NPM_TOKEN secret exists; auth is OIDC at runtime). | |
| # Until both are true, no publish happens. | |
| name: release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| # Least-privilege defaults; the publish job in deploy.yml elevates id-token for OIDC. | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Read the release version from plugin.cfg and decide whether THIS push | |
| # should cut a release: only when no `v<version>` tag exists yet AND this | |
| # push actually bumped plugin.cfg's version line (see should_release). | |
| check-version-tag: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.get_version.outputs.version }} | |
| tag: ${{ steps.get_version.outputs.tag }} | |
| tag_exists: ${{ steps.tag_exists.outputs.exists }} | |
| version_changed: ${{ steps.version_changed.outputs.changed }} | |
| should_release: ${{ steps.gate.outputs.should_release }} | |
| server_version: ${{ steps.server_version.outputs.server_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| # Single-source the harness server version from the addon's authoritative constant | |
| # (GodotMcpServerView.ServerVersion) so the advisory runtime-harness legs download the | |
| # server release the addon actually pins — never a stale hardcoded default. | |
| - name: Read ServerVersion from the addon constant | |
| id: server_version | |
| run: | | |
| set -euo pipefail | |
| src="addons/godot_mcp/Runtime/Connection/GodotMcpServerView.cs" | |
| server_version="$(grep -oE 'ServerVersion[[:space:]]*=[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' "$src" \ | |
| | head -n1 | sed -E 's/.*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/')" | |
| if [ -z "${server_version}" ]; then | |
| echo "::error::Could not read ServerVersion from ${src}" | |
| exit 1 | |
| fi | |
| echo "server_version=${server_version}" >> "$GITHUB_OUTPUT" | |
| echo "Addon-pinned server version: ${server_version}" | |
| # plugin.cfg is an INI file, so read its `version="x.y.z"` line directly | |
| # rather than with npm-get-version-action (which expects a package.json). | |
| - name: Get version from plugin.cfg | |
| id: get_version | |
| run: | | |
| set -euo pipefail | |
| version="$(grep -E '^version=' addons/godot_mcp/plugin.cfg | head -n1 | sed -E 's/^version="?([^"]*)"?.*/\1/')" | |
| if [ -z "${version}" ]; then | |
| echo "::error::Could not read version= from addons/godot_mcp/plugin.cfg" | |
| exit 1 | |
| fi | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "tag=v${version}" >> "$GITHUB_OUTPUT" | |
| echo "Release version: ${version} (tag v${version})" | |
| - name: Check if tag exists | |
| id: tag_exists | |
| uses: mukunku/tag-exists-action@v1.7.0 | |
| with: | |
| tag: ${{ steps.get_version.outputs.tag }} | |
| # Did THIS push bump the version line in plugin.cfg? On a push event we | |
| # diff the pushed range (before..after) and look for a change to the | |
| # `version=` line of addons/godot_mcp/plugin.cfg. A plain feature/workflow | |
| # merge that does not touch that line yields changed=false, which keeps | |
| # the whole release inert. On workflow_dispatch (no push diff) we cannot | |
| # inspect a range, so we report changed=true and rely on tag_exists to | |
| # keep dispatch a no-op for an already-released version. | |
| - name: Detect version-line bump | |
| id: version_changed | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| AFTER_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${EVENT_NAME}" != "push" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "Non-push event (${EVENT_NAME}): version-bump check skipped (relying on tag_exists gate)." | |
| exit 0 | |
| fi | |
| # A brand-new branch push reports an all-zero before SHA; treat as changed | |
| # so a first push that already contains a bump is not silently ignored. | |
| if [ -z "${BEFORE_SHA}" ] || [ "${BEFORE_SHA}" = "0000000000000000000000000000000000000000" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "No usable 'before' SHA — treating as changed." | |
| exit 0 | |
| fi | |
| if git diff "${BEFORE_SHA}" "${AFTER_SHA}" -- addons/godot_mcp/plugin.cfg \ | |
| | grep -qE '^\+version='; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "plugin.cfg version line changed in ${BEFORE_SHA}..${AFTER_SHA}." | |
| else | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "plugin.cfg version line unchanged in ${BEFORE_SHA}..${AFTER_SHA} — release is a no-op." | |
| fi | |
| # The release gate: cut a release only when no tag exists yet for the | |
| # current version AND this push actually bumped the version line. | |
| - name: Compute release gate | |
| id: gate | |
| env: | |
| TAG_EXISTS: ${{ steps.tag_exists.outputs.exists }} | |
| VERSION_CHANGED: ${{ steps.version_changed.outputs.changed }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${TAG_EXISTS}" = "false" ] && [ "${VERSION_CHANGED}" = "true" ]; then | |
| echo "should_release=true" >> "$GITHUB_OUTPUT" | |
| echo "Gate OPEN: tag does not exist and version was bumped -> release." | |
| else | |
| echo "should_release=false" >> "$GITHUB_OUTPUT" | |
| echo "Gate CLOSED: tag_exists=${TAG_EXISTS}, version_changed=${VERSION_CHANGED} -> no release." | |
| fi | |
| # --- TEST GATE (only runs when the version tag does not yet exist) --- | |
| # .NET build + xUnit (mirrors ci.yml). Required before a release is cut. | |
| dotnet-build-test: | |
| runs-on: ubuntu-latest | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET 8 | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Restore | |
| run: dotnet restore Godot-MCP.sln | |
| - name: Build | |
| run: dotnet build Godot-MCP.sln --configuration Debug --no-restore | |
| - name: Test | |
| run: dotnet test Godot-MCP.Tests/Godot-MCP.Tests.csproj --configuration Debug --no-build --verbosity normal | |
| # godot-mcp-cli node tests (Node 20 & 22). Required before a release is cut. | |
| test-cli: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli.yml | |
| # godot_mcp addon-load smoke across the Godot engine matrix (mono). | |
| test-godot-4-3: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_plugin.yml | |
| with: | |
| godotVersion: "4.3.0" | |
| test-godot-4-4: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_plugin.yml | |
| with: | |
| godotVersion: "4.4.0" | |
| test-godot-4-5: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_plugin.yml | |
| with: | |
| godotVersion: "4.5.1" | |
| test-godot-4-6: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_plugin.yml | |
| with: | |
| godotVersion: "4.6.3" | |
| test-godot-4-7: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_plugin.yml | |
| with: | |
| godotVersion: "4.7.0" | |
| # in-session-rebuild dock-survival guard across the Godot engine matrix (mono): | |
| # boots a headless editor held open with the dev-control bridge, triggers a genuine | |
| # in-session C# rebuild, and asserts the "AI Game Developer" dock SURVIVES the hot | |
| # reload (godotengine/godot#51626). Gates the release alongside the addon-load smoke | |
| # so the "missing dock tab" regression can NEVER ship. MUST stay in lockstep with | |
| # test_pull_request.yml's dock-reload-4-{3..7} legs. | |
| dock-reload-4-3: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_dock_reload.yml | |
| with: | |
| godotVersion: "4.3.0" | |
| dock-reload-4-4: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_dock_reload.yml | |
| with: | |
| godotVersion: "4.4.0" | |
| dock-reload-4-5: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_dock_reload.yml | |
| with: | |
| godotVersion: "4.5.1" | |
| dock-reload-4-6: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_dock_reload.yml | |
| with: | |
| godotVersion: "4.6.3" | |
| dock-reload-4-7: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_dock_reload.yml | |
| with: | |
| godotVersion: "4.7.0" | |
| # from-scratch terminal-install E2E across the Godot engine matrix (mono). Gates | |
| # the release alongside the addon-load smoke so a broken from-scratch terminal | |
| # install (create-project + install-plugin) can NEVER ship in a release. | |
| e2e-install-4-3: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli_e2e_install.yml | |
| with: | |
| godotVersion: "4.3.0" | |
| e2e-install-4-4: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli_e2e_install.yml | |
| with: | |
| godotVersion: "4.4.0" | |
| e2e-install-4-5: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli_e2e_install.yml | |
| with: | |
| godotVersion: "4.5.1" | |
| e2e-install-4-6: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli_e2e_install.yml | |
| with: | |
| godotVersion: "4.6.3" | |
| e2e-install-4-7: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_cli_e2e_install.yml | |
| with: | |
| godotVersion: "4.7.0" | |
| # godot_mcp runtime INTEGRATION harness across the Godot engine matrix (mono): | |
| # downloads the released gamedev-mcp-server, boots a headless game with the | |
| # in-game runtime + error capture, and asserts the live SignalR roundtrip + | |
| # runtime-error capture (issue #186). Gates the release alongside the addon-load | |
| # smoke so a transport/capture regression cannot ship in a release. | |
| # serverVersion is single-sourced from the addon constant (check-version-tag.server_version), | |
| # so these advisory legs download the server release the addon actually pins. | |
| runtime-harness-4-3: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_runtime_harness.yml | |
| with: | |
| godotVersion: "4.3.0" | |
| serverVersion: ${{ needs.check-version-tag.outputs.server_version }} | |
| runtime-harness-4-4: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_runtime_harness.yml | |
| with: | |
| godotVersion: "4.4.0" | |
| serverVersion: ${{ needs.check-version-tag.outputs.server_version }} | |
| runtime-harness-4-5: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_runtime_harness.yml | |
| with: | |
| godotVersion: "4.5.1" | |
| serverVersion: ${{ needs.check-version-tag.outputs.server_version }} | |
| runtime-harness-4-6: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_runtime_harness.yml | |
| with: | |
| godotVersion: "4.6.3" | |
| serverVersion: ${{ needs.check-version-tag.outputs.server_version }} | |
| runtime-harness-4-7: | |
| needs: [check-version-tag] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| uses: ./.github/workflows/test_godot_runtime_harness.yml | |
| with: | |
| godotVersion: "4.7.0" | |
| serverVersion: ${{ needs.check-version-tag.outputs.server_version }} | |
| # NOTE: the MCP server is NOT built or released here anymore. The addon consumes the | |
| # shared GameDev-MCP-Server (https://github.com/IvanMurzak/GameDev-MCP-Server), whose | |
| # own release workflow publishes the `gamedev-mcp-server-<rid>.zip` assets the addon | |
| # downloads at the version pinned by `GodotMcpServerView.ServerVersion`. That pinned | |
| # `v<ServerVersion>` release must already exist BEFORE cutting an addon release. | |
| # --- PUBLISH (gated on tag-not-existing AND every test passing) --- | |
| # Build + package the addon zip and cut the GitHub Release on the new | |
| # `v<version>` tag. Identical packaging to the previous tag-triggered flow, | |
| # now gated on the version check + tests instead of a tag push. The release | |
| # carries ONLY the addon zip — the server binaries live on GameDev-MCP-Server's | |
| # own releases. | |
| release-addon: | |
| runs-on: ubuntu-latest | |
| # The release gate depends ONLY on the DETERMINISTIC test legs (check-version-tag, | |
| # the .NET build+test, the cli node tests, the addon-LOAD godot smoke, the | |
| # in-session-rebuild dock-survival guard (dock-reload), and the from-scratch | |
| # e2e-install) across the FULL Godot matrix (4.3 → 4.7). This MUST stay | |
| # in lockstep with test_pull_request.yml's matrix so a green PR can never produce a red | |
| # release gate — when you add/remove a Godot version, edit BOTH files together. The | |
| # runtime-harness-4-{3..7} legs are deliberately NOT in this `needs:` array: they are | |
| # an end-to-end INTEGRATION harness that downloads an external server release and runs a | |
| # live, concurrency-sensitive roundtrip — a transient harness flake or an unavailable | |
| # upstream server release must NEVER block a Godot-MCP release. The harness legs still | |
| # RUN on the release trigger (in parallel, providing advisory signal against the live | |
| # server version); they are just decoupled from the release gate. They remain BLOCKING on | |
| # PR CI (test_pull_request.yml), so main is always harness-green before a release is cut. | |
| needs: | |
| [ | |
| check-version-tag, | |
| dotnet-build-test, | |
| test-cli, | |
| test-godot-4-3, | |
| test-godot-4-4, | |
| test-godot-4-5, | |
| test-godot-4-6, | |
| test-godot-4-7, | |
| dock-reload-4-3, | |
| dock-reload-4-4, | |
| dock-reload-4-5, | |
| dock-reload-4-6, | |
| dock-reload-4-7, | |
| e2e-install-4-3, | |
| e2e-install-4-4, | |
| e2e-install-4-5, | |
| e2e-install-4-6, | |
| e2e-install-4-7, | |
| ] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| outputs: | |
| version: ${{ needs.check-version-tag.outputs.version }} | |
| tag: ${{ needs.check-version-tag.outputs.tag }} | |
| published: ${{ steps.mark.outputs.published }} | |
| release_notes: ${{ steps.read_notes.outputs.release_body }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET 8 | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: "8.0.x" | |
| # Sanity build + test before producing the zip, so a release is never cut | |
| # on a red commit. Godot.NET.Sdk is a NuGet SDK -> no Godot binary needed. | |
| - name: Restore | |
| run: dotnet restore Godot-MCP.sln | |
| - name: Build | |
| run: dotnet build Godot-MCP.sln --configuration Debug --no-restore | |
| - name: Test | |
| run: dotnet test Godot-MCP.Tests/Godot-MCP.Tests.csproj --configuration Debug --no-build --verbosity normal | |
| # Package only the addon SOURCE folder (the deliverable — Godot has no | |
| # compiled artifact to ship). Exclude dev cruft so the zip is install-clean. | |
| - name: Package addon zip | |
| id: package | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.check-version-tag.outputs.version }}" | |
| zip_name="godot-mcp-addon-${version}.zip" | |
| # Zip the addons/ tree so the archive expands to addons/godot_mcp/..., | |
| # which drops straight into a consumer project's res:// root. | |
| zip -r "${zip_name}" addons/godot_mcp \ | |
| -x '*.uid' \ | |
| -x '*.import' \ | |
| -x '*/bin/*' \ | |
| -x '*/obj/*' \ | |
| -x '*/.godot/*' | |
| echo "zip_name=${zip_name}" >> "$GITHUB_OUTPUT" | |
| echo "Created ${zip_name}:" | |
| unzip -l "${zip_name}" | |
| # Create the GitHub Release + tag with the addon zip attached and | |
| # auto-generated notes. tag_name is the `v<version>` computed from | |
| # plugin.cfg. The server binaries are NOT attached — the addon downloads | |
| # the shared GameDev-MCP-Server release pinned by ServerVersion. | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: ${{ needs.check-version-tag.outputs.tag }} | |
| tag_name: ${{ needs.check-version-tag.outputs.tag }} | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| files: | | |
| ${{ steps.package.outputs.zip_name }} | |
| - name: Mark publish success | |
| id: mark | |
| run: echo "published=true" >> "$GITHUB_OUTPUT" | |
| # Expose the just-published release body as a job output so publish_discord | |
| # can announce it. Unlike Unity-MCP (which builds a release-notes artifact | |
| # and reads ./release-notes/release.md), this repo has NO release-notes | |
| # file — the body is GENERATED BY GITHUB via `generate_release_notes: true` | |
| # above, so the only source is reading it back from the API. | |
| # | |
| # SAFETY: this step is purely informational and must NEVER be able to fail | |
| # the release. It runs AFTER `mark` (so `published` is already set), is | |
| # `continue-on-error`, and its script is internally non-fatal (no `set -e`, | |
| # every command `|| true`-guarded). Worst case the notes come back empty | |
| # and Discord gets the tag + release link. | |
| - name: Read release notes into job output | |
| id: read_notes | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.check-version-tag.outputs.tag }} | |
| run: | | |
| set -uo pipefail | |
| body="" | |
| for attempt in 1 2 3; do | |
| body="$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json body --jq '.body' 2>/dev/null || true)" | |
| if [ -n "${body}" ]; then | |
| break | |
| fi | |
| echo "Generated release body not readable yet (attempt ${attempt}/3); retrying in 5s..." | |
| sleep 5 | |
| done | |
| if [ -z "${body}" ]; then | |
| echo "::warning::Could not read the generated release body for ${TAG}; falling back to the tag name." | |
| body="Release ${TAG}" | |
| fi | |
| # Randomized heredoc delimiter so a release-notes line that happens to | |
| # match the sentinel cannot terminate the heredoc early. | |
| DELIM="ENDOFRELEASEBODY_$(openssl rand -hex 16)" | |
| { | |
| printf 'release_body<<%s\n' "$DELIM" | |
| printf '%s\n' "$body" | |
| printf '%s\n' "$DELIM" | |
| } >> "$GITHUB_OUTPUT" | |
| # Auto-submit the Godot Asset Library VERSION EDIT for the just-released | |
| # version. Runs AFTER the GitHub Release (needs release-addon) and only on a | |
| # real version bump (should_release == 'true'). This job is intentionally | |
| # ISOLATED: nothing downstream needs it, so an AssetLib failure shows up as a | |
| # red job for visibility but never blocks or fails the npm `deploy` job (which | |
| # only needs release-addon.published). The one-time INITIAL submission + | |
| # moderator approval remain manual (see docs/RELEASING.md); this automates the | |
| # per-release EDIT only. | |
| # | |
| # Credentials/ids come ONLY from GitHub-managed config (never hardcoded): | |
| # secrets.GODOT_ASSETLIB_USERNAME / secrets.GODOT_ASSETLIB_PASSWORD | |
| # vars.GODOT_ASSETLIB_ASSET_ID | |
| # If they are not configured, the action fails auth (job red) but the release | |
| # and npm publish are unaffected. | |
| assetlib-edit: | |
| runs-on: ubuntu-latest | |
| needs: [check-version-tag, release-addon] | |
| if: needs.check-version-tag.outputs.should_release == 'true' | |
| steps: | |
| - name: Checkout repository | |
| # The action reads the handlebars template (.github/assetlib/edit.hbs) | |
| # from the workspace, so the repo must be checked out. | |
| uses: actions/checkout@v6 | |
| - name: Submit Asset Library version edit | |
| # deep-entertainment/godot-asset-lib-action @ v0.6.0 | |
| # Pinned to the commit SHA for supply-chain safety (the action runs | |
| # arbitrary JS with the AssetLib credentials). Bump the SHA + the | |
| # version comment together when upgrading. | |
| uses: deep-entertainment/godot-asset-lib-action@056fa4060f062a8b209a5a6744a2726ad48d6bb0 # v0.6.0 | |
| env: | |
| # Canonical version (from plugin.cfg via check-version-tag) and the | |
| # released commit (github.sha is the commit the v<version> tag points | |
| # at — release-addon cut the tag on this same commit). The handlebars | |
| # template reads these as {{ env.ASSETLIB_VERSION }} and | |
| # {{ env.ASSETLIB_DOWNLOAD_COMMIT }}. | |
| ASSETLIB_VERSION: ${{ needs.check-version-tag.outputs.version }} | |
| ASSETLIB_DOWNLOAD_COMMIT: ${{ github.sha }} | |
| with: | |
| action: addEdit | |
| username: ${{ secrets.GODOT_ASSETLIB_USERNAME }} | |
| password: ${{ secrets.GODOT_ASSETLIB_PASSWORD }} | |
| assetId: ${{ vars.GODOT_ASSETLIB_ASSET_ID }} | |
| assetTemplate: .github/assetlib/edit.hbs | |
| # Publish godot-mcp-cli to npm (OIDC Trusted Publishing). Gated on the GitHub | |
| # Release having been created. The cli version is set to the release version | |
| # at publish time inside deploy.yml. | |
| deploy: | |
| needs: [check-version-tag, release-addon] | |
| if: needs.release-addon.outputs.published == 'true' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| uses: ./.github/workflows/deploy.yml | |
| with: | |
| version: ${{ needs.check-version-tag.outputs.version }} | |
| # Announce the just-cut GitHub Release in Discord (Unity-MCP parity — see the | |
| # `publish_discord` job in Unity-MCP's release.yml). Like assetlib-edit this | |
| # job is deliberately ISOLATED: nothing downstream needs it, so a webhook | |
| # hiccup surfaces as a red job for visibility but can never block or fail the | |
| # release or the npm `deploy`. | |
| # | |
| # Gated `always() && needs.release-addon.result == 'success'` so it fires ONLY | |
| # for a release that actually published — a skipped (no version bump) or failed | |
| # release-addon leaves result != 'success' and nothing is announced. | |
| publish_discord: | |
| runs-on: ubuntu-latest | |
| needs: [release-addon] | |
| if: | | |
| always() && | |
| needs.release-addon.result == 'success' | |
| steps: | |
| - name: Send Release Notes to Discord | |
| env: | |
| DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_NOTES_WEBHOOK }} | |
| RELEASE_NOTES: ${{ needs.release-addon.outputs.release_notes }} | |
| VERSION: ${{ needs.release-addon.outputs.version }} | |
| TAG: ${{ needs.release-addon.outputs.tag }} | |
| run: | | |
| # Get the release notes from environment variables | |
| # (Using env vars avoids shell quoting issues with apostrophes in commit messages) | |
| release_notes="$RELEASE_NOTES" | |
| version="$VERSION" | |
| tag="$TAG" | |
| if [ -z "${DISCORD_WEBHOOK:-}" ]; then | |
| echo "::warning::DISCORD_RELEASE_NOTES_WEBHOOK is not configured — skipping the Discord announcement." | |
| exit 0 | |
| fi | |
| # --- Unity-MCP formatting parity: synthesize the release-notes PREAMBLE --- | |
| # Unity-MCP hand-builds a `release.md` whose first lines are a | |
| # project+version heading, a "**Released:** *<date>*" line and a `---` | |
| # separator (see its `prepare-release-notes` job), and feeds that ONE file to | |
| # both the GitHub Release body and Discord — which is why its Discord post | |
| # opens with a large heading. This repo's release body is GitHub-generated | |
| # (`generate_release_notes: true`), so it has no heading by construction. | |
| # Prepending the same preamble here, BEFORE the shared cleanup below, makes | |
| # the rendered Discord message identical in shape to Unity's. | |
| # `date +'%B %e, %Y'` is copied verbatim from Unity-MCP (%e is space-padded) | |
| # so the date renders character-for-character the same as Unity's. | |
| today=$(date +'%B %e, %Y') | |
| release_notes="$(printf '# AI Game Developer (Godot MCP) %s\n**Released:** *%s*\n\n---\n\n%s\n' \ | |
| "$tag" "$today" "$release_notes")" | |
| # Convert usernames to GitHub profile links | |
| # Convert "by @username" to "by [@username](https://github.com/username)" | |
| release_notes_with_links=$(echo "$release_notes" | sed -E 's/by @([A-Za-z0-9_-]+)/by [@\1](https:\/\/github.com\/\1)/g') | |
| # Remove horizontal separators (---) and clean up extra blank lines | |
| # 1. Remove lines containing only --- and whitespace | |
| # 2. Remove lines that contain only whitespace | |
| # 3. Compress multiple consecutive empty lines into one | |
| release_notes_cleaned=$(echo "$release_notes_with_links" | \ | |
| sed '/^[[:space:]]*---[[:space:]]*$/d' | \ | |
| sed 's/^[[:space:]]*$//' | \ | |
| sed '/^$/N;/^\n$/d') | |
| # Create the release URL (the tag is `v<version>` from plugin.cfg) | |
| release_url="https://github.com/${{ github.repository }}/releases/tag/${tag}" | |
| # Append the release link to the notes | |
| full_message="${release_notes_cleaned}"$'\n\n'"📦 **[View Full Release](${release_url})**" | |
| # Discord has a 2000 character limit, so we'll truncate if needed | |
| if [ ${#full_message} -gt 2000 ]; then | |
| # Remove lines from the end until it fits, keeping the link | |
| link_text=$'\n\n'"📦 **[View Full Release](${release_url})**" | |
| # The truncated message is reassembled as `<notes>` + `\n...` + `<link>`, | |
| # so the ellipsis line costs 4 more characters on top of the link. Budget | |
| # for BOTH so the assembled message is provably <= 2000 (Unity-MCP omits | |
| # the ellipsis from its budget and can emit 2004 chars, which Discord | |
| # rejects with a 400 — see the report on this change). The heading | |
| # synthesized above is part of `release_notes_cleaned` and is therefore | |
| # kept by the loop below (it is the first lines); it simply consumes | |
| # budget that would otherwise hold more note lines. | |
| ellipsis_text=$'\n'"..." | |
| max_length=$((2000 - ${#link_text} - ${#ellipsis_text})) | |
| # Split into lines and rebuild until we exceed the limit | |
| truncated_notes="" | |
| while IFS= read -r line; do | |
| test_notes="${truncated_notes}${line}"$'\n' | |
| if [ ${#test_notes} -gt $max_length ]; then | |
| break | |
| fi | |
| truncated_notes="$test_notes" | |
| done <<< "$release_notes_cleaned" | |
| # Add ellipsis on its own line if we truncated | |
| full_message="${truncated_notes}"$'\n'"...${link_text}" | |
| fi | |
| # Create JSON payload for Discord webhook | |
| # flags: 4 sets the SUPPRESS_EMBEDS flag to suppress embeds/link previews. | |
| # See: https://discord.com/developers/docs/resources/channel#create-message | |
| json_payload=$(jq -n \ | |
| --arg content "$full_message" \ | |
| '{content: $content, flags: 4}') | |
| # Send to Discord webhook | |
| curl -X POST "$DISCORD_WEBHOOK" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$json_payload" | |
| echo "Release notes sent to Discord for version $version" |