-
-
Notifications
You must be signed in to change notification settings - Fork 0
805 lines (680 loc) · 31.1 KB
/
release.yml
File metadata and controls
805 lines (680 loc) · 31.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
# Release Workflow
#
# This workflow automates the release process when semantic version tags are pushed.
# It reuses build artifacts from the build workflow (must merge to main first), validates
# the version, creates GitHub releases, and publishes to package managers.
#
# IMPORTANT: Tags must be on commits that have been merged to main and built by build.yml
#
# Triggers: Push of semantic version tag (e.g., v1.2.3)
# Performance Target: ≤10 minutes total (excluding manual approval)
# Artifacts: Reuses build artifacts, creates release packages
#
# Manual Testing on PR:
# 1. Run Build workflow manually on your PR branch first
# 2. Note the commit SHA from the build run
# 3. Use Actions tab → Release → Run workflow
# - Select your PR branch
# - Enter test version (e.g., v0.0.1-test)
# - Check 'Skip Homebrew' to avoid tap updates
# - Check 'Dry run' to skip actual release creation
# 4. Review logs to verify workflow logic
#
# Production Release:
# 1. Merge PR to main → triggers build.yml automatically
# 2. Wait for build to complete successfully
# 3. Tag the merge commit: git tag v1.0.0 && git push origin v1.0.0
# 4. Release workflow triggers automatically
name: Release
on:
push:
tags:
- 'v*.*.*' # Semantic version tags only (e.g., v1.2.3, v0.1.0)
workflow_dispatch: # Allow manual triggering for testing
inputs:
tag:
description: 'Version tag to release (e.g., v0.0.1-test for testing)'
required: true
type: string
skip_homebrew:
description: 'Skip Homebrew publication (for PR testing)'
required: false
type: boolean
default: false
skip_winget:
description: 'Skip Winget publication (set to false once winget package is live)'
required: false
type: boolean
default: true # TODO: Change to false once winget PR is merged
dry_run:
description: 'Dry run - skip release creation (logs only)'
required: false
type: boolean
default: false
# Prevent concurrent releases to avoid conflicts
# Do not cancel in-progress releases as they involve publishing to external services
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Workflow-level permissions
permissions:
contents: write # Required for creating releases and reading repo
actions: read # Required for downloading artifacts from build workflow
packages: write # Required for GitHub Packages (bottles)
issues: write # Required for creating Winget/Chocolatey publication issues
jobs:
# Job 1: Validate Version
# Extract/validate version, ensure tag is on main
# Performance Target: <1 minute
validate-version:
name: Validate Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.extract.outputs.version }}
commit-sha: ${{ steps.validate.outputs.commit-sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for release notes
- name: Extract version from tag
id: extract
run: |
# Get tag name (either from push event or manual input)
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ github.event.inputs.tag }}"
else
TAG_NAME="${GITHUB_REF#refs/tags/}"
fi
# Remove 'v' prefix to get version
VERSION="${TAG_NAME#v}"
echo "📦 Tag: $TAG_NAME"
echo "🔢 Version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Validate semantic version format
run: |
VERSION="${{ steps.extract.outputs.version }}"
# Allow semver with optional pre-release/build metadata
# Valid formats: 1.2.3, 1.2.3-beta.1, 1.2.3+build.123, 1.2.3-rc.1+build.456
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'; then
echo "❌ Error: Invalid semantic version format: $VERSION"
echo "Expected format: MAJOR.MINOR.PATCH with optional pre-release and build metadata"
echo "Examples: 1.2.3, 0.1.0-beta.1, 1.0.0-rc.1+build.123"
exit 1
fi
echo "✅ Valid version format: $VERSION"
- name: Validate tag is on main branch
id: validate
run: |
# Get the commit SHA for this tag
COMMIT_SHA=$(git rev-list -n 1 ${{ github.ref }})
echo "commit-sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "📍 Tag commit: $COMMIT_SHA"
# For manual testing, allow any branch
# For production tag push, enforce main branch only
if [ "${{ github.event_name }}" = "push" ]; then
# Check if this commit is on main branch
if ! git branch -r --contains $COMMIT_SHA | grep -q "origin/main"; then
echo "❌ Error: Tag must be on a commit that exists on main branch"
echo "Please merge to main first, then tag the merge commit"
exit 1
fi
echo "✅ Tag is on main branch"
else
echo "⚠️ Manual dispatch: Skipping main branch check"
echo "✅ Running on branch: ${{ github.ref_name }}"
fi
- name: Check for duplicate version in releases
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ steps.extract.outputs.version }}"
# Check if this version already exists as a release
if gh release view "v$VERSION" &>/dev/null; then
# For test versions, just warn instead of failing
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && echo "$VERSION" | grep -q -- "-"; then
echo "⚠️ Warning: Release v$VERSION already exists (test version)"
echo "💡 Consider using a different test version or deleting the existing one"
else
echo "❌ Error: Release v$VERSION already exists"
echo "Please use a new version number"
exit 1
fi
else
echo "✅ Version v$VERSION is unique"
fi
# Job 2: Build Release Artifacts
# Rebuild binaries with release version embedded (not reusing dev builds)
# Performance Target: ≤10 minutes (parallel execution)
build-release-artifacts:
name: Build ${{ matrix.platform }} Release
runs-on: ${{ matrix.os }}
needs: [validate-version]
strategy:
matrix:
include:
- os: macos-latest
platform: osx-arm64
runtime: osx-arm64
executable: tom
- os: windows-latest
platform: win-x64
runtime: win-x64
executable: tom.exe
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.validate-version.outputs.commit-sha }}
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build macOS Extension
if: startsWith(matrix.platform, 'osx-')
shell: bash
run: |
echo "🔨 Building macOS Extension for ${{ matrix.platform }}"
chmod +x src/Extensions/MacOS/build.sh
cd src/Extensions/MacOS && ./build.sh
# Verify extension was built
if [ ! -f ../../../bin/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier ]; then
echo "❌ Error: Extension binary not found after build"
exit 1
fi
echo "✅ Extension built successfully"
ls -la ../../../bin/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/
- name: Publish ${{ matrix.platform }} executable with release version
shell: bash
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
echo "📦 Building ${{ matrix.platform }} with version: ${VERSION}"
dotnet publish src/TenSecondTom.csproj \
--configuration Release \
--runtime ${{ matrix.runtime }} \
--self-contained true \
--output ./publish \
-p:PublishSingleFile=true \
-p:Version=${VERSION}
- name: Verify Extension in Publish Output
if: startsWith(matrix.platform, 'osx-')
shell: bash
run: |
EXTENSION_PATH="./publish/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier"
if [ ! -f "$EXTENSION_PATH" ]; then
echo "❌ Error: Extension not found in publish output at $EXTENSION_PATH"
echo "Contents of publish directory:"
find ./publish -type f
exit 1
fi
echo "✅ Extension verified in publish directory"
ls -lh "$EXTENSION_PATH"
- name: Verify artifact size
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
SIZE=$(stat -c%s ./publish/${{ matrix.executable }})
else
SIZE=$(stat -f%z ./publish/${{ matrix.executable }})
fi
SIZE_MB=$((SIZE / 1024 / 1024))
echo "📦 Executable size: ${SIZE_MB}MB"
# Size limit accounts for:
# - Main binary (~28MB)
# - Microsoft AI Foundry Local SDK (~194MB)
# - Whisper.NET platform runtime (~5-100MB depending on CUDA/CoreML)
# Total expected: ~230-330MB
if [ $SIZE -gt 367001600 ]; then
echo "❌ Error: Executable exceeds 350MB limit (${SIZE_MB}MB)"
exit 1
fi
echo "✅ Size check passed: ${SIZE_MB}MB"
- name: Calculate checksum
shell: bash
run: |
cd ./publish
if [ "${{ runner.os }}" = "Windows" ]; then
sha256sum ${{ matrix.executable }} > ${{ matrix.executable }}.sha256
else
shasum -a 256 ${{ matrix.executable }} > ${{ matrix.executable }}.sha256
fi
echo "📝 SHA256:"
cat ${{ matrix.executable }}.sha256
- name: Smoke test - version command
shell: bash
run: |
if [ "${{ runner.os }}" != "Windows" ]; then
chmod +x ./publish/${{ matrix.executable }}
fi
echo "🧪 Running smoke test: --version"
./publish/${{ matrix.executable }} --version
if [ $? -eq 0 ]; then
echo "✅ Smoke test passed"
else
echo "❌ Smoke test failed"
exit 1
fi
- name: List artifact contents
shell: bash
run: |
echo "📦 Contents of publish directory:"
find ./publish -type f -exec ls -lh {} \; | head -50
echo ""
echo "📊 Total size:"
du -sh ./publish/
echo ""
echo "📊 Size by type:"
du -sh ./publish/*.dylib 2>/dev/null || true
du -sh ./publish/*.dll 2>/dev/null || true
du -sh ./publish/runtimes 2>/dev/null || true
- name: Build Windows Installer
if: runner.os == 'Windows'
shell: pwsh
run: |
$VERSION = "${{ needs.validate-version.outputs.version }}"
Write-Host "📦 Building Windows installer for version $VERSION"
# Create output directory
New-Item -ItemType Directory -Force -Path ".\artifacts\installer" | Out-Null
# Download and install Inno Setup
Write-Host "⬇️ Installing Inno Setup..."
$innoUrl = "https://jrsoftware.org/download.php/is.exe"
$innoInstaller = "$env:TEMP\innosetup.exe"
Invoke-WebRequest -Uri $innoUrl -OutFile $innoInstaller
Start-Process -FilePath $innoInstaller -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART" -Wait
# Add Inno Setup to PATH
$innoPath = "${env:ProgramFiles(x86)}\Inno Setup 6"
$env:PATH = "$innoPath;$env:PATH"
# Build installer
Write-Host "🔨 Compiling installer..."
& "$innoPath\ISCC.exe" /DMyAppVersion=$VERSION `
/O".\artifacts\installer" `
/F"ten-second-tom-$VERSION-win-x64-setup" `
".\installer\windows\installer.iss"
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Installer build failed"
exit 1
}
Write-Host "✅ Installer built successfully"
Get-ChildItem ".\artifacts\installer" | Format-Table Name, Length
- name: Calculate installer checksum
if: runner.os == 'Windows'
shell: bash
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
INSTALLER="./artifacts/installer/ten-second-tom-${VERSION}-win-x64-setup.exe"
if [ -f "$INSTALLER" ]; then
sha256sum "$INSTALLER" > "${INSTALLER}.sha256"
echo "📝 Installer SHA256:"
cat "${INSTALLER}.sha256"
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ten-second-tom-${{ matrix.platform }}-release
path: |
./publish/${{ matrix.executable }}
./publish/appsettings*.json
./publish/*.dylib
./publish/*.dll
./publish/*.metal
./publish/runtimes/**/*
./publish/TenSecondTom.Extensions.MacOS.app/**/*
retention-days: 90
- name: Upload Windows installer
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ten-second-tom-win-x64-installer
path: ./artifacts/installer/*.exe
retention-days: 90
# Job 3: Create GitHub Release
# Generate release notes and create GitHub release with all binaries
# Performance Target: ≤2 minutes
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [validate-version, build-release-artifacts]
outputs:
release-id: ${{ steps.create-release.outputs.id }}
upload-url: ${{ steps.create-release.outputs.upload_url }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for release notes
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: ten-second-tom-*-release
path: ./artifacts
- name: Download Windows installer
uses: actions/download-artifact@v4
with:
name: ten-second-tom-win-x64-installer
path: ./artifacts/installer
- name: Package artifacts for distribution
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
echo "📦 Creating distribution packages for version ${VERSION}"
mkdir -p ./release-packages
# Package macOS ARM64
echo "Packaging macOS ARM64..."
cd ./artifacts/ten-second-tom-osx-arm64-release
tar czf ../../release-packages/ten-second-tom-${VERSION}-osx-arm64.tar.gz *
cd ../..
# Package Windows x64 ZIP (for manual install)
echo "Packaging Windows x64 ZIP..."
cd ./artifacts/ten-second-tom-win-x64-release
# Exclude Linux runtime files from Windows package
zip -r ../../release-packages/ten-second-tom-${VERSION}-win-x64.zip * -x "runtimes/cuda/linux-x64/*"
cd ../..
# Copy Windows installer
echo "Copying Windows installer..."
cp ./artifacts/installer/*.exe ./release-packages/
echo "✅ Distribution packages created:"
ls -lh ./release-packages/
- name: Generate release notes
id: release-notes
run: |
VERSION="v${{ needs.validate-version.outputs.version }}"
# Get previous tag for changelog
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "📝 First release - no previous tag found"
NOTES="## 🎉 Initial Release
This is the first release of Ten Second Tom.
### 📦 Downloads
Choose the appropriate package for your platform:
- **macOS (Apple Silicon)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz\`
- **Windows (Installer)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64-setup.exe\` ⭐ Recommended
- **Windows (Portable)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64.zip\`
### 🚀 Installation
**macOS (Apple Silicon):**
\`\`\`bash
tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz
chmod +x tom
./tom --version
\`\`\`
**Windows (Recommended):**
Run the installer (\`*-setup.exe\`). It will add \`tom\` to your PATH automatically.
**Windows (Portable):**
Extract the zip file and run \`tom.exe\` from the extracted folder."
else
echo "📝 Generating changelog from $PREV_TAG to $VERSION"
# Generate commit log
COMMITS=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges)
NOTES="## 📋 Changes
$COMMITS
### 📦 Downloads
Choose the appropriate package for your platform:
- **macOS (Apple Silicon)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz\`
- **Windows (Installer)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64-setup.exe\` ⭐ Recommended
- **Windows (Portable)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64.zip\`
### 🚀 Installation
**macOS (Apple Silicon):**
\`\`\`bash
tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz
chmod +x tom
./tom --version
\`\`\`
**Windows (Recommended):**
Run the installer (\`*-setup.exe\`). It will add \`tom\` to your PATH automatically.
**Windows (Portable):**
Extract the zip file and run \`tom.exe\` from the extracted folder."
fi
# Save notes to file (handle multiline)
echo "$NOTES" > release-notes.md
cat release-notes.md
- name: Create GitHub Release
id: create-release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="v${{ needs.validate-version.outputs.version }}"
# Check if this is a dry run
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "🧪 DRY RUN - Would create release $VERSION"
echo "📝 Release notes:"
cat release-notes.md
echo "⏭️ Skipping actual release creation"
exit 0
fi
# Create release with notes
gh release create "$VERSION" \
--title "Release $VERSION" \
--notes-file release-notes.md \
--draft=false \
--latest
echo "✅ GitHub release created: $VERSION"
# Check if this is a dry run
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "🧪 DRY RUN - Skipping artifact uploads"
exit 0
fi
# Upload all distribution packages
echo "📤 Uploading distribution packages..."
gh release upload "$VERSION" \
./release-packages/* \
--clobber
echo "✅ All distribution packages uploaded"
# Job 4: Publish to Homebrew with Bottles
# Create Homebrew bottles and upload to GitHub Packages, then update tap formula
# Performance Target: ≤7 minutes
# Requires: HOMEBREW_TAP_TOKEN secret configured in 'production' environment
# Note: VS Code may show "'production' is not valid" - this is a false positive
# Can be skipped with workflow_dispatch skip_homebrew input for PR testing
publish-homebrew:
name: Publish to Homebrew
runs-on: macos-latest # Changed to macOS for bottle creation
needs: [validate-version, create-github-release]
if: ${{ github.event.inputs.skip_homebrew != 'true' && github.event.inputs.dry_run != 'true' }}
environment:
name: production # Configured in GitHub repository settings
permissions:
contents: write # Required for uploading bottles to release
packages: write # Required for GitHub Packages (bottles)
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: ten-second-tom-*-release
path: ./artifacts
- name: Create Homebrew bottles
id: bottles
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
REPO_OWNER="${{ github.repository_owner }}"
echo "🍾 Creating Homebrew bottles for version $VERSION"
# Determine macOS version for bottle tag
MACOS_VERSION=$(sw_vers -productVersion | cut -d '.' -f 1)
case $MACOS_VERSION in
12) MACOS_TAG="monterey" ;;
13) MACOS_TAG="ventura" ;;
14) MACOS_TAG="sonoma" ;;
15) MACOS_TAG="sequoia" ;;
*) MACOS_TAG="monterey" ;; # Default fallback
esac
echo "📦 Building bottles for macOS $MACOS_TAG (both architectures)"
# Create ARM64 bottle
BOTTLE_TAG_ARM64="arm64_${MACOS_TAG}"
BOTTLE_NAME_ARM64="ten-second-tom-${VERSION}.${BOTTLE_TAG_ARM64}.bottle.tar.gz"
BOTTLE_DIR_ARM64="ten-second-tom/${VERSION}/bin"
mkdir -p "$BOTTLE_DIR_ARM64"
cp "./artifacts/ten-second-tom-osx-arm64-release/tom" "$BOTTLE_DIR_ARM64/tom"
cp ./artifacts/ten-second-tom-osx-arm64-release/appsettings*.json "$BOTTLE_DIR_ARM64/" 2>/dev/null || true
cp ./artifacts/ten-second-tom-osx-arm64-release/*.dylib "$BOTTLE_DIR_ARM64/" 2>/dev/null || true
cp ./artifacts/ten-second-tom-osx-arm64-release/*.metal "$BOTTLE_DIR_ARM64/" 2>/dev/null || true
# Copy Whisper.NET CoreML runtimes
if [ -d "./artifacts/ten-second-tom-osx-arm64-release/runtimes" ]; then
cp -R "./artifacts/ten-second-tom-osx-arm64-release/runtimes" "$BOTTLE_DIR_ARM64/"
fi
# Copy macOS extension to bottle (in prefix, not bin)
EXTENSION_DIR_ARM64="ten-second-tom/${VERSION}"
cp -R "./artifacts/ten-second-tom-osx-arm64-release/TenSecondTom.Extensions.MacOS.app" "$EXTENSION_DIR_ARM64/" 2>/dev/null || true
# Ensure executables have proper permissions
chmod +x "$BOTTLE_DIR_ARM64/tom"
chmod +x "$EXTENSION_DIR_ARM64/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier" 2>/dev/null || true
tar czf "$BOTTLE_NAME_ARM64" ten-second-tom
BOTTLE_SHA_ARM64=$(shasum -a 256 "$BOTTLE_NAME_ARM64" | cut -d ' ' -f 1)
echo "✅ ARM64 bottle: $BOTTLE_NAME_ARM64"
echo " SHA256: $BOTTLE_SHA_ARM64"
# Output variables for later steps
echo "bottle-name-arm64=$BOTTLE_NAME_ARM64" >> $GITHUB_OUTPUT
echo "bottle-tag-arm64=$BOTTLE_TAG_ARM64" >> $GITHUB_OUTPUT
echo "bottle-sha-arm64=$BOTTLE_SHA_ARM64" >> $GITHUB_OUTPUT
- name: Upload bottles to GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
BOTTLE_NAME_ARM64="${{ steps.bottles.outputs.bottle-name-arm64 }}"
echo "📤 Uploading bottle to GitHub release"
# Upload ARM64 bottle to GitHub release
gh release upload "v${VERSION}" "$BOTTLE_NAME_ARM64" --clobber
echo "✅ Bottle uploaded to release assets"
echo "📦 ARM64: https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${BOTTLE_NAME_ARM64}"
- name: Generate Homebrew formula with bottle blocks
id: formula
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
BOTTLE_TAG_ARM64="${{ steps.bottles.outputs.bottle-tag-arm64 }}"
BOTTLE_SHA_ARM64="${{ steps.bottles.outputs.bottle-sha-arm64 }}"
echo "📝 Generating Homebrew formula for version $VERSION with ARM64 bottle"
# Download source tarball to calculate SHA256
SOURCE_TARBALL_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/v${VERSION}.tar.gz"
curl -sSL "$SOURCE_TARBALL_URL" -o source.tar.gz
SOURCE_SHA=$(shasum -a 256 source.tar.gz | cut -d ' ' -f 1)
echo "Source tarball SHA256: $SOURCE_SHA"
# Create formula file with ARM64 bottle only
{
echo "class TenSecondTom < Formula"
echo " desc \"CLI tool for daily work summaries using Claude AI with voice entry support\""
echo " homepage \"https://github.com/${REPO_OWNER}/${REPO_NAME}\""
echo " url \"https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/v${VERSION}.tar.gz\""
echo " sha256 \"${SOURCE_SHA}\""
echo " license \"MIT\""
echo ""
echo " # Bottle (pre-built binary) for Apple Silicon"
echo " bottle do"
echo " root_url \"https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}\""
echo " sha256 cellar: :any_skip_relocation, ${BOTTLE_TAG_ARM64}: \"${BOTTLE_SHA_ARM64}\""
echo " end"
echo ""
echo " # Dependencies for voice entry feature"
echo " depends_on \"ffmpeg\""
echo ""
echo " def install"
echo " bin.install \"tom\""
echo " # Install native macOS extension for notifications"
echo " prefix.install \"TenSecondTom.Extensions.MacOS.app\" if OS.mac?"
echo " # Ensure notifier binary is executable"
echo " chmod 0755, prefix/\"TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier\" if OS.mac?"
echo " end"
echo ""
echo " def caveats"
echo " <<~EOS"
echo " Legal: Ten Second Tom is designed for single-user personal use on your own"
echo " device. Recording conversations may require consent in your jurisdiction."
echo " EOS"
echo " end"
echo ""
echo " test do"
echo " system \"#{bin}/tom\", \"--version\""
echo " end"
echo "end"
} > ten-second-tom.rb
cat ten-second-tom.rb
- name: Validate formula syntax
run: |
echo "✅ Formula syntax validation (basic check)"
# Basic syntax validation - full validation requires Homebrew installation
if ! grep -q "class TenSecondTom" ten-second-tom.rb; then
echo "❌ Error: Invalid formula structure"
exit 1
fi
# Check that source URL contains the version
if ! grep -q "archive/refs/tags/v${{ needs.validate-version.outputs.version }}.tar.gz" ten-second-tom.rb; then
echo "❌ Error: Version not found in source URL"
exit 1
fi
# Check that source has SHA256
if ! grep -q "sha256 \"" ten-second-tom.rb; then
echo "❌ Error: Source SHA256 not found in formula"
exit 1
fi
if ! grep -q "bottle do" ten-second-tom.rb; then
echo "❌ Error: Bottle block not found in formula"
exit 1
fi
echo "✅ Basic formula validation passed (including bottle block)"
- name: Push formula to Homebrew tap
env:
# Note: This secret is configured in the 'production' environment
# Go to Settings → Environments → production → Environment secrets
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
if [ -z "$TAP_TOKEN" ]; then
echo "❌ Error: HOMEBREW_TAP_TOKEN secret not configured"
echo "Please configure the secret in the 'production' environment"
echo "Settings → Environments → production → Environment secrets"
exit 1
fi
VERSION="${{ needs.validate-version.outputs.version }}"
REPO_OWNER="${{ github.repository_owner }}"
TAP_REPO="homebrew-ten-second-tom"
echo "📤 Pushing formula with bottles to ${REPO_OWNER}/${TAP_REPO}"
# Clone tap repository
git clone "https://x-access-token:${TAP_TOKEN}@github.com/${REPO_OWNER}/${TAP_REPO}.git" tap-repo
cd tap-repo
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Copy formula
mkdir -p Formula
cp ../ten-second-tom.rb Formula/ten-second-tom.rb
# Commit and push
git add Formula/ten-second-tom.rb
git commit -m "Release version ${VERSION} with bottles"
git push origin main
echo "✅ Formula with bottle support published to Homebrew tap"
# Job 5: Publish to Winget
# Automatically submit package update to Windows Package Manager
# Uses winget-releaser action with Komac under the hood
# Performance Target: <2 minutes
#
# Prerequisites:
# 1. WINGET_TOKEN secret configured in 'production' environment
# - Classic PAT with 'public_repo' scope
# - Create at: https://github.com/settings/tokens/new
# 2. Fork of microsoft/winget-pkgs under same account/org
# 3. First version must be submitted manually to winget-pkgs
#
# First-time setup:
# 1. Fork https://github.com/microsoft/winget-pkgs
# 2. Manually submit first version using wingetcreate:
# wingetcreate new https://github.com/sirkirby/ten-second-tom/releases/download/v1.0.0/ten-second-tom-1.0.0-win-x64.zip
# 3. After first version is merged, automation handles all future updates
#
# TODO: Remove 'false &&' once SirKirby.TenSecondTom PR is merged into winget-pkgs
publish-winget:
name: Publish to Winget
runs-on: ubuntu-latest
needs: [validate-version, create-github-release]
if: ${{ false && github.event.inputs.skip_winget != 'true' && github.event.inputs.dry_run != 'true' }}
environment:
name: production
steps:
- name: Publish to Winget
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: SirKirby.TenSecondTom
installers-regex: '\.zip$' # Match our Windows zip package
token: ${{ secrets.WINGET_TOKEN }}
# Optional: specify fork user if different from repo owner
# fork-user: sirkirby