Skip to content

Commit f8fe0fd

Browse files
authored
Feat/flatpak nightly channel (#1957)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description <!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. --> Fixes # #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. -->
2 parents 61e0e7c + 675a9e3 commit f8fe0fd

15 files changed

Lines changed: 589 additions & 5 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env node
2+
/* oxlint-disable no-console */
3+
4+
// Usage: render-flatpak-nightly.mjs <version> <x86_64-sha256:size> <aarch64-sha256:size> [date]
5+
// Pass "-" for an architecture that is not being published.
6+
7+
import { readFileSync, writeFileSync } from 'node:fs';
8+
import process from 'node:process';
9+
10+
const [version, x86, arm, dateArg] = process.argv.slice(2);
11+
12+
if (!version || !x86 || !arm) {
13+
console.error(
14+
'Usage: render-flatpak-nightly.mjs <version> <x86_64-sha256:size> <aarch64-sha256:size> [date]'
15+
);
16+
process.exit(1);
17+
}
18+
19+
const parseArch = (arch, value) => {
20+
if (value === '-') return null;
21+
const [sha, sizeStr] = value.split(':');
22+
if (!/^[0-9a-f]{64}$/.test(sha ?? '')) {
23+
console.error(`${arch}: expected a 64-character sha256 (got: ${sha})`);
24+
process.exit(1);
25+
}
26+
const size = Number(sizeStr);
27+
if (!Number.isInteger(size) || size <= 0) {
28+
console.error(`${arch}: expected a positive integer size (got: ${sizeStr})`);
29+
process.exit(1);
30+
}
31+
return { sha, size };
32+
};
33+
34+
const arches = {
35+
x86_64: parseArch('x86_64', x86),
36+
aarch64: parseArch('aarch64', arm),
37+
};
38+
39+
if (!arches.x86_64 && !arches.aarch64) {
40+
console.error('At least one architecture must be published.');
41+
process.exit(1);
42+
}
43+
44+
const date = dateArg || new Date().toISOString().slice(0, 10);
45+
const dir = 'packaging/flatpak/nightly';
46+
47+
let manifest = readFileSync(`${dir}/moe.sable.client.Nightly.yml.in`, 'utf8').replaceAll(
48+
'@VERSION@',
49+
version
50+
);
51+
52+
for (const [arch, values] of Object.entries(arches)) {
53+
const suffix = arch.toUpperCase();
54+
if (values) {
55+
manifest = manifest
56+
.replaceAll(`@SHA256_${suffix}@`, values.sha)
57+
.replaceAll(`@SIZE_${suffix}@`, String(values.size));
58+
continue;
59+
}
60+
const block = new RegExp(
61+
` - type: extra-data\\n(?: .*\\n)*? only-arches: \\[${arch}\\]\\n`
62+
);
63+
if (!block.test(manifest)) {
64+
console.error(`Could not find the ${arch} extra-data source to drop.`);
65+
process.exit(1);
66+
}
67+
manifest = manifest.replace(block, '');
68+
}
69+
70+
const leftover = manifest.match(/@[A-Z0-9_]+@/);
71+
if (leftover) {
72+
console.error(`Unsubstituted placeholder left in the manifest: ${leftover[0]}`);
73+
process.exit(1);
74+
}
75+
76+
writeFileSync(`${dir}/moe.sable.client.Nightly.yml`, manifest);
77+
writeFileSync(
78+
`${dir}/moe.sable.client.Nightly.metainfo.xml`,
79+
readFileSync(`${dir}/moe.sable.client.Nightly.metainfo.xml.in`, 'utf8')
80+
.replaceAll('@VERSION@', version)
81+
.replaceAll('@DATE@', date)
82+
);
83+
84+
console.log(`Rendered the nightly Flatpak manifest for ${version} (${date})`);

.github/workflows/tauri-build.yml

Lines changed: 196 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,12 +632,23 @@ jobs:
632632
env:
633633
GH_TOKEN: ${{ github.token }}
634634
run: |
635+
gh release view nightly --json assets > nightly-assets.json
636+
637+
# extra-data URLs in the published Flatpak repo point at these.
638+
jq -r '[.assets[] | select(.name | test("-linux-(x86_64|aarch64)\\.tar\\.gz$"))]
639+
| sort_by(.updatedAt) | reverse | .[0:3] | .[].name' \
640+
nightly-assets.json > flatpak-keep.txt
641+
635642
# .json manifests are overwritten in place, not accumulated.
636-
gh release view nightly --json assets \
637-
| jq -r --arg cutoff "$STARTED_AT" \
638-
'.assets[] | select(.updatedAt <= $cutoff and (.name | endswith(".json") | not)) | .name' \
643+
jq -r --arg cutoff "$STARTED_AT" \
644+
'.assets[] | select(.updatedAt <= $cutoff and (.name | endswith(".json") | not)) | .name' \
645+
nightly-assets.json \
639646
| while read -r name; do
640647
[ -n "$name" ] || continue
648+
if grep -Fxq "$name" flatpak-keep.txt; then
649+
echo "Keeping $name for the nightly Flatpak"
650+
continue
651+
fi
641652
echo "Removing superseded nightly asset: $name"
642653
gh release delete-asset nightly "$name" --yes
643654
done
@@ -846,6 +857,188 @@ jobs:
846857
git commit -m "nightly ${VERSION}"
847858
git push
848859
860+
build-nightly-flatpak:
861+
name: Build nightly Flatpak ${{ matrix.arch }}
862+
needs: [setup-release, linux]
863+
runs-on: ${{ matrix.runner }}
864+
timeout-minutes: 30
865+
if: ${{ needs.setup-release.outputs.nightly == 'true' }}
866+
strategy:
867+
fail-fast: false
868+
matrix:
869+
# x86_64 only, matching packaging/flatpak/flathub.json.
870+
include:
871+
- arch: x86_64
872+
runner: ubuntu-24.04
873+
permissions:
874+
contents: read
875+
env:
876+
ARCH: ${{ matrix.arch }}
877+
VERSION: ${{ needs.setup-release.outputs.version }}
878+
steps:
879+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
880+
with:
881+
persist-credentials: false
882+
883+
- name: Install flatpak-builder
884+
shell: bash
885+
run: |
886+
sudo apt-get update
887+
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder
888+
flatpak remote-add --user --if-not-exists flathub \
889+
https://dl.flathub.org/repo/flathub.flatpakrepo
890+
891+
- name: Measure the release tarball
892+
shell: bash
893+
env:
894+
GH_TOKEN: ${{ github.token }}
895+
run: |
896+
TARBALL="Sable-${VERSION}-linux-${ARCH}.tar.gz"
897+
gh release download nightly --repo "$GITHUB_REPOSITORY" \
898+
--pattern "$TARBALL" --dir tarball
899+
echo "SHA256=$(sha256sum "tarball/$TARBALL" | awk '{print $1}')" >> "$GITHUB_ENV"
900+
echo "SIZE=$(stat -c %s "tarball/$TARBALL")" >> "$GITHUB_ENV"
901+
902+
- name: Render the manifest
903+
shell: bash
904+
run: |
905+
if [ "$ARCH" = "x86_64" ]; then
906+
node .github/scripts/render-flatpak-nightly.mjs "$VERSION" "$SHA256:$SIZE" -
907+
else
908+
node .github/scripts/render-flatpak-nightly.mjs "$VERSION" - "$SHA256:$SIZE"
909+
fi
910+
911+
- name: Build the Flatpak
912+
shell: bash
913+
run: |
914+
# rofiles-fuse is unavailable on hosted runners.
915+
flatpak-builder \
916+
--user \
917+
--install-deps-from=flathub \
918+
--force-clean \
919+
--disable-rofiles-fuse \
920+
--default-branch=master \
921+
--repo=repo \
922+
builddir packaging/flatpak/nightly/moe.sable.client.Nightly.yml
923+
924+
- name: Upload the exported repo
925+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
926+
with:
927+
name: flatpak-nightly-repo-${{ matrix.arch }}
928+
path: repo
929+
if-no-files-found: error
930+
retention-days: 1
931+
include-hidden-files: true
932+
933+
publish-nightly-flatpak:
934+
name: Publish the nightly Flatpak repo
935+
needs: [setup-release, build-nightly-flatpak]
936+
runs-on: ubuntu-24.04
937+
timeout-minutes: 15
938+
if: ${{ needs.setup-release.outputs.nightly == 'true' }}
939+
permissions:
940+
contents: read
941+
pages: write
942+
id-token: write
943+
environment:
944+
name: github-pages
945+
url: ${{ steps.deploy.outputs.page_url }}
946+
env:
947+
VERSION: ${{ needs.setup-release.outputs.version }}
948+
steps:
949+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
950+
with:
951+
persist-credentials: false
952+
953+
- name: Check for the signing key
954+
id: gate
955+
shell: bash
956+
env:
957+
FLATPAK_GPG_KEY: ${{ secrets.FLATPAK_GPG_KEY }}
958+
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
959+
run: |
960+
if [ -z "${FLATPAK_GPG_KEY:-}" ] || [ -z "${FLATPAK_GPG_KEY_ID:-}" ]; then
961+
echo "::notice::FLATPAK_GPG_KEY/FLATPAK_GPG_KEY_ID are not set; skipping the Flatpak publish."
962+
echo "enabled=false" >> "$GITHUB_OUTPUT"
963+
else
964+
echo "enabled=true" >> "$GITHUB_OUTPUT"
965+
fi
966+
967+
- name: Install flatpak
968+
if: ${{ steps.gate.outputs.enabled == 'true' }}
969+
shell: bash
970+
run: |
971+
sudo apt-get update
972+
sudo apt-get install -y --no-install-recommends flatpak ostree
973+
974+
- name: Download the exported repos
975+
if: ${{ steps.gate.outputs.enabled == 'true' }}
976+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
977+
with:
978+
pattern: flatpak-nightly-repo-*
979+
path: arch-repos
980+
981+
- name: Import the signing key
982+
if: ${{ steps.gate.outputs.enabled == 'true' }}
983+
shell: bash
984+
env:
985+
FLATPAK_GPG_KEY: ${{ secrets.FLATPAK_GPG_KEY }}
986+
run: |
987+
export GNUPGHOME="$RUNNER_TEMP/gnupg"
988+
mkdir -p "$GNUPGHOME"
989+
chmod 700 "$GNUPGHOME"
990+
printf '%s\n' "$FLATPAK_GPG_KEY" | gpg --batch --import
991+
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
992+
993+
- name: Assemble and sign the repo
994+
if: ${{ steps.gate.outputs.enabled == 'true' }}
995+
shell: bash
996+
env:
997+
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
998+
run: |
999+
# A fresh repo each night: older commits point at pruned assets.
1000+
ostree --repo=site/repo init --mode=archive-z2
1001+
for src in arch-repos/*/; do
1002+
echo "Pulling $src"
1003+
ostree --repo=site/repo pull-local "$src"
1004+
done
1005+
flatpak build-update-repo \
1006+
--title="Sable Nightly" \
1007+
--default-branch=master \
1008+
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
1009+
--gpg-homedir="$GNUPGHOME" \
1010+
site/repo
1011+
ostree --repo=site/repo refs
1012+
1013+
- name: Write the .flatpakrepo and landing page
1014+
if: ${{ steps.gate.outputs.enabled == 'true' }}
1015+
shell: bash
1016+
env:
1017+
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
1018+
run: |
1019+
OWNER="${GITHUB_REPOSITORY%%/*}"
1020+
NAME="${GITHUB_REPOSITORY##*/}"
1021+
BASE="https://$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]').github.io/$NAME"
1022+
GPGKEY="$(gpg --homedir "$GNUPGHOME" --export "$FLATPAK_GPG_KEY_ID" | base64 -w0)"
1023+
1024+
sed -e "s|@REPO_URL@|$BASE/repo/|" -e "s|@GPGKEY@|$GPGKEY|" \
1025+
packaging/flatpak/nightly/sable-nightly.flatpakrepo.in \
1026+
> site/sable-nightly.flatpakrepo
1027+
1028+
sed -e "s|@BASE_URL@|$BASE|g" -e "s|@VERSION@|$VERSION|g" \
1029+
packaging/flatpak/nightly/index.html.in > site/index.html
1030+
1031+
- name: Upload the Pages artifact
1032+
if: ${{ steps.gate.outputs.enabled == 'true' }}
1033+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
1034+
with:
1035+
path: site
1036+
1037+
- name: Deploy to GitHub Pages
1038+
id: deploy
1039+
if: ${{ steps.gate.outputs.enabled == 'true' }}
1040+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
1041+
8491042
distribute-homebrew:
8501043
name: Publish Homebrew cask
8511044
needs: [setup-release, build]

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,7 @@ result
7070
## auto-generated pre-commit config
7171
.pre-commit-config.yaml
7272
src-tauri/icons/generated/
73+
74+
# Rendered by render-flatpak-nightly.mjs
75+
packaging/flatpak/nightly/moe.sable.client.Nightly.yml
76+
packaging/flatpak/nightly/moe.sable.client.Nightly.metainfo.xml

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ brew install --cask SableClient/sable/sable
2727

