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
86 changes: 66 additions & 20 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ on:
- patch
- minor
- major
mark_critical:
description: "Mandatory security update (clients can't dismiss the prompt)"
type: boolean
default: false
advisory_url:
description: "Optional advisory/release link shown on the security banner"
type: string
default: ""

# Never run two releases at once — they race on the version bump + tag.
concurrency:
Expand Down Expand Up @@ -212,18 +220,18 @@ jobs:
src-tauri/target/**/release/bundle/**/*.sig

build-flatpak:
# Wrap the .deb from the `build` job into a distro-agnostic .flatpak bundle
# (installs on Alpine, Arch, Fedora, Ubuntu, …). x86_64 ONLY: the builder
# image `bilelmoussaoui/flatpak-github-actions:gnome-47` ships no arm64
# manifest, so `docker pull` on an arm64 runner fails outright — native
# aarch64 flatpak isn't buildable with this toolchain. arm64 Linux users are
# covered by the .deb/.rpm/.AppImage arm64 artifacts; only the arm64 .flatpak
# is omitted. Revisit if the image gains arm64 (or via QEMU cross-build).
# The `gnome-47` tag MUST match `runtime-version` in flatpak/ai.canonic.app.yml.
# Wrap each arch's .deb (from the `build` job) into a distro-agnostic
# .flatpak bundle (installs on Alpine, Arch, Fedora, Ubuntu, …). Built
# NATIVELY per-arch on matching runners: x86_64 on ubuntu-latest, aarch64 on
# ubuntu-24.04-arm. We do NOT use the `bilelmoussaoui/...:gnome-47` container
# because it ships no arm64 image (docker pull fails on arm64 runners) —
# instead flatpak-builder is apt-installed and the GNOME runtime pulled from
# Flathub, which works identically on both arches. The runtime version below
# MUST match `runtime-version` in flatpak/ai.canonic.app.yml.
needs:
- compute-version
- build
timeout-minutes: 30
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
Expand All @@ -232,14 +240,33 @@ jobs:
artifact: release-linux-x64
deb_glob: "*_amd64.deb"
arch: x86_64
- runner: ubuntu-24.04-arm
artifact: release-linux-arm64
deb_glob: "*_arm64.deb"
arch: aarch64
runs-on: ${{ matrix.runner }}
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-47
options: --privileged
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Install Flatpak toolchain
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
# GitHub's ubuntu-24.04 images restrict unprivileged user namespaces
# via AppArmor, which breaks flatpak-builder's bwrap sandbox. Relax it
# (no-op if the knob doesn't exist on the runner image).
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true

- name: Install GNOME runtime + SDK (native arch)
# The manifest builds against org.gnome.{Platform,Sdk}//47. Flathub
# serves both x86_64 and aarch64, so this resolves to the runner's arch.
run: |
flatpak remote-add --if-not-exists --user \
flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y --noninteractive flathub \
org.gnome.Platform//47 org.gnome.Sdk//47

- name: Download Linux build (.deb)
uses: actions/download-artifact@v8
with:
Expand All @@ -265,12 +292,13 @@ jobs:
flatpak/ai.canonic.app.metainfo.xml

- name: Build Flatpak bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
with:
bundle: canonic_${{ needs.compute-version.outputs.version }}_${{ matrix.arch }}.flatpak
manifest-path: flatpak/ai.canonic.app.yml
# Per-arch cache so the two matrix legs don't clobber each other.
cache-key: flatpak-builder-${{ matrix.arch }}-${{ github.sha }}
env:
BUNDLE: canonic_${{ needs.compute-version.outputs.version }}_${{ matrix.arch }}.flatpak
run: |
flatpak-builder --user --force-clean --disable-rofiles-fuse \
--install-deps-from=flathub \
--repo=flatpak-repo flatpak-build flatpak/ai.canonic.app.yml
flatpak build-bundle flatpak-repo "$BUNDLE" ai.canonic.app master

- name: Upload Flatpak artifact
uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -358,16 +386,24 @@ jobs:
# with its signed bundle URL + detached signature (read from the .sig
# files). VERSION/REPO are passed via env (never interpolated into the
# script) so untrusted input can't reach the shell or node.
# MARK_CRITICAL/ADVISORY_URL flag a mandatory security update: from the
# workflow_dispatch input, or a `security-critical` label on the merged
# PR. They become unsigned manifest fields the client enforces (the
# binary is still signature-verified, so this can only force-with-update).
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.compute-version.outputs.version }}
REPO: Canonical-AI/canonic
MARK_CRITICAL: ${{ github.event.inputs.mark_critical == 'true' || contains(github.event.pull_request.labels.*.name, 'security-critical') }}
ADVISORY_URL: ${{ github.event.inputs.advisory_url }}
run: |
node <<'EOF'
const fs = require('fs');
const path = require('path');
const version = process.env.VERSION;
const repo = process.env.REPO;
const critical = process.env.MARK_CRITICAL === 'true';
const advisory = process.env.ADVISORY_URL || '';

