Skip to content

Commit 35323dd

Browse files
committed
feat(updates): Implement Development Channel system for pre-release testing
**Problem:** SAM needed a way to release features faster to willing testers while keeping the stable channel reliable for production users. No mechanism existed for users to opt into pre-release builds. **Solution:** Implemented complete Development Channel system with dual appcast feeds, UI toggle, automated workflows, and comprehensive documentation. **Components Implemented:** 1. Appcast Infrastructure: - appcast-dev.xml (development + stable releases) - appcast-dev-items.xml (hand-maintained dev items) 2. Preferences UI: - Development updates toggle in General preferences - Confirmation dialog warning about instability - Preference saved to UserDefaults (developmentUpdatesEnabled) 3. Feed Switching: - AppcastURLs.swift (centralized feed URL management) - AppDelegate dynamic feed switching based on preference - NotificationCenter integration for live preference changes 4. Build Scripts: - scripts/increment-dev-version.sh (auto-version with -dev.N suffix) - scripts/generate-dev-appcast.sh (merges dev + stable items) 5. Makefile Targets: - make build-dev (build with version increment) - make release-dev (guide for creating releases) - make appcast-dev (generate merged appcast) 6. Documentation: - VERSIONING.md (development version format and comparison) - BUILDING.md (development build workflow) - README.md (Development Program section) 7. GitHub Workflow: - .github/workflows/nightly-dev.yml (automated nightly builds) - Checks for changes before building - Keeps last 7 development releases - Manual trigger supported **Version Format:** - Stable: YYYYMMDD.RELEASE (e.g., 20260110.1) - Development: YYYYMMDD.RELEASE-dev.BUILD (e.g., 20260110.1-dev.1) **Version Comparison:** - 20260110.1-dev.1 < 20260110.1-dev.2 < 20260110.1 (stable) - Development users get dev builds, then upgrade to stable - Stable users never see dev builds **Testing:** ✅ Build: PASS (all phases compile successfully) ✅ Scripts: increment-dev-version.sh tested and working ✅ Scripts: generate-dev-appcast.sh tested and working ✅ Makefile: appcast-dev target verified ✅ XML: All appcast files validated **Files Changed:** - Sources/ConfigurationSystem/AppcastURLs.swift (NEW) - Sources/SAM/AppDelegate.swift (feed switching) - Sources/UserInterface/PreferencesView.swift (UI toggle) - scripts/increment-dev-version.sh (NEW) - scripts/generate-dev-appcast.sh (NEW) - appcast-dev.xml (NEW) - appcast-dev-items.xml (NEW) - .github/workflows/nightly-dev.yml (NEW) - Makefile (build-dev, release-dev, appcast-dev targets) - VERSIONING.md (development section) - BUILDING.md (development workflow) - README.md (Development Program section)
1 parent d54a59b commit 35323dd

13 files changed

Lines changed: 842 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,24 @@ logger.debug("Detail: \(value)")
216216
- Validate inputs, don't assume
217217

