diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml
new file mode 100644
index 0000000..0083ec3
--- /dev/null
+++ b/.github/workflows/sync-upstream.yml
@@ -0,0 +1,95 @@
+name: Sync Upstream Releases
+
+on:
+ schedule:
+ # Every 6 hours, off the hour to dodge the scheduling backlog at :00
+ - cron: '20 */6 * * *'
+ workflow_dispatch:
+ inputs:
+ packages:
+ description: 'Specific packages to update (space-separated, leave empty for all)'
+ required: false
+ default: ''
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ # Runs in an Arch container for vercmp: whether a release is an upgrade has
+ # to be decided by the same comparator pacman will use on users' machines.
+ - name: Update packages from upstream release feeds
+ run: |
+ docker run --rm \
+ -e PACKAGES="$PACKAGES" \
+ -e HOST_UID="$(id -u)" \
+ -e HOST_GID="$(id -g)" \
+ -v "$PWD/bin:/workspace/bin:ro" \
+ -v "$PWD/helpers:/workspace/helpers:ro" \
+ -v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
+ -w /workspace \
+ archlinux:base-devel bash -lc '
+ set -euo pipefail
+
+ pacman -Syu --noconfirm jq
+
+ groupadd -g "$HOST_GID" runner
+ useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
+ chown -R runner:runner /workspace/pkgbuilds
+
+ if [[ -n "${PACKAGES:-}" ]]; then
+ read -r -a package_args <<< "$PACKAGES"
+ runuser -u runner -- ./bin/sync-upstream "${package_args[@]}"
+ else
+ runuser -u runner -- ./bin/sync-upstream
+ fi
+ '
+ env:
+ PACKAGES: ${{ github.event.inputs.packages }}
+
+ - name: Check for changes
+ id: changes
+ run: |
+ if [ -z "$(git status --porcelain)" ]; then
+ echo "has_changes=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_changes=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Create Pull Request
+ if: steps.changes.outputs.has_changes == 'true'
+ uses: peter-evans/create-pull-request@v7
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ commit-message: 'chore: sync upstream releases'
+ title: 'chore: sync upstream releases'
+ body: |
+ Automated update of packages that track an upstream vendor release
+ feed rather than the AUR.
+
+ Each package reports its newest release through
+ `.omarchy/upstream.sh`.
+ branch: auto/sync-upstream
+ delete-branch: true
+ labels: automated
+ reviewers: ryanrhughes
+
+ - name: Notify Basecamp on failure
+ if: failure() && env.BASECAMP_CHATBOT_URL != ''
+ env:
+ BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
+ run: |
+ curl -s -o /dev/null \
+ -H "Content-Type: application/json" \
+ -d "$(jq -n --arg content \
+ "π΄ Upstream sync failed
View run" \
+ '{content: $content}')" \
+ "$BASECAMP_CHATBOT_URL"
diff --git a/README.md b/README.md
index 5f1e891..50c03f0 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ The filesystem no longer encodes release policy. Instead:
- all other packages reach `stable` by promoting tested edge artifacts with `bin/repo migrate`
- AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/`
- packages can opt out of unscoped builds with `skip_build`; explicit `--package` builds remain available
+- packages that follow a vendor release feed instead of the AUR carry an `.omarchy/upstream.sh` hook
## Prerequisites
### aarch64 Builds (Optional)
@@ -248,6 +249,40 @@ bin/sync-aur yay v4l2-relayd # Sync specific packages
AUR sync is metadata-driven. It preserves `.omarchy/`, replaces the package root with AUR contents, applies `.omarchy/patches/*.patch`, runs `.omarchy/post-sync.sh` when present, applies pkgrel metadata, removes AUR-only `.SRCINFO` and `.gitignore` files, and records `upstream_commit`.
+### Sync Upstream Releases
+
+```bash
+bin/sync-upstream # Update every package with an upstream hook
+bin/sync-upstream openai-codex-desktop # Update specific packages
+```
+
+Some vendors publish a release feed of their own that is faster and more precise
+than the AUR packaging of it. Those packages are `source: local` β Omarchy owns
+the PKGBUILD β and provide `.omarchy/upstream.sh`, a hook that reports the newest
+upstream release as JSON on stdout:
+
+```json
+{
+ "pkgver": "1.2.3",
+ "sha256sums": { "x86_64": [""], "aarch64": [""] }
+}
+```
+
+Architecture keys become `sha256sums_` in the PKGBUILD; the key `any` means
+the unsuffixed `sha256sums` array, and only the arrays a hook names are touched.
+An empty object (`{}`) reports no update, which is how a hook waits out a release
+that has landed for one architecture but not yet the other.
+
+When the reported version is newer than the checked-in one, `bin/sync-upstream`
+rewrites `pkgver` and those checksum arrays and resets `pkgrel` to 1. A version
+that is equal or older leaves the package alone, so a vendor rolling a release
+back cannot walk the repository backwards.
+
+Hooks should read checksums from whatever manifest the vendor publishes rather
+than downloading the artifacts β see `pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh`,
+which reads OpenAI's Debian package index and never fetches the 750 MB of debs
+it describes.
+
### Other
```bash
@@ -260,6 +295,7 @@ bin/repo push # Upload local builds to the host and publi
bin/add-package # Add an AUR/local package with metadata
bin/package-worktree # Create upstream/patched/current scratch workspace
bin/repo remove # Remove package
+bin/sync-upstream # Update packages that track a vendor release feed
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
```
@@ -345,7 +381,8 @@ omarchy-pkgs/
β βββ .omarchy/
β βββ package.json # Source/sync/release metadata
β βββ patches/ # Omarchy patches reapplied after AUR sync
-β βββ post-sync.sh # Optional dynamic post-sync customization hook
+β βββ post-sync.sh # Optional dynamic post-sync customization hook
+β βββ upstream.sh # Optional vendor release feed hook (non-AUR packages)
βββ build/
βββ build-output/ # Unsigned packages (temporary)
β βββ edge/
@@ -396,7 +433,7 @@ Minimal examples:
Fields:
-- `source`: `aur` or `local`
+- `source`: `aur` or `local`. A `local` package can still follow an upstream release with an `.omarchy/upstream.sh` hook.
- `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually.
- `aur`: optional AUR package name when it differs from the local package directory, usually for split packages.
- `release_ring`: optional. `fast` means the package is built directly for stable as well as edge. Packages without a ring build in edge and reach stable through tested artifact promotion (`bin/repo migrate`).
@@ -539,6 +576,7 @@ The repository includes GitHub workflows and systemd services for automated rele
#### GitHub Workflows
1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found.
+2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out.
#### Systemd Services
diff --git a/bin/sync-upstream b/bin/sync-upstream
new file mode 100755
index 0000000..93e6f0d
--- /dev/null
+++ b/bin/sync-upstream
@@ -0,0 +1,390 @@
+#!/bin/bash
+set -euo pipefail
+
+BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
+source "$BUILD_ROOT/helpers/message-helpers.sh"
+source "$BUILD_ROOT/helpers/paths.sh"
+source "$BUILD_ROOT/helpers/package-metadata.sh"
+
+TEMP_DIR=$(mktemp -d)
+trap 'rm -rf "$TEMP_DIR"' EXIT
+
+SPECIFIC_PACKAGES=()
+
+usage() {
+ cat </.omarchy/upstream.sh, a hook
+that reports the newest upstream release as JSON on stdout:
+
+ {
+ "pkgver": "1.2.3",
+ "sha256sums": { "x86_64": [""], "aarch64": [""] }
+ }
+
+Architecture keys become sha256sums_ in the PKGBUILD; the key "any" means
+the unsuffixed sha256sums array. An empty object ({}) reports no update.
+
+When the reported version is newer than the checked-in one, pkgver and the
+listed checksum arrays are rewritten and pkgrel is reset to 1.
+
+Arguments:
+ PACKAGE One or more package names to update (optional)
+
+Examples:
+ $0 # Update every package with an upstream hook
+ $0 openai-codex-desktop # Update specific packages
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --*)
+ print_error "Unknown option: $1"
+ exit 1
+ ;;
+ *)
+ SPECIFIC_PACKAGES+=("$1")
+ shift
+ ;;
+ esac
+done
+
+if ! command -v vercmp >/dev/null 2>&1; then
+ print_error "vercmp not found: this needs pacman to decide whether a release is an upgrade"
+ exit 1
+fi
+
+print_header "Upstream Package Sync"
+
+UPDATED=0
+SKIPPED=0
+FAILED=0
+SPECIFIC_MODE=false
+
+get_pkgver() {
+ local package_dir="$1"
+
+ grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'"
+}
+
+assert_single_assignment() {
+ local pkgbuild="$1"
+ local pattern="$2"
+ local label="$3"
+
+ if [[ $(grep -c "$pattern" "$pkgbuild") -ne 1 ]]; then
+ print_error "Expected exactly one $label assignment in $pkgbuild"
+ return 1
+ fi
+}
+
+set_pkgbuild_scalar() {
+ local pkgbuild="$1"
+ local field="$2"
+ local value="$3"
+
+ assert_single_assignment "$pkgbuild" "^${field}=" "$field" || return 1
+ sed -i "s/^${field}=.*/${field}=${value}/" "$pkgbuild"
+}
+
+# Replace an array assignment, however many lines the original spans.
+set_pkgbuild_array() {
+ local pkgbuild="$1"
+ local name="$2"
+ shift 2
+ local values=("$@")
+
+ assert_single_assignment "$pkgbuild" "^${name}=(" "$name" || return 1
+
+ if [[ ${#values[@]} -eq 0 ]]; then
+ print_error "No values to write for ${name}"
+ return 1
+ fi
+
+ local block="$TEMP_DIR/array-block"
+ if [[ ${#values[@]} -eq 1 ]]; then
+ printf "%s=('%s')\n" "$name" "${values[0]}" > "$block" || return 1
+ else
+ {
+ printf '%s=(\n' "$name"
+ printf " '%s'\n" "${values[@]}"
+ printf ')\n'
+ } > "$block" || return 1
+ fi
+
+ local rewritten="$TEMP_DIR/pkgbuild-rewritten"
+ if ! awk -v prefix="${name}=(" -v block="$block" '
+ !replaced && index($0, prefix) == 1 {
+ while ((getline line < block) > 0) print line
+ close(block)
+ replaced = 1
+ # A ")" anywhere past the opening closes the array; testing for one at end
+ # of line instead would treat a trailing comment as a continuation and eat
+ # every line up to the next ")".
+ if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
+ next
+ }
+ skipping { if ($0 ~ /\)/) skipping = 0; next }
+ { print }
+ ' "$pkgbuild" > "$rewritten"; then
+ print_error "Failed to rewrite ${name} in $pkgbuild"
+ rm -f "$rewritten"
+ return 1
+ fi
+
+ mv "$rewritten" "$pkgbuild"
+}
+
+# pacman's own comparator, because nothing else agrees with it at the corners:
+# sort -V calls 1.0a newer than 1.0, vercmp calls it older, and pacman is what
+# decides whether a published package is an upgrade.
+version_is_newer() {
+ local candidate="$1"
+ local current="$2"
+
+ [[ "$candidate" != "$current" ]] || return 1
+ [[ "$(vercmp "$candidate" "$current")" -gt 0 ]]
+}
+
+validate_release() {
+ local release="$1"
+
+ # pkgver is written into the PKGBUILD, which makepkg sources as shell, so it
+ # is held to pacman's own character set rather than merely being non-empty.
+ # The anchors are \A and \z, not ^ and $: jq's $ also matches before a
+ # trailing newline, which would let "1.0\n" through and break the rewrite.
+ jq -e '
+ (.pkgver | type == "string" and test("\\A[A-Za-z0-9._+]+\\z"))
+ and (.sha256sums | type == "object" and length > 0)
+ and (.sha256sums | to_entries | all(
+ .key | test("\\A[a-z0-9_]+\\z")
+ ))
+ and (.sha256sums | to_entries | all(
+ .value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z"))
+ ))
+ ' <<<"$release" >/dev/null
+}
+
+# Confirm the rewritten PKGBUILD parses and actually holds what we meant to put
+# in it. Editing shell with awk and sed can go wrong in ways no amount of
+# pattern-matching anticipates -- an array element carrying a ")" in a comment,
+# say -- so the result is checked rather than trusted.
+verify_pkgbuild() {
+ local pkgbuild="$1"
+ local release="$2"
+ local pkgver="$3"
+ shift 3
+ local arrays=("$@")
+
+ if ! bash -n "$pkgbuild" 2>/dev/null; then
+ print_error "Rewritten PKGBUILD is not valid shell"
+ return 1
+ fi
+
+ local dump
+ if ! dump=$(CARCH=x86_64 bash -c '
+ source "$1" >/dev/null 2>&1 || exit 1
+ printf "pkgver\t%s\n" "$pkgver"
+ printf "pkgrel\t%s\n" "$pkgrel"
+ for name in "${@:2}"; do
+ declare -n array="$name"
+ printf "%s\t%s\n" "$name" "${array[*]}"
+ done
+ ' _ "$pkgbuild" "${arrays[@]}" 2>/dev/null); then
+ print_error "Rewritten PKGBUILD could not be read back"
+ return 1
+ fi
+
+ local expected
+ expected=$(
+ printf 'pkgver\t%s\n' "$pkgver"
+ printf 'pkgrel\t1\n'
+ local array arch
+ for array in "${arrays[@]}"; do
+ arch="${array#sha256sums}"
+ arch="${arch#_}"
+ [[ -n "$arch" ]] || arch="any"
+ printf '%s\t%s\n' "$array" \
+ "$(jq -r --arg arch "$arch" '.sha256sums[$arch] | join(" ")' <<<"$release")"
+ done
+ )
+
+ if [[ "$dump" != "$expected" ]]; then
+ print_error "Rewritten PKGBUILD does not hold the reported release"
+ diff <(echo "$expected") <(echo "$dump") | sed 's/^/ /' >&2 || true
+ return 1
+ fi
+}
+
+apply_release() {
+ local package_dir="$1"
+ local release="$2"
+ local pkgver="$3"
+ local pkgbuild="$package_dir/PKGBUILD"
+
+ local arch array values
+ local arrays=()
+
+ while IFS= read -r arch; do
+ if [[ "$arch" == "any" ]]; then
+ array="sha256sums"
+ else
+ array="sha256sums_$arch"
+ fi
+ arrays+=("$array")
+ done < <(jq -r '.sha256sums | keys[]' <<<"$release")
+
+ # validate_release guarantees at least one entry, so an empty list here means
+ # jq died inside the process substitution rather than that there is nothing
+ # to do.
+ if [[ ${#arrays[@]} -eq 0 ]]; then
+ print_error "Could not read the checksum architectures from the reported release"
+ return 1
+ fi
+
+ # Every edit lands on a scratch copy that replaces the PKGBUILD in one rename
+ # at the end, so a failure part way through leaves the original untouched
+ # rather than half updated.
+ local scratch="$pkgbuild.sync-upstream"
+ cp "$pkgbuild" "$scratch" || return 1
+
+ if ! (
+ assert_single_assignment "$scratch" '^pkgver=' pkgver || exit 1
+ assert_single_assignment "$scratch" '^pkgrel=' pkgrel || exit 1
+
+ for array in "${arrays[@]}"; do
+ arch="${array#sha256sums}"
+ arch="${arch#_}"
+ [[ -n "$arch" ]] || arch="any"
+
+ mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release")
+ set_pkgbuild_array "$scratch" "$array" "${values[@]}" || exit 1
+ done
+
+ set_pkgbuild_scalar "$scratch" pkgver "$pkgver" || exit 1
+ set_pkgbuild_scalar "$scratch" pkgrel 1 || exit 1
+
+ verify_pkgbuild "$scratch" "$release" "$pkgver" "${arrays[@]}" || exit 1
+ ); then
+ rm -f "$scratch"
+ return 1
+ fi
+
+ chmod --reference="$pkgbuild" "$scratch"
+ mv "$scratch" "$pkgbuild"
+}
+
+sync_package() {
+ local package="$1"
+ local package_dir="$PKGBUILDS_DIR/$package"
+ local hook="$package_dir/.omarchy/upstream.sh"
+
+ if [[ ! -f "$package_dir/PKGBUILD" ]]; then
+ print_error "Package $package has no PKGBUILD"
+ ((++FAILED))
+ return 0
+ fi
+
+ if [[ ! -f "$hook" ]]; then
+ if [[ "$SPECIFIC_MODE" == true ]]; then
+ print_error "Package $package is missing .omarchy/upstream.sh"
+ ((++FAILED))
+ else
+ print_info "Skipping $package: no upstream hook"
+ ((++SKIPPED))
+ fi
+ return 0
+ fi
+
+ print_info "Checking $package for upstream releases..."
+
+ local release
+ if ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" bash .omarchy/upstream.sh); then
+ print_error "Upstream hook failed for $package"
+ ((++FAILED))
+ return 0
+ fi
+
+ if ! jq -e . >/dev/null 2>&1 <<<"$release"; then
+ print_error "Upstream hook for $package did not report valid JSON"
+ ((++FAILED))
+ return 0
+ fi
+
+ if [[ "$(jq -r 'has("pkgver")' <<<"$release")" == "false" ]]; then
+ print_info " No upstream update reported"
+ ((++SKIPPED))
+ return 0
+ fi
+
+ if ! validate_release "$release"; then
+ print_error "Upstream hook for $package reported a malformed release"
+ ((++FAILED))
+ return 0
+ fi
+
+ local pkgver current_pkgver
+ pkgver=$(jq -r '.pkgver' <<<"$release")
+ current_pkgver=$(get_pkgver "$package_dir")
+
+ if [[ -z "$current_pkgver" ]]; then
+ print_error "Could not read pkgver from $package_dir/PKGBUILD"
+ ((++FAILED))
+ return 0
+ fi
+
+ if [[ "$pkgver" == "$current_pkgver" ]]; then
+ print_info " Already at $current_pkgver"
+ ((++SKIPPED))
+ return 0
+ fi
+
+ if ! version_is_newer "$pkgver" "$current_pkgver"; then
+ print_warning " Upstream reports $pkgver, older than the checked-in $current_pkgver; leaving it alone"
+ ((++SKIPPED))
+ return 0
+ fi
+
+ if ! apply_release "$package_dir" "$release" "$pkgver"; then
+ print_error "Failed to update $package"
+ ((++FAILED))
+ return 0
+ fi
+
+ print_success " $current_pkgver -> $pkgver"
+ ((++UPDATED))
+}
+
+if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
+ SPECIFIC_MODE=true
+ for package in "${SPECIFIC_PACKAGES[@]}"; do
+ sync_package "$package"
+ done
+else
+ while IFS= read -r package; do
+ sync_package "$package"
+ done < <(packages_for_upstream_sync)
+fi
+
+echo ""
+if [[ $FAILED -gt 0 ]]; then
+ print_error "Upstream sync completed with failures"
+else
+ print_success "Upstream sync complete!"
+fi
+echo " Target: $PKGBUILDS_DIR"
+echo " Updated: $UPDATED"
+echo " Skipped: $SKIPPED"
+echo " Failed: $FAILED"
+
+if [[ $FAILED -gt 0 ]]; then
+ exit 1
+fi
diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh
index 1160fff..b1f1b70 100644
--- a/helpers/package-metadata.sh
+++ b/helpers/package-metadata.sh
@@ -135,6 +135,19 @@ packages_for_aur_sync() {
done
}
+package_has_upstream_hook() {
+ local pkgdir="$1"
+ [[ -f "$pkgdir/.omarchy/upstream.sh" ]]
+}
+
+packages_for_upstream_sync() {
+ package_dirs | while IFS= read -r pkgdir; do
+ if package_has_upstream_hook "$pkgdir"; then
+ basename "$pkgdir"
+ fi
+ done
+}
+
packages_for_mirror() {
local mirror="$1"
diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/package.json b/pkgbuilds/openai-codex-desktop/.omarchy/package.json
index 8d7f14b..db153c3 100644
--- a/pkgbuilds/openai-codex-desktop/.omarchy/package.json
+++ b/pkgbuilds/openai-codex-desktop/.omarchy/package.json
@@ -1,5 +1,4 @@
{
- "source": "aur",
- "release_ring": "fast",
- "upstream_commit": "05f12e3c51ce07efb996698d4fae94327f45f03b"
+ "source": "local",
+ "release_ring": "fast"
}
diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch b/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch
deleted file mode 100644
index 58bd5a8..0000000
--- a/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/PKGBUILD
-+++ b/PKGBUILD
-@@ -55,6 +55,12 @@
- conflicts=('chatgpt')
- options=('!debug' '!strip')
-
-+# Omarchy: the build image compresses with zstd at its default level, which
-+# leaves this 1.3 GB Electron tree at 447 MB. Maximum zstd takes it to 322 MB
-+# for ~4 extra minutes of build time -- worth it for a package this large that
-+# every user re-downloads on each of OpenAI's frequent releases.
-+COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -)
-+
- _deb_x86_64="chatgpt_${pkgver}_amd64.deb"
- _deb_aarch64="chatgpt_${pkgver}_arm64.deb"
- source=('chatgpt-launcher.sh')
diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh
new file mode 100755
index 0000000..a090a35
--- /dev/null
+++ b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+# OpenAI ships the ChatGPT desktop app from its own Debian repository. The
+# per-architecture package index carries both the version and the SHA256 of
+# every deb, so an update costs two small HTTP requests instead of a 750 MB
+# download, and the pool keeps old versions, so the URLs pinned in the PKGBUILD
+# stay resolvable after the next release.
+set -euo pipefail
+
+BASE_URL="https://persistent.oaistatic.com/codex-app-prod/linux/deb"
+declare -A DEB_ARCHES=([x86_64]=amd64 [aarch64]=arm64)
+
+# Print " " for the newest stanza in a Packages index. Newest
+# is vercmp's opinion, which is the one bin/sync-upstream and pacman both use;
+# sort -V disagrees with it over versions like 1.0a.
+newest_release() {
+ local index="$1"
+ local version sha256 best_version="" best_sha256=""
+
+ while read -r version sha256; do
+ if [[ -z "$best_version" ]] || [[ "$(vercmp "$version" "$best_version")" -gt 0 ]]; then
+ best_version="$version"
+ best_sha256="$sha256"
+ fi
+ done < <(awk '
+ { sub(/\r$/, "") }
+ /^Version:/ { version = $2 }
+ /^SHA256:/ { sha256 = $2 }
+ /^$/ { if (version && sha256) print version, sha256; version = sha256 = "" }
+ END { if (version && sha256) print version, sha256 }
+ ' <<<"$index")
+
+ [[ -n "$best_version" ]] || return 1
+ echo "$best_version $best_sha256"
+}
+
+versions=()
+declare -A checksums=()
+
+for arch in "${!DEB_ARCHES[@]}"; do
+ index=$(curl -fsSL "$BASE_URL/dists/stable/main/binary-${DEB_ARCHES[$arch]}/Packages")
+
+ read -r version sha256 <<<"$(newest_release "$index")"
+ if [[ -z "${version:-}" || -z "${sha256:-}" ]]; then
+ echo "No usable release found for $arch in the upstream package index" >&2
+ exit 1
+ fi
+
+ versions+=("$version")
+ checksums[$arch]="$sha256"
+done
+
+# A release lands one architecture at a time, and a single pkgver has to cover
+# both. Report no update until they agree; the next run picks it up.
+for version in "${versions[@]}"; do
+ if [[ "$version" != "${versions[0]}" ]]; then
+ echo "Upstream architectures are mid-release (${versions[*]}); skipping" >&2
+ echo '{}'
+ exit 0
+ fi
+done
+
+jq -n \
+ --arg pkgver "${versions[0]}" \
+ --arg x86_64 "${checksums[x86_64]}" \
+ --arg aarch64 "${checksums[aarch64]}" \
+ '{pkgver: $pkgver, sha256sums: {x86_64: [$x86_64], aarch64: [$aarch64]}}'
diff --git a/pkgbuilds/openai-codex-desktop/LICENSE b/pkgbuilds/openai-codex-desktop/LICENSE
deleted file mode 100644
index b87c5e4..0000000
--- a/pkgbuilds/openai-codex-desktop/LICENSE
+++ /dev/null
@@ -1,12 +0,0 @@
-Copyright Arch Linux Contributors
-
-Permission to use, copy, modify, and/or distribute this software for
-any purpose with or without fee is hereby granted.
-
-THE SOFTWARE IS PROVIDED βAS ISβ AND THE AUTHOR DISCLAIMS ALL
-WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
-FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
-DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
-AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
-OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt b/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt
deleted file mode 120000
index ea5b606..0000000
--- a/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt
+++ /dev/null
@@ -1 +0,0 @@
-../LICENSE
\ No newline at end of file
diff --git a/pkgbuilds/openai-codex-desktop/PKGBUILD b/pkgbuilds/openai-codex-desktop/PKGBUILD
index 52393e7..a92ee95 100644
--- a/pkgbuilds/openai-codex-desktop/PKGBUILD
+++ b/pkgbuilds/openai-codex-desktop/PKGBUILD
@@ -1,10 +1,12 @@
-# Maintainer: Parsiad Azimzadeh
-# Maintainer: mothran
-# Official downloads: https://chatgpt.com/codex/
+# Maintainer: David Heinemeier Hansson
+
+# Omarchy tracks OpenAI's own Debian repository rather than the AUR, which lags
+# behind releases that ship several times a week. .omarchy/upstream.sh rewrites
+# the version and checksums below from that repository's package index.
pkgname=openai-codex-desktop
-pkgver=26.803.81509
-pkgrel=8.1
+pkgver=26.810.52044
+pkgrel=1
pkgdesc="Official ChatGPT desktop app with Codex"
arch=('x86_64' 'aarch64')
url="https://chatgpt.com/codex/"
@@ -64,16 +66,13 @@ COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -)
_deb_x86_64="chatgpt_${pkgver}_amd64.deb"
_deb_aarch64="chatgpt_${pkgver}_arm64.deb"
source=('chatgpt-launcher.sh')
-source_x86_64=(
- "${_deb_x86_64}::https://persistent.oaistatic.com/codex-app-prod/linux/deb/latest/chatgpt_amd64.deb"
-)
-source_aarch64=(
- "${_deb_aarch64}::https://persistent.oaistatic.com/codex-app-prod/linux/deb/latest/chatgpt_arm64.deb"
-)
+_pool="https://persistent.oaistatic.com/codex-app-prod/linux/deb/pool/main/c/chatgpt"
+source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}")
+source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}")
noextract=("${_deb_x86_64}" "${_deb_aarch64}")
-sha256sums=('4e3ca9302600bed268f8fd3ba2c9ac2f1ceb99da139ed71c50db0289b118d06f')
-sha256sums_x86_64=('a9bf91a368f9f7c4eea38082a9fb8fb46b8d005b719a6d7715d2e5a1982c38eb')
-sha256sums_aarch64=('f38fcc194eca9ab0327dc10c92340681eae77c5d75164df700384ce2adaccbc1')
+sha256sums=('b3a4503b5931f102444bc7015c3cf4e40266cf034e0d682bd2a407dc5b3ee58c')
+sha256sums_x86_64=('708a15a1bb76e2bb7f0e376e5145391fa277ad3a64057c1d32537bdc2a1b4e6e')
+sha256sums_aarch64=('6ebea681b1e494d218a199f638b4bc886e94e1458dd61079b1e390a6fb98fdd2')
package() {
cd "${srcdir}"
diff --git a/pkgbuilds/openai-codex-desktop/REUSE.toml b/pkgbuilds/openai-codex-desktop/REUSE.toml
deleted file mode 100644
index 49e3625..0000000
--- a/pkgbuilds/openai-codex-desktop/REUSE.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-version = 1
-
-[[annotations]]
-path = [
- "PKGBUILD",
- "README.md",
- "keys/**",
- ".SRCINFO",
- ".gitignore",
- ".nvchecker.toml",
- "*.install",
- "*.sysusers",
- "*sysusers.conf",
- "*.tmpfiles",
- "*tmpfiles.conf",
- "*.logrotate",
- "*.pam",
- "*.service",
- "*.socket",
- "*.timer",
- "*.desktop",
- "*.hook",
-]
-SPDX-FileCopyrightText = "Arch Linux contributors"
-SPDX-License-Identifier = "0BSD"
diff --git a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh
old mode 100644
new mode 100755
index 0f8bc6b..2c4f4f2
--- a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh
+++ b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh
@@ -1,37 +1,32 @@
#!/bin/bash
-# SPDX-FileCopyrightText: 2026 Arch Linux Contributors
-# SPDX-License-Identifier: 0BSD
set -euo pipefail
user_flags=()
-ozone_flags=()
-
config_home="${XDG_CONFIG_HOME:-}"
-if [[ -z "${config_home}" && -n "${HOME:-}" ]]; then
- config_home="${HOME}/.config"
+[[ -n "$config_home" || -z "${HOME:-}" ]] || config_home="$HOME/.config"
+flags_file="${config_home:+$config_home/codex-flags.conf}"
+
+if [[ -n "$flags_file" && -f "$flags_file" && -r "$flags_file" ]]; then
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line="${line%%#*}"
+ [[ -n "${line//[[:space:]]/}" ]] || continue
+ read -r -a flags <<<"$line"
+ user_flags+=("${flags[@]}")
+ done <"$flags_file"
fi
-if [[ -n "${config_home}" && -f "${config_home}/codex-flags.conf" ]]; then
- while IFS= read -r flag_line || [[ -n "${flag_line}" ]]; do
- flag_line="${flag_line%%#*}"
- read -r -a flag_parts <<<"${flag_line}"
- user_flags+=("${flag_parts[@]}")
- done <"${config_home}/codex-flags.conf"
+# Chromium's own Ozone detection falls back to XWayland often enough to matter,
+# and the result is a blurry window on every scaled display. Ask for Wayland
+# directly, unless the user has already picked a platform themselves.
+platform_flags=()
+if [[ -n "${WAYLAND_DISPLAY:-}" || "${XDG_SESSION_TYPE:-}" == wayland ]]; then
+ platform_flags=(--ozone-platform=wayland)
+
+ for flag in "${user_flags[@]}" "$@"; do
+ case "$flag" in
+ --ozone-platform=* | --ozone-platform-hint=*) platform_flags=() ;;
+ esac
+ done
fi
-# Use native Wayland rendering in Wayland sessions. Chromium's automatic Ozone
-# selection can fall back to XWayland, which produces a blurry UI on scaled
-# displays. An explicit platform argument from the user always takes precedence.
-if [[ "${XDG_SESSION_TYPE:-}" == wayland || -n "${WAYLAND_DISPLAY:-}" ]]; then
- ozone_flags=(--ozone-platform=wayland)
-fi
-
-# A platform selected in the flags file or on the command line overrides the
-# native Wayland default.
-for flag in "${user_flags[@]}" "$@"; do
- case "${flag}" in
- --ozone-platform=*|--ozone-platform-hint=*) ozone_flags=() ;;
- esac
-done
-
-exec /usr/lib/chatgpt/ChatGPT "${ozone_flags[@]}" "${user_flags[@]}" "$@"
+exec /usr/lib/chatgpt/ChatGPT "${platform_flags[@]}" "${user_flags[@]}" "$@"