Repository navigation
359 lines (336 loc) · 17.1 KB
/
Copy pathflatpak.yml
File metadata and controls
359 lines (336 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
name: Flatpak
on:
workflow_run:
workflows: ["Release"]
types:
- completed
# Manual runs rebuild the flatpak for the LATEST release and re-mirror it
# to R2 — the backfill path when the mirror below was added after a
# release, or when a flatpak needs regenerating without a new tag.
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: Build Flatpak
runs-on: ubuntu-22.04
# Serialize runs from tags pushed in quick succession: a slower build for
# an older release must not finish after a newer one and overwrite the
# release asset and latest/clai.flatpak with stale content.
concurrency:
group: flatpak-build
cancel-in-progress: false
# Manual dispatches only count from main: a branch dispatch would build
# the flatpak from that branch's packaging files and overwrite both the
# release asset and the latest/ mirror with unreleased content.
if: ${{ (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get release info
id: release
uses: actions/github-script@v7
with:
script: |
const release = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo,
}).then(r => r.data);
const debAsset = release.assets.find(a => a.name.endsWith('.deb'));
return {
id: release.id,
tag: release.tag_name,
deb_url: debAsset ? debAsset.browser_download_url : null
};
- name: Download deb package
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Env indirection, not ${{ }} inline in the script: a quote in the
# API-provided JSON must never be able to alter shell source.
RELEASE_JSON: ${{ steps.release.outputs.result }}
run: |
RELEASE_TAG=$(printf '%s' "$RELEASE_JSON" | jq -r '.tag')
gh release download "$RELEASE_TAG" --pattern "*.deb" --output clai.deb
- name: Extract binary from deb
run: |
ar x clai.deb
tar xf data.tar.*
ls -la usr/bin/
# The OSTree repo mirrored to R2 (served at
# https://download.clai.run/flatpak/repo) is only published when both
# the R2 mirror secrets and the repo signing key are configured.
# Without them this workflow degrades to the classic side-loadable
# bundle with no origin remote — exactly the pre-repo behavior — so a
# MISSING secret can never break the release train. A secret that is
# set but malformed fails the job loudly instead (import/signing
# errors must never silently ship an unsigned repo).
- name: Check repo publishing secrets
id: repocfg
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
LINUX_REPO_GPG_PRIVATE_KEY: ${{ secrets.LINUX_REPO_GPG_PRIVATE_KEY }}
run: |
if [ -n "$LINUX_REPO_GPG_PRIVATE_KEY" ] && [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ] && [ -n "$R2_ACCOUNT_ID" ]; then
echo "publish=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::LINUX_REPO_GPG_PRIVATE_KEY and/or R2 secrets not configured; building a side-load bundle without the OSTree repo (see flatpak/README.md)."
echo "publish=false" >> "$GITHUB_OUTPUT"
fi
# Repo commits and summaries are GPG-signed so clients can verify what
# they pull from R2. The private key exists ONLY as the
# LINUX_REPO_GPG_PRIVATE_KEY secret (armored, no passphrase); see
# flatpak/README.md for how it was generated and what its loss means.
- name: Import repo signing key
if: steps.repocfg.outputs.publish == 'true'
id: gpg
env:
LINUX_REPO_GPG_PRIVATE_KEY: ${{ secrets.LINUX_REPO_GPG_PRIVATE_KEY }}
run: |
export GNUPGHOME="$RUNNER_TEMP/flatpak-gpg"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
printf '%s' "$LINUX_REPO_GPG_PRIVATE_KEY" | gpg --batch --import
KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/ {print $5; exit}')
if [ -z "$KEY_ID" ]; then
echo "::error::LINUX_REPO_GPG_PRIVATE_KEY did not contain a usable secret key"
exit 1
fi
gpg --export "$KEY_ID" > "$RUNNER_TEMP/clai-flatpak.gpg"
echo "keyid=$KEY_ID" >> "$GITHUB_OUTPUT"
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
# The repo keeps its commit history across releases: pull the previous
# state, let build-export append the new commit, push it back below.
# First release: the prefix doesn't exist yet and sync creates an
# empty dir, which build-export initializes. A workflow_dispatch
# rebuild of the LATEST release appends a second commit for the same
# version — clients then see one no-op update to identical content
# (objects are content-addressed, so the download is metadata-only).
- name: Pull existing OSTree repo from R2
if: steps.repocfg.outputs.publish == 'true'
env:
BUCKET: clai-releases
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
run: |
mkdir -p repo
aws s3 sync "s3://$BUCKET/flatpak/repo" repo --endpoint-url "$R2_ENDPOINT"
# S3/R2 cannot store empty directories, so the sync drops the
# OSTree repo skeleton (refs/remotes, refs/mirrors, ...). ostree
# requires them: build-update-repo fails with "Listing refs:
# opendir(refs/remotes): No such file or directory". Restore them
# whenever the pull found an existing repo.
if [ -f repo/config ]; then
mkdir -p repo/objects repo/refs/heads repo/refs/mirrors \
repo/refs/remotes repo/state repo/tmp/cache repo/extensions
fi
- name: Install Flatpak tools
run: |
sudo apt-get update
sudo apt-get install -y flatpak
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install -y flathub org.gnome.Platform//49 org.gnome.Sdk//49
- name: Create Flatpak
env:
PUBLISH_REPO: ${{ steps.repocfg.outputs.publish }}
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }}
REPO_URL: https://download.clai.run/flatpak/repo
run: |
# Initialize flatpak build directory
flatpak build-init flatpak-build run.clai.CLAI org.gnome.Sdk//49 org.gnome.Platform//49
# Copy files into the build
mkdir -p flatpak-build/files/bin
mkdir -p flatpak-build/files/share/applications
mkdir -p flatpak-build/files/share/icons/hicolor/128x128/apps
mkdir -p flatpak-build/files/share/icons/hicolor/scalable/apps
mkdir -p flatpak-build/files/share/metainfo
mkdir -p flatpak-build/files/share/licenses/run.clai.CLAI
cp usr/bin/clai flatpak-build/files/bin/
cp flatpak/run.clai.CLAI.desktop flatpak-build/files/share/applications/
cp src-tauri/icons/128x128.png flatpak-build/files/share/icons/hicolor/128x128/apps/run.clai.CLAI.png
cp public/icon.svg flatpak-build/files/share/icons/hicolor/scalable/apps/run.clai.CLAI.svg
cp flatpak/run.clai.CLAI.metainfo.xml flatpak-build/files/share/metainfo/
cp LICENSE flatpak-build/files/share/licenses/run.clai.CLAI/
# Finalize the build
#
# --talk-name=org.freedesktop.Flatpak is REQUIRED: clai runs the
# AI provider CLIs (claude/codex/…) and its bash_exec sandbox on
# the HOST via `flatpak-spawn --host`. The host-spawn portal is
# gated behind this name; without it both the provider CLIs and
# the sandboxed shell fail. The bash_exec sandbox runs the host's
# bwrap through flatpak-spawn because nested user namespaces are
# blocked inside Flatpak's own sandbox.
# --socket=x11 (NOT fallback-x11) is REQUIRED for image paste: arboard
# reads the image clipboard via the Wayland data-control protocol,
# which GNOME/Mutter does not implement, so it falls back to the X11
# (XWayland) clipboard. fallback-x11 withholds the X11 socket whenever
# Wayland is present, silently breaking Ctrl+V image paste.
flatpak build-finish flatpak-build \
--command=clai \
--share=ipc \
--share=network \
--socket=x11 \
--socket=wayland \
--device=dri \
--filesystem=home \
--talk-name=org.freedesktop.secrets \
--talk-name=org.freedesktop.Flatpak
# Export and bundle. With repo publishing configured, the commit
# and summary are GPG-signed and the bundle carries the origin
# remote URL + public key, so installing the bundle auto-attaches
# the remote and future releases arrive via `flatpak update` /
# GNOME Software. Without it: the classic unsigned side-load
# bundle (old installs keep the in-app notify-only fallback).
if [ "$PUBLISH_REPO" = "true" ]; then
flatpak build-export --gpg-sign="$GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" repo flatpak-build
flatpak build-update-repo --gpg-sign="$GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" --generate-static-deltas repo
flatpak build-bundle --repo-url="$REPO_URL" --gpg-keys="$RUNNER_TEMP/clai-flatpak.gpg" repo clai.flatpak run.clai.CLAI
else
flatpak build-export repo flatpak-build
flatpak build-bundle repo clai.flatpak run.clai.CLAI
fi
- name: Upload to Release
uses: actions/github-script@v7
env:
# Same env indirection as the shell steps: never inline the
# API-provided JSON into script source.
RELEASE_JSON: ${{ steps.release.outputs.result }}
with:
script: |
const fs = require('fs');
const releaseId = JSON.parse(process.env.RELEASE_JSON).id;
// Check if asset already exists and delete it
const assets = await github.rest.repos.listReleaseAssets({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId
});
const existingAsset = assets.data.find(a => a.name === 'clai.flatpak');
if (existingAsset) {
console.log('Deleting existing clai.flatpak asset...');
await github.rest.repos.deleteReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: existingAsset.id
});
}
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
name: 'clai.flatpak',
data: fs.readFileSync('clai.flatpak')
});
# The Release workflow's sync-to-r2 job runs before this workflow even
# starts (workflow_run fires on completion), so clai.flatpak is never
# in the release when that mirror executes. Mirror it from here instead,
# with the same guards: silent no-op without secrets, tag-shape
# validation, immutable versioned copy, and a latest/ alias that
# pre-releases never touch.
- name: Check R2 credentials
id: creds
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
# Env indirection, not ${{ }} inline in the script: a quote in the
# API-provided JSON must never be able to alter shell source.
RELEASE_JSON: ${{ steps.release.outputs.result }}
run: |
TAG=$(printf '%s' "$RELEASE_JSON" | jq -r '.tag')
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$R2_ACCOUNT_ID" ]; then
echo "::notice::R2 secrets not configured; skipping flatpak mirror."
echo "configured=false" >> "$GITHUB_OUTPUT"
else
# The tag becomes an R2 object-key prefix; refuse surprising
# shapes (slashes etc.) before any aws call sees it.
if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+(\.[0-9]+)*(-[0-9A-Za-z.]+)*$'; then
echo "::error::Unexpected tag format: $TAG"
exit 1
fi
echo "configured=true" >> "$GITHUB_OUTPUT"
fi
- name: Mirror flatpak to R2
if: steps.creds.outputs.configured == 'true'
env:
BUCKET: clai-releases
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
# R2 does not implement the newer S3 integrity checksums that recent
# AWS CLI versions send by default; "when_required" is Cloudflare's
# documented compatibility setting.
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
RELEASE_JSON: ${{ steps.release.outputs.result }}
run: |
TAG=$(printf '%s' "$RELEASE_JSON" | jq -r '.tag')
aws s3 cp clai.flatpak "s3://$BUCKET/$TAG/clai.flatpak" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "public, max-age=31536000, immutable"
# getLatestRelease never returns pre-releases, but keep the same
# guard as the main mirror in case that lookup ever changes.
case "$TAG" in
*beta*|*alpha*)
echo "Pre-release $TAG: leaving latest/ untouched."
exit 0
;;
esac
aws s3 cp clai.flatpak "s3://$BUCKET/latest/clai.flatpak" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "public, max-age=300"
# Push the updated OSTree repo back to R2. Upload order matters for
# clients pulling mid-publish: content-addressed objects and static
# deltas first (immutable, cache forever), then the mutable metadata
# (config, refs, summary + signature) with a short edge cache so new
# releases become visible within minutes and refs never point at
# objects that have not been uploaded yet. summary and summary.sig
# are still two separate PUTs — a client fetching between them can
# hit a transient verify failure; that window is unavoidable on
# plain object storage and a retry resolves it.
- name: Publish OSTree repo to R2
if: steps.repocfg.outputs.publish == 'true'
env:
BUCKET: clai-releases
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
REPO_URL: https://download.clai.run/flatpak/repo
run: |
aws s3 sync repo/objects "s3://$BUCKET/flatpak/repo/objects" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "public, max-age=31536000, immutable"
if [ -d repo/deltas ]; then
aws s3 sync repo/deltas "s3://$BUCKET/flatpak/repo/deltas" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "public, max-age=31536000, immutable"
fi
aws s3 sync repo "s3://$BUCKET/flatpak/repo" \
--exclude "objects/*" --exclude "deltas/*" \
--exclude "tmp/*" --exclude ".lock" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "public, max-age=300"
# The .flatpakrepo file users add with `flatpak remote-add`; it
# embeds the repo public key so the remote is added gpg-verified.
GPG_B64=$(base64 -w0 "$RUNNER_TEMP/clai-flatpak.gpg")
{
printf '%s\n' "[Flatpak Repo]"
printf '%s\n' "Title=CLAI"
printf '%s\n' "Url=$REPO_URL"
printf '%s\n' "Homepage=https://github.com/clairun/clai"
printf '%s\n' "Description=Desktop app for building, running, and supervising teams of AI agents"
printf '%s\n' "GPGKey=$GPG_B64"
} > clai.flatpakrepo
aws s3 cp clai.flatpakrepo "s3://$BUCKET/flatpak/clai.flatpakrepo" \
--endpoint-url "$R2_ENDPOINT" \
--content-type "text/plain" \
--cache-control "public, max-age=300"