diff --git a/.github/workflows/desktop-packaging.yml b/.github/workflows/desktop-packaging.yml index bc6a5ec..59bf859 100644 --- a/.github/workflows/desktop-packaging.yml +++ b/.github/workflows/desktop-packaging.yml @@ -19,6 +19,93 @@ jobs: working-directory: . steps: - uses: actions/checkout@v4 + - name: Determine next release version + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + version="$(gh release list --repo "$GITHUB_REPOSITORY" --limit 1000 --json tagName --jq '.[].tagName' | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+ + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + + - name: Install Linux system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev librsvg2-dev libayatana-appindicator3-dev patchelf + + - name: Download Linux llama.cpp runtime + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import json, os, pathlib, urllib.request + headers = {"User-Agent": "deepLocal-CI", "Accept": "application/vnd.github+json"} + request = urllib.request.Request("https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=10", headers=headers) + with urllib.request.urlopen(request) as response: + releases = json.load(response) + assets = [a for r in releases if not r["draft"] for a in r["assets"]] + matches = [a for a in assets if "ubuntu-x64" in a["name"].lower() and a["name"].endswith(".tar.gz")] + if not matches: + raise SystemExit("No compatible Linux llama.cpp release asset found") + url = matches[0]["browser_download_url"] + urllib.request.urlretrieve(url, "/tmp/llama-runtime.tar.gz") + PY + mkdir -p .tools/llama.cpp + tar -xzf /tmp/llama-runtime.tar.gz -C .tools/llama.cpp + server="$(find .tools/llama.cpp -type f -name llama-server -print -quit)" + test -n "$server" + chmod +x "$server" + echo "DEEPLOCAL_LLAMA_SERVER=$GITHUB_WORKSPACE/$server" >> "$GITHUB_ENV" + + - name: Download Windows llama.cpp runtime + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=10' -Headers @{ 'User-Agent' = 'deepLocal-CI' } + $asset = $releases | Where-Object { -not $_.draft } | ForEach-Object { $_.assets } | + Where-Object { $_.name -match 'win-cpu-x64.*\.zip$' } | Select-Object -First 1 + if (-not $asset) { throw 'No compatible Windows llama.cpp release asset found.' } + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile "$env:RUNNER_TEMP\llama-runtime.zip" + New-Item -ItemType Directory -Force '.tools\llama.cpp' | Out-Null + Expand-Archive "$env:RUNNER_TEMP\llama-runtime.zip" '.tools\llama.cpp' -Force + $server = Get-ChildItem '.tools\llama.cpp' -Filter 'llama-server.exe' -Recurse | Select-Object -First 1 + if (-not $server) { throw 'llama-server.exe was not found in the downloaded archive.' } + "DEEPLOCAL_LLAMA_SERVER=$($server.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Package Linux + if: runner.os == 'Linux' + shell: bash + run: bash scripts/package-linux-app.sh + + - name: Package Windows + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/package-windows-app.ps1 + + - name: Upload installers + uses: actions/upload-artifact@v4 + with: + name: deeplocal-${{ runner.os }}-${{ runner.arch }} + path: | + dist/*.deb + dist/*.AppImage + dist/*.exe + release-version.txt + if-no-files-found: error + retention-days: 14 + | sort -V | tail -n 1 || true)" + if [[ -z "$version" ]]; then version=v0.1.0; else version="$(awk -F. '{printf "v%d.%d.%d", substr($1,2),$2,$3+1}' <<< "$version")"; fi + echo "DEEPLOCAL_RELEASE_VERSION=$version" >> "$GITHUB_ENV" + echo "$version" > release-version.txt - uses: actions/setup-node@v4 with: node-version: "22" diff --git a/.github/workflows/publish-desktop-release.yml b/.github/workflows/publish-desktop-release.yml index 5744a5d..da027f3 100644 --- a/.github/workflows/publish-desktop-release.yml +++ b/.github/workflows/publish-desktop-release.yml @@ -16,7 +16,7 @@ jobs: name: Publish Windows and Linux installers runs-on: ubuntu-latest steps: - - name: Resolve matching build and release + - name: Select latest successful packaging run id: resolve env: GH_TOKEN: ${{ github.token }} @@ -24,33 +24,12 @@ jobs: shell: bash run: | set -euo pipefail - gh api --paginate "repos/$GH_REPO/actions/workflows/desktop-packaging.yml/runs?event=workflow_dispatch&status=success&per_page=100" --jq '.workflow_runs[] | [.id, .head_sha] | @tsv' > "$RUNNER_TEMP/build-runs.tsv" - gh release list --limit 1000 --json tagName,isDraft --jq '.[] | select(.isDraft == false) | .tagName' > "$RUNNER_TEMP/release-tags.txt" - while IFS=$'\t' read -r run_id run_sha; do - [[ -n "$run_id" ]] || continue - while IFS= read -r tag; do - [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]] || continue - ref_json="$(gh api "repos/$GH_REPO/git/ref/tags/$tag")" - tag_sha="$(jq -r '.object.sha' <<< "$ref_json")" - if [[ "$(jq -r '.object.type' <<< "$ref_json")" == "tag" ]]; then - tag_sha="$(gh api "repos/$GH_REPO/git/tags/$tag_sha" --jq '.object.sha')" - fi - if [[ "$run_sha" == "$tag_sha" ]]; then - echo "run_id=$run_id" >> "$GITHUB_OUTPUT" - echo "release_tag=$tag" >> "$GITHUB_OUTPUT" - echo "Selected build $run_id for release $tag ($run_sha)" >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - done < "$RUNNER_TEMP/release-tags.txt" - done < "$RUNNER_TEMP/build-runs.tsv" - echo "::error::No successful packaging run matches an existing published release tag." - echo "Successful packaging runs:" >&2 - head -n 10 "$RUNNER_TEMP/build-runs.tsv" >&2 - echo "Published release tags:" >&2 - head -n 20 "$RUNNER_TEMP/release-tags.txt" >&2 - echo "Create a release tag at the packaging run commit, or run Desktop packaging on the commit of an existing release tag." >&2 - echo "The publishing workflow will not silently attach binaries built from a different commit." >&2 - exit 1 + gh api "repos/$GH_REPO/actions/workflows/desktop-packaging.yml/runs?event=workflow_dispatch&status=success&per_page=100" > "$RUNNER_TEMP/runs.json" + run_id="$(jq -r '.workflow_runs[0].id // empty' "$RUNNER_TEMP/runs.json")" + run_sha="$(jq -r '.workflow_runs[0].head_sha // empty' "$RUNNER_TEMP/runs.json")" + [[ -n "$run_id" && -n "$run_sha" ]] || { echo "No successful packaging run found." >&2; exit 1; } + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + echo "run_sha=$run_sha" >> "$GITHUB_OUTPUT" - name: Download packaging artifacts env: @@ -63,6 +42,31 @@ jobs: mkdir -p "$RUNNER_TEMP/installers" gh run download "$RUN_ID" --repo "$GH_REPO" --dir "$RUNNER_TEMP/installers" + - name: Resolve packaged version + id: version + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_SHA: ${{ steps.resolve.outputs.run_sha }} + shell: bash + run: | + set -euo pipefail + mapfile -t manifests < <(find "$RUNNER_TEMP/installers" -name release-version.txt -type f) + [[ "${#manifests[@]}" == 2 ]] || { echo "Expected two version manifests." >&2; exit 1; } + version="$(cat "${manifests[0]}")" + [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1 + [[ "$(cat "${manifests[1]}")" == "$version" ]] || { echo "Package versions differ." >&2; exit 1; } + if gh release view "$version" >/dev/null 2>&1; then + echo "Release $version already exists. Refusing to overwrite it." >&2 + exit 1 + fi + if gh api "repos/$GH_REPO/git/ref/tags/$version" >/dev/null 2>&1; then + echo "Tag $version already exists. Refusing to overwrite it." >&2 + exit 1 + fi + echo "release_tag=$version" >> "$GITHUB_OUTPUT" + echo "Publishing $version from commit $RUN_SHA" >> "$GITHUB_STEP_SUMMARY" + - name: Validate installer files shell: bash run: | @@ -86,12 +90,12 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }} + RELEASE_TAG: ${{ steps.version.outputs.release_tag }} shell: bash run: | set -euo pipefail shopt -s nullglob files=("$RUNNER_TEMP"/upload/*) (("${#files[@]}" > 0)) || exit 1 - gh release upload "$RELEASE_TAG" "${files[@]}" --repo "$GH_REPO" + gh release create "$RELEASE_TAG" "${files[@]}" --repo "$GH_REPO" --target "${{ steps.resolve.outputs.run_sha }}" --title "deepLocal $RELEASE_TAG" --notes "Windows and Linux desktop installers. macOS packages can be added separately." gh release view "$RELEASE_TAG" --repo "$GH_REPO" --json url --jq '.url' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/package-linux-app.sh b/scripts/package-linux-app.sh index e9cd14e..20e8698 100644 --- a/scripts/package-linux-app.sh +++ b/scripts/package-linux-app.sh @@ -46,6 +46,7 @@ const fs = require('fs'); const [source, target] = process.argv.slice(2); const config = JSON.parse(fs.readFileSync(source, 'utf8')); config.bundle.targets = ['deb', 'appimage']; +if (process.env.DEEPLOCAL_RELEASE_VERSION) config.version = process.env.DEEPLOCAL_RELEASE_VERSION.replace(/^v/, ''); if (!process.env.TAURI_SIGNING_PRIVATE_KEY) { config.bundle.createUpdaterArtifacts = false; delete config.plugins?.updater; diff --git a/scripts/package-windows-app.ps1 b/scripts/package-windows-app.ps1 index de2c1c0..649af6a 100644 --- a/scripts/package-windows-app.ps1 +++ b/scripts/package-windows-app.ps1 @@ -27,6 +27,7 @@ $configPath = Join-Path ([IO.Path]::GetTempPath()) ("deeplocal-tauri-" + [guid]: try { $config = Get-Content (Join-Path $desktop 'src-tauri\tauri.conf.json') -Raw | ConvertFrom-Json $config.bundle.targets = @('nsis') + if ($env:DEEPLOCAL_RELEASE_VERSION) { $config.version = $env:DEEPLOCAL_RELEASE_VERSION.TrimStart('v') } if (-not $env:TAURI_SIGNING_PRIVATE_KEY) { $config.bundle.createUpdaterArtifacts = $false $config.plugins.PSObject.Properties.Remove('updater') diff --git a/scripts/upload-new-app.sh b/scripts/upload-new-app.sh index 98955fa..aca3b84 100755 --- a/scripts/upload-new-app.sh +++ b/scripts/upload-new-app.sh @@ -136,7 +136,24 @@ cat > "$LATEST_JSON" </dev/null 2>&1; then + RELEASE_COMMIT="$(git rev-list -n 1 "$RELEASE_VERSION" 2>/dev/null || true)" + if [[ -z "$RELEASE_COMMIT" ]]; then + git fetch origin "refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION" + RELEASE_COMMIT="$(git rev-list -n 1 "$RELEASE_VERSION")" + fi + if [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then + echo "The local commit does not match the release tag. Check out the release tag before uploading macOS assets." >&2 + exit 1 + fi + gh release upload "$RELEASE_VERSION" \ + dist/deepLocal-macos.zip \ + dist/deepLocal-macos.dmg \ + "$UPDATER_ARCHIVE" \ + "$UPDATER_SIGNATURE" \ + "$LATEST_JSON" +else + gh release create "$RELEASE_VERSION" \ dist/deepLocal-macos.zip \ dist/deepLocal-macos.dmg \ "$UPDATER_ARCHIVE" \ @@ -162,6 +179,8 @@ SHA256: - deepLocal-macos.zip: $(shasum -a 256 dist/deepLocal-macos.zip | awk '{print $1}') - deepLocal-macos.dmg: $(shasum -a 256 dist/deepLocal-macos.dmg | awk '{print $1}')" +fi + echo "" echo "=== Release ===" gh release view "$RELEASE_VERSION"