Repository navigation
chore: release 2.2.7 #96
Workflow file for this run
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
| # Build Stroke for Linux, Windows, and macOS and publish to GitHub Releases. | |
| # | |
| # Releases, the updater's latest.json and the Scoop bucket all live in THIS | |
| # repo. Builds shipped before the move to the stroke-app org point at | |
| # github.com/broisnischal/stroke, which GitHub redirects here. That redirect is | |
| # what keeps their updater working, so that repo name must never be reused. | |
| # | |
| # Create a release: | |
| # git tag v0.1.0 | |
| # git push origin v0.1.0 | |
| # | |
| # Or run manually: Actions → Release → Run workflow | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Release tag (must match tauri.conf.json version, e.g. v0.1.0) | |
| required: true | |
| default: v0.1.0 | |
| permissions: | |
| contents: write | |
| env: | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} | |
| # This repo hosts the release assets, latest.json and the Scoop bucket. It is | |
| # named `stroke` because that name is baked into every shipped updater | |
| # endpoint and package-manager URL, and those cannot be changed retroactively. | |
| RELEASES_REPO: ${{ github.repository }} | |
| # ── 1. Create the release as a DRAFT before builds ─────────────────────────── | |
| # A draft is private and unannounced, so users never see an empty/partial | |
| # release. Builds attach their assets to the draft; the final publish-release | |
| # job un-drafts it only after every build + updater JSON + scoop succeeds. | |
| # If any build fails, it stays a draft (delete & re-tag to retry). | |
| jobs: | |
| create-release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Create GitHub release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| PRERELEASE="" | |
| [[ "$TAG" == *-* ]] && PRERELEASE="--prerelease" | |
| # Extract changelog for this version from CHANGELOG.md. | |
| # Grabs everything between the matching "## [X.Y.Z]" header and the next one. | |
| # VERSION goes in the environment: it used to trail the script as | |
| # `VERSION=...`, which node receives as an argument, so the script | |
| # looked for "## [undefined]" and every release said "No changelog". | |
| export VERSION="${TAG#v}" | |
| CHANGELOG=$(node -e " | |
| const fs = require('fs') | |
| const lines = fs.readFileSync('CHANGELOG.md', 'utf8').split('\n') | |
| const header = '## [' + process.env.VERSION + ']' | |
| let inside = false | |
| const out = [] | |
| for (const l of lines) { | |
| if (l.startsWith(header)) { inside = true; continue } | |
| if (inside && /^## \[/.test(l)) break | |
| if (inside) out.push(l) | |
| } | |
| process.stdout.write(out.join('\n').trim()) | |
| ") | |
| cat > /tmp/notes.md << EOF | |
| ${CHANGELOG:-No changelog available for this release.} | |
| --- | |
| ### Install | |
| **Package managers (no security warnings):** | |
| \`\`\`sh | |
| # macOS | |
| brew install --cask stroke-app/tap/stroke | |
| # Windows | |
| scoop bucket add stroke https://github.com/stroke-app/stroke | |
| scoop install stroke | |
| \`\`\` | |
| **Or download directly below:** | |
| **macOS:** \`stroke_*_aarch64.dmg\` (Apple Silicon) · \`stroke_*_x64.dmg\` (Intel) | |
| > Ad-hoc signed. If macOS says it's "damaged", run once: \`xattr -cr /Applications/Stroke.app\` | |
| **Windows:** \`stroke_*_x64-setup.exe\` (recommended) · \`.msi\` | |
| > Unsigned. On SmartScreen, click **More info**, then **Run anyway**. | |
| **Linux:** \`.deb\` · \`.rpm\` · \`.AppImage\` | |
| Built from \`${SHA}\`. | |
| EOF | |
| # Idempotent: a failed prior run can leave a draft behind, so drop an | |
| # existing release before creating this one. | |
| # | |
| # NOT --cleanup-tag. That was safe when releases lived in a separate | |
| # repo whose tags were throwaway pointers at its default branch; here | |
| # the tag IS the source tag that triggered this run, and deleting it | |
| # would destroy the only record of which commit shipped. A re-run | |
| # would then re-create it at whatever master had moved on to. | |
| if gh release view "$TAG" --repo "$RELEASES_REPO" >/dev/null 2>&1; then | |
| echo "Existing $TAG release found — deleting it for a clean rebuild." | |
| gh release delete "$TAG" --repo "$RELEASES_REPO" --yes | |
| fi | |
| # --target pins the release to the commit this run built. Ignored when | |
| # the tag already exists (the tag-push path); it matters for a manual | |
| # workflow_dispatch of a tag that hasn't been created yet. | |
| gh release create "$TAG" \ | |
| --repo "$RELEASES_REPO" \ | |
| --target "$SHA" \ | |
| --title "Stroke $TAG" \ | |
| --notes-file /tmp/notes.md \ | |
| --draft \ | |
| $PRERELEASE | |
| # ── 2. Build on each platform ──────────────────────────────────────────────── | |
| build: | |
| needs: create-release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: ubuntu-22.04 | |
| name: linux | |
| args: '' | |
| # Pin to windows-2022 (stable VS 2022 / MSVC 14.4x). windows-latest now | |
| # redirects to a preview VS 2026 image (MSVC 14.51) that fails to compile | |
| # DuckDB (libduckdb-sys, cc-rs exit code 2). VS 2022 builds it reliably. | |
| - platform: windows-2022 | |
| name: windows | |
| args: '' | |
| - platform: macos-latest | |
| name: macos-aarch64 | |
| args: '--target aarch64-apple-darwin' | |
| - platform: macos-latest | |
| name: macos-x64 | |
| args: '--target x86_64-apple-darwin' | |
| runs-on: ${{ matrix.platform }} | |
| name: build-${{ matrix.name }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ startsWith(matrix.name, 'macos') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} | |
| - name: Install Linux system dependencies | |
| if: matrix.platform == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| build-essential | |
| - run: npm ci | |
| # ── macOS: ad-hoc signing only (signingIdentity "-" in tauri.conf.json). | |
| # No Apple Developer cert / notarization. Users run `xattr -cr` once. | |
| # See install notes in the create-release step. | |
| # ── Windows: unsigned. A self-signed cert gives no benefit (SmartScreen | |
| # still blocks it), so we don't sign. Users either click | |
| # "More info → Run anyway" on the direct .exe, or install warning-free | |
| # via Scoop (`scoop install stroke`). To enable real signing later, add | |
| # an EV cert / Azure Trusted Signing step here and set the thumbprint. | |
| - name: Build | |
| run: npx tauri build ${{ matrix.args }} | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| NO_STRIP: ${{ matrix.name == 'linux' && '1' || '' }} | |
| # macOS: ad-hoc signed only — no APPLE_CERTIFICATE, so Tauri skips the | |
| # PKCS12 import and notarization. Re-add the Apple secrets here to | |
| # restore full signing once a valid cert + password are available. | |
| # ── macOS aarch64 ─────────────────────────────────────────────────────── | |
| - name: Upload macOS aarch64 artifacts | |
| if: matrix.name == 'macos-aarch64' | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") | |
| DIR="src-tauri/target/aarch64-apple-darwin/release/bundle" | |
| pushd "$DIR/macos" | |
| tar czf "Stroke.app.tar.gz" "Stroke.app" | |
| popd | |
| npx tauri signer sign "$DIR/macos/Stroke.app.tar.gz" | |
| cp "$DIR/macos/Stroke.app.tar.gz" stroke_aarch64.app.tar.gz | |
| cp "$DIR/macos/Stroke.app.tar.gz.sig" updater.sig | |
| gh release upload "$TAG" --repo "$RELEASES_REPO" \ | |
| "$DIR/dmg/stroke_${VERSION}_aarch64.dmg" \ | |
| stroke_aarch64.app.tar.gz \ | |
| --clobber | |
| # ── macOS x64 ─────────────────────────────────────────────────────────── | |
| - name: Upload macOS x64 artifacts | |
| if: matrix.name == 'macos-x64' | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") | |
| DIR="src-tauri/target/x86_64-apple-darwin/release/bundle" | |
| pushd "$DIR/macos" | |
| tar czf "Stroke.app.tar.gz" "Stroke.app" | |
| popd | |
| npx tauri signer sign "$DIR/macos/Stroke.app.tar.gz" | |
| cp "$DIR/macos/Stroke.app.tar.gz" stroke_x64.app.tar.gz | |
| cp "$DIR/macos/Stroke.app.tar.gz.sig" updater.sig | |
| gh release upload "$TAG" --repo "$RELEASES_REPO" \ | |
| "$DIR/dmg/stroke_${VERSION}_x64.dmg" \ | |
| stroke_x64.app.tar.gz \ | |
| --clobber | |
| # ── Linux ─────────────────────────────────────────────────────────────── | |
| - name: Upload Linux artifacts | |
| if: matrix.name == 'linux' | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") | |
| DIR="src-tauri/target/release/bundle" | |
| # Tauri names Linux bundles after the product name ("Stroke", capital), | |
| # so glob the actual files instead of assuming a case, then normalize to | |
| # lowercase stroke_* asset names for predictable download URLs. | |
| DEB=$(ls "$DIR"/deb/*.deb | head -1) | |
| RPM=$(ls "$DIR"/rpm/*.rpm | head -1) | |
| APP=$(ls "$DIR"/appimage/*.AppImage | head -1) | |
| cp "$DEB" "stroke_${VERSION}_amd64.deb" | |
| cp "$RPM" "stroke-${VERSION}-1.x86_64.rpm" | |
| cp "$APP" "stroke_${VERSION}_amd64.AppImage" | |
| # Sign the AppImage for the auto-updater. | |
| tar czf "stroke_${VERSION}_amd64.AppImage.tar.gz" "stroke_${VERSION}_amd64.AppImage" | |
| npx tauri signer sign "stroke_${VERSION}_amd64.AppImage.tar.gz" | |
| cp "stroke_${VERSION}_amd64.AppImage.tar.gz" stroke_amd64.AppImage.tar.gz | |
| cp "stroke_${VERSION}_amd64.AppImage.tar.gz.sig" updater.sig | |
| gh release upload "$TAG" --repo "$RELEASES_REPO" \ | |
| "stroke_${VERSION}_amd64.deb" \ | |
| "stroke-${VERSION}-1.x86_64.rpm" \ | |
| "stroke_${VERSION}_amd64.AppImage" \ | |
| stroke_amd64.AppImage.tar.gz \ | |
| --clobber | |
| # ── Windows ───────────────────────────────────────────────────────────── | |
| - name: Upload Windows artifacts | |
| if: matrix.name == 'windows' | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| $version = (Get-Content src-tauri/tauri.conf.json | ConvertFrom-Json).version | |
| $dir = "src-tauri/target/release/bundle" | |
| $exe = "$dir/nsis/stroke_${version}_x64-setup.exe" | |
| $zip = "$dir/nsis/stroke_${version}_x64-setup.nsis.zip" | |
| # Use NoCompression (Store = method 0) — universally supported by every | |
| # ZIP implementation including Tauri's zip crate. | |
| # Compress-Archive → Deflate64 (method 9) ❌ not supported by Tauri | |
| # CompressionLevel::Optimal on newer .NET → may vary ❌ unreliable | |
| # CompressionLevel::NoCompression → Store (method 0) ✅ always works | |
| Add-Type -AssemblyName System.IO.Compression.FileSystem | |
| $stream = [System.IO.Compression.ZipFile]::Open($zip, 'Create') | |
| [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( | |
| $stream, | |
| $exe, | |
| [System.IO.Path]::GetFileName($exe), | |
| [System.IO.Compression.CompressionLevel]::NoCompression | |
| ) | Out-Null | |
| $stream.Dispose() | |
| # Verify the ZIP uses Store (method 0) — fail fast if something is wrong | |
| $verify = [System.IO.Compression.ZipFile]::OpenRead($zip) | |
| $entry = $verify.Entries[0] | |
| $method = [int]$entry.CompressionMethod | |
| $verify.Dispose() | |
| Write-Host "ZIP entry: $($entry.Name) compression method: $method" | |
| if ($method -ne 0) { | |
| Write-Error "Wrong compression method $method — expected 0 (Store/NoCompression)" | |
| exit 1 | |
| } | |
| Write-Host "✅ Compression method verified: Store (0) — Tauri compatible" | |
| npx tauri signer sign $zip | |
| Copy-Item $zip "stroke_x64-setup.nsis.zip" | |
| Copy-Item "$zip.sig" "updater.sig" | |
| gh release upload "$env:TAG" --repo "$env:RELEASES_REPO" ` | |
| "$dir/msi/stroke_${version}_x64_en-US.msi" ` | |
| "$dir/nsis/stroke_${version}_x64-setup.exe" ` | |
| "stroke_x64-setup.nsis.zip" ` | |
| --clobber | |
| - name: Save updater sig as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sig-${{ matrix.name }} | |
| path: updater.sig | |
| if-no-files-found: warn | |
| # ── 3. Combine signatures into latest.json ─────────────────────────────────── | |
| publish-updater-json: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Download all sig artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: sig-* | |
| - name: Generate and upload latest.json | |
| env: | |
| TAG: ${{ env.RELEASE_TAG }} | |
| run: | | |
| export VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") | |
| export BASE="https://github.com/${RELEASES_REPO}/releases/download/${TAG}" | |
| node - <<'JSEOF' | |
| const fs = require('fs') | |
| const { VERSION, BASE } = process.env | |
| const read = (f) => { try { return fs.readFileSync(f, 'utf8').trim() } catch { return '' } } | |
| // Extract this version's changelog section from CHANGELOG.md | |
| function extractChangelog(version) { | |
| try { | |
| const lines = fs.readFileSync('CHANGELOG.md', 'utf8').split('\n') | |
| const header = `## [${version}]` | |
| let inside = false | |
| const out = [] | |
| for (const l of lines) { | |
| if (l.startsWith(header)) { inside = true; continue } | |
| if (inside && /^## \[/.test(l)) break | |
| if (inside) out.push(l) | |
| } | |
| return out.join('\n').trim() | |
| } catch { return '' } | |
| } | |
| const platforms = {} | |
| const add = (key, sigFile, url) => { | |
| const sig = read(sigFile) | |
| if (sig) platforms[key] = { signature: sig, url } | |
| else console.warn(`⚠ no sig at ${sigFile} — skipping ${key}`) | |
| } | |
| add('darwin-aarch64', 'sig-macos-aarch64/updater.sig', `${BASE}/stroke_aarch64.app.tar.gz`) | |
| add('darwin-x86_64', 'sig-macos-x64/updater.sig', `${BASE}/stroke_x64.app.tar.gz`) | |
| add('linux-x86_64', 'sig-linux/updater.sig', `${BASE}/stroke_amd64.AppImage.tar.gz`) | |
| add('windows-x86_64', 'sig-windows/updater.sig', `${BASE}/stroke_x64-setup.nsis.zip`) | |
| const notes = extractChangelog(VERSION) || `See https://github.com/stroke-app/stroke/releases/tag/v${VERSION}` | |
| const out = { | |
| version: VERSION, | |
| notes, | |
| pub_date: new Date().toISOString(), | |
| platforms, | |
| } | |
| fs.writeFileSync('latest.json', JSON.stringify(out, null, 2)) | |
| console.log(JSON.stringify(out, null, 2)) | |
| if (!Object.keys(platforms).length) { console.error('ERROR: no signatures found'); process.exit(1) } | |
| JSEOF | |
| - name: Upload latest.json | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| run: gh release upload "$TAG" --repo "$RELEASES_REPO" latest.json --clobber | |
| # ── 4. Refresh the Scoop manifest ──────────────────────────────────────────── | |
| # `scoop bucket add stroke https://github.com/stroke-app/stroke` clones | |
| # THIS repo anonymously and reads bucket/stroke.json, so the manifest is | |
| # committed straight to the default branch. | |
| update-scoop: | |
| # Runs AFTER publish-release: it downloads the installer from the public | |
| # releases/download/ URL to hash it, which 404s while the release is a draft. | |
| needs: publish-release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| - name: Update bucket/stroke.json | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| EXE="stroke_${VERSION}_x64-setup.exe" | |
| URL="https://github.com/stroke-app/stroke/releases/download/${TAG}/${EXE}" | |
| # Hash the published installer so the manifest is always accurate. | |
| curl -fsSL -o "$EXE" "$URL" | |
| HASH=$(sha256sum "$EXE" | cut -d' ' -f1) | |
| rm -f "$EXE" | |
| VERSION="$VERSION" HASH="$HASH" node - <<'JSEOF' | |
| const fs = require('fs') | |
| const { VERSION, HASH } = process.env | |
| const manifest = { | |
| version: VERSION, | |
| description: 'Fast desktop database client for PostgreSQL, MySQL, SQLite, and Cloudflare D1', | |
| homepage: 'https://github.com/stroke-app/stroke', | |
| license: 'Freeware', | |
| architecture: { | |
| '64bit': { | |
| url: `https://github.com/stroke-app/stroke/releases/download/v${VERSION}/stroke_${VERSION}_x64-setup.exe#/dl.7z`, | |
| hash: HASH, | |
| }, | |
| }, | |
| bin: 'Stroke.exe', | |
| shortcuts: [['Stroke.exe', 'Stroke']], | |
| checkver: 'github', | |
| autoupdate: { | |
| architecture: { | |
| '64bit': { url: 'https://github.com/stroke-app/stroke/releases/download/v$version/stroke_$version_x64-setup.exe#/dl.7z' }, | |
| }, | |
| }, | |
| } | |
| fs.mkdirSync('bucket', { recursive: true }) | |
| fs.writeFileSync('bucket/stroke.json', JSON.stringify(manifest, null, 2) + '\n') | |
| JSEOF | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add bucket/stroke.json | |
| if git diff --cached --quiet; then | |
| echo "Scoop manifest already current." | |
| else | |
| git commit -m "chore(scoop): stroke v${VERSION}" | |
| git push origin HEAD | |
| fi | |
| # Optional instant Homebrew tap refresh. Without a PAT this is skipped and | |
| # the tap updates itself on its own schedule instead (see homebrew-tap repo). | |
| # (secrets can't be used in an `if:`, so the check lives in the script.) | |
| - name: Trigger Homebrew tap update | |
| env: | |
| GH_TOKEN: ${{ secrets.TAP_DISPATCH_TOKEN }} | |
| run: | | |
| if [ -z "$GH_TOKEN" ]; then | |
| echo "No TAP_DISPATCH_TOKEN — tap will self-update on its schedule." | |
| exit 0 | |
| fi | |
| gh workflow run update-cask.yml -R stroke-app/homebrew-tap || \ | |
| echo "Tap dispatch failed — tap will self-update on schedule." | |
| # ── 5. Publish the release (un-draft) — ONLY after everything succeeded ─────── | |
| # This is the gate the user wants: no public release until all platform | |
| # builds, the updater JSON, and the Scoop manifest are done. If any of those | |
| # jobs fail, this job is skipped and the release stays a draft. | |
| publish-release: | |
| needs: [build, publish-updater-json] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Un-draft the release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ env.RELEASE_TAG }} | |
| run: gh release edit "$TAG" --repo "$RELEASES_REPO" --draft=false |