Skip to content

Troubleshoot fastlane launch_lite installation error #132

Troubleshoot fastlane launch_lite installation error

Troubleshoot fastlane launch_lite installation error #132

Workflow file for this run

# ClickIt CI/CD Pipeline
# Inspired by macos-auto-clicker-main with adaptations for Swift Package Manager
name: CI/CD Pipeline
on:
push:
branches: [ main, staging ]
tags: [ 'v*', 'beta*' ]
pull_request:
branches: [ main, staging, dev ]
types: [ opened, synchronize, reopened ]
# Prevent overlapping builds
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Required permissions for GitHub Actions
permissions:
contents: write
packages: read
pages: write
id-token: write
env:
# Consistent environment across jobs
APP_NAME: ClickIt
BUNDLE_ID: com.jsonify.clickit
# macOS deployment target
MACOSX_DEPLOYMENT_TARGET: "14.0"
# Swift configuration
SWIFT_VERSION: "6.1"
jobs:
# === Code Quality Checks ===
quality_checks:
name: 🔍 Code Quality
runs-on: macos-15
timeout-minutes: 15
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for proper linting
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: 📦 Swift Package Cache
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-swift-${{ hashFiles('Package.resolved', 'Package.swift') }}
restore-keys: |
${{ runner.os }}-swift-
- name: 🏗️ Swift Build (Debug)
run: swift build --configuration debug
- name: 🧪 Run Tests
run: |
swift test --enable-code-coverage
echo "✅ All tests passed"
continue-on-error: true
- name: 📊 Test Coverage
run: |
xcrun llvm-cov export -format="lcov" \
.build/debug/ClickItPackageTests.xctest/Contents/MacOS/ClickItPackageTests \
-instr-profile .build/debug/codecov/default.profdata > coverage.lcov
continue-on-error: true
- name: 📈 Upload Coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.lcov
fail_ci_if_error: false
continue-on-error: true
# === Build Tests ===
build_tests:
name: 🔨 Build Tests
runs-on: macos-15
needs: quality_checks
timeout-minutes: 20
strategy:
matrix:
configuration: [debug, release]
architecture: [x86_64, arm64]
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: 📦 Swift Package Cache
uses: actions/cache@v4
with:
path: .build
key: build-${{ runner.os }}-${{ matrix.configuration }}-${{ matrix.architecture }}-${{ hashFiles('Package.resolved', 'Package.swift') }}
restore-keys: |
build-${{ runner.os }}-${{ matrix.configuration }}-${{ matrix.architecture }}-
build-${{ runner.os }}-${{ matrix.configuration }}-
build-${{ runner.os }}-
- name: 🏗️ Build for ${{ matrix.architecture }}
run: |
echo "Building for ${{ matrix.architecture }} in ${{ matrix.configuration }} mode..."
swift build --configuration ${{ matrix.configuration }} --arch ${{ matrix.architecture }}
echo "✅ Build successful"
- name: 📋 Verify Binary
run: |
BINARY_PATH=$(swift build --configuration ${{ matrix.configuration }} --arch ${{ matrix.architecture }} --show-bin-path)/ClickIt
if [ -f "$BINARY_PATH" ]; then
echo "✅ Binary exists: $BINARY_PATH"
file "$BINARY_PATH"
lipo -info "$BINARY_PATH" 2>/dev/null || echo "Single architecture binary"
else
echo "❌ Binary not found at $BINARY_PATH"
exit 1
fi
# === Beta Release ===
beta_release:
name: 🚀 Beta Release
runs-on: macos-15
needs: [quality_checks, build_tests]
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'beta') && github.ref_name != ''
timeout-minutes: 30
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: 🏗️ Build App Bundle
run: |
# Set environment variables for CI build (disable code signing, fix deployment target)
export CODE_SIGN_IDENTITY=""
export CODE_SIGNING_REQUIRED=NO
export CODE_SIGNING_ALLOWED=NO
export MACOSX_DEPLOYMENT_TARGET=14.0
./build_app_unified.sh release
echo "✅ App bundle created"
- name: 📦 Create Release Assets
run: |
# Extract version from tag (e.g., beta-v1.0.0-20250713 -> 1.0.0)
TAG_NAME="${{ github.ref_name }}"
VERSION=$(echo "$TAG_NAME" | sed -n 's/beta-v\?\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/p')
if [ -z "$VERSION" ]; then
VERSION="1.0.0"
fi
BETA_VERSION="${VERSION}-beta-$(git rev-parse --short HEAD)"
echo "BETA_VERSION=$BETA_VERSION" >> $GITHUB_ENV
# Create ZIP
cd dist
zip -r "${APP_NAME}-${BETA_VERSION}.zip" "${APP_NAME}.app"
echo "✅ Created ZIP: ${APP_NAME}-${BETA_VERSION}.zip"
- name: 🔐 Generate Asset Signatures
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
if [ -n "$SPARKLE_PRIVATE_KEY" ]; then
echo "🔐 Generating signatures for release assets..."
python3 scripts/generate_signatures.py dist
echo "✅ Signatures generated"
else
echo "⚠️ SPARKLE_PRIVATE_KEY not set, skipping signature generation"
fi
- name: 📝 Generate Changelog
id: changelog
run: |
# Get previous tag for changelog
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG="## What's New\n\nInitial release of ${{ env.APP_NAME }}!"
else
CHANGELOG="## What's New\n\n"
git log ${PREVIOUS_TAG}..${{ github.ref_name }} --oneline --no-merges | while read commit; do
commit_msg=$(echo "$commit" | cut -d' ' -f2-)
CHANGELOG="${CHANGELOG}- ${commit_msg}\n"
done
fi
CHANGELOG="⚠️ **This is a beta release for testing purposes.**\n\n${CHANGELOG}"
CHANGELOG="${CHANGELOG}\n\n---\n🤖 Generated with [Claude Code](https://claude.ai/code)\n📱 Compatible with macOS 14.0 or later"
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: 🐙 Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ github.ref_name }}
name: "${{ env.APP_NAME }} ${{ env.BETA_VERSION }}"
body: ${{ steps.changelog.outputs.changelog }}
prerelease: true
artifacts: "dist/${{ env.APP_NAME }}-${{ env.BETA_VERSION }}.zip"
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: true
replacesArtifacts: true
- name: 📡 Generate Beta Appcast
run: |
# Create appcast directory
mkdir -p docs
# Generate beta appcast using GitHub API
cat > generate_appcast.swift << 'EOF'
import Foundation
struct GitHubRelease: Codable {
let tagName: String
let name: String?
let body: String?
let prerelease: Bool
let publishedAt: String?
let assets: [GitHubAsset]
let htmlUrl: String
private enum CodingKeys: String, CodingKey {
case name, body, prerelease, assets
case tagName = "tag_name"
case publishedAt = "published_at"
case htmlUrl = "html_url"
}
}
struct GitHubAsset: Codable {
let name: String
let size: Int
let browserDownloadUrl: String
private enum CodingKeys: String, CodingKey {
case name, size
case browserDownloadUrl = "browser_download_url"
}
}
func loadSignatures() -> [String: String] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: "dist/signatures.json")),
let signatures = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
print("⚠️ No signatures found")
return [:]
}
return signatures
}
// Fetch releases from GitHub API
let url = URL(string: "https://api.github.com/repos/${{ github.repository }}/releases")!
var request = URLRequest(url: url)
request.setValue("Bearer ${{ secrets.GITHUB_TOKEN }}", forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print("Failed to fetch releases")
exit(1)
}
do {
let releases = try JSONDecoder().decode([GitHubRelease].self, from: data)
let betaReleases = releases.filter { $0.prerelease && !$0.assets.isEmpty }
.sorted { lhs, rhs in
(lhs.publishedAt ?? "") > (rhs.publishedAt ?? "")
}
let appcastXML = generateAppcast(releases: betaReleases, isBeta: true)
try appcastXML.write(to: URL(fileURLWithPath: "docs/appcast-beta.xml"), atomically: true, encoding: .utf8)
print("✅ Beta appcast generated successfully")
exit(0)
} catch {
print("Error: \(error)")
exit(1)
}
}
task.resume()
// Keep the script running
RunLoop.main.run()
func generateAppcast(releases: [GitHubRelease], isBeta: Bool) -> String {
let items = releases.compactMap { release -> String? in
guard let zipAsset = release.assets.first(where: { $0.name.hasSuffix(".zip") }) else {
return nil
}
let version = release.tagName.replacingOccurrences(of: "^(beta-)?v?", with: "", options: .regularExpression)
let title = release.name ?? "ClickIt \(version)"
let description = release.body ?? "No release notes available."
let pubDate = formatDate(release.publishedAt)
// Get signature for this asset
let signatures = loadSignatures()
let signatureAttr = signatures[zipAsset.name].map { " sparkle:edSignature=\"\($0)\"" } ?? ""
return """
<item>
<title><![CDATA[\(title)]]></title>
<description><![CDATA[\(description)]]></description>
<link>\(release.htmlUrl)</link>
<sparkle:version>\(version)</sparkle:version>
<sparkle:shortVersionString>\(version)</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<pubDate>\(pubDate)</pubDate>
<enclosure url="\(zipAsset.browserDownloadUrl)"
length="\(zipAsset.size)"
type="application/octet-stream"
sparkle:version="\(version)"
sparkle:shortVersionString="\(version)"\(signatureAttr) />
</item>
"""
}
let itemsXML = items.joined(separator: "\n ")
let channelType = isBeta ? "Beta " : ""
let lastBuildDate = formatDate(nil)
return """
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>ClickIt \(channelType)Updates</title>
<link>https://github.com/${{ github.repository }}</link>
<description>Software updates for ClickIt</description>
<language>en</language>
<lastBuildDate>\(lastBuildDate)</lastBuildDate>
\(itemsXML)
</channel>
</rss>
"""
}
func formatDate(_ dateString: String?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "GMT")
if let dateString = dateString {
let isoFormatter = ISO8601DateFormatter()
if let date = isoFormatter.date(from: dateString) {
return formatter.string(from: date)
}
}
return formatter.string(from: Date())
}
EOF
# Run the Swift script
swift generate_appcast.swift
# === Production Release ===
production_release:
name: 🚀 Production Release
runs-on: macos-15
needs: [quality_checks, build_tests]
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.ref_name != ''
timeout-minutes: 30
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: 🏗️ Build App Bundle
run: |
# Set environment variables for CI build (disable code signing, fix deployment target)
export CODE_SIGN_IDENTITY=""
export CODE_SIGNING_REQUIRED=NO
export CODE_SIGNING_ALLOWED=NO
export MACOSX_DEPLOYMENT_TARGET=14.0
./build_app_unified.sh release
echo "✅ App bundle created"
- name: 📦 Create Release Assets
run: |
# Extract version from tag (e.g., v1.0.0 -> 1.0.0)
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
cd dist
# Create ZIP
zip -r "${APP_NAME}-${VERSION}.zip" "${APP_NAME}.app"
echo "✅ Created ZIP: ${APP_NAME}-${VERSION}.zip"
# Create DMG
hdiutil create -volname "${{ env.APP_NAME }}" -srcfolder "${APP_NAME}.app" -ov -format UDZO "${APP_NAME}-${VERSION}.dmg"
echo "✅ Created DMG: ${APP_NAME}-${VERSION}.dmg"
- name: 🔐 Generate Asset Signatures
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
if [ -n "$SPARKLE_PRIVATE_KEY" ]; then
echo "🔐 Generating signatures for release assets..."
python3 scripts/generate_signatures.py dist
echo "✅ Signatures generated"
else
echo "⚠️ SPARKLE_PRIVATE_KEY not set, skipping signature generation"
fi
- name: 📝 Generate Changelog
id: changelog
run: |
# Get previous tag for changelog
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG="## What's New\n\nInitial release of ${{ env.APP_NAME }}!"
else
CHANGELOG="## What's New\n\n"
git log ${PREVIOUS_TAG}..${{ github.ref_name }} --oneline --no-merges | while read commit; do
commit_msg=$(echo "$commit" | cut -d' ' -f2-)
CHANGELOG="${CHANGELOG}- ${commit_msg}\n"
done
fi
CHANGELOG="${CHANGELOG}\n\n---\n🤖 Generated with [Claude Code](https://claude.ai/code)\n📱 Compatible with macOS 14.0 or later"
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: 🐙 Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ github.ref_name }}
name: "${{ env.APP_NAME }} v${{ env.VERSION }}"
body: ${{ steps.changelog.outputs.changelog }}
prerelease: false
artifacts: |
dist/${{ env.APP_NAME }}-${{ env.VERSION }}.zip
dist/${{ env.APP_NAME }}-${{ env.VERSION }}.dmg
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: true
replacesArtifacts: true
- name: 📡 Generate Production Appcast
run: |
# Create appcast directory
mkdir -p docs
# Generate production appcast using GitHub API
cat > generate_appcast.swift << 'EOF'
import Foundation
struct GitHubRelease: Codable {
let tagName: String
let name: String?
let body: String?
let prerelease: Bool
let publishedAt: String?
let assets: [GitHubAsset]
let htmlUrl: String
private enum CodingKeys: String, CodingKey {
case name, body, prerelease, assets
case tagName = "tag_name"
case publishedAt = "published_at"
case htmlUrl = "html_url"
}
}
struct GitHubAsset: Codable {
let name: String
let size: Int
let browserDownloadUrl: String
private enum CodingKeys: String, CodingKey {
case name, size
case browserDownloadUrl = "browser_download_url"
}
}
func loadSignatures() -> [String: String] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: "dist/signatures.json")),
let signatures = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
print("⚠️ No signatures found")
return [:]
}
return signatures
}
// Fetch releases from GitHub API
let url = URL(string: "https://api.github.com/repos/${{ github.repository }}/releases")!
var request = URLRequest(url: url)
request.setValue("Bearer ${{ secrets.GITHUB_TOKEN }}", forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Network error: \(error)")
exit(1)
}
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response type")
exit(1)
}
guard let data = data else {
print("No data received")
exit(1)
}
// Log response for debugging
if httpResponse.statusCode != 200 {
print("HTTP \(httpResponse.statusCode): \(String(data: data, encoding: .utf8) ?? "unknown error")")
exit(1)
}
do {
// First try to decode as a GitHubError to handle API errors
if let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let message = errorResponse["message"] as? String {
print("GitHub API Error: \(message)")
if let documentationUrl = errorResponse["documentation_url"] as? String {
print("Documentation: \(documentationUrl)")
}
exit(1)
}
let releases = try JSONDecoder().decode([GitHubRelease].self, from: data)
let prodReleases = releases.filter { !$0.prerelease && !$0.assets.isEmpty }
.sorted { lhs, rhs in
(lhs.publishedAt ?? "") > (rhs.publishedAt ?? "")
}
let appcastXML = generateAppcast(releases: prodReleases, isBeta: false)
try appcastXML.write(to: URL(fileURLWithPath: "docs/appcast.xml"), atomically: true, encoding: .utf8)
print("✅ Production appcast generated successfully")
exit(0)
} catch {
print("JSON Decoding Error: \(error)")
print("Raw response: \(String(data: data, encoding: .utf8) ?? "Unable to decode")")
exit(1)
}
}
task.resume()
// Keep the script running
RunLoop.main.run()
func generateAppcast(releases: [GitHubRelease], isBeta: Bool) -> String {
let items = releases.compactMap { release -> String? in
guard let zipAsset = release.assets.first(where: { $0.name.hasSuffix(".zip") }) else {
return nil
}
let version = release.tagName.replacingOccurrences(of: "^(beta-)?v?", with: "", options: .regularExpression)
let title = release.name ?? "ClickIt \(version)"
let description = release.body ?? "No release notes available."
let pubDate = formatDate(release.publishedAt)
// Get signature for this asset
let signatures = loadSignatures()
let signatureAttr = signatures[zipAsset.name].map { " sparkle:edSignature=\"\($0)\"" } ?? ""
return """
<item>
<title><![CDATA[\(title)]]></title>
<description><![CDATA[\(description)]]></description>
<link>\(release.htmlUrl)</link>
<sparkle:version>\(version)</sparkle:version>
<sparkle:shortVersionString>\(version)</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<pubDate>\(pubDate)</pubDate>
<enclosure url="\(zipAsset.browserDownloadUrl)"
length="\(zipAsset.size)"
type="application/octet-stream"
sparkle:version="\(version)"
sparkle:shortVersionString="\(version)"\(signatureAttr) />
</item>
"""
}
let itemsXML = items.joined(separator: "\n ")
let channelType = isBeta ? "Beta " : ""
let lastBuildDate = formatDate(nil)
return """
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>ClickIt \(channelType)Updates</title>
<link>https://github.com/${{ github.repository }}</link>
<description>Software updates for ClickIt</description>
<language>en</language>
<lastBuildDate>\(lastBuildDate)</lastBuildDate>
\(itemsXML)
</channel>
</rss>
"""
}
func formatDate(_ dateString: String?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "GMT")
if let dateString = dateString {
let isoFormatter = ISO8601DateFormatter()
if let date = isoFormatter.date(from: dateString) {
return formatter.string(from: date)
}
}
return formatter.string(from: Date())
}
EOF
# Run the Swift script
swift generate_appcast.swift
# === GitHub Pages Deployment ===
deploy_appcast:
name: 📡 Deploy Appcast
runs-on: ubuntu-latest
needs: [beta_release, production_release]
if: always() && (needs.beta_release.result == 'success' || needs.production_release.result == 'success')
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 📥 Download Appcast Artifacts
uses: actions/download-artifact@v4
with:
name: appcast-files
path: docs/
continue-on-error: true
- name: 📡 Generate Updated Appcasts
run: |
# Create docs directory if it doesn't exist
mkdir -p docs
# Generate both production and beta appcasts
cat > generate_all_appcasts.swift << 'EOF'
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct GitHubRelease: Codable {
let tagName: String
let name: String?
let body: String?
let prerelease: Bool
let publishedAt: String?
let assets: [GitHubAsset]
let htmlUrl: String
private enum CodingKeys: String, CodingKey {
case name, body, prerelease, assets
case tagName = "tag_name"
case publishedAt = "published_at"
case htmlUrl = "html_url"
}
}
struct GitHubAsset: Codable {
let name: String
let size: Int
let browserDownloadUrl: String
private enum CodingKeys: String, CodingKey {
case name, size
case browserDownloadUrl = "browser_download_url"
}
}
func loadSignatures() -> [String: String] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: "dist/signatures.json")),
let signatures = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
print("⚠️ No signatures found")
return [:]
}
return signatures
}
// Fetch releases from GitHub API
let url = URL(string: "https://api.github.com/repos/${{ github.repository }}/releases")!
var request = URLRequest(url: url)
request.setValue("Bearer ${{ secrets.GITHUB_TOKEN }}", forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print("Failed to fetch releases")
exit(1)
}
do {
let releases = try JSONDecoder().decode([GitHubRelease].self, from: data)
// Generate production appcast
let prodReleases = releases.filter { !$0.prerelease && !$0.assets.isEmpty }
.sorted { ($0.publishedAt ?? "") > ($1.publishedAt ?? "") }
let prodAppcast = generateAppcast(releases: prodReleases, isBeta: false)
try prodAppcast.write(to: URL(fileURLWithPath: "docs/appcast.xml"), atomically: true, encoding: .utf8)
// Generate beta appcast
let betaReleases = releases.filter { $0.prerelease && !$0.assets.isEmpty }
.sorted { ($0.publishedAt ?? "") > ($1.publishedAt ?? "") }
let betaAppcast = generateAppcast(releases: betaReleases, isBeta: true)
try betaAppcast.write(to: URL(fileURLWithPath: "docs/appcast-beta.xml"), atomically: true, encoding: .utf8)
print("✅ All appcasts generated successfully")
exit(0)
} catch {
print("Error: \(error)")
exit(1)
}
}
task.resume()
RunLoop.main.run()
func generateAppcast(releases: [GitHubRelease], isBeta: Bool) -> String {
let items = releases.compactMap { release -> String? in
guard let zipAsset = release.assets.first(where: { $0.name.hasSuffix(".zip") }) else {
return nil
}
let version = release.tagName.replacingOccurrences(of: "^(beta-)?v?", with: "", options: .regularExpression)
let title = release.name ?? "ClickIt \(version)"
let description = release.body ?? "No release notes available."
let pubDate = formatDate(release.publishedAt)
// Get signature for this asset
let signatures = loadSignatures()
let signatureAttr = signatures[zipAsset.name].map { " sparkle:edSignature=\"\($0)\"" } ?? ""
return """
<item>
<title><![CDATA[\(title)]]></title>
<description><![CDATA[\(description)]]></description>
<link>\(release.htmlUrl)</link>
<sparkle:version>\(version)</sparkle:version>
<sparkle:shortVersionString>\(version)</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<pubDate>\(pubDate)</pubDate>
<enclosure url="\(zipAsset.browserDownloadUrl)"
length="\(zipAsset.size)"
type="application/octet-stream"
sparkle:version="\(version)"
sparkle:shortVersionString="\(version)"\(signatureAttr) />
</item>
"""
}
let itemsXML = items.joined(separator: "\n ")
let channelType = isBeta ? "Beta " : ""
let lastBuildDate = formatDate(nil)
return """
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>ClickIt \(channelType)Updates</title>
<link>https://github.com/${{ github.repository }}</link>
<description>Software updates for ClickIt</description>
<language>en</language>
<lastBuildDate>\(lastBuildDate)</lastBuildDate>
\(itemsXML)
</channel>
</rss>
"""
}
func formatDate(_ dateString: String?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "GMT")
if let dateString = dateString {
let isoFormatter = ISO8601DateFormatter()
if let date = isoFormatter.date(from: dateString) {
return formatter.string(from: date)
}
}
return formatter.string(from: Date())
}
EOF
# Run the Swift script
swift generate_all_appcasts.swift
# Create index.html for GitHub Pages
cat > docs/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ClickIt Update Service</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 40px; }
.container { max-width: 600px; margin: 0 auto; }
.feed { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
a { color: #007AFF; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>ClickIt Update Service</h1>
<p>Sparkle update feeds for ClickIt auto-clicker application.</p>
<div class="feed">
<h3>Production Updates</h3>
<p>Stable releases for general use.</p>
<a href="appcast.xml">appcast.xml</a>
</div>
<div class="feed">
<h3>Beta Updates</h3>
<p>Pre-release versions for testing.</p>
<a href="appcast-beta.xml">appcast-beta.xml</a>
</div>
<hr>
<p><small>Generated automatically by GitHub Actions</small></p>
</div>
</body>
</html>
EOF
echo "✅ Appcast files generated"
ls -la docs/
- name: 🚀 Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
publish_branch: gh-pages
commit_message: "Update appcast feeds [ci skip]"