Skip to content

Commit fa59edc

Browse files
authored
ci: add automated nightly build workflow (#86)
- Runs daily at midnight UTC from latest main branch - Only keeps 1 nightly pre-release active at a time (deletes previous) - Generates categorized release notes (features, fixes, UI, maintenance) - Shows all changes since the last stable tag release - Includes clear disclaimer about instability (intended for devs/testers) - Links to the latest stable release for production users - Supports manual trigger via workflow_dispatch - Skips build entirely if no new commits since last tag - Produces DMG + ZIP artifacts with SHA-256 checksums
1 parent b8a1b78 commit fa59edc

1 file changed

Lines changed: 295 additions & 0 deletions

File tree

.github/workflows/nightly.yml

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
name: Nightly Build
2+
3+
on:
4+
schedule:
5+
# Run every day at midnight UTC
6+
- cron: '0 0 * * *'
7+
workflow_dispatch:
8+
# Allow manual trigger for testing
9+
10+
permissions:
11+
contents: write
12+
13+
concurrency:
14+
group: nightly-build
15+
cancel-in-progress: true
16+
17+
jobs:
18+
check-for-changes:
19+
name: Check for New Commits
20+
runs-on: ubuntu-latest
21+
outputs:
22+
has_changes: ${{ steps.check.outputs.has_changes }}
23+
latest_tag: ${{ steps.check.outputs.latest_tag }}
24+
changelog: ${{ steps.changelog.outputs.changelog }}
25+
commit_count: ${{ steps.check.outputs.commit_count }}
26+
short_sha: ${{ steps.check.outputs.short_sha }}
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
fetch-tags: true
34+
35+
- name: Check for changes since last tag
36+
id: check
37+
run: |
38+
# Find the latest stable release tag (vX.Y.Z format)
39+
LATEST_TAG=$(git tag --sort=-version:refname --list 'v*' | head -1)
40+
echo "latest_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
41+
echo "📌 Latest stable tag: ${LATEST_TAG}"
42+
43+
SHORT_SHA=$(git rev-parse --short HEAD)
44+
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
45+
46+
if [ -z "$LATEST_TAG" ]; then
47+
echo "No tags found — treating all commits as new"
48+
COMMIT_COUNT=$(git rev-list --count HEAD)
49+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
50+
echo "commit_count=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT"
51+
else
52+
# Count commits since last tag
53+
COMMIT_COUNT=$(git rev-list --count "${LATEST_TAG}..HEAD")
54+
echo "commit_count=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT"
55+
56+
if [ "$COMMIT_COUNT" -eq 0 ]; then
57+
echo "✅ No new commits since ${LATEST_TAG} — skipping nightly build"
58+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
59+
else
60+
echo "🔄 Found ${COMMIT_COUNT} new commit(s) since ${LATEST_TAG}"
61+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
62+
fi
63+
fi
64+
65+
- name: Generate changelog
66+
id: changelog
67+
if: steps.check.outputs.has_changes == 'true'
68+
run: |
69+
LATEST_TAG="${{ steps.check.outputs.latest_tag }}"
70+
71+
# Build changelog from commits since last tag
72+
if [ -z "$LATEST_TAG" ]; then
73+
RANGE="HEAD"
74+
else
75+
RANGE="${LATEST_TAG}..HEAD"
76+
fi
77+
78+
# Generate categorized changelog
79+
{
80+
echo "changelog<<CHANGELOG_EOF"
81+
82+
# Features
83+
FEATS=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^feat" --no-merges 2>/dev/null || true)
84+
if [ -n "$FEATS" ]; then
85+
echo "### ✨ New Features"
86+
echo "$FEATS"
87+
echo ""
88+
fi
89+
90+
# Fixes
91+
FIXES=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^fix" --no-merges 2>/dev/null || true)
92+
if [ -n "$FIXES" ]; then
93+
echo "### 🐛 Bug Fixes"
94+
echo "$FIXES"
95+
echo ""
96+
fi
97+
98+
# UI changes
99+
UI=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^ui" --no-merges 2>/dev/null || true)
100+
if [ -n "$UI" ]; then
101+
echo "### 🎨 UI Improvements"
102+
echo "$UI"
103+
echo ""
104+
fi
105+
106+
# CI/Chore/Docs
107+
OTHER=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^chore\|^ci\|^docs\|^refactor\|^perf\|^test" --no-merges 2>/dev/null || true)
108+
if [ -n "$OTHER" ]; then
109+
echo "### 🔧 Maintenance & Other"
110+
echo "$OTHER"
111+
echo ""
112+
fi
113+
114+
# Uncategorized (commits not matching conventional commit prefixes)
115+
ALL_CATEGORIZED=$(git log "$RANGE" --pretty=format:"%h" --grep="^feat\|^fix\|^ui\|^chore\|^ci\|^docs\|^refactor\|^perf\|^test" --no-merges 2>/dev/null | sort || true)
116+
ALL_COMMITS=$(git log "$RANGE" --pretty=format:"%h" --no-merges 2>/dev/null | sort || true)
117+
UNCATEGORIZED_HASHES=$(comm -23 <(echo "$ALL_COMMITS") <(echo "$ALL_CATEGORIZED") 2>/dev/null || true)
118+
119+
if [ -n "$UNCATEGORIZED_HASHES" ]; then
120+
UNCATEGORIZED=""
121+
while IFS= read -r hash; do
122+
[ -z "$hash" ] && continue
123+
MSG=$(git log -1 --pretty=format:"- %s (%h)" "$hash" 2>/dev/null || true)
124+
UNCATEGORIZED="${UNCATEGORIZED}${MSG}"$'\n'
125+
done <<< "$UNCATEGORIZED_HASHES"
126+
if [ -n "$(echo "$UNCATEGORIZED" | tr -d '[:space:]')" ]; then
127+
echo "### 📝 Other Changes"
128+
echo "$UNCATEGORIZED"
129+
echo ""
130+
fi
131+
fi
132+
133+
echo "CHANGELOG_EOF"
134+
} >> "$GITHUB_OUTPUT"
135+
136+
build-nightly:
137+
name: Build Nightly
138+
needs: check-for-changes
139+
if: needs.check-for-changes.outputs.has_changes == 'true'
140+
runs-on: macos-15
141+
timeout-minutes: 30
142+
143+
steps:
144+
- name: Checkout
145+
uses: actions/checkout@v4
146+
147+
- name: Cache SPM dependencies
148+
uses: actions/cache@v4
149+
with:
150+
path: .build
151+
key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
152+
restore-keys: |
153+
spm-${{ runner.os }}-
154+
155+
- name: Run tests
156+
run: swift test
157+
158+
- name: Initialize Xcode tools
159+
run: sudo xcodebuild -runFirstLaunch
160+
161+
- name: Build release app bundle
162+
run: ./scripts/build.sh release
163+
164+
- name: Verify code signature
165+
run: codesign -v --deep --strict VocaMac.app
166+
167+
- name: Create DMG
168+
run: |
169+
NIGHTLY_DATE=$(date -u +%Y%m%d)
170+
SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}"
171+
DMG_NAME="VocaMac-nightly-${NIGHTLY_DATE}-${SHORT_SHA}-arm64.dmg"
172+
173+
mkdir -p dmg-staging
174+
cp -R VocaMac.app dmg-staging/
175+
ln -s /Applications dmg-staging/Applications
176+
177+
hdiutil create -volname "VocaMac Nightly" \
178+
-srcfolder dmg-staging \
179+
-ov -format UDZO \
180+
"$DMG_NAME"
181+
182+
echo "DMG_NAME=${DMG_NAME}" >> "$GITHUB_ENV"
183+
echo "DMG created:"
184+
ls -lh "$DMG_NAME"
185+
186+
- name: Create ZIP archive
187+
run: |
188+
NIGHTLY_DATE=$(date -u +%Y%m%d)
189+
SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}"
190+
ZIP_NAME="VocaMac-nightly-${NIGHTLY_DATE}-${SHORT_SHA}-arm64.zip"
191+
192+
ditto -c -k --sequesterRsrc --keepParent VocaMac.app "$ZIP_NAME"
193+
194+
echo "ZIP_NAME=${ZIP_NAME}" >> "$GITHUB_ENV"
195+
echo "ZIP created:"
196+
ls -lh "$ZIP_NAME"
197+
198+
- name: Generate checksums
199+
run: |
200+
shasum -a 256 VocaMac-nightly-*.dmg VocaMac-nightly-*.zip > checksums.txt
201+
cat checksums.txt
202+
203+
- name: Delete existing nightly release
204+
env:
205+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
206+
run: |
207+
# Delete the existing nightly release (if any) to keep only one active
208+
echo "🗑️ Checking for existing nightly release..."
209+
if gh release view nightly &>/dev/null; then
210+
echo " Found existing nightly release — deleting..."
211+
gh release delete nightly --yes --cleanup-tag
212+
echo " ✅ Old nightly release deleted"
213+
else
214+
echo " No existing nightly release found"
215+
fi
216+
217+
- name: Create nightly release
218+
env:
219+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220+
CHANGELOG: ${{ needs.check-for-changes.outputs.changelog }}
221+
run: |
222+
LATEST_TAG="${{ needs.check-for-changes.outputs.latest_tag }}"
223+
COMMIT_COUNT="${{ needs.check-for-changes.outputs.commit_count }}"
224+
SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}"
225+
NIGHTLY_DATE=$(date -u +%Y-%m-%d)
226+
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
227+
228+
# Build the stable release link
229+
if [ -n "$LATEST_TAG" ]; then
230+
STABLE_LINK="[**${LATEST_TAG}**](${REPO_URL}/releases/tag/${LATEST_TAG})"
231+
else
232+
STABLE_LINK="_No stable release available yet._"
233+
fi
234+
235+
# Write release notes to a file (avoids heredoc indentation issues)
236+
{
237+
echo "## ⚠️ Nightly Build — Not for Production Use"
238+
echo ""
239+
echo "> **This is an automated nightly build from the latest \`main\` branch.**"
240+
echo "> It is intended for **developers and testers** who want to try the latest changes"
241+
echo "> before they are included in a stable release."
242+
echo ">"
243+
echo "> **This build may be unstable, contain bugs, or include incomplete features.**"
244+
echo "> Do not use it as your daily driver. If you encounter issues, please"
245+
echo "> [report them](${REPO_URL}/issues/new) — your feedback helps us improve!"
246+
echo ""
247+
echo "### 🏷️ Latest Stable Release"
248+
echo ""
249+
echo "👉 For a reliable experience, use the latest stable release: ${STABLE_LINK}"
250+
echo ""
251+
echo "---"
252+
echo ""
253+
echo "### 📋 What's Changed Since ${LATEST_TAG:-the beginning}"
254+
echo ""
255+
echo "_${COMMIT_COUNT} commit(s) since the last stable release (${LATEST_TAG:-N/A}) • built from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${SHORT_SHA})_"
256+
echo ""
257+
echo "$CHANGELOG"
258+
echo ""
259+
echo "---"
260+
echo ""
261+
echo "### 📥 Installation"
262+
echo ""
263+
echo "1. Download \`${DMG_NAME}\`"
264+
echo "2. Open the DMG and drag VocaMac to Applications"
265+
echo "3. **Important:** Remove the quarantine attribute before launching:"
266+
echo " \`\`\`bash"
267+
echo " xattr -cr /Applications/VocaMac.app"
268+
echo " \`\`\`"
269+
echo "4. Open VocaMac from Applications"
270+
echo "5. Grant Microphone, Accessibility, and Input Monitoring permissions when prompted"
271+
echo ""
272+
echo "> **Why step 3?** macOS Gatekeeper blocks apps that aren't notarized with Apple."
273+
echo "> Since VocaMac is ad-hoc signed (no Apple Developer certificate yet), you need to"
274+
echo "> clear the quarantine flag manually."
275+
echo ""
276+
echo "### Checksums (SHA-256)"
277+
echo "\`\`\`"
278+
cat checksums.txt
279+
echo "\`\`\`"
280+
echo ""
281+
echo "### Requirements"
282+
echo "- macOS 13 (Ventura) or later"
283+
echo "- Apple Silicon (arm64)"
284+
} > release-notes.md
285+
286+
gh release create nightly \
287+
--title "🌙 Nightly Build — ${NIGHTLY_DATE}" \
288+
--notes-file release-notes.md \
289+
--prerelease \
290+
--target main \
291+
VocaMac-nightly-*.dmg \
292+
VocaMac-nightly-*.zip \
293+
checksums.txt
294+
295+
echo "✅ Nightly release created!"

0 commit comments

Comments
 (0)