218218
#### Commits
219+
220+
**CRITICAL: NEVER COMMIT HANDOFF DOCUMENTATION**
221+
222+
**NEVER commit these files:**
223+
- `ai-assisted/**/*` - ALL handoff documentation stays LOCAL ONLY
224+
- `CONTINUATION_PROMPT.md` - Session handoffs
225+
- `AGENT_PLAN.md` - Task breakdowns
226+
- Any file in `ai-assisted/` directory
227+
228+
**Why:** Handoff documentation contains internal context, work notes, and session details
229+
that should NEVER be in the public repository. This is a HARD REQUIREMENT.
230+
231+
**Before EVERY commit:**
232+
1. Run `git status` and verify no `ai-assisted/` files are staged
233+
2. If any handoff files appear, use `git reset HEAD ai-assisted/` to unstage them
234+
3. ONLY commit actual code, documentation, and configuration changes
235+
236+
**Standard Commit Format:**
219237
```bash
220238
git add -A && git commit -m "type(scope): description
221239
@@ -485,6 +503,13 @@ ls -lt ai-assisted/ | head -20
485503
❌ Create handoffs without complete context
486504
❌ Leave documentation out of sync with code
487505

506+
### Git/Commit Violations ⚠️ CRITICAL
507+
**Commit handoff documentation to repository** (ai-assisted/ files - SESSION FAILURE)
508+
**Push CONTINUATION_PROMPT.md or AGENT_PLAN.md to GitHub** (violates security)
509+
**Stage any file in ai-assisted/ directory** (contains internal context)
510+
❌ Skip checking `git status` before commit
511+
❌ Force push without verifying what's being pushed
512+
488513
---
489514

490515
## REMEMBER

.github/workflows/nightly-dev.yml

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
name: Nightly Development Release
2+
3+
# Automated nightly builds for development channel testing
4+
# Runs at 2 AM UTC if there are changes since last development release
5+
# Keeps last 7 development releases, deletes older ones
6+
# Uses self-hosted runner like the release workflow
7+
8+
on:
9+
schedule:
10+
# Run every day at 2 AM UTC if there are changes
11+
- cron: '0 2 * * *'
12+
workflow_dispatch:
13+
# Allow manual triggering
14+
15+
env:
16+
XCODE_VERSION: '26.2'
17+
MACOS_VERSION: '14.0'
18+
19+
permissions:
20+
contents: write
21+
22+
jobs:
23+
check-changes:
24+
name: Check for Changes
25+
runs-on: ubuntu-latest
26+
outputs:
27+
should_build: ${{ steps.check.outputs.has_changes }}
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Check for changes since last dev release
35+
id: check
36+
run: |
37+
# Get the latest development release tag
38+
LATEST_DEV_TAG=$(git tag -l '*-dev.*' --sort=-version:refname | head -1)
39+
40+
if [ -z "$LATEST_DEV_TAG" ]; then
41+
echo "No previous development releases found"
42+
echo "has_changes=true" >> $GITHUB_OUTPUT
43+
exit 0
44+
fi
45+
46+
# Check if there are commits since the last dev release
47+
COMMITS_SINCE=$(git rev-list ${LATEST_DEV_TAG}..HEAD --count)
48+
49+
if [ "$COMMITS_SINCE" -gt 0 ]; then
50+
echo "Found $COMMITS_SINCE commits since $LATEST_DEV_TAG"
51+
echo "has_changes=true" >> $GITHUB_OUTPUT
52+
else
53+
echo "No changes since $LATEST_DEV_TAG"
54+
echo "has_changes=false" >> $GITHUB_OUTPUT
55+
fi
56+
57+
build-and-release:
58+
name: Build, Sign, and Release
59+
needs: check-changes
60+
if: needs.check-changes.outputs.should_build == 'true'
61+
runs-on: [self-hosted]
62+
63+
steps:
64+
- name: Checkout code
65+
uses: actions/checkout@v4
66+
with:
67+
fetch-depth: 0
68+
submodules: recursive
69+
70+
- name: Set up Xcode
71+
uses: maxim-lobanov/setup-xcode@v1
72+
with:
73+
xcode-version: ${{ env.XCODE_VERSION }}
74+
75+
- name: Download Metal Toolchain
76+
run: |
77+
echo "Downloading Metal Toolchain for Xcode ${{ env.XCODE_VERSION }}..."
78+
sudo xcodebuild -downloadComponent MetalToolchain || echo "Metal Toolchain already installed or download failed"
79+
80+
- name: Increment development version
81+
run: |
82+
./scripts/increment-dev-version.sh
83+
DEV_VERSION=$(cat Info.plist | grep -A1 CFBundleShortVersionString | tail -1 | sed 's/.*<string>\(.*\)<\/string>/\1/')
84+
echo "DEV_VERSION=$DEV_VERSION" >> $GITHUB_ENV
85+
echo "Building version: $DEV_VERSION"
86+
87+
- name: Build development release
88+
run: make build-release
89+
90+
- name: Validate Python Bundle
91+
run: ./scripts/validate_python_bundle.sh Release
92+
93+
- name: Sign and notarize
94+
run: |
95+
if [ -f /Users/andrew/.sam_runner_env ]; then
96+
source /Users/andrew/.sam_runner_env
97+
fi
98+
make sign-only-release
99+
100+
- name: Verify distribution files
101+
run: |
102+
if [ ! -f "dist/SAM-${DEV_VERSION}.dmg" ]; then
103+
echo "ERROR: DMG not found"
104+
exit 1
105+
fi
106+
if [ ! -f "dist/SAM-${DEV_VERSION}.zip" ]; then
107+
echo "ERROR: ZIP not found"
108+
exit 1
109+
fi
110+
echo "Distribution files ready:"
111+
ls -lh dist/SAM-${DEV_VERSION}.*
112+
113+
- name: Create GitHub pre-release
114+
uses: softprops/action-gh-release@v1
115+
with:
116+
tag_name: ${{ env.DEV_VERSION }}
117+
name: SAM ${{ env.DEV_VERSION }} (Development)
118+
body: |
119+
⚠️ **This is a DEVELOPMENT pre-release build**
120+
121+
Development builds are released automatically and may contain:
122+
- Bugs and instability
123+
- Incomplete features
124+
- Breaking changes
125+
126+
**Do not use development builds for production work.**
127+
128+
Enable development updates in SAM → Preferences → General → "Receive development updates"
129+
130+
### Changes Since Last Development Release
131+
132+
See commit history for details.
133+
draft: false
134+
prerelease: true
135+
files: |
136+
dist/SAM-${{ env.DEV_VERSION }}.zip
137+
dist/SAM-${{ env.DEV_VERSION }}.dmg
138+
139+
cleanup-old-releases:
140+
name: Cleanup Old Development Releases
141+
needs: build-and-release
142+
runs-on: ubuntu-latest
143+
steps:
144+
- name: Delete old development releases
145+
uses: actions/github-script@v7
146+
with:
147+
script: |
148+
const KEEP_COUNT = 7; // Keep last 7 development releases
149+
150+
// Get all releases
151+
const { data: releases } = await github.rest.repos.listReleases({
152+
owner: context.repo.owner,
153+
repo: context.repo.repo,
154+
});
155+
156+
// Filter for development pre-releases
157+
const devReleases = releases.filter(r =>
158+
r.prerelease && r.tag_name.includes('-dev.')
159+
);
160+
161+
// Sort by created date (newest first)
162+
devReleases.sort((a, b) =>
163+
new Date(b.created_at) - new Date(a.created_at)
164+
);
165+
166+
// Delete releases beyond KEEP_COUNT
167+
for (let i = KEEP_COUNT; i < devReleases.length; i++) {
168+
const release = devReleases[i];
169+
console.log(`Deleting old development release: ${release.tag_name}`);
170+
171+
// Delete the release
172+
await github.rest.repos.deleteRelease({
173+
owner: context.repo.owner,
174+
repo: context.repo.repo,
175+
release_id: release.id,
176+
});
177+
178+
// Delete the tag
179+
try {
180+
await github.rest.git.deleteRef({
181+
owner: context.repo.owner,
182+
repo: context.repo.repo,
183+
ref: `tags/${release.tag_name}`,
184+
});
185+
} catch (error) {
186+
console.log(`Could not delete tag ${release.tag_name}: ${error.message}`);
187+
}
188+
}
189+
190+
console.log(`Kept ${Math.min(KEEP_COUNT, devReleases.length)} development releases`);

BUILDING.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,73 @@ swift package generate-xcodeproj # (if needed)
217217
open Package.swift
218218
```
219219

