name: Release on: push: tags: - 'v*' concurrency: group: release-${{ github.ref }} cancel-in-progress: false permissions: actions: read contents: write jobs: quality-gate: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.5 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules vendor/subminer-yomitan/node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/subminer-yomitan/package-lock.json') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build (TypeScript check) run: bun run typecheck - name: Test suite (source) run: bun run test:fast - name: Launcher smoke suite (source) run: bun run test:launcher:smoke:src - name: Upload launcher smoke artifacts (on failure) if: failure() uses: actions/upload-artifact@v4 with: name: launcher-smoke path: .tmp/launcher-smoke/** if-no-files-found: ignore - name: Build (bundle) run: bun run build - name: Immersion SQLite verification run: bun run test:immersion:sqlite:dist - name: Dist smoke suite run: bun run test:smoke:dist build-linux: needs: [quality-gate] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.5 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules vendor/texthooker-ui/node_modules vendor/subminer-yomitan/node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build texthooker-ui run: | cd vendor/texthooker-ui bun install bun run build - name: Build AppImage run: bun run build:appimage - name: Build unversioned AppImage run: | shopt -s nullglob appimages=(release/SubMiner-*.AppImage) if [ "${#appimages[@]}" -eq 0 ]; then echo "No versioned AppImage found to create unversioned artifact." ls -la release exit 1 fi cp "${appimages[0]}" release/SubMiner.AppImage - name: Upload AppImage artifact uses: actions/upload-artifact@v4 with: name: appimage path: release/*.AppImage build-macos: needs: [quality-gate] runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.5 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules vendor/texthooker-ui/node_modules vendor/subminer-yomitan/node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }} restore-keys: | ${{ runner.os }}-bun- - name: Validate macOS signing/notarization secrets run: | missing=0 for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do if [ -z "${!name}" ]; then echo "Missing required secret: $name" missing=1 fi done if [ "$missing" -ne 0 ]; then echo "Set all required macOS signing/notarization secrets and rerun." exit 1 fi env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Install dependencies run: bun install --frozen-lockfile - name: Build texthooker-ui run: | cd vendor/texthooker-ui bun install bun run build - name: Build signed + notarized macOS artifacts run: bun run build:mac env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Upload macOS artifacts uses: actions/upload-artifact@v4 with: name: macos path: | release/*.dmg release/*.zip build-windows: needs: [quality-gate] runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.5 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules vendor/texthooker-ui/node_modules vendor/subminer-yomitan/node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }} restore-keys: | ${{ runner.os }}-bun- - name: Validate Windows signing secrets shell: bash run: | missing=0 for name in SIGNPATH_API_TOKEN SIGNPATH_ORGANIZATION_ID SIGNPATH_PROJECT_SLUG SIGNPATH_SIGNING_POLICY_SLUG; do if [ -z "${!name}" ]; then echo "Missing required secret: $name" missing=1 fi done if [ "$missing" -ne 0 ]; then echo "Set the SignPath Windows signing secrets and rerun." exit 1 fi env: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} SIGNPATH_ORGANIZATION_ID: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} SIGNPATH_PROJECT_SLUG: ${{ secrets.SIGNPATH_PROJECT_SLUG }} SIGNPATH_SIGNING_POLICY_SLUG: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} - name: Install dependencies run: bun install --frozen-lockfile - name: Build texthooker-ui shell: powershell run: | Set-Location vendor/texthooker-ui bun install bun run build - name: Build unsigned Windows artifacts run: bun run build:win - name: Upload unsigned Windows artifact for SignPath id: upload-unsigned-windows-artifact uses: actions/upload-artifact@v4 with: name: unsigned-windows path: | release/*.exe release/*.zip if-no-files-found: error - name: Submit Windows signing request (attempt 1) id: signpath-sign-attempt-1 continue-on-error: true uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} github-artifact-id: ${{ steps.upload-unsigned-windows-artifact.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: signed-windows-attempt-1 github-token: ${{ secrets.GITHUB_TOKEN }} - name: Submit Windows signing request (attempt 2) id: signpath-sign-attempt-2 if: steps.signpath-sign-attempt-1.outcome == 'failure' continue-on-error: true uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} github-artifact-id: ${{ steps.upload-unsigned-windows-artifact.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: signed-windows-attempt-2 github-token: ${{ secrets.GITHUB_TOKEN }} - name: Submit Windows signing request (attempt 3) id: signpath-sign-attempt-3 if: steps.signpath-sign-attempt-1.outcome == 'failure' && steps.signpath-sign-attempt-2.outcome == 'failure' continue-on-error: true uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} github-artifact-id: ${{ steps.upload-unsigned-windows-artifact.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: signed-windows-attempt-3 github-token: ${{ secrets.GITHUB_TOKEN }} - name: Fail when all SignPath signing attempts fail if: steps.signpath-sign-attempt-1.outcome == 'failure' && steps.signpath-sign-attempt-2.outcome == 'failure' && steps.signpath-sign-attempt-3.outcome == 'failure' shell: bash run: | echo "All SignPath signing attempts failed; rerun the workflow when SignPath is healthy." exit 1 - name: Upload signed Windows artifacts (attempt 1) if: steps.signpath-sign-attempt-1.outcome == 'success' uses: actions/upload-artifact@v4 with: name: windows path: | signed-windows-attempt-1/*.exe signed-windows-attempt-1/*.zip - name: Upload signed Windows artifacts (attempt 2) if: steps.signpath-sign-attempt-2.outcome == 'success' uses: actions/upload-artifact@v4 with: name: windows path: | signed-windows-attempt-2/*.exe signed-windows-attempt-2/*.zip - name: Upload signed Windows artifacts (attempt 3) if: steps.signpath-sign-attempt-3.outcome == 'success' uses: actions/upload-artifact@v4 with: name: windows path: | signed-windows-attempt-3/*.exe signed-windows-attempt-3/*.zip release: needs: [build-linux, build-macos, build-windows] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download AppImage uses: actions/download-artifact@v4 with: name: appimage path: release - name: Download macOS artifacts uses: actions/download-artifact@v4 with: name: macos path: release - name: Download Windows artifacts uses: actions/download-artifact@v4 with: name: windows path: release - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.5 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build Bun subminer wrapper run: make build-launcher - name: Verify Bun subminer wrapper run: dist/launcher/subminer --help >/dev/null - name: Enforce generated launcher workflow run: bash scripts/verify-generated-launcher.sh - name: Package optional assets bundle run: | tar -czf "release/subminer-assets.tar.gz" \ config.example.jsonc \ plugin/subminer \ plugin/subminer.conf \ assets/themes/subminer.rasi - name: Generate checksums run: | shopt -s nullglob files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer) if [ "${#files[@]}" -eq 0 ]; then echo "No release artifacts found for checksum generation." exit 1 fi sha256sum "${files[@]}" > release/SHA256SUMS.txt - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Verify changelog is ready for tagged release run: bun run changelog:check --version "${{ steps.version.outputs.VERSION }}" - name: Generate release notes from changelog run: bun run changelog:release-notes --version "${{ steps.version.outputs.VERSION }}" - name: Publish Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then # Do not pass the prerelease flag here; gh defaults to a normal release. gh release edit "${{ steps.version.outputs.VERSION }}" \ --draft=false \ --title "${{ steps.version.outputs.VERSION }}" \ --notes-file release/release-notes.md else gh release create "${{ steps.version.outputs.VERSION }}" \ --title "${{ steps.version.outputs.VERSION }}" \ --notes-file release/release-notes.md fi shopt -s nullglob artifacts=( release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/SHA256SUMS.txt dist/launcher/subminer ) if [ "${#artifacts[@]}" -eq 0 ]; then echo "No release artifacts found for upload." exit 1 fi for asset in "${artifacts[@]}"; do gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber done