chore: import upstream snapshot with attribution
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
name: APT/DNF Repo Heartbeat
|
||||
|
||||
# Walks the published .deb and .rpm URLs through the full
|
||||
# Pages 301 → Worker 302 → Releases 302 → CDN 200 chain daily,
|
||||
# asserts ordered hops, asserts size match against the Releases
|
||||
# asset, and opens a tracking issue (with a format-specific label)
|
||||
# on failure. Auto-closes the issue when the format recovers.
|
||||
#
|
||||
# Pre-Phase-4a: the gate step skips gracefully when the production
|
||||
# Worker isn't live yet. Once Phase 4a is done, the gate passes
|
||||
# and the full chain is exercised every day.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # daily noon UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
ping:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# deb-transitional probes the fixed-version dummy package
|
||||
# (Package: claude-desktop → Depends: claude-desktop-unofficial)
|
||||
# that migrates legacy apt installs; its Worker redirect goes
|
||||
# via releases/latest and gets one extra hop.
|
||||
format: [deb, rpm, deb-transitional]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
WORKER_DOMAIN: pkg.claude-desktop-debian.dev
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
steps:
|
||||
- name: Skip if Worker not live yet
|
||||
id: gate
|
||||
run: |
|
||||
if curl -fsI --max-time 10 \
|
||||
"https://${WORKER_DOMAIN}/dists/stable/InRelease" >/dev/null; then
|
||||
echo "live=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Worker live; running heartbeat."
|
||||
else
|
||||
echo "live=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Worker not live; heartbeat skipping (expected before Phase 4a)."
|
||||
fi
|
||||
|
||||
- name: Resolve latest release for ${{ matrix.format }}
|
||||
if: steps.gate.outputs.live == 'true'
|
||||
id: latest
|
||||
run: |
|
||||
tag=$(gh release list --limit 1 --json tagName \
|
||||
--jq '.[0].tagName' \
|
||||
--repo aaddrick/claude-desktop-debian)
|
||||
repoVer="${tag#v}"; repoVer="${repoVer%+claude*}"
|
||||
claudeVer="${tag#*+claude}"
|
||||
if [[ "${{ matrix.format }}" == "deb" ]]; then
|
||||
asset="claude-desktop-unofficial_${claudeVer}-${repoVer}_amd64.deb"
|
||||
url="https://aaddrick.github.io/claude-desktop-debian/pool/main/c/claude-desktop-unofficial/${asset}"
|
||||
elif [[ "${{ matrix.format }}" == "deb-transitional" ]]; then
|
||||
# Fixed-version dummy deb; filename is release-independent
|
||||
asset="claude-desktop_1.16000.0-1_all.deb"
|
||||
url="https://aaddrick.github.io/claude-desktop-debian/pool/main/c/claude-desktop/${asset}"
|
||||
else
|
||||
asset="claude-desktop-unofficial-${claudeVer}-${repoVer}-1.x86_64.rpm"
|
||||
url="https://aaddrick.github.io/claude-desktop-debian/rpm/x86_64/${asset}"
|
||||
fi
|
||||
{
|
||||
echo "tag=${tag}"
|
||||
echo "asset=${asset}"
|
||||
echo "url=${url}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ordered chain + fetch + size match
|
||||
if: steps.gate.outputs.live == 'true'
|
||||
env:
|
||||
ASSET: ${{ steps.latest.outputs.asset }}
|
||||
URL: ${{ steps.latest.outputs.url }}
|
||||
TAG: ${{ steps.latest.outputs.tag }}
|
||||
FORMAT: ${{ matrix.format }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Wait for propagation; fail after 5 min instead of cargo-cult sleep
|
||||
deadline=$((SECONDS + 300))
|
||||
until curl -fsI --max-time 10 "$URL" -o /dev/null; do
|
||||
if [[ $SECONDS -gt $deadline ]]; then
|
||||
echo "::error::Reachability timeout for ${URL}"
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Walk redirect chain hop-by-hop, asserting each hop's pattern
|
||||
# in order. Hop 0 may be http:// (see ci.yml smoke-test comment
|
||||
# for the Pages https_enforced=false background).
|
||||
expected_hops=(
|
||||
"https?://${WORKER_DOMAIN}/"
|
||||
"https://github\\.com/aaddrick/claude-desktop-debian/releases/download/"
|
||||
"https://(objects|release-assets)\\.githubusercontent\\.com/"
|
||||
)
|
||||
if [[ "$FORMAT" == "deb-transitional" ]]; then
|
||||
# The transitional deb's fixed version encodes no release
|
||||
# tag, so the Worker sends it to releases/latest/download,
|
||||
# which GitHub resolves with one extra 302.
|
||||
expected_hops=(
|
||||
"https?://${WORKER_DOMAIN}/"
|
||||
"https://github\\.com/aaddrick/claude-desktop-debian/releases/latest/download/"
|
||||
"https://github\\.com/aaddrick/claude-desktop-debian/releases/download/"
|
||||
"https://(objects|release-assets)\\.githubusercontent\\.com/"
|
||||
)
|
||||
fi
|
||||
url="$URL"
|
||||
release_url=""
|
||||
for i in "${!expected_hops[@]}"; do
|
||||
hop_status=$(curl -s -o /dev/null -w '%{http_code}' "$url")
|
||||
redirect_url=$(curl -s -o /dev/null -w '%{redirect_url}' "$url")
|
||||
echo "Hop ${i}: ${hop_status} ${url} -> ${redirect_url}"
|
||||
if [[ ! "$hop_status" =~ ^30[12]$ ]]; then
|
||||
echo "::error::Hop ${i}: expected 301/302, got ${hop_status}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$redirect_url" =~ ^${expected_hops[$i]} ]]; then
|
||||
echo "::error::Hop ${i} mismatch:"
|
||||
echo "::error:: expected: ${expected_hops[$i]}"
|
||||
echo "::error:: got: ${redirect_url}"
|
||||
exit 1
|
||||
fi
|
||||
# Remember the tag-bearing releases/download hop for the
|
||||
# size check (needed for the transitional deb, whose chain
|
||||
# may land on a different tag than the newest listed one).
|
||||
if [[ "$redirect_url" == */releases/download/* ]]; then
|
||||
release_url="$redirect_url"
|
||||
fi
|
||||
url="$redirect_url"
|
||||
done
|
||||
|
||||
# Fetch the asset and validate its format
|
||||
curl -fsSL -o "/tmp/${ASSET}" "$URL"
|
||||
|
||||
if [[ "$FORMAT" == deb* ]]; then
|
||||
if ! file "/tmp/${ASSET}" | grep -q 'Debian binary package'; then
|
||||
echo "::error::Fetched file is not a valid Debian package"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
sudo apt-get update >/dev/null
|
||||
sudo apt-get install -y rpm >/dev/null
|
||||
if ! rpm -qpi "/tmp/${ASSET}" >/dev/null 2>&1; then
|
||||
echo "::error::Fetched file is not a valid RPM"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Size match against the Releases asset. The transitional deb
|
||||
# rides releases/latest, which skips prereleases and so may
|
||||
# resolve to an older tag than `gh release list` returns —
|
||||
# take the tag from the chain it actually walked instead.
|
||||
release_tag="$TAG"
|
||||
if [[ "$FORMAT" == "deb-transitional" ]]; then
|
||||
release_tag="${release_url#*/releases/download/}"
|
||||
release_tag="${release_tag%%/*}"
|
||||
release_tag="${release_tag//%2B/+}" # GitHub may URL-encode '+'
|
||||
fi
|
||||
|
||||
asset_size=$(gh release view "$release_tag" \
|
||||
--repo aaddrick/claude-desktop-debian \
|
||||
--json assets \
|
||||
--jq ".assets[] | select(.name == \"${ASSET}\") | .size")
|
||||
local_size=$(stat -c %s "/tmp/${ASSET}")
|
||||
if [[ "$asset_size" != "$local_size" ]]; then
|
||||
echo "::error::Size mismatch: local ${local_size} vs Releases ${asset_size}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Heartbeat passed: chain validated, file matches Releases asset."
|
||||
|
||||
- name: Open or update failure issue
|
||||
if: failure() && steps.gate.outputs.live == 'true'
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
FORMAT: ${{ matrix.format }}
|
||||
with:
|
||||
script: |
|
||||
const fmt = process.env.FORMAT;
|
||||
const label = `heartbeat-failure-${fmt}`;
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const body = `Heartbeat failed for \`${fmt}\` at ${new Date().toISOString()}.\nRun: ${runUrl}`;
|
||||
const { data: open } = await github.rest.issues.listForRepo({
|
||||
...context.repo,
|
||||
labels: label,
|
||||
state: 'open',
|
||||
});
|
||||
if (open.length === 0) {
|
||||
await github.rest.issues.create({
|
||||
...context.repo,
|
||||
title: `APT/DNF repo heartbeat failing (${fmt})`,
|
||||
body,
|
||||
labels: [label],
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: open[0].number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Auto-close failure issue on recovery
|
||||
if: success() && steps.gate.outputs.live == 'true'
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
FORMAT: ${{ matrix.format }}
|
||||
with:
|
||||
script: |
|
||||
const fmt = process.env.FORMAT;
|
||||
const label = `heartbeat-failure-${fmt}`;
|
||||
const { data: open } = await github.rest.issues.listForRepo({
|
||||
...context.repo,
|
||||
labels: label,
|
||||
state: 'open',
|
||||
});
|
||||
for (const issue of open) {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: issue.number,
|
||||
body: `Heartbeat for \`${fmt}\` recovered at ${new Date().toISOString()}; auto-closing.`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
...context.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build Package (Reusable)
|
||||
|
||||
# Single cross-building reusable workflow. Repackaging Anthropic's
|
||||
# prebuilt official .deb is arch-independent (no compilation), so both
|
||||
# amd64 and arm64 build on ubuntu-latest; build.sh fetches the official
|
||||
# .deb for `--arch` and re-emits it in the requested format.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
arch:
|
||||
description: "Target architecture (amd64 or arm64)"
|
||||
required: true
|
||||
type: string
|
||||
build_flags:
|
||||
description: 'Flags to pass to build.sh (e.g., "--build appimage --clean no")'
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
artifact_suffix:
|
||||
description: "Suffix for the artifact name (e.g., deb, appimage, rpm)"
|
||||
required: true
|
||||
type: string
|
||||
release_tag:
|
||||
description: "Optional release tag (e.g., v1.3.2+claude1.1.799) to derive wrapper version"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ inputs.artifact_suffix == 'rpm' && 'fedora:42' || '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Install dependencies (Fedora)
|
||||
if: inputs.artifact_suffix == 'rpm'
|
||||
run: |
|
||||
dnf install -y git findutils
|
||||
|
||||
- name: Install FUSE for AppImageTool (Ubuntu)
|
||||
if: inputs.artifact_suffix != 'rpm'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
|
||||
- name: Make build script executable
|
||||
run: chmod +x ./build.sh
|
||||
|
||||
- name: Run build script
|
||||
run: |
|
||||
tag_flag=()
|
||||
if [[ -n "${{ inputs.release_tag }}" ]]; then
|
||||
tag_flag=(--release-tag "${{ inputs.release_tag }}")
|
||||
fi
|
||||
# build_flags is intentionally unquoted so its space-separated
|
||||
# flags word-split into separate argv entries.
|
||||
./build.sh ${{ inputs.build_flags }} --arch ${{ inputs.arch }} "${tag_flag[@]}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
# Our artifacts carry the claude-desktop-unofficial name. The
|
||||
# amd64 deb leg additionally emits the transitional dummy
|
||||
# claude-desktop_<ver>_all.deb (Depends: claude-desktop-unofficial)
|
||||
# that migrates legacy apt installs — the second glob picks it up.
|
||||
with:
|
||||
name: package-${{ inputs.arch }}-${{ inputs.artifact_suffix }}
|
||||
path: |
|
||||
claude-desktop-unofficial_*.deb
|
||||
claude-desktop_*_all.deb
|
||||
claude-desktop-unofficial-*.rpm
|
||||
claude-desktop-unofficial-*.AppImage
|
||||
claude-desktop-unofficial-*.AppImage.zsync
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,213 @@
|
||||
name: Check Claude Desktop Version
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: main-branch-auto-update
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Resolve newest official versions
|
||||
id: resolve
|
||||
run: |
|
||||
source scripts/_common.sh
|
||||
source scripts/setup/official-deb.sh
|
||||
|
||||
# amd64 is authoritative — a failure here fails the job.
|
||||
if ! resolve_official_deb amd64; then
|
||||
echo "::error::Failed to resolve amd64 official package"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "amd64_version=$resolved_official_version"
|
||||
echo "amd64_filename=$resolved_official_filename"
|
||||
echo "amd64_sha256=$resolved_official_sha256"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# arm64 may lag the pool; a failure just leaves arm64_version
|
||||
# empty, which the cross-arch gate treats as "not yet published".
|
||||
if resolve_official_deb arm64; then
|
||||
{
|
||||
echo "arm64_version=$resolved_official_version"
|
||||
echo "arm64_filename=$resolved_official_filename"
|
||||
echo "arm64_sha256=$resolved_official_sha256"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Failed to resolve arm64 official package"
|
||||
fi
|
||||
|
||||
- name: Check if update needed
|
||||
id: check_update
|
||||
run: |
|
||||
AMD64_VERSION="${{ steps.resolve.outputs.amd64_version }}"
|
||||
ARM64_VERSION="${{ steps.resolve.outputs.arm64_version }}"
|
||||
CURRENT_PIN=$(grep -oP "^OFFICIAL_DEB_VERSION='\K[^']+" \
|
||||
scripts/setup/official-deb.sh)
|
||||
STORED_CLAUDE_VERSION="${{ vars.CLAUDE_DESKTOP_VERSION }}"
|
||||
REPO_VERSION="${{ vars.REPO_VERSION }}"
|
||||
|
||||
echo "Resolved amd64 version: $AMD64_VERSION"
|
||||
echo "Resolved arm64 version: $ARM64_VERSION"
|
||||
echo "Current pin (OFFICIAL_DEB_VERSION): $CURRENT_PIN"
|
||||
echo "Stored CLAUDE_DESKTOP_VERSION: $STORED_CLAUDE_VERSION"
|
||||
echo "Repository version: $REPO_VERSION"
|
||||
|
||||
# Cross-arch agreement gate: the official pool occasionally
|
||||
# publishes one arch ahead of the other. Only bump when both
|
||||
# arches expose the same version, so a half-published release
|
||||
# never pins mismatched binaries.
|
||||
if [[ "$AMD64_VERSION" != "$ARM64_VERSION" ]]; then
|
||||
echo "pool lag — arches disagree" \
|
||||
"(amd64='$AMD64_VERSION' arm64='$ARM64_VERSION');" \
|
||||
"skipping until both publish"
|
||||
echo "update_needed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VER="$AMD64_VERSION"
|
||||
UPDATE_NEEDED=false
|
||||
|
||||
if [[ "$VER" != "$CURRENT_PIN" ]]; then
|
||||
echo "Version differs from pin ($CURRENT_PIN -> $VER)"
|
||||
UPDATE_NEEDED=true
|
||||
fi
|
||||
if [[ "$VER" != "$STORED_CLAUDE_VERSION" ]]; then
|
||||
echo "Version differs from CLAUDE_DESKTOP_VERSION"
|
||||
UPDATE_NEEDED=true
|
||||
fi
|
||||
|
||||
if [[ "$UPDATE_NEEDED" == "true" ]]; then
|
||||
NEW_TAG="v${REPO_VERSION}+claude${VER}"
|
||||
{
|
||||
echo "update_needed=true"
|
||||
echo "claude_version=$VER"
|
||||
echo "new_tag=$NEW_TAG"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Update needed! New tag will be: $NEW_TAG"
|
||||
else
|
||||
echo "No updates needed"
|
||||
echo "update_needed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Update official-deb.sh pins
|
||||
if: steps.check_update.outputs.update_needed == 'true'
|
||||
run: |
|
||||
VER="${{ steps.check_update.outputs.claude_version }}"
|
||||
AMD64_FILENAME="${{ steps.resolve.outputs.amd64_filename }}"
|
||||
AMD64_SHA256="${{ steps.resolve.outputs.amd64_sha256 }}"
|
||||
ARM64_FILENAME="${{ steps.resolve.outputs.arm64_filename }}"
|
||||
ARM64_SHA256="${{ steps.resolve.outputs.arm64_sha256 }}"
|
||||
|
||||
f="scripts/setup/official-deb.sh"
|
||||
|
||||
# The resolver's Filename: field is already pool-relative, so it
|
||||
# drops straight into the OFFICIAL_DEB_POOL_* pins. Each sed
|
||||
# anchors on ^NAME= so the five pin lines rewrite unambiguously.
|
||||
# '|' delimiter clears the '/' in the pool paths.
|
||||
sed -i "s|^OFFICIAL_DEB_VERSION=.*|OFFICIAL_DEB_VERSION='$VER'|" "$f"
|
||||
sed -i "s|^OFFICIAL_DEB_POOL_AMD64=.*|OFFICIAL_DEB_POOL_AMD64='$AMD64_FILENAME'|" "$f"
|
||||
sed -i "s|^OFFICIAL_DEB_SHA256_AMD64=.*|OFFICIAL_DEB_SHA256_AMD64='$AMD64_SHA256'|" "$f"
|
||||
sed -i "s|^OFFICIAL_DEB_POOL_ARM64=.*|OFFICIAL_DEB_POOL_ARM64='$ARM64_FILENAME'|" "$f"
|
||||
sed -i "s|^OFFICIAL_DEB_SHA256_ARM64=.*|OFFICIAL_DEB_SHA256_ARM64='$ARM64_SHA256'|" "$f"
|
||||
|
||||
echo "Updated pins in $f:"
|
||||
grep -E "^OFFICIAL_DEB_(VERSION|POOL|SHA256)" "$f"
|
||||
|
||||
- name: Update Nix SRI hashes
|
||||
if: steps.check_update.outputs.update_needed == 'true'
|
||||
run: |
|
||||
NIX_FILE="nix/claude-desktop.nix"
|
||||
|
||||
# The Nix derivation is a `throw` stub until @typedrat lands the
|
||||
# official-deb rework. Skip cleanly until it exposes a real
|
||||
# `version = "..."` line to anchor on.
|
||||
if ! grep -q 'version = ' "$NIX_FILE"; then
|
||||
echo "nix derivation is still a stub; skipping SRI update"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VER="${{ steps.check_update.outputs.claude_version }}"
|
||||
AMD64_SHA256="${{ steps.resolve.outputs.amd64_sha256 }}"
|
||||
ARM64_SHA256="${{ steps.resolve.outputs.arm64_sha256 }}"
|
||||
|
||||
# The APT Packages index SHA-256 is authoritative — no download.
|
||||
# Convert the hex digest to the SRI (base64) form Nix expects.
|
||||
amd64_sri="sha256-$(echo "$AMD64_SHA256" | xxd -r -p | base64 -w0)"
|
||||
arm64_sri="sha256-$(echo "$ARM64_SHA256" | xxd -r -p | base64 -w0)"
|
||||
|
||||
sed -i "s|version = \"[^\"]*\"|version = \"$VER\"|" "$NIX_FILE"
|
||||
|
||||
# Expected per-arch block shape once the derivation lands:
|
||||
# x86_64-linux = { url = "..."; hash = "sha256-..."; };
|
||||
# aarch64-linux = { url = "..."; hash = "sha256-..."; };
|
||||
# Each arch block holds exactly one `hash = "..."` before its
|
||||
# closing `};`, so the range sed rewrites only that arch's hash.
|
||||
sed -i "/x86_64-linux/,/};/{s|hash = \"[^\"]*\"|hash = \"$amd64_sri\"|}" "$NIX_FILE"
|
||||
sed -i "/aarch64-linux/,/};/{s|hash = \"[^\"]*\"|hash = \"$arm64_sri\"|}" "$NIX_FILE"
|
||||
|
||||
echo "Updated $NIX_FILE (version + per-arch SRI hashes)"
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check_update.outputs.update_needed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
VER="${{ steps.check_update.outputs.claude_version }}"
|
||||
NEW_TAG="${{ steps.check_update.outputs.new_tag }}"
|
||||
|
||||
# Check if we have a PAT
|
||||
if [ -z "$GH_TOKEN" ]; then
|
||||
echo "Error: GH_PAT secret is not configured"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --quiet scripts/setup/official-deb.sh nix/claude-desktop.nix; then
|
||||
echo "No changes to scripts/setup/official-deb.sh or nix/claude-desktop.nix"
|
||||
else
|
||||
git add scripts/setup/official-deb.sh nix/claude-desktop.nix
|
||||
git commit -m "$(cat <<COMMIT_MSG
|
||||
Update Claude Desktop to version $VER
|
||||
|
||||
Bumped the official .deb pins (version, pool paths, SHA-256) and
|
||||
the Nix SRI hashes to the newest release in Anthropic's official
|
||||
APT pool.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
COMMIT_MSG
|
||||
)"
|
||||
git push
|
||||
echo "Changes committed and pushed"
|
||||
fi
|
||||
|
||||
# Update the version variable
|
||||
gh variable set CLAUDE_DESKTOP_VERSION --body "$VER"
|
||||
|
||||
echo "Creating and pushing new tag: $NEW_TAG"
|
||||
|
||||
# Create annotated tag
|
||||
git tag -a "$NEW_TAG" -m "Update to Claude Desktop $VER"
|
||||
|
||||
# Push the tag
|
||||
git push origin "$NEW_TAG"
|
||||
|
||||
echo "Successfully created tag $NEW_TAG"
|
||||
@@ -0,0 +1,989 @@
|
||||
name: CI
|
||||
run-name: |
|
||||
CI: ${{
|
||||
github.event_name == 'pull_request' && format('PR #{0} by @{1} - {2}', github.event.pull_request.number, github.actor, github.event.pull_request.title) ||
|
||||
github.event_name == 'push' && github.event.head_commit && format('Push by @{0} - {1}', github.actor, github.event.head_commit.message) ||
|
||||
format('{0} triggered by @{1}', github.event_name, github.actor)
|
||||
}}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.sh"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/**"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
test-flags:
|
||||
name: Test Flags Parsing
|
||||
uses: ./.github/workflows/test-flags.yml
|
||||
|
||||
build:
|
||||
name: Build Packages (${{ matrix.arch }} - ${{ matrix.artifact_suffix }})
|
||||
needs: test-flags
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
flags: "--build deb"
|
||||
artifact_suffix: "deb"
|
||||
- arch: amd64
|
||||
flags: "--build rpm"
|
||||
artifact_suffix: "rpm"
|
||||
- arch: amd64
|
||||
flags: "--build appimage --clean no"
|
||||
artifact_suffix: "appimage"
|
||||
- arch: arm64
|
||||
flags: "--build deb --clean no"
|
||||
artifact_suffix: "deb"
|
||||
- arch: arm64
|
||||
flags: "--build rpm"
|
||||
artifact_suffix: "rpm"
|
||||
- arch: arm64
|
||||
flags: "--build appimage"
|
||||
artifact_suffix: "appimage"
|
||||
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build_flags: ${{ matrix.flags }}
|
||||
artifact_suffix: ${{ matrix.artifact_suffix }}
|
||||
release_tag: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
|
||||
|
||||
test-artifacts:
|
||||
name: Test Build Artifacts (amd64)
|
||||
needs: [build]
|
||||
uses: ./.github/workflows/test-artifacts.yml
|
||||
with:
|
||||
arch: amd64
|
||||
|
||||
test-artifacts-arm64:
|
||||
name: Test Build Artifacts (arm64)
|
||||
needs: [build]
|
||||
uses: ./.github/workflows/test-artifacts.yml
|
||||
with:
|
||||
arch: arm64
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [test-flags, build, test-artifacts, test-artifacts-arm64]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download AMD64 deb artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-deb
|
||||
path: artifacts/
|
||||
|
||||
- name: Download AMD64 rpm artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-rpm
|
||||
path: artifacts/
|
||||
|
||||
- name: Download AMD64 AppImage artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-appimage
|
||||
path: artifacts/
|
||||
|
||||
- name: Download ARM64 deb artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-arm64-deb
|
||||
path: artifacts/
|
||||
|
||||
- name: Download ARM64 rpm artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-arm64-rpm
|
||||
path: artifacts/
|
||||
|
||||
- name: Download ARM64 AppImage artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-arm64-appimage
|
||||
path: artifacts/
|
||||
|
||||
# --- Release notes generation (inline, with fallback) ---
|
||||
|
||||
- name: Checkout claude-desktop-versions
|
||||
id: checkout_versions
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: aaddrick/claude-desktop-versions
|
||||
path: versions
|
||||
|
||||
- name: Set up Python 3.12
|
||||
if: steps.checkout_versions.outcome == 'success'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Node.js 20
|
||||
if: steps.checkout_versions.outcome == 'success'
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install difftastic
|
||||
if: steps.checkout_versions.outcome == 'success'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -fsSL https://github.com/Wilfred/difftastic/releases/latest/download/difft-x86_64-unknown-linux-gnu.tar.gz \
|
||||
| sudo tar xz -C /usr/local/bin
|
||||
|
||||
- name: Install Node.js tools
|
||||
if: steps.checkout_versions.outcome == 'success'
|
||||
continue-on-error: true
|
||||
run: npm install -g @anthropic-ai/claude-code @electron/asar prettier
|
||||
|
||||
- name: Checkout repo for git history
|
||||
id: checkout_repo
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: repo
|
||||
|
||||
- name: Find previous release tags
|
||||
id: prev
|
||||
if: steps.checkout_repo.outcome == 'success'
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
current="${GITHUB_REF_NAME}"
|
||||
current_cv="${current#*+claude}"
|
||||
|
||||
upstream_tag=""
|
||||
any_tag=""
|
||||
while IFS= read -r tag; do
|
||||
# Skip the current tag
|
||||
[[ "$tag" == "$current" ]] && continue
|
||||
# Track the first previous tag (any version)
|
||||
if [[ -z "$any_tag" ]]; then
|
||||
any_tag="$tag"
|
||||
fi
|
||||
# Track the first tag with a different Claude version
|
||||
tag_cv="${tag#*+claude}"
|
||||
if [[ -z "$upstream_tag" && "$tag_cv" != "$current_cv" ]]; then
|
||||
upstream_tag="$tag"
|
||||
fi
|
||||
# Stop once we have both
|
||||
[[ -n "$any_tag" && -n "$upstream_tag" ]] && break
|
||||
done < <(gh release list --limit 50 -R "$GITHUB_REPOSITORY" --json tagName -q '.[].tagName')
|
||||
|
||||
if [[ -n "$any_tag" ]]; then
|
||||
any_cv="${any_tag#*+claude}"
|
||||
if [[ "$any_cv" != "$current_cv" && -n "$upstream_tag" ]]; then
|
||||
echo "type=upstream" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$upstream_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "Upstream change: $upstream_tag -> $current"
|
||||
else
|
||||
echo "type=wrapper" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$any_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "Wrapper-only update: $any_tag -> $current"
|
||||
fi
|
||||
else
|
||||
echo "type=none" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::No previous release found"
|
||||
fi
|
||||
|
||||
- name: Run compare-releases (upstream change)
|
||||
if: false # disabled — release notes are managed manually
|
||||
# was: steps.prev.outcome == 'success' && steps.prev.outputs.type == 'upstream'
|
||||
timeout-minutes: 180
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
appimage=$(find artifacts/ -name '*amd64*.AppImage' ! -name '*.zsync' | head -1)
|
||||
python versions/scripts/compare-releases.py \
|
||||
--old "${{ steps.prev.outputs.tag }}" \
|
||||
--new "${GITHUB_REF_NAME}" \
|
||||
--new-appimage "$appimage" \
|
||||
--model sonnet \
|
||||
--voice-profile-url "https://raw.githubusercontent.com/aaddrick/written-voice-replication/master/.claude/agents/aaddrick-voice.md" \
|
||||
--workdir compare-work
|
||||
|
||||
- name: Append wrapper commits to upstream notes
|
||||
if: steps.prev.outcome == 'success' && steps.prev.outputs.type == 'upstream'
|
||||
continue-on-error: true
|
||||
working-directory: repo
|
||||
run: |
|
||||
prev_tag="${{ steps.prev.outputs.tag }}"
|
||||
current="${GITHUB_REF_NAME}"
|
||||
commits=$(git log "${prev_tag}..${current}" --pretty=format:"- %s (%h)" --no-merges)
|
||||
|
||||
if [[ -n "$commits" && -f ../compare-work/summary.md ]]; then
|
||||
{
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Wrapper/Packaging Changes"
|
||||
echo ""
|
||||
echo "The following commits were made to the build wrapper and packaging between ${prev_tag} and ${current}:"
|
||||
echo ""
|
||||
echo "$commits"
|
||||
} >> ../compare-work/summary.md
|
||||
fi
|
||||
|
||||
- name: Generate commit-based notes (wrapper update)
|
||||
if: steps.prev.outcome == 'success' && steps.prev.outputs.type == 'wrapper'
|
||||
continue-on-error: true
|
||||
working-directory: repo
|
||||
run: |
|
||||
prev_tag="${{ steps.prev.outputs.tag }}"
|
||||
current="${GITHUB_REF_NAME}"
|
||||
|
||||
mkdir -p ../compare-work
|
||||
{
|
||||
echo "# Wrapper Update: ${current}"
|
||||
echo ""
|
||||
echo "This release updates the wrapper/packaging only — the upstream Claude Desktop version is unchanged."
|
||||
echo ""
|
||||
echo "## Changes since ${prev_tag}"
|
||||
echo ""
|
||||
git log "${prev_tag}..${current}" --pretty=format:"- %s (%h)" --no-merges
|
||||
echo ""
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Installation"
|
||||
echo ""
|
||||
echo "### APT (Debian/Ubuntu)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "# First time? Add the repo:"
|
||||
echo "curl -fsSL https://pkg.claude-desktop-debian.dev/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/claude-desktop-unofficial.gpg"
|
||||
echo 'echo "deb [signed-by=/usr/share/keyrings/claude-desktop-unofficial.gpg arch=amd64,arm64] https://pkg.claude-desktop-debian.dev stable main" | sudo tee /etc/apt/sources.list.d/claude-desktop-unofficial.list'
|
||||
echo ""
|
||||
echo "# Install or update:"
|
||||
echo "sudo apt update && sudo apt install claude-desktop-unofficial"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### DNF (Fedora/RHEL)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "# First time? Add the repo:"
|
||||
echo "sudo curl -fsSL https://pkg.claude-desktop-debian.dev/rpm/claude-desktop-unofficial.repo -o /etc/yum.repos.d/claude-desktop-unofficial.repo"
|
||||
echo ""
|
||||
echo "# Install or update:"
|
||||
echo "sudo dnf install claude-desktop-unofficial"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### AUR (Arch Linux)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "yay -S claude-desktop-appimage"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### Manual Download"
|
||||
echo ""
|
||||
echo "Download \`.deb\`, \`.rpm\`, or \`.AppImage\` from the assets below."
|
||||
} > ../compare-work/summary.md
|
||||
|
||||
- name: Generate fallback release notes
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
# Only generate fallback if AI-generated notes don't exist
|
||||
if [[ -f compare-work/summary.md ]]; then
|
||||
echo "AI-generated release notes found, skipping fallback"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract Claude version from tag
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
claude_version="${tag#*+claude}"
|
||||
|
||||
mkdir -p compare-work
|
||||
{
|
||||
echo "## Claude Desktop Update"
|
||||
echo ""
|
||||
echo "This release updates the packaged Claude Desktop version to **${claude_version}**."
|
||||
echo ""
|
||||
echo "### What's Changed"
|
||||
echo "- Updated Claude Desktop to version ${claude_version}"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Installation"
|
||||
echo ""
|
||||
echo "### APT (Debian/Ubuntu)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "# First time? Add the repo:"
|
||||
echo "curl -fsSL https://pkg.claude-desktop-debian.dev/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/claude-desktop-unofficial.gpg"
|
||||
echo 'echo "deb [signed-by=/usr/share/keyrings/claude-desktop-unofficial.gpg arch=amd64,arm64] https://pkg.claude-desktop-debian.dev stable main" | sudo tee /etc/apt/sources.list.d/claude-desktop-unofficial.list'
|
||||
echo ""
|
||||
echo "# Install or update:"
|
||||
echo "sudo apt update && sudo apt install claude-desktop-unofficial"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### DNF (Fedora/RHEL)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "# First time? Add the repo:"
|
||||
echo "sudo curl -fsSL https://pkg.claude-desktop-debian.dev/rpm/claude-desktop-unofficial.repo -o /etc/yum.repos.d/claude-desktop-unofficial.repo"
|
||||
echo ""
|
||||
echo "# Install or update:"
|
||||
echo "sudo dnf install claude-desktop-unofficial"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### AUR (Arch Linux)"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "yay -S claude-desktop-appimage"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### Manual Download"
|
||||
echo ""
|
||||
echo "Download \`.deb\`, \`.rpm\`, or \`.AppImage\` from the assets below."
|
||||
} > compare-work/summary.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: ${{ always() }}
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
with:
|
||||
files: artifacts/**/*
|
||||
body_path: compare-work/summary.md
|
||||
# RC tags (e.g. v3.0.0-rc1+claude...) publish as a GitHub
|
||||
# prerelease — the opt-in beta channel — while final tags
|
||||
# publish as a full release.
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
|
||||
- name: Generate and upload reference source
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
appimage=$(find artifacts/ -name '*amd64*.AppImage' ! -name '*.zsync' | head -1)
|
||||
if [[ -z "$appimage" ]]; then
|
||||
echo "::warning::No AppImage found, skipping reference-source"
|
||||
exit 0
|
||||
fi
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract >/dev/null 2>&1
|
||||
asar_path=$(find squashfs-root -name 'app.asar' -path '*/resources/*' | head -1)
|
||||
if [[ -z "$asar_path" ]]; then
|
||||
echo "::warning::app.asar not found in AppImage"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p reference-source
|
||||
asar extract "$asar_path" reference-source/app-extracted
|
||||
npx prettier --write "reference-source/app-extracted/.vite/build/*.js" 2>/dev/null || true
|
||||
tar -czf reference-source.tar.gz -C reference-source app-extracted
|
||||
gh release upload "${{ github.ref_name }}" reference-source.tar.gz \
|
||||
--repo "$GITHUB_REPOSITORY" --clobber
|
||||
|
||||
mirror-official-deb:
|
||||
name: Mirror Official .deb to Release
|
||||
# Runs for RC tags too — the mirror is version-keyed insurance so the
|
||||
# exact upstream binary a release was built from stays retrievable
|
||||
# from our Releases even if it rotates out of Anthropic's pool.
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Mirror pinned official .deb per architecture
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
source scripts/_common.sh
|
||||
source scripts/setup/official-deb.sh
|
||||
|
||||
for arch in amd64 arm64; do
|
||||
official_deb_pin "$arch" || {
|
||||
echo "::error::Failed to pin official .deb for $arch"
|
||||
exit 1
|
||||
}
|
||||
echo "Mirroring $official_deb_filename ($arch)..."
|
||||
if ! wget -q -O "$official_deb_filename" "$official_deb_url"; then
|
||||
echo "::error::Failed to download $official_deb_url"
|
||||
exit 1
|
||||
fi
|
||||
if ! verify_sha256 "$official_deb_filename" \
|
||||
"$official_deb_sha256" "official $arch .deb"; then
|
||||
exit 1
|
||||
fi
|
||||
# Upload under the original pool filename
|
||||
# (claude-desktop_<ver>_<arch>.deb). It cannot collide with our
|
||||
# own assets, which carry the _<ver>-<repo>_ wrapper suffix.
|
||||
gh release upload "$GITHUB_REF_NAME" "$official_deb_filename" \
|
||||
--clobber
|
||||
done
|
||||
|
||||
update-apt-repo:
|
||||
name: Update APT Repository
|
||||
# RC tags never touch the stable APT repo.
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-rc')
|
||||
needs: [release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
WORKER_DOMAIN: pkg.claude-desktop-debian.dev
|
||||
|
||||
steps:
|
||||
- name: Checkout gh-pages branch
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: apt-repo
|
||||
|
||||
- name: Download AMD64 deb artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-deb
|
||||
path: incoming/
|
||||
|
||||
- name: Download ARM64 deb artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-arm64-deb
|
||||
path: incoming/
|
||||
|
||||
- name: Install reprepro
|
||||
run: sudo apt-get update && sudo apt-get install -y reprepro
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.APT_GPG_PRIVATE_KEY }}
|
||||
|
||||
- name: Publish KEY.gpg with all public keys from keyring
|
||||
# Fix #501: APT InRelease and DNF repomd.xml are signed with
|
||||
# different keys from the same keyring. Export every public key
|
||||
# so strict clients (e.g. rockylinux:9) can verify both.
|
||||
working-directory: apt-repo
|
||||
run: |
|
||||
gpg --armor --export > KEY.gpg
|
||||
echo "Keys published in KEY.gpg:"
|
||||
gpg --show-keys < KEY.gpg
|
||||
|
||||
- name: Add packages to repository
|
||||
working-directory: apt-repo
|
||||
run: |
|
||||
# incoming/ carries our renamed claude-desktop-unofficial .debs
|
||||
# plus the transitional dummy claude-desktop_<ver>_all.deb
|
||||
# (Depends: claude-desktop-unofficial) that auto-migrates legacy
|
||||
# `apt install claude-desktop` users. reprepro derives pool paths
|
||||
# from package names, so they land in
|
||||
# pool/main/c/claude-desktop-unofficial/ and
|
||||
# pool/main/c/claude-desktop/ respectively.
|
||||
|
||||
# Remove existing versions to allow re-uploads and wrapper version bumps
|
||||
# (reprepro rejects new files with same name but different checksums)
|
||||
for deb in ../incoming/*.deb; do
|
||||
pkg_name=$(dpkg-deb -f "$deb" Package)
|
||||
if reprepro list stable "$pkg_name" 2>/dev/null | grep -q .; then
|
||||
echo "Removing existing $pkg_name from repository..."
|
||||
reprepro remove stable "$pkg_name"
|
||||
fi
|
||||
done
|
||||
|
||||
# Add each .deb file to the repository
|
||||
# --section and --priority provide defaults for older packages missing these fields
|
||||
for deb in ../incoming/*.deb; do
|
||||
echo "Adding $deb to repository..."
|
||||
reprepro --section utils --priority optional includedeb stable "$deb"
|
||||
done
|
||||
|
||||
- name: Strip binaries from pool (gated on Worker liveness)
|
||||
working-directory: apt-repo
|
||||
run: |
|
||||
# The Worker on WORKER_DOMAIN serves /pool/.../*.deb requests by
|
||||
# 302-redirecting to GitHub Release assets. When it's live we strip
|
||||
# binaries from the gh-pages tree (the metadata's Filename: field
|
||||
# still references pool paths; the Worker intercepts). The find
|
||||
# covers both pool dirs: our claude-desktop-unofficial debs and
|
||||
# the transitional claude-desktop_*_all.deb under
|
||||
# pool/main/c/claude-desktop/.
|
||||
# When the Worker isn't live (pre-Phase-4a, outage, misconfiguration)
|
||||
# the strip is skipped to avoid serving 404s for binary fetches.
|
||||
probe_url="https://${WORKER_DOMAIN}/dists/stable/InRelease"
|
||||
if curl -fsI --max-time 10 "$probe_url" >/dev/null; then
|
||||
echo "Worker live at ${WORKER_DOMAIN}; stripping binaries from pool"
|
||||
find pool -type f -name '*.deb' -delete
|
||||
else
|
||||
echo "Worker not responding at ${WORKER_DOMAIN}; preserving .debs in pool"
|
||||
echo "(expected before Phase 4a; after that, an error worth investigating)"
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
working-directory: apt-repo
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git diff --staged --quiet || git commit -m "Update APT repository for ${{ github.ref_name }}"
|
||||
# Retry loop to handle concurrent pushes to gh-pages
|
||||
for i in 1 2 3 4 5; do
|
||||
git pull --rebase && git push && break
|
||||
if [[ $i -eq 5 ]]; then
|
||||
echo "::error::Failed to push APT repo after 5 attempts"
|
||||
exit 1
|
||||
fi
|
||||
wait_time=$((2 ** i))
|
||||
echo "Push failed, retrying in ${wait_time}s... (attempt $i/5)"
|
||||
sleep "$wait_time"
|
||||
done
|
||||
|
||||
- name: Smoke test published deb (ordered chain + size)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! curl -fsI --max-time 10 \
|
||||
"https://${WORKER_DOMAIN}/dists/stable/InRelease" >/dev/null; then
|
||||
echo "Worker not live; skipping smoke test (expected before Phase 4a)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse versions from tag (e.g., v2.0.2+claude1.3883.0)
|
||||
repoVer="${TAG#v}"; repoVer="${repoVer%+claude*}"
|
||||
claudeVer="${TAG#*+claude}"
|
||||
deb_name="claude-desktop-unofficial_${claudeVer}-${repoVer}_amd64.deb"
|
||||
# Intentionally starts at the github.io URL: the smoke test
|
||||
# walks the full Pages-301 → Worker-302 → Releases chain to
|
||||
# confirm the legacy redirect path still works for clients
|
||||
# that follow HTTPS→HTTP downgrades (DNF, curl without -L).
|
||||
deb_url="https://aaddrick.github.io/claude-desktop-debian/pool/main/c/claude-desktop-unofficial/${deb_name}"
|
||||
|
||||
# Wait for propagation
|
||||
deadline=$((SECONDS + 300))
|
||||
until curl -fsI --max-time 10 "$deb_url" -o /dev/null; do
|
||||
[[ $SECONDS -gt $deadline ]] \
|
||||
&& { echo "::error::Reachability timeout for ${deb_url}"; exit 1; }
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Walk redirect chain hop-by-hop
|
||||
# Hop 0 is Pages' auto-301 from github.io to pkg.<domain>.
|
||||
# Pages emits http:// in the Location because https_enforced
|
||||
# can't be set (DNS points at Cloudflare, not Pages, so Pages
|
||||
# can't provision its own cert). Cloudflare/Worker answers
|
||||
# both schemes, so http vs https is cosmetic here.
|
||||
expected_hops=(
|
||||
"https?://${WORKER_DOMAIN}/"
|
||||
"https://github\\.com/aaddrick/claude-desktop-debian/releases/download/v${repoVer}\\+claude${claudeVer}/"
|
||||
"https://(objects|release-assets)\\.githubusercontent\\.com/"
|
||||
)
|
||||
url="$deb_url"
|
||||
for i in "${!expected_hops[@]}"; do
|
||||
hop_status=$(curl -s -o /dev/null -w '%{http_code}' "$url")
|
||||
redirect_url=$(curl -s -o /dev/null -w '%{redirect_url}' "$url")
|
||||
echo "Hop ${i}: ${hop_status} ${url} -> ${redirect_url}"
|
||||
[[ "$hop_status" =~ ^30[12]$ ]] \
|
||||
|| { echo "::error::Hop ${i} expected 301/302, got ${hop_status}"; exit 1; }
|
||||
[[ "$redirect_url" =~ ^${expected_hops[$i]} ]] \
|
||||
|| { echo "::error::Hop ${i} mismatch: expected ${expected_hops[$i]}, got ${redirect_url}"; exit 1; }
|
||||
url="$redirect_url"
|
||||
done
|
||||
|
||||
# Fetch and validate
|
||||
curl -fsSL -o /tmp/smoke.deb "$deb_url"
|
||||
file /tmp/smoke.deb | grep -q 'Debian binary package' \
|
||||
|| { echo "::error::Not a valid Debian package"; exit 1; }
|
||||
|
||||
# Size match against the Releases asset
|
||||
asset_size=$(gh release view "$TAG" \
|
||||
--repo aaddrick/claude-desktop-debian \
|
||||
--json assets \
|
||||
--jq ".assets[] | select(.name == \"${deb_name}\") | .size")
|
||||
local_size=$(stat -c %s /tmp/smoke.deb)
|
||||
[[ "$asset_size" == "$local_size" ]] \
|
||||
|| { echo "::error::Size mismatch: ${local_size} vs ${asset_size}"; exit 1; }
|
||||
|
||||
echo "APT smoke test passed: chain validated, file matches Releases asset"
|
||||
|
||||
update-dnf-repo:
|
||||
name: Update DNF Repository
|
||||
# RC tags never touch the stable DNF repo.
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-rc')
|
||||
needs: [release, update-apt-repo]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
WORKER_DOMAIN: pkg.claude-desktop-debian.dev
|
||||
|
||||
steps:
|
||||
- name: Checkout gh-pages branch
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: dnf-repo
|
||||
|
||||
- name: Download AMD64 rpm artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-rpm
|
||||
path: incoming/
|
||||
|
||||
- name: Download ARM64 rpm artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-arm64-rpm
|
||||
path: incoming/
|
||||
|
||||
- name: Install createrepo_c and rpm-sign
|
||||
run: sudo apt-get update && sudo apt-get install -y createrepo-c rpm
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.APT_GPG_PRIVATE_KEY }}
|
||||
|
||||
- name: Configure RPM signing
|
||||
run: |
|
||||
# Configure RPM macros for signing
|
||||
cat > ~/.rpmmacros << EOF
|
||||
%_signature gpg
|
||||
%_gpg_name ${{ steps.import_gpg.outputs.keyid }}
|
||||
%__gpg /usr/bin/gpg
|
||||
%__gpg_sign_cmd %{__gpg} gpg --batch --no-verbose --no-armor --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} %{__plaintext_filename}
|
||||
EOF
|
||||
|
||||
- name: Update DNF repository
|
||||
working-directory: dnf-repo
|
||||
run: |
|
||||
# Create directory structure
|
||||
mkdir -p rpm/x86_64 rpm/aarch64
|
||||
|
||||
# Remove old RPMs to prevent accumulation across releases
|
||||
rm -f rpm/x86_64/*.rpm rpm/aarch64/*.rpm
|
||||
|
||||
# Copy RPMs to appropriate architecture directories
|
||||
for rpm_file in ../incoming/*.rpm; do
|
||||
filename=$(basename "$rpm_file")
|
||||
if [[ "$filename" == *"x86_64"* ]]; then
|
||||
cp "$rpm_file" rpm/x86_64/
|
||||
echo "Added $filename to x86_64"
|
||||
elif [[ "$filename" == *"aarch64"* ]]; then
|
||||
cp "$rpm_file" rpm/aarch64/
|
||||
echo "Added $filename to aarch64"
|
||||
fi
|
||||
done
|
||||
|
||||
# Sign RPM packages and generate repository metadata for each architecture
|
||||
for arch in x86_64 aarch64; do
|
||||
if ls "rpm/$arch/"*.rpm 1> /dev/null 2>&1; then
|
||||
# Sign each RPM package
|
||||
echo "Signing RPM packages for $arch..."
|
||||
for rpm_file in "rpm/$arch/"*.rpm; do
|
||||
echo "Signing $rpm_file..."
|
||||
rpmsign --addsign "$rpm_file"
|
||||
done
|
||||
|
||||
echo "Generating repodata for $arch..."
|
||||
createrepo_c --update "rpm/$arch/"
|
||||
|
||||
# Sign repodata. Trailing '!' on keyid forces gpg to use
|
||||
# the primary key; without it gpg picks the most recent
|
||||
# signing subkey, and rpm 4.20+ / zypper reject repomd.xml
|
||||
# signed by anything other than the primary key.
|
||||
# Regression of #213 — PR #217 added --default-key but
|
||||
# dropped the '!'. Do not strip it. --yes overwrites .asc.
|
||||
echo "Signing repodata for $arch..."
|
||||
gpg --batch --yes --default-key "${{ steps.import_gpg.outputs.keyid }}!" --detach-sign --armor "rpm/$arch/repodata/repomd.xml"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create .repo file for users (reuses existing KEY.gpg)
|
||||
# shellcheck disable=SC2016 # $basearch is a DNF variable, not a shell variable
|
||||
printf '%s\n' \
|
||||
'[claude-desktop-unofficial]' \
|
||||
'name=Claude Desktop (unofficial packaging) for Fedora/RHEL' \
|
||||
'baseurl=https://pkg.claude-desktop-debian.dev/rpm/$basearch' \
|
||||
'enabled=1' \
|
||||
'gpgcheck=1' \
|
||||
'repo_gpgcheck=1' \
|
||||
'gpgkey=https://pkg.claude-desktop-debian.dev/KEY.gpg' \
|
||||
'metadata_expire=1h' \
|
||||
> rpm/claude-desktop-unofficial.repo
|
||||
|
||||
# Legacy path: rpm/claude-desktop.repo predates the
|
||||
# claude-desktop-unofficial rename and is baked into old install
|
||||
# instructions and bookmarks. Keep publishing it with the same
|
||||
# baseurl/key so those URLs keep working. It retains the original
|
||||
# [claude-desktop] section id — existing installs already carry
|
||||
# that id, and reusing [claude-desktop-unofficial] here would
|
||||
# create a duplicate repo id for anyone holding both files.
|
||||
# shellcheck disable=SC2016 # $basearch is a DNF variable, not a shell variable
|
||||
printf '%s\n' \
|
||||
'[claude-desktop]' \
|
||||
'name=Claude Desktop for Fedora/RHEL' \
|
||||
'baseurl=https://pkg.claude-desktop-debian.dev/rpm/$basearch' \
|
||||
'enabled=1' \
|
||||
'gpgcheck=1' \
|
||||
'repo_gpgcheck=1' \
|
||||
'gpgkey=https://pkg.claude-desktop-debian.dev/KEY.gpg' \
|
||||
'metadata_expire=1h' \
|
||||
> rpm/claude-desktop.repo
|
||||
|
||||
- name: Re-upload signed RPMs to GitHub Release
|
||||
# Fix #500: rpmsign --addsign mutates the RPM in place. The release
|
||||
# job (needs: release) already uploaded the unsigned build artifact.
|
||||
# Clobber it with the signed copy so the sha256 in repodata matches
|
||||
# the binary the Worker redirects to.
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
working-directory: dnf-repo
|
||||
run: |
|
||||
for arch in x86_64 aarch64; do
|
||||
if ls "rpm/$arch/"*.rpm 1> /dev/null 2>&1; then
|
||||
gh release upload "${{ github.ref_name }}" \
|
||||
"rpm/$arch/"*.rpm \
|
||||
--repo aaddrick/claude-desktop-debian \
|
||||
--clobber
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Strip RPMs from pool (gated on Worker liveness)
|
||||
working-directory: dnf-repo
|
||||
run: |
|
||||
# Mirror of the APT-side strip. Repodata (signed) stays; the .rpm
|
||||
# binaries themselves are deleted because the Worker 302-redirects
|
||||
# /rpm/<arch>/*.rpm requests to GitHub Release assets.
|
||||
probe_url="https://${WORKER_DOMAIN}/dists/stable/InRelease"
|
||||
if curl -fsI --max-time 10 "$probe_url" >/dev/null; then
|
||||
echo "Worker live; stripping RPMs from pool (repodata + signatures retained)"
|
||||
find rpm -type f -name '*.rpm' -delete
|
||||
else
|
||||
echo "Worker not responding; preserving .rpms in pool"
|
||||
echo "(expected before Phase 4a; after that, an error worth investigating)"
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
working-directory: dnf-repo
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git diff --staged --quiet || git commit -m "Update DNF repository for ${{ github.ref_name }}"
|
||||
# Retry loop to handle concurrent pushes to gh-pages
|
||||
for i in 1 2 3 4 5; do
|
||||
git pull --rebase && git push && break
|
||||
if [[ $i -eq 5 ]]; then
|
||||
echo "::error::Failed to push DNF repo after 5 attempts"
|
||||
exit 1
|
||||
fi
|
||||
wait_time=$((2 ** i))
|
||||
echo "Push failed, retrying in ${wait_time}s... (attempt $i/5)"
|
||||
sleep "$wait_time"
|
||||
done
|
||||
|
||||
- name: Smoke test published rpm (ordered chain + size)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! curl -fsI --max-time 10 \
|
||||
"https://${WORKER_DOMAIN}/dists/stable/InRelease" >/dev/null; then
|
||||
echo "Worker not live; skipping smoke test (expected before Phase 4a)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
repoVer="${TAG#v}"; repoVer="${repoVer%+claude*}"
|
||||
claudeVer="${TAG#*+claude}"
|
||||
rpm_name="claude-desktop-unofficial-${claudeVer}-${repoVer}-1.x86_64.rpm"
|
||||
# Intentionally starts at the github.io URL — see APT smoke
|
||||
# test comment above for why.
|
||||
rpm_url="https://aaddrick.github.io/claude-desktop-debian/rpm/x86_64/${rpm_name}"
|
||||
|
||||
deadline=$((SECONDS + 300))
|
||||
until curl -fsI --max-time 10 "$rpm_url" -o /dev/null; do
|
||||
[[ $SECONDS -gt $deadline ]] \
|
||||
&& { echo "::error::Reachability timeout for ${rpm_url}"; exit 1; }
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Hop 0 is Pages' auto-301 from github.io to pkg.<domain>.
|
||||
# Pages emits http:// in the Location because https_enforced
|
||||
# can't be set (DNS points at Cloudflare, not Pages, so Pages
|
||||
# can't provision its own cert). Cloudflare/Worker answers
|
||||
# both schemes, so http vs https is cosmetic here.
|
||||
expected_hops=(
|
||||
"https?://${WORKER_DOMAIN}/"
|
||||
"https://github\\.com/aaddrick/claude-desktop-debian/releases/download/v${repoVer}\\+claude${claudeVer}/"
|
||||
"https://(objects|release-assets)\\.githubusercontent\\.com/"
|
||||
)
|
||||
url="$rpm_url"
|
||||
for i in "${!expected_hops[@]}"; do
|
||||
hop_status=$(curl -s -o /dev/null -w '%{http_code}' "$url")
|
||||
redirect_url=$(curl -s -o /dev/null -w '%{redirect_url}' "$url")
|
||||
echo "Hop ${i}: ${hop_status} ${url} -> ${redirect_url}"
|
||||
[[ "$hop_status" =~ ^30[12]$ ]] \
|
||||
|| { echo "::error::Hop ${i} expected 301/302, got ${hop_status}"; exit 1; }
|
||||
[[ "$redirect_url" =~ ^${expected_hops[$i]} ]] \
|
||||
|| { echo "::error::Hop ${i} mismatch: expected ${expected_hops[$i]}, got ${redirect_url}"; exit 1; }
|
||||
url="$redirect_url"
|
||||
done
|
||||
|
||||
curl -fsSL -o /tmp/smoke.rpm "$rpm_url"
|
||||
rpm -qpi /tmp/smoke.rpm >/dev/null \
|
||||
|| { echo "::error::Not a valid RPM"; exit 1; }
|
||||
|
||||
asset_size=$(gh release view "$TAG" \
|
||||
--repo aaddrick/claude-desktop-debian \
|
||||
--json assets \
|
||||
--jq ".assets[] | select(.name == \"${rpm_name}\") | .size")
|
||||
local_size=$(stat -c %s /tmp/smoke.rpm)
|
||||
[[ "$asset_size" == "$local_size" ]] \
|
||||
|| { echo "::error::Size mismatch: ${local_size} vs ${asset_size}"; exit 1; }
|
||||
|
||||
echo "DNF smoke test passed: chain validated, file matches Releases asset"
|
||||
|
||||
update-aur-repo:
|
||||
name: Update AUR Package
|
||||
# RC tags never touch the AUR package.
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-rc')
|
||||
needs: [release]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download AMD64 AppImage artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-amd64-appimage
|
||||
path: artifacts/
|
||||
|
||||
- name: Extract version components from tag
|
||||
id: version
|
||||
run: |
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
# Tag format: v1.3.8+claude1.1.799
|
||||
# pkgver for AUR: 1.3.8+claude1.1.799
|
||||
pkgver="${tag#v}"
|
||||
# Wrapper version: 1.3.8 (before +claude)
|
||||
wrapper_ver="${pkgver%%+claude*}"
|
||||
# Claude version: 1.1.799 (after +claude)
|
||||
claude_ver="${pkgver#*+claude}"
|
||||
# AppImage filename:
|
||||
# claude-desktop-unofficial-{claude_ver}-{wrapper_ver}-amd64.AppImage
|
||||
# (the AUR *package* stays claude-desktop-appimage; only the
|
||||
# release asset it downloads carries the -unofficial name)
|
||||
appimage_name="claude-desktop-unofficial-${claude_ver}-${wrapper_ver}-amd64.AppImage"
|
||||
|
||||
echo "pkgver=$pkgver" >> "$GITHUB_OUTPUT"
|
||||
echo "appimage_name=$appimage_name" >> "$GITHUB_OUTPUT"
|
||||
echo "Tag: $tag"
|
||||
echo "pkgver: $pkgver"
|
||||
echo "AppImage name: $appimage_name"
|
||||
|
||||
- name: Compute AppImage checksum
|
||||
id: checksum
|
||||
env:
|
||||
APPIMAGE_NAME: ${{ steps.version.outputs.appimage_name }}
|
||||
run: |
|
||||
appimage="artifacts/${APPIMAGE_NAME}"
|
||||
if [[ ! -f "$appimage" ]]; then
|
||||
echo "::error::Expected AppImage not found: ${APPIMAGE_NAME}"
|
||||
echo "Available artifacts:"
|
||||
ls -la artifacts/
|
||||
exit 1
|
||||
fi
|
||||
sha256=$(sha256sum "$appimage" | awk '{print $1}')
|
||||
echo "sha256=$sha256" >> "$GITHUB_OUTPUT"
|
||||
echo "AppImage: ${APPIMAGE_NAME}"
|
||||
echo "SHA256: $sha256"
|
||||
|
||||
- name: Configure SSH for AUR
|
||||
env:
|
||||
AUR_SSH_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [[ -z "$AUR_SSH_KEY" ]]; then
|
||||
echo "::error::AUR_SSH_PRIVATE_KEY secret is not configured"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p ~/.ssh
|
||||
echo "$AUR_SSH_KEY" > ~/.ssh/aur
|
||||
chmod 600 ~/.ssh/aur
|
||||
ssh-keyscan -t ed25519,rsa aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
|
||||
cat > ~/.ssh/config <<-'EOF'
|
||||
Host aur.archlinux.org
|
||||
IdentityFile ~/.ssh/aur
|
||||
User aur
|
||||
EOF
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
- name: Clone AUR repository
|
||||
run: |
|
||||
git clone ssh://aur@aur.archlinux.org/claude-desktop-appimage.git aur-repo
|
||||
|
||||
- name: Generate PKGBUILD from template
|
||||
working-directory: aur-repo
|
||||
env:
|
||||
PKGVER: ${{ steps.version.outputs.pkgver }}
|
||||
APPIMAGE_NAME: ${{ steps.version.outputs.appimage_name }}
|
||||
SHA256: ${{ steps.checksum.outputs.sha256 }}
|
||||
run: |
|
||||
if [[ ! -f PKGBUILD.template ]]; then
|
||||
echo "::error::PKGBUILD.template not found in AUR repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s/%%PKGVER%%/${PKGVER}/" \
|
||||
-e "s/%%APPIMAGE_NAME%%/${APPIMAGE_NAME}/" \
|
||||
-e "s/%%SHA256_APPIMAGE%%/${SHA256}/" \
|
||||
PKGBUILD.template > PKGBUILD
|
||||
|
||||
- name: Generate .SRCINFO
|
||||
working-directory: aur-repo
|
||||
run: |
|
||||
docker run --rm -v "$PWD":/pkg -w /pkg archlinux:base bash -c "
|
||||
useradd -m builder
|
||||
chown builder:builder /pkg
|
||||
find /pkg -maxdepth 1 -not -name .git -not -path /pkg -exec chown -R builder:builder {} +
|
||||
su builder -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||
"
|
||||
# Restore ownership after docker (container uid may differ from runner)
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
|
||||
- name: Commit and push to AUR
|
||||
working-directory: aur-repo
|
||||
env:
|
||||
PKGVER: ${{ steps.version.outputs.pkgver }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add PKGBUILD .SRCINFO
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update to v${PKGVER}"
|
||||
git push
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Cleanup Workflow Runs
|
||||
run-name: |
|
||||
Cleanup: ${{
|
||||
github.event_name == 'schedule' && 'Weekly scheduled cleanup' ||
|
||||
format('Manual cleanup by @{0}', github.actor)
|
||||
}}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Thursday at midnight UTC
|
||||
- cron: "0 0 * * 4"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Delete Non-Release Runs
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: Delete old workflow runs not tied to release tags
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
echo "Fetching workflow runs..."
|
||||
|
||||
# Calculate cutoff date (3 days ago)
|
||||
cutoff=$(date -u -d '3 days ago' '+%Y-%m-%dT%H:%M:%SZ')
|
||||
echo "Deleting non-release runs older than: $cutoff"
|
||||
|
||||
# Get all runs where:
|
||||
# - headBranch doesn't start with "v" (not a release tag)
|
||||
# - createdAt is older than 3 days
|
||||
runs_to_delete=$(gh run list --json databaseId,headBranch,createdAt --limit 1000 | \
|
||||
jq -r --arg cutoff "$cutoff" \
|
||||
'.[] | select((.headBranch | startswith("v") | not) and (.createdAt < $cutoff)) | .databaseId')
|
||||
|
||||
if [ -z "$runs_to_delete" ]; then
|
||||
echo "No old non-release runs to delete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
count=$(echo "$runs_to_delete" | wc -l)
|
||||
echo "Found $count runs to delete..."
|
||||
|
||||
# Delete each run
|
||||
echo "$runs_to_delete" | while read -r run_id; do
|
||||
echo "Deleting run $run_id..."
|
||||
gh run delete "$run_id" || echo "Failed to delete run $run_id (may already be deleted)"
|
||||
done
|
||||
|
||||
echo "Cleanup complete!"
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Deploy Worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'worker/**'
|
||||
- '.github/workflows/deploy-worker.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Deploy Worker
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: worker
|
||||
|
||||
- name: Verify route is bound and Worker responds
|
||||
env:
|
||||
# Must match the hostname in worker/wrangler.toml's route.
|
||||
PROBE_HOST: pkg.claude-desktop-debian.dev
|
||||
run: |
|
||||
# Wait briefly for deploy + DNS propagation
|
||||
sleep 30
|
||||
|
||||
# Worker proxies metadata path through to gh-pages; expect any
|
||||
# 2xx/3xx. A 5xx or 521/523/530 means the route isn't bound or
|
||||
# the Worker errored at edge.
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
--max-time 30 \
|
||||
"https://${PROBE_HOST}/dists/stable/InRelease")
|
||||
echo "Probe status: ${status}"
|
||||
if [[ ! "$status" =~ ^[23] ]]; then
|
||||
echo "::error::Worker probe at ${PROBE_HOST} returned ${status}"
|
||||
echo "::error::Expected 2xx or 3xx (route bound + Worker responding)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Route bound, Worker responding."
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,673 @@
|
||||
name: Issue Triage (v1 — manual fallback only)
|
||||
run-name: |
|
||||
Triage v1: #${{ inputs.issue_number }}
|
||||
|
||||
# v1 pipeline kept as a workflow_dispatch-only fallback. Automatic
|
||||
# triggering on `issues` was removed when v2 (issue-triage-v2.yml)
|
||||
# took over production routing. If v2 is ever paused or rolled back,
|
||||
# re-enable the `issues: [opened, reopened]` trigger here.
|
||||
#
|
||||
# Kept (not deleted) because v1 uses different code paths for
|
||||
# investigation and label application, which still occasionally help
|
||||
# for backfilled issues the maintainer wants a second opinion on.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to triage"
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: issue-triage-${{ inputs.issue_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 1: Gate Check — decide whether triage should proceed
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
gate:
|
||||
name: Gate Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| github.event.sender.login != 'github-actions[bot]'
|
||||
outputs:
|
||||
should_triage: ${{ steps.check.outputs.should_triage }}
|
||||
issue_number: ${{ steps.check.outputs.issue_number }}
|
||||
steps:
|
||||
- name: Evaluate gate conditions
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
SENDER: ${{ github.event.sender.login }}
|
||||
run: |
|
||||
echo "issue_number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
labels=$(gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--json labels --jq '[.labels[].name]')
|
||||
|
||||
# Manual dispatch bypasses all gates
|
||||
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
|
||||
echo "should_triage=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Manual triage requested, bypassing gate checks"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Always skip needs-human unless manually triggered
|
||||
if printf '%s' "$labels" \
|
||||
| jq -e 'any(. == "triage: needs-human")' >/dev/null 2>&1; then
|
||||
echo "should_triage=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping: issue requires human triage"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip if already triaged (except reopened)
|
||||
if [[ "$EVENT_ACTION" != "reopened" ]]; then
|
||||
terminal_labels='["triage: investigated", "triage: duplicate", "triage: not-actionable"]'
|
||||
if printf '%s' "$labels" \
|
||||
| jq -e --argjson terms "$terminal_labels" \
|
||||
'any(. as $l | $terms | any(. == $l))' >/dev/null 2>&1; then
|
||||
echo "should_triage=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping: issue already triaged"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "should_triage=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 2: Classify Issue — gather context + run Claude classification
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
classify:
|
||||
name: Classify Issue
|
||||
runs-on: ubuntu-latest
|
||||
needs: gate
|
||||
if: needs.gate.outputs.should_triage == 'true'
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ needs.gate.outputs.issue_number }}
|
||||
outputs:
|
||||
classification: ${{ steps.classify.outputs.classification }}
|
||||
skip_comment: ${{ steps.classify.outputs.skip_comment }}
|
||||
needs_investigation: ${{ steps.classify.outputs.needs_investigation }}
|
||||
confidence: ${{ steps.classify.outputs.confidence }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Claude CLI
|
||||
run: npm install -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Gather issue context
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p /tmp/triage-context
|
||||
|
||||
# Fetch full issue details
|
||||
gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--json number,title,body,labels,comments,author,state,createdAt \
|
||||
> /tmp/triage-context/issue.json
|
||||
|
||||
# Extract title for searching related context
|
||||
title=$(jq -r '.title' /tmp/triage-context/issue.json)
|
||||
|
||||
# Search for related issues (open and closed)
|
||||
gh issue list \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--search "$title" \
|
||||
--state all \
|
||||
--limit 10 \
|
||||
--json number,title,state,labels \
|
||||
> /tmp/triage-context/related-issues.json
|
||||
|
||||
# Search for related PRs (open and closed)
|
||||
gh pr list \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--search "$title" \
|
||||
--state all \
|
||||
--limit 10 \
|
||||
--json number,title,state \
|
||||
> /tmp/triage-context/related-prs.json
|
||||
|
||||
- name: Classify issue with Claude
|
||||
id: classify
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
schema=$(cat .claude/scripts/schemas/triage-classify.json)
|
||||
|
||||
# Build prompt from template + data files (avoids shell injection)
|
||||
jq -n \
|
||||
--slurpfile issue /tmp/triage-context/issue.json \
|
||||
--slurpfile related_issues /tmp/triage-context/related-issues.json \
|
||||
--slurpfile related_prs /tmp/triage-context/related-prs.json \
|
||||
--rawfile claude_md CLAUDE.md \
|
||||
-r '"You are classifying a GitHub issue for the claude-desktop-debian project.\nThis project repackages Claude Desktop (Electron app) for Debian/Ubuntu Linux.\n\n## Project Context\n" + $claude_md + "\n\n## Issue\n" + ($issue[0] | tostring) + "\n\n## Related Issues\n" + ($related_issues[0] | tostring) + "\n\n## Related PRs\n" + ($related_prs[0] | tostring) + "\n\n## Label Glossary\nOnly suggest labels that accurately apply. Here is what each label means:\n- bug: Confirmed or likely software defect in THIS project (packaging, patching, build scripts)\n- enhancement: New feature request or improvement to this project\n- question: Usage question, not a bug or feature request\n- duplicate: This issue duplicates another existing issue\n- regression: Previously working functionality that broke in a newer release\n- security: Security-related issue (always set skip_comment=true for these)\n- cowork: Related to Cowork mode ONLY — the VM-based Claude Code session feature launched from the desktop app Code tab. Do NOT use for general Code tab issues or session history issues.\n- mcp: Related to MCP (Model Context Protocol) server/plugin integration\n- blocked: Waiting on an external dependency to be resolved\n- needs reproduction: Cannot reproduce, need more info from reporter\n- platform: amd64 / platform: arm64: Issue is specific to one CPU architecture\n- format: deb / format: appimage / format: rpm / format: nix: Issue is specific to one package format\n- priority: critical: Blocks usage for most users\n- priority: high: Important, should be addressed soon\n- priority: medium: Should be addressed when possible\n- priority: low: Nice to have, not urgent\n\n## Instructions\n1. Read the issue carefully. Consider the title, body, and any comments.\n2. Check the related issues and PRs for duplicates or prior discussion.\n3. Classify the issue into one of: bug, feature, question, duplicate, needs-info, not-actionable, needs-human.\n4. Set skip_comment to true if: classification is needs-human, you have low confidence on a complex or sensitive issue, or the issue involves security concerns.\n5. Set needs_source_investigation to true only if understanding the original Claude Desktop JavaScript source would help investigate.\n6. Suggest additional labels from the Label Glossary above. Only apply labels you are confident are correct.\n7. If classifying as duplicate, set duplicate_of to the issue number.\n8. If classifying as needs-info, list specific questions to ask."' \
|
||||
> /tmp/classify-prompt.txt
|
||||
|
||||
result=$(claude -p "$(cat /tmp/classify-prompt.txt)" \
|
||||
--output-format json \
|
||||
--json-schema "$schema" \
|
||||
--model claude-sonnet-4-6 \
|
||||
--max-budget-usd 2.00 \
|
||||
2>/dev/null) || {
|
||||
echo "::error::Claude classification failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract structured output (key is .structured_output per claude CLI)
|
||||
structured=$(printf '%s' "$result" \
|
||||
| jq -c '.structured_output // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$structured" ]]; then
|
||||
echo "::error::No structured output from classification"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$structured" > /tmp/triage-context/classification.json
|
||||
|
||||
classification=$(jq -r '.classification' /tmp/triage-context/classification.json)
|
||||
skip_comment=$(jq -r '.skip_comment // false' /tmp/triage-context/classification.json)
|
||||
needs_investigation=$(jq -r '.needs_source_investigation' \
|
||||
/tmp/triage-context/classification.json)
|
||||
confidence=$(jq -r '.confidence // "medium"' /tmp/triage-context/classification.json)
|
||||
|
||||
{
|
||||
echo "classification=$classification"
|
||||
echo "skip_comment=$skip_comment"
|
||||
echo "needs_investigation=$needs_investigation"
|
||||
echo "confidence=$confidence"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Classification: $classification (skip=$skip_comment, investigate=$needs_investigation, confidence=$confidence)"
|
||||
|
||||
- name: Upload triage context
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: triage-context
|
||||
path: /tmp/triage-context/
|
||||
retention-days: 1
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 3: Fetch Reference Source — download beautified original source
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
fetch-reference:
|
||||
name: Fetch Reference Source
|
||||
runs-on: ubuntu-latest
|
||||
needs: classify
|
||||
if: >-
|
||||
needs.classify.outputs.needs_investigation == 'true'
|
||||
&& needs.classify.outputs.skip_comment != 'true'
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install extraction tools
|
||||
run: npm install -g @electron/asar prettier
|
||||
|
||||
- name: Download amd64 AppImage from latest release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release download \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--pattern '*amd64*.AppImage' \
|
||||
--skip-existing \
|
||||
--dir /tmp/ref-source || {
|
||||
echo "::error::Could not download AppImage from latest release"
|
||||
exit 1
|
||||
}
|
||||
appimage=$(find /tmp/ref-source -name '*amd64*.AppImage' ! -name '*.zsync' | head -1)
|
||||
if [[ -z "$appimage" ]]; then
|
||||
echo "::error::No amd64 AppImage found in release assets"
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloaded: $appimage"
|
||||
|
||||
- name: Extract and beautify reference source
|
||||
run: |
|
||||
appimage=$(find /tmp/ref-source -name '*amd64*.AppImage' ! -name '*.zsync' | head -1)
|
||||
chmod +x "$appimage"
|
||||
cd /tmp/ref-source
|
||||
"$appimage" --appimage-extract >/dev/null 2>&1
|
||||
asar_path=$(find squashfs-root -name 'app.asar' -path '*/resources/*' | head -1)
|
||||
if [[ -z "$asar_path" ]]; then
|
||||
echo "::error::app.asar not found in AppImage"
|
||||
exit 1
|
||||
fi
|
||||
asar extract "$asar_path" app-extracted
|
||||
|
||||
echo "Extracted contents (top-level):"
|
||||
ls -la app-extracted/
|
||||
echo ""
|
||||
if [[ -d app-extracted/.vite/build ]]; then
|
||||
echo "Beautifying JS files in .vite/build/:"
|
||||
ls app-extracted/.vite/build/*.js
|
||||
npx prettier --write "app-extracted/.vite/build/*.js"
|
||||
else
|
||||
echo "::warning::.vite/build/ directory not found in extracted source"
|
||||
find app-extracted -name '*.js' -not -path '*/node_modules/*' | head -20
|
||||
fi
|
||||
echo ""
|
||||
echo "Total files: $(find app-extracted -type f | wc -l)"
|
||||
|
||||
- name: Upload reference source
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: reference-source
|
||||
path: /tmp/ref-source/app-extracted/
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 4: Investigate Source — deep-dive with Claude into repo + ref
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
investigate:
|
||||
name: Investigate Source
|
||||
runs-on: ubuntu-latest
|
||||
needs: [classify, fetch-reference]
|
||||
if: needs.fetch-reference.result == 'success'
|
||||
outputs:
|
||||
has_findings: ${{ steps.investigate.outputs.has_findings }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Claude CLI
|
||||
run: npm install -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Download triage context
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: triage-context
|
||||
path: /tmp/triage-context/
|
||||
|
||||
- name: Download reference source
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: reference-source
|
||||
path: /tmp/ref-source/app-extracted/
|
||||
|
||||
- name: Verify reference source
|
||||
run: |
|
||||
echo "Reference source contents:"
|
||||
ls -la /tmp/ref-source/app-extracted/
|
||||
if [[ -d /tmp/ref-source/app-extracted/.vite/build ]]; then
|
||||
echo "Key JS files:"
|
||||
ls -la /tmp/ref-source/app-extracted/.vite/build/*.js
|
||||
else
|
||||
echo "::warning::.vite/build/ not found in downloaded artifact"
|
||||
fi
|
||||
|
||||
- name: Investigate with Claude
|
||||
id: investigate
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
# Build investigation prompt from files (avoids shell injection)
|
||||
{
|
||||
cat << 'PREAMBLE'
|
||||
Investigate the following GitHub issue for the claude-desktop-debian project.
|
||||
|
||||
## Classification
|
||||
PREAMBLE
|
||||
cat /tmp/triage-context/classification.json
|
||||
echo ""
|
||||
echo "## Investigation Hints"
|
||||
jq -r '.investigation_hints // "None"' /tmp/triage-context/classification.json
|
||||
cat << CONTEXT
|
||||
|
||||
The project repository is at $(pwd). Search the source code for relevant patterns.
|
||||
The beautified reference source (original app.asar) is at /tmp/ref-source/app-extracted/.
|
||||
Key files: .vite/build/index.js (main-process entry stub; since 1.19367.0 it require()s the code-split main chunk), .vite/build/index.chunk-<hash>.js (main process — the real code), .vite/build/mainWindow.js, .vite/build/mainView.js.
|
||||
|
||||
## Project Documentation
|
||||
CONTEXT
|
||||
cat CLAUDE.md
|
||||
cat << 'BODY'
|
||||
|
||||
## How This Project Patches Upstream Code
|
||||
IMPORTANT: All fixes to the original JavaScript are applied via sed/regex in scripts/patches/*.sh.
|
||||
Each subsystem owns its own file — tray.sh, cowork.sh, claude-code.sh, quick-window.sh,
|
||||
titlebar.sh, app-asar.sh — with shared helpers in scripts/patches/_common.sh.
|
||||
build.sh is a ~300-line orchestrator that sources these modules in order.
|
||||
Variable and function names are MINIFIED and change between releases.
|
||||
Patches must use regex patterns that match both minified and beautified spacing.
|
||||
Variable names are extracted dynamically with grep -oP, never hardcoded.
|
||||
See scripts/patches/*.sh for examples of existing patches (search for patch_ functions).
|
||||
The wrapper files (frame-fix-wrapper.js, frame-fix-entry.js) intercept require('electron')
|
||||
and can patch BrowserWindow defaults without touching minified code.
|
||||
|
||||
## Investigation Rules
|
||||
|
||||
### All bugs are ours to fix
|
||||
This project's goal is to take a working Anthropic product and make it work
|
||||
on Linux. Every bug is something we can investigate and potentially patch.
|
||||
Check scripts/patches/*.sh first for bugs in patched areas (cowork.sh for cowork,
|
||||
tray.sh for tray, titlebar.sh or quick-window.sh for window decorations, app-asar.sh
|
||||
for platform checks / frame). Read the relevant patch_ function and trace what it
|
||||
modifies. If a behavior difference exists between Windows/macOS and our Linux build,
|
||||
that is a gap in our patching.
|
||||
|
||||
### Verify before stating
|
||||
Only state facts you verified by reading actual code or running commands.
|
||||
Never claim code exists, functions behave a certain way, or patterns match
|
||||
without finding them in the source. If you cannot find evidence, say so
|
||||
explicitly rather than speculating.
|
||||
|
||||
### Validate network assumptions
|
||||
For download, CDN, or network-related issues, use curl to verify URLs
|
||||
actually exist before speculating about failures. For example:
|
||||
curl -sI "https://example.com/file" | head -5
|
||||
Check HTTP status codes rather than assuming 404 or success.
|
||||
|
||||
## Output Format
|
||||
Structure your response in these sections:
|
||||
|
||||
### Findings
|
||||
Concise root cause analysis. Be specific about file paths and line numbers.
|
||||
|
||||
### Relevant Code
|
||||
Include the actual code snippets relevant to diagnosing or fixing this issue.
|
||||
For each snippet, include:
|
||||
- The file path and line numbers
|
||||
- The code block itself
|
||||
- A one-line note on why it matters
|
||||
|
||||
### Patch Approach
|
||||
If a fix is feasible, provide everything an agent needs to implement it:
|
||||
- The exact anchor strings or regex patterns to locate the target code in minified source
|
||||
- What the sed replacement should do (insert, wrap, modify)
|
||||
- Any variable names that need dynamic extraction (with the grep -oP pattern to extract them)
|
||||
- Whether the fix belongs in scripts/patches/*.sh (sed patch) or frame-fix-wrapper.js (Electron intercept)
|
||||
- Surrounding context (what comes before/after the target) to make the regex unique
|
||||
The goal is to give enough context that an agent can write the patch without re-reading the source.
|
||||
BODY
|
||||
} > /tmp/investigate-prompt.txt
|
||||
|
||||
investigation=$(claude -p "$(cat /tmp/investigate-prompt.txt)" \
|
||||
--dangerously-skip-permissions \
|
||||
--model claude-sonnet-4-6 \
|
||||
--max-budget-usd 3.00 \
|
||||
2>/dev/null) || {
|
||||
echo "::warning::Investigation failed"
|
||||
echo "has_findings=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Handle both JSON and plain text output
|
||||
if printf '%s' "$investigation" | jq -e '.result' >/dev/null 2>&1; then
|
||||
printf '%s' "$investigation" | jq -r '.result' > /tmp/investigation.txt
|
||||
else
|
||||
printf '%s' "$investigation" > /tmp/investigation.txt
|
||||
fi
|
||||
|
||||
if [[ -s /tmp/investigation.txt ]]; then
|
||||
echo "has_findings=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_findings=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Upload investigation findings
|
||||
if: steps.investigate.outputs.has_findings == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: investigation-findings
|
||||
path: /tmp/investigation.txt
|
||||
retention-days: 1
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 5: Fetch Voice Profile — download aaddrick's writing style guide
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
fetch-voice:
|
||||
name: Fetch Voice Profile
|
||||
runs-on: ubuntu-latest
|
||||
needs: classify
|
||||
if: needs.classify.outputs.skip_comment != 'true'
|
||||
steps:
|
||||
- name: Download voice profile
|
||||
run: |
|
||||
curl -fsSL \
|
||||
"https://raw.githubusercontent.com/aaddrick/written-voice-replication/master/.claude/agents/aaddrick-voice.md" \
|
||||
-o /tmp/voice-profile.md
|
||||
|
||||
- name: Upload voice profile
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: voice-profile
|
||||
path: /tmp/voice-profile.md
|
||||
retention-days: 1
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 6: Write Comment — generate and post triage comment
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
comment:
|
||||
name: Write Comment
|
||||
runs-on: ubuntu-latest
|
||||
needs: [classify, investigate, fetch-voice]
|
||||
if: >-
|
||||
always()
|
||||
&& needs.classify.result == 'success'
|
||||
&& needs.classify.outputs.skip_comment != 'true'
|
||||
&& needs.investigate.result != 'cancelled'
|
||||
&& needs.fetch-voice.result != 'cancelled'
|
||||
outputs:
|
||||
comment_posted: ${{ steps.post.outputs.comment_posted }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Claude CLI
|
||||
run: npm install -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Download triage context
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: triage-context
|
||||
path: /tmp/triage-context/
|
||||
|
||||
- name: Download investigation findings
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: investigation-findings
|
||||
path: /tmp/investigation/
|
||||
|
||||
- name: Download voice profile
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: voice-profile
|
||||
path: /tmp/voice/
|
||||
|
||||
- name: Generate triage comment
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
# Build comment prompt from files (avoids shell expansion issues)
|
||||
{
|
||||
cat << 'HEADER'
|
||||
Generate a triage comment for this GitHub issue.
|
||||
|
||||
## Writing Voice (CRITICAL — follow this exactly)
|
||||
The following voice profile defines HOW you write. Match this voice
|
||||
precisely. Every aspect of tone, sentence structure, and word choice
|
||||
must follow this profile. This is the most important instruction.
|
||||
HEADER
|
||||
echo ""
|
||||
if [[ -f /tmp/voice/voice-profile.md ]]; then
|
||||
cat /tmp/voice/voice-profile.md
|
||||
fi
|
||||
echo ""
|
||||
echo "## Project Context"
|
||||
cat CLAUDE.md
|
||||
echo ""
|
||||
echo "## Classification"
|
||||
cat /tmp/triage-context/classification.json
|
||||
echo ""
|
||||
echo "## Related Issues"
|
||||
cat /tmp/triage-context/related-issues.json
|
||||
echo ""
|
||||
echo "## Related PRs"
|
||||
cat /tmp/triage-context/related-prs.json
|
||||
echo ""
|
||||
echo "## Investigation Findings"
|
||||
if [[ -f /tmp/investigation/investigation.txt ]]; then
|
||||
cat /tmp/investigation/investigation.txt
|
||||
echo ""
|
||||
echo "NOTE: The investigation includes relevant code samples. When useful, include key snippets in the comment to help whoever picks up the fix. Don't dump everything — just the code that clarifies the root cause or shows where a patch would go."
|
||||
else
|
||||
echo "No investigation performed."
|
||||
fi
|
||||
echo ""
|
||||
cat << 'INSTRUCTIONS'
|
||||
## Formatting Constraints
|
||||
- This is an automated one-shot triage comment. You will NOT be part of any follow-up conversation. Do not ask the reporter to share output with you, do not offer to write fixes, do not imply you will respond again. Write as if leaving a final note.
|
||||
- Every bug is ours to investigate and fix. Frame findings in terms of what could be patched. Never dismiss an issue as someone else's problem.
|
||||
- Lead with the finding, then reasoning
|
||||
- Keep to 2-4 short paragraphs
|
||||
- Use code blocks or links where helpful
|
||||
- Reference related issues/PRs if they provide useful context (use #NNN format)
|
||||
- Don't open with "Thank you for your report" or similar
|
||||
- Don't overpromise fixes or timelines
|
||||
- If the classification is "duplicate", link to the duplicate issue
|
||||
- If "needs-info", ask the specific questions from the classification
|
||||
- Output ONLY the comment text, no wrapping or explanation. Do not ask for approval, confirmation, or permission. Your output will be posted directly.
|
||||
- End with this exact attribution block:
|
||||
|
||||
---
|
||||
*This is an automated triage comment. A maintainer will review this issue and may provide further guidance.*
|
||||
|
||||
Written by Claude Sonnet via [Claude Code](https://claude.ai/code)
|
||||
INSTRUCTIONS
|
||||
} > /tmp/comment-prompt.txt
|
||||
|
||||
comment_result=$(claude -p "$(cat /tmp/comment-prompt.txt)" \
|
||||
--dangerously-skip-permissions \
|
||||
--model claude-sonnet-4-6 \
|
||||
--max-budget-usd 2.00 \
|
||||
2>/dev/null) || {
|
||||
echo "::error::Comment generation failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Handle both JSON (.result key) and plain text output
|
||||
if printf '%s' "$comment_result" | jq -e '.result' >/dev/null 2>&1; then
|
||||
printf '%s' "$comment_result" | jq -r '.result' > /tmp/comment.md
|
||||
else
|
||||
printf '%s' "$comment_result" > /tmp/comment.md
|
||||
fi
|
||||
|
||||
- name: Post comment
|
||||
id: post
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
issue_num=$(jq -r '.number' /tmp/triage-context/issue.json)
|
||||
|
||||
if [[ -s /tmp/comment.md ]]; then
|
||||
gh issue comment "$issue_num" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--body-file /tmp/comment.md
|
||||
echo "comment_posted=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Posted triage comment on issue #$issue_num"
|
||||
else
|
||||
echo "comment_posted=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Comment file is empty, skipping post"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Job 7: Apply Labels — triage label + suggested labels (LAST)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
label:
|
||||
name: Apply Labels
|
||||
runs-on: ubuntu-latest
|
||||
needs: [classify, comment]
|
||||
if: >-
|
||||
always()
|
||||
&& needs.classify.result == 'success'
|
||||
steps:
|
||||
- name: Download triage context
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: triage-context
|
||||
path: /tmp/triage-context/
|
||||
|
||||
- name: Apply triage and suggested labels
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
issue_num=$(jq -r '.number' /tmp/triage-context/issue.json)
|
||||
classification=$(jq -r '.classification' /tmp/triage-context/classification.json)
|
||||
|
||||
# Remove old triage labels and needs triage
|
||||
for label in \
|
||||
"triage: investigated" \
|
||||
"triage: needs-info" \
|
||||
"triage: duplicate" \
|
||||
"triage: not-actionable" \
|
||||
"triage: needs-human" \
|
||||
"needs triage"; do
|
||||
gh issue edit "$issue_num" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--remove-label "$label" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Map classification to triage label
|
||||
case "$classification" in
|
||||
bug|feature|question)
|
||||
triage_label="triage: investigated"
|
||||
;;
|
||||
duplicate|needs-info|not-actionable|needs-human)
|
||||
triage_label="triage: $classification"
|
||||
;;
|
||||
*)
|
||||
triage_label="triage: needs-human"
|
||||
;;
|
||||
esac
|
||||
|
||||
gh issue edit "$issue_num" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--add-label "$triage_label"
|
||||
|
||||
# Apply additional suggested labels
|
||||
suggested=$(jq -r '.suggested_labels[]? // empty' \
|
||||
/tmp/triage-context/classification.json 2>/dev/null)
|
||||
while IFS= read -r label; do
|
||||
if [[ -n "$label" ]]; then
|
||||
gh issue edit "$issue_num" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--add-label "$label" 2>/dev/null || true
|
||||
fi
|
||||
done <<< "$suggested"
|
||||
|
||||
echo "Applied triage label: $triage_label"
|
||||
@@ -0,0 +1,71 @@
|
||||
# Records the newest claude-desktop version in Anthropic's official APT
|
||||
# pool once a day, on the `official-pool-log` branch. The log answers the
|
||||
# rebase go/no-go question "does the official Linux pool track the release
|
||||
# train closely enough to rebase on?" with data instead of optimism.
|
||||
# See docs/learnings/official-deb-rebase-verification.md.
|
||||
name: Official Pool Recorder
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: official-pool-recorder
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
record:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve newest official versions
|
||||
id: resolve
|
||||
run: |
|
||||
source scripts/_common.sh
|
||||
source scripts/setup/official-deb.sh
|
||||
resolve_official_deb amd64
|
||||
echo "amd64=$resolved_official_version" >> "$GITHUB_OUTPUT"
|
||||
resolve_official_deb arm64
|
||||
echo "arm64=$resolved_official_version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Append to log branch
|
||||
env:
|
||||
AMD64_VERSION: ${{ steps.resolve.outputs.amd64 }}
|
||||
ARM64_VERSION: ${{ steps.resolve.outputs.arm64 }}
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
if git fetch origin official-pool-log:official-pool-log; then
|
||||
git checkout official-pool-log
|
||||
else
|
||||
git checkout --orphan official-pool-log
|
||||
git rm -rf --quiet . || true
|
||||
printf 'date\tamd64\tarm64\n' > official-pool-versions.tsv
|
||||
fi
|
||||
|
||||
last=$(tail -n1 official-pool-versions.tsv | cut -f2,3)
|
||||
new=$(printf '%s\t%s' "$AMD64_VERSION" "$ARM64_VERSION")
|
||||
if [[ "$last" == "$new" ]]; then
|
||||
echo 'No new pool version; nothing to record.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$(date -u +%F)" "$new" \
|
||||
>> official-pool-versions.tsv
|
||||
git add official-pool-versions.tsv
|
||||
git commit -m "record official pool versions $(date -u +%F)"
|
||||
|
||||
for _ in 1 2 3; do
|
||||
if git push origin official-pool-log; then
|
||||
exit 0
|
||||
fi
|
||||
git pull --rebase origin official-pool-log
|
||||
done
|
||||
echo 'Failed to push after 3 attempts' >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Shellcheck
|
||||
run-name: |
|
||||
Shellcheck: ${{
|
||||
github.event_name == 'pull_request' && format('PR #{0} by @{1} - {2}', github.event.pull_request.number, github.actor, github.event.pull_request.title) ||
|
||||
github.event_name == 'push' && github.event.head_commit && format('Push by @{0} - {1}', github.actor, github.event.head_commit.message) ||
|
||||
format('{0} triggered by @{1}', github.event_name, github.actor)
|
||||
}}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: Check shell scripts
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y shellcheck
|
||||
- name: shellcheck
|
||||
run: |
|
||||
git grep -l '^#\( *shellcheck \|!\(/bin/\|/usr/bin/env \)\(sh\|bash\|dash\|ksh\)\)' -- '*.sh' | xargs shellcheck -x
|
||||
@@ -0,0 +1,89 @@
|
||||
name: Test Build Artifacts (Reusable)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
arch:
|
||||
description: Architecture of the artifacts under test (amd64/arm64)
|
||||
type: string
|
||||
default: amd64
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-artifact:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- format: deb
|
||||
container: ""
|
||||
- format: rpm
|
||||
container: "fedora:42"
|
||||
- format: appimage
|
||||
container: ""
|
||||
|
||||
name: Validate ${{ inputs.arch }} ${{ matrix.format }} package
|
||||
# arm64 artifacts run on a native arm64 runner (matching build-arm64)
|
||||
# so the launch smoke test actually executes the packaged binary
|
||||
# rather than failing on a foreign architecture.
|
||||
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-latest' }}
|
||||
container: ${{ matrix.container || '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: package-${{ inputs.arch }}-${{ matrix.format }}
|
||||
path: artifacts/
|
||||
|
||||
- name: Install test dependencies (Fedora)
|
||||
if: matrix.format == 'rpm'
|
||||
# Electron's shared libraries (nss/nspr/gtk3/X11/etc.) must be
|
||||
# installed explicitly: the rpm is installed with `rpm -ivh --nodeps`
|
||||
# and its spec sets `AutoReqProv: no`, so the package declares no
|
||||
# runtime Requires and nothing pulls these in. Without them the
|
||||
# launch smoke test dies with `libnspr4.so: cannot open shared
|
||||
# object file` (exit 127). The Ubuntu runner already carries them.
|
||||
run: |
|
||||
dnf install -y findutils file nodejs npm \
|
||||
xorg-x11-server-Xvfb dbus-daemon util-linux procps-ng \
|
||||
nss nspr atk at-spi2-atk at-spi2-core cups-libs gtk3 \
|
||||
libdrm mesa-libgbm alsa-lib libX11 libXcomposite libXdamage \
|
||||
libXext libXfixes libXrandr libxcb libxkbcommon pango cairo \
|
||||
libXScrnSaver libXtst libxshmfence
|
||||
|
||||
- name: Install test dependencies (Ubuntu)
|
||||
if: matrix.format != 'rpm'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y file libfuse2 nodejs npm \
|
||||
xvfb dbus-x11 procps
|
||||
|
||||
# Fail loud if a smoke-test tool is missing. Without this guard a
|
||||
# missing/renamed tool turns run_launch_smoke_test into a silent
|
||||
# green skip (it does `pass "$skip"; return`), masking the test.
|
||||
- name: Verify smoke-test tools are present (Ubuntu)
|
||||
if: matrix.format != 'rpm'
|
||||
run: |
|
||||
for t in xvfb-run dbus-run-session setsid; do
|
||||
command -v "$t" >/dev/null || { echo "::error::missing $t"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Verify smoke-test tools are present (Fedora)
|
||||
if: matrix.format == 'rpm'
|
||||
run: |
|
||||
for t in xvfb-run dbus-run-session setsid runuser; do
|
||||
command -v "$t" >/dev/null || { echo "::error::missing $t"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Run artifact tests
|
||||
env:
|
||||
TARGET_ARCH: ${{ inputs.arch }}
|
||||
run: |
|
||||
chmod +x tests/test-artifact-${{ matrix.format }}.sh
|
||||
tests/test-artifact-${{ matrix.format }}.sh artifacts/
|
||||
@@ -0,0 +1,161 @@
|
||||
name: Test Build Script Flags (Reusable)
|
||||
|
||||
on:
|
||||
workflow_call: # Make this workflow reusable
|
||||
workflow_dispatch: # Allows manual triggering for testing
|
||||
|
||||
concurrency:
|
||||
group: test-flags-${{ github.ref }}
|
||||
# Matches ci.yml: queue rather than cancel, so a reusable invocation
|
||||
# from an in-flight CI run isn't killed mid-flight on the next push.
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
test-flags:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
# FUSE install removed - not needed for --test-flags
|
||||
|
||||
- name: Make build script executable
|
||||
run: chmod +x ./build.sh
|
||||
|
||||
# Test Case 1: Defaults (deb, clean=yes)
|
||||
- name: Test Case 1 - Defaults (deb, clean=yes)
|
||||
run: |
|
||||
echo "--- Running Test Case 1: ./build.sh --test-flags ---"
|
||||
OUTPUT=$(./build.sh --test-flags)
|
||||
echo "$OUTPUT" # Print output for logs
|
||||
echo "--- Verifying Test Case 1 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 1 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 2: --build deb (clean=yes)
|
||||
- name: Test Case 2 - --build deb (clean=yes)
|
||||
run: |
|
||||
echo "--- Running Test Case 2: ./build.sh --build deb --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build deb --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 2 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 2 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 3: --build appimage (clean=yes)
|
||||
- name: Test Case 3 - --build appimage (clean=yes)
|
||||
run: |
|
||||
echo "--- Running Test Case 3: ./build.sh --build appimage --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build appimage --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 3 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: appimage" || (echo "❌ Expected 'Build Format: appimage'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 3 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 4: --clean yes (build=deb)
|
||||
- name: Test Case 4 - --clean yes (build=deb)
|
||||
run: |
|
||||
echo "--- Running Test Case 4: ./build.sh --clean yes --test-flags ---"
|
||||
OUTPUT=$(./build.sh --clean yes --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 4 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 4 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 5: --clean no (build=deb)
|
||||
- name: Test Case 5 - --clean no (build=deb)
|
||||
run: |
|
||||
echo "--- Running Test Case 5: ./build.sh --clean no --test-flags ---"
|
||||
OUTPUT=$(./build.sh --clean no --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 5 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: no" || (echo "❌ Expected 'Clean Action: no'" && exit 1)
|
||||
echo "✓ Test Case 5 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 6: --build deb --clean yes
|
||||
- name: Test Case 6 - --build deb --clean yes
|
||||
run: |
|
||||
echo "--- Running Test Case 6: ./build.sh --build deb --clean yes --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build deb --clean yes --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 6 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 6 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 7: --build deb --clean no
|
||||
- name: Test Case 7 - --build deb --clean no
|
||||
run: |
|
||||
echo "--- Running Test Case 7: ./build.sh --build deb --clean no --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build deb --clean no --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 7 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: deb" || (echo "❌ Expected 'Build Format: deb'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: no" || (echo "❌ Expected 'Clean Action: no'" && exit 1)
|
||||
echo "✓ Test Case 7 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 8: --build appimage --clean yes
|
||||
- name: Test Case 8 - --build appimage --clean yes
|
||||
run: |
|
||||
echo "--- Running Test Case 8: ./build.sh --build appimage --clean yes --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build appimage --clean yes --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 8 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: appimage" || (echo "❌ Expected 'Build Format: appimage'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: yes" || (echo "❌ Expected 'Clean Action: yes'" && exit 1)
|
||||
echo "✓ Test Case 8 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 9: --build appimage --clean no
|
||||
- name: Test Case 9 - --build appimage --clean no
|
||||
run: |
|
||||
echo "--- Running Test Case 9: ./build.sh --build appimage --clean no --test-flags ---"
|
||||
OUTPUT=$(./build.sh --build appimage --clean no --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 9 ---"
|
||||
echo "$OUTPUT" | grep -q "Build Format: appimage" || (echo "❌ Expected 'Build Format: appimage'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Clean Action: no" || (echo "❌ Expected 'Clean Action: no'" && exit 1)
|
||||
echo "✓ Test Case 9 Passed"
|
||||
# No cleanup needed
|
||||
|
||||
# Test Case 10: --arch arm64 overrides the detected architecture
|
||||
# (cross-build support; the runner itself is amd64). The last
|
||||
# "Target Architecture" line is the authoritative post-override one
|
||||
# from parse_arguments; detect_architecture's earlier line reports
|
||||
# the raw host detection by design.
|
||||
- name: Test Case 10 - --arch arm64 override
|
||||
run: |
|
||||
echo "--- Running Test Case 10: ./build.sh --arch arm64 --build appimage --test-flags ---"
|
||||
OUTPUT=$(./build.sh --arch arm64 --build appimage --test-flags)
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 10 ---"
|
||||
echo "$OUTPUT" | grep "Target Architecture:" | tail -1 | grep -q "arm64" || (echo "❌ Expected final 'Target Architecture: arm64'" && exit 1)
|
||||
echo "$OUTPUT" | grep -q "Build Format: appimage" || (echo "❌ Expected 'Build Format: appimage'" && exit 1)
|
||||
echo "✓ Test Case 10 Passed"
|
||||
|
||||
# Test Case 11: --arch rejects invalid values
|
||||
- name: Test Case 11 - --arch rejects invalid values
|
||||
run: |
|
||||
echo "--- Running Test Case 11: ./build.sh --arch bogus --test-flags ---"
|
||||
if OUTPUT=$(./build.sh --arch bogus --test-flags 2>&1); then
|
||||
echo "$OUTPUT"
|
||||
echo "❌ Expected non-zero exit for invalid --arch"
|
||||
exit 1
|
||||
fi
|
||||
echo "$OUTPUT"
|
||||
echo "--- Verifying Test Case 11 ---"
|
||||
echo "$OUTPUT" | grep -q "Invalid architecture specified" || (echo "❌ Expected invalid-architecture error message" && exit 1)
|
||||
echo "✓ Test Case 11 Passed"
|
||||
@@ -0,0 +1,56 @@
|
||||
name: BATS Tests
|
||||
run-name: |
|
||||
BATS: ${{
|
||||
github.event_name == 'pull_request' && format('PR #{0} by @{1} - {2}', github.event.pull_request.number, github.actor, github.event.pull_request.title) ||
|
||||
github.event_name == 'push' && github.event.head_commit && format('Push by @{0} - {1}', github.actor, github.event.head_commit.message) ||
|
||||
format('{0} triggered by @{1}', github.event_name, github.actor)
|
||||
}}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/tests.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: bats-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bats:
|
||||
name: BATS unit tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Install BATS and Node.js
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats nodejs
|
||||
|
||||
- name: Run BATS test suite
|
||||
# Cowork tests load scripts/cowork-fallback/cowork-vm-service.js
|
||||
# via `node` — the `nodejs` install above is what they need.
|
||||
# The cowork-fallback suites cover the bwrap fallback daemon and
|
||||
# its asar patch, which now ship in every package (they were
|
||||
# exempt while the code shipped in no artifact).
|
||||
run: |
|
||||
bats --print-output-on-failure \
|
||||
tests/*.bats scripts/cowork-fallback/tests/*.bats
|
||||
|
||||
- name: Chromium switch-list smoke
|
||||
# Fails if the launcher's effective Chromium switch list drifts
|
||||
# from tools/chromium-switches.baseline without a deliberate
|
||||
# update (every upstream bump / launcher PR is checked).
|
||||
run: ./tools/chromium-switch-smoke.sh
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Update Flake Lock
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 1" # Monday 2 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: main-branch-auto-update
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update-flake-lock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@c5a866b6ab867e88becbed4467b93592bce69f8a # v21
|
||||
|
||||
- name: Update flake.lock
|
||||
run: nix flake update --flake .
|
||||
|
||||
- name: Check for changes
|
||||
id: check
|
||||
run: |
|
||||
if git diff --quiet flake.lock; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "flake.lock is already up to date"
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "flake.lock has changes:"
|
||||
nix flake metadata --json | jq -r '.locks.nodes | to_entries[] | select(.key != "root") | "\(.key): \(.value.locked.rev[0:8])"'
|
||||
fi
|
||||
|
||||
- name: Commit and push
|
||||
if: steps.check.outputs.changed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add flake.lock
|
||||
git commit -m "chore: update flake.lock"
|
||||
git push
|
||||
Reference in New Issue
Block a user