-
Notifications
You must be signed in to change notification settings - Fork 1
353 lines (299 loc) · 16 KB
/
Copy pathrelease.yml
File metadata and controls
353 lines (299 loc) · 16 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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
name: Test, Build & Release
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Read version
id: version
run: echo "version=$(cat VERSION | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
- name: Verify version sources agree
# The repo has FIVE per-language package coordinates plus the
# top-level VERSION file that MUST all match:
# * VERSION → workflow tag, release title, artifact filenames
# * kotlin/gradle.properties:VERSION_NAME → Maven Central artifact coordinate
# * python/pyproject.toml:version → wheel + sdist filename, PyPI coordinate
# * typescript/package.json:version → npm tarball filename, npm coordinate
# * csharp/.../Meshtastic.TAK.csproj:<Version> → NuGet package filename, NuGet coordinate
# A mismatch silently ships a partial release (e.g. Kotlin 0.3.3 jar
# alongside a TypeScript 0.1.0 tarball). Fail loudly here so the
# workflow never gets to publish in a misaligned state.
run: |
VERSION_FILE="${{ steps.version.outputs.version }}"
VERSION_GRADLE=$(grep -E "^VERSION_NAME=" kotlin/gradle.properties | cut -d'=' -f2 | tr -d '[:space:]')
VERSION_PYTHON=$(grep -E '^version\s*=' python/pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
VERSION_NPM=$(grep -E '^\s*"version"\s*:' typescript/package.json | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
VERSION_CSHARP=$(grep -oE '<Version>[^<]+</Version>' csharp/src/Meshtastic.TAK/Meshtastic.TAK.csproj | head -1 | sed -E 's|<Version>([^<]+)</Version>|\1|')
echo "VERSION file: $VERSION_FILE"
echo "kotlin/gradle.properties: $VERSION_GRADLE"
echo "python/pyproject.toml: $VERSION_PYTHON"
echo "typescript/package.json: $VERSION_NPM"
echo "csharp/Meshtastic.TAK.csproj: $VERSION_CSHARP"
FAIL=0
for SOURCE in "VERSION:$VERSION_FILE" \
"kotlin/gradle.properties:$VERSION_GRADLE" \
"python/pyproject.toml:$VERSION_PYTHON" \
"typescript/package.json:$VERSION_NPM" \
"csharp/Meshtastic.TAK.csproj:$VERSION_CSHARP"; do
FILE="${SOURCE%%:*}"
VER="${SOURCE##*:}"
if [ "$VER" != "$VERSION_FILE" ]; then
echo "::error::Version mismatch — $FILE is '$VER' but VERSION file is '$VERSION_FILE'"
FAIL=1
fi
done
if [ $FAIL -ne 0 ]; then
echo "::error::Sync all five version sources before releasing."
exit 1
fi
echo "✓ All five version sources agree: $VERSION_FILE"
- name: Resolve tag state on origin
# Centralizes "does the v$VERSION tag exist on origin?" so the
# tag-push and Maven-Central-publish steps below can be idempotent.
# Run for both push:tags (where the tag is guaranteed to exist —
# it triggered the workflow) and workflow_dispatch (where the tag
# may or may not exist yet).
id: tag_state
run: |
VERSION="${{ steps.version.outputs.version }}"
# Pull the tag from origin into the local ref if it exists there.
# `|| true` because a missing tag is a normal case, not an error.
git fetch origin "refs/tags/v$VERSION:refs/tags/v$VERSION" 2>/dev/null || true
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "::notice::Tag v$VERSION already on origin — tag-push step will no-op."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
# ── Setup all runtimes ──────────────────────────────────────────────
- name: Setup JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.0.x"
- name: Install zstd (for Swift)
run: brew install zstd
- name: Install Python build tools
run: pip install build pytest protobuf zstandard
# ── Run all tests ───────────────────────────────────────────────────
- name: Test Kotlin
working-directory: kotlin
run: ./gradlew jvmTest --quiet
- name: Test Swift
working-directory: swift
run: swift test
- name: Test Python
working-directory: python
run: python -m pytest tests/ -q
- name: Test TypeScript
working-directory: typescript
run: |
rm -f package-lock.json && npm install
npx vitest run
- name: Test C#
working-directory: csharp
run: dotnet test --verbosity quiet
# ── Publish to Maven Central ─────────────────────────────────────────
- name: Push release tag to origin
# The tag is the canonical "release was attempted" marker — pushed
# AFTER tests pass (so we don't tag a broken commit) but BEFORE
# Maven Central publish (so Apple SPM consumers can always resolve
# the release even if the Maven step fails or anything else aborts).
# No-op if the tag already exists on origin (retry / push:tags trigger).
if: github.event_name == 'workflow_dispatch' && steps.tag_state.outputs.exists == 'false'
run: |
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
echo "::notice::Pushed tag v$VERSION → SPM consumers can now resolve this release."
- name: Stage signed artifacts locally
working-directory: kotlin
env:
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
run: ./gradlew publishToMavenLocal --stacktrace
- name: Collect Maven artifacts for attestation
run: |
mkdir -p maven-attestation
find ~/.m2/repository/org/meshtastic -type f \
\( -name "*.jar" -o -name "*.pom" -o -name "*.module" -o -name "*.klib" \) \
-exec cp {} maven-attestation/ \;
echo "Maven artifacts to attest:"
ls -lh maven-attestation/
- name: Attest Maven artifacts
uses: actions/attest-build-provenance@v4
with:
subject-path: maven-attestation/*
- name: Check Maven Central state
# Probe whether v$VERSION is already on Maven Central. If yes, the
# publish step below skips — Sonatype rejects re-uploads with
# "component already exists" which is a fatal workflow error
# without this guard. Lets the workflow recover safely from any
# partial-publish state (e.g. tag pushed + publish failed + retry).
id: mc_check
run: |
VERSION="${{ steps.version.outputs.version }}"
HTTP=$(curl -sS -o /dev/null -w "%{http_code}" \
"https://repo1.maven.org/maven2/org/meshtastic/takpacket-sdk-jvm/$VERSION/takpacket-sdk-jvm-$VERSION.pom")
echo "Maven Central POM probe for v$VERSION: HTTP $HTTP"
if [ "$HTTP" = "200" ]; then
echo "already_published=true" >> "$GITHUB_OUTPUT"
echo "::notice::v$VERSION already on Maven Central — skipping publish step."
else
echo "already_published=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to Maven Central
if: steps.mc_check.outputs.already_published == 'false'
working-directory: kotlin
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
run: ./gradlew publishAllPublicationsToMavenCentralRepository --stacktrace
# ── Build artifacts ─────────────────────────────────────────────────
- name: Build Kotlin JAR
working-directory: kotlin
run: ./gradlew jvmJar --quiet
- name: Build Python wheel
working-directory: python
run: python -m build
- name: Build TypeScript tarball
working-directory: typescript
run: |
npm run build
npm pack
- name: Build C# NuGet package
working-directory: csharp
run: dotnet pack src/Meshtastic.TAK -c Release --verbosity quiet
- name: Build Swift source zip
run: |
zip -rq meshtastic-tak-swift-${{ steps.version.outputs.version }}.zip swift/ \
-x "swift/.build/*" "swift/.swiftpm/*"
# ── Collect artifacts ───────────────────────────────────────────────
- name: Collect release artifacts
run: |
mkdir -p release-artifacts
VERSION="${{ steps.version.outputs.version }}"
# Kotlin JAR — select only the main artifact, not -sources or -javadoc
cp "kotlin/build/libs/takpacket-sdk-jvm-${VERSION}.jar" "release-artifacts/meshtastic-tak-${VERSION}.jar"
# Python wheel + sdist
cp python/dist/*.whl release-artifacts/
cp python/dist/*.tar.gz release-artifacts/
# TypeScript tarball
cp typescript/*.tgz release-artifacts/
# C# NuGet
cp csharp/src/Meshtastic.TAK/bin/Release/*.nupkg release-artifacts/
# Swift zip
cp meshtastic-tak-swift-*.zip release-artifacts/
echo "Release artifacts:"
ls -lh release-artifacts/
# ── Create release ──────────────────────────────────────────────────
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ steps.version.outputs.version }}
name: v${{ steps.version.outputs.version }}
body: |
## TAKPacket-SDK v${{ steps.version.outputs.version }}
Cross-platform CoT XML to TAKPacketV2 conversion and zstd dictionary compression for Meshtastic.
### Artifacts
| Platform | File | Usage |
|----------|------|-------|
| **Kotlin** | `.jar` | `implementation("org.meshtastic:takpacket-sdk:${{ steps.version.outputs.version }}")` via Maven Central |
| **Swift** | `.zip` | Add via SPM: point to this repo tag `v${{ steps.version.outputs.version }}` |
| **Python** | `.whl` | `pip install meshtastic_tak-*.whl` |
| **TypeScript** | `.tgz` | `npm install meshtastic-takpacket-sdk-*.tgz` |
| **C#** | `.nupkg` | `dotnet add package Meshtastic.TAK` (or install local nupkg) |
files: release-artifacts/*
generate_release_notes: true
# ── Verify release consistency ──────────────────────────────────────
#
# Backstop against the failure mode that motivated this hardening: a
# successful Maven Central publish whose v$VERSION git tag never made
# it to origin (or vice versa), leaving Apple SPM consumers stuck on
# the previous release while Kotlin/Android Gradle consumers see the
# new one. Both halves MUST be present — fail the workflow loudly if
# either is missing so we never silently ship a partial release.
- name: Verify release consistency
run: |
VERSION="${{ steps.version.outputs.version }}"
FAIL=0
echo "=== Tag check ==="
# Re-fetch in case the tag landed via softprops/action-gh-release@v3.
git fetch origin "refs/tags/v$VERSION:refs/tags/v$VERSION" 2>/dev/null || true
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
SHA=$(git rev-parse "v$VERSION")
echo "✓ Tag v$VERSION on origin (points at $SHA)"
else
echo "✗ Tag v$VERSION MISSING on origin — Apple SPM consumers will not resolve this release"
FAIL=1
fi
echo ""
echo "=== Maven Central check (with retry — Sonatype→repo1 sync lag is normal) ==="
# By the time this step runs, the "Publish to Maven Central" step above
# ALREADY succeeded (it's an earlier step in this same job; a failed
# Gradle publish fails the job before we get here). So a 404 below means
# the deployment is still propagating from the Sonatype Central Portal
# to repo1.maven.org — NOT that the publish failed. With
# automaticRelease=true that propagation is guaranteed; it just routinely
# takes 10–25 min (occasionally more). So we poll generously and, on
# timeout, emit a WARNING rather than fail the release red — a false
# "PARTIAL" here (v0.5.0 synced at ~14 min, past the old 10-min window)
# blocks downstream consumers from a release that is actually complete.
# A genuinely partial release (publish failed, or tag missing) is caught
# by the publish step itself / the tag check above.
HTTP=000
for i in $(seq 1 25); do
HTTP=$(curl -sS -o /dev/null -w "%{http_code}" \
"https://repo1.maven.org/maven2/org/meshtastic/takpacket-sdk-jvm/$VERSION/takpacket-sdk-jvm-$VERSION.pom")
if [ "$HTTP" = "200" ]; then
echo "✓ Maven Central POM for v$VERSION reachable (attempt $i)"
break
fi
echo " attempt $i/25: HTTP $HTTP — retrying in 60s"
sleep 60
done
if [ "$HTTP" != "200" ]; then
echo "::warning::Maven Central POM for v$VERSION not yet synced after 25 min. The publish step succeeded and the tag is on origin, so this is Sonatype→repo1.maven.org propagation lag, not a failed publish. Verify https://repo1.maven.org/maven2/org/meshtastic/takpacket-sdk-jvm/$VERSION/ resolves within ~30–60 min before Gradle/Android consumers bump; if it never does, check the Sonatype Central Portal deployment status."
fi
# Only a MISSING TAG is a hard partial-release failure (Apple SPM can't
# resolve it, and tags are pushed before publish so a missing tag means
# something is badly wrong). Maven sync lag is a warning (see above).
if [ $FAIL -ne 0 ]; then
echo "::error::Release v$VERSION is PARTIAL — the git tag is missing on origin, so Apple SPM consumers cannot resolve it. Investigate before downstream consumers (Meshtastic-Android, Meshtastic-Apple) bump."
exit 1
fi
echo ""
if [ "$HTTP" = "200" ]; then
echo "✓ Release v$VERSION verified end-to-end: tag on origin + POM on Maven Central"
else
echo "✓ Release v$VERSION: tag on origin + GitHub release published. Maven Central sync still pending (see warning) — expected to resolve shortly."
fi