Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
045d6c5
feat(ci): publish stable releases to the AUR as cap-bin
Wraient Oct 25, 2026
ffc34f8
fix(ci): bump pkgrel when the AUR recipe changes for the same release
Wraient Nov 3, 2026
8b2e5ee
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 3, 2026
7ff42ba
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 3, 2026
c6a5c1e
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 3, 2026
e28542d
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 3, 2026
ffcaa68
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 3, 2026
1955c49
fix(ci): validate AUR pkgrel as a plain integer before shell arithmetic
Wraient Nov 3, 2026
cc1a107
Merge remote-tracking branch 'origin/main' into HEAD
Wraient Nov 4, 2026
e409909
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
5bd0775
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
b94a80b
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
d3cec7a
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
fe94454
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
7f9438d
Merge remote-tracking branch 'origin/main' into HEAD
Wraient Nov 4, 2026
14dd970
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
06132c7
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 4, 2026
8b66dd9
Merge origin/main into feat/aur-publish
Wraient Nov 4, 2026
d9ac6ae
Merge remote-tracking branch 'origin/main' into feat/aur-publish
Wraient Nov 5, 2026
072b2d8
Merge origin/main into feat/aur-publish
Wraient Nov 5, 2026
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
167 changes: 167 additions & 0 deletions .github/workflows/publish-aur.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
name: "publish-aur"

# Publishes each stable release to the AUR as cap-bin. The PKGBUILD is the one
# scripts/build-linux-arch-package.sh builds the release's own pacman package
# from, pointed at the release's CrabNebula deb, so the two stay identical.
#
# Release events run this file from the tagged commit; for a tag that predates
# it, dispatch it from main with that tag. Pushing needs the AUR_SSH_PRIVATE_KEY
# secret of an AUR account that maintains cap-bin. Without it the job still
# builds the package and uploads the PKGBUILD as an artifact.

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Published stable release tag, e.g. cap-v0.6.1"
required: true
type: string

concurrency:
group: publish-aur
cancel-in-progress: false

permissions:
contents: read

