A comprehensive validator for Sparkle appcast.xml feeds.
Available as a CLI tool, JavaScript library, and web application.
Note: This is an independent community project. It is not affiliated with, endorsed by, or sponsored by the official Sparkle project or its maintainers.
This validator was developed by analyzing 500+ real-world appcasts from production macOS applications including iTerm2, VLC, Cyberduck, Brave Browser, Dash, Transmission, and many others. Our validation rules are grounded in:
- Sparkle Source Code — Direct analysis of version comparison logic, signature verification, and parsing behavior in the Sparkle 2.x codebase
- Maintainer Feedback — Rule refinements based on feedback from Sparkle maintainer @zorgiepoo
- Real-World Patterns — Identifying common issues that cause silent failures (missing versions, malformed signatures, date/version ordering mismatches)
The test corpus includes apps that are:
- Perfect (zero warnings): LowProfile, Scroll Reverser, SourceTree, Skim
- Real-world valid (minor warnings): iTerm2, Dash, Cyberduck, Tunnelblick
- Edge cases: Large feeds (200+ items), delta-heavy feeds, multi-channel feeds
This empirical approach ensures the validator catches issues that actually matter in production while minimizing false positives.
- Validates Sparkle appcast.xml feeds against all known requirements
- Reports errors, warnings, and informational messages with line numbers
- Provides fix suggestions for common issues
- Works as CLI, library, web app, or GitHub Action
- Checks:
- XML structure (RSS 2.0 + Sparkle namespace)
- Version declarations
- Enclosure attributes (url, length, type)
- URL validity
- Date formats (RFC 2822)
- Signatures (EdDSA/DSA)
- System requirements
- Delta updates
- Phased rollouts
- Channel names
- And more...
Try it online at SparkleValidator.com
npm install -g sparkle-validatorOr run directly without installing:
npx sparkle-validator https://example.com/appcast.xmlOr with Homebrew:
brew tap dweekly/sparkle-validator
brew install sparkle-validator# Validate a local file
sparkle-validator appcast.xml
# Validate from URL
sparkle-validator https://example.com/appcast.xml
# Validate from stdin
cat appcast.xml | sparkle-validator -
# JSON output
sparkle-validator --format json appcast.xml
# Strict mode (warnings as errors)
sparkle-validator --strict appcast.xml
# Only show errors
sparkle-validator --quiet appcast.xml
# Check that URLs exist and sizes match
sparkle-validator --check-urls appcast.xml
# Check URLs with custom timeout (ms)
sparkle-validator --check-urls --timeout 30000 appcast.xml| Option | Description |
|---|---|
-f, --format <type> |
Output format: text (default) or json |
-s, --strict |
Treat warnings as errors |
-c, --check-urls |
Check that URLs exist and sizes match |
--timeout <ms> |
Timeout for URL checks (default: 10000ms) |
--no-info |
Suppress informational messages |
--no-color |
Disable colored output |
-q, --quiet |
Only show errors |
-v, --version |
Show version number |
-h, --help |
Show help |
| Code | Meaning |
|---|---|
| 0 | Valid (no errors) |
| 1 | Invalid (has errors, or warnings with --strict) |
| 2 | Input error (file not found, network error, etc.) |
Use the official GitHub Action for the simplest integration:
name: Validate Appcast
on:
push:
paths: ['appcast.xml']
pull_request:
paths: ['appcast.xml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate appcast.xml
uses: dweekly/Sparkle-Validator@v1
with:
file: appcast.xml
# With options
- name: Validate with URL checking
uses: dweekly/Sparkle-Validator@v1
with:
file: appcast.xml
strict: true
check-urls: true| Input | Description | Default |
|---|---|---|
file |
Path or URL to appcast.xml | (required) |
strict |
Treat warnings as errors | false |
check-urls |
Verify enclosure URLs exist | false |
timeout |
URL check timeout (ms) | 10000 |
quiet |
Only show errors | false |
format |
Output format: text or json |
text |
| Output | Description |
|---|---|
valid |
true if no errors |
error-count |
Number of errors |
warning-count |
Number of warnings |
info-count |
Number of info messages |
json |
Full result as JSON |
- name: Validate appcast.xml
run: npx sparkle-validator appcast.xml
# Strict mode (warnings fail the build)
- name: Validate appcast.xml (strict)
run: npx sparkle-validator --strict appcast.xml
# JSON output for further processing
- name: Validate and capture results
run: |
npx sparkle-validator --format json appcast.xml > validation.json
cat validation.json - name: Validate published appcast
uses: dweekly/Sparkle-Validator@v1
with:
file: https://example.com/appcast.xml# .git/hooks/pre-commit
#!/bin/sh
npx sparkle-validator appcast.xml || exit 1import { validate } from 'sparkle-validator';
const xml = `<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>My App</title>
<link>https://example.com</link>
<item>
<title>Version 2.0</title>
<pubDate>Thu, 13 Jul 2023 14:30:00 -0700</pubDate>
<sparkle:version>200</sparkle:version>
<description><![CDATA[<p>New features!</p>]]></description>
<enclosure url="https://example.com/app.zip"
length="12345678"
type="application/octet-stream"
sparkle:edSignature="eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eA==" />
</item>
</channel>
</rss>`;
const result = validate(xml);
console.log(result.valid); // true
console.log(result.errorCount); // 0
console.log(result.diagnostics); // Array of diagnosticsinterface ValidationResult {
valid: boolean; // true if no errors
diagnostics: Diagnostic[];
errorCount: number;
warningCount: number;
infoCount: number;
}
interface Diagnostic {
id: string; // e.g. "E008", "W003"
severity: "error" | "warning" | "info";
message: string;
line?: number; // 1-based
column?: number; // 1-based
path?: string; // e.g. "rss > channel > item[2] > enclosure"
fix?: string; // Suggestion for fixing the issue
}| ID | Description |
|---|---|
| E001 | Not well-formed XML |
| E002 | Root element is not <rss> |
| E003 | Missing version="2.0" on <rss> |
| E004 | Missing Sparkle namespace declaration |
| E005 | Missing <channel> inside <rss> |
| E006 | More than one <channel> element |
| E007 | No <item> elements in <channel> |
| E008 | Item missing sparkle:version |
| E009 | Item has neither <enclosure> with url nor <link> |
| E010-E013 | Enclosure missing/invalid attributes |
| E014-E018 | Invalid URLs |
| E019 | Invalid channel name characters |
| E020-E021 | Phased rollout errors |
| E022 | Invalid installationType |
| E023-E025 | Delta update structure errors |
| E027 | URL returns non-2xx status (--check-urls) |
| E028 | Content-Length doesn't match declared length (--check-urls) |
| E029 | Version string is empty or whitespace-only |
| E030 | Invalid sparkle:os value (must be "macos" or "windows") |
| E031 | Invalid Ed25519/DSA signature (malformed base64 or wrong length) |
| ID | Description |
|---|---|
| W001-W002 | Missing title on channel/item |
| W003-W004 | Missing or invalid pubDate |
| W006 | DSA-only signature (deprecated, use EdDSA) |
| W007-W008 | Redundant version declarations |
| W009 | No release notes |
| W010 | Non-standard MIME type |
| W011-W013 | System version format issues |
| W014 | (Moved to I011) |
| W016 | Unencoded URL characters |
| W017 | informationalUpdate with enclosure |
| W018 | Items not sorted by version (newest first) |
| W019 | Enclosure length is 0 |
| W020 | Duplicate version |
| W021 | URL redirects to different location (--check-urls) |
| W022 | Content-Length header missing (--check-urls) |
| W023 | Local/private URL skipped (--check-urls) |
| W024 | URL uses insecure HTTP instead of HTTPS (--check-urls) |
| W025 | pubDate is in the future |
| W026 | pubDate is implausibly old (before 2001/Mac OS X era) |
| W027 | Version string is non-numeric (may cause comparison failures) |
| W028 | Version decreases while pubDate increases |
| W030 | URL file extension doesn't match expected type |
| W031 | (Moved to I012) |
| W032 | Multiple delta enclosures for same deltaFrom |
| W033 | shortVersionString format unusual (not x.y.z) |
| W034 | criticalUpdate version attribute not valid format |
| W035 | Feed mixes HTTP and HTTPS URLs |
| W036 | hardwareRequirements contains unknown architecture |
| W037 | releaseNotesLink missing xml:lang for localization |
| W038 | CDATA section used in version/signature elements |
| W039 | XML declaration missing encoding attribute |
| W040 | Channel has language but items have different lang |
| W041 | Version missing but deducible from filename (Sparkle fallback) |
| W042 | Version only as enclosure attribute (prefer <sparkle:version> element) |
| W043 | sparkle:os deprecated (prefer separate feeds per platform) |
| ID | Description |
|---|---|
| I001 | Summary: N items across M channels |
| I002 | Item contains N delta updates |
| I003 | Item uses phased rollout |
| I004 | Item marked as critical update |
| I005 | Item targets non-macOS platform |
| I006 | Item requires specific hardware (Sparkle 2.9+) |
| I007 | Item requires minimum app version to update (Sparkle 2.9+) |
| I008 | Feed contains >50 items (performance consideration) |
| I009 | Summary of OS support range across all items |
| I010 | Enclosure has no signature (signatures are optional) |
| I011 | Missing channel link (informational) |
| I012 | Delta references version not in feed (old versions may be pruned) |
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run build
# Type check
npm run lintThis package is published with:
- npm provenance — cryptographically attests that the package was built from this repository via GitHub Actions
- SBOM — Software Bill of Materials (CycloneDX format) attached to each GitHub release
You can verify provenance on npm: npm audit signatures
MIT License - see LICENSE for details.
See CONTRIBUTING.md for guidelines.