Release #61
Workflow file for this run
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
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| # Dispatched explicitly by bump-and-release.yml — see that workflow's | |
| # "Trigger release workflow" step for why the tag push alone isn't enough. | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| ## Block telemetry from CI — same rationale as ci.yml. The release | |
| ## workflow runs pytest before publishing; without this flag every tag | |
| ## push would fire one or more synthetic events. | |
| env: | |
| GODOT_AI_DISABLE_TELEMETRY: "true" | |
| jobs: | |
| # Publish the Python server package to PyPI FIRST so that by the time the | |
| # plugin ZIP is released and users self-update, the matching Python package | |
| # is already available on PyPI for `uvx --from godot-ai~=<version> godot-ai` | |
| # to resolve. | |
| # | |
| # Uses PyPI Trusted Publishing (OIDC) — no API tokens / secrets required. | |
| # Requires a one-time pending-publisher setup on PyPI (see README or the | |
| # PR description). | |
| publish-pypi: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write # required for PyPI OIDC | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Verify ref is a version tag | |
| # Guard against a manual workflow_dispatch on a branch ref (e.g. | |
| # `main`), which would otherwise fall through and fail the version | |
| # check with a confusing "tag (main) does not match..." message. | |
| run: | | |
| if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "ERROR: release.yml must run against a version tag (vX.Y.Z), got: $GITHUB_REF_NAME" >&2 | |
| echo "If dispatching manually, pick a v* tag in the ref dropdown." >&2 | |
| exit 1 | |
| fi | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Install build tools | |
| run: python -m pip install --upgrade build | |
| - name: Verify package version matches tag | |
| run: | | |
| tag="${GITHUB_REF_NAME#v}" | |
| pkg=$(python -c "import tomllib; print(tomllib.loads(open('pyproject.toml','rb').read().decode())['project']['version'])") | |
| if [ "$tag" != "$pkg" ]; then | |
| echo "ERROR: tag ($tag) does not match pyproject.toml version ($pkg)" >&2 | |
| exit 1 | |
| fi | |
| echo "Tag and package version match: $tag" | |
| - name: Build sdist + wheel | |
| run: python -m build | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| # Idempotent re-runs: if this version is already on PyPI (e.g. a | |
| # previous run published but a downstream job failed), skip the | |
| # upload instead of erroring with "File already exists" — which | |
| # would block the build-plugin-zip job that depends on this one. | |
| skip-existing: true | |
| # Build the Godot plugin ZIP and attach it to the GitHub Release. | |
| # Runs after publish-pypi so the Python package is live on PyPI before the | |
| # plugin ZIP download becomes available — prevents a racing user from | |
| # installing the plugin and having uvx fail to resolve the Python package. | |
| build-plugin-zip: | |
| runs-on: ubuntu-latest | |
| needs: publish-pypi | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Build plugin ZIP | |
| # Ship `addons/` + `godot-ai-LICENSE.txt` at the zip's top level. The | |
| # multi-top shape matters: Godot's AssetLib install dialog defaults | |
| # "Ignore asset root" to CHECKED whenever it detects a single top- | |
| # level folder, which would strip `addons/` and drop `godot_ai/` at | |
| # res:// — outside the plugin path and outside the addons-warning | |
| # exclusion. With two top-level entries, Godot doesn't offer the | |
| # strip option, so files land as-archived at res://addons/godot_ai/ | |
| # regardless of any user-toggled preference. | |
| # | |
| # The second top-level entry is namespaced (`godot-ai-LICENSE.txt`) | |
| # rather than a bare `LICENSE`. A bare `LICENSE` lands at | |
| # `res://LICENSE` on install and silently overwrites the user's own | |
| # project LICENSE file — issue #450. The namespaced name preserves | |
| # the AssetLib trick without clobbering anything in a typical project. | |
| # The canonical addon-scoped license still ships at | |
| # `addons/godot_ai/LICENSE` for in-tree consumers. | |
| run: | | |
| mkdir -p staging/addons | |
| cp -r plugin/addons/godot_ai staging/addons/ | |
| cp LICENSE staging/godot-ai-LICENSE.txt | |
| cd staging | |
| # `-D` strips zero-byte directory entries. The 2.2.x/2.3.0 self- | |
| # update runner has an over-strict safety check that flags the | |
| # bare `addons/godot_ai/` directory entry as unsafe and aborts | |
| # the extract. Stripping directory entries lets those installs | |
| # successfully self-update to a fixed runner. | |
| zip -D -r ../godot-ai-plugin.zip addons/ godot-ai-LICENSE.txt | |
| - name: Verify zip structure | |
| run: | | |
| # unzip -Z1 lists only entry names, one per line, no header/footer | |
| tops=$(unzip -Z1 godot-ai-plugin.zip | awk -F/ '{print $1}' | sort -u | tr '\n' ' ' | sed 's/ $//') | |
| if [ "$tops" != "addons godot-ai-LICENSE.txt" ]; then | |
| echo "ERROR: expected top-level entries 'addons godot-ai-LICENSE.txt' in zip, got:" >&2 | |
| echo " $tops" >&2 | |
| exit 1 | |
| fi | |
| if ! unzip -Z1 godot-ai-plugin.zip | grep -qx 'addons/godot_ai/plugin.cfg'; then | |
| echo "ERROR: plugin.cfg not at expected path addons/godot_ai/plugin.cfg" >&2 | |
| exit 1 | |
| fi | |
| if ! unzip -Z1 godot-ai-plugin.zip | grep -qx 'godot-ai-LICENSE.txt'; then | |
| echo "ERROR: godot-ai-LICENSE.txt not present at zip root" >&2 | |
| exit 1 | |
| fi | |
| # Guard against the issue-#450 regression: a bare `LICENSE` at zip | |
| # root would silently overwrite the installing project's own | |
| # LICENSE on AssetLib install. | |
| if unzip -Z1 godot-ai-plugin.zip | grep -qx 'LICENSE'; then | |
| echo "ERROR: zip contains bare 'LICENSE' at root — would clobber user project LICENSE on install (see #450)" >&2 | |
| exit 1 | |
| fi | |
| # Hard-fail if the zip contains zero-byte directory entries (paths | |
| # ending in `/`). The 2.2.x/2.3.0 self-update runner has an over- | |
| # strict `_is_safe_zip_addon_file` guard that rejects any path | |
| # ending in `/`, so any release zip emitted without `zip -D` will | |
| # abort the extract on those installs and strand them. This guard | |
| # is the rescue mechanism's CI invariant — see PR #281, issue #283. | |
| if unzip -Z1 godot-ai-plugin.zip | grep -qE '/$'; then | |
| echo "ERROR: zip contains directory entries (forgot \`zip -D\`?):" >&2 | |
| unzip -Z1 godot-ai-plugin.zip | grep -E '/$' >&2 | |
| exit 1 | |
| fi | |
| # Guard against macOS iCloud-sync duplicates (filenames like `foo 2.gd`) | |
| # sneaking in via a contaminated checkout. CI should never hit this | |
| # on a fresh actions/checkout, but a manual build from a synced | |
| # working tree could. Fail loudly if any are present. | |
| if unzip -Z1 godot-ai-plugin.zip | grep -q ' 2\.'; then | |
| echo "ERROR: zip contains iCloud-duplicate files (pattern ' 2.'):" >&2 | |
| unzip -Z1 godot-ai-plugin.zip | grep ' 2\.' >&2 | |
| exit 1 | |
| fi | |
| echo "Zip structure verified: addons/godot_ai/... + godot-ai-LICENSE.txt (multi-top)" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| files: godot-ai-plugin.zip | |
| generate_release_notes: true |