name: Axolotl release on: push: tags: - 'v*' permissions: contents: write concurrency: group: axolotl-release-${{ github.ref }} cancel-in-progress: false jobs: preflight: name: Release preflight runs-on: ubuntu-24.04 env: SQLX_OFFLINE: true steps: - name: Check out source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 submodules: recursive - name: Set up pnpm uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9 - name: Set up Node.js uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version-file: .nvmrc cache: pnpm - name: Set up Rust uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install Blockbench skin editor dependencies run: npm ci working-directory: third-party/blockbench - name: Run release guardrails env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | node scripts/axolotl/check-migrations.mjs --release node scripts/axolotl/brand-guard.mjs node scripts/axolotl/i18n-check.mjs cargo fmt --all --check - name: Prepare draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | release_notes="${RUNNER_TEMP}/axolotl-release-notes.md" node scripts/axolotl/create-release-notes.mjs "${GITHUB_REF_NAME}" "$release_notes" existing_release="$( gh release list --limit 100 --json tagName \ --jq ".[] | select(.tagName == \"${GITHUB_REF_NAME}\") | .tagName" )" if [[ -n "$existing_release" ]]; then echo "Release ${GITHUB_REF_NAME} already exists" gh release edit "${GITHUB_REF_NAME}" --notes-file "$release_notes" exit 0 fi release_args=( --draft --notes-file "$release_notes" --title "Axolotl Launcher ${GITHUB_REF_NAME}" ) if [[ "${GITHUB_REF_NAME}" == *-* ]]; then release_args+=(--prerelease) fi gh release create "${GITHUB_REF_NAME}" "${release_args[@]}" build: name: Build (${{ matrix.name }}) needs: preflight strategy: fail-fast: true max-parallel: 4 matrix: include: - name: Linux x64 runner: ubuntu-24.04 args: --config tauri-release.conf.json --config tauri-updater.conf.json - name: Linux arm64 runner: ubuntu-24.04-arm args: --config tauri-release.conf.json --config tauri-updater.conf.json - name: Windows x64 runner: windows-2025 args: --config tauri-release.conf.json --config tauri-modern.conf.json --config tauri-updater.conf.json - name: macOS universal runner: macos-15 args: --target universal-apple-darwin --config tauri-release.conf.json --config tauri-updater.conf.json runs-on: ${{ matrix.runner }} env: SQLX_OFFLINE: true steps: - name: Check out source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 submodules: recursive - name: Set up pnpm uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9 - name: Set up Node.js uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version-file: .nvmrc cache: pnpm - name: Set up Rust uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - name: Set up Rust compiler cache if: runner.os == 'Windows' uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 - name: Check Rust compiler cache availability id: sccache if: runner.os == 'Windows' continue-on-error: true shell: bash env: SCCACHE_GHA_ENABLED: 'true' run: ${SCCACHE_PATH} rustc -vV - name: Enable Rust compiler cache if: runner.os == 'Windows' && steps.sccache.outcome == 'success' shell: bash run: | echo 'RUSTC_WRAPPER=sccache' >> "$GITHUB_ENV" echo 'SCCACHE_GHA_ENABLED=true' >> "$GITHUB_ENV" echo 'SCCACHE_CACHE_SIZE=4G' >> "$GITHUB_ENV" - name: Add macOS universal targets if: runner.os == 'macOS' run: rustup target add aarch64-apple-darwin x86_64-apple-darwin - name: Install Linux dependencies if: runner.os == 'Linux' run: sudo apt-get -o Acquire::ForceIPv4=true update && sudo apt-get -o Acquire::ForceIPv4=true install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install Blockbench skin editor dependencies run: npm ci working-directory: third-party/blockbench - uses: ./.github/actions/replace-gradle-mirror - name: Set release version run: node scripts/axolotl/set-version.mjs "${{ github.ref_name }}" - name: Build and upload release assets uses: tauri-apps/tauri-action@1ddc7b49b13c3038297360482fcb3e058764d7c9 # v0.6.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_BUNDLER_DMG_IGNORE_CI: ${{ runner.os == 'macOS' && 'true' || '' }} with: projectPath: apps/app tauriScript: pnpm tauri args: ${{ matrix.args }} tagName: ${{ github.ref_name }} releaseName: Axolotl Launcher ${{ github.ref_name }} releaseDraft: true prerelease: ${{ contains(github.ref_name, '-') }} generateReleaseNotes: false includeUpdaterJson: false updaterJsonPreferNsis: true retryAttempts: 2 - name: Show Rust compiler cache statistics if: runner.os == 'Windows' && steps.sccache.outcome == 'success' && always() shell: bash run: ${SCCACHE_PATH} --show-stats - name: Publish Windows distribution variants if: runner.os == 'Windows' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} shell: pwsh run: | $version = "${{ github.ref_name }}" -replace '^v', '' $appName = "Axolotl Launcher" $outDir = "target/release/bundle/nsis" $buildDir = "target/portable-build" $publishDir = Join-Path $env:RUNNER_TEMP "windows-assets" New-Item -ItemType Directory -Force -Path $publishDir | Out-Null $modernInstallers = @(Get-ChildItem $outDir -Filter '*-setup.exe' -File) if ($modernInstallers.Count -ne 1) { throw "Expected one modern installer, found $($modernInstallers.Count)" } $modernName = "Axolotl_Launcher_${version}_x64_modern-setup.exe" $modernPath = Join-Path $publishDir $modernName Copy-Item -Force $modernInstallers[0].FullName $modernPath gh release upload "${{ github.ref_name }}" $modernPath --clobber $modernSignature = "$($modernInstallers[0].FullName).sig" if (Test-Path $modernSignature) { $modernSignaturePath = "$modernPath.sig" Copy-Item -Force $modernSignature $modernSignaturePath gh release upload "${{ github.ref_name }}" $modernSignaturePath --clobber gh release delete-asset "${{ github.ref_name }}" "$($modernInstallers[0].Name).sig" --yes } gh release delete-asset "${{ github.ref_name }}" $modernInstallers[0].Name --yes pnpm --filter @modrinth/app tauri build --config tauri-release.conf.json --config tauri-native.conf.json $nativeInstallers = @(Get-ChildItem $outDir -Filter '*-setup.exe' -File) if ($nativeInstallers.Count -ne 1) { throw "Expected one native NSIS installer, found $($nativeInstallers.Count)" } $nativeName = "Axolotl_Launcher_${version}_x64_nsis-setup.exe" $nativePath = Join-Path $publishDir $nativeName Copy-Item -Force $nativeInstallers[0].FullName $nativePath gh release upload "${{ github.ref_name }}" $nativePath --clobber $nativeSignature = "$($nativeInstallers[0].FullName).sig" if (Test-Path $nativeSignature) { $nativeSignaturePath = "$nativePath.sig" Copy-Item -Force $nativeSignature $nativeSignaturePath gh release upload "${{ github.ref_name }}" $nativeSignaturePath --clobber } # Reuse the binary already built by tauri-action $appExe = "target/release/${appName}.exe" if (Test-Path $appExe) { New-Item -ItemType Directory -Force -Path "$buildDir\Axolotl" | Out-Null Copy-Item -Force $appExe "$buildDir\Axolotl\" # Create empty .Axolotl folder to trigger portable mode New-Item -ItemType Directory -Force -Path "$buildDir\Axolotl\.Axolotl" | Out-Null # Create portable zip (for manual download) $zipName = "Axolotl_Launcher_${version}_x64_portable.zip" $zipPath = Join-Path $publishDir $zipName Compress-Archive -Path "$buildDir\Axolotl" -DestinationPath $zipPath -Force Write-Output "Created portable zip: $zipPath" # Upload portable zip (for manual download only) gh release upload "${{ github.ref_name }}" "$zipPath" --clobber } else { throw "Binary not found at $appExe" } verify-and-publish: name: Verify and publish needs: build runs-on: ubuntu-24.04 steps: - name: Check out source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: main - name: Download updater signatures env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release download "${GITHUB_REF_NAME}" --pattern '*.sig' --dir release-check/signatures - name: Download release metadata env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | release_id="$( gh api "repos/${GITHUB_REPOSITORY}/releases" \ --paginate \ --jq ".[] | select(.tag_name == \"${GITHUB_REF_NAME}\") | .id" )" if [[ -z "$release_id" ]]; then echo "Release ${GITHUB_REF_NAME} was not found" exit 1 fi gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" > release-check/release.json - name: Generate update manifest run: node scripts/axolotl/create-update-manifest.mjs release-check/release.json release-check/signatures "${GITHUB_REF_NAME}" release-check/latest.json - name: Verify update manifest run: node scripts/axolotl/verify-update-manifest.mjs release-check/latest.json "${GITHUB_REF_NAME}" - name: Upload update manifest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload "${GITHUB_REF_NAME}" release-check/latest.json --clobber - name: Publish release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [[ "${GITHUB_REF_NAME}" == *-* ]]; then gh release edit "${GITHUB_REF_NAME}" --draft=false --prerelease else gh release edit "${GITHUB_REF_NAME}" --draft=false --latest fi - name: Refresh published GitHub release metadata env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | release_id="$( gh api "repos/${GITHUB_REPOSITORY}/releases" \ --paginate \ --jq ".[] | select(.tag_name == \"${GITHUB_REF_NAME}\") | .id" )" if [[ -z "$release_id" ]]; then echo "Published release ${GITHUB_REF_NAME} was not found" exit 1 fi gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" > release-check/release.json - name: Build Update Server artifact catalog run: | node scripts/axolotl/create-update-server-catalog.mjs \ release-check/release.json \ "${GITHUB_REF_NAME}" \ release-check/update-server-catalog.json node scripts/axolotl/create-update-server-catalog.test.mjs - name: Notify Update Server to publish release assets env: UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} UPDATE_SERVER_WEBHOOK_SECRET: ${{ secrets.UPDATE_SERVER_WEBHOOK_SECRET }} UPDATE_SERVER_FORCE_UPDATE: 'false' run: | node scripts/axolotl/publish-update-server.mjs \ release-check/update-server-catalog.json \ release-check/release.json \ "${GITHUB_REF_NAME}" - name: Publish website download metadata if: ${{ !contains(github.ref_name, '-') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | node scripts/axolotl/create-website-release-metadata.mjs \ release-check/release.json \ apps/website/releases/latest.json node scripts/axolotl/create-website-release-catalog.mjs \ apps/website/releases/catalog.json git config user.name "Axolotl Release Bot" git config user.email "release@axolotl.local" git add apps/website/releases/latest.json apps/website/releases/catalog.json if git diff --cached --quiet; then echo "Website metadata is unchanged" else git commit -m "chore(website): update download metadata to ${GITHUB_REF_NAME} [skip ci]" git push origin main fi - name: Mirror published release to CNB env: CNB_REPO_URL_HTTPS: ${{ secrets.CNB_REPO_URL_HTTPS }} CNB_TOKEN: ${{ secrets.CNB_TOKEN }} CNB_TOKEN_USER_NAME: ${{ secrets.CNB_TOKEN_USER_NAME }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail if [[ -z "${CNB_TOKEN:-}" ]]; then echo "CNB_TOKEN is not configured" exit 1 fi cnb_url="${CNB_REPO_URL_HTTPS:-https://cnb.cool/axlmc/Axolotl.git}" token_user="${CNB_TOKEN_USER_NAME:-cnb}" auth="$(printf '%s:%s' "$token_user" "$CNB_TOKEN" | base64 -w0)" git_cnb() { git -c "http.extraHeader=Authorization: Basic $auth" "$@" } git fetch origin "refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}" git_cnb push --force "$cnb_url" \ "refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}" node scripts/axolotl/cnb-release.mjs finalize "${GITHUB_REF_NAME}" cnb-release/final node scripts/axolotl/verify-update-manifest.mjs \ cnb-release/final/latest.json "${GITHUB_REF_NAME}" cnb