2828
The fully qualified name matters: since [Homebrew 6.0.0](https://brew.sh/2026/06/11/homebrew-6.0.0/) non-official taps need explicit trust, and installing this way trusts just this one cask. `brew tap` followed by a short-name install now fails unless you also run `brew trust`.
2929

30+
On Linux, nightly builds have their own Flatpak remote, rebuilt from `dev` every night:
31+
32+
```sh
33+
flatpak remote-add --if-not-exists sable-nightly https://sableclient.github.io/Sable/sable-nightly.flatpakrepo
34+
flatpak install sable-nightly moe.sable.client.Nightly
35+
```
36+
37+
It installs alongside the stable Flathub build under a separate application ID. Built by the `build-nightly-flatpak` and `publish-nightly-flatpak` jobs in [`tauri-build.yml`](.github/workflows/tauri-build.yml).
38+
3039
## Android (Obtainium)
3140

3241
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.
3.2 KB
Loading
7.29 KB
Loading

packaging/flatpak/icons/32x32.png

461 Bytes
Loading

packaging/flatpak/icons/64x64.png

1.24 KB
Loading

packaging/flatpak/icons/icon.svg

Lines changed: 7 additions & 0 deletions
Loading

packaging/flatpak/moe.sable.client.metainfo.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
<url type="bugtracker">https://github.com/SableClient/Sable/issues</url>
1717
<url type="vcs-browser">https://github.com/SableClient/Sable</url>
1818

19+
<branding>
20+
<color type="primary" scheme_preference="light">#b9abfa</color>
21+
<color type="primary" scheme_preference="dark">#4b3f8f</color>
22+
</branding>
23+
1924
<content_rating type="oars-1.1">
2025
<content_attribute id="social-chat">intense</content_attribute>
2126
<content_attribute id="social-audio">intense</content_attribute>
@@ -43,10 +48,10 @@
4348

4449
<launchable type="desktop-id">moe.sable.client.desktop</launchable>
4550

46-
<screenshots>
51+
<screenshots>
4752
<screenshot type="default">
4853
<image>https://raw.githubusercontent.com/SableClient/Sable/dev/docs/screenshots/timeline.png</image>
49-
<caption>A room timeline</caption>
54+
<caption>Catching up on a room timeline</caption>
5055
</screenshot>
5156
</screenshots>
5257

0 commit comments

Comments
 (0)