Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-obtainium-app-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Publish an `obtainium.json` app config with every release so Android builds can be installed and auto-updated through Obtainium.
27 changes: 27 additions & 0 deletions .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,33 @@ jobs:
gh release upload "$TAG" "$f" --clobber
done

obtainium:
name: Publish Obtainium config
needs: [setup-release, android]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
env:
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.setup-release.outputs.ref }}
persist-credentials: false

- name: Generate obtainium.json
shell: bash
run: node scripts/obtainium-generate.js "$VERSION" "$TAG" obtainium.json

- name: Attach Obtainium config to release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$TAG" obtainium.json --clobber

ios:
name: Build iOS (AltStore/SideStore)
needs: setup-release
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ The stable web app is available at [app.sable.moe](https://app.sable.moe/) and t

You can also download our desktop app for Windows and Linux from [releases](https://github.com/SableClient/Sable/releases/latest). Release artifacts include build attestations, and desktop installations update automatically.

## Android (Obtainium)

Android APKs are published to every release, and [Obtainium](https://obtainium.imranr.dev) keeps them updated straight from GitHub. Each release also ships an `obtainium.json` app config. Use it for the nightly channel, where prereleases and date-based version tracking have to be enabled to follow the rolling `nightly` tag.

### Stable

<a href="https://intradeus.github.io/http-protocol-redirector?r=obtainium://add/https://github.com/SableClient/Sable"><img alt="Add to Obtainium" src="https://img.shields.io/badge/Add_to_Obtainium-6750A3?style=for-the-badge"></a>
&nbsp;
<a href="https://github.com/SableClient/Sable/releases/latest/download/obtainium.json"><img alt="App config" src="https://img.shields.io/badge/App_config-6B7280?style=for-the-badge"></a>
&nbsp;
<a href="https://github.com/SableClient/Sable/releases/latest"><img alt="Download APK" src="https://img.shields.io/badge/Download_APK-3DDC84?style=for-the-badge&logo=android"></a>

### Nightly

<a href="https://github.com/SableClient/Sable/releases/download/nightly/obtainium.json"><img alt="App config" src="https://img.shields.io/badge/App_config-6B7280?style=for-the-badge"></a>
&nbsp;
<a href="https://github.com/SableClient/Sable/releases/tag/nightly"><img alt="Download APK" src="https://img.shields.io/badge/Download_APK-3DDC84?style=for-the-badge&logo=android"></a>

### Setup & install

1. Install [Obtainium](https://github.com/ImranR98/Obtainium/releases/latest).
2. For stable, tap the button above, or add `https://github.com/SableClient/Sable` by hand under **Add App**.
3. For nightly, download the `obtainium.json` above and import it with **Import/Export → Import from file**.

Android builds are produced by the `android` job in [`tauri-build.yml`](.github/workflows/tauri-build.yml), and the config by the `obtainium` job in the same workflow.

## iOS (AltStore / SideStore)

Sable iOS builds are distributed as unsigned IPAs through [AltStore](https://altstore.io) and [SideStore](https://sidestore.io). Each release publishes both the IPA and an `altstore-source.json` manifest — stable builds to the [latest GitHub release](https://github.com/SableClient/Sable/releases/latest), nightly builds to the [`nightly` GitHub release](https://github.com/SableClient/Sable/releases/tag/nightly).
Expand Down
63 changes: 63 additions & 0 deletions scripts/obtainium-generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env node
//MISE hide=true
//MISE description="Generate obtainium.json for one-click app import"
/* oxlint-disable no-console */

import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import process from 'node:process';

const version = process.argv[2];
const tag = process.argv[3] || `v${version}`;
const outputPath = process.argv[4] || 'obtainium.json';

if (!version) {
console.error('Usage: node scripts/obtainium-generate.js <version> [tag] [output-path]');
process.exit(1);
}

const GITHUB_REPO = 'SableClient/Sable';
const APK_NAME = `Sable-${version}-android-universal.apk`;
const APK_URL = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${APK_NAME}`;
const isNightly = tag === 'nightly';

// Obtainium fills in the rest from its own defaults on import.
const additionalSettings = {
about: 'An almost stable Matrix client',
...(isNightly && {
includePrereleases: true,
// The nightly tag name never changes, so the re-uploaded APK's date is the
// only usable version, which in turn needs pseudo-versioning.
useLatestAssetDateAsReleaseDate: true,
releaseDateAsVersion: true,
versionDetection: false,
}),
};

const config = {
apps: [
{
id: 'moe.sable.client',
url: `https://github.com/${GITHUB_REPO}`,
author: 'SableClient',
name: 'Sable',
installedVersion: null,
latestVersion: version,
apkUrls: JSON.stringify([[APK_NAME, APK_URL]]),
otherAssetUrls: '[]',
preferredApkIndex: 0,
additionalSettings: JSON.stringify(additionalSettings),
lastUpdateCheck: null,
pinned: false,
categories: ['Communication'],
releaseDate: null,
changeLog: null,
overrideSource: 'GitHub',
allowIdChange: false,
pendingRepoRenameUrl: null,
},
],
};

writeFileSync(resolve(outputPath), JSON.stringify(config, null, 2) + '\n');
console.log(`Generated ${outputPath}`);
Loading