name: "publish" on: release: types: [published] jobs: publish-windows: name: Build and publish Windows installer permissions: contents: write environment: production env: # --- Gitea (unchanged) --- GITEA_API: https://git.cbsk-tech.de/api/v1 OWNER: Christoph REPO: GitLite GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }} GITEA_FALLBACK_TOKEN: ${{ github.token }} # --- MinIO / S3-compatible upload --- MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }} # e.g. https://updates.example.com S3_BUCKET: ${{ vars.S3_BUCKET }} # e.g. updates AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} # used by aws cli AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} # used by aws cli ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }} # e.g. https://updates.example.com S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} # used by cicd_tool S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} # used by cicd_tool defaults: run: working-directory: GitLite runs-on: windows-latest steps: # cicd_tool/main.py expects the repo at /GitLite, so check out there. - uses: actions/checkout@v5 with: path: GitLite - name: setup node uses: actions/setup-node@v4 with: node-version: lts/* - name: install frontend dependencies run: npm ci - name: Update package.json Version run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version - name: Update tauri.conf.json Version env: RELEASE_VERSION: ${{ github.ref_name }} run: | node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');" - name: Commit and Push Changes shell: powershell run: | git config user.name '${{ vars.USERNAME_GIT }}' git config user.email '${{ vars.EMAIL_GIT }}' # npm version also bumps the version inside package-lock.json, so stage it # too -- otherwise it stays as an unstaged change and blocks the rebase. git add package.json package-lock.json src-tauri/tauri.conf.json git commit -m 'Update version to ${{ github.ref_name }}' if ($LASTEXITCODE -ne 0) { # Version files already match the tag -> nothing to push, succeed quietly. Write-Host 'No version changes to commit; skipping push.' exit 0 } # The release build runs in detached HEAD on the tag. Resolve the branch # that should receive the version bump. $branch = '${{ github.event.release.target_commitish }}' if ([string]::IsNullOrEmpty($branch) -or $branch -match '^[0-9a-f]{40}$') { $branch = ( git branch -r --contains $env:GITHUB_SHA | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '->') } | ForEach-Object { $_ -replace '^(?:remotes/)?[^/]+/', '' } | Select-Object -First 1 ) } if ([string]::IsNullOrEmpty($branch)) { Write-Host 'Could not determine branch for push.' git branch -a exit 1 } # The tag may be behind the branch tip. Replay the bump commit on top of # the current remote branch so the push is a clean fast-forward. git fetch origin $branch git rebase "origin/$branch" if ($LASTEXITCODE -ne 0) { git rebase --abort Write-Host "Rebase onto origin/$branch failed." exit 1 } git push origin "HEAD:refs/heads/$branch" - name: Build Tauri App env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}} NO_STRIP: "" run: npm run tauri -- build --bundles nsis # ---------------------- # Upload to MinIO (via mc) and create latest.json # ---------------------- - name: Upload artifacts to MinIO with cicd_tool working-directory: GitLite/cicd_tool env: PUBLISH_PLATFORM: windows-x86_64 run: | uv sync uv run main.py publish-arch: name: Build and publish AUR packages needs: publish-ubuntu permissions: contents: write runs-on: archlinux environment: production env: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} GITEA_API: https://git.cbsk-tech.de/api/v1 OWNER: Christoph REPO: GitLite GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }} GITEA_FALLBACK_TOKEN: ${{ github.token }} defaults: run: working-directory: GitLite steps: - name: Prepare Arch job container working-directory: / run: | set -euo pipefail # The runner image may contain only a single geo mirror. Seed several # currently active HTTPS mirrors and retain the image's list as a # fallback so one slow CDN cannot fail the complete release. ARCH_MIRROR_BACKUP="$(mktemp)" cp /etc/pacman.d/mirrorlist "$ARCH_MIRROR_BACKUP" printf '%s\n' \ 'Server = https://frankfurt.mirror.pkgbuild.com/$repo/os/$arch' \ 'Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch' \ 'Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch' \ 'Server = https://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch' \ 'Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' \ > /etc/pacman.d/mirrorlist sed -n '/^[[:space:]]*Server[[:space:]]*=/p' "$ARCH_MIRROR_BACKUP" \ >> /etc/pacman.d/mirrorlist ARCH_PACKAGES=( base-devel curl git nodejs npm openssh rust webkit2gtk-4.1 gtk3 hicolor-icon-theme libappindicator-gtk3 librsvg xdotool ) for ARCH_INSTALL_ATTEMPT in 1 2 3; do echo "pacman attempt $ARCH_INSTALL_ATTEMPT of 3" timeout --signal=TERM 30m \ pacman -Syu --needed --noconfirm --noprogressbar \ "${ARCH_PACKAGES[@]}" \ /dev/null || true wait "$ARCH_HEARTBEAT_PID" 2>/dev/null || true if [ "$ARCH_PACMAN_STATUS" -eq 0 ]; then break fi if [ "$ARCH_PACMAN_STATUS" -eq 124 ]; then echo "pacman attempt timed out after 30 minutes" >&2 fi if [ "$ARCH_INSTALL_ATTEMPT" -eq 3 ]; then echo "pacman failed after 3 attempts" >&2 exit 1 fi sleep "$((ARCH_INSTALL_ATTEMPT * 5))" done - uses: actions/checkout@v5 with: path: GitLite - name: Set release version env: RELEASE_VERSION: ${{ github.ref_name }} run: | npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version PACKAGE_VERSION="$(node -p "require('./package.json').version")" echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV" echo "RELEASE_TAG=$RELEASE_VERSION" >> "$GITHUB_ENV" - name: Generate and build AUR package run: | if ! id -u builder >/dev/null 2>&1; then useradd --create-home builder fi AUR_SOURCE_DIR="$(mktemp -d)" cp PKGBUILD "$AUR_SOURCE_DIR/PKGBUILD" SOURCE_ARCHIVE="$AUR_SOURCE_DIR/gitty-desktop-$PACKAGE_VERSION.tar.gz" curl --fail --location --silent --show-error \ --output "$SOURCE_ARCHIVE" \ "https://git.cbsk-tech.de/Christoph/GitLite/archive/$RELEASE_TAG.tar.gz" CHECKSUM="$(sha256sum "$SOURCE_ARCHIVE" | cut -d ' ' -f 1)" sed -i \ -e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \ -e "s/^pkgrel=.*/pkgrel=1/" \ -e "s/^_tag=.*/_tag=$RELEASE_TAG/" \ -e "s/^sha256sums=.*/sha256sums=('$CHECKSUM')/" \ "$AUR_SOURCE_DIR/PKGBUILD" chown -R builder:builder "$AUR_SOURCE_DIR" runuser -u builder -- \ bash -lc "cd '$AUR_SOURCE_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO" echo "AUR_SOURCE_DIR=$AUR_SOURCE_DIR" >> "$GITHUB_ENV" - name: Generate and build binary AUR package run: | set -euo pipefail TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}" if [ -z "$TOKEN" ]; then echo "A Gitea token is required to download the AppImage" >&2 exit 1 fi RELEASE_JSON="$(curl --fail --location --silent --show-error \ --header "Authorization: token $TOKEN" \ "$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")" export RELEASE_JSON APPIMAGE_URL="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); const a=(r.assets || []).find(a => String(a.name || '').toLowerCase().endsWith('.appimage')); if (!a?.browser_download_url) process.exit(1); process.stdout.write(a.browser_download_url)")" APPIMAGE_NAME="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); const a=(r.assets || []).find(a => String(a.name || '').toLowerCase().endsWith('.appimage')); if (!a?.name) process.exit(1); process.stdout.write(a.name)")" AUR_BIN_DIR="$(mktemp -d)" cp PKGBUILD-bin "$AUR_BIN_DIR/PKGBUILD" APPIMAGE_PATH="$AUR_BIN_DIR/$APPIMAGE_NAME" curl --fail --location --silent --show-error \ --header "Authorization: token $TOKEN" \ --output "$APPIMAGE_PATH" "$APPIMAGE_URL" APPIMAGE_CHECKSUM="$(sha256sum "$APPIMAGE_PATH" | cut -d ' ' -f 1)" ICON_PATH="$AUR_BIN_DIR/gitty-desktop.png" curl --fail --location --silent --show-error \ --output "$ICON_PATH" \ "https://git.cbsk-tech.de/Christoph/GitLite/raw/tag/$RELEASE_TAG/src-tauri/icons/icon.png" ICON_CHECKSUM="$(sha256sum "$ICON_PATH" | cut -d ' ' -f 1)" sed -i \ -e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \ -e "s/^pkgrel=.*/pkgrel=1/" \ -e "s|^_appimage=.*|_appimage=\"$APPIMAGE_NAME\"|" \ -e "s|^_artifact_url=.*|_artifact_url=\"$APPIMAGE_URL\"|" \ -e "s/^_tag=.*/_tag=$RELEASE_TAG/" \ -e "/^sha256sums=/,/^[[:space:]]*'SKIP')$/c\\sha256sums=('$APPIMAGE_CHECKSUM'\\n '$ICON_CHECKSUM')" \ "$AUR_BIN_DIR/PKGBUILD" chown -R builder:builder "$AUR_BIN_DIR" runuser -u builder -- \ bash -lc "cd '$AUR_BIN_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO" ARCH_BIN_PACKAGE_PATH="$(find "$AUR_BIN_DIR" -maxdepth 1 -type f -name 'gitty-desktop-bin-*.pkg.tar.zst' -print -quit)" if [ -z "$ARCH_BIN_PACKAGE_PATH" ]; then echo "The binary Arch package was not created" >&2 exit 1 fi echo "AUR_BIN_DIR=$AUR_BIN_DIR" >> "$GITHUB_ENV" echo "ARCH_BIN_PACKAGE_PATH=$ARCH_BIN_PACKAGE_PATH" >> "$GITHUB_ENV" - name: Attach binary Arch package to Gitea release run: | set -euo pipefail TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}" if [ -z "$TOKEN" ]; then echo "A Gitea token is required to upload the Arch package" >&2 exit 1 fi RELEASE_JSON="$(curl --fail --location --silent --show-error \ --header "Authorization: token $TOKEN" \ "$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")" export RELEASE_JSON RELEASE_ID="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); if (!r.id) process.exit(1); process.stdout.write(String(r.id))")" PACKAGE_NAME="${ARCH_BIN_PACKAGE_PATH##*/}" export PACKAGE_NAME if node -e "const r=JSON.parse(process.env.RELEASE_JSON); process.exit((r.assets || []).some(a => a.name === process.env.PACKAGE_NAME) ? 0 : 1)"; then echo "Release asset $PACKAGE_NAME already exists; skipping upload." else curl --fail --location --silent --show-error \ --request POST \ --header "Authorization: token $TOKEN" \ --form "attachment=@$ARCH_BIN_PACKAGE_PATH;type=application/octet-stream" \ "$GITEA_API/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets?name=$PACKAGE_NAME" echo echo "Attached $PACKAGE_NAME to Gitea release $RELEASE_TAG." fi - name: Publish PKGBUILD to AUR env: AUR_GIT_NAME: ${{ vars.USERNAME_GIT }} AUR_GIT_EMAIL: ${{ vars.EMAIL_GIT }} run: | set -euo pipefail if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then echo "AUR_SSH_PRIVATE_KEY is required" >&2 exit 1 fi install -d -m 0700 "$HOME/.ssh" printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$HOME/.ssh/aur" chmod 0600 "$HOME/.ssh/aur" AUR_RETRY_DELAYS=(30 60 120 240) AUR_KNOWN_HOSTS_TEMP="$(mktemp)" AUR_HOST_KEY_READY=0 for AUR_ATTEMPT in 1 2 3 4 5; do echo "AUR host-key attempt $AUR_ATTEMPT of 5" if timeout 30s ssh-keyscan -T 15 -H aur.archlinux.org > "$AUR_KNOWN_HOSTS_TEMP" \ && [ -s "$AUR_KNOWN_HOSTS_TEMP" ]; then AUR_HOST_KEY_READY=1 break fi if [ "$AUR_ATTEMPT" -lt 5 ]; then sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}" fi done if [ "$AUR_HOST_KEY_READY" -ne 1 ]; then echo "Could not retrieve the AUR SSH host key after 5 attempts" >&2 exit 1 fi install -m 0600 "$AUR_KNOWN_HOSTS_TEMP" "$HOME/.ssh/known_hosts" export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/aur -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=2" publish_aur_package() { local package_name="$1" local package_source_dir="$2" local checkout_root checkout clone_candidate pushed checkout_root="$(mktemp -d)" checkout="" for AUR_ATTEMPT in 1 2 3 4 5; do clone_candidate="$checkout_root/attempt-$AUR_ATTEMPT" echo "$package_name clone attempt $AUR_ATTEMPT of 5" if timeout 3m git -c init.defaultBranch=master clone \ "ssh://aur@aur.archlinux.org/$package_name.git" "$clone_candidate"; then checkout="$clone_candidate" break fi if [ "$AUR_ATTEMPT" -lt 5 ]; then echo "AUR is unavailable; retrying after backoff" sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}" fi done if [ -z "$checkout" ]; then echo "Could not clone $package_name after 5 attempts" >&2 return 1 fi cp "$package_source_dir/PKGBUILD" "$package_source_dir/.SRCINFO" "$checkout/" git -C "$checkout" config user.name "${AUR_GIT_NAME:-Gitty Release Bot}" git -C "$checkout" config user.email "${AUR_GIT_EMAIL:-aur@localhost}" git -C "$checkout" add PKGBUILD .SRCINFO if git -C "$checkout" diff --cached --quiet; then echo "$package_name already matches release $PACKAGE_VERSION" return 0 fi git -C "$checkout" commit -m "Update to $PACKAGE_VERSION" pushed=0 for AUR_ATTEMPT in 1 2 3 4 5; do echo "$package_name push attempt $AUR_ATTEMPT of 5" if timeout 3m git -C "$checkout" push origin HEAD:master; then pushed=1 break fi if [ "$AUR_ATTEMPT" -lt 5 ]; then echo "AUR push failed; retrying after backoff" sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}" fi done if [ "$pushed" -ne 1 ]; then echo "Could not publish $package_name after 5 attempts" >&2 return 1 fi } publish_aur_package gitty-desktop "$AUR_SOURCE_DIR" publish_aur_package gitty-desktop-bin "$AUR_BIN_DIR" publish-ubuntu: name: Build and publish Ubuntu AppImage permissions: contents: write environment: production env: GITEA_API: https://git.cbsk-tech.de/api/v1 OWNER: Christoph REPO: GitLite GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }} GITEA_FALLBACK_TOKEN: ${{ github.token }} MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }} S3_BUCKET: ${{ vars.S3_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }} S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} defaults: run: working-directory: GitLite runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 with: path: GitLite - name: Install Ubuntu dependencies run: | sudo apt-get update sudo apt-get install -y build-essential curl wget file libssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev libfuse2 - name: Setup Node uses: actions/setup-node@v4 with: node-version: lts/* - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install frontend dependencies run: npm ci - name: Update package.json version run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version - name: Update tauri.conf.json version env: RELEASE_VERSION: ${{ github.ref_name }} run: | node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');" - name: Build Tauri App env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}} NO_STRIP: "1" run: npm run tauri -- build --bundles appimage - name: Upload artifacts to MinIO with cicd_tool working-directory: GitLite/cicd_tool env: PUBLISH_PLATFORM: linux-x86_64 run: | uv sync uv run main.py