fix(ci): fix Docker sha tag prefix for tag pushes (was generating inv… #5
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
| # ============================================================================== | ||
| # RELEASE WORKFLOW — Tag + GitHub Release pipeline | ||
| # | ||
| # Trigger: workflow_dispatch with version input | ||
| # Steps: | ||
| # 1. Pre-flight: Validate version, check CI status, verify version in files | ||
| # 2. Git Tag: vX.Y.Z on HEAD → triggers ci-cd.yml → Docker Multi-Arch Build | ||
| # 3. GitHub Release: Auto-generated changelog + Docker instructions | ||
| # 4. Trigger: build-raspi-image.yml for arm64 + armhf | ||
| # | ||
| # NOTE: Version bumps in package.json / pyproject.toml / CHANGELOG.md must be | ||
| # done in the PR BEFORE merging to main. This workflow does NOT push | ||
| # commits — it only creates a tag + release on the current HEAD. | ||
| # This is required because branch protection blocks direct pushes to main. | ||
| # ============================================================================== | ||
| name: Release | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'Release version (e.g., 1.0.0 — without v prefix)' | ||
| required: true | ||
| type: string | ||
| prerelease: | ||
| description: 'Mark as pre-release (beta/rc)' | ||
| required: false | ||
| type: boolean | ||
| default: false | ||
| skip_raspi: | ||
| description: 'Skip Raspberry Pi image builds' | ||
| required: false | ||
| type: boolean | ||
| default: false | ||
| jobs: | ||
| # ============================================================================ | ||
| # PRE-FLIGHT CHECKS | ||
| # ============================================================================ | ||
| preflight: | ||
| name: Pre-flight Checks | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Validate version format | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | ||
| echo "::error::Invalid version format: $VERSION" | ||
| echo "Expected: X.Y.Z or X.Y.Z-beta.1 (e.g., 1.0.0, 2.1.0-rc.1)" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Valid version: $VERSION" | ||
| - name: Checkout code | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Check tag does not exist | ||
| run: | | ||
| TAG="v${{ inputs.version }}" | ||
| if git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| echo "::error::Tag $TAG already exists! Delete first: git push --delete origin $TAG && git tag -d $TAG" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Tag $TAG is available" | ||
| - name: Verify version in package files | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
| echo "🔠Checking that version $VERSION is present in all package files..." | ||
| ERRORS=0 | ||
| ROOT_VER=$(grep '"version"' package.json | head -1 | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^"]*') | ||
| if [ "$ROOT_VER" != "$VERSION" ]; then | ||
| echo "::error::package.json version is '$ROOT_VER', expected '$VERSION'" | ||
| ERRORS=$((ERRORS + 1)) | ||
| else | ||
| echo " ✅ package.json → $ROOT_VER" | ||
| fi | ||
| BACKEND_VER=$(grep '^version' apps/backend/pyproject.toml | head -1 | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^"]*') | ||
| if [ "$BACKEND_VER" != "$VERSION" ]; then | ||
| echo "::error::pyproject.toml version is '$BACKEND_VER', expected '$VERSION'" | ||
| ERRORS=$((ERRORS + 1)) | ||
| else | ||
| echo " ✅ apps/backend/pyproject.toml → $BACKEND_VER" | ||
| fi | ||
| FRONTEND_VER=$(grep '"version"' apps/frontend/package.json | head -1 | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^"]*') | ||
| if [ "$FRONTEND_VER" != "$VERSION" ]; then | ||
| echo "::error::frontend/package.json version is '$FRONTEND_VER', expected '$VERSION'" | ||
| ERRORS=$((ERRORS + 1)) | ||
| else | ||
| echo " ✅ apps/frontend/package.json → $FRONTEND_VER" | ||
| fi | ||
| if [ "$ERRORS" -gt 0 ]; then | ||
| echo "" | ||
| echo "::error::Version mismatch! Bump versions in a PR before running the release workflow." | ||
| exit 1 | ||
| fi | ||
| echo "✅ All package files match version $VERSION" | ||
| - name: Check latest CI status on main | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| echo "🔠Checking CI/CD status on main..." | ||
| LAST_RUN=$(gh run list --workflow=ci-cd.yml --branch=main --limit=1 \ | ||
| --json conclusion --jq '.[0].conclusion' 2>/dev/null || echo "unknown") | ||
| echo "Last CI run: $LAST_RUN" | ||
| if [ "$LAST_RUN" = "failure" ]; then | ||
| echo "::warning::Last CI/CD run on main failed. Proceeding anyway (manual trigger)." | ||
| fi | ||
| echo "✅ Pre-flight checks passed" | ||
| # ============================================================================ | ||
| # TAG + RELEASE (no commits — branch protection safe) | ||
| # ============================================================================ | ||
| release: | ||
| name: Create Release | ||
| runs-on: ubuntu-latest | ||
| needs: preflight | ||
| permissions: | ||
| contents: write | ||
| outputs: | ||
| tag: ${{ steps.tag.outputs.tag }} | ||
| version: ${{ steps.tag.outputs.version }} | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Configure Git | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| - name: Create and push tag | ||
| id: tag | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
| TAG="v${VERSION}" | ||
| git tag -a "$TAG" -m "Release $TAG | ||
| OpenCloudTouch $TAG — Local control for Bose SoundTouch devices. | ||
| See CHANGELOG.md for details." | ||
| git push origin "$TAG" | ||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "✅ Tag $TAG created and pushed" | ||
| echo "🚀 CI/CD Pipeline will build Docker images automatically" | ||
| - name: Generate release notes | ||
| id: notes | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
| TAG="v${VERSION}" | ||
| # Get previous tag | ||
| PREV_TAG=$(git tag --sort=-v:refname | grep -v "$TAG" | head -1 || echo "") | ||
| if [ -z "$PREV_TAG" ]; then | ||
| RANGE="" | ||
| COMPARE_TEXT="🎉 **First release of OpenCloudTouch!**" | ||
| else | ||
| RANGE="${PREV_TAG}..${TAG}" | ||
| COMPARE_TEXT="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}" | ||
| fi | ||
| # Build release notes file | ||
| cat > release-notes.md << EOF | ||
| ## 🚀 OpenCloudTouch v${VERSION} | ||
| Local control for Bose® SoundTouch® devices — after the cloud shutdown. | ||
| EOF | ||
| sed -i 's/^ //' release-notes.md | ||
| # Parse conventional commits (only if we have a range) | ||
| add_section() { | ||
| local title="$1" | ||
| local pattern="$2" | ||
| local icon="$3" | ||
| local content="" | ||
| if [ -n "$RANGE" ]; then | ||
| content=$(git log $RANGE --pretty=format:"- %s (%h)" --no-merges --grep="$pattern" 2>/dev/null || echo "") | ||
| else | ||
| content=$(git log --pretty=format:"- %s (%h)" --no-merges --grep="$pattern" 2>/dev/null || echo "") | ||
| fi | ||
| if [ -n "$content" ]; then | ||
| echo "" >> release-notes.md | ||
| echo "### ${icon} ${title}" >> release-notes.md | ||
| echo "" >> release-notes.md | ||
| echo "$content" >> release-notes.md | ||
| fi | ||
| } | ||
| add_section "Features" "^feat" "✨" | ||
| add_section "Bug Fixes" "^fix" "ðŸ›" | ||
| add_section "Performance" "^perf" "âš¡" | ||
| add_section "Refactoring" "^refactor" "â™»ï¸" | ||
| add_section "Tests" "^test" "🧪" | ||
| add_section "Documentation" "^docs" "📚" | ||
| add_section "CI/CD" "^ci" "🔧" | ||
| add_section "Maintenance" "^chore\|^build" "🛠ï¸" | ||
| add_section "Style" "^style" "🎨" | ||
| # Docker instructions | ||
| cat >> release-notes.md << EOF | ||
| --- | ||
| ## 📦 Installation | ||
| ### Docker (recommended) | ||
| \`\`\`bash | ||
| # Pull the latest stable release | ||
| docker pull ghcr.io/${{ github.repository }}:stable | ||
| # Or pull this specific version | ||
| docker pull ghcr.io/${{ github.repository }}:${VERSION} | ||
| # Run the container | ||
| docker run -d \\ | ||
| --name opencloudtouch \\ | ||
| --network host \\ | ||
| -v opencloudtouch-data:/data \\ | ||
| -e OCT_DISCOVERY_ENABLED=true \\ | ||
| ghcr.io/${{ github.repository }}:${VERSION} | ||
| \`\`\` | ||
| ### Docker Compose | ||
| \`\`\`yaml | ||
| services: | ||
| opencloudtouch: | ||
| image: ghcr.io/${{ github.repository }}:${VERSION} | ||
| container_name: opencloudtouch | ||
| restart: unless-stopped | ||
| network_mode: host | ||
| volumes: | ||
| - oct-data:/data | ||
| environment: | ||
| - OCT_DISCOVERY_ENABLED=true | ||
| - OCT_LOG_LEVEL=INFO | ||
| volumes: | ||
| oct-data: | ||
| \`\`\` | ||
| ### Raspberry Pi (SD-Card Image) | ||
| Pre-built SD card images for Raspberry Pi 3/4/5 will be attached to this release shortly. | ||
| 1. Download the \`.img.xz\` file for your architecture (arm64 or armhf) | ||
| 2. Flash with [Raspberry Pi Imager](https://www.raspberrypi.com/software/) or: \`xz -d *.img.xz && sudo dd if=*.img of=/dev/sdX bs=4M status=progress\` | ||
| 3. Boot → OpenCloudTouch starts automatically on port 7777 | ||
| 4. Default credentials: \`oct\` / \`opencloudtouch\` | ||
| ### Available Docker Tags | ||
| | Tag | Description | | ||
| |-----|-------------| | ||
| | \`stable\` | Latest stable release (recommended) | | ||
| | \`${VERSION}\` | This specific version | | ||
| | \`latest\` | Latest build from main branch | | ||
| | \`$(echo $VERSION | cut -d. -f1-2)\` | Latest patch of this minor version | | ||
| ### Supported Architectures | ||
| | Architecture | Platform | Example Devices | | ||
| |-------------|----------|-----------------| | ||
| | \`amd64\` | x86_64 | Desktop, Server, NAS | | ||
| | \`arm64\` | aarch64 | Raspberry Pi 4/5, Apple Silicon | | ||
| | \`arm/v7\` | armhf | Raspberry Pi 2/3 | | ||
| --- | ||
| ## 🔗 Resources | ||
| - 📖 [Documentation (Wiki)](https://github.com/scheilch/opencloudtouch/wiki) | ||
| - 🛠[Report Issues](https://github.com/scheilch/opencloudtouch/issues) | ||
| - 📋 [Upgrade Guide](https://github.com/scheilch/opencloudtouch/blob/main/UPGRADING.md) | ||
| - 📄 [Full Changelog](https://github.com/scheilch/opencloudtouch/blob/main/CHANGELOG.md) | ||
| ${COMPARE_TEXT} | ||
| EOF | ||
| # Remove heredoc indentation | ||
| sed -i 's/^ //' release-notes.md | ||
| echo "=== Release Notes Preview ===" | ||
| head -30 release-notes.md | ||
| - name: Create GitHub Release | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| tag_name: v${{ inputs.version }} | ||
| name: "🎉 OpenCloudTouch v${{ inputs.version }}" | ||
| body_path: release-notes.md | ||
| prerelease: ${{ inputs.prerelease }} | ||
| draft: false | ||
| make_latest: ${{ !inputs.prerelease }} | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Step summary | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
| cat >> $GITHUB_STEP_SUMMARY << EOF | ||
| # 🎉 Release v${VERSION} Created! | ||
| | Step | Status | | ||
| |------|--------| | ||
| | Version check | ✅ All package files match v${VERSION} | | ||
| | Git tag | ✅ v${VERSION} | | ||
| | GitHub Release | ✅ Published | | ||
| ## 🚀 Automated Downstream Pipelines | ||
| 1. **CI/CD** → Docker multi-arch build + push to GHCR | ||
| [Watch](https://github.com/${{ github.repository }}/actions/workflows/ci-cd.yml) | ||
| 2. **Raspberry Pi** → SD card images (arm64 + armhf) | ||
| [Watch](https://github.com/${{ github.repository }}/actions/workflows/build-raspi-image.yml) | ||
| ## 📦 After CI completes | ||
| \`\`\`bash | ||
| docker pull ghcr.io/${{ github.repository }}:stable | ||
| docker pull ghcr.io/${{ github.repository }}:${VERSION} | ||
| \`\`\` | ||
| **Release:** https://github.com/${{ github.repository }}/releases/tag/v${VERSION} | ||
| EOF | ||
| # ============================================================================ | ||
| # TRIGGER RASPBERRY PI IMAGE BUILDS | ||
| # ============================================================================ | ||
| trigger-raspi: | ||
| name: Trigger Raspberry Pi Builds | ||
| runs-on: ubuntu-latest | ||
| needs: release | ||
| if: ${{ !inputs.skip_raspi }} | ||
| steps: | ||
| - name: Trigger build | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| VERSION="${{ needs.release.outputs.version }}" | ||
| echo "📠Triggering Raspberry Pi image builds for v${VERSION}..." | ||
| gh workflow run build-raspi-image.yml \ | ||
| --repo "${{ github.repository }}" \ | ||
| --field oct_version="${VERSION}" \ | ||
| --field architectures=all \ | ||
| --field attach_to_release=true | ||
| echo "✅ Raspberry Pi build triggered" | ||
| echo "📋 Images will be automatically attached to the release when ready" | ||
| # ============================================================================ | ||
| # VERIFY DOWNSTREAM WORKFLOWS | ||
| # ============================================================================ | ||
| verify: | ||
| name: Verify Downstream | ||
| runs-on: ubuntu-latest | ||
| needs: [release, trigger-raspi] | ||
| if: always() && needs.release.result == 'success' | ||
| steps: | ||
| - name: Wait and check | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| sleep 15 | ||
| VERSION="${{ needs.release.outputs.version }}" | ||
| TAG="v${VERSION}" | ||
| echo "# 🔠Downstream Status" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| # CI/CD | ||
| CI_STATUS=$(gh run list --workflow=ci-cd.yml --limit=1 \ | ||
| --repo "${{ github.repository }}" \ | ||
| --json status --jq '.[0].status' 2>/dev/null || echo "unknown") | ||
| echo "- **CI/CD Pipeline**: ${CI_STATUS}" >> $GITHUB_STEP_SUMMARY | ||
| # RasPi | ||
| if [ "${{ needs.trigger-raspi.result }}" = "success" ]; then | ||
| RASPI_STATUS=$(gh run list --workflow=build-raspi-image.yml --limit=1 \ | ||
| --repo "${{ github.repository }}" \ | ||
| --json status --jq '.[0].status' 2>/dev/null || echo "unknown") | ||
| echo "- **Raspberry Pi Builds**: ${RASPI_STATUS}" >> $GITHUB_STEP_SUMMARY | ||
| else | ||
| echo "- **Raspberry Pi Builds**: âï¸ Skipped" >> $GITHUB_STEP_SUMMARY | ||
| fi | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "📋 [Release Page](https://github.com/${{ github.repository }}/releases/tag/${TAG})" >> $GITHUB_STEP_SUMMARY | ||