Download Kit

Have you ever built a download page?

Have you every wished building all those different OS / Architecture / Package Manager combinations was easier to manage?

Download Kit solves two problems:

  1. Workflow management for building and testing binary artifacts across multiple platforms for desktop software release candidates.

  2. Clarity - the same frontend components that render your customer download page also render your internal release candidate management tool, whether you're using React, Angular, Solid, or whatever.

Why?

When you have a business distributing desktop software to consumers, you're casting a wide net. Desktop software instead of a web app strongly implies not many people have the specific problem you're solving. So you need to reach everyone with a computer - using whatever they bought - a mix of Windows and Mac machines. You might support Linux as that's what you used while developing the software.

You write code, then you need to build binaries for Windows x64, Windows ARM64, macOS Intel, macOS Apple Silicon, Linux x64, Linux ARM64. Each platform needs different file types. Windows wants both installers and portable executables, macOS wants DMGs, Linux wants AppImages, DEBs, RPMs, and more. That's easily 12+ different artifacts per release.

Your CI/CD handles the "not exists → uploaded" transition just fine. GitHub Actions can build everything and upload artifacts to your CDN. But that's not the end of the story.

It's not sufficient to just make the binaries available for download. Especially if you're building for non-technical users, you need to ensure the complete process works: clicking the link, downloading the file, installing it, and running it. You specifically need to put the binaries on the public Internet, because that's the only way to test Windows Defender SmartScreen and macOS Gatekeeper security features. Your binaries need to survive the same gauntlet your customers will face.

So you put each binary somewhere publicly accessible, then you download it on fresh VMs, install it, run smoke tests. Maybe Windows works perfectly but macOS fails because you messed up code signing. Maybe Linux AppImage works but the DEB has dependency issues.

Now you're tracking more than just "uploaded" - you're tracking: is this publicly accessible? has someone downloaded and tested it? did the test pass or fail? You only want to ship a specific commit if every single artifact for every single platform combination passes testing.

Multiply this across 12+ artifacts, and you realize you have scripts running on several different computers - build scripts, upload scripts, testing scripts - potentially running in parallel. This is why I added an API to track the state of a release candidate. There needs to be a single source of truth about where each artifact stands in the workflow.

All the scripts use the API to update state: "Windows x64 installer build started", "macOS ARM DMG uploaded", "Linux AppImage testing passed". Once the process of building and testing all these artifacts takes longer than five minutes, you can no longer justify doing this manually every day. It needs automation, and that's why there's an API to enable that automation.

The Workflow Model

Each artifact progresses through states:

Planned → Building → Uploaded aka Not Tested → Testing → ✓ Pass
                                                       → ✗ Fail

You can have mixed results - a release candidate might work on every platform except Windows. Individual test results are tracked with optional report URLs.

export type Artifact =
  | { type: "Planned", kind: string, filename: string }
  | { type: "Building", kind: string, filename: string, build?: URL }
  | {
    type: "Uploaded" | "Testing" | "Passed" | "Failed",
    kind: string,
    filename: string,
    build?: URL,
    download: URL,
    sizeBytes: number,
    note?: string,
    tests: Array<
      | { type: "NotAsked", name: string }
      | { type: "Running" | "Passed" | "Failed"
        , name: string
        , report?: URL
        }
    >
  }

type ReleaseCandidate = {
  commit: string;
  version: string;
  created: string;
  channel: string;
  changelog?: URL;
  platforms: Record<
    string,
    { archDisplayName: string; artifacts: Artifact[] }
  >;
};

The workflow is deliberately flexible:

Component Reuse Strategy

Imagine a video of the internal system converging to to match the public download page.

"Why reuse download page components for internal tools?"

Because you've already solved the hard design problem: how to clearly present multiple download options to users. Your public download page layout communicates platform choices, file types, and sizes in a way that makes sense to customers.

Why build separate admin interfaces when you can make those same components render workflow states? Your internal release candidate page becomes a preview of exactly what customers will see. As artifacts move through the workflow - planned, building, uploaded, tested - you watch the page converge toward the final customer experience.

This isn't just code reuse, it's design reuse. The spatial layout that helps customers understand their options also helps you understand release progress.

What's Included

The core is a programming language independent API definition with type definitions generated for TypeScript, Go, Python, and Rust.

Everything else is concrete implementations:

The goal is to provide enough context and examples that you can hand this to an LLM coding agent and say "make our download page use this API" and it will be able to modify your existing components. The server code works as-is if you want to use it.

What This Is NOT

This is not for complex integration testing or large teams with sophisticated CI/CD. This is not for running 50 automated tests to validate a binary.

This is for small teams that need to validate: can I download this binary from my CDN, install it, and run basic smoke tests before customers get it?

Usage

For a new project, use the server implementation and SDKs. Look at the example UI components (about 200 lines of code) and adapt them for your branding.

Since my projects all use the same branding, I include my specific UI components as examples in this monorepo.

Want a weekly digest of this blog?