Skip to content

chore: release 2.2.7 #96

chore: release 2.2.7

chore: release 2.2.7 #96

Workflow file for this run

# 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