// Recursively collect every downloaded artifact file.
const files = [];
Expand Down Expand Up @@ -408,12 +444,22 @@ jobs:
throw new Error('no updater artifacts found — check createUpdaterArtifacts + signing secrets');
}

fs.writeFileSync('latest.json', JSON.stringify({
const manifest = {
version,
notes: `See https://github.com/${repo}/releases/tag/v${version}`,
pub_date: new Date().toISOString(),
platforms,
}, null, 2));
};
// Custom fields the client reads via Update.raw_json to force a
// mandatory, non-dismissible security update.
if (critical) {
manifest.critical = true;
manifest.severity = 'critical';
manifest.advisory =
advisory || `https://github.com/${repo}/releases/tag/v${version}`;
}

fs.writeFileSync('latest.json', JSON.stringify(manifest, null, 2));
console.log(fs.readFileSync('latest.json', 'utf8'));
EOF
gh release upload "v$VERSION" latest.json --repo "$REPO" --clobber
Expand Down
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ Full usage guide, keyboard shortcuts, and feature walkthroughs:
curl -fsSL https://raw.githubusercontent.com/Canonical-AI/canonic/main/install.sh | sh
```

Detects your OS and architecture, downloads the right build, and installs it.
Detects your OS, architecture, and Linux distro, downloads the right build, and installs it.

- **macOS** (Apple Silicon) → `/Applications/Canonic.app`
- **Linux** (x64 / arm64) → `~/.local/bin/canonic` (AppImage)
- **macOS** (Apple Silicon) → `/Applications/canonic.app`
- **Linux** (glibc, x64 / arm64) → `~/.local/bin/canonic` (AppImage)
- **Arch / Alpine** (x64 / arm64) → Flatpak `ai.canonic.app` (AppImage can't run on musl-libc Alpine; Arch ships no FUSE by default)

To pin a specific version: `curl -fsSL ... | sh -s -- --version v0.1.2-alpha`
To pin a specific version: `curl -fsSL ... | sh -s -- --version v0.2.2-alpha`

<br />

Expand All @@ -84,35 +85,41 @@ brew install --cask canonic

### Linux

**AppImage**
**AppImage** (most glibc distros — Ubuntu, Fedora, …):

```bash
chmod +x canonic-*.AppImage
./canonic-*.AppImage
chmod +x canonic_*.AppImage
./canonic_*.AppImage
```

**Flatpak** (recommended on Alpine-based / postmarketOS):
**Flatpak** (Alpine / postmarketOS / Arch — and any musl-libc distro; x86_64 & arm64):

```bash
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user canonic-*.flatpak
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user canonic_*.flatpak
flatpak run ai.canonic.app
```

**Pacman** (Arch / postmarketOS):
**Debian / Ubuntu** (`.deb`):

```bash
sudo pacman -U canonic-*.pacman
sudo apt install ./canonic_*.deb
```

**Fedora / RHEL** (`.rpm`):

```bash
sudo dnf install ./canonic_*.rpm
```

### Direct download (all platforms)

Grab the latest build for your platform from the [Releases](https://github.com/Canonical-AI/canonic/releases) page:

* **macOS** — `.dmg` (Apple Silicon / Intel)
* **macOS** — `.dmg` (universal — Apple Silicon & Intel)
* **Windows** — `.exe`
* **Linux x64** — `.AppImage`, `.flatpak`, `.deb`, `.pacman`, `.tar.gz`
* **Linux arm64** — `.AppImage`, `.flatpak`, `.deb`, `.pacman`, `.tar.gz` (ARM Arch, postmarketOS, PinePhone, Raspberry Pi)
* **Linux x64** — `.AppImage`, `.flatpak`, `.deb`, `.rpm`
* **Linux arm64** — `.AppImage`, `.deb`, `.flatpak` (Raspberry Pi, PinePhone, postmarketOS, ARM servers)

<br />

Expand Down
78 changes: 76 additions & 2 deletions docs/specs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,21 @@ Source of truth for product requirements. When a requirement changes, update thi
when: the comments panel opens
then: the version filter is set to "All versions" and every unresolved comment is shown

* scenario: anchor text still present highlights normally
given: a comment whose quoted anchor text still appears in the open document
when: the comments panel is shown
then: the comment highlights in the document and shows no "text changed" warning

* scenario: anchor text changed shows a warning
given: a comment whose quoted anchor text no longer appears in the open document (it was edited)
when: the comments panel is shown
then: the comment is marked stale and shows a "Text has changed" badge instead of a missing highlight

* scenario: resolved or short anchors are never flagged stale
given: a resolved comment, or a comment whose quoted text is shorter than 2 characters
when: anchors are evaluated against the document
then: the comment is never marked stale

***

## Browser Commenting (BCM)
Expand Down Expand Up @@ -812,6 +827,11 @@ Source of truth for product requirements. When a requirement changes, update thi
when: an action is selected
then: the trigger `/` character is removed from the document before the block is inserted

* scenario: insert today's date
given: the slash menu is open at the root
when: the user selects "Today's date" (or types `today` and presses Enter)
then: the current date is inserted at the caret in ISO 8601 format (YYYY-MM-DD) using the local calendar day, and the trigger `/` is removed

### Agent slash commands

* scenario: root menu lists Review and Build
Expand Down Expand Up @@ -2016,6 +2036,38 @@ GNOME runtime carries glibc and webkit2gtk inside the sandbox.
when: the pipeline reaches finalize
then: no version bump, tag, or release is published (finalize depends on the Flatpak job)

## One-Line Installer (PKG-INS)

`install.sh` is the curl-pipe-sh installer. It detects OS, CPU architecture, and
Linux distro, then downloads the matching release asset (names match Tauri's
bundle output, e.g. `canonic_<version>_universal.dmg`,
`canonic_<version>_amd64.AppImage`, `canonic_<version>_x86_64.flatpak`).

* scenario: install on macOS
given: a user runs the installer on macOS (Apple Silicon)
when: the script detects `Darwin`
then: it downloads `canonic_<version>_universal.dmg`, mounts it, copies the `.app` into `/Applications`, and clears the Gatekeeper quarantine flag

* scenario: install on a glibc Linux distro
given: a user on a glibc distro (e.g. Ubuntu, Fedora) that is neither Arch nor Alpine
when: the script detects the distro
then: it downloads the AppImage (`amd64` on x86_64, `aarch64` on arm64) to `~/.local/bin/canonic` and marks it executable

* scenario: install on Arch or Alpine
given: a user on Arch or Alpine, x86_64 or arm64 (detected via `/etc/os-release` ID/ID_LIKE, or `/etc/arch-release` / `/etc/alpine-release`)
when: the script selects an install method
then: it installs the Flatpak matching the CPU (`canonic_<version>_x86_64.flatpak` or `canonic_<version>_aarch64.flatpak`) — adds the Flathub remote, then `flatpak install --user` — because Alpine is musl-libc and Arch ships no FUSE for AppImages

* scenario: Flatpak missing on Arch or Alpine
given: an Arch or Alpine user without `flatpak` installed
when: the installer reaches the Flatpak step
then: it exits with a message telling them to install flatpak (`doas apk add flatpak` / `sudo pacman -S flatpak`) and re-run

* scenario: pin a specific version
given: a user passes `--version <tag>`
when: the script resolves the version
then: it skips the GitHub "latest release" lookup and installs the named tag

## Auto-Update (UPD)

Canonic updates itself in place using the Tauri updater. On launch it checks the
Expand Down Expand Up @@ -2238,11 +2290,33 @@ pinned in `tauri.conf.json`, so an unsigned or tampered package is rejected.
when: the app starts
then: no greeting is shown and the stored target is cleared

### Mandatory security updates

* scenario: a critical update can't be dismissed
given: the release manifest marks the update `critical`
when: the update is detected
then: the over-editor banner and the left-panel widget both show a red "Security update required" notice with no close button (overriding the usual dismiss)

* scenario: advisory link is shown for security updates
given: a critical update whose manifest includes an advisory URL
when: the security banner is shown
then: it includes an "Advisory" link that opens the advisory in the browser

* scenario: forcing is best-effort and non-destructive
given: the critical flag lives in the unsigned part of the manifest
when: the client enforces it
then: it only nags/forces-with-update — the downloaded binary is still signature-verified before install, so a tampered flag can never run a malicious build

* scenario: releases can be flagged critical from CI
given: a release is published
when: the `mark_critical` workflow input is true or the merged PR has the `security-critical` label
then: `latest.json` is written with `critical: true`, `severity: critical`, and an advisory URL

### Demo Mode

* scenario: update flow is demoable end to end
given: the app is in demo mode
when: the user downloads and restarts the fake update
then: progress animates, then an "Updated to v0.3.0" greeting with a release-notes link is shown — no real binary or network is touched
when: the user downloads and restarts the fake (critical) update
then: a non-dismissible "Security update required" banner is shown, progress animates, then an "Updated to v0.3.0" greeting with a release-notes link — no real binary or network is touched

*Last updated: 2026-06-10*
Loading
Loading