music-assistant-server

17.4 KBYML
release.yml
17.4 KB541 lines • yaml
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