jobs:
aur:
if: ${{ github.event_name == 'workflow_dispatch' || (startsWith(github.event.release.tag_name, 'cap-v') && !github.event.release.prerelease) }}
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
TAG: ${{ inputs.tag || github.event.release.tag_name }}
ARCH_IMAGE: archlinux:base-devel@sha256:68bfc3b0d277b08a99101dc9b94aaa03e5ae70cf1b4fb965c03b2b87b915760d
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Resolve release deb
id: release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
VERSION="${TAG#cap-v}"
if [[ "$TAG" != cap-v* || ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::'$TAG' is not a stable cap-v{x.y.z} release tag."
exit 1
fi
RELEASE="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,body)"
if [[ "$(jq -r '.isDraft or .isPrerelease' <<< "$RELEASE")" != false ]]; then
echo "::error::$TAG is not a published stable release."
exit 1
fi
DEB_URL="$(jq -r '.body | capture("<!--\\s*DOWNLOADS_JSON\\s*(?<downloads>\\{[^}]+\\})\\s*-->").downloads | fromjson | .["linux-deb"] // empty' <<< "$RELEASE")"
if [[ ! "$DEB_URL" =~ ^https://cdn\.crabnebula\.app/asset/[0-9A-Za-z]+$ ]]; then
echo "::error::$TAG has no CrabNebula linux-deb link in its DOWNLOADS_JSON."
exit 1
fi
{
echo "version=$VERSION"
echo "deb_url=$DEB_URL"
} >> "$GITHUB_OUTPUT"

- name: Build AUR package
shell: bash
env:
DEB_URL: ${{ steps.release.outputs.deb_url }}
VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/deb" "$RUNNER_TEMP/aur-package"
curl -fsSL --retry 5 --retry-all-errors -o "$RUNNER_TEMP/deb/Cap.deb" "$DEB_URL"
docker run --rm --network none \
--env CAP_PACKAGE_UID="$(id -u)" \
--volume "$GITHUB_WORKSPACE:/source:ro" \
--volume "$RUNNER_TEMP/deb:/input:ro" \
--volume "$RUNNER_TEMP/aur-package:/output" \
"$ARCH_IMAGE" \
bash -c 'useradd --uid "$CAP_PACKAGE_UID" --create-home cap-package && runuser -u cap-package -- bash /source/scripts/build-linux-arch-package.sh /input/Cap.deb /output "$1"' \
bash "$DEB_URL"
PKGVER="$(awk -F ' = ' '$1 == "\tpkgver" { print $2 }' "$RUNNER_TEMP/aur-package/.SRCINFO")"
if [[ "$PKGVER" != "$VERSION" ]]; then
echo "::error::The linux-deb link holds Cap $PKGVER, not $VERSION."
exit 1
fi

- name: Upload AUR package files
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: aur-cap-bin-${{ steps.release.outputs.version }}
path: |
${{ runner.temp }}/aur-package/PKGBUILD
${{ runner.temp }}/aur-package/.SRCINFO
include-hidden-files: true
overwrite: true
if-no-files-found: error

- name: Push to the AUR
shell: bash
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
if [[ -z "$AUR_SSH_PRIVATE_KEY" ]]; then
echo "::warning::AUR_SSH_PRIVATE_KEY is not set, so cap-bin $VERSION was built but not pushed to the AUR."
exit 0
fi
install -d -m 700 "$HOME/.ssh"
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$HOME/.ssh/aur"
chmod 600 "$HOME/.ssh/aur"
echo "aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN" > "$HOME/.ssh/known_hosts"
export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/aur -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes"
PACKAGE="$RUNNER_TEMP/aur-package"
PKGBASE="$(awk -F ' = ' '$1 == "pkgbase" { print $2 }' "$PACKAGE/.SRCINFO")"
AUR="$RUNNER_TEMP/aur"
git -c init.defaultBranch=master clone "ssh://aur@aur.archlinux.org/$PKGBASE.git" "$AUR"
version_of() {
awk -F ' = ' '$1 == "\tepoch" { epoch = $2 ":" } $1 == "\tpkgver" { pkgver = $2 } $1 == "\tpkgrel" { pkgrel = $2 } END { print epoch pkgver "-" pkgrel }' "$1"
}
upstream_of() {
awk -F ' = ' '$1 == "\tepoch" { epoch = $2 ":" } $1 == "\tpkgver" { pkgver = $2 } END { print epoch pkgver }' "$1"
}
pkgrel_of() {
awk -F ' = ' '$1 == "\tpkgrel" { print $2 }' "$1"
}
if [[ -f "$AUR/.SRCINFO" ]]; then
CURRENT="$(version_of "$AUR/.SRCINFO")"
NEW="$(version_of "$PACKAGE/.SRCINFO")"
if (( $(docker run --rm --network none "$ARCH_IMAGE" vercmp "$NEW" "$CURRENT") < 0 )); then
echo "::notice::The AUR already has $PKGBASE $CURRENT, which is newer than $NEW."
exit 0
fi
if [[ "$(upstream_of "$AUR/.SRCINFO")" == "$(upstream_of "$PACKAGE/.SRCINFO")" ]] &&
! cmp -s "$AUR/PKGBUILD" "$PACKAGE/PKGBUILD"; then
# Same upstream version but a changed recipe: installed users
# only upgrade on a higher pkgrel, so carry the AUR's forward.
# The AUR clone is not Cap-controlled, so never let its pkgrel
# reach shell arithmetic unvalidated: bash re-evaluates command
# substitutions inside $(( )), which would run attacker-chosen
# commands with AUR_SSH_PRIVATE_KEY in scope. Fail closed.
AUR_PKGREL="$(pkgrel_of "$AUR/.SRCINFO")"
if ! [[ "$AUR_PKGREL" =~ ^[0-9]+$ ]]; then
echo "::error::Refusing to publish: AUR .SRCINFO has non-numeric pkgrel: $AUR_PKGREL" >&2
exit 1
fi
NEW_PKGREL=$(( AUR_PKGREL + 1 ))
sed -i "s/^pkgrel=.*/pkgrel=$NEW_PKGREL/" "$PACKAGE/PKGBUILD"
sed -i "s/^\(\tpkgrel = \).*/\1$NEW_PKGREL/" "$PACKAGE/.SRCINFO"
echo "::notice::Recipe changed for $PKGBASE $VERSION, publishing as pkgrel $NEW_PKGREL."
fi
fi
cp "$PACKAGE/PKGBUILD" "$PACKAGE/.SRCINFO" "$AUR/"
cd "$AUR"
git add PKGBUILD .SRCINFO
if git diff --cached --quiet; then
Comment on lines +159 to +162

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Recipe fixes miss installed users

Re-running publish-aur after changing the recipe can push different files with the same package version. The builder always writes pkgrel=1, and this step allows equal versions whenever the files differ. For example, correcting depends and dispatching the same release tag updates the AUR recipe, but installed users see no newer version to upgrade to.

Increment pkgrel for same-release package changes, or reject changed recipes with an unchanged package version.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/publish-aur.yml
Line: 135-138

Comment:
**Recipe fixes miss installed users**

Re-running `publish-aur` after changing the recipe can push different files with the same package version. The builder always writes `pkgrel=1`, and this step allows equal versions whenever the files differ. For example, correcting `depends` and dispatching the same release tag updates the AUR recipe, but installed users see no newer version to upgrade to.

Increment `pkgrel` for same-release package changes, or reject changed recipes with an unchanged package version.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ffc34f8. When the AUR already carries the same upstream version and the generated PKGBUILD differs, the push step now carries the AUR pkgrel forward by one in both the PKGBUILD and .SRCINFO before pushing, so a recipe-only change reaches installed users as an upgrade. Verified against a local bare repo standing in for the AUR: changed recipe at the same version publishes as 0.6.1-2, an identical re-run does nothing, a newer version still publishes at pkgrel 1, and an older version is still skipped.

echo "The AUR already has $PKGBASE $VERSION."
exit 0
fi
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" commit -m "Update to $(version_of .SRCINFO)"
git push origin HEAD:master
18 changes: 18 additions & 0 deletions packaging/linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ The builder reuses the exact DEB payload, changes the package-format marker, and

On Omarchy, screen capture uses the installed Hyprland desktop portal and PipeWire. Package installation alone does not verify recording, playback, export, or device access; these require tests in the target desktop session.

## AUR

`.github/workflows/publish-aur.yml` publishes each stable release to the AUR as `cap-bin`, installable with `yay -S cap-bin` or `paru -S cap-bin`. When a `cap-v{x.y.z}` GitHub release is published, it downloads the `linux-deb` asset listed in the release's `DOWNLOADS_JSON` and runs the builder with that URL:

```sh
bash scripts/build-linux-arch-package.sh Cap_0.6.1_amd64.deb ./aur https://cdn.crabnebula.app/asset/<asset-id>
```

Given a download URL, the builder also writes the `PKGBUILD` it used and its `.SRCINFO` to the output directory. The `PKGBUILD` downloads the versioned DEB from that URL, so AUR builds produce the same package as the release. The workflow checks that the DEB holds the tagged version, then pushes both files to `ssh://aur@aur.archlinux.org/cap-bin.git`. It skips the push when the AUR already has a newer version or the same files.

One-time setup:

1. Create an AUR account for Cap and add a dedicated SSH public key to it, for example from `ssh-keygen -t ed25519 -f cap-aur -C cap-aur`.
2. Store the private key as the `AUR_SSH_PRIVATE_KEY` repository secret. Without it, the workflow builds the package and uploads the `PKGBUILD` and `.SRCINFO` as an artifact, but does not push.
3. Run `publish-aur` from `main` with the latest release tag, such as `cap-v0.6.1`. The first push creates `cap-bin` with that account as its maintainer.

A release event runs the workflow file from the tagged commit. For a tag that predates the workflow, such as a promoted nightly from before it was added, run it from `main` with that tag.

## CLI camera in Instant mode

On Linux, `cap record start --mode instant --camera DEVICE` composites the camera directly into the recorded screen or window. The CLI uses an unmirrored square at the bottom right, sized to 30% of the shorter screen edge with a 2% margin. Camera images are center-cropped without stretching. If camera frames stop arriving, screen recording continues without a stale camera image.
Expand Down
26 changes: 19 additions & 7 deletions scripts/build-linux-arch-package.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 2 ]]; then
printf 'Usage: %s <Cap.deb> <output-directory>\n' "$0" >&2
if [[ $# -lt 2 || $# -gt 3 ]]; then
printf 'Usage: %s <Cap.deb> <output-directory> [<deb-download-url>]\n' "$0" >&2
exit 1
fi

Expand All @@ -11,6 +11,12 @@ if [[ $(id -u) -eq 0 ]]; then
exit 1
fi

source_url="${3:-}"
if [[ -n "$source_url" && ! "$source_url" =~ ^https://[A-Za-z0-9._~%/:?=+-]+$ ]]; then
printf 'Unsupported download URL: %s\n' "$source_url" >&2
exit 1
fi

deb="$(realpath "$1")"
mkdir -p "$2"
output="$(realpath "$2")"
Expand Down Expand Up @@ -39,9 +45,11 @@ case "$architecture" in
*) printf 'Unsupported package architecture: %s\n' "$architecture" >&2; exit 1 ;;
esac

cp "$deb" "$work/Cap.deb"
checksum="$(sha256sum "$work/Cap.deb" | cut -d ' ' -f 1)"
deb_file="Cap_${version}_${architecture}.deb"
cp "$deb" "$work/$deb_file"
checksum="$(sha256sum "$work/$deb_file" | cut -d ' ' -f 1)"
cat > "$work/PKGBUILD" <<EOF
# Maintainer: Cap Software <hello at cap dot so>
pkgname=cap-bin
pkgver=${version//-/pre}
pkgrel=1
Expand All @@ -56,12 +64,12 @@ optdepends=("xdg-desktop-portal-hyprland: screen capture on Hyprland and Omarchy
provides=("cap=\$epoch:\$pkgver")
conflicts=("cap")
options=("!strip" "!debug")
source=("Cap.deb")
noextract=("Cap.deb")
source=("$deb_file${source_url:+::$source_url}")
noextract=("$deb_file")
sha256sums=("$checksum")

package() {
bsdtar -xOf "\$srcdir/Cap.deb" "$data_member" | bsdtar -xf - -C "\$pkgdir"
bsdtar -xOf "\$srcdir/$deb_file" "$data_member" | bsdtar -xf - -C "\$pkgdir"
for binary in Cap cap-cli cap-muxer; do
test -x "\$pkgdir/usr/bin/\$binary"
done
Expand All @@ -70,6 +78,10 @@ package() {
EOF

cd "$work"
if [[ -n "$source_url" ]]; then
cp PKGBUILD "$output/PKGBUILD"
makepkg --printsrcinfo > "$output/.SRCINFO"
fi
PKGDEST="$output" makepkg --nodeps --noconfirm
package="$(PKGDEST="$output" makepkg --packagelist)"
filename="${package##*/}"
Expand Down
Loading