Create Release #360
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: Create Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version number (e.g., 1.2.3, 1.2.3b1, or 1.2.3.dev1)" | |
| required: true | |
| type: string | |
| channel: | |
| description: "Release channel" | |
| required: true | |
| type: choice | |
| options: | |
| - stable | |
| - beta | |
| - nightly | |
| workflow_call: | |
| inputs: | |
| version: | |
| description: "Version number (e.g., 1.2.3, 1.2.3b1, or 1.2.3.dev1)" | |
| required: true | |
| type: string | |
| channel: | |
| description: "Release channel" | |
| required: true | |
| type: string | |
| secrets: | |
| PYPI_TOKEN: | |
| required: true | |
| PRIVILEGED_GITHUB_TOKEN: | |
| required: true | |
| env: | |
| PYTHON_VERSION: "3.12" | |
| BASE_IMAGE_VERSION_STABLE: "1.3.1" | |
| BASE_IMAGE_VERSION_BETA: "1.4.4" | |
| BASE_IMAGE_VERSION_NIGHTLY: "1.4.4" | |
| jobs: | |
| preflight-checks: | |
| name: Run tests and linting before release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| branch: ${{ steps.branch.outputs.branch }} | |
| steps: | |
| - name: Determine branch to use | |
| id: branch | |
| run: | | |
| CHANNEL="${{ inputs.channel }}" | |
| if [ "$CHANNEL" = "stable" ]; then | |
| echo "branch=stable" >> $GITHUB_OUTPUT | |
| echo "Using stable branch for stable release" | |
| else | |
| echo "branch=dev" >> $GITHUB_OUTPUT | |
| echo "Using dev branch for $CHANNEL release" | |
| fi | |
| - name: Trigger test workflow | |
| uses: convictional/[email protected] | |
| with: | |
| owner: ${{ github.repository_owner }} | |
| repo: server | |
| github_token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }} | |
| workflow_file_name: test.yml | |
| ref: ${{ steps.branch.outputs.branch }} | |
| wait_interval: 10 | |
| propagate_failure: true | |
| trigger_workflow: true | |
| wait_workflow: true | |
| validate-and-build: | |
| name: Validate version and build Python artifact | |
| runs-on: ubuntu-latest | |
| needs: preflight-checks | |
| outputs: | |
| version: ${{ inputs.version }} | |
| is_prerelease: ${{ steps.validate.outputs.is_prerelease }} | |
| base_image_version: ${{ steps.validate.outputs.base_image_version }} | |
| branch: ${{ needs.preflight-checks.outputs.branch }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.preflight-checks.outputs.branch }} | |
| fetch-depth: 0 | |
| - name: Validate version number format | |
| id: validate | |
| run: | | |
| VERSION="${{ inputs.version }}" | |
| CHANNEL="${{ inputs.channel }}" | |
| # Regex patterns for each channel | |
| STABLE_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+$' | |
| BETA_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$' | |
| NIGHTLY_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$' | |
| # Validate version format matches channel | |
| case "$CHANNEL" in | |
| stable) | |
| if ! [[ "$VERSION" =~ $STABLE_PATTERN ]]; then | |
| echo "Error: Stable channel requires version format: X.Y.Z (e.g., 1.2.3)" | |
| exit 1 | |
| fi | |
| echo "is_prerelease=false" >> $GITHUB_OUTPUT | |
| echo "base_image_version=${{ env.BASE_IMAGE_VERSION_STABLE }}" >> $GITHUB_OUTPUT | |
| ;; | |
| beta) | |
| if ! [[ "$VERSION" =~ $BETA_PATTERN ]]; then | |
| echo "Error: Beta channel requires version format: X.Y.ZbN (e.g., 1.2.3b1)" | |
| exit 1 | |
| fi | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "base_image_version=${{ env.BASE_IMAGE_VERSION_BETA }}" >> $GITHUB_OUTPUT | |
| ;; | |
| nightly) | |
| if ! [[ "$VERSION" =~ $NIGHTLY_PATTERN ]]; then | |
| echo "Error: Nightly channel requires version format: X.Y.Z.devN (e.g., 1.2.3.dev1)" | |
| exit 1 | |
| fi | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "base_image_version=${{ env.BASE_IMAGE_VERSION_NIGHTLY }}" >> $GITHUB_OUTPUT | |
| ;; | |
| *) | |
| echo "Error: Invalid channel: $CHANNEL" | |
| exit 1 | |
| ;; | |
| esac | |
| echo "✅ Version $VERSION is valid for $CHANNEL channel" | |
| - name: Set up Python ${{ env.PYTHON_VERSION }} | |
| uses: actions/[email protected] | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install build dependencies | |
| run: >- | |
| pip install build tomli tomli-w | |
| - name: Update version in pyproject.toml | |
| shell: python | |
| run: |- | |
| import tomli | |
| import tomli_w | |
| with open("pyproject.toml", "rb") as f: | |
| pyproject = tomli.load(f) | |
| pyproject["project"]["version"] = "${{ inputs.version }}" | |
| with open("pyproject.toml", "wb") as f: | |
| tomli_w.dump(pyproject, f) | |
| print(f"✅ Updated pyproject.toml version to ${{ inputs.version }}") | |
| - name: Build python package | |
| run: >- | |
| python3 -m build | |
| - name: Upload distributions | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| create-release: | |
| name: Create GitHub Release with Release Drafter | |
| runs-on: ubuntu-latest | |
| needs: validate-and-build | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| outputs: | |
| release_id: ${{ steps.create_release.outputs.id }} | |
| upload_url: ${{ steps.create_release.outputs.upload_url }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.validate-and-build.outputs.branch }} | |
| fetch-depth: 0 | |
| - name: Download distributions | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| - name: Determine previous version tag | |
| id: prev_version | |
| run: | | |
| CHANNEL="${{ inputs.channel }}" | |
| # Find the previous tag of this channel | |
| case "$CHANNEL" in | |
| stable) | |
| PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) | |
| ;; | |
| beta) | |
| PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$' | head -n 1) | |
| ;; | |
| nightly) | |
| PREV_TAG=$(git tag --sort=-version:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$' | head -n 1) | |
| ;; | |
| esac | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "⚠️ No previous $CHANNEL release found" | |
| echo "prev_tag=" >> $GITHUB_OUTPUT | |
| echo "has_prev_tag=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "✅ Previous $CHANNEL release: $PREV_TAG" | |
| echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT | |
| echo "has_prev_tag=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Generate complete release notes | |
| id: generate_notes | |
| uses: ./.github/actions/generate-release-notes | |
| with: | |
| version: ${{ inputs.version }} | |
| previous-tag: ${{ steps.prev_version.outputs.prev_tag }} | |
| branch: ${{ needs.validate-and-build.outputs.branch }} | |
| channel: ${{ inputs.channel }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Format release title | |
| id: format_title | |
| run: | | |
| VERSION="${{ inputs.version }}" | |
| CHANNEL="${{ inputs.channel }}" | |
| if [[ "$CHANNEL" == "nightly" ]]; then | |
| # Extract base version and date from X.Y.Z.devYYYYMMDD | |
| if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev([0-9]+)$ ]]; then | |
| BASE="${BASH_REMATCH[1]}" | |
| DATE="${BASH_REMATCH[2]}" | |
| TITLE="$BASE NIGHTLY $DATE" | |
| else | |
| TITLE="$VERSION NIGHTLY" | |
| fi | |
| elif [[ "$CHANNEL" == "beta" ]]; then | |
| # Extract base version and beta number from X.Y.ZbN | |
| if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)b([0-9]+)$ ]]; then | |
| BASE="${BASH_REMATCH[1]}" | |
| BETA_NUM="${BASH_REMATCH[2]}" | |
| TITLE="$BASE BETA $BETA_NUM" | |
| else | |
| TITLE="$VERSION BETA" | |
| fi | |
| else | |
| # Stable release - just use the version | |
| TITLE="$VERSION" | |
| fi | |
| echo "title=$TITLE" >> $GITHUB_OUTPUT | |
| echo "Release title: $TITLE" | |
| - name: Create GitHub Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ inputs.version }} | |
| name: ${{ steps.format_title.outputs.title }} | |
| body: ${{ steps.generate_notes.outputs.release-notes }} | |
| prerelease: ${{ needs.validate-and-build.outputs.is_prerelease }} | |
| target_commitish: ${{ needs.validate-and-build.outputs.branch }} | |
| files: dist/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Output release info | |
| run: | | |
| echo "✅ Created release ${{ inputs.version }}" | |
| echo "Release URL: ${{ steps.create_release.outputs.url }}" | |
| pypi-publish: | |
| name: Publish release to PyPI (stable releases only) | |
| runs-on: ubuntu-latest | |
| needs: create-release | |
| if: inputs.channel == 'stable' | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download distributions | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| build-and-push-container-image: | |
| name: Build and push Music Assistant Server container to ghcr.io | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| needs: | |
| - validate-and-build | |
| - create-release | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.validate-and-build.outputs.branch }} | |
| - name: Download distributions | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| - name: Log in to the GitHub container registry | |
| uses: docker/[email protected] | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/[email protected] | |
| - name: Generate Docker tags | |
| id: tags | |
| run: | | |
| VERSION="${{ inputs.version }}" | |
| CHANNEL="${{ inputs.channel }}" | |
| # Extract version components | |
| if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then | |
| MAJOR="${BASH_REMATCH[1]}" | |
| MINOR="${BASH_REMATCH[2]}" | |
| PATCH="${BASH_REMATCH[3]}" | |
| fi | |
| TAGS="ghcr.io/${{ github.repository_owner }}/server:$VERSION" | |
| case "$CHANNEL" in | |
| stable) | |
| # For stable: add major, minor, major.minor, stable, and latest tags | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR.$MINOR.$PATCH" | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR.$MINOR" | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:$MAJOR" | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:stable" | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:latest" | |
| ;; | |
| beta) | |
| # For beta: add beta tag | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:beta" | |
| ;; | |
| nightly) | |
| # For nightly: add nightly tag | |
| TAGS="$TAGS,ghcr.io/${{ github.repository_owner }}/server:nightly" | |
| ;; | |
| esac | |
| echo "tags=$TAGS" >> $GITHUB_OUTPUT | |
| echo "Docker tags: $TAGS" | |
| - name: Build and push Docker image | |
| uses: docker/[email protected] | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| file: Dockerfile | |
| tags: ${{ steps.tags.outputs.tags }} | |
| push: true | |
| build-args: | | |
| MASS_VERSION=${{ inputs.version }} | |
| BASE_IMAGE_VERSION=${{ needs.validate-and-build.outputs.base_image_version }} | |
| update-addon-repository: | |
| name: Update Home Assistant Add-on Repository | |
| runs-on: ubuntu-latest | |
| needs: | |
| - validate-and-build | |
| - create-release | |
| - build-and-push-container-image | |
| steps: | |
| - name: Determine addon folder | |
| id: addon_folder | |
| run: | | |
| CHANNEL="${{ inputs.channel }}" | |
| case "$CHANNEL" in | |
| stable) | |
| echo "folder=music_assistant" >> $GITHUB_OUTPUT | |
| echo "Updating stable add-on" | |
| ;; | |
| beta) | |
| echo "folder=music_assistant_beta" >> $GITHUB_OUTPUT | |
| echo "Updating beta add-on" | |
| ;; | |
| nightly) | |
| echo "folder=music_assistant_nightly" >> $GITHUB_OUTPUT | |
| echo "Updating nightly add-on" | |
| ;; | |
| esac | |
| - name: Checkout add-on repository | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: music-assistant/home-assistant-addon | |
| token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }} | |
| path: addon-repo | |
| - name: Get release notes | |
| id: get_notes | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| cd addon-repo | |
| # Get the release body from the server repository | |
| RELEASE_NOTES=$(gh release view "${{ inputs.version }}" --repo music-assistant/server --json body --jq .body) | |
| # Save to file for processing | |
| echo "$RELEASE_NOTES" > /tmp/release_notes.md | |
| - name: Update config.yaml version | |
| run: | | |
| ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}" | |
| VERSION="${{ inputs.version }}" | |
| cd addon-repo/$ADDON_FOLDER | |
| # Update version in config.yaml using sed | |
| sed -i "s/^version: .*/version: $VERSION/" config.yaml | |
| echo "✅ Updated config.yaml to version $VERSION" | |
| - name: Update CHANGELOG.md | |
| run: | | |
| ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}" | |
| VERSION="${{ inputs.version }}" | |
| cd addon-repo/$ADDON_FOLDER | |
| # Get current date | |
| RELEASE_DATE=$(date +"%d.%m.%Y") | |
| # Read the new release notes | |
| NEW_NOTES=$(cat /tmp/release_notes.md) | |
| # Create new changelog entry | |
| { | |
| echo "# [$VERSION] - $RELEASE_DATE" | |
| echo "" | |
| echo "$NEW_NOTES" | |
| echo "" | |
| echo "" | |
| } > /tmp/new_changelog.md | |
| # If CHANGELOG.md exists, keep only the last 2 versions | |
| if [ -f CHANGELOG.md ]; then | |
| # Extract headers to count versions | |
| VERSION_COUNT=$(grep -c "^# \[" CHANGELOG.md || echo "0") | |
| if [ "$VERSION_COUNT" -ge 2 ]; then | |
| # Keep only first 2 versions (extract everything before the 3rd version header) | |
| awk '/^# \[/{i++}i==3{exit}1' CHANGELOG.md > /tmp/old_changelog.md | |
| # Combine new entry with trimmed old changelog | |
| cat /tmp/new_changelog.md /tmp/old_changelog.md > CHANGELOG.md | |
| else | |
| # Less than 2 versions, just prepend | |
| cat /tmp/new_changelog.md CHANGELOG.md > /tmp/combined_changelog.md | |
| mv /tmp/combined_changelog.md CHANGELOG.md | |
| fi | |
| else | |
| # No existing changelog, create new | |
| mv /tmp/new_changelog.md CHANGELOG.md | |
| fi | |
| echo "✅ Updated CHANGELOG.md with new release" | |
| - name: Commit and push changes | |
| run: | | |
| ADDON_FOLDER="${{ steps.addon_folder.outputs.folder }}" | |
| VERSION="${{ inputs.version }}" | |
| CHANNEL="${{ inputs.channel }}" | |
| cd addon-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add "$ADDON_FOLDER/config.yaml" "$ADDON_FOLDER/CHANGELOG.md" | |
| git commit -m "🤖 Bump $CHANNEL add-on to version $VERSION" || { | |
| echo "No changes to commit" | |
| exit 0 | |
| } | |
| git push | |
| echo "✅ Successfully updated add-on repository" |