/
/
/
1name: Create Release
2
3on:
4 workflow_dispatch:
5 inputs:
6 version:
7 description: "Version number (e.g., 1.2.3, 1.2.3b1, or 1.2.3.dev1)"
8 required: true
9 type: string
10 channel:
11 description: "Release channel"
12 required: true
13 type: choice
14 options:
15 - stable
16 - beta
17 - nightly
18 important_notes:
19 description: "Important notes (breaking changes, critical info, etc.)"
20 required: false
21 workflow_call:
22 inputs:
23 version:
24 description: "Version number (e.g., 1.2.3, 1.2.3b1, or 1.2.3.dev1)"
25 required: true
26 type: string
27 channel:
28 description: "Release channel"
29 required: true
30 type: string
31 important_notes:
32 description: "Important notes (breaking changes, critical info, etc.)"
33 required: false
34 type: string
35 secrets:
36 PYPI_TOKEN:
37 required: true
38 PRIVILEGED_GITHUB_TOKEN:
39 required: true
40
41env:
42 PYTHON_VERSION: "3.12"
43 BASE_IMAGE_VERSION_STABLE: "1.4.11"
44 BASE_IMAGE_VERSION_BETA: "1.4.11"
45 BASE_IMAGE_VERSION_NIGHTLY: "1.4.11"
46
47permissions:
48 contents: read
49
50jobs:
51 determine-branch:
52 name: Determine release branch
53 runs-on: ubuntu-latest
54 outputs:
55 branch: ${{ steps.branch.outputs.branch }}
56 steps:
57 - name: Determine branch to use
58 id: branch
59 run: |
60 CHANNEL="${{ inputs.channel }}"
61
62 if [ "$CHANNEL" = "stable" ]; then
63 echo "branch=stable" >> $GITHUB_OUTPUT
64 echo "Using stable branch for stable release"
65 else
66 echo "branch=dev" >> $GITHUB_OUTPUT
67 echo "Using dev branch for $CHANNEL release"
68 fi
69
70 preflight-checks:
71 name: Run tests and linting before release
72 needs: determine-branch
73 uses: ./.github/workflows/test.yml
74 with:
75 ref: ${{ needs.determine-branch.outputs.branch }}
76
77 validate-and-build:
78 name: Validate version and build Python artifact
79 runs-on: ubuntu-latest
80 needs: [determine-branch, preflight-checks]
81 outputs:
82 version: ${{ inputs.version }}
83 is_prerelease: ${{ steps.validate.outputs.is_prerelease }}
84 base_image_version: ${{ steps.validate.outputs.base_image_version }}
85 branch: ${{ needs.determine-branch.outputs.branch }}
86 steps:
87 - uses: actions/checkout@v6
88 with:
89 ref: ${{ needs.determine-branch.outputs.branch }}
90 fetch-depth: 0
91
92 - name: Validate version number format
93 id: validate
94 run: |
95 VERSION="${{ inputs.version }}"
96 CHANNEL="${{ inputs.channel }}"
97
98 # Regex patterns for each channel
99 STABLE_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+$'
100 BETA_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$'
101 NIGHTLY_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$'
102
103 # Validate version format matches channel
104 case "$CHANNEL" in
105 stable)
106 if ! [[ "$VERSION" =~ $STABLE_PATTERN ]]; then
107 echo "Error: Stable channel requires version format: X.Y.Z (e.g., 1.2.3)"
108 exit 1
109 fi
110 echo "is_prerelease=false" >> $GITHUB_OUTPUT
111 echo "base_image_version=${{ env.BASE_IMAGE_VERSION_STABLE }}" >> $GITHUB_OUTPUT
112 ;;
113 beta)
114 if ! [[ "$VERSION" =~ $BETA_PATTERN ]]; then
115 echo "Error: Beta channel requires version format: X.Y.ZbN (e.g., 1.2.3b1)"
116 exit 1
117 fi
118 echo "is_prerelease=true" >> $GITHUB_OUTPUT
119 echo "base_image_version=${{ env.BASE_IMAGE_VERSION_BETA }}" >> $GITHUB_OUTPUT
120 ;;
121 nightly)
122 if ! [[ "$VERSION" =~ $NIGHTLY_PATTERN ]]; then
123 echo "Error: Nightly channel requires version format: X.Y.Z.devN (e.g., 1.2.3.dev1)"
124 exit 1
125 fi
126 echo "is_prerelease=true" >> $GITHUB_OUTPUT
127 echo "base_image_version=${{ env.BASE_IMAGE_VERSION_NIGHTLY }}" >> $GITHUB_OUTPUT
128 ;;
129 *)
130 echo "Error: Invalid channel: $CHANNEL"
131 exit 1
132 ;;
133 esac
134
135 echo "â
Version $VERSION is valid for $CHANNEL channel"
136
137 - name: Set up Python ${{ env.PYTHON_VERSION }}
138 uses: actions/[email protected]
139 with:
140 python-version: ${{ env.PYTHON_VERSION }}
141
142 - name: Install build dependencies
143 run: >-
144 pip install build tomli tomli-w
145
146 - name: Update version in pyproject.toml
147 shell: python
148 run: |-
149 import tomli
150 import tomli_w
151
152 with open("pyproject.toml", "rb") as f:
153 pyproject = tomli.load(f)
154
155 pyproject["project"]["version"] = "${{ inputs.version }}"
156
157 with open("pyproject.toml", "wb") as f:
158 tomli_w.dump(pyproject, f)
159
160 print(f"â
Updated pyproject.toml version to ${{ inputs.version }}")
161
162 - name: Build python package
163 run: >-
164 python3 -m build
165
166 - name: Upload distributions
167 uses: actions/upload-artifact@v6
168 with:
169 name: release-dists
170 path: dist/
171
172 create-release:
173 name: Create GitHub Release with Release Drafter
174 runs-on: ubuntu-latest
175 needs: validate-and-build
176 permissions:
177 contents: write
178 pull-requests: read
179 outputs:
180 release_id: ${{ steps.create_release.outputs.id }}
181 upload_url: ${{ steps.create_release.outputs.upload_url }}
182 steps:
183 - uses: actions/checkout@v6
184 with:
185 ref: ${{ needs.validate-and-build.outputs.branch }}
186 fetch-depth: 0
187
188 - name: Download distributions
189 uses: actions/download-artifact@v7
190 with:
191 name: release-dists
192 path: dist/
193
194 - name: Determine previous version tag
195 id: prev_version
196 run: |
197 CHANNEL="${{ inputs.channel }}"
198
199 # Find the previous tag of this channel
200 case "$CHANNEL" in
201 stable)
202 PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
203 ;;
204 beta)
205 PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$' | head -n 1)
206 ;;
207 nightly)
208 PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$' | head -n 1)
209 ;;
210 esac
211
212 if [ -z "$PREV_TAG" ]; then
213 echo "â ï¸ No previous $CHANNEL release found"
214 echo "prev_tag=" >> $GITHUB_OUTPUT
215 echo "has_prev_tag=false" >> $GITHUB_OUTPUT
216 else
217 echo "â
Previous $CHANNEL release: $PREV_TAG"
218 echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
219 echo "has_prev_tag=true" >> $GITHUB_OUTPUT
220 fi
221
222 - name: Generate complete release notes
223 id: generate_notes
224 uses: ./.github/actions/generate-release-notes
225 with:
226 version: ${{ inputs.version }}
227 previous-tag: ${{ steps.prev_version.outputs.prev_tag }}
228 branch: ${{ needs.validate-and-build.outputs.branch }}
229 channel: ${{ inputs.channel }}
230 github-token: ${{ secrets.GITHUB_TOKEN }}
231 important-notes: ${{ inputs.important_notes }}
232
233 - name: Format release title
234 id: format_title
235 run: |
236 VERSION="${{ inputs.version }}"
237 CHANNEL="${{ inputs.channel }}"
238
239 if [[ "$CHANNEL" == "nightly" ]]; then
240 # Extract base version and date from X.Y.Z.devYYYYMMDD
241 if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev([0-9]+)$ ]]; then
242 BASE="${BASH_REMATCH[1]}"
243 DATE="${BASH_REMATCH[2]}"
244 TITLE="$BASE NIGHTLY $DATE"
245 else
246 TITLE="$VERSION NIGHTLY"
247 fi
248 elif [[ "$CHANNEL" == "beta" ]]; then
249 # Extract base version and beta number from X.Y.ZbN
250 if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)b([0-9]+)$ ]]; then
251 BASE="${BASH_REMATCH[1]}"
252 BETA_NUM="${BASH_REMATCH[2]}"
253 TITLE="$BASE BETA $BETA_NUM"
254 else
255 TITLE="$VERSION BETA"
256 fi
257 else
258 # Stable release - just use the version
259 TITLE="$VERSION"
260 fi
261
262 echo "title=$TITLE" >> $GITHUB_OUTPUT
263 echo "Release title: $TITLE"
264
265 - name: Create GitHub Release
266 id: create_release
267 uses: softprops/action-gh-release@v2
268 with:
269 tag_name: ${{ inputs.version }}
270 name: ${{ steps.format_title.outputs.title }}
271 body: ${{ steps.generate_notes.outputs.release-notes }}
272 prerelease: ${{ needs.validate-and-build.outputs.is_prerelease }}
273 target_commitish: ${{ needs.validate-and-build.outputs.branch }}
274 files: dist/*
275 env:
276 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
277
278 - name: Output release info
279 run: |
280 echo "â
Created release ${{ inputs.version }}"
281 echo "Release URL: ${{ steps.create_release.outputs.url }}"
282
283 pypi-publish:
284 name: Publish release to PyPI (stable releases only)
285 runs-on: ubuntu-latest
286 needs: create-release
287 if: inputs.channel == 'stable'
288 permissions:
289 id-token: write
290 steps:
291 - name: Download distributions
292 uses: actions/download-artifact@v7
293 with:
294 name: release-dists
295 path: dist/
296
297 - name: Publish to PyPI
298 uses: pypa/gh-action-pypi-publish@release/v1
299
300 build-and-push-container-image:
301 name: Build and push Music Assistant Server container to ghcr.io
302 runs-on: ubuntu-latest
303 permissions:
304 packages: write
305 needs:
306 - validate-and-build
307 - create-release
308 steps:
309 - uses: actions/checkout@v6
310 with:
311 ref: ${{ needs.validate-and-build.outputs.branch }}
312
313 - name: Download distributions
314 uses: actions/download-artifact@v7
315 with:
316 name: release-dists
317 path: dist/
318
319 - name: Log in to the GitHub container registry
320 uses: docker/[email protected]
321 with:
322 registry: ghcr.io
323 username: ${{ github.repository_owner }}
324 password: ${{ secrets.GITHUB_TOKEN }}
325
326 - name: Set up Docker Buildx
327 uses: docker/[email protected]
328
329 - name: Generate Docker tags
330 id: tags
331 run: |
332 VERSION="${{ inputs.version }}"
333 CHANNEL="${{ inputs.channel }}"
334
335 # Extract version components
336 if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
337 MAJOR="${BASH_REMATCH[1]}"
338 MINOR="${BASH_REMATCH[2]}"
339 PATCH="${BASH_REMATCH[3]}"
340 fi
341
342 TAGS="ghcr.io/${{ github.repository_owner }}/server:$VERSION"
343
344 case "$CHANNEL" in
345 stable)
346 # For stable: add major, minor, major.minor, stable, and latest tags
347 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR.$MINOR.$PATCH"
348 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR.$MINOR"
349 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR"
350 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:stable"
351 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:latest"
352 ;;
353 beta)
354 # For beta: add beta tag
355 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:beta"
356 ;;
357 nightly)
358 # For nightly: add nightly tag
359 TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:nightly"
360 ;;
361 esac
362
363 echo "tags=$TAGS" >> $GITHUB_OUTPUT
364 echo "Docker tags: $TAGS"
365
366 - name: Build and push Docker image
367 uses: docker/[email protected]
368 with:
369 context: .
370 platforms: linux/amd64,linux/arm64
371 file: Dockerfile
372 tags: ${{ steps.tags.outputs.tags }}
373 push: true
374 build-args: |
375 MASS_VERSION=${{ inputs.version }}
376 BASE_IMAGE_VERSION=${{ needs.validate-and-build.outputs.base_image_version }}
377
378 update-addon-repository:
379 name: Update Home Assistant Add-on Repository
380 runs-on: ubuntu-latest
381 needs:
382 - validate-and-build
383 - create-release
384 - build-and-push-container-image
385 steps:
386 - name: Determine addon folder
387 id: addon_folder
388 run: |
389 CHANNEL="${{ inputs.channel }}"
390
391 case "$CHANNEL" in
392 stable)
393 echo "folder=music_assistant" >> $GITHUB_OUTPUT
394 echo "Updating stable add-on"
395 ;;
396 beta)
397 echo "folder=music_assistant_beta" >> $GITHUB_OUTPUT
398 echo "Updating beta add-on"
399 ;;
400 nightly)
401 echo "folder=music_assistant_nightly" >> $GITHUB_OUTPUT
402 echo "Updating nightly add-on"
403 ;;
404 esac
405
406 - name: Checkout add-on repository
407 uses: actions/checkout@v6
408 with:
409 repository: music-assistant/home-assistant-addon
410 token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }}
411 path: addon-repo
412
413 - name: Get release notes
414 id: get_notes
415 env:
416 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
417 run: |
418 cd addon-repo
419
420 # Get the release body from the server repository
421 RELEASE_NOTES=$(gh release view "${{ inputs.version }}" --repo music-assistant/server --json body --jq .body)
422
423 # Save to file for processing
424 echo "$RELEASE_NOTES" > /tmp/release_notes.md
425
426 - name: Update config.yaml version
427 run: |
428 ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}"
429 VERSION="${{ inputs.version }}"
430
431 cd addon-repo/$ADDON_FOLDER
432
433 # Update version in config.yaml using sed
434 sed -i "s/^version: .*/version: $VERSION/" config.yaml
435
436 echo "â
Updated config.yaml to version $VERSION"
437
438 - name: Update CHANGELOG.md
439 run: |
440 ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}"
441 VERSION="${{ inputs.version }}"
442
443 cd addon-repo/$ADDON_FOLDER
444
445 # Get current date
446 RELEASE_DATE=$(date +"%d.%m.%Y")
447
448 # Read the new release notes
449 NEW_NOTES=$(cat /tmp/release_notes.md)
450
451 # Create new changelog entry
452 {
453 echo "# [$VERSION] - $RELEASE_DATE"
454 echo ""
455 echo "$NEW_NOTES"
456 echo ""
457 echo ""
458 } > /tmp/new_changelog.md
459
460 # If CHANGELOG.md exists, keep only the last 2 versions
461 if [ -f CHANGELOG.md ]; then
462 # Extract headers to count versions
463 VERSION_COUNT=$(grep -c "^# \[" CHANGELOG.md || echo "0")
464
465 if [ "$VERSION_COUNT" -ge 2 ]; then
466 # Keep only first 2 versions (extract everything before the 3rd version header)
467 awk '/^# \[/{i++}i==3{exit}1' CHANGELOG.md > /tmp/old_changelog.md
468
469 # Combine new entry with trimmed old changelog
470 cat /tmp/new_changelog.md /tmp/old_changelog.md > CHANGELOG.md
471 else
472 # Less than 2 versions, just prepend
473 cat /tmp/new_changelog.md CHANGELOG.md > /tmp/combined_changelog.md
474 mv /tmp/combined_changelog.md CHANGELOG.md
475 fi
476 else
477 # No existing changelog, create new
478 mv /tmp/new_changelog.md CHANGELOG.md
479 fi
480
481 echo "â
Updated CHANGELOG.md with new release"
482
483 - name: Commit and push changes
484 run: |
485 ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}"
486 VERSION="${{ inputs.version }}"
487 CHANNEL="${{ inputs.channel }}"
488
489 cd addon-repo
490
491 git config user.name "github-actions[bot]"
492 git config user.email "github-actions[bot]@users.noreply.github.com"
493
494 git add "$ADDON_FOLDER/config.yaml" "$ADDON_FOLDER/CHANGELOG.md"
495
496 git commit -m "ð¤ Bump $CHANNEL add-on to version $VERSION" || {
497 echo "No changes to commit"
498 exit 0
499 }
500
501 git push
502
503 echo "â
Successfully updated add-on repository"
504
505 update-remote-app:
506 name: Update app.music-assistant.io frontend
507 runs-on: ubuntu-latest
508 needs:
509 - validate-and-build
510 - create-release
511 steps:
512 - uses: actions/checkout@v6
513 with:
514 ref: ${{ needs.validate-and-build.outputs.branch }}
515
516 - name: Extract frontend version from pyproject.toml
517 id: frontend
518 run: |
519 # Extract frontend version from pyproject.toml
520 FRONTEND_VERSION=$(grep 'music-assistant-frontend==' pyproject.toml | sed 's/.*==\([^"]*\).*/\1/')
521 echo "version=${FRONTEND_VERSION}" >> $GITHUB_OUTPUT
522 echo "Frontend version: ${FRONTEND_VERSION}"
523
524 - name: Trigger app.music-assistant.io update
525 uses: peter-evans/repository-dispatch@v4
526 with:
527 token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }}
528 repository: music-assistant/app.music-assistant.io
529 event-type: frontend-update
530 client-payload: |
531 {
532 "channel": "${{ inputs.channel }}",
533 "frontend_version": "${{ steps.frontend.outputs.version }}"
534 }
535
536 - name: Output update info
537 run: |
538 echo "â
Triggered app.music-assistant.io update"
539 echo "Channel: ${{ inputs.channel }}"
540 echo "Frontend version: ${{ steps.frontend.outputs.version }}"
541