220+
## Building Development Releases
221+
222+
SAM supports a development channel for testing pre-release features. Development builds use `-dev.N` version suffixes.
223+
224+
### Creating Development Builds
225+
226+
```bash
227+
# Build development version (auto-increments version)
228+
make build-dev
229+
230+
# Example version progression:
231+
# 20260109.1 → 20260109.1-dev.1 (first dev build)
232+
# 20260109.1-dev.1 → 20260109.1-dev.2 (next dev build)
233+
```
234+
235+
The `build-dev` target:
236+
1. Runs `scripts/increment-dev-version.sh` to bump version with `-dev.N` suffix
237+
2. Builds release configuration
238+
3. Ready for signing and distribution
239+
240+
### Development Release Workflow
241+
242+
```bash
243+
# 1. Build development version
244+
make build-dev
245+
246+
# 2. Sign and notarize (requires APPLE_DEVELOPER_ID)
247+
./scripts/sign-and-notarize.sh
248+
249+
# 3. Create GitHub pre-release with -dev tag
250+
# (manual step via GitHub UI or gh CLI)
251+
252+
# 4. Update development appcast items
253+
# Edit appcast-dev-items.xml with release details
254+
255+
# 5. Generate merged development appcast
256+
make appcast-dev
257+
258+
# 6. Commit and push
259+
git add Info.plist appcast-dev.xml appcast-dev-items.xml
260+
git commit -m "release(dev): 20260109.1-dev.1"
261+
git push
262+
```
263+
264+
### Development vs Stable Releases
265+
266+
**Development releases:**
267+
- Version format: `YYYYMMDD.RELEASE-dev.BUILD`
268+
- Published as GitHub pre-releases
269+
- Listed in `appcast-dev.xml` (includes stable releases too)
270+
- Users must opt-in via Preferences → General → "Receive development updates"
271+
- May contain bugs, incomplete features, breaking changes
272+
273+
**Stable releases:**
274+
- Version format: `YYYYMMDD.RELEASE`
275+
- Published as GitHub releases
276+
- Listed in `appcast.xml` only
277+
- All users receive these by default
278+
- Fully tested and documented
279+
280+
**Version comparison:**
281+
- `20260109.1-dev.1` < `20260109.1-dev.2` < `20260109.1` (stable)
282+
- Development users get dev builds, then upgrade to stable when released
283+
- Stable users never see dev builds
284+
285+
See `VERSIONING.md` for complete version scheme details.
286+
220287
## Signing / Notarization
221288

