-
Notifications
You must be signed in to change notification settings - Fork 33
226 lines (209 loc) · 10.7 KB
/
Copy pathrelease.yml
File metadata and controls
226 lines (209 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 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@cef221092ed1bacb1cc03d23a2d87d1d172e277b # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
files: godot-ai-plugin.zip
generate_release_notes: true
- name: Post changelog to Discord
# Posts the release's auto-generated notes to the #changelog channel
# via a channel-scoped webhook. Skips cleanly when the webhook secret
# isn't configured, so releases never break if it's unset/removed.
#
# The post is silent: flags:4096 is Discord's SUPPRESS_NOTIFICATIONS
# (the @silent mechanism) — the message lands in the channel and marks
# it unread, but triggers NO push/desktop notification for anyone,
# regardless of their per-channel notification settings. The generated
# notes never contain @everyone/@here, and allowed_mentions.parse:[] is
# belt-and-suspenders against any @mention in the notes ever pinging.
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Strip any whitespace/newlines from the secret. `gh secret set`
# captures a trailing newline if the value was piped/pasted with
# one, which makes curl reject the URL with "(3) Malformed input to
# a URL function". A webhook URL contains no internal whitespace, so
# stripping all whitespace is safe and fixes contaminated secrets
# regardless of how they were set.
DISCORD_WEBHOOK=$(printf '%s' "$DISCORD_WEBHOOK" | tr -d '[:space:]')
if [ -z "$DISCORD_WEBHOOK" ]; then
echo "DISCORD_CHANGELOG_WEBHOOK not set — skipping changelog post."
exit 0
fi
tag="$GITHUB_REF_NAME"
name=$(gh release view "$tag" --json name --jq '.name')
url=$(gh release view "$tag" --json url --jq '.url')
notes=$(gh release view "$tag" --json body --jq '.body')
# Discord embed description caps at 4096 chars; leave headroom and
# append a link line so the full notes are always one click away.
desc=$(printf '%s' "$notes" | head -c 3900)
payload=$(jq -n \
--arg title "$name" \
--arg url "$url" \
--arg desc "$desc" \
'{
username: "Godot AI",
allowed_mentions: { parse: [] },
flags: 4096,
embeds: [ {
title: $title,
url: $url,
description: ($desc + "\n\n[View full release →](" + $url + ")"),
color: 5793266
} ]
}')
# The changelog post is non-essential: the release already shipped
# before this step. A Discord outage or a bad webhook must not turn
# a successful release red — log a warning annotation and exit 0.
if curl -fsS --retry 3 --retry-delay 2 -H "Content-Type: application/json" \
-X POST -d "$payload" "$DISCORD_WEBHOOK"; then
echo "Posted changelog for $tag to Discord."
else
echo "::warning::Discord changelog post for $tag failed (release itself is unaffected)."
fi