diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd41909..38acccc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,393 +1,115 @@ -name: Build Multi-Platform +name: Build and Push +# Off-infra build (ADR-0002). Canonical repo is Forgejo viktor/freedify, which +# push-mirrors here; this workflow builds on GitHub-hosted runners, pushes the +# image to GHCR, then signals the Woodpecker deploy pipeline (repo 202) +# to roll the cluster — the homelab never sees build IO or registry pushes. +# +# Committed on the FORGEJO side (the mirror is one-way; commits made on GitHub +# are overwritten by the next sync). Generated by infra/scripts/offinfra-onboard. on: push: - tags: - - 'v*' - workflow_dispatch: + branches: [master] + workflow_dispatch: {} -env: - GO_VERSION: '1.25.5' - NODE_VERSION: '24' +permissions: + contents: read + packages: write jobs: - build-windows: - name: Build Windows - runs-on: windows-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get version from tag - id: version - shell: bash - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/} - else - VERSION="dev" - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - continue-on-error: true - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('frontend/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - - - name: Install frontend dependencies - working-directory: frontend - run: | - pnpm install - pnpm run generate-icon - - - name: Install UPX - run: | - choco install upx -y - - - name: Build application - run: wails build -platform windows/amd64 - - - name: Compress with UPX - run: | - upx --best --lzma "build\bin\SpotiFLAC.exe" - - - name: Prepare artifacts - run: | - mkdir -p dist - Copy-Item -Path "build\bin\SpotiFLAC.exe" -Destination "dist\SpotiFLAC.exe" - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: windows-portable - path: dist/SpotiFLAC.exe - retention-days: 7 - - build-macos: - name: Build macOS - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get version from tag - id: version - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/} - else - VERSION="dev" - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Get pnpm store directory - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - continue-on-error: true - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('frontend/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - - - name: Install frontend dependencies - working-directory: frontend - run: | - pnpm install - pnpm run generate-icon - - - name: Build application - run: wails build -platform darwin/universal - - - name: Create DMG - run: | - mkdir -p dist - # Install create-dmg if not available - brew install create-dmg || true - - # Create DMG - create-dmg \ - --volname "SpotiFLAC" \ - --window-pos 200 120 \ - --window-size 600 400 \ - --icon-size 100 \ - --icon "SpotiFLAC.app" 175 120 \ - --hide-extension "SpotiFLAC.app" \ - --app-drop-link 425 120 \ - "dist/SpotiFLAC.dmg" \ - "build/bin/SpotiFLAC.app" || \ - # Fallback to hdiutil if create-dmg fails - hdiutil create -volname SpotiFLAC -srcfolder build/bin/SpotiFLAC.app -ov -format UDZO dist/SpotiFLAC.dmg - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: macos-portable - path: dist/SpotiFLAC.dmg - retention-days: 7 - - build-linux: - name: Build Linux - runs-on: ubuntu-24.04 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get version from tag - id: version - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/} - else - VERSION="dev" - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Get pnpm store directory - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - continue-on-error: true - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('frontend/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse2 imagemagick upx-ucl - - # Create symlink for webkit2gtk-4.0 -> webkit2gtk-4.1 (Ubuntu 24.04 compatibility) - sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.0.pc - - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - - - name: Install frontend dependencies - working-directory: frontend - run: | - pnpm install - pnpm run generate-icon - - - name: Build application - run: wails build -platform linux/amd64 - - - name: Compress with UPX - run: | - upx --best --lzma build/bin/SpotiFLAC - - - name: Cache appimagetool - id: cache-appimagetool - uses: actions/cache@v4 - with: - path: appimagetool - key: appimagetool-x86_64-v1 - - - name: Download appimagetool - if: steps.cache-appimagetool.outputs.cache-hit != 'true' - run: | - wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage || \ - wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage - - - name: Make appimagetool executable - run: chmod +x appimagetool - - - name: Create AppImage - run: | - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - - # Copy binary - cp build/bin/SpotiFLAC AppDir/usr/bin/ - - # Create desktop file - cat > AppDir/spotiflac.desktop << 'EOF' - [Desktop Entry] - Name=SpotiFLAC - Exec=SpotiFLAC - Icon=spotiflac - Type=Application - Categories=Audio;AudioVideo; - Comment=Get Spotify tracks in true FLAC from Tidal/Deezer - EOF - - cp AppDir/spotiflac.desktop AppDir/usr/share/applications/ - - # Create icon - if [ -f "build/appicon.png" ]; then - convert build/appicon.png -resize 256x256 AppDir/spotiflac.png - elif [ -f "frontend/public/icon.svg" ]; then - convert -background none -size 256x256 frontend/public/icon.svg AppDir/spotiflac.png - else - echo "Warning: No icon found, building without icon" - fi - - # Copy icon if exists - if [ -f "AppDir/spotiflac.png" ]; then - cp AppDir/spotiflac.png AppDir/usr/share/icons/hicolor/256x256/apps/ - cp AppDir/spotiflac.png AppDir/.DirIcon - fi - - # Create AppRun - cat > AppDir/AppRun << 'EOF' - #!/bin/sh - SELF=$(readlink -f "$0") - HERE=${SELF%/*} - export PATH="${HERE}/usr/bin/:${PATH}" - export LD_LIBRARY_PATH="${HERE}/usr/lib/:${LD_LIBRARY_PATH}" - exec "${HERE}/usr/bin/SpotiFLAC" "$@" - EOF - chmod +x AppDir/AppRun - - # Create AppImage - mkdir -p dist - ARCH=x86_64 ./appimagetool --no-appstream AppDir dist/SpotiFLAC.AppImage - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: linux-portable - path: dist/SpotiFLAC.AppImage - retention-days: 7 - - create-release: - name: Create Release - needs: [build-windows, build-macos, build-linux] + lint-and-test: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') - permissions: - contents: write steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - run: echo "no test steps configured" - - name: Get version from tag - id: version - run: | - VERSION=${GITHUB_REF#refs/tags/} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Download all artifacts - uses: actions/download-artifact@v4 + build: + needs: lint-and-test + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.sha }} + steps: + - uses: actions/checkout@v6 with: - path: artifacts - - - name: Display structure of downloaded files - run: ls -R artifacts - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - draft: true - prerelease: false - generate_release_notes: false - body: | - ## Changelog - - ## Downloads - - - `SpotiFLAC.exe` - Windows - - `SpotiFLAC.dmg` - macOS - - `SpotiFLAC.AppImage` - Linux - -
- Linux Requirements - - The AppImage requires `webkit2gtk-4.1` to be installed on your system: - - **Ubuntu/Debian:** - ```bash - sudo apt install libwebkit2gtk-4.1-0 - ``` - - **Arch Linux:** - ```bash - sudo pacman -S webkit2gtk-4.1 - ``` - - **Fedora:** - ```bash - sudo dnf install webkit2gtk4.1 - ``` - - After installing the dependency, make the AppImage executable: - ```bash - chmod +x SpotiFLAC.AppImage - ./SpotiFLAC.AppImage - ``` - -
- files: | - artifacts/windows-portable/*.exe - artifacts/macos-portable/*.dmg - artifacts/linux-portable/*.AppImage + fetch-depth: 0 # full history + tags so svu sees the last vX.Y.Z + fetch-tags: true + # Auto-semver (svu): tag-only, pushed to CANONICAL Forgejo (GitHub tags + # would be wiped by the next mirror sync). Best-effort: never blocks the build. + - name: Compute + tag semver (svu) env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FORGEJO_GIT_TOKEN: ${{ secrets.FORGEJO_GIT_TOKEN }} + run: | + set +e + git config user.email "ci@viktorbarzin.me" + git config user.name "freedify-ci" + git config --global --add safe.directory "$GITHUB_WORKSPACE" + curl -sSL https://github.com/caarlos0/svu/releases/download/v3.4.1/svu_3.4.1_linux_amd64.tar.gz | tar -xz svu + CUR=$(./svu current 2>/dev/null) + NEXT=$(./svu next 2>/dev/null) + echo "svu current=[$CUR] next=[$NEXT]" + if [ -n "$NEXT" ] && [ "$NEXT" != "$CUR" ]; then + git tag "$NEXT" 2>/dev/null + git push "https://viktor:${FORGEJO_GIT_TOKEN}@forgejo.viktorbarzin.me/viktor/freedify.git" "$NEXT" && echo "pushed tag $NEXT to forgejo" || echo "tag push failed (non-blocking)" + fi + exit 0 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + run: echo "sha=$(echo ${{ github.sha }} | cut -c1-8)" >> "$GITHUB_OUTPUT" + - uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: linux/amd64 + # Single-manifest images (no provenance/SBOM attestation children) so + # registry retention can never orphan index children (ADR-0002). + provenance: false + tags: | + ghcr.io/viktorbarzin/freedify:${{ steps.meta.outputs.sha }} + ghcr.io/viktorbarzin/freedify:latest + cache-from: type=gha + cache-to: type=gha,mode=max + # Keep the newest ~10 versions on ghcr (latest rides the newest one). + - name: ghcr retention (keep 10) + uses: actions/delete-package-versions@v5 + continue-on-error: true + with: + package-name: freedify + package-type: container + min-versions-to-keep: 10 + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + # Signal Woodpecker (repo 202 = ViktorBarzin/freedify mirror) to run + # .woodpecker/deploy.yml — kubectl set image in-cluster (agent SA is cluster-admin). + - name: Trigger Woodpecker deploy + run: | + for attempt in 1 2 3; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "https://ci.viktorbarzin.me/api/repos/202/pipelines" \ + -H "Authorization: Bearer ${{ secrets.WOODPECKER_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{\"branch\":\"master\",\"variables\":{\"IMAGE_TAG\":\"${{ needs.build.outputs.image_tag }}\",\"IMAGE_NAME\":\"ghcr.io/viktorbarzin/freedify\"}}") + if [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 300 ]; then + echo "Woodpecker deploy triggered (HTTP $STATUS)"; exit 0 + fi + echo "Attempt $attempt failed (HTTP $STATUS), retrying in 30s..."; sleep 30 + done + echo "Failed to trigger Woodpecker deploy after 3 attempts"; exit 1 + + notify-failure: + needs: [lint-and-test, build, deploy] + if: failure() + runs-on: ubuntu-latest + steps: + - name: Slack notify + run: | + curl -sf -X POST -H 'Content-Type: application/json' \ + -d "{\"text\":\":rotating_light: freedify off-infra build FAILED: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + "${{ secrets.SLACK_WEBHOOK }}" || true diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index 886d8b2..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,28 +0,0 @@ -when: - event: push - -clone: - git: - image: woodpeckerci/plugin-git - settings: - attempts: 5 - backoff: 10s - -steps: - - name: build-and-push - image: woodpeckerci/plugin-docker-buildx - settings: - # Phase 4 of forgejo-registry-consolidation 2026-05-07 — Forgejo only. - repo: - - forgejo.viktorbarzin.me/viktor/freedify - logins: - - registry: forgejo.viktorbarzin.me - username: - from_secret: forgejo_user - password: - from_secret: forgejo_push_token - dockerfile: Dockerfile - context: . - auto_tag: true - platforms: - - linux/amd64 diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 0000000..856ecac --- /dev/null +++ b/.woodpecker/deploy.yml @@ -0,0 +1,22 @@ +# Auto-deploy, triggered ONLY by the GitHub Actions build POSTing to the +# Woodpecker API (manual event, with IMAGE_TAG + IMAGE_NAME) after a successful +# off-infra build+push to GHCR (ADR-0002). event:[manual] (NOT push) so the +# Forgejo->GitHub mirror's raw pushes don't fire a spurious deploy. +# The woodpecker-agent SA is cluster-admin — no kubeconfig needed. +# Generated by infra/scripts/offinfra-onboard. +when: + - event: manual + +steps: + - name: check-vars + image: alpine + commands: + - "[ -n \"$IMAGE_TAG\" ] || (echo 'IMAGE_TAG not set — refusing to deploy'; exit 1)" + + - name: deploy + image: bitnami/kubectl:latest + commands: + - "kubectl -n freedify set image deployment/music-viktor freedify=${IMAGE_NAME}:${IMAGE_TAG}" + - "kubectl -n freedify rollout status deployment/music-viktor --timeout=300s" + - "kubectl -n freedify set image deployment/music-emo freedify=${IMAGE_NAME}:${IMAGE_TAG}" + - "kubectl -n freedify rollout status deployment/music-emo --timeout=300s"