From 3189f0ea5ba4cced5c917ed687913a02ab5133e2 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 17 Jul 2026 21:18:56 +0000 Subject: [PATCH 01/11] feat(packaging): add in-repo AUR packaging for t3code-bin and t3code-nightly-bin Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 194 +++++++++++++++++++ README.md | 4 + packaging/aur/README.md | 47 +++++ packaging/aur/scripts/check_upstream.sh | 100 ++++++++++ packaging/aur/scripts/publish_aur.sh | 83 ++++++++ packaging/aur/scripts/stage_shared_assets.sh | 27 +++ packaging/aur/scripts/update_pkgbuild.sh | 74 +++++++ packaging/aur/t3code-bin/PKGBUILD | 152 +++++++++++++++ packaging/aur/t3code-nightly-bin/PKGBUILD | 152 +++++++++++++++ 9 files changed, 833 insertions(+) create mode 100644 .github/workflows/publish-aur.yml create mode 100644 packaging/aur/README.md create mode 100755 packaging/aur/scripts/check_upstream.sh create mode 100755 packaging/aur/scripts/publish_aur.sh create mode 100755 packaging/aur/scripts/stage_shared_assets.sh create mode 100755 packaging/aur/scripts/update_pkgbuild.sh create mode 100644 packaging/aur/t3code-bin/PKGBUILD create mode 100644 packaging/aur/t3code-nightly-bin/PKGBUILD diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml new file mode 100644 index 00000000000..f470307c3b7 --- /dev/null +++ b/.github/workflows/publish-aur.yml @@ -0,0 +1,194 @@ +name: Publish AUR packages + +# Publishes t3code-bin (stable) and t3code-nightly-bin to the AUR whenever a +# GitHub release is published. Each matrix job resolves the release matching +# its channel, patches its PKGBUILD, test-builds the package with makepkg in an +# Arch container, and pushes PKGBUILD/.SRCINFO to the AUR over SSH. +# +# Publishing requires the AUR_SSH_PRIVATE_KEY secret (an SSH key authorized on +# the AUR account maintaining the packages). When the secret is absent (for +# example on forks) the job still test-builds the package and prepares the +# payload as a dry run. + +on: + release: + types: + - published + workflow_dispatch: + inputs: + channel: + description: "Which package(s) to publish" + required: false + default: all + type: choice + options: + - all + - stable + - nightly + pkgrel: + description: "Override pkgrel (for republishing the same upstream version)" + required: false + type: string + +permissions: + contents: read + +env: + UPSTREAM_REPO: ${{ vars.UPSTREAM_REPO || github.repository }} + AUR_COMMIT_NAME: ${{ vars.AUR_COMMIT_NAME || 't3code-ci' }} + AUR_COMMIT_EMAIL: ${{ vars.AUR_COMMIT_EMAIL || 't3code-ci@users.noreply.github.com' }} + +jobs: + publish: + name: Build and publish (${{ matrix.package.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 1 + matrix: + package: + - name: t3code-bin + channel: stable + package_dir: packaging/aur/t3code-bin + aur_git_url: ssh://aur@aur.archlinux.org/t3code-bin.git + release_prerelease: "false" + release_tag_regex: '^v[0-9]+\.[0-9]+\.[0-9]+$' + release_version_prefix: "v" + asset_regex: '^T3-Code-.*-x86_64\.AppImage$' + - name: t3code-nightly-bin + channel: nightly + package_dir: packaging/aur/t3code-nightly-bin + aur_git_url: ssh://aur@aur.archlinux.org/t3code-nightly-bin.git + release_prerelease: "true" + release_tag_regex: '^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.' + release_version_prefix: "v" + asset_regex: '^T3-Code-.*-x86_64\.AppImage$' + defaults: + run: + shell: bash + container: + image: archlinux:base-devel + + steps: + - name: Skip channels excluded by dispatch input + id: channel + env: + DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} + PACKAGE_CHANNEL: ${{ matrix.package.channel }} + run: | + set -euo pipefail + enabled=true + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" \ + && -n "${DISPATCH_CHANNEL}" \ + && "${DISPATCH_CHANNEL}" != "all" \ + && "${DISPATCH_CHANNEL}" != "${PACKAGE_CHANNEL}" ]]; then + enabled=false + fi + echo "enabled=$enabled" >> "$GITHUB_OUTPUT" + echo "enabled=$enabled" + + - name: Install tooling + if: steps.channel.outputs.enabled == 'true' + run: | + set -euo pipefail + pacman -Sy --noconfirm --needed git openssh rsync curl github-cli jq + + - name: Checkout repository + if: steps.channel.outputs.enabled == 'true' + uses: actions/checkout@v6 + + - name: Validate repository layout + if: steps.channel.outputs.enabled == 'true' + run: | + set -euo pipefail + test -f "${{ matrix.package.package_dir }}/PKGBUILD" + test -f LICENSE + test -x packaging/aur/scripts/check_upstream.sh + test -x packaging/aur/scripts/stage_shared_assets.sh + test -x packaging/aur/scripts/update_pkgbuild.sh + test -x packaging/aur/scripts/publish_aur.sh + + - name: Resolve upstream release + if: steps.channel.outputs.enabled == 'true' + id: check + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_PRERELEASE: ${{ matrix.package.release_prerelease }} + RELEASE_TAG_REGEX: ${{ matrix.package.release_tag_regex }} + RELEASE_VERSION_PREFIX: ${{ matrix.package.release_version_prefix }} + ASSET_REGEX: ${{ matrix.package.asset_regex }} + run: | + set -euo pipefail + packaging/aur/scripts/check_upstream.sh + + - name: Skip when the release does not match this package + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched != 'true' + run: | + echo "Release does not match the ${{ matrix.package.channel }} channel; nothing to do." + + - name: Update PKGBUILD + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + env: + PKGBUILD_PATH: ${{ matrix.package.package_dir }}/PKGBUILD + UPSTREAM_TAG: ${{ steps.check.outputs.upstream_tag }} + UPSTREAM_VERSION: ${{ steps.check.outputs.upstream_version }} + PKGVER_CANDIDATE: ${{ steps.check.outputs.pkgver_candidate }} + APPIMAGE_SHA256: ${{ steps.check.outputs.appimage_sha256 }} + PKGREL_OVERRIDE: ${{ github.event.inputs.pkgrel }} + run: | + set -euo pipefail + packaging/aur/scripts/update_pkgbuild.sh + + - name: Create non-root build user + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + run: | + set -euo pipefail + useradd -m builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Stage shared package assets + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + env: + WORKSPACE_ROOT: ${{ github.workspace }} + PACKAGE_DIR: ${{ github.workspace }}/${{ matrix.package.package_dir }} + run: | + set -euo pipefail + packaging/aur/scripts/stage_shared_assets.sh + + - name: Regenerate .SRCINFO + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + run: | + set -euo pipefail + su - builder -c "cd '$GITHUB_WORKSPACE/${{ matrix.package.package_dir }}' && makepkg --printsrcinfo > .SRCINFO" + + - name: Build package + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + run: | + set -euo pipefail + su - builder -c "cd '$GITHUB_WORKSPACE/${{ matrix.package.package_dir }}' && makepkg -f --nodeps --noconfirm" + + - name: Upload build artifact + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.package.name }}-package + if-no-files-found: error + path: ${{ matrix.package.package_dir }}/*.pkg.tar.zst + + - name: Publish package files to AUR + if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + AUR_GIT_URL: ${{ matrix.package.aur_git_url }} + AUR_BRANCH: ${{ vars.AUR_BRANCH || 'master' }} + AUR_COMMIT_NAME: ${{ env.AUR_COMMIT_NAME }} + AUR_COMMIT_EMAIL: ${{ env.AUR_COMMIT_EMAIL }} + run: | + set -euo pipefail + if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then + echo "AUR_SSH_PRIVATE_KEY is not configured; running publish as a dry run." + export DRY_RUN=true + fi + cd "${{ matrix.package.package_dir }}" + "$GITHUB_WORKSPACE/packaging/aur/scripts/publish_aur.sh" diff --git a/README.md b/README.md index 6aebfc7e8b8..b7af8c046b0 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,12 @@ brew install --cask t3-code ```bash yay -S t3code-bin +# nightly builds +yay -S t3code-nightly-bin ``` +The AUR packaging is maintained in this repository under [`packaging/aur`](./packaging/aur). + ## Some notes We are very very early in this project. Expect bugs. diff --git a/packaging/aur/README.md b/packaging/aur/README.md new file mode 100644 index 00000000000..2eeb3d0b4ac --- /dev/null +++ b/packaging/aur/README.md @@ -0,0 +1,47 @@ +# AUR packaging + +Packaging for the Arch Linux (AUR) distribution of the T3 Code desktop app: + +- [`t3code-bin`](https://aur.archlinux.org/packages/t3code-bin) — stable releases +- [`t3code-nightly-bin`](https://aur.archlinux.org/packages/t3code-nightly-bin) — nightly prereleases + +Both packages repackage the official x86_64 AppImage published on GitHub Releases. Icons and the +desktop launcher are taken from the AppImage payload itself, so each channel ships its own branding +(stable vs nightly icon) automatically. + +## How publishing works + +`.github/workflows/publish-aur.yml` runs when a GitHub release is published: + +1. Resolves the release matching each package's channel (stable tags `vX.Y.Z`, nightly tags + `vX.Y.Z-nightly.*`) and reads the AppImage asset's sha256 digest from the release metadata. +2. Patches the package's `PKGBUILD` (`pkgver`, `pkgrel`, `_upstream_tag`, `sha256sums`) via + `scripts/update_pkgbuild.sh`. +3. Regenerates `.SRCINFO` and test-builds the package with `makepkg` in an `archlinux:base-devel` + container. +4. Pushes `PKGBUILD`, `.SRCINFO`, and `LICENSE` to the AUR over SSH (`scripts/publish_aur.sh`). + +The workflow can also be run manually (`workflow_dispatch`) to republish, with an optional `pkgrel` +override for republishing the same upstream version. + +The `PKGBUILD` files in this directory are templates: CI patches the version fields at publish +time, so the committed `pkgver`/`sha256sums` values are only a snapshot of the last edit. + +## Required configuration + +| Kind | Name | Purpose | +| ------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Secret | `AUR_SSH_PRIVATE_KEY` | SSH private key authorized on the AUR account that maintains both packages. Publishing is skipped (dry run) when unset, so forks only test-build. | +| Variable (optional) | `AUR_COMMIT_NAME` / `AUR_COMMIT_EMAIL` | Committer identity used for AUR pushes. | +| Variable (optional) | `UPSTREAM_REPO` | Overrides the release source repo (defaults to this repository). | + +## Testing locally + +On an Arch system (or `archlinux:base-devel` container): + +```bash +cd packaging/aur/t3code-bin +cp ../../../LICENSE . +makepkg --printsrcinfo > .SRCINFO +makepkg -f --nodeps --noconfirm +``` diff --git a/packaging/aur/scripts/check_upstream.sh b/packaging/aur/scripts/check_upstream.sh new file mode 100755 index 00000000000..615f60ac2ff --- /dev/null +++ b/packaging/aur/scripts/check_upstream.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Resolves the upstream release + AppImage asset a package should publish. +# +# When RELEASE_TAG is set (release-event runs) the script inspects that exact +# release and reports matched=false when the tag does not belong to this +# package's channel. Otherwise it resolves the newest matching release. +set -euo pipefail + +UPSTREAM_REPO="${UPSTREAM_REPO:-${GITHUB_REPOSITORY:-pingdotgg/t3code}}" +RELEASE_TAG="${RELEASE_TAG:-}" +RELEASE_TAG_REGEX="${RELEASE_TAG_REGEX:-^v}" +RELEASE_PRERELEASE="${RELEASE_PRERELEASE:-false}" +RELEASE_VERSION_PREFIX="${RELEASE_VERSION_PREFIX:-v}" +ASSET_REGEX="${ASSET_REGEX:-^T3-Code-.*-x86_64\.AppImage$}" +out_file="${GITHUB_OUTPUT:-}" + +emit() { + printf '%s=%s\n' "$1" "$2" + if [[ -n "$out_file" ]]; then + printf '%s=%s\n' "$1" "$2" >> "$out_file" + fi +} + +if [[ -n "$RELEASE_TAG" ]]; then + release_json="$(gh api "repos/$UPSTREAM_REPO/releases/tags/$RELEASE_TAG")" + release_json="$(jq -c \ + --arg regex "$RELEASE_TAG_REGEX" \ + --arg prerelease "$RELEASE_PRERELEASE" ' + select( + (.draft | not) + and (.prerelease == ($prerelease == "true")) + and (.tag_name | test($regex)) + ) // empty + ' <<<"$release_json")" +else + releases_json="$(gh api "repos/$UPSTREAM_REPO/releases?per_page=100")" + release_json="$(jq -c \ + --arg regex "$RELEASE_TAG_REGEX" \ + --arg prerelease "$RELEASE_PRERELEASE" ' + map( + select( + (.draft | not) + and (.prerelease == ($prerelease == "true")) + and (.tag_name | test($regex)) + ) + ) + | first // empty + ' <<<"$releases_json")" +fi + +if [[ -z "$release_json" ]]; then + echo "No release matching this package's channel (tag='${RELEASE_TAG:-latest}')." + emit matched false + exit 0 +fi + +upstream_tag="$(jq -r '.tag_name // empty' <<<"$release_json")" +if [[ -z "$upstream_tag" ]]; then + echo "Failed to resolve matching upstream tag from $UPSTREAM_REPO" >&2 + exit 1 +fi + +asset_json="$(jq -c --arg regex "$ASSET_REGEX" ' + .assets + | map(select(.name | test($regex))) + | first // empty +' <<<"$release_json")" + +if [[ -z "$asset_json" || "$asset_json" == "null" ]]; then + echo "Failed to find x86_64 AppImage asset in release $upstream_tag for $UPSTREAM_REPO" >&2 + exit 1 +fi + +appimage_name="$(jq -r '.name // empty' <<<"$asset_json")" +appimage_url="$(jq -r '.browser_download_url // empty' <<<"$asset_json")" +appimage_sha256="$(jq -r '.digest // empty' <<<"$asset_json")" +appimage_sha256="${appimage_sha256#sha256:}" + +if [[ -z "$appimage_name" || -z "$appimage_url" ]]; then + echo "Incomplete AppImage asset metadata returned by GitHub." >&2 + exit 1 +fi + +if [[ -z "$appimage_sha256" ]]; then + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + curl -fL --retry 3 --retry-delay 2 "$appimage_url" -o "$tmp_dir/$appimage_name" + appimage_sha256="$(sha256sum "$tmp_dir/$appimage_name" | awk '{print $1}')" +fi + +upstream_version="${upstream_tag#"$RELEASE_VERSION_PREFIX"}" +pkgver_candidate="$(printf '%s' "$upstream_version" | tr '-' '_' | tr -cd '[:alnum:]_.+')" + +emit matched true +emit upstream_tag "$upstream_tag" +emit upstream_version "$upstream_version" +emit pkgver_candidate "$pkgver_candidate" +emit appimage_name "$appimage_name" +emit appimage_url "$appimage_url" +emit appimage_sha256 "$appimage_sha256" diff --git a/packaging/aur/scripts/publish_aur.sh b/packaging/aur/scripts/publish_aur.sh new file mode 100755 index 00000000000..ab940415f48 --- /dev/null +++ b/packaging/aur/scripts/publish_aur.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Pushes the package payload (PKGBUILD, .SRCINFO, LICENSE) to the AUR. +# Run from the package directory after makepkg has regenerated .SRCINFO. +set -euo pipefail + +AUR_GIT_URL="${AUR_GIT_URL:-ssh://aur@aur.archlinux.org/t3code-bin.git}" +AUR_BRANCH="${AUR_BRANCH:-master}" +AUR_COMMIT_NAME="${AUR_COMMIT_NAME:-t3code-ci}" +AUR_COMMIT_EMAIL="${AUR_COMMIT_EMAIL:-t3code-ci@users.noreply.github.com}" +AUR_PAYLOAD_FILES="${AUR_PAYLOAD_FILES:-PKGBUILD .SRCINFO LICENSE}" +DRY_RUN="${DRY_RUN:-false}" + +retry() { + local attempt max_attempts delay + max_attempts=3 + delay=5 + + for ((attempt = 1; attempt <= max_attempts; attempt++)); do + if "$@"; then + return 0 + fi + + if ((attempt == max_attempts)); then + return 1 + fi + + echo "Command failed; retrying in ${delay}s (${attempt}/${max_attempts})..." >&2 + sleep "$delay" + delay=$((delay * 2)) + done +} + +if [[ "$DRY_RUN" != "true" && -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then + echo "AUR_SSH_PRIVATE_KEY is required" >&2 + exit 1 +fi + +rm -rf /tmp/aur-payload +mkdir -p /tmp/aur-payload + +for file in $AUR_PAYLOAD_FILES; do + if [[ ! -f "$file" ]]; then + echo "Missing payload file: $file" >&2 + exit 1 + fi + cp -f "$file" /tmp/aur-payload/ +done + +printf '%s\n' "${GITHUB_SHA:-local}" > /tmp/aur-payload/.upstream-commit + +if [[ "$DRY_RUN" == "true" ]]; then + echo "DRY_RUN=true, skipping AUR git push." + echo "Prepared payload in /tmp/aur-payload" + exit 0 +fi + +printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > /tmp/aur_id_ed25519 +chmod 600 /tmp/aur_id_ed25519 + +ssh-keyscan -H -t ed25519,rsa aur.archlinux.org > /tmp/aur_known_hosts 2>/dev/null +chmod 644 /tmp/aur_known_hosts + +export GIT_SSH_COMMAND="ssh -i /tmp/aur_id_ed25519 -o IdentitiesOnly=yes -o UserKnownHostsFile=/tmp/aur_known_hosts -o StrictHostKeyChecking=yes" + +rm -rf /tmp/aur-repo +retry git clone "$AUR_GIT_URL" /tmp/aur-repo + +git config --global --add safe.directory /tmp/aur-repo +rsync -a --delete --exclude='.git/' /tmp/aur-payload/ /tmp/aur-repo/ + +cd /tmp/aur-repo +git checkout -B "$AUR_BRANCH" +git config user.name "$AUR_COMMIT_NAME" +git config user.email "$AUR_COMMIT_EMAIL" + +git add -A +if git diff --cached --quiet; then + echo "No staged changes to publish." + exit 0 +fi + +git commit -m "chore(aur): sync from ${GITHUB_REPOSITORY:-local}@${GITHUB_SHA:-local}" +retry git push origin "$AUR_BRANCH" diff --git a/packaging/aur/scripts/stage_shared_assets.sh b/packaging/aur/scripts/stage_shared_assets.sh new file mode 100755 index 00000000000..801f600230d --- /dev/null +++ b/packaging/aur/scripts/stage_shared_assets.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copies repo-level assets shared by every AUR package into a package dir so +# makepkg can resolve its local sources. +set -euo pipefail + +WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(pwd)}" +PACKAGE_DIR="${PACKAGE_DIR:-.}" +SHARED_FILES="${SHARED_FILES:-LICENSE}" + +package_dir_abs="$(cd "$PACKAGE_DIR" && pwd)" +workspace_root_abs="$(cd "$WORKSPACE_ROOT" && pwd)" + +for file in $SHARED_FILES; do + src_path="$workspace_root_abs/$file" + dest_path="$package_dir_abs/$(basename "$file")" + + if [[ ! -f "$src_path" ]]; then + echo "Missing shared asset: $src_path" >&2 + exit 1 + fi + + if [[ "$src_path" == "$dest_path" ]]; then + continue + fi + + cp -f "$src_path" "$dest_path" +done diff --git a/packaging/aur/scripts/update_pkgbuild.sh b/packaging/aur/scripts/update_pkgbuild.sh new file mode 100755 index 00000000000..8f7e88f9608 --- /dev/null +++ b/packaging/aur/scripts/update_pkgbuild.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Patches a PKGBUILD in place with the resolved upstream release metadata. +# +# pkgrel handling: a new pkgver always resets pkgrel to 1. When the pkgver is +# unchanged (a forced republish of the same release), pkgrel is bumped, or set +# to PKGREL_OVERRIDE when provided. +set -euo pipefail + +PKGBUILD_PATH="${PKGBUILD_PATH:-PKGBUILD}" +UPSTREAM_TAG="${UPSTREAM_TAG:-}" +UPSTREAM_VERSION="${UPSTREAM_VERSION:-}" +PKGVER_CANDIDATE="${PKGVER_CANDIDATE:-}" +APPIMAGE_SHA256="${APPIMAGE_SHA256:-}" +PKGREL_OVERRIDE="${PKGREL_OVERRIDE:-}" +out_file="${GITHUB_OUTPUT:-}" + +if [[ -z "$UPSTREAM_TAG" || -z "$UPSTREAM_VERSION" ]]; then + echo "UPSTREAM_TAG and UPSTREAM_VERSION are required" >&2 + exit 1 +fi + +if [[ -z "$PKGVER_CANDIDATE" ]]; then + echo "PKGVER_CANDIDATE is required" >&2 + exit 1 +fi + +if [[ -z "$APPIMAGE_SHA256" ]]; then + echo "APPIMAGE_SHA256 is required" >&2 + exit 1 +fi + +if [[ ! -f "$PKGBUILD_PATH" ]]; then + echo "Missing PKGBUILD at $PKGBUILD_PATH" >&2 + exit 1 +fi + +current_pkgver="$(awk -F= '/^pkgver=/{print $2; exit}' "$PKGBUILD_PATH")" +current_pkgrel="$(awk -F= '/^pkgrel=/{print $2; exit}' "$PKGBUILD_PATH")" + +if [[ -z "$current_pkgver" || -z "$current_pkgrel" ]]; then + echo "Failed to read current pkgver/pkgrel from $PKGBUILD_PATH" >&2 + exit 1 +fi + +new_pkgver="$PKGVER_CANDIDATE" +if [[ -n "$PKGREL_OVERRIDE" ]]; then + new_pkgrel="$PKGREL_OVERRIDE" +elif [[ "$new_pkgver" != "$current_pkgver" ]]; then + new_pkgrel=1 +else + new_pkgrel=$((current_pkgrel + 1)) +fi + +escape_sed_replacement() { + printf '%s' "$1" | sed -e 's/[\/&]/\\&/g' +} + +new_pkgver_escaped="$(escape_sed_replacement "$new_pkgver")" +upstream_tag_escaped="$(escape_sed_replacement "$UPSTREAM_TAG")" +upstream_version_escaped="$(escape_sed_replacement "$UPSTREAM_VERSION")" +appimage_sha256_escaped="$(escape_sed_replacement "$APPIMAGE_SHA256")" + +sed -Ei "s/^pkgver=.*/pkgver=${new_pkgver_escaped}/" "$PKGBUILD_PATH" +sed -Ei "s/^pkgrel=.*/pkgrel=${new_pkgrel}/" "$PKGBUILD_PATH" +sed -Ei "s/^_upstream_tag=.*/_upstream_tag='${upstream_tag_escaped}'/" "$PKGBUILD_PATH" +sed -Ei "s/^_upstream_version=.*/_upstream_version='${upstream_version_escaped}'/" "$PKGBUILD_PATH" +sed -Ei "/^sha256sums=\(/,/^\)/{0,/^[[:space:]]*'[^']*'[[:space:]]*$/{s// '${appimage_sha256_escaped}'/}}" "$PKGBUILD_PATH" + +echo "Updated $PKGBUILD_PATH to pkgver=$new_pkgver pkgrel=$new_pkgrel" + +if [[ -n "$out_file" ]]; then + printf 'pkgver=%s\n' "$new_pkgver" >> "$out_file" + printf 'pkgrel=%s\n' "$new_pkgrel" >> "$out_file" +fi diff --git a/packaging/aur/t3code-bin/PKGBUILD b/packaging/aur/t3code-bin/PKGBUILD new file mode 100644 index 00000000000..1f5048b0c04 --- /dev/null +++ b/packaging/aur/t3code-bin/PKGBUILD @@ -0,0 +1,152 @@ +# Maintainer: T3 Tools +# Contributor: maria-rcks + +pkgname=t3code-bin +pkgver=0.0.28 +pkgrel=1 +pkgdesc='T3 Code desktop app (official AppImage repackaged for Arch Linux)' +arch=('x86_64') +_upstream_tag='v0.0.28' +_upstream_version='0.0.28' +_appimage_name="T3-Code-${_upstream_version}-x86_64.AppImage" +url='https://t3.codes' +license=('MIT') +depends=( + 'alsa-lib' + 'at-spi2-core' + 'cairo' + 'dbus' + 'expat' + 'gcc-libs' + 'gdk-pixbuf2' + 'glib2' + 'gtk3' + 'hicolor-icon-theme' + 'libcups' + 'libdrm' + 'libx11' + 'libxcb' + 'libxcomposite' + 'libxdamage' + 'libxext' + 'libxfixes' + 'libxkbcommon' + 'libxrandr' + 'mesa' + 'nspr' + 'nss' + 'pango' + 'systemd-libs' + 'xdg-utils' + 'zlib' +) +optdepends=( + 'openai-codex: use the system-installed Codex CLI' +) +provides=("t3code=${pkgver}") +conflicts=('t3code') +options=('!debug' '!emptydirs' '!strip') +source=( + "${_appimage_name}::https://github.com/pingdotgg/t3code/releases/download/${_upstream_tag}/${_appimage_name}" + 'LICENSE' +) +sha256sums=( + 'fa6069fb03eb25157f1e96a29901dca81bb0a9970f5936ca255342556ec42e0a' + '935d8f2af0c703f9c39517ee57cc4930b19d02d533be930b63f0e82f93614b43' +) + +prepare() { + chmod +x "$srcdir/$_appimage_name" + rm -rf "$srcdir/squashfs-root" + "$srcdir/$_appimage_name" --appimage-extract >/dev/null + + if [[ ! -d "$srcdir/squashfs-root" ]]; then + echo "Failed to extract AppImage payload." >&2 + return 1 + fi +} + +package() { + install -d "$pkgdir/opt/$pkgname" + cp -a "$srcdir/squashfs-root/." "$pkgdir/opt/$pkgname/" + + # Preserve upstream execute bits while ensuring the payload stays readable. + chmod -R a+rX "$pkgdir/opt/$pkgname" + + install -Dm755 /dev/stdin "$pkgdir/usr/bin/t3code" << 'EOF' +#!/usr/bin/env bash +set -euo pipefail + +appdir='/opt/t3code-bin' +export APPDIR="$appdir" + +if [[ -z "${CODEX_CLI_PATH-}" ]] && command -v codex >/dev/null 2>&1; then + export CODEX_CLI_PATH="$(command -v codex)" +fi + +export PATH="$appdir:$appdir/usr/bin:$appdir/usr/sbin:$PATH" +export XDG_DATA_DIRS="$appdir/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" +export GSETTINGS_SCHEMA_DIR="$appdir/usr/share/glib-2.0/schemas${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}" + +extra_flags=() +if [[ -n "${WAYLAND_DISPLAY-}" || "${XDG_SESSION_TYPE-}" == "wayland" ]]; then + extra_flags+=(--enable-features=UseOzonePlatform --ozone-platform=wayland --ozone-platform-hint=wayland) +else + extra_flags+=(--ozone-platform-hint=auto) +fi + +exec "$appdir/t3code" --no-sandbox "${extra_flags[@]}" "$@" +EOF + + ln -s t3code "$pkgdir/usr/bin/t3-code-desktop" + + # Install the channel-specific icons shipped inside the AppImage so the + # installed icon always matches the packaged release. + local icon_root="$srcdir/squashfs-root/usr/share/icons/hicolor" + local installed_icon=false + local icon size_dir + if [[ -d "$icon_root" ]]; then + for icon in "$icon_root"/*/apps/*.png; do + [[ -e "$icon" ]] || continue + size_dir="$(basename "$(dirname "$(dirname "$icon")")")" + install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code.png" + ln -sf t3code.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-desktop.png" + installed_icon=true + done + fi + + local root_icon + for root_icon in "$srcdir/squashfs-root/t3code.png" "$srcdir/squashfs-root/.DirIcon"; do + if [[ -f "$root_icon" ]]; then + install -Dm644 "$root_icon" "$pkgdir/usr/share/pixmaps/t3code.png" + ln -sf t3code.png "$pkgdir/usr/share/pixmaps/t3-code-desktop.png" + if [[ "$installed_icon" == false ]]; then + install -Dm644 "$root_icon" \ + "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code.png" + ln -sf t3code.png \ + "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3-code-desktop.png" + installed_icon=true + fi + break + fi + done + + if [[ "$installed_icon" == false ]]; then + echo "No icon found inside the AppImage payload." >&2 + return 1 + fi + + install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code.desktop" << 'EOF' +[Desktop Entry] +Name=T3 Code +Comment=T3 Code desktop build +Exec=t3code %U +Terminal=false +Type=Application +Icon=t3code +StartupWMClass=t3code +Categories=Development; +EOF + + install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/packaging/aur/t3code-nightly-bin/PKGBUILD b/packaging/aur/t3code-nightly-bin/PKGBUILD new file mode 100644 index 00000000000..97160fc01c9 --- /dev/null +++ b/packaging/aur/t3code-nightly-bin/PKGBUILD @@ -0,0 +1,152 @@ +# Maintainer: T3 Tools +# Contributor: maria-rcks + +pkgname=t3code-nightly-bin +pkgver=0.0.29_nightly.20260717.835 +pkgrel=1 +pkgdesc='T3 Code nightly desktop app (official AppImage repackaged for Arch Linux)' +arch=('x86_64') +_upstream_tag='v0.0.29-nightly.20260717.835' +_upstream_version='0.0.29-nightly.20260717.835' +_appimage_name="T3-Code-${_upstream_version}-x86_64.AppImage" +url='https://t3.codes' +license=('MIT') +depends=( + 'alsa-lib' + 'at-spi2-core' + 'cairo' + 'dbus' + 'expat' + 'gcc-libs' + 'gdk-pixbuf2' + 'glib2' + 'gtk3' + 'hicolor-icon-theme' + 'libcups' + 'libdrm' + 'libx11' + 'libxcb' + 'libxcomposite' + 'libxdamage' + 'libxext' + 'libxfixes' + 'libxkbcommon' + 'libxrandr' + 'mesa' + 'nspr' + 'nss' + 'pango' + 'systemd-libs' + 'xdg-utils' + 'zlib' +) +optdepends=( + 'openai-codex: use the system-installed Codex CLI' +) +provides=("t3code-nightly=${pkgver}") +conflicts=('t3code-nightly') +options=('!debug' '!emptydirs' '!strip') +source=( + "${_appimage_name}::https://github.com/pingdotgg/t3code/releases/download/${_upstream_tag}/${_appimage_name}" + 'LICENSE' +) +sha256sums=( + 'b36412e5d141de90837e579f3a09f1721282b8f3aedc3a802fb1723bd1c3019b' + '935d8f2af0c703f9c39517ee57cc4930b19d02d533be930b63f0e82f93614b43' +) + +prepare() { + chmod +x "$srcdir/$_appimage_name" + rm -rf "$srcdir/squashfs-root" + "$srcdir/$_appimage_name" --appimage-extract >/dev/null + + if [[ ! -d "$srcdir/squashfs-root" ]]; then + echo "Failed to extract AppImage payload." >&2 + return 1 + fi +} + +package() { + install -d "$pkgdir/opt/$pkgname" + cp -a "$srcdir/squashfs-root/." "$pkgdir/opt/$pkgname/" + + # Preserve upstream execute bits while ensuring the payload stays readable. + chmod -R a+rX "$pkgdir/opt/$pkgname" + + install -Dm755 /dev/stdin "$pkgdir/usr/bin/t3code-nightly" << 'EOF' +#!/usr/bin/env bash +set -euo pipefail + +appdir='/opt/t3code-nightly-bin' +export APPDIR="$appdir" + +if [[ -z "${CODEX_CLI_PATH-}" ]] && command -v codex >/dev/null 2>&1; then + export CODEX_CLI_PATH="$(command -v codex)" +fi + +export PATH="$appdir:$appdir/usr/bin:$appdir/usr/sbin:$PATH" +export XDG_DATA_DIRS="$appdir/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" +export GSETTINGS_SCHEMA_DIR="$appdir/usr/share/glib-2.0/schemas${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}" + +extra_flags=() +if [[ -n "${WAYLAND_DISPLAY-}" || "${XDG_SESSION_TYPE-}" == "wayland" ]]; then + extra_flags+=(--enable-features=UseOzonePlatform --ozone-platform=wayland --ozone-platform-hint=wayland) +else + extra_flags+=(--ozone-platform-hint=auto) +fi + +exec "$appdir/t3code" --no-sandbox "${extra_flags[@]}" "$@" +EOF + + ln -s t3code-nightly "$pkgdir/usr/bin/t3-code-nightly-desktop" + + # Install the channel-specific icons shipped inside the AppImage so the + # installed icon always matches the packaged release. + local icon_root="$srcdir/squashfs-root/usr/share/icons/hicolor" + local installed_icon=false + local icon size_dir + if [[ -d "$icon_root" ]]; then + for icon in "$icon_root"/*/apps/*.png; do + [[ -e "$icon" ]] || continue + size_dir="$(basename "$(dirname "$(dirname "$icon")")")" + install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code-nightly.png" + ln -sf t3code-nightly.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-nightly-desktop.png" + installed_icon=true + done + fi + + local root_icon + for root_icon in "$srcdir/squashfs-root/t3code.png" "$srcdir/squashfs-root/.DirIcon"; do + if [[ -f "$root_icon" ]]; then + install -Dm644 "$root_icon" "$pkgdir/usr/share/pixmaps/t3code-nightly.png" + ln -sf t3code-nightly.png "$pkgdir/usr/share/pixmaps/t3-code-nightly-desktop.png" + if [[ "$installed_icon" == false ]]; then + install -Dm644 "$root_icon" \ + "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code-nightly.png" + ln -sf t3code-nightly.png \ + "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3-code-nightly-desktop.png" + installed_icon=true + fi + break + fi + done + + if [[ "$installed_icon" == false ]]; then + echo "No icon found inside the AppImage payload." >&2 + return 1 + fi + + install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code-nightly.desktop" << 'EOF' +[Desktop Entry] +Name=T3 Code Nightly +Comment=T3 Code nightly desktop build +Exec=t3code-nightly %U +Terminal=false +Type=Application +Icon=t3code-nightly +StartupWMClass=t3code +Categories=Development; +EOF + + install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} From 5210685a193cb4e0f82c57822b7e575d8024970d Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 17 Jul 2026 21:42:58 +0000 Subject: [PATCH 02/11] refactor(packaging): consolidate AUR helpers into a single release script Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 178 +++---------------- packaging/aur/.gitignore | 6 + packaging/aur/README.md | 52 +++--- packaging/aur/scripts/check_upstream.sh | 100 ----------- packaging/aur/scripts/publish_aur.sh | 83 --------- packaging/aur/scripts/release.sh | 139 +++++++++++++++ packaging/aur/scripts/stage_shared_assets.sh | 27 --- packaging/aur/scripts/update_pkgbuild.sh | 74 -------- packaging/aur/t3code-bin/PKGBUILD | 37 +--- packaging/aur/t3code-nightly-bin/PKGBUILD | 45 ++--- 10 files changed, 221 insertions(+), 520 deletions(-) create mode 100644 packaging/aur/.gitignore delete mode 100755 packaging/aur/scripts/check_upstream.sh delete mode 100755 packaging/aur/scripts/publish_aur.sh create mode 100755 packaging/aur/scripts/release.sh delete mode 100755 packaging/aur/scripts/stage_shared_assets.sh delete mode 100755 packaging/aur/scripts/update_pkgbuild.sh diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index f470307c3b7..738cfbd0ce7 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -1,14 +1,13 @@ name: Publish AUR packages # Publishes t3code-bin (stable) and t3code-nightly-bin to the AUR whenever a -# GitHub release is published. Each matrix job resolves the release matching -# its channel, patches its PKGBUILD, test-builds the package with makepkg in an -# Arch container, and pushes PKGBUILD/.SRCINFO to the AUR over SSH. +# GitHub release is published. All logic lives in packaging/aur/scripts/release.sh: +# it matches the release tag to the package's channel, patches the PKGBUILD, +# test-builds with makepkg, and pushes PKGBUILD/.SRCINFO to the AUR over SSH. # # Publishing requires the AUR_SSH_PRIVATE_KEY secret (an SSH key authorized on -# the AUR account maintaining the packages). When the secret is absent (for -# example on forks) the job still test-builds the package and prepares the -# payload as a dry run. +# the AUR account maintaining the packages). Without it (for example on forks) +# the job still test-builds the package as a dry run. on: release: @@ -16,179 +15,56 @@ on: - published workflow_dispatch: inputs: - channel: - description: "Which package(s) to publish" + release_tag: + description: "Release tag to publish (empty = newest matching release)" required: false - default: all - type: choice - options: - - all - - stable - - nightly + type: string pkgrel: - description: "Override pkgrel (for republishing the same upstream version)" + description: "pkgrel override (bump to republish the same version)" required: false type: string permissions: contents: read -env: - UPSTREAM_REPO: ${{ vars.UPSTREAM_REPO || github.repository }} - AUR_COMMIT_NAME: ${{ vars.AUR_COMMIT_NAME || 't3code-ci' }} - AUR_COMMIT_EMAIL: ${{ vars.AUR_COMMIT_EMAIL || 't3code-ci@users.noreply.github.com' }} - jobs: publish: - name: Build and publish (${{ matrix.package.name }}) - runs-on: ubuntu-latest + name: Build and publish (${{ matrix.package }}) + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 30 strategy: fail-fast: false max-parallel: 1 matrix: package: - - name: t3code-bin - channel: stable - package_dir: packaging/aur/t3code-bin - aur_git_url: ssh://aur@aur.archlinux.org/t3code-bin.git - release_prerelease: "false" - release_tag_regex: '^v[0-9]+\.[0-9]+\.[0-9]+$' - release_version_prefix: "v" - asset_regex: '^T3-Code-.*-x86_64\.AppImage$' - - name: t3code-nightly-bin - channel: nightly - package_dir: packaging/aur/t3code-nightly-bin - aur_git_url: ssh://aur@aur.archlinux.org/t3code-nightly-bin.git - release_prerelease: "true" - release_tag_regex: '^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.' - release_version_prefix: "v" - asset_regex: '^T3-Code-.*-x86_64\.AppImage$' - defaults: - run: - shell: bash + - t3code-bin + - t3code-nightly-bin container: image: archlinux:base-devel steps: - - name: Skip channels excluded by dispatch input - id: channel - env: - DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} - PACKAGE_CHANNEL: ${{ matrix.package.channel }} - run: | - set -euo pipefail - enabled=true - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" \ - && -n "${DISPATCH_CHANNEL}" \ - && "${DISPATCH_CHANNEL}" != "all" \ - && "${DISPATCH_CHANNEL}" != "${PACKAGE_CHANNEL}" ]]; then - enabled=false - fi - echo "enabled=$enabled" >> "$GITHUB_OUTPUT" - echo "enabled=$enabled" - - name: Install tooling - if: steps.channel.outputs.enabled == 'true' - run: | - set -euo pipefail - pacman -Sy --noconfirm --needed git openssh rsync curl github-cli jq + run: pacman -Sy --noconfirm --needed git openssh curl github-cli jq - name: Checkout repository - if: steps.channel.outputs.enabled == 'true' uses: actions/checkout@v6 - - name: Validate repository layout - if: steps.channel.outputs.enabled == 'true' - run: | - set -euo pipefail - test -f "${{ matrix.package.package_dir }}/PKGBUILD" - test -f LICENSE - test -x packaging/aur/scripts/check_upstream.sh - test -x packaging/aur/scripts/stage_shared_assets.sh - test -x packaging/aur/scripts/update_pkgbuild.sh - test -x packaging/aur/scripts/publish_aur.sh - - - name: Resolve upstream release - if: steps.channel.outputs.enabled == 'true' - id: check - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - RELEASE_PRERELEASE: ${{ matrix.package.release_prerelease }} - RELEASE_TAG_REGEX: ${{ matrix.package.release_tag_regex }} - RELEASE_VERSION_PREFIX: ${{ matrix.package.release_version_prefix }} - ASSET_REGEX: ${{ matrix.package.asset_regex }} - run: | - set -euo pipefail - packaging/aur/scripts/check_upstream.sh - - - name: Skip when the release does not match this package - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched != 'true' - run: | - echo "Release does not match the ${{ matrix.package.channel }} channel; nothing to do." - - - name: Update PKGBUILD - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' - env: - PKGBUILD_PATH: ${{ matrix.package.package_dir }}/PKGBUILD - UPSTREAM_TAG: ${{ steps.check.outputs.upstream_tag }} - UPSTREAM_VERSION: ${{ steps.check.outputs.upstream_version }} - PKGVER_CANDIDATE: ${{ steps.check.outputs.pkgver_candidate }} - APPIMAGE_SHA256: ${{ steps.check.outputs.appimage_sha256 }} - PKGREL_OVERRIDE: ${{ github.event.inputs.pkgrel }} - run: | - set -euo pipefail - packaging/aur/scripts/update_pkgbuild.sh - - name: Create non-root build user - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' - run: | - set -euo pipefail - useradd -m builder - chown -R builder:builder "$GITHUB_WORKSPACE" + run: useradd -m builder - - name: Stage shared package assets - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' + - name: Build and publish env: - WORKSPACE_ROOT: ${{ github.workspace }} - PACKAGE_DIR: ${{ github.workspace }}/${{ matrix.package.package_dir }} - run: | - set -euo pipefail - packaging/aur/scripts/stage_shared_assets.sh - - - name: Regenerate .SRCINFO - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' - run: | - set -euo pipefail - su - builder -c "cd '$GITHUB_WORKSPACE/${{ matrix.package.package_dir }}' && makepkg --printsrcinfo > .SRCINFO" - - - name: Build package - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' - run: | - set -euo pipefail - su - builder -c "cd '$GITHUB_WORKSPACE/${{ matrix.package.package_dir }}' && makepkg -f --nodeps --noconfirm" + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} + PKGREL: ${{ github.event.inputs.pkgrel }} + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + AUR_COMMIT_NAME: ${{ vars.AUR_COMMIT_NAME }} + AUR_COMMIT_EMAIL: ${{ vars.AUR_COMMIT_EMAIL }} + run: packaging/aur/scripts/release.sh "packaging/aur/${{ matrix.package }}" - name: Upload build artifact - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' uses: actions/upload-artifact@v7 with: - name: ${{ matrix.package.name }}-package - if-no-files-found: error - path: ${{ matrix.package.package_dir }}/*.pkg.tar.zst - - - name: Publish package files to AUR - if: steps.channel.outputs.enabled == 'true' && steps.check.outputs.matched == 'true' - env: - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - AUR_GIT_URL: ${{ matrix.package.aur_git_url }} - AUR_BRANCH: ${{ vars.AUR_BRANCH || 'master' }} - AUR_COMMIT_NAME: ${{ env.AUR_COMMIT_NAME }} - AUR_COMMIT_EMAIL: ${{ env.AUR_COMMIT_EMAIL }} - run: | - set -euo pipefail - if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then - echo "AUR_SSH_PRIVATE_KEY is not configured; running publish as a dry run." - export DRY_RUN=true - fi - cd "${{ matrix.package.package_dir }}" - "$GITHUB_WORKSPACE/packaging/aur/scripts/publish_aur.sh" + name: ${{ matrix.package }}-package + if-no-files-found: ignore + path: packaging/aur/${{ matrix.package }}/*.pkg.tar.zst diff --git a/packaging/aur/.gitignore b/packaging/aur/.gitignore new file mode 100644 index 00000000000..df1e8cc4485 --- /dev/null +++ b/packaging/aur/.gitignore @@ -0,0 +1,6 @@ +src/ +pkg/ +*.AppImage +*.pkg.tar.zst +.SRCINFO +LICENSE diff --git a/packaging/aur/README.md b/packaging/aur/README.md index 2eeb3d0b4ac..b3332506f77 100644 --- a/packaging/aur/README.md +++ b/packaging/aur/README.md @@ -5,43 +5,45 @@ Packaging for the Arch Linux (AUR) distribution of the T3 Code desktop app: - [`t3code-bin`](https://aur.archlinux.org/packages/t3code-bin) — stable releases - [`t3code-nightly-bin`](https://aur.archlinux.org/packages/t3code-nightly-bin) — nightly prereleases -Both packages repackage the official x86_64 AppImage published on GitHub Releases. Icons and the -desktop launcher are taken from the AppImage payload itself, so each channel ships its own branding -(stable vs nightly icon) automatically. +Both packages repackage the official x86_64 AppImage published on GitHub Releases. Icons are taken +from the AppImage payload itself, so each channel ships its own branding (stable vs nightly icon) +automatically. ## How publishing works -`.github/workflows/publish-aur.yml` runs when a GitHub release is published: +`.github/workflows/publish-aur.yml` runs `scripts/release.sh ` for both packages +whenever a GitHub release is published. The script: -1. Resolves the release matching each package's channel (stable tags `vX.Y.Z`, nightly tags - `vX.Y.Z-nightly.*`) and reads the AppImage asset's sha256 digest from the release metadata. -2. Patches the package's `PKGBUILD` (`pkgver`, `pkgrel`, `_upstream_tag`, `sha256sums`) via - `scripts/update_pkgbuild.sh`. -3. Regenerates `.SRCINFO` and test-builds the package with `makepkg` in an `archlinux:base-devel` - container. -4. Pushes `PKGBUILD`, `.SRCINFO`, and `LICENSE` to the AUR over SSH (`scripts/publish_aur.sh`). +1. Resolves the release matching the package's channel (stable tags `vX.Y.Z`, nightly tags + `vX.Y.Z-nightly.*`) and reads the AppImage sha256 from the release asset digest. A release for + the other channel is a no-op. +2. Patches the `PKGBUILD` (`pkgver`, `pkgrel`, `_upstream_tag`, `sha256sums`), regenerates + `.SRCINFO`, and test-builds the package with `makepkg`. +3. Pushes `PKGBUILD`, `.SRCINFO`, and `LICENSE` to the AUR over SSH — skipped as a dry run when + `AUR_SSH_PRIVATE_KEY` is not configured (for example on forks). -The workflow can also be run manually (`workflow_dispatch`) to republish, with an optional `pkgrel` -override for republishing the same upstream version. +The workflow can also be run manually (`workflow_dispatch`) with an optional release tag and a +`pkgrel` override for republishing the same upstream version. -The `PKGBUILD` files in this directory are templates: CI patches the version fields at publish -time, so the committed `pkgver`/`sha256sums` values are only a snapshot of the last edit. +The committed `PKGBUILD` files are templates: CI patches the version fields at publish time, so the +committed `pkgver`/`sha256sums` values are only a snapshot of the last edit. ## Required configuration -| Kind | Name | Purpose | -| ------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Secret | `AUR_SSH_PRIVATE_KEY` | SSH private key authorized on the AUR account that maintains both packages. Publishing is skipped (dry run) when unset, so forks only test-build. | -| Variable (optional) | `AUR_COMMIT_NAME` / `AUR_COMMIT_EMAIL` | Committer identity used for AUR pushes. | -| Variable (optional) | `UPSTREAM_REPO` | Overrides the release source repo (defaults to this repository). | +| Kind | Name | Purpose | +| ------------------- | -------------------------------------- | --------------------------------------------------------------------------- | +| Secret | `AUR_SSH_PRIVATE_KEY` | SSH private key authorized on the AUR account that maintains both packages. | +| Variable (optional) | `AUR_COMMIT_NAME` / `AUR_COMMIT_EMAIL` | Committer identity for AUR pushes (defaults to `t3code-ci`). | +| Variable (optional) | `UPSTREAM_REPO` | Overrides the release source repo (defaults to this repository). | ## Testing locally -On an Arch system (or `archlinux:base-devel` container): +On an Arch system (or `archlinux:base-devel` container) with `gh`, `jq`, and `curl`: ```bash -cd packaging/aur/t3code-bin -cp ../../../LICENSE . -makepkg --printsrcinfo > .SRCINFO -makepkg -f --nodeps --noconfirm +GH_TOKEN=$(gh auth token) GITHUB_REPOSITORY=pingdotgg/t3code \ + packaging/aur/scripts/release.sh packaging/aur/t3code-bin ``` + +Without `AUR_SSH_PRIVATE_KEY` this resolves the latest stable release, builds the package, and +stops before pushing anything. diff --git a/packaging/aur/scripts/check_upstream.sh b/packaging/aur/scripts/check_upstream.sh deleted file mode 100755 index 615f60ac2ff..00000000000 --- a/packaging/aur/scripts/check_upstream.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -# Resolves the upstream release + AppImage asset a package should publish. -# -# When RELEASE_TAG is set (release-event runs) the script inspects that exact -# release and reports matched=false when the tag does not belong to this -# package's channel. Otherwise it resolves the newest matching release. -set -euo pipefail - -UPSTREAM_REPO="${UPSTREAM_REPO:-${GITHUB_REPOSITORY:-pingdotgg/t3code}}" -RELEASE_TAG="${RELEASE_TAG:-}" -RELEASE_TAG_REGEX="${RELEASE_TAG_REGEX:-^v}" -RELEASE_PRERELEASE="${RELEASE_PRERELEASE:-false}" -RELEASE_VERSION_PREFIX="${RELEASE_VERSION_PREFIX:-v}" -ASSET_REGEX="${ASSET_REGEX:-^T3-Code-.*-x86_64\.AppImage$}" -out_file="${GITHUB_OUTPUT:-}" - -emit() { - printf '%s=%s\n' "$1" "$2" - if [[ -n "$out_file" ]]; then - printf '%s=%s\n' "$1" "$2" >> "$out_file" - fi -} - -if [[ -n "$RELEASE_TAG" ]]; then - release_json="$(gh api "repos/$UPSTREAM_REPO/releases/tags/$RELEASE_TAG")" - release_json="$(jq -c \ - --arg regex "$RELEASE_TAG_REGEX" \ - --arg prerelease "$RELEASE_PRERELEASE" ' - select( - (.draft | not) - and (.prerelease == ($prerelease == "true")) - and (.tag_name | test($regex)) - ) // empty - ' <<<"$release_json")" -else - releases_json="$(gh api "repos/$UPSTREAM_REPO/releases?per_page=100")" - release_json="$(jq -c \ - --arg regex "$RELEASE_TAG_REGEX" \ - --arg prerelease "$RELEASE_PRERELEASE" ' - map( - select( - (.draft | not) - and (.prerelease == ($prerelease == "true")) - and (.tag_name | test($regex)) - ) - ) - | first // empty - ' <<<"$releases_json")" -fi - -if [[ -z "$release_json" ]]; then - echo "No release matching this package's channel (tag='${RELEASE_TAG:-latest}')." - emit matched false - exit 0 -fi - -upstream_tag="$(jq -r '.tag_name // empty' <<<"$release_json")" -if [[ -z "$upstream_tag" ]]; then - echo "Failed to resolve matching upstream tag from $UPSTREAM_REPO" >&2 - exit 1 -fi - -asset_json="$(jq -c --arg regex "$ASSET_REGEX" ' - .assets - | map(select(.name | test($regex))) - | first // empty -' <<<"$release_json")" - -if [[ -z "$asset_json" || "$asset_json" == "null" ]]; then - echo "Failed to find x86_64 AppImage asset in release $upstream_tag for $UPSTREAM_REPO" >&2 - exit 1 -fi - -appimage_name="$(jq -r '.name // empty' <<<"$asset_json")" -appimage_url="$(jq -r '.browser_download_url // empty' <<<"$asset_json")" -appimage_sha256="$(jq -r '.digest // empty' <<<"$asset_json")" -appimage_sha256="${appimage_sha256#sha256:}" - -if [[ -z "$appimage_name" || -z "$appimage_url" ]]; then - echo "Incomplete AppImage asset metadata returned by GitHub." >&2 - exit 1 -fi - -if [[ -z "$appimage_sha256" ]]; then - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' EXIT - curl -fL --retry 3 --retry-delay 2 "$appimage_url" -o "$tmp_dir/$appimage_name" - appimage_sha256="$(sha256sum "$tmp_dir/$appimage_name" | awk '{print $1}')" -fi - -upstream_version="${upstream_tag#"$RELEASE_VERSION_PREFIX"}" -pkgver_candidate="$(printf '%s' "$upstream_version" | tr '-' '_' | tr -cd '[:alnum:]_.+')" - -emit matched true -emit upstream_tag "$upstream_tag" -emit upstream_version "$upstream_version" -emit pkgver_candidate "$pkgver_candidate" -emit appimage_name "$appimage_name" -emit appimage_url "$appimage_url" -emit appimage_sha256 "$appimage_sha256" diff --git a/packaging/aur/scripts/publish_aur.sh b/packaging/aur/scripts/publish_aur.sh deleted file mode 100755 index ab940415f48..00000000000 --- a/packaging/aur/scripts/publish_aur.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# Pushes the package payload (PKGBUILD, .SRCINFO, LICENSE) to the AUR. -# Run from the package directory after makepkg has regenerated .SRCINFO. -set -euo pipefail - -AUR_GIT_URL="${AUR_GIT_URL:-ssh://aur@aur.archlinux.org/t3code-bin.git}" -AUR_BRANCH="${AUR_BRANCH:-master}" -AUR_COMMIT_NAME="${AUR_COMMIT_NAME:-t3code-ci}" -AUR_COMMIT_EMAIL="${AUR_COMMIT_EMAIL:-t3code-ci@users.noreply.github.com}" -AUR_PAYLOAD_FILES="${AUR_PAYLOAD_FILES:-PKGBUILD .SRCINFO LICENSE}" -DRY_RUN="${DRY_RUN:-false}" - -retry() { - local attempt max_attempts delay - max_attempts=3 - delay=5 - - for ((attempt = 1; attempt <= max_attempts; attempt++)); do - if "$@"; then - return 0 - fi - - if ((attempt == max_attempts)); then - return 1 - fi - - echo "Command failed; retrying in ${delay}s (${attempt}/${max_attempts})..." >&2 - sleep "$delay" - delay=$((delay * 2)) - done -} - -if [[ "$DRY_RUN" != "true" && -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then - echo "AUR_SSH_PRIVATE_KEY is required" >&2 - exit 1 -fi - -rm -rf /tmp/aur-payload -mkdir -p /tmp/aur-payload - -for file in $AUR_PAYLOAD_FILES; do - if [[ ! -f "$file" ]]; then - echo "Missing payload file: $file" >&2 - exit 1 - fi - cp -f "$file" /tmp/aur-payload/ -done - -printf '%s\n' "${GITHUB_SHA:-local}" > /tmp/aur-payload/.upstream-commit - -if [[ "$DRY_RUN" == "true" ]]; then - echo "DRY_RUN=true, skipping AUR git push." - echo "Prepared payload in /tmp/aur-payload" - exit 0 -fi - -printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > /tmp/aur_id_ed25519 -chmod 600 /tmp/aur_id_ed25519 - -ssh-keyscan -H -t ed25519,rsa aur.archlinux.org > /tmp/aur_known_hosts 2>/dev/null -chmod 644 /tmp/aur_known_hosts - -export GIT_SSH_COMMAND="ssh -i /tmp/aur_id_ed25519 -o IdentitiesOnly=yes -o UserKnownHostsFile=/tmp/aur_known_hosts -o StrictHostKeyChecking=yes" - -rm -rf /tmp/aur-repo -retry git clone "$AUR_GIT_URL" /tmp/aur-repo - -git config --global --add safe.directory /tmp/aur-repo -rsync -a --delete --exclude='.git/' /tmp/aur-payload/ /tmp/aur-repo/ - -cd /tmp/aur-repo -git checkout -B "$AUR_BRANCH" -git config user.name "$AUR_COMMIT_NAME" -git config user.email "$AUR_COMMIT_EMAIL" - -git add -A -if git diff --cached --quiet; then - echo "No staged changes to publish." - exit 0 -fi - -git commit -m "chore(aur): sync from ${GITHUB_REPOSITORY:-local}@${GITHUB_SHA:-local}" -retry git push origin "$AUR_BRANCH" diff --git a/packaging/aur/scripts/release.sh b/packaging/aur/scripts/release.sh new file mode 100755 index 00000000000..e61768ba6b3 --- /dev/null +++ b/packaging/aur/scripts/release.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Builds and publishes one AUR package from a published GitHub release. +# +# Usage: release.sh (packaging/aur/t3code-bin or .../t3code-nightly-bin) +# +# Environment: +# GH_TOKEN GitHub token for release lookups (required) +# RELEASE_TAG Release tag to publish; empty = newest matching release +# PKGREL Override pkgrel (default 1; bump to republish a version) +# AUR_SSH_PRIVATE_KEY SSH key for the AUR push; unset = dry run (build only) +# UPSTREAM_REPO Release source repo (default: $GITHUB_REPOSITORY) +# AUR_COMMIT_NAME / AUR_COMMIT_EMAIL Committer identity for the AUR push +# +# The stable package only matches vX.Y.Z releases and the nightly package only +# matches vX.Y.Z-nightly.* prereleases; a non-matching RELEASE_TAG is a no-op. +set -euo pipefail + +package_dir="${1:?usage: release.sh }" +package_dir="$(cd "$package_dir" && pwd)" +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +pkgname="$(basename "$package_dir")" + +UPSTREAM_REPO="${UPSTREAM_REPO:-${GITHUB_REPOSITORY:-pingdotgg/t3code}}" +AUR_COMMIT_NAME="${AUR_COMMIT_NAME:-t3code-ci}" +AUR_COMMIT_EMAIL="${AUR_COMMIT_EMAIL:-t3code-ci@users.noreply.github.com}" + +case "$pkgname" in + t3code-bin) + tag_regex='^v[0-9]+\.[0-9]+\.[0-9]+$' + prerelease=false + ;; + t3code-nightly-bin) + tag_regex='^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.' + prerelease=true + ;; + *) + echo "Unknown package: $pkgname" >&2 + exit 1 + ;; +esac + +# --- Resolve the release and its AppImage asset ------------------------------ + +if [[ -n "${RELEASE_TAG:-}" ]]; then + release_json="$(gh api "repos/$UPSTREAM_REPO/releases/tags/$RELEASE_TAG")" +else + release_json="$(gh api "repos/$UPSTREAM_REPO/releases?per_page=100" | jq -c '.[]' | head -n 100)" +fi + +release_json="$(jq -cs \ + --arg regex "$tag_regex" \ + --argjson prerelease "$prerelease" ' + map(select( + (.draft | not) + and .prerelease == $prerelease + and (.tag_name | test($regex)) + )) | first // empty + ' <<<"$release_json")" + +if [[ -z "$release_json" ]]; then + echo "No release matching $pkgname (tag='${RELEASE_TAG:-latest}'); nothing to do." + exit 0 +fi + +tag="$(jq -r '.tag_name' <<<"$release_json")" +version="${tag#v}" +pkgver="$(printf '%s' "$version" | tr '-' '_' | tr -cd '[:alnum:]_.+')" + +asset_json="$(jq -c '.assets | map(select(.name | test("^T3-Code-.*-x86_64\\.AppImage$"))) | first // empty' <<<"$release_json")" +if [[ -z "$asset_json" ]]; then + echo "Release $tag has no x86_64 AppImage asset." >&2 + exit 1 +fi + +sha256="$(jq -r '.digest // empty' <<<"$asset_json")" +sha256="${sha256#sha256:}" +if [[ -z "$sha256" ]]; then + # Old releases predate GitHub's asset digests; hash the download instead. + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + curl -fL --retry 3 "$(jq -r '.browser_download_url' <<<"$asset_json")" -o "$tmp/appimage" + sha256="$(sha256sum "$tmp/appimage" | awk '{print $1}')" +fi + +echo "Publishing $pkgname $pkgver (release $tag, sha256 $sha256)" + +# --- Patch the PKGBUILD and build --------------------------------------------- + +cd "$package_dir" +sed -Ei \ + -e "s/^pkgver=.*/pkgver=$pkgver/" \ + -e "s/^pkgrel=.*/pkgrel=${PKGREL:-1}/" \ + -e "s/^_upstream_tag=.*/_upstream_tag='$tag'/" \ + -e "s/^_upstream_version=.*/_upstream_version='$version'/" \ + -e "0,/^ '[0-9a-f]{64}'$/s// '$sha256'/" \ + PKGBUILD +cp -f "$repo_root/LICENSE" LICENSE + +# makepkg refuses to run as root; hand off to the "builder" user in CI. +run_makepkg() { + if [[ "$(id -u)" == 0 ]]; then + chown -R builder:builder "$package_dir" + su builder -c "cd '$package_dir' && $*" + else + (cd "$package_dir" && "$@") + fi +} + +run_makepkg makepkg --printsrcinfo > .SRCINFO +run_makepkg makepkg -f --nodeps --noconfirm + +# --- Push PKGBUILD/.SRCINFO/LICENSE to the AUR -------------------------------- + +if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then + echo "AUR_SSH_PRIVATE_KEY is not set; skipping AUR push (dry run)." + exit 0 +fi + +key_file="$(mktemp)" +hosts_file="$(mktemp)" +printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$key_file" +chmod 600 "$key_file" +ssh-keyscan -H -t ed25519,rsa aur.archlinux.org > "$hosts_file" 2>/dev/null +export GIT_SSH_COMMAND="ssh -i $key_file -o IdentitiesOnly=yes -o UserKnownHostsFile=$hosts_file -o StrictHostKeyChecking=yes" + +aur_dir="$(mktemp -d)" +git clone "ssh://aur@aur.archlinux.org/$pkgname.git" "$aur_dir" +cp -f PKGBUILD .SRCINFO LICENSE "$aur_dir/" + +cd "$aur_dir" +git config user.name "$AUR_COMMIT_NAME" +git config user.email "$AUR_COMMIT_EMAIL" +git add -A +if git diff --cached --quiet; then + echo "AUR already up to date." + exit 0 +fi +git commit -m "chore: update to $pkgver (from ${GITHUB_REPOSITORY:-local}@${GITHUB_SHA:-local})" +git push origin HEAD:master diff --git a/packaging/aur/scripts/stage_shared_assets.sh b/packaging/aur/scripts/stage_shared_assets.sh deleted file mode 100755 index 801f600230d..00000000000 --- a/packaging/aur/scripts/stage_shared_assets.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# Copies repo-level assets shared by every AUR package into a package dir so -# makepkg can resolve its local sources. -set -euo pipefail - -WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(pwd)}" -PACKAGE_DIR="${PACKAGE_DIR:-.}" -SHARED_FILES="${SHARED_FILES:-LICENSE}" - -package_dir_abs="$(cd "$PACKAGE_DIR" && pwd)" -workspace_root_abs="$(cd "$WORKSPACE_ROOT" && pwd)" - -for file in $SHARED_FILES; do - src_path="$workspace_root_abs/$file" - dest_path="$package_dir_abs/$(basename "$file")" - - if [[ ! -f "$src_path" ]]; then - echo "Missing shared asset: $src_path" >&2 - exit 1 - fi - - if [[ "$src_path" == "$dest_path" ]]; then - continue - fi - - cp -f "$src_path" "$dest_path" -done diff --git a/packaging/aur/scripts/update_pkgbuild.sh b/packaging/aur/scripts/update_pkgbuild.sh deleted file mode 100755 index 8f7e88f9608..00000000000 --- a/packaging/aur/scripts/update_pkgbuild.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Patches a PKGBUILD in place with the resolved upstream release metadata. -# -# pkgrel handling: a new pkgver always resets pkgrel to 1. When the pkgver is -# unchanged (a forced republish of the same release), pkgrel is bumped, or set -# to PKGREL_OVERRIDE when provided. -set -euo pipefail - -PKGBUILD_PATH="${PKGBUILD_PATH:-PKGBUILD}" -UPSTREAM_TAG="${UPSTREAM_TAG:-}" -UPSTREAM_VERSION="${UPSTREAM_VERSION:-}" -PKGVER_CANDIDATE="${PKGVER_CANDIDATE:-}" -APPIMAGE_SHA256="${APPIMAGE_SHA256:-}" -PKGREL_OVERRIDE="${PKGREL_OVERRIDE:-}" -out_file="${GITHUB_OUTPUT:-}" - -if [[ -z "$UPSTREAM_TAG" || -z "$UPSTREAM_VERSION" ]]; then - echo "UPSTREAM_TAG and UPSTREAM_VERSION are required" >&2 - exit 1 -fi - -if [[ -z "$PKGVER_CANDIDATE" ]]; then - echo "PKGVER_CANDIDATE is required" >&2 - exit 1 -fi - -if [[ -z "$APPIMAGE_SHA256" ]]; then - echo "APPIMAGE_SHA256 is required" >&2 - exit 1 -fi - -if [[ ! -f "$PKGBUILD_PATH" ]]; then - echo "Missing PKGBUILD at $PKGBUILD_PATH" >&2 - exit 1 -fi - -current_pkgver="$(awk -F= '/^pkgver=/{print $2; exit}' "$PKGBUILD_PATH")" -current_pkgrel="$(awk -F= '/^pkgrel=/{print $2; exit}' "$PKGBUILD_PATH")" - -if [[ -z "$current_pkgver" || -z "$current_pkgrel" ]]; then - echo "Failed to read current pkgver/pkgrel from $PKGBUILD_PATH" >&2 - exit 1 -fi - -new_pkgver="$PKGVER_CANDIDATE" -if [[ -n "$PKGREL_OVERRIDE" ]]; then - new_pkgrel="$PKGREL_OVERRIDE" -elif [[ "$new_pkgver" != "$current_pkgver" ]]; then - new_pkgrel=1 -else - new_pkgrel=$((current_pkgrel + 1)) -fi - -escape_sed_replacement() { - printf '%s' "$1" | sed -e 's/[\/&]/\\&/g' -} - -new_pkgver_escaped="$(escape_sed_replacement "$new_pkgver")" -upstream_tag_escaped="$(escape_sed_replacement "$UPSTREAM_TAG")" -upstream_version_escaped="$(escape_sed_replacement "$UPSTREAM_VERSION")" -appimage_sha256_escaped="$(escape_sed_replacement "$APPIMAGE_SHA256")" - -sed -Ei "s/^pkgver=.*/pkgver=${new_pkgver_escaped}/" "$PKGBUILD_PATH" -sed -Ei "s/^pkgrel=.*/pkgrel=${new_pkgrel}/" "$PKGBUILD_PATH" -sed -Ei "s/^_upstream_tag=.*/_upstream_tag='${upstream_tag_escaped}'/" "$PKGBUILD_PATH" -sed -Ei "s/^_upstream_version=.*/_upstream_version='${upstream_version_escaped}'/" "$PKGBUILD_PATH" -sed -Ei "/^sha256sums=\(/,/^\)/{0,/^[[:space:]]*'[^']*'[[:space:]]*$/{s// '${appimage_sha256_escaped}'/}}" "$PKGBUILD_PATH" - -echo "Updated $PKGBUILD_PATH to pkgver=$new_pkgver pkgrel=$new_pkgrel" - -if [[ -n "$out_file" ]]; then - printf 'pkgver=%s\n' "$new_pkgver" >> "$out_file" - printf 'pkgrel=%s\n' "$new_pkgrel" >> "$out_file" -fi diff --git a/packaging/aur/t3code-bin/PKGBUILD b/packaging/aur/t3code-bin/PKGBUILD index 1f5048b0c04..3eb6ccc35b2 100644 --- a/packaging/aur/t3code-bin/PKGBUILD +++ b/packaging/aur/t3code-bin/PKGBUILD @@ -102,39 +102,20 @@ EOF # Install the channel-specific icons shipped inside the AppImage so the # installed icon always matches the packaged release. - local icon_root="$srcdir/squashfs-root/usr/share/icons/hicolor" - local installed_icon=false - local icon size_dir - if [[ -d "$icon_root" ]]; then - for icon in "$icon_root"/*/apps/*.png; do - [[ -e "$icon" ]] || continue - size_dir="$(basename "$(dirname "$(dirname "$icon")")")" - install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code.png" - ln -sf t3code.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-desktop.png" - installed_icon=true - done - fi - - local root_icon - for root_icon in "$srcdir/squashfs-root/t3code.png" "$srcdir/squashfs-root/.DirIcon"; do - if [[ -f "$root_icon" ]]; then - install -Dm644 "$root_icon" "$pkgdir/usr/share/pixmaps/t3code.png" - ln -sf t3code.png "$pkgdir/usr/share/pixmaps/t3-code-desktop.png" - if [[ "$installed_icon" == false ]]; then - install -Dm644 "$root_icon" \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code.png" - ln -sf t3code.png \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3-code-desktop.png" - installed_icon=true - fi - break - fi + local icon size_dir installed_icon=false + for icon in "$srcdir/squashfs-root/usr/share/icons/hicolor"/*/apps/*.png; do + [[ -e "$icon" ]] || continue + size_dir="$(basename "$(dirname "$(dirname "$icon")")")" + install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code.png" + ln -sf t3code.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-desktop.png" + installed_icon=true done - if [[ "$installed_icon" == false ]]; then echo "No icon found inside the AppImage payload." >&2 return 1 fi + install -Dm644 "$srcdir/squashfs-root/t3code.png" "$pkgdir/usr/share/pixmaps/t3code.png" + ln -sf t3code.png "$pkgdir/usr/share/pixmaps/t3-code-desktop.png" install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code.desktop" << 'EOF' [Desktop Entry] diff --git a/packaging/aur/t3code-nightly-bin/PKGBUILD b/packaging/aur/t3code-nightly-bin/PKGBUILD index 97160fc01c9..c54c661e7b8 100644 --- a/packaging/aur/t3code-nightly-bin/PKGBUILD +++ b/packaging/aur/t3code-nightly-bin/PKGBUILD @@ -2,12 +2,12 @@ # Contributor: maria-rcks pkgname=t3code-nightly-bin -pkgver=0.0.29_nightly.20260717.835 +pkgver=0.0.29_nightly.20260717.836 pkgrel=1 pkgdesc='T3 Code nightly desktop app (official AppImage repackaged for Arch Linux)' arch=('x86_64') -_upstream_tag='v0.0.29-nightly.20260717.835' -_upstream_version='0.0.29-nightly.20260717.835' +_upstream_tag='v0.0.29-nightly.20260717.836' +_upstream_version='0.0.29-nightly.20260717.836' _appimage_name="T3-Code-${_upstream_version}-x86_64.AppImage" url='https://t3.codes' license=('MIT') @@ -51,7 +51,7 @@ source=( 'LICENSE' ) sha256sums=( - 'b36412e5d141de90837e579f3a09f1721282b8f3aedc3a802fb1723bd1c3019b' + '72c3923e33ce386fab68fb68c47fa4a41497579bd43901e8d5cba2e87ce920d2' '935d8f2af0c703f9c39517ee57cc4930b19d02d533be930b63f0e82f93614b43' ) @@ -102,39 +102,20 @@ EOF # Install the channel-specific icons shipped inside the AppImage so the # installed icon always matches the packaged release. - local icon_root="$srcdir/squashfs-root/usr/share/icons/hicolor" - local installed_icon=false - local icon size_dir - if [[ -d "$icon_root" ]]; then - for icon in "$icon_root"/*/apps/*.png; do - [[ -e "$icon" ]] || continue - size_dir="$(basename "$(dirname "$(dirname "$icon")")")" - install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code-nightly.png" - ln -sf t3code-nightly.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-nightly-desktop.png" - installed_icon=true - done - fi - - local root_icon - for root_icon in "$srcdir/squashfs-root/t3code.png" "$srcdir/squashfs-root/.DirIcon"; do - if [[ -f "$root_icon" ]]; then - install -Dm644 "$root_icon" "$pkgdir/usr/share/pixmaps/t3code-nightly.png" - ln -sf t3code-nightly.png "$pkgdir/usr/share/pixmaps/t3-code-nightly-desktop.png" - if [[ "$installed_icon" == false ]]; then - install -Dm644 "$root_icon" \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code-nightly.png" - ln -sf t3code-nightly.png \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3-code-nightly-desktop.png" - installed_icon=true - fi - break - fi + local icon size_dir installed_icon=false + for icon in "$srcdir/squashfs-root/usr/share/icons/hicolor"/*/apps/*.png; do + [[ -e "$icon" ]] || continue + size_dir="$(basename "$(dirname "$(dirname "$icon")")")" + install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3code-nightly.png" + ln -sf t3code-nightly.png "$pkgdir/usr/share/icons/hicolor/$size_dir/apps/t3-code-nightly-desktop.png" + installed_icon=true done - if [[ "$installed_icon" == false ]]; then echo "No icon found inside the AppImage payload." >&2 return 1 fi + install -Dm644 "$srcdir/squashfs-root/t3code.png" "$pkgdir/usr/share/pixmaps/t3code-nightly.png" + ln -sf t3code-nightly.png "$pkgdir/usr/share/pixmaps/t3-code-nightly-desktop.png" install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code-nightly.desktop" << 'EOF' [Desktop Entry] From fd501ac0ab0702edb209603d665e3bf818f6e5a4 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 17 Jul 2026 21:56:48 +0000 Subject: [PATCH 03/11] chore(packaging): trim comments and shorten AUR README Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 9 +--- packaging/aur/README.md | 52 ++++++----------------- packaging/aur/scripts/release.sh | 24 +---------- packaging/aur/t3code-bin/PKGBUILD | 3 -- packaging/aur/t3code-nightly-bin/PKGBUILD | 3 -- 5 files changed, 16 insertions(+), 75 deletions(-) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index 738cfbd0ce7..272352fdcbd 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -1,13 +1,6 @@ name: Publish AUR packages -# Publishes t3code-bin (stable) and t3code-nightly-bin to the AUR whenever a -# GitHub release is published. All logic lives in packaging/aur/scripts/release.sh: -# it matches the release tag to the package's channel, patches the PKGBUILD, -# test-builds with makepkg, and pushes PKGBUILD/.SRCINFO to the AUR over SSH. -# -# Publishing requires the AUR_SSH_PRIVATE_KEY secret (an SSH key authorized on -# the AUR account maintaining the packages). Without it (for example on forks) -# the job still test-builds the package as a dry run. +# See packaging/aur/README.md. on: release: diff --git a/packaging/aur/README.md b/packaging/aur/README.md index b3332506f77..0cee10c2a6d 100644 --- a/packaging/aur/README.md +++ b/packaging/aur/README.md @@ -1,49 +1,23 @@ # AUR packaging -Packaging for the Arch Linux (AUR) distribution of the T3 Code desktop app: +Packaging for [`t3code-bin`](https://aur.archlinux.org/packages/t3code-bin) (stable) and +[`t3code-nightly-bin`](https://aur.archlinux.org/packages/t3code-nightly-bin) (nightly), both +repackaging the official x86_64 AppImage from GitHub Releases. Icons come from the AppImage +payload, so each channel ships its own branding. -- [`t3code-bin`](https://aur.archlinux.org/packages/t3code-bin) — stable releases -- [`t3code-nightly-bin`](https://aur.archlinux.org/packages/t3code-nightly-bin) — nightly prereleases +`.github/workflows/publish-aur.yml` runs `scripts/release.sh ` for both packages when +a release is published (or via `workflow_dispatch` with optional `release_tag`/`pkgrel` inputs). +The script matches the release tag to the package's channel (other tags are a no-op), patches the +PKGBUILD, regenerates `.SRCINFO`, test-builds with `makepkg`, and pushes to the AUR over SSH. +Without the `AUR_SSH_PRIVATE_KEY` secret (an SSH key authorized on the AUR account) the push is +skipped as a dry run. Optional vars: `AUR_COMMIT_NAME`/`AUR_COMMIT_EMAIL` (committer identity), +`UPSTREAM_REPO` (release source, defaults to this repo). -Both packages repackage the official x86_64 AppImage published on GitHub Releases. Icons are taken -from the AppImage payload itself, so each channel ships its own branding (stable vs nightly icon) -automatically. +Committed PKGBUILDs are templates — CI patches the version fields at publish time. -## How publishing works - -`.github/workflows/publish-aur.yml` runs `scripts/release.sh ` for both packages -whenever a GitHub release is published. The script: - -1. Resolves the release matching the package's channel (stable tags `vX.Y.Z`, nightly tags - `vX.Y.Z-nightly.*`) and reads the AppImage sha256 from the release asset digest. A release for - the other channel is a no-op. -2. Patches the `PKGBUILD` (`pkgver`, `pkgrel`, `_upstream_tag`, `sha256sums`), regenerates - `.SRCINFO`, and test-builds the package with `makepkg`. -3. Pushes `PKGBUILD`, `.SRCINFO`, and `LICENSE` to the AUR over SSH — skipped as a dry run when - `AUR_SSH_PRIVATE_KEY` is not configured (for example on forks). - -The workflow can also be run manually (`workflow_dispatch`) with an optional release tag and a -`pkgrel` override for republishing the same upstream version. - -The committed `PKGBUILD` files are templates: CI patches the version fields at publish time, so the -committed `pkgver`/`sha256sums` values are only a snapshot of the last edit. - -## Required configuration - -| Kind | Name | Purpose | -| ------------------- | -------------------------------------- | --------------------------------------------------------------------------- | -| Secret | `AUR_SSH_PRIVATE_KEY` | SSH private key authorized on the AUR account that maintains both packages. | -| Variable (optional) | `AUR_COMMIT_NAME` / `AUR_COMMIT_EMAIL` | Committer identity for AUR pushes (defaults to `t3code-ci`). | -| Variable (optional) | `UPSTREAM_REPO` | Overrides the release source repo (defaults to this repository). | - -## Testing locally - -On an Arch system (or `archlinux:base-devel` container) with `gh`, `jq`, and `curl`: +Test locally on Arch (or `archlinux:base-devel`) with `gh`, `jq`, and `curl`: ```bash GH_TOKEN=$(gh auth token) GITHUB_REPOSITORY=pingdotgg/t3code \ packaging/aur/scripts/release.sh packaging/aur/t3code-bin ``` - -Without `AUR_SSH_PRIVATE_KEY` this resolves the latest stable release, builds the package, and -stops before pushing anything. diff --git a/packaging/aur/scripts/release.sh b/packaging/aur/scripts/release.sh index e61768ba6b3..9cb0aac43c2 100755 --- a/packaging/aur/scripts/release.sh +++ b/packaging/aur/scripts/release.sh @@ -1,18 +1,6 @@ #!/usr/bin/env bash -# Builds and publishes one AUR package from a published GitHub release. -# -# Usage: release.sh (packaging/aur/t3code-bin or .../t3code-nightly-bin) -# -# Environment: -# GH_TOKEN GitHub token for release lookups (required) -# RELEASE_TAG Release tag to publish; empty = newest matching release -# PKGREL Override pkgrel (default 1; bump to republish a version) -# AUR_SSH_PRIVATE_KEY SSH key for the AUR push; unset = dry run (build only) -# UPSTREAM_REPO Release source repo (default: $GITHUB_REPOSITORY) -# AUR_COMMIT_NAME / AUR_COMMIT_EMAIL Committer identity for the AUR push -# -# The stable package only matches vX.Y.Z releases and the nightly package only -# matches vX.Y.Z-nightly.* prereleases; a non-matching RELEASE_TAG is a no-op. +# Usage: release.sh +# See packaging/aur/README.md for the environment variables. set -euo pipefail package_dir="${1:?usage: release.sh }" @@ -39,8 +27,6 @@ case "$pkgname" in ;; esac -# --- Resolve the release and its AppImage asset ------------------------------ - if [[ -n "${RELEASE_TAG:-}" ]]; then release_json="$(gh api "repos/$UPSTREAM_REPO/releases/tags/$RELEASE_TAG")" else @@ -75,7 +61,6 @@ fi sha256="$(jq -r '.digest // empty' <<<"$asset_json")" sha256="${sha256#sha256:}" if [[ -z "$sha256" ]]; then - # Old releases predate GitHub's asset digests; hash the download instead. tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT curl -fL --retry 3 "$(jq -r '.browser_download_url' <<<"$asset_json")" -o "$tmp/appimage" @@ -84,8 +69,6 @@ fi echo "Publishing $pkgname $pkgver (release $tag, sha256 $sha256)" -# --- Patch the PKGBUILD and build --------------------------------------------- - cd "$package_dir" sed -Ei \ -e "s/^pkgver=.*/pkgver=$pkgver/" \ @@ -96,7 +79,6 @@ sed -Ei \ PKGBUILD cp -f "$repo_root/LICENSE" LICENSE -# makepkg refuses to run as root; hand off to the "builder" user in CI. run_makepkg() { if [[ "$(id -u)" == 0 ]]; then chown -R builder:builder "$package_dir" @@ -109,8 +91,6 @@ run_makepkg() { run_makepkg makepkg --printsrcinfo > .SRCINFO run_makepkg makepkg -f --nodeps --noconfirm -# --- Push PKGBUILD/.SRCINFO/LICENSE to the AUR -------------------------------- - if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then echo "AUR_SSH_PRIVATE_KEY is not set; skipping AUR push (dry run)." exit 0 diff --git a/packaging/aur/t3code-bin/PKGBUILD b/packaging/aur/t3code-bin/PKGBUILD index 3eb6ccc35b2..eeafbcab4ff 100644 --- a/packaging/aur/t3code-bin/PKGBUILD +++ b/packaging/aur/t3code-bin/PKGBUILD @@ -70,7 +70,6 @@ package() { install -d "$pkgdir/opt/$pkgname" cp -a "$srcdir/squashfs-root/." "$pkgdir/opt/$pkgname/" - # Preserve upstream execute bits while ensuring the payload stays readable. chmod -R a+rX "$pkgdir/opt/$pkgname" install -Dm755 /dev/stdin "$pkgdir/usr/bin/t3code" << 'EOF' @@ -100,8 +99,6 @@ EOF ln -s t3code "$pkgdir/usr/bin/t3-code-desktop" - # Install the channel-specific icons shipped inside the AppImage so the - # installed icon always matches the packaged release. local icon size_dir installed_icon=false for icon in "$srcdir/squashfs-root/usr/share/icons/hicolor"/*/apps/*.png; do [[ -e "$icon" ]] || continue diff --git a/packaging/aur/t3code-nightly-bin/PKGBUILD b/packaging/aur/t3code-nightly-bin/PKGBUILD index c54c661e7b8..ccf5b82cf03 100644 --- a/packaging/aur/t3code-nightly-bin/PKGBUILD +++ b/packaging/aur/t3code-nightly-bin/PKGBUILD @@ -70,7 +70,6 @@ package() { install -d "$pkgdir/opt/$pkgname" cp -a "$srcdir/squashfs-root/." "$pkgdir/opt/$pkgname/" - # Preserve upstream execute bits while ensuring the payload stays readable. chmod -R a+rX "$pkgdir/opt/$pkgname" install -Dm755 /dev/stdin "$pkgdir/usr/bin/t3code-nightly" << 'EOF' @@ -100,8 +99,6 @@ EOF ln -s t3code-nightly "$pkgdir/usr/bin/t3-code-nightly-desktop" - # Install the channel-specific icons shipped inside the AppImage so the - # installed icon always matches the packaged release. local icon size_dir installed_icon=false for icon in "$srcdir/squashfs-root/usr/share/icons/hicolor"/*/apps/*.png; do [[ -e "$icon" ]] || continue From 8bc01753bb8b8cd6ffd856b1565676aee9a44b93 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 17 Jul 2026 22:45:24 +0000 Subject: [PATCH 04/11] docs: drop nightly comment from AUR install snippet Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b7af8c046b0..fb6223e92df 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ brew install --cask t3-code ```bash yay -S t3code-bin -# nightly builds yay -S t3code-nightly-bin ``` From e023df62e0b1f10ba0525328681b6e10336b3fcd Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 18 Jul 2026 02:34:24 +0000 Subject: [PATCH 05/11] test: run AUR publish on ubuntu-latest Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index 272352fdcbd..efe2fcd7ec5 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -23,7 +23,7 @@ permissions: jobs: publish: name: Build and publish (${{ matrix.package }}) - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false From ca0ab90692b76b54240327222c821300f83a5bdd Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 18 Jul 2026 02:34:56 +0000 Subject: [PATCH 06/11] test: temporary push trigger for AUR workflow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index efe2fcd7ec5..a15af4a7f8d 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -3,6 +3,9 @@ name: Publish AUR packages # See packaging/aur/README.md. on: + push: + branches: + - devin/1784322789-aur-packaging release: types: - published From f6c7d90e238df40cd30bfe1e1db2cf806cf39413 Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 18 Jul 2026 02:38:03 +0000 Subject: [PATCH 07/11] test: point UPSTREAM_REPO at pingdotgg for fork test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index a15af4a7f8d..e060c57e9dd 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -51,6 +51,7 @@ jobs: - name: Build and publish env: GH_TOKEN: ${{ github.token }} + UPSTREAM_REPO: pingdotgg/t3code RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} PKGREL: ${{ github.event.inputs.pkgrel }} AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} From 466f805462835f69c4340eecc10dbc54dbd1b5a6 Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 18 Jul 2026 02:41:35 +0000 Subject: [PATCH 08/11] chore: restore blacksmith runner after ubuntu CI test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/publish-aur.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index e060c57e9dd..272352fdcbd 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -3,9 +3,6 @@ name: Publish AUR packages # See packaging/aur/README.md. on: - push: - branches: - - devin/1784322789-aur-packaging release: types: - published @@ -26,7 +23,7 @@ permissions: jobs: publish: name: Build and publish (${{ matrix.package }}) - runs-on: ubuntu-latest + runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 30 strategy: fail-fast: false @@ -51,7 +48,6 @@ jobs: - name: Build and publish env: GH_TOKEN: ${{ github.token }} - UPSTREAM_REPO: pingdotgg/t3code RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} PKGREL: ${{ github.event.inputs.pkgrel }} AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} From af34b90de33b434591589fb3e53b708e293e8ec2 Mon Sep 17 00:00:00 2001 From: maria Date: Mon, 20 Jul 2026 00:48:14 +0000 Subject: [PATCH 09/11] feat(web): add composer attach button and drag-drop overlay Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/src/components/chat/ChatComposer.tsx | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 6ec2e631d19..1ef475b266d 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -97,12 +97,14 @@ import { BotIcon, CheckIcon, CircleAlertIcon, + ImageUpIcon, ListTodoIcon, PencilRulerIcon, type LucideIcon, LockIcon, LockOpenIcon, PenLineIcon, + PlusIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -1832,6 +1834,20 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) removeComposerImageFromDraft(imageId); }; + const attachFileInputRef = useRef(null); + + const openAttachFilePicker = () => { + attachFileInputRef.current?.click(); + }; + + const onAttachFileInputChange = (event: React.ChangeEvent) => { + const files = Array.from(event.target.files ?? []); + event.target.value = ""; + if (files.length === 0) return; + addComposerImages(files); + focusComposer(); + }; + // ------------------------------------------------------------------ // Callbacks: paste / drag // ------------------------------------------------------------------ @@ -2092,7 +2108,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ref={composerSurfaceRef} data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"} className={cn( - "chat-composer-glass rounded-[20px] border transition-[background-color] duration-200 has-focus-visible:border-ring/45", + "chat-composer-glass relative rounded-[20px] border transition-[background-color] duration-200 has-focus-visible:border-ring/45", isDragOverComposer ? "border-primary/70 bg-accent/45" : "border-border", environmentUnavailable || projectSelectionRequired ? "opacity-75" : null, composerProviderState.composerSurfaceClassName, @@ -2116,6 +2132,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) scheduleComposerCollapseCheck(); }} > + {isDragOverComposer ? ( +
+
+ + Drop images to attach +
+
+ ) : null} {!isComposerCollapsedMobile && (activePendingApproval ? (
@@ -2508,6 +2532,32 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) )} >
+ + + + } + > + + + Attach images + Date: Mon, 20 Jul 2026 02:15:15 +0000 Subject: [PATCH 10/11] feat(web): accept image drops anywhere on the chat view Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/src/components/ChatView.tsx | 53 +++++++++++++++- apps/web/src/components/chat/ChatComposer.tsx | 60 +++---------------- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f175a21a131..62ddbdee444 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -138,7 +138,7 @@ import { BranchToolbar } from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; -import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; +import { ChevronDownIcon, ImageUpIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; import { cn, randomHex } from "~/lib/utils"; import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; import { stackedThreadToast, toastManager } from "./ui/toast"; @@ -1195,6 +1195,8 @@ function ChatViewContent(props: ChatViewProps) { const localComposerRef = useRef(null); const composerRef = useComposerHandleContext() ?? localComposerRef; const [showScrollToBottom, setShowScrollToBottom] = useState(false); + const [isDraggingFilesOverView, setIsDraggingFilesOverView] = useState(false); + const viewDragDepthRef = useRef(0); const [expandedImage, setExpandedImage] = useState(null); const [optimisticUserMessages, setOptimisticUserMessages] = useState([]); const optimisticUserMessagesRef = useRef(optimisticUserMessages); @@ -5198,8 +5200,55 @@ function ChatViewContent(props: ChatViewProps) { ) : null ) : null; + const onViewDragEnter = (event: React.DragEvent) => { + if (!event.dataTransfer.types.includes("Files")) return; + event.preventDefault(); + viewDragDepthRef.current += 1; + setIsDraggingFilesOverView(true); + }; + + const onViewDragOver = (event: React.DragEvent) => { + if (!event.dataTransfer.types.includes("Files")) return; + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + setIsDraggingFilesOverView(true); + }; + + const onViewDragLeave = (event: React.DragEvent) => { + if (!event.dataTransfer.types.includes("Files")) return; + event.preventDefault(); + const nextTarget = event.relatedTarget; + if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return; + viewDragDepthRef.current = Math.max(0, viewDragDepthRef.current - 1); + if (viewDragDepthRef.current === 0) { + setIsDraggingFilesOverView(false); + } + }; + + const onViewDrop = (event: React.DragEvent) => { + if (!event.dataTransfer.types.includes("Files")) return; + event.preventDefault(); + viewDragDepthRef.current = 0; + setIsDraggingFilesOverView(false); + composerRef.current?.addImages(Array.from(event.dataTransfer.files)); + }; + return ( -
+
+ {isDraggingFilesOverView ? ( +
+
+ + Drop images to attach +
+
+ ) : null} {rightPanelOpen && !shouldUsePlanSidebarSheet ? panelLayoutControls : null}
void; /** Insert a terminal context from the terminal drawer. */ addTerminalContext: (selection: TerminalContextSelection) => void; + /** Attach image files (e.g. from a view-level drop zone). */ + addImages: (files: File[]) => void; /** Get the current prompt/effort/model state for use in send. */ getSendContext: () => { prompt: string; @@ -893,7 +894,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const [composerHighlightedSearchKey, setComposerHighlightedSearchKey] = useState( null, ); - const [isDragOverComposer, setIsDragOverComposer] = useState(false); const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); @@ -916,7 +916,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const mobileComposerExpandFrameRef = useRef(null); const mobileComposerExpandReleaseFrameRef = useRef(null); const mobileComposerExpandInFlightRef = useRef(false); - const dragDepthRef = useRef(0); // ------------------------------------------------------------------ // Derived: composer send state @@ -1317,8 +1316,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) setComposerHighlightedItemId(null); setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length)); - dragDepthRef.current = 0; - setIsDragOverComposer(false); }, [draftId, activeThreadId, promptRef]); // ------------------------------------------------------------------ @@ -1860,40 +1857,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) addComposerImages(imageFiles); }; - const onComposerDragEnter = (event: React.DragEvent) => { - if (!event.dataTransfer.types.includes("Files")) return; - event.preventDefault(); - dragDepthRef.current += 1; - setIsDragOverComposer(true); - }; - - const onComposerDragOver = (event: React.DragEvent) => { - if (!event.dataTransfer.types.includes("Files")) return; - event.preventDefault(); - event.dataTransfer.dropEffect = "copy"; - setIsDragOverComposer(true); - }; - - const onComposerDragLeave = (event: React.DragEvent) => { - if (!event.dataTransfer.types.includes("Files")) return; - event.preventDefault(); - const nextTarget = event.relatedTarget; - if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return; - dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); - if (dragDepthRef.current === 0) { - setIsDragOverComposer(false); - } - }; - - const onComposerDrop = (event: React.DragEvent) => { - if (!event.dataTransfer.types.includes("Files")) return; - event.preventDefault(); - dragDepthRef.current = 0; - setIsDragOverComposer(false); - const files = Array.from(event.dataTransfer.files); - addComposerImages(files); - focusComposer(); - }; const handleInterruptPrimaryAction = useCallback(() => { void onInterrupt(); }, [onInterrupt]); @@ -2005,6 +1968,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : null, ); }, + addImages: (files: File[]) => { + addComposerImages(files); + focusComposer(); + }, addTerminalContext: (selection: TerminalContextSelection) => { if (!activeThread) return; const snapshot = composerEditorRef.current?.readSnapshot() ?? { @@ -2099,17 +2066,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) "group rounded-[22px] p-px transition-colors duration-200", composerProviderState.composerFrameClassName, )} - onDragEnter={onComposerDragEnter} - onDragOver={onComposerDragOver} - onDragLeave={onComposerDragLeave} - onDrop={onComposerDrop} >
- {isDragOverComposer ? ( -
-
- - Drop images to attach -
-
- ) : null} {!isComposerCollapsedMobile && (activePendingApproval ? (
From 96e60279a0eb991fb3b1ed26272f879fb667ea2d Mon Sep 17 00:00:00 2001 From: maria Date: Mon, 20 Jul 2026 18:14:39 +0000 Subject: [PATCH 11/11] refactor(web): tidy attach input placement Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/src/components/chat/ChatComposer.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 063a175c81c..0fce9b8fc83 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -916,6 +916,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const mobileComposerExpandFrameRef = useRef(null); const mobileComposerExpandReleaseFrameRef = useRef(null); const mobileComposerExpandInFlightRef = useRef(false); + const attachFileInputRef = useRef(null); // ------------------------------------------------------------------ // Derived: composer send state @@ -1831,8 +1832,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) removeComposerImageFromDraft(imageId); }; - const attachFileInputRef = useRef(null); - const openAttachFilePicker = () => { attachFileInputRef.current?.click(); }; @@ -1846,7 +1845,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }; // ------------------------------------------------------------------ - // Callbacks: paste / drag + // Callbacks: paste // ------------------------------------------------------------------ const onComposerPaste = (event: React.ClipboardEvent) => { const files = Array.from(event.clipboardData.files); @@ -2071,7 +2070,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ref={composerSurfaceRef} data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"} className={cn( - "chat-composer-glass relative rounded-[20px] border border-border transition-[background-color] duration-200 has-focus-visible:border-ring/45", + "chat-composer-glass rounded-[20px] border border-border transition-[background-color] duration-200 has-focus-visible:border-ring/45", environmentUnavailable || projectSelectionRequired ? "opacity-75" : null, composerProviderState.composerSurfaceClassName, )}