Skip to content

chore: bump com.IvanMurzak.McpPlugin 7.5.1 -> 7.5.2 (#330) #165

chore: bump com.IvanMurzak.McpPlugin 7.5.1 -> 7.5.2 (#330)

chore: bump com.IvanMurzak.McpPlugin 7.5.1 -> 7.5.2 (#330) #165

Workflow file for this run

# ┌──────────────────────────────────────────────────────────────────┐
# │ 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"