Skip to content

feat: Add CI/CD workflows for automated build, test, and release proc… #24

feat: Add CI/CD workflows for automated build, test, and release proc…

feat: Add CI/CD workflows for automated build, test, and release proc… #24

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
env:
# Consistent environment across jobs
APP_NAME: ClickIt
BUNDLE_ID: com.jsonify.clickit
# macOS deployment target
MACOSX_DEPLOYMENT_TARGET: "15.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"
- 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
- name: πŸ” SwiftLint
run: |
if ! command -v swiftlint &> /dev/null; then
echo "Installing SwiftLint..."
brew install swiftlint
fi
swiftlint lint --strict --reporter github-actions-logging
echo "βœ… Linting passed"
# === 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: πŸ’Ž Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: πŸ”§ Install Fastlane Dependencies
run: |
bundle install
bundle exec fastlane install_plugins
- name: πŸ—οΈ Build App Bundle
run: |
./build_app.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 15.0 or later"
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: πŸ™ Create GitHub Release
uses: ncipollo/create-release@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 }}
- 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")!
let task = URLSession.shared.dataTask(with: url) { 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>15.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: πŸ’Ž Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: πŸ”§ Install Fastlane Dependencies
run: |
bundle install
bundle exec fastlane install_plugins
- name: πŸ—οΈ Build App Bundle
run: |
./build_app.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 15.0 or later"
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: πŸ™ Create GitHub Release
uses: ncipollo/create-release@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 }}
- 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"
}
}
// Fetch releases from GitHub API
let url = URL(string: "https://api.github.com/repos/${{ github.repository }}/releases")!
let task = URLSession.shared.dataTask(with: url) { 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 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("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>15.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
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"
}
}
// Fetch releases from GitHub API
let url = URL(string: "https://api.github.com/repos/${{ github.repository }}/releases")!
let task = URLSession.shared.dataTask(with: url) { 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>15.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]"