Repository navigation
feat(cli-manager): add quota refresh and stabilize CI #6
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: release | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| fork-release-disabled: | |
| if: github.repository != 'dyndynjyxa/aio-coding-hub' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Skip release automation in forks | |
| run: echo "Release automation is disabled outside dyndynjyxa/aio-coding-hub." | |
| release-please: | |
| if: github.repository == 'dyndynjyxa/aio-coding-hub' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_created: ${{ steps.release.outputs.release_created }} | |
| tag_name: ${{ steps.release.outputs.tag_name }} | |
| release_id: ${{ steps.release.outputs.id }} | |
| build_matrix: ${{ steps.support_matrix.outputs.build_matrix }} | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Validate support matrix contract | |
| run: node scripts/support-matrix.mjs check | |
| - id: support_matrix | |
| name: Load release support matrix | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "build_matrix=$(node scripts/support-matrix.mjs build-matrix)" >> "$GITHUB_OUTPUT" | |
| - id: release | |
| uses: googleapis/release-please-action@8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5 # v4 | |
| with: | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| target-branch: main | |
| skip-github-pull-request: "${{ github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(main): release aio-coding-hub ') }}" | |
| token: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| - name: Check release PR changelog range | |
| if: steps.release.outputs.prs_created == 'true' | |
| env: | |
| RELEASE_PLEASE_PRS: ${{ steps.release.outputs.prs }} | |
| GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| run: node scripts/check-release-pr-changelog.mjs | |
| build: | |
| needs: release-please | |
| if: needs.release-please.outputs.release_created == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.release-please.outputs.build_matrix) }} | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Install Rust 1.90.0 | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.90.0 | |
| components: rustfmt, clippy | |
| targets: ${{ matrix.target }} | |
| - name: Rust cache | |
| uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 | |
| with: | |
| workspaces: src-tauri | |
| - name: Install Linux dependencies | |
| if: matrix.platform == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libasound2-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf | |
| - name: Install deps | |
| run: pnpm install --frozen-lockfile | |
| - name: Prepare updater signing key (validate password) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| raw_key="${TAURI_SIGNING_PRIVATE_KEY_SECRET:-}" | |
| raw_pwd="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET:-}" | |
| if [[ -z "${raw_key//[[:space:]]/}" ]]; then | |
| echo "Missing required secret: TAURI_SIGNING_PRIVATE_KEY" >&2 | |
| exit 1 | |
| fi | |
| if [[ -z "$raw_pwd" ]]; then | |
| echo "Missing required secret: TAURI_SIGNING_PRIVATE_KEY_PASSWORD" >&2 | |
| exit 1 | |
| fi | |
| # Tauri v2 updater key is stored as a single-line base64 string (as generated by `tauri signer generate --ci`). | |
| # Normalize by stripping whitespace to avoid accidental newline issues in GitHub Secrets. | |
| key_b64="$(printf '%s' "$raw_key" | tr -d '\r\n\t ')" | |
| printf '%s' "$key_b64" > tauri-updater.key | |
| chmod 600 tauri-updater.key | |
| echo "::add-mask::$raw_pwd" | |
| echo "sign-test" > sign-test.txt | |
| if ! pnpm tauri signer sign -f tauri-updater.key -p "$raw_pwd" sign-test.txt >/dev/null 2>sign-check.err; then | |
| cat sign-check.err | |
| echo "Signing key/password check failed." >&2 | |
| exit 1 | |
| fi | |
| rm -f sign-test.txt.sig sign-check.err | |
| echo "TAURI_SIGNING_PRIVATE_KEY=$key_b64" >> "$GITHUB_ENV" | |
| echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$raw_pwd" >> "$GITHUB_ENV" | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY_SECRET: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| - id: tauri | |
| name: Build & upload (Tauri) | |
| uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| with: | |
| releaseId: ${{ needs.release-please.outputs.release_id }} | |
| tagName: ${{ needs.release-please.outputs.tag_name }} | |
| args: --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} | |
| # Avoid matrix race on the shared `latest.json` asset; we generate/upload it in a dedicated job. | |
| includeUpdaterJson: false | |
| - name: Prepare stable updater assets (+ macOS zip) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node scripts/support-matrix.mjs prepare-stable-assets \ | |
| --artifact-paths "$ARTIFACT_PATHS_JSON" \ | |
| --updater-platform "${{ matrix.updater_platform }}" \ | |
| --stable-label "${{ matrix.stable_label }}" \ | |
| --output-dir stable-assets | |
| env: | |
| ARTIFACT_PATHS_JSON: ${{ steps.tauri.outputs.artifactPaths }} | |
| - name: Build macOS zip (portable) | |
| if: startsWith(matrix.updater_platform, 'darwin-') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| label="${{ matrix.stable_label }}" | |
| tgz="stable-assets/aio-coding-hub-${label}.tar.gz" | |
| if [[ ! -f "$tgz" ]]; then | |
| echo "Missing tarball: $tgz" | |
| ls -la stable-assets || true | |
| exit 1 | |
| fi | |
| tmp="$(mktemp -d)" | |
| tar -xzf "$tgz" -C "$tmp" | |
| app_path="$(find "$tmp" -maxdepth 2 -name "*.app" -type d | head -n 1 || true)" | |
| if [[ -z "$app_path" ]]; then | |
| echo "No .app found inside $tgz" | |
| find "$tmp" -maxdepth 3 -print || true | |
| exit 1 | |
| fi | |
| out="stable-assets/aio-coding-hub-${label}.zip" | |
| ditto -c -k --sequesterRsrc --keepParent "$app_path" "$out" | |
| echo "Created zip: $out" | |
| - name: Build Windows portable ZIP | |
| if: startsWith(matrix.updater_platform, 'windows-') | |
| shell: pwsh | |
| run: | | |
| $label = "${{ matrix.stable_label }}" | |
| $targetDir = "src-tauri/target/${{ matrix.target }}/release" | |
| # Find the built application exe (not the NSIS/MSI installer) | |
| $exeName = "AIO Coding Hub.exe" | |
| $exePath = Join-Path $targetDir $exeName | |
| if (-not (Test-Path $exePath)) { | |
| $exeName = "aio-coding-hub.exe" | |
| $exePath = Join-Path $targetDir $exeName | |
| } | |
| if (-not (Test-Path $exePath)) { | |
| Write-Error "Cannot find built exe in $targetDir" | |
| Get-ChildItem $targetDir -Filter "*.exe" | ForEach-Object { Write-Host $_.FullName } | |
| exit 1 | |
| } | |
| # Create a temp directory for the portable package | |
| $portableDir = "portable-build/AIO Coding Hub" | |
| New-Item -ItemType Directory -Path $portableDir -Force | Out-Null | |
| # Copy the exe | |
| Copy-Item $exePath $portableDir | |
| # Copy resources if they exist (icons, webview assets, etc.) | |
| $resourceDir = Join-Path $targetDir "resources" | |
| if (Test-Path $resourceDir) { | |
| Copy-Item -Recurse $resourceDir (Join-Path $portableDir "resources") | |
| } | |
| # Create the ZIP | |
| $zipPath = "stable-assets/aio-coding-hub-${label}-portable.zip" | |
| Compress-Archive -Path "$portableDir/*" -DestinationPath $zipPath -Force | |
| Write-Host "Created portable ZIP: $zipPath" | |
| Get-Item $zipPath | Select-Object Name, Length | |
| - name: Build Wayland-compatible AppImage (Linux only) | |
| if: startsWith(matrix.updater_platform, 'linux-') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Download pinned stable release of appimagetool (no FUSE required). | |
| TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage" | |
| TOOL_BIN="$(mktemp)" | |
| curl -fsSL "$TOOL_URL" -o "$TOOL_BIN" | |
| chmod +x "$TOOL_BIN" | |
| label="${{ matrix.stable_label }}" | |
| src="stable-assets/aio-coding-hub-${label}.AppImage" | |
| out="stable-assets/aio-coding-hub-${label}-wayland.AppImage" | |
| APPIMAGETOOL="$TOOL_BIN" ./scripts/repack-linux-appimage-wayland.sh "$src" "$out" | |
| rm -f "$TOOL_BIN" | |
| echo "Wayland AppImage: $out" | |
| ls -lh "$out" | |
| - name: Upload stable assets (workflow artifact) | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: stable-assets-${{ matrix.updater_platform }} | |
| path: stable-assets/* | |
| if-no-files-found: error | |
| - name: Upload stable assets (GitHub Release) | |
| uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| with: | |
| tag_name: ${{ needs.release-please.outputs.tag_name }} | |
| files: stable-assets/* | |
| fail_on_unmatched_files: true | |
| assemble-latest-json: | |
| needs: [release-please, build] | |
| if: needs.release-please.outputs.release_created == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Download stable assets (workflow artifacts) | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| pattern: stable-assets-* | |
| merge-multiple: true | |
| path: stable-assets | |
| - name: Fetch release body | |
| id: release_body | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| script: | | |
| const releaseId = Number('${{ needs.release-please.outputs.release_id }}'); | |
| if (!Number.isFinite(releaseId) || releaseId <= 0) { | |
| core.setFailed(`Invalid release_id: ${releaseId}`); | |
| return; | |
| } | |
| const { data: release } = await github.rest.repos.getRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: releaseId, | |
| }); | |
| core.setOutput('body', release.body || ''); | |
| - name: Generate latest.json | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node scripts/support-matrix.mjs generate-latest-json \ | |
| --tag "${{ needs.release-please.outputs.tag_name }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --pub-date "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | |
| --stable-assets-dir stable-assets \ | |
| --output latest.json | |
| env: | |
| RELEASE_BODY: ${{ steps.release_body.outputs.body }} | |
| FALLBACK_NOTES: "See release: https://github.com/${{ github.repository }}/releases/tag/${{ needs.release-please.outputs.tag_name }}" | |
| - name: Delete existing latest.json (best-effort) | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| script: | | |
| const releaseId = Number('${{ needs.release-please.outputs.release_id }}'); | |
| if (!Number.isFinite(releaseId) || releaseId <= 0) { | |
| core.setFailed(`Invalid release_id: ${releaseId}`); | |
| return; | |
| } | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const assets = await github.paginate(github.rest.repos.listReleaseAssets, { | |
| owner, | |
| repo, | |
| release_id: releaseId, | |
| per_page: 100, | |
| }); | |
| const existing = assets.find((a) => a && a.name === 'latest.json'); | |
| if (!existing) { | |
| core.info('No existing latest.json asset found.'); | |
| return; | |
| } | |
| core.info(`Deleting existing latest.json asset id=${existing.id}`); | |
| await github.rest.repos.deleteReleaseAsset({ | |
| owner, | |
| repo, | |
| asset_id: existing.id, | |
| }); | |
| - name: Upload latest.json (GitHub Release) | |
| uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| with: | |
| tag_name: ${{ needs.release-please.outputs.tag_name }} | |
| files: latest.json | |
| fail_on_unmatched_files: true | |
| publish: | |
| needs: [release-please, build, assemble-latest-json] | |
| if: needs.release-please.outputs.release_created == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Publish GitHub Release (unset draft) | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_PLEASE_TOKEN }} | |
| script: | | |
| const releaseId = Number('${{ needs.release-please.outputs.release_id }}'); | |
| if (!Number.isFinite(releaseId) || releaseId <= 0) { | |
| core.setFailed(`Invalid release_id: ${releaseId}`); | |
| return; | |
| } | |
| await github.rest.repos.updateRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: releaseId, | |
| draft: false, | |
| }); |