GitComet/.github/workflows/build-release-artifacts.yml
2026-03-17 17:01:25 +02:00

483 lines
16 KiB
YAML

name: Build Release Artifacts
on:
workflow_call:
inputs:
tag:
required: true
type: string
version:
required: true
type: string
deb_revision:
required: false
type: string
default: "1"
release_id:
required: false
type: string
workflow_dispatch:
inputs:
tag:
description: "Release tag to build from (e.g. v0.2.0)"
required: true
type: string
version:
description: "Optional version override (e.g. 0.2.0). If omitted, derived from tag."
required: false
type: string
deb_revision:
description: "Debian package revision suffix appended to the upstream version (e.g. 1 for 1.2.3-1)."
required: true
default: "1"
type: string
release_id:
description: "Optional release id for manual backfills"
required: false
type: string
permissions:
contents: write
id-token: write
concurrency:
group: build-release-artifacts-${{ inputs.tag || github.event.inputs.tag || github.run_id }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
WIXTOOLSET_VERSION: 3.14.1.20250415
CARGO_WIX_VERSION: 0.3.9
APPIMAGETOOL_VERSION: 1.9.1
APPIMAGETOOL_X86_64_SHA256: ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
jobs:
prepare:
name: Normalize release metadata
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
tag: ${{ steps.norm.outputs.tag }}
version: ${{ steps.norm.outputs.version }}
deb_revision: ${{ steps.norm.outputs.deb_revision }}
release_id: ${{ steps.norm.outputs.release_id }}
steps:
- name: Normalize tag and version
id: norm
env:
INPUT_TAG: ${{ inputs.tag }}
DISPATCH_TAG: ${{ github.event.inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
DISPATCH_VERSION: ${{ github.event.inputs.version }}
INPUT_DEB_REVISION: ${{ inputs.deb_revision }}
DISPATCH_DEB_REVISION: ${{ github.event.inputs.deb_revision }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
DISPATCH_RELEASE_ID: ${{ github.event.inputs.release_id }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
deb_revision="${INPUT_DEB_REVISION:-${DISPATCH_DEB_REVISION:-1}}"
release_id="${INPUT_RELEASE_ID:-${DISPATCH_RELEASE_ID:-}}"
tag="$(echo "$tag" | tr -d '[:space:]')"
version="$(echo "$version" | tr -d '[:space:]')"
deb_revision="$(echo "$deb_revision" | tr -d '[:space:]')"
release_id="$(echo "$release_id" | tr -d '[:space:]')"
if [ -z "$tag" ]; then
echo "::error title=Missing tag::Tag is required."
exit 1
fi
if [[ "$tag" != v* ]]; then
tag="v${tag}"
fi
if [ -z "$version" ]; then
version="${tag#v}"
fi
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "::error title=Invalid version::Expected semver like 1.2.3 or 1.2.3-rc.1."
exit 1
fi
if [ "$tag" != "v${version}" ]; then
echo "::error title=Tag/version mismatch::Tag '$tag' does not match version '$version'."
exit 1
fi
if ! [[ "$deb_revision" =~ ^[1-9][0-9]*$ ]]; then
echo "::error title=Invalid Debian revision::Expected a positive integer like 1, 2, or 3."
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "deb_revision=$deb_revision" >> "$GITHUB_OUTPUT"
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
build_windows:
name: Build Windows artifacts
runs-on: windows-latest
timeout-minutes: 120
needs: prepare
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
- name: Build release binary
shell: pwsh
run: |
cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins
- name: Package portable ZIP
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Path dist\portable -Force | Out-Null
Copy-Item target\release\gitcomet.exe dist\portable\gitcomet.exe -Force
Copy-Item README.md dist\portable\README.md -Force
Copy-Item LICENSE-AGPL-3.0 dist\portable\LICENSE-AGPL-3.0 -Force
Copy-Item NOTICE dist\portable\NOTICE -Force
$zipName = "gitcomet-v${env:VERSION}-windows-x86_64-portable.zip"
if (Test-Path "dist\$zipName") { Remove-Item "dist\$zipName" -Force }
Compress-Archive -Path dist\portable\* -DestinationPath "dist\$zipName"
- name: Build MSI installer (WiX)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
choco install wixtoolset --version "${env:WIXTOOLSET_VERSION}" --yes --no-progress
cargo install cargo-wix --version "${env:CARGO_WIX_VERSION}" --locked
if (!(Test-Path "crates\gitcomet\wix\main.wxs")) {
cargo wix init --package gitcomet
}
$msiName = "gitcomet-v${env:VERSION}-windows-x86_64.msi"
cargo wix --package gitcomet --profile release --nocapture --no-build --output "dist\$msiName"
- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
with:
name: windows-release-artifacts
path: |
dist/*.zip
dist/*.msi
if-no-files-found: error
retention-days: 7
build_linux:
name: Build Linux artifacts
runs-on: ubuntu-22.04
timeout-minutes: 120
needs: prepare
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
DEB_REVISION: ${{ needs.prepare.outputs.deb_revision }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Install Linux UI native dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
desktop-file-utils \
pkg-config \
libxcb1-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
- name: Build release binary
run: cargo build -p gitcomet --release --locked --features ui-gpui,gix
- name: Normalize desktop entry metadata
run: |
set -euo pipefail
mkdir -p dist
# Backward compatibility for older tags that used non-standard categories.
sed 's/VersionControl;/RevisionControl;/g' assets/linux/gitcomet.desktop > dist/gitcomet.desktop
desktop-file-validate dist/gitcomet.desktop
- name: Package tar.gz
run: |
set -euo pipefail
root="gitcomet-v${VERSION}-linux-x86_64"
mkdir -p "dist/stage/${root}"
install -m755 target/release/gitcomet "dist/stage/${root}/gitcomet"
install -m644 README.md "dist/stage/${root}/README.md"
install -m644 LICENSE-AGPL-3.0 "dist/stage/${root}/LICENSE-AGPL-3.0"
install -m644 NOTICE "dist/stage/${root}/NOTICE"
tar -C dist/stage -czf "dist/${root}.tar.gz" "${root}"
- name: Package .deb
run: |
set -euo pipefail
deb_upstream_version="${VERSION/-rc./~rc.}"
deb_revision="${DEB_REVISION}"
deb_version="${deb_upstream_version}-${deb_revision}"
pkg_root="dist/deb-root"
mkdir -p "${pkg_root}/DEBIAN"
mkdir -p "${pkg_root}/usr/bin"
mkdir -p "${pkg_root}/usr/share/applications"
mkdir -p "${pkg_root}/usr/share/icons/hicolor/512x512/apps"
install -m755 target/release/gitcomet "${pkg_root}/usr/bin/gitcomet"
install -m644 dist/gitcomet.desktop "${pkg_root}/usr/share/applications/gitcomet.desktop"
install -m644 assets/gitcomet-512.png "${pkg_root}/usr/share/icons/hicolor/512x512/apps/gitcomet.png"
{
echo "Package: gitcomet"
echo "Version: ${deb_version}"
echo "Architecture: amd64"
echo "Maintainer: AutoExplore Oy <info@autoexplore.ai>"
echo "Depends: git"
echo "Section: utils"
echo "Priority: optional"
echo "Description: Fast, resource-efficient Git GUI written in Rust."
} > "${pkg_root}/DEBIAN/control"
dpkg-deb --build "${pkg_root}" "dist/gitcomet_${deb_version}_amd64.deb"
- name: Package AppImage
run: |
set -euo pipefail
appdir="dist/AppDir"
mkdir -p "${appdir}/usr/bin"
install -m755 target/release/gitcomet "${appdir}/usr/bin/gitcomet"
install -m644 dist/gitcomet.desktop "${appdir}/gitcomet.desktop"
install -m644 assets/gitcomet-512.png "${appdir}/gitcomet.png"
{
echo '#!/usr/bin/env bash'
echo 'set -euo pipefail'
echo 'HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"'
echo 'exec "${HERE}/usr/bin/gitcomet" "$@"'
} > "${appdir}/AppRun"
chmod +x "${appdir}/AppRun"
curl -fsSL \
"https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage" \
-o /tmp/appimagetool.AppImage
echo "${APPIMAGETOOL_X86_64_SHA256} /tmp/appimagetool.AppImage" | sha256sum -c -
chmod +x /tmp/appimagetool.AppImage
ARCH=x86_64 /tmp/appimagetool.AppImage --appimage-extract-and-run \
"${appdir}" "dist/gitcomet-v${VERSION}-linux-x86_64.AppImage"
- name: Upload Linux artifacts
uses: actions/upload-artifact@v7
with:
name: linux-release-artifacts
path: |
dist/*.tar.gz
dist/*.AppImage
dist/*.deb
if-no-files-found: error
retention-days: 7
build_macos:
name: Build macOS artifacts (${{ matrix.arch }})
runs-on: ${{ matrix.runs_on }}
timeout-minutes: 120
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runs_on: macos-latest
- arch: x86_64
runs_on: macos-15-intel
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
ARCH: ${{ matrix.arch }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
- name: Package macOS release assets
run: |
set -euo pipefail
scripts/package-macos.sh \
--version "${VERSION}" \
--arch "${ARCH}" \
--release \
--out-dir dist
- name: Upload macOS artifacts
uses: actions/upload-artifact@v7
with:
name: macos-release-artifacts-${{ matrix.arch }}
path: |
dist/*.tar.gz
dist/*.dmg
if-no-files-found: error
retention-days: 7
publish_release_assets:
name: Attach assets and checksums to GitHub release
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [prepare, build_windows, build_linux, build_macos]
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
ENABLE_KEYLESS_SIGNING: ${{ vars.ENABLE_KEYLESS_SIGNING }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
name: windows-release-artifacts
path: dist/windows
- uses: actions/download-artifact@v8
with:
name: linux-release-artifacts
path: dist/linux
- uses: actions/download-artifact@v8
with:
pattern: macos-release-artifacts-*
path: dist/macos
merge-multiple: true
- name: Assemble release payload
run: |
set -euo pipefail
mkdir -p dist/release
find dist/windows -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
find dist/linux -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
find dist/macos -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
file_count="$(find dist/release -maxdepth 1 -type f | wc -l | tr -d '[:space:]')"
if [ "${file_count}" = "0" ]; then
echo "::error title=No release artifacts::No files were assembled into dist/release."
exit 1
fi
ls -lah dist/release
- name: Generate Homebrew formula
run: |
set -euo pipefail
arm_tar="gitcomet-v${VERSION}-macos-arm64.tar.gz"
intel_tar="gitcomet-v${VERSION}-macos-x86_64.tar.gz"
linux_tar="gitcomet-v${VERSION}-linux-x86_64.tar.gz"
arm_path="dist/release/${arm_tar}"
intel_path="dist/release/${intel_tar}"
linux_path="dist/release/${linux_tar}"
if [ ! -f "${arm_path}" ] || [ ! -f "${intel_path}" ] || [ ! -f "${linux_path}" ]; then
echo "::error title=Missing release tarballs::Expected ${arm_tar}, ${intel_tar}, and ${linux_tar} in dist/release."
exit 1
fi
scripts/generate-homebrew-formula.sh \
--version "${VERSION}" \
--github-repo "${GITHUB_REPOSITORY}" \
--arm-tar "${arm_path}" \
--intel-tar "${intel_path}" \
--linux-tar "${linux_path}" \
--output "dist/release/gitcomet.rb"
- name: Generate checksums
run: |
set -euo pipefail
pushd dist/release >/dev/null
md5_file="gitcomet-v${VERSION}-checksums.md5"
sha_file="gitcomet-v${VERSION}-checksums.sha256"
find . -maxdepth 1 -type f \
! -name "*checksums*" \
-printf "%P\n" \
| LC_ALL=C sort > /tmp/release_files.txt
xargs -I{} md5sum "{}" < /tmp/release_files.txt > "$md5_file"
xargs -I{} sha256sum "{}" < /tmp/release_files.txt > "$sha_file"
popd >/dev/null
- name: Install cosign
if: ${{ env.ENABLE_KEYLESS_SIGNING == 'true' }}
uses: sigstore/cosign-installer@v3
- name: Sign SHA256 checksums with keyless signing
if: ${{ env.ENABLE_KEYLESS_SIGNING == 'true' }}
run: |
set -euo pipefail
sha_file="dist/release/gitcomet-v${VERSION}-checksums.sha256"
sig_file="dist/release/gitcomet-v${VERSION}-checksums.sha256.sig"
cert_file="dist/release/gitcomet-v${VERSION}-checksums.sha256.pem"
cosign sign-blob --yes \
--output-signature "$sig_file" \
--output-certificate "$cert_file" \
"$sha_file"
- name: Upload payload artifact
uses: actions/upload-artifact@v7
with:
name: release-payload
path: dist/release/*
if-no-files-found: error
retention-days: 7
- name: Upload assets to GitHub release
if: ${{ env.RELEASE_ID != '' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
gh release upload "$TAG" dist/release/* --clobber --repo "$GITHUB_REPOSITORY"
- name: Emit release summary
run: |
set -euo pipefail
uploaded="no"
if [ -n "${RELEASE_ID}" ]; then
uploaded="yes"
fi
{
echo "### Built release artifacts"
echo ""
echo "- Tag: \`$TAG\`"
echo "- Version: \`$VERSION\`"
echo "- Release upload attempted: \`${uploaded}\`"
echo ""
echo "Files:"
ls -1 dist/release | sed 's/^/- `/;s/$/`/'
} >> "$GITHUB_STEP_SUMMARY"