# Publishing & Auto Updates

Source: https://docs.interop.io/desktop/developers/seed-project/publishing-auto-updates/index.html

## Overview

Publish your build artifacts to a location accessible to the platform, then configure **io.Connect Desktop** to check that location for updates. You can upload artifacts via Electron Forge publishers or your own deployment tools.

> ⚠️ *Note that auto updates are supported for installations created with Squirrel.Windows setup executables on Windows and DMG files on macOS. Deployments extracted directly from ZIP archives don't support auto updates. The macOS update payload is still a ZIP archive, even when the initial installation uses a DMG.*

> ℹ️ *For details on all updater properties, see the [Developers > Configuration > System > Auto Updates](https://docs.interop.io/desktop/developers/configuration/system/index.md#auto_updates) section.*

## Update Sources

### Static Storage

Use `"StaticStorage"` for a file server, CDN, or cloud storage location serving the update artifacts and metadata. The `"baseUrl"` must identify the directory for the target platform and architecture. On macOS, the updater appends `/RELEASES.json`. On Windows, Squirrel uses the directory to retrieve `RELEASES` and its referenced packages.

New seed projects include a `/modifications/base/iocd/config/system.json.merge-autoUpdate` file that disables auto updates and contains a placeholder `"Service"` source. Merge files in the same folder are applied in an unspecified order, so don't set the same property in more than one of them. Replace the contents of the generated file with the settings shared by both platforms:

```json
{
    "autoUpdater": {
        "enabled": true,
        "updateSource": {
            "type": "StaticStorage"
        },
        "updateIntervalMinutes": 60
    }
}
```

Then specify only the `"baseUrl"` for each platform in a separate merge file. The following example demonstrates a macOS update location. Save it in `/modifications/base/iocd/config/system.json.merge-autoUpdate-darwin`:

```json
{
    "autoUpdater": {
        "updateSource": {
            "baseUrl": "https://cdn.example.com/my-product/darwin/arm64"
        }
    }
}
```

Use a `system.json.merge-autoUpdate-win32` file with the Windows directory (e.g., `https://cdn.example.com/my-product/win32/x64`). The architecture directory must match the machine that builds the package. The generated GitHub Actions workflow builds macOS packages on Apple silicon (`arm64`) runners and Windows packages on `x64` runners.

### Update Service

Use `"Service"` when a server decides which update to return based on the platform, installed version, or update channel. The platform resolves `update/<platform>/<version>/<channel>` relative to the `"baseUrl"`. If the `"baseUrl"` contains a path, end it with `/` - e.g., `https://example.com/my-product/` resolves to `https://example.com/my-product/update/...`, while `https://example.com/my-product` resolves to `https://example.com/update/...`.

The following example demonstrates a service configuration. Replace the contents of the generated `/modifications/base/iocd/config/system.json.merge-autoUpdate` file with it:

```json
{
    "autoUpdater": {
        "enabled": true,
        "updateSource": {
            "type": "Service",
            "baseUrl": "https://updates.example.com",
            "channel": "stable"
        },
        "updateIntervalMinutes": 60
    }
}
```

A product at version `1.0.0` will request `https://updates.example.com/update/darwin/1.0.0/stable` on macOS or `https://updates.example.com/update/win32/1.0.0/stable` on Windows. Your service must implement that route and return update responses compatible with the platform updater.

> ℹ️ *For details on update services, see the [Electron update documentation](https://www.electronjs.org/docs/latest/tutorial/updates#using-other-update-services).*

## Publishing with Electron Forge

Install the publisher required by your hosting provider and add it to `publishers` in `config/forge.config.js`. Keep its version compatible with the Electron Forge version required by the CLI. Preserve the generated makers and hooks when editing this file.

### Amazon S3

The S3 publisher uses the object path `<prefix>/<platform>/<architecture>/<filename>`. The prefix defaults to `"name"` from `package.json`, or can be set explicitly with `folder`.

The following example demonstrates consistent S3 paths for Windows `x64` and macOS `arm64` distributions with the prefix `my-product`. Apply these entries to the existing `makers` and `publishers` arrays in `config/forge.config.js`, preserving other settings and the generated hooks:

```javascript
const updateRoot = "https://my-app-updates.s3.amazonaws.com/my-product";
const windowsUpdateUrl = `${updateRoot}/win32/x64`;
const macUpdateUrl = `${updateRoot}/darwin/arm64`;

module.exports = {
    // Preserve the generated hooks and other configuration.
    makers: [
        {
            name: "@electron-forge/maker-squirrel",
            config: {
                loadingGif: "assets/install.gif",
                remoteReleases: windowsUpdateUrl
            }
        },
        {
            name: require.resolve("@interopio/iocd-cli/dist/services/installer/zip-maker"),
            config: {
                macUpdateManifestBaseUrl: macUpdateUrl
            }
        },
        {
            name: "@electron-forge/maker-dmg"
        }
    ],
    publishers: [
        {
            name: "@electron-forge/publisher-s3",
            config: {
                bucket: "my-app-updates",
                region: "us-east-1",
                folder: "my-product",
                public: true
            }
        }
    ]
};
```

Supply AWS credentials via the environment or the credential mechanism supported by the publisher. Set the platform updater `"baseUrl"` to the same directory as `windowsUpdateUrl` or `macUpdateUrl`, using separate platform merge files (see [Static Storage](#update_sources-static_storage)). Update every corresponding URL when changing the prefix or architecture.

> ⚠️ *Note that `public: true` uploads the objects with a public-read ACL. S3 buckets with ACLs disabled, the default for new buckets, reject these uploads. For such buckets, omit `public` and grant read access via a bucket policy or a CDN.*

> ⚠️ *Note that the CLI merges the Squirrel and DMG maker settings with its own, so use static `config` objects for these makers. The function form `config: (arch) => ({ ... })` shown in the Electron Forge documentation is ignored for them.*

> ℹ️ *For details on publisher options and authentication, see the [Electron Forge S3 publisher documentation](https://www.electronforge.io/config/publishers/s3).*

### GitHub Releases

The following example demonstrates a publisher for GitHub Releases:

```javascript
module.exports = {
    // Preserve the generated makers, hooks, and other configuration.
    publishers: [
        {
            name: "@electron-forge/publisher-github",
            config: {
                repository: {
                    owner: "your-org",
                    name: "your-repo"
                },
                prerelease: false
            }
        }
    ]
};
```

Publishing to GitHub Releases uploads artifacts. You must also provide a stable update metadata location or a compatible update service. A repository or release web page isn't itself a static update directory. Ensure that clients can access the referenced assets with the authentication available to the updater.

### Update Service Publisher

To upload to a compatible Electron Release Server, configure its publisher:

```javascript
module.exports = {
    // Preserve the generated makers, hooks, and other configuration.
    publishers: [
        {
            name: "@electron-forge/publisher-electron-release-server",
            config: {
                baseUrl: "https://updates.example.com",
                username: process.env.RELEASE_SERVER_USERNAME,
                password: process.env.RELEASE_SERVER_PASSWORD
            }
        }
    ]
};
```

Configure the platform with the service URL and channel described in [Update Service](#update_sources-update_service). For a custom service, use its supported upload API or administration interface.

## Update Metadata & Manual Uploads

Packaging generates update metadata independently of publishing. You can upload the resulting files manually without configuring an Electron Forge publisher.

### Windows

The Squirrel.Windows maker generates a setup executable, a full `.nupkg` package, and a `RELEASES` file. Upload these files to the directory configured as the Windows update source. Include generated delta packages if you use them. The package filenames must match the entries in `RELEASES`.

### macOS

The ZIP maker supplied by the CLI generates `RELEASES.json` when `macUpdateManifestBaseUrl` is configured. As of `@interopio/iocd-cli` 1.1.1, the CLI supplies its own ZIP maker. Seeds whose `config/forge.config.js` still lists `@electron-forge/maker-zip` use it automatically, with the same configuration.

The maker reads an existing manifest at the `macUpdateManifestBaseUrl` without credentials, updates the entry for the product version, and writes the resulting manifest beside the ZIP archive. A missing manifest returning HTTP `404` starts a new release history. Any other HTTP status or a connection failure fails the build.

> ⚠️ *Note that some storage services return HTTP `403` instead of `404` for a missing object (e.g., S3 buckets that don't allow listing for anonymous requests). Before the first release, upload a manifest without releases, `{"currentRelease": "", "releases": []}`, as `RELEASES.json` to that location.*

To set the release notes of the new entry, use the `macUpdateReleaseNotes` property of the maker configuration. The notes default to an empty string.

The following example demonstrates the manifest structure:

```json
{
    "currentRelease": "1.0.0",
    "releases": [
        {
            "version": "1.0.0",
            "updateTo": {
                "version": "1.0.0",
                "pub_date": "2026-10-18T10:00:00Z",
                "notes": "Initial release",
                "name": "My Product v1.0.0",
                "url": "https://cdn.example.com/my-product/darwin/arm64/My%20Product-darwin-arm64-1.0.0.zip"
            }
        }
    ]
}
```

Upload the generated ZIP archive and `RELEASES.json` to the directory identified by `macUpdateManifestBaseUrl`. Publish the DMG for initial installations as well. Keep the ZIP filename and URL generated by the maker. A publisher isn't required to generate the manifest, and you don't need to edit it manually when using this maker.

### Upload Procedure

1. Configure the platform update URL and the relevant maker settings.
2. Build with `npm run build` and locate the artifacts under `/dist/make` or your configured output directory.
3. Upload the packages before making the new metadata available to clients.
4. Upload `RELEASES` on Windows or `RELEASES.json` on macOS.
5. Check that the metadata and every referenced package URL are accessible to the clients.

## Verifying an Update

1. Install an earlier version using its signed Windows installer or macOS DMG.
2. Increase the product version in `package.json` and build the new distribution with the same update source.
3. Publish or manually upload the new packages and metadata.
4. Start the installed version and verify that it detects and installs the update.
5. Check the updated version, configuration, custom apps, and user data.

> ℹ️ *For details on build options, including the behavior of `--publish-only`, see the [Building for Production > Build Commands](https://docs.interop.io/desktop/developers/seed-project/building-for-production/index.md#build_commands) section.*