222289
The Makefile contains targets to sign and notarize (`sign`, `notarize`, `distribute`). For signing you need a valid Developer ID and the `APPLE_DEVELOPER_ID` environment variable set. Example:

Makefile

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ APP_BUNDLE_RELEASE = .build/Build/Products/Release/SAM.app
1919
.PHONY: all build clean test test-unit test-e2e test-all test-quick run help metallib llamacpp build-debug build-release bundle-python
2020
.PHONY: sign sign-debug sign-release verify-signature notarize staple distribute production
2121
.PHONY: dist
22+
.PHONY: build-dev release-dev appcast-dev
2223

2324
# Default target
2425
all: build
@@ -168,6 +169,38 @@ build-release: build-release-no-python bundle-python-release
168169
build-release-python: build-release
169170
@echo "Note: build-release-python is deprecated. Use 'make build-release' (Python now included by default)"
170171

172+
# Development Channel Build Targets
173+
174+
# Build development version (increments version with -dev tag)
175+
build-dev:
176+
@echo "Building development version..."
177+
@./scripts/increment-dev-version.sh
178+
@$(MAKE) build-release
179+
@echo "SUCCESS: Development build complete"
180+
@echo "Version: $$(cat Info.plist | grep -A1 CFBundleShortVersionString | tail -1 | sed 's/.*<string>\(.*\)<\/string>/\1/')"
181+
182+
# Create signed development release (for distribution)
183+
release-dev: build-dev
184+
@echo "Creating signed development release..."
185+
@if [ -z "$(DEVELOPER_ID)" ]; then \
186+
echo "ERROR: DEVELOPER_ID not set"; \
187+
echo "Set APPLE_DEVELOPER_ID in your environment"; \
188+
exit 1; \
189+
fi
190+
@echo "Development release requires manual steps:"
191+
@echo "1. Sign and notarize: ./scripts/sign-and-notarize.sh"
192+
@echo "2. Create GitHub pre-release with -dev tag"
193+
@echo "3. Update appcast-dev-items.xml manually"
194+
@echo "4. Run: make appcast-dev"
195+
@echo "5. Commit and push changes"
196+
197+
# Generate development appcast (merges dev items + stable releases)
198+
appcast-dev:
199+
@echo "Generating development appcast..."
200+
@./scripts/generate-dev-appcast.sh
201+
@echo "SUCCESS: appcast-dev.xml generated"
202+
@echo "Remember to commit: git add appcast-dev.xml && git commit"
203+
171204
# Ensure Metal library bundle is available
172205
# NOTE: The metallib is automatically built by mlx-swift package during xcodebuild.
173206
# This target is kept for backwards compatibility but is no longer required as a build prerequisite.

0 commit comments

Comments
 (0)