name: Release on: push: tags: - 'v*' workflow_dispatch: jobs: # Stage 1: create (or reuse) ONE draft release up front and expose its id. # The build matrix then uploads to this fixed id instead of each job racing to # create/update the release for the tag — that race dropped Windows assets on # v0.41.0/v0.41.1. A draft stays invisible (and off the updater feed) until # stage 3 publishes it, so users never see a half-uploaded release. create-release: permissions: contents: write runs-on: ubuntu-22.04 outputs: release_id: ${{ steps.create-release.outputs.result }} steps: - name: Create draft release id: create-release uses: actions/github-script@v7 with: script: | const tag = process.env.GITHUB_REF_NAME; const { owner, repo } = context.repo; // Idempotent: reuse the release if the tag already has one (a re-cut // tag), else create a fresh draft. Force draft so the matrix uploads // land before anyone can see the release. try { const { data } = await github.rest.repos.getReleaseByTag({ owner, repo, tag }); await github.rest.repos.updateRelease({ owner, repo, release_id: data.id, draft: true }); return data.id; } catch (e) { const { data } = await github.rest.repos.createRelease({ owner, repo, tag_name: tag, name: `Agent Console ${tag}`, draft: true, prerelease: false, }); return data.id; } # Stage 2: build every platform and upload its bundles to the release created # above (releaseId is fixed, so no create/clobber race between jobs). build-tauri: needs: create-release permissions: contents: write strategy: fail-fast: false matrix: include: - platform: 'macos-latest' args: '--target aarch64-apple-darwin' - platform: 'macos-latest' args: '--target x86_64-apple-darwin' - platform: 'ubuntu-22.04' args: '' - platform: 'windows-latest' args: '' runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - name: Install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libasound2-dev - name: Rust setup uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Rust cache uses: swatinem/rust-cache@v2 # Never fail the release over caching. The save runs in a post-job step # after artifacts are already built/uploaded, and tar/zstd on Windows # occasionally errors there — a cosmetic red ❌ on an otherwise-complete # release. continue-on-error covers both the main and post steps. continue-on-error: true with: workspaces: './src-tauri -> target' - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 - name: Install frontend dependencies run: npm ci - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} # whisper.cpp (via whisper-rs) uses std::filesystem, which Apple marks # available only from macOS 10.15. The cc crate reads this env var to # set -mmacosx-version-min; without it the default (10.13) fails to # compile ggml-backend-reg.cpp. Harmless on Linux/Windows. MACOSX_DEPLOYMENT_TARGET: '10.15' with: # Upload into the pre-created draft. The updater feed (latest.json) is # deliberately NOT touched here: each job merging its platforms into # the release's latest.json was a read-modify-write race that 404'd # the mac job on v0.52.0 and could publish half-complete feeds. The # assemble-updater-json job below builds it once, from the uploaded # assets, after every build finished. includeUpdaterJson: false releaseId: ${{ needs.create-release.outputs.release_id }} args: ${{ matrix.args }} # Stage 3: single writer for the updater feed. Assembles latest.json from the # uploaded assets (+ their .sig companions) once all platform builds are done # — no concurrent merges, and the feed is complete by construction or this # job fails loudly (publish then stays skipped and the release stays draft). assemble-updater-json: needs: [create-release, build-tauri] permissions: contents: write runs-on: ubuntu-22.04 steps: - name: Assemble and upload latest.json uses: actions/github-script@v7 env: release_id: ${{ needs.create-release.outputs.release_id }} with: script: | const { owner, repo } = context.repo; const release_id = Number(process.env.release_id); const version = process.env.GITHUB_REF_NAME.replace(/^v/, ""); const assets = await github.paginate(github.rest.repos.listReleaseAssets, { owner, repo, release_id, per_page: 100, }); const byName = new Map(assets.map((a) => [a.name, a])); const find = (re) => { const hit = assets.find((a) => re.test(a.name)); if (!hit) throw new Error(`missing release asset matching ${re}`); return hit.name; }; // Draft-release assets aren't downloadable via browser URL; fetch // the .sig content through the API with an octet-stream accept. const sigOf = async (name) => { const a = byName.get(`${name}.sig`); if (!a) throw new Error(`missing signature asset: ${name}.sig`); const res = await github.request( "GET /repos/{owner}/{repo}/releases/assets/{asset_id}", { owner, repo, asset_id: a.id, headers: { accept: "application/octet-stream" } }, ); return Buffer.from(res.data).toString("utf8"); }; const entry = async (name) => ({ signature: await sigOf(name), url: `https://github.com/${owner}/${repo}/releases/latest/download/${name}`, }); const macX64 = await entry(find(/_x64\.app\.tar\.gz$/)); const macArm = await entry(find(/_aarch64\.app\.tar\.gz$/)); const appimage = await entry(find(/\.AppImage$/)); const deb = await entry(find(/\.deb$/)); const rpm = await entry(find(/\.rpm$/)); const msi = await entry(find(/\.msi$/)); const nsis = await entry(find(/-setup\.exe$/)); // Exactly the key set tauri-action used to produce (v0.58.0 feed), // so existing installs keep resolving their platform + alias keys. const platforms = { "darwin-x86_64": macX64, "darwin-x86_64-app": macX64, "darwin-aarch64": macArm, "darwin-aarch64-app": macArm, "linux-x86_64": appimage, "linux-x86_64-appimage": appimage, "linux-x86_64-deb": deb, "linux-x86_64-rpm": rpm, "windows-x86_64": msi, "windows-x86_64-msi": msi, "windows-x86_64-nsis": nsis, }; const latest = { version, notes: "", pub_date: new Date().toISOString(), platforms, }; // Idempotent re-runs: replace any existing latest.json. const existing = byName.get("latest.json"); if (existing) { await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: existing.id }); } await github.rest.repos.uploadReleaseAsset({ owner, repo, release_id, name: "latest.json", data: JSON.stringify(latest, null, 2), headers: { "content-type": "application/json" }, }); core.info(`latest.json uploaded: version ${version}, ${Object.keys(platforms).length} platforms`); # Stage 4: every platform uploaded and the updater feed assembled — flip the # draft to published so the release goes live all at once, complete. publish-release: needs: [create-release, build-tauri, assemble-updater-json] permissions: contents: write runs-on: ubuntu-22.04 steps: - name: Publish release uses: actions/github-script@v7 env: release_id: ${{ needs.create-release.outputs.release_id }} with: script: | const { owner, repo } = context.repo; await github.rest.repos.updateRelease({ owner, repo, release_id: Number(process.env.release_id), draft: false, prerelease: false, }); # Submit the new version to winget (microsoft/winget-pkgs) AFTER the release # is live — the manifest points at the published installer URL. Lives inside # this workflow on purpose: publish-release flips the draft with the Actions # GITHUB_TOKEN, and events created by that token never trigger other # workflows, so a standalone `on: release` listener would never fire. # Skips cleanly when the WINGET_TOKEN secret (classic PAT, public_repo # scope — used to fork/PR winget-pkgs as the owner) isn't configured. # NOTE: winget-releaser updates manifests relative to the PREVIOUS version # already in winget-pkgs master, so this only works once the initial # manifest PR (microsoft/winget-pkgs#404984) is merged. publish-winget: needs: publish-release if: startsWith(github.ref, 'refs/tags/v') runs-on: windows-latest env: WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} steps: - name: Compute version from tag id: ver shell: bash run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - name: Submit winget manifest PR if: env.WINGET_TOKEN != '' uses: vedantmgoyal9/winget-releaser@v2 with: identifier: CylCastillo.AgentConsole version: ${{ steps.ver.outputs.version }} release-tag: ${{ github.ref_name }} installers-regex: '_x64-setup\.exe$' token: ${{ secrets.WINGET_TOKEN }} - name: Skipped (no WINGET_TOKEN) if: env.WINGET_TOKEN == '' shell: bash run: echo "WINGET_TOKEN secret not set — skipping winget submission." # Publish/update the AUR package (agent-console-bin) AFTER the release is # live — the PKGBUILD downloads the published .deb. Same in-workflow # placement rationale as publish-winget (GITHUB_TOKEN events don't trigger # other workflows). The action runs makepkg --printsrcinfo + updpkgsums in # an Arch container, so version AND checksums are derived fresh each time. # Skips cleanly without the AUR_SSH_PRIVATE_KEY secret. publish-aur: needs: publish-release if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-22.04 env: AUR_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} steps: - uses: actions/checkout@v4 - name: Set pkgver from tag shell: bash run: | set -euo pipefail sed -i "s/^pkgver=.*/pkgver=${GITHUB_REF_NAME#v}/" packaging/aur/PKGBUILD grep '^pkgver=' packaging/aur/PKGBUILD - name: Publish to AUR if: env.AUR_KEY != '' uses: KSXGitHub/github-actions-deploy-aur@v3 with: pkgname: agent-console-bin pkgbuild: packaging/aur/PKGBUILD updpkgsums: true commit_username: cyl-castillo commit_email: cmcastillochacon91@gmail.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: Update to ${{ github.ref_name }} - name: Skipped (no AUR_SSH_PRIVATE_KEY) if: env.AUR_KEY == '' shell: bash run: echo "AUR_SSH_PRIVATE_KEY secret not set — skipping AUR publish." # Publish the npm launcher (launcher/) AFTER the release is live — the launcher # fetches those assets at runtime, so it must not go out ahead of them. Only # runs for real version tags; skips cleanly if the version is already on npm # (e.g. a re-cut tag). publish-launcher: needs: publish-release if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 registry-url: 'https://registry.npmjs.org' - name: Publish launcher to npm working-directory: launcher env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set -euo pipefail # No token configured → skip cleanly instead of failing the release. # The npm launcher is an optional install path (npx); the platform # release assets above are unaffected by it. if [ -z "${NODE_AUTH_TOKEN:-}" ]; then echo "NPM_TOKEN secret not set — skipping launcher publish." exit 0 fi # The published version always matches the release tag, so the launcher # fetches the assets from its own release. The committed version is just # a dev default. VERSION="${GITHUB_REF_NAME#v}" npm pkg set version="$VERSION" NAME="$(npm pkg get name | tr -d '"')" # `npm view pkg@version version` prints the version when published and # nothing (exit 0) when that version is absent — so test for output, # not exit code. EXISTING="$(npm view "$NAME@$VERSION" version 2>/dev/null || true)" if [ -n "$EXISTING" ]; then echo "$NAME@$VERSION already on npm — skipping publish." else npm publish --access public fi