367 lines
15 KiB
YAML
367 lines
15 KiB
YAML
name: OneClient Release Build
|
|
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY }}
|
|
CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD }}
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
version: ${{ steps.version.outputs.version }}
|
|
tag: ${{ steps.version.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Resolve version from Cargo.toml
|
|
id: version
|
|
shell: bash
|
|
run: |
|
|
VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "tag=oneclient-$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create/ensure draft release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ steps.version.outputs.tag }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
|
gh release create "$TAG" \
|
|
--draft \
|
|
--title "OneClient $VERSION" \
|
|
--notes "Automated release for OneClient $VERSION."
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
package:
|
|
needs: prepare
|
|
permissions:
|
|
contents: write
|
|
# Required so SignPath can download the uploaded build artifact.
|
|
actions: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: macos-latest
|
|
target: aarch64-apple-darwin
|
|
os_key: darwin
|
|
arch: aarch64
|
|
formats: app,dmg
|
|
- platform: macos-latest
|
|
target: x86_64-apple-darwin
|
|
os_key: darwin
|
|
arch: x86_64
|
|
formats: app,dmg
|
|
- platform: ubuntu-22.04
|
|
target: ''
|
|
os_key: linux
|
|
arch: x86_64
|
|
formats: deb,appimage
|
|
- platform: windows-latest
|
|
target: ''
|
|
os_key: windows
|
|
arch: x86_64
|
|
formats: nsis
|
|
|
|
runs-on: ${{ matrix.platform }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Rust
|
|
uses: ./.github/actions/setup-rust
|
|
with:
|
|
target: ${{ matrix.target }}
|
|
save-cache: true
|
|
restore-cache: true
|
|
|
|
- name: (Linux) Install system dependencies
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
build-essential cmake pkg-config clang python3 \
|
|
libfontconfig-dev libfreetype-dev \
|
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
|
libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \
|
|
libfuse2
|
|
|
|
- name: Install cargo-packager
|
|
shell: bash
|
|
run: cargo install cargo-packager --version 0.11.8 --locked
|
|
|
|
- name: Import Apple Developer certificate
|
|
if: startsWith(matrix.platform, 'macos')
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
run: |
|
|
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
|
security default-keychain -s build.keychain
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
|
security set-keychain-settings -t 3600 -u build.keychain
|
|
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
|
CERT_ID=$(security find-identity -v -p codesigning build.keychain | awk -F'"' '/Developer ID Application/ {print $2; exit}')
|
|
if [ -z "$CERT_ID" ]; then
|
|
echo "::error::No Developer ID Application certificate found in keychain."
|
|
exit 1
|
|
fi
|
|
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV"
|
|
# cargo-packager only codesigns when `signing-identity` is present in the
|
|
# packager config; it does NOT read APPLE_SIGNING_IDENTITY. Inject it into
|
|
# the macOS metadata so signing + notarization actually run.
|
|
CERT_ID="$CERT_ID" python3 - <<'PY'
|
|
import os, re
|
|
p = "packages/oneclient_app/Cargo.toml"
|
|
s = open(p).read()
|
|
ident = os.environ["CERT_ID"]
|
|
hdr = "[package.metadata.packager.macos]"
|
|
assert hdr in s, "macos packager section not found"
|
|
if "signing-identity" not in s:
|
|
s = s.replace(hdr, f'{hdr}\nsigning-identity = "{ident}"', 1)
|
|
open(p, "w").write(s)
|
|
print(f"signing-identity set to: {ident}")
|
|
PY
|
|
|
|
- name: Build release binary
|
|
shell: bash
|
|
run: |
|
|
if [ -n "${{ matrix.target }}" ]; then
|
|
cargo build --release --bin oneclient_app --target ${{ matrix.target }}
|
|
else
|
|
cargo build --release --bin oneclient_app
|
|
fi
|
|
|
|
- name: Package
|
|
shell: bash
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
CI: ${{ startsWith(matrix.platform, 'macos') && 'false' || 'true' }}
|
|
run: |
|
|
ARGS="--release --formats ${{ matrix.formats }}"
|
|
if [ -n "${{ matrix.target }}" ]; then
|
|
ARGS="$ARGS --target ${{ matrix.target }}"
|
|
fi
|
|
cargo packager $ARGS --verbose
|
|
|
|
- name: Resolve packager output dir
|
|
id: outdir
|
|
shell: bash
|
|
run: |
|
|
if [ -n "${{ matrix.target }}" ]; then
|
|
echo "dir=target/${{ matrix.target }}/release" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "dir=target/release" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: (Linux) Build RPM
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
cargo install cargo-generate-rpm --version 0.21.0 --locked
|
|
cargo generate-rpm -p packages/oneclient_app
|
|
cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/"
|
|
|
|
# Upload debug info (dSYM / .dwp / .pdb produced by the release profile's
|
|
# split-debuginfo) to Sentry so crash reports get real file/function/line
|
|
# stack traces instead of raw addresses. Skips silently unless the repo has
|
|
# a SENTRY_AUTH_TOKEN secret plus SENTRY_ORG / SENTRY_PROJECT variables.
|
|
- name: Upload debug symbols to Sentry
|
|
shell: bash
|
|
env:
|
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
OUT_DIR: ${{ steps.outdir.outputs.dir }}
|
|
run: |
|
|
if [ -z "${SENTRY_AUTH_TOKEN:-}" ] || [ -z "${SENTRY_ORG:-}" ] || [ -z "${SENTRY_PROJECT:-}" ]; then
|
|
echo "Sentry credentials not configured (need SENTRY_AUTH_TOKEN secret + SENTRY_ORG/SENTRY_PROJECT vars); skipping debug-symbol upload."
|
|
exit 0
|
|
fi
|
|
curl -sL https://sentry.io/get-cli/ | bash
|
|
sentry-cli debug-files upload --include-sources "$OUT_DIR"
|
|
|
|
- name: (Windows) Locate installer
|
|
if: matrix.platform == 'windows-latest'
|
|
id: win_installer
|
|
shell: bash
|
|
run: echo "path=$(ls ${{ steps.outdir.outputs.dir }}/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: (Windows) Upload unsigned installer artifact
|
|
if: matrix.platform == 'windows-latest'
|
|
id: win_upload_unsigned
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: oneclient-windows-installer-unsigned
|
|
path: ${{ steps.win_installer.outputs.path }}
|
|
|
|
- name: (Windows) Submit SignPath signing request
|
|
if: matrix.platform == 'windows-latest'
|
|
uses: signpath/github-action-submit-signing-request@v1
|
|
with:
|
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
|
project-slug: onelauncher
|
|
signing-policy-slug: release-signing
|
|
artifact-configuration-slug: onelauncher-installer
|
|
github-artifact-id: ${{ steps.win_upload_unsigned.outputs.artifact-id }}
|
|
wait-for-completion: true
|
|
output-artifact-directory: signed
|
|
|
|
- name: (Windows) Replace installer with signed one + recompute updater sig
|
|
if: matrix.platform == 'windows-latest'
|
|
shell: bash
|
|
run: |
|
|
signed="$(ls signed/*-setup.exe | head -n1)"
|
|
# Overwrite the unsigned installer in the packager out dir with the signed exe.
|
|
cp "$signed" "${{ steps.win_installer.outputs.path }}"
|
|
# Recompute the updater signature over the SIGNED exe.
|
|
cargo packager signer sign \
|
|
--private-key "$CARGO_PACKAGER_SIGN_PRIVATE_KEY" \
|
|
--password "$CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD" \
|
|
"${{ steps.win_installer.outputs.path }}"
|
|
|
|
- name: Upload installers and build manifest fragment
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ needs.prepare.outputs.tag }}
|
|
OUT_DIR: ${{ steps.outdir.outputs.dir }}
|
|
OS_KEY: ${{ matrix.os_key }}
|
|
ARCH: ${{ matrix.arch }}
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
|
|
# Identify the single updater artifact for this platform (the file with a
|
|
# sibling .sig): -setup.exe (nsis), *.app.tar.gz (app), *.AppImage (appimage).
|
|
# NOTE: nullglob is on, so an unmatched glob expands to nothing. Use arrays
|
|
# rather than `ls <glob> | head` — the latter falls back to listing the cwd
|
|
# when nothing matches, silently picking an unrelated file.
|
|
fmt=""; ext=""; matches=()
|
|
case "$OS_KEY" in
|
|
windows) matches=("$OUT_DIR"/*-setup.exe); fmt="nsis"; ext="exe" ;;
|
|
darwin) matches=("$OUT_DIR"/*.app.tar.gz); fmt="app"; ext="app.tar.gz" ;;
|
|
linux) matches=("$OUT_DIR"/*.AppImage); fmt="appimage"; ext="AppImage" ;;
|
|
esac
|
|
updater_file="${matches[0]:-}"
|
|
if [ -z "$updater_file" ]; then
|
|
echo "::error::No updater artifact found in $OUT_DIR for $OS_KEY"
|
|
exit 1
|
|
fi
|
|
|
|
# The updater bundle isn't always arch-tagged by cargo-packager, so the two
|
|
# macOS legs would collide on the release. Rename the updater artifact + its .sig
|
|
# to an arch-unique name before upload, and point the manifest at that name.
|
|
unique="OneClient_${VERSION}_${OS_KEY}_${ARCH}.${ext}"
|
|
cp "$updater_file" "$OUT_DIR/$unique"
|
|
cp "$updater_file.sig" "$OUT_DIR/$unique.sig"
|
|
|
|
# cargo-packager names the .deb after the binary (oneclient_app_<ver>_amd64.deb).
|
|
# Rename it to the same OneClient_<ver>_<os>_<arch> scheme as the other assets so
|
|
# the AUR oneclient-bin package can point at a stable, predictable URL.
|
|
deb_unique=""
|
|
deb_matches=("$OUT_DIR"/*.deb)
|
|
if [ -e "${deb_matches[0]:-}" ]; then
|
|
deb_unique="OneClient_${VERSION}_${OS_KEY}_${ARCH}.deb"
|
|
cp "${deb_matches[0]}" "$OUT_DIR/$deb_unique"
|
|
fi
|
|
|
|
# Same treatment for the .rpm (cargo-generate-rpm names it
|
|
# oneclient_app-<ver>-1.x86_64.rpm).
|
|
rpm_unique=""
|
|
rpm_matches=("$OUT_DIR"/*.rpm)
|
|
if [ -e "${rpm_matches[0]:-}" ]; then
|
|
rpm_unique="OneClient_${VERSION}_${OS_KEY}_${ARCH}.rpm"
|
|
cp "${rpm_matches[0]}" "$OUT_DIR/$rpm_unique"
|
|
fi
|
|
|
|
# Upload human-facing installers (dmg/renamed deb/renamed rpm/AppImage/signed exe) +
|
|
# the renamed updater bundle and its .sig. Skip the un-renamed updater bundle to avoid clashes.
|
|
for f in "$OUT_DIR"/*.dmg ${deb_unique:+"$OUT_DIR/$deb_unique"} ${rpm_unique:+"$OUT_DIR/$rpm_unique"} "$OUT_DIR"/*.AppImage "$OUT_DIR"/*-setup.exe "$OUT_DIR/$unique" "$OUT_DIR/$unique.sig"; do
|
|
[ -e "$f" ] || continue
|
|
gh release upload "$TAG" "$f" --clobber
|
|
done
|
|
|
|
base="$unique"
|
|
mkdir -p fragments
|
|
case "$OS_KEY" in
|
|
darwin) KEYS="darwin-${ARCH} macos-${ARCH}" ;;
|
|
*) KEYS="${OS_KEY}-${ARCH}" ;;
|
|
esac
|
|
SIG="$(cat "$updater_file.sig")" \
|
|
URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${base}" \
|
|
FMT="$fmt" \
|
|
KEYS="$KEYS" \
|
|
FRAG_NAME="${OS_KEY}-${ARCH}" \
|
|
python3 - <<'PY'
|
|
import json, os
|
|
entry = {
|
|
"signature": os.environ["SIG"],
|
|
"url": os.environ["URL"],
|
|
"format": os.environ["FMT"],
|
|
}
|
|
frag = {key: entry for key in os.environ["KEYS"].split()}
|
|
with open(f"fragments/{os.environ['FRAG_NAME']}.json", "w") as f:
|
|
json.dump(frag, f)
|
|
PY
|
|
|
|
- name: Upload manifest fragment
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: manifest-${{ matrix.os_key }}-${{ matrix.arch }}
|
|
path: fragments/*.json
|
|
|
|
manifest:
|
|
needs: [prepare, package]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Download all manifest fragments
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: manifest-*
|
|
path: fragments
|
|
merge-multiple: true
|
|
|
|
- name: Assemble latest.json
|
|
env:
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
python3 - <<'PY'
|
|
import json, glob, os, datetime
|
|
platforms = {}
|
|
for path in glob.glob("fragments/*.json"):
|
|
with open(path) as f:
|
|
platforms.update(json.load(f))
|
|
manifest = {
|
|
"version": os.environ["VERSION"],
|
|
"pub_date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"platforms": platforms,
|
|
}
|
|
with open("latest.json", "w") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
print(json.dumps(manifest, indent=2))
|
|
PY
|
|
|
|
- name: Upload latest.json to release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ needs.prepare.outputs.tag }}
|
|
run: gh release upload "$TAG" latest.json --clobber --repo "$GITHUB_REPOSITORY"
|