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
92 changes: 88 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# A pushed vX.Y.Z tag becomes a draft GitHub Release with the macOS build and
# the Windows installers attached; someone publishes it by hand after checking
# it (#68, #348). Started by hand instead, it builds without publishing and
# A pushed vX.Y.Z tag becomes a draft GitHub Release with the macOS build, the
# Windows installers and the Linux packages attached; someone publishes it by
# hand after checking it (#68, #348, #356). Started by hand instead, it builds without publishing and
# keeps each platform's files as a run artifact, to test the workflow without
# cutting a tag.
#
Expand Down Expand Up @@ -120,9 +120,93 @@ jobs:
path: dist/
if-no-files-found: error

linux:
# Each CPU builds on a runner of its own rather than cross-building, so
# each runner can install and start what it built (#356). Both on 24.04,
# whose AppArmor limits on sandboxes are the ones Ubuntu users have.
strategy:
matrix:
include:
- runner: ubuntu-24.04
arch: x64
- runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4

- name: Tag matches app version
if: github.event_name == 'push'
run: |
version=$(node -p "require('./app/package.json').version")
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match app/package.json version $version"
exit 1
fi

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm

- run: npm ci

# The RPM maker runs `rpmbuild`; the runner has dpkg and fakeroot for
# the .deb already.
- name: Install packaging tools
run: |
sudo apt-get update
sudo apt-get install -y rpm

- name: Build
run: npm run make -w app

# The makers already put the CPU in each name, the way each distro
# spells it: pbnotes_<version>_amd64.deb, pbnotes-<version>-1.x86_64.rpm.
- name: Collect the packages
run: |
shopt -s nullglob
debs=(app/out/make/deb/*/*.deb)
rpms=(app/out/make/rpm/*/*.rpm)
[ ${#debs[@]} -eq 1 ] && [ ${#rpms[@]} -eq 1 ] || {
echo "::error::Expected one .deb and one .rpm under app/out/make, found: ${debs[*]} ${rpms[*]}"
exit 1
}
mkdir dist
cp "${debs[0]}" "${rpms[0]}" dist/
ls -l dist

# Installed the way a user would, then started on a virtual screen. The
# app's code is the same on both CPUs; this is for packaging mistakes.
- name: Smoke-test the .deb
run: |
sudo apt-get install -y xvfb x11-utils ./dist/*.deb
app/scripts/smoke-launch.sh pbnotes

# In a Fedora container, as a user who isn't root: Electron won't run as
# root without --no-sandbox. The .rpm's sandbox relies on unprivileged
# user namespaces, which Fedora allows; the runner's Ubuntu kernel and
# Docker's defaults don't, so both are lifted for the container.
- name: Smoke-test the .rpm
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
docker run --rm \
--security-opt seccomp=unconfined --security-opt apparmor=unconfined \
-v "$PWD:/w:ro" fedora:latest bash -euc '
dnf install -y /w/dist/*.rpm xorg-x11-server-Xvfb xwininfo util-linux
useradd -m tester
runuser -u tester -- /w/app/scripts/smoke-launch.sh pbnotes
'

- uses: actions/upload-artifact@v4
with:
name: pbnotes-linux-${{ matrix.arch }}
path: dist/
if-no-files-found: error

release:
if: github.event_name == 'push'
needs: [macos, windows]
needs: [macos, windows, linux]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ The app is not yet signed, so the first time Windows shows "Windows protected yo

To uninstall, go to Settings → Apps → Installed apps, find Pb Notes and choose **Uninstall**. Your notes stay where they are: they live in the folders you picked, not in the app.

### Linux

Download the package for your distribution and your PC's processor:

| Distribution | Intel or AMD | ARM |
| --- | --- | --- |
| Debian, Ubuntu, Linux Mint, Pop!_OS | `pbnotes_…_amd64.deb` | `pbnotes_…_arm64.deb` |
| Fedora, RHEL, openSUSE | `pbnotes-…x86_64.rpm` | `pbnotes-…arm64.rpm` |

Not sure which processor? `uname -m` prints `x86_64` for Intel or AMD, and `aarch64` for ARM.

Install it from the folder you downloaded it to. On Debian or Ubuntu:

```sh
sudo apt install ./pbnotes_*.deb
```

On Fedora or RHEL (on openSUSE, `sudo zypper install` instead):

```sh
sudo dnf install ./pbnotes-*.rpm
```

Pb Notes then shows up among your apps. Neither package updates itself yet: to get a new version, download and install it the same way.

To uninstall, run `sudo apt remove pbnotes` or `sudo dnf remove pbnotes`. Your notes stay where they are: they live in the folders you picked, not in the app.

## Development

The repo is an npm workspace: `app/` and the packages under `packages/`. Install once, at the root:
Expand Down
13 changes: 11 additions & 2 deletions app/forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { execFileSync } from 'node:child_process';
import { readdirSync } from 'node:fs';
import path from 'node:path';

const linuxPackage = {
icon: 'assets/icon.png',
genericName: 'Notes',
categories: ['Office' as const],
homepage: 'https://github.com/shovon/pbnotes',
};

const config: ForgeConfig = {
packagerConfig: {
asar: true,
Expand Down Expand Up @@ -55,8 +62,10 @@ const config: ForgeConfig = {
// consume later (#67).
new MakerDMG({ icon: 'assets/icon.icns' }),
new MakerZIP({}, ['darwin']),
new MakerRpm({ options: { icon: 'assets/icon.png' } }),
new MakerDeb({ options: { icon: 'assets/icon.png' } }),
// Both write the same .desktop entry. `categories` would otherwise be
// GNOME;GTK;Utility, and `genericName` the app's own name again.
new MakerRpm({ options: linuxPackage }),
new MakerDeb({ options: linuxPackage }),
],
// No publishers: `release.yml` makes the draft Release from every
// platform's build at once (#348), so Forge publishes nothing itself.
Expand Down
75 changes: 75 additions & 0 deletions app/scripts/smoke-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Starts an installed Pb Notes on a virtual screen and checks that its window
# shows and that it is still running a few seconds later (#356). This catches a
# package that installs but can't start: the wrong CPU's Electron, a missing
# library, a sandbox that won't come up.
#
# smoke-launch.sh <command> [args...]
#
# Needs Xvfb and xwininfo. The app gets a throwaway HOME, so it never sees real
# settings or folders.
set -euo pipefail

[ $# -ge 1 ] || {
echo "usage: $0 <command> [args...]" >&2
exit 2
}

tmp=$(mktemp -d)
app=
xvfb=
cleanup() {
[ -n "$app" ] && kill -TERM -- "-$app" 2>/dev/null || true
[ -n "$xvfb" ] && kill "$xvfb" 2>/dev/null || true
wait 2>/dev/null || true
rm -rf "$tmp"
}
trap cleanup EXIT

fail() {
echo "::error::$1"
echo "--- app output ---"
cat "$tmp/app.log" || true
exit 1
}

# Xvfb picks a free display and writes its number to fd 3.
Xvfb -displayfd 3 -screen 0 1280x800x24 -nolisten tcp 3>"$tmp/display" 2>"$tmp/xvfb.log" &
xvfb=$!
for _ in $(seq 100); do
[ -s "$tmp/display" ] && break
sleep 0.1
done
[ -s "$tmp/display" ] || {
cat "$tmp/xvfb.log"
echo "::error::Xvfb did not start"
exit 1
}

mkdir -p "$tmp/home"
# Unset WAYLAND_DISPLAY so that, run from a Wayland desktop, the app still
# opens on the virtual screen and not the real one.
env -u WAYLAND_DISPLAY \
DISPLAY=":$(cat "$tmp/display")" \
HOME="$tmp/home" \
XDG_CONFIG_HOME="$tmp/home/.config" \
XDG_DATA_HOME="$tmp/home/.local/share" \
setsid "$@" >"$tmp/app.log" 2>&1 &
app=$!

# The window's title is "Pb Notes" and its class "pbnotes".
shown=
for _ in $(seq 60); do
kill -0 "$app" 2>/dev/null || fail "Pb Notes exited before its window showed"
if DISPLAY=":$(cat "$tmp/display")" xwininfo -root -tree | grep -Eq '"Pb Notes"|"pbnotes"'; then
shown=1
break
fi
sleep 1
done
[ -n "$shown" ] || fail "No Pb Notes window after 60 seconds"

sleep 5
kill -0 "$app" 2>/dev/null || fail "Pb Notes exited within 5 seconds of showing its window"

echo "Pb Notes started and showed its window."
Loading