Skip to content

Commit fca0e21

Browse files
ndbroadbentclaude
andcommitted
Add GitHub Actions release workflow
- Create release.yml triggered by vite.config.ts vitest.config.ts tags - Add wait-for-checks.js to wait for CI before building - Build unsigned macOS (.dmg) and Windows (.exe) installers - Use tauri-apps/tauri-action for cross-platform builds - Generate SHA256 checksums for all artifacts - Include installation instructions in release notes - Update biome.json to include .github files in formatting Phase 2 of distribution: unsigned builds ready for testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 528a09c commit fca0e21

3 files changed

Lines changed: 291 additions & 1 deletion

File tree

.github/wait-for-checks.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Wait for GitHub Actions checks to complete before proceeding with release
3+
*
4+
* @param {Object} params
5+
* @param {Object} params.github - GitHub API object
6+
* @param {Object} params.context - GitHub context
7+
* @param {Object} params.core - GitHub Actions core
8+
* @param {Array<string>} params.checks - Optional array of check names to wait for
9+
*/
10+
11+
const DEFAULT_REQUIRED_CHECKS = [
12+
'Lint & Test', // from ci.yml - main check job
13+
'Build' // from ci.yml - build verification job
14+
]
15+
16+
const TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes overall timeout
17+
const WARMUP_MS = 5 * 60 * 1000 // 5 minutes for checks to appear
18+
const POLL_INTERVAL_MS = 10 * 1000 // 10 seconds between polls
19+
20+
module.exports = async ({ github, context, core, checks }) => {
21+
const REQUIRED_CHECKS = checks || DEFAULT_REQUIRED_CHECKS
22+
23+
if (checks) {
24+
core.info(`Waiting for specific checks: ${checks.join(', ')}`)
25+
} else {
26+
core.info('Waiting for all default required checks')
27+
}
28+
29+
function sleep(ms) {
30+
return new Promise((resolve) => setTimeout(resolve, ms))
31+
}
32+
33+
async function listChecks() {
34+
const { data } = await github.rest.checks.listForRef({
35+
owner: context.repo.owner,
36+
repo: context.repo.repo,
37+
ref: context.sha,
38+
per_page: 100
39+
})
40+
return data.check_runs.map((c) => ({
41+
name: c.name,
42+
status: c.status, // queued, in_progress, completed
43+
conclusion: c.conclusion // success, failure, neutral, cancelled, etc
44+
}))
45+
}
46+
47+
function matchesRequired(name) {
48+
return REQUIRED_CHECKS.some((check) => name === check || name.startsWith(check))
49+
}
50+
51+
// Phase 1: Discover which required checks are actually present for this SHA
52+
core.info('Discovering present checks...')
53+
let presentChecks = []
54+
const warmupStart = Date.now()
55+
56+
while (Date.now() - warmupStart < WARMUP_MS) {
57+
const allChecks = await listChecks()
58+
presentChecks = allChecks.filter((c) => matchesRequired(c.name))
59+
60+
if (presentChecks.length > 0) {
61+
core.info(
62+
`Found ${presentChecks.length} required check(s): ${presentChecks
63+
.map((c) => c.name)
64+
.join(', ')}`
65+
)
66+
break
67+
}
68+
69+
core.info(
70+
`No required checks found yet, waiting... (${Math.round(
71+
(Date.now() - warmupStart) / 1000
72+
)}s elapsed)`
73+
)
74+
await sleep(5000)
75+
}
76+
77+
if (presentChecks.length === 0) {
78+
core.info('No required checks present on this commit - continuing without waiting.')
79+
core.info('This is normal if CI was triggered by a different event or already completed.')
80+
return
81+
}
82+
83+
// Phase 2: Wait for all present checks to complete
84+
core.info(`Waiting for ${presentChecks.length} check(s) to complete...`)
85+
const waitStart = Date.now()
86+
87+
while (Date.now() - waitStart < TIMEOUT_MS) {
88+
const allChecks = await listChecks()
89+
const relevant = allChecks.filter((c) => matchesRequired(c.name))
90+
91+
// If checks disappeared (canceled?), keep waiting within timeout
92+
if (relevant.length === 0) {
93+
core.warning('Required checks disappeared - they may have been canceled')
94+
await sleep(POLL_INTERVAL_MS)
95+
continue
96+
}
97+
98+
const pending = relevant.filter((c) => c.status !== 'completed')
99+
100+
if (pending.length === 0) {
101+
// All checks completed - check their conclusions
102+
const failed = relevant.filter(
103+
(c) => c.conclusion !== 'success' && c.conclusion !== 'skipped'
104+
)
105+
106+
if (failed.length > 0) {
107+
const failureDetails = failed.map((f) => `${f.name} (${f.conclusion})`).join(', ')
108+
core.setFailed(`Some required checks failed: ${failureDetails}`)
109+
process.exit(1)
110+
}
111+
112+
const successful = relevant.filter((c) => c.conclusion === 'success')
113+
core.info(`✅ All ${successful.length} required check(s) passed successfully!`)
114+
return
115+
}
116+
117+
// Still waiting
118+
const elapsed = Math.round((Date.now() - waitStart) / 1000)
119+
const pendingDetails = pending.map((p) => `${p.name} (${p.status})`).join(', ')
120+
core.info(`[${elapsed}s] Waiting for: ${pendingDetails}`)
121+
122+
await sleep(POLL_INTERVAL_MS)
123+
}
124+
125+
// Timeout reached
126+
core.setFailed(`Timeout after ${TIMEOUT_MS / 1000}s waiting for checks to complete`)
127+
process.exit(1)
128+
}

.github/workflows/release.yml

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
checks: read
12+
13+
jobs:
14+
wait-for-ci:
15+
name: Wait for CI
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
fetch-tags: true
23+
24+
- name: Wait for CI checks
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const script = require('./.github/wait-for-checks.js');
29+
await script({ github, context, core });
30+
31+
build:
32+
name: Build ${{ matrix.platform }}
33+
needs: wait-for-ci
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
include:
38+
# macOS builds (Intel + Apple Silicon universal binary)
39+
- platform: macos-latest
40+
target: universal-apple-darwin
41+
args: "--target universal-apple-darwin"
42+
name: macos
43+
44+
# Windows build
45+
- platform: windows-latest
46+
target: x86_64-pc-windows-msvc
47+
args: ""
48+
name: windows
49+
50+
runs-on: ${{ matrix.platform }}
51+
steps:
52+
- name: Checkout repository
53+
uses: actions/checkout@v4
54+
with:
55+
fetch-depth: 0
56+
fetch-tags: true
57+
58+
- name: Install Bun
59+
uses: oven-sh/setup-bun@v2
60+
with:
61+
bun-version: latest
62+
63+
- name: Install Rust
64+
uses: dtolnay/rust-toolchain@stable
65+
with:
66+
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
67+
68+
- name: Cache Cargo
69+
uses: actions/cache@v4
70+
with:
71+
path: |
72+
~/.cargo/bin/
73+
~/.cargo/registry/index/
74+
~/.cargo/registry/cache/
75+
~/.cargo/git/db/
76+
src-tauri/target/
77+
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
78+
restore-keys: ${{ runner.os }}-cargo-release-
79+
80+
- name: Cache Bun
81+
uses: actions/cache@v4
82+
with:
83+
path: ~/.bun/install/cache
84+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
85+
restore-keys: ${{ runner.os }}-bun-
86+
87+
- name: Install dependencies
88+
run: bun install
89+
90+
- name: Build Tauri app
91+
uses: tauri-apps/tauri-action@v0
92+
env:
93+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94+
# Disable signing for now (Phase 2 - unsigned builds)
95+
TAURI_SIGNING_PRIVATE_KEY: ""
96+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
97+
with:
98+
tagName: ${{ github.ref_name }}
99+
releaseName: "ChatToMap v__VERSION__"
100+
releaseBody: |
101+
## ChatToMap Desktop v__VERSION__
102+
103+
Export your iMessage history and discover all the places you've talked about!
104+
105+
### Installation
106+
107+
**macOS:**
108+
1. Download `ChatToMap_*.dmg`
109+
2. Open the DMG and drag ChatToMap to Applications
110+
3. Right-click and select "Open" the first time (required for unsigned apps)
111+
112+
**Windows:**
113+
1. Download `ChatToMap_*_x64-setup.exe`
114+
2. Run the installer
115+
3. You may need to click "More info" → "Run anyway" (unsigned app warning)
116+
117+
---
118+
**Note:** These are unsigned builds. Code signing and notarization coming in a future release.
119+
releaseDraft: false
120+
prerelease: ${{ contains(github.ref_name, '-') }}
121+
args: ${{ matrix.args }}
122+
123+
- name: Generate checksums (macOS)
124+
if: matrix.platform == 'macos-latest'
125+
run: |
126+
cd src-tauri/target/universal-apple-darwin/release/bundle/dmg
127+
for file in *.dmg; do
128+
shasum -a 256 "$file" > "${file}.sha256"
129+
echo "Generated checksum for $file"
130+
done
131+
132+
- name: Generate checksums (Windows)
133+
if: matrix.platform == 'windows-latest'
134+
shell: pwsh
135+
run: |
136+
$files = Get-ChildItem -Path "src-tauri/target/release/bundle/nsis" -Filter "*.exe"
137+
foreach ($file in $files) {
138+
$hash = Get-FileHash -Path $file.FullName -Algorithm SHA256
139+
"$($hash.Hash.ToLower()) $($file.Name)" | Out-File -FilePath "$($file.FullName).sha256" -Encoding ASCII
140+
Write-Host "Generated checksum for $($file.Name)"
141+
}
142+
143+
- name: Upload checksums (macOS)
144+
if: matrix.platform == 'macos-latest'
145+
uses: softprops/action-gh-release@v2
146+
with:
147+
files: src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.sha256
148+
tag_name: ${{ github.ref_name }}
149+
150+
- name: Upload checksums (Windows)
151+
if: matrix.platform == 'windows-latest'
152+
uses: softprops/action-gh-release@v2
153+
with:
154+
files: src-tauri/target/release/bundle/nsis/*.sha256
155+
tag_name: ${{ github.ref_name }}

biome.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
},
88
"files": {
99
"ignoreUnknown": true,
10-
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx", "*.json"],
10+
"include": [
11+
"src/**/*.ts",
12+
"src/**/*.tsx",
13+
"src/**/*.js",
14+
"src/**/*.jsx",
15+
".github/**/*.js",
16+
"*.json"
17+
],
1118
"ignore": ["target/", "dist/", "node_modules/", "src-tauri/gen/"]
1219
},
1320
"formatter": {

0 commit comments

Comments
 (0)