OneLauncher-Cracked/.github/workflows/oneclient_release.yml
2026-08-07 14:55:55 +07:00

589 lines
24 KiB
YAML

name: OneClient Release Build
on:
workflow_dispatch:
inputs:
bump:
description: 'Bump the workspace version in Cargo.toml before releasing'
type: choice
required: false
default: none
options:
- none
- patch
- minor
- major
# NOTE: CARGO_PACKAGER_SIGN_PRIVATE_KEY* are deliberately NOT declared at
# workflow level. Swatinem/rust-cache folds every `CARGO*`-prefixed environment
# variable into its cache key, so a workflow-level declaration gave this workflow
# a cache namespace that the nightly workflow could never populate — every
# release built the whole dependency graph from scratch. They are scoped to the
# individual steps that actually need them instead.
concurrency:
group: oneclient-release
cancel-in-progress: false
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
sha: ${{ steps.version.outputs.sha }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Setup Rust
if: inputs.bump != 'none'
uses: ./.github/actions/setup-rust
with:
restore-cache: false
- name: Resolve version, bump if requested, refuse to clobber a release
id: version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUMP: ${{ inputs.bump }}
shell: bash
run: |
set -euo pipefail
read_version() {
sed -n -E '/^\[workspace\.package\]/,/^\[/ s/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n1
}
VERSION="$(read_version)"
if [ -z "$VERSION" ]; then
echo "::error::Could not read version from [workspace.package] in Cargo.toml"
exit 1
fi
echo "Current version: $VERSION"
if [ "$BUMP" != "none" ]; then
if ! [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::Version '$VERSION' is not plain semver major.minor.patch; cannot bump automatically."
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"; MINOR="${BASH_REMATCH[2]}"; PATCH="${BASH_REMATCH[3]}"
case "$BUMP" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "Bumping ($BUMP): $VERSION -> $NEW_VERSION"
# Only rewrite the `version` key inside [workspace.package]; the
# dependency pins further down the file must not be touched.
NEW_VERSION="$NEW_VERSION" python3 - <<'PY'
import os, re
new = os.environ["NEW_VERSION"]
src = open("Cargo.toml").read()
start = src.index("[workspace.package]")
end = src.find("\n[", start + 1)
end = len(src) if end == -1 else end
section, count = re.subn(
r'(?m)^version\s*=\s*"[^"]+"', f'version = "{new}"', src[start:end], count=1
)
assert count == 1, "no version key found in [workspace.package]"
open("Cargo.toml", "w").write(src[:start] + section + src[end:])
PY
# Workspace members inherit `version.workspace = true`, so only the
# lockfile's own entries for those members need refreshing.
cargo update --workspace --offline || cargo update --workspace
ACTUAL="$(read_version)"
if [ "$ACTUAL" != "$NEW_VERSION" ]; then
echo "::error::Version rewrite failed (Cargo.toml still reports '$ACTUAL')"
exit 1
fi
VERSION="$NEW_VERSION"
fi
TAG="oneclient-$VERSION"
# Abort rather than clobber: a published release (or an existing tag,
# which only appears once a release is published) means this version
# already shipped. A *draft* is a previous failed/partial run of this
# same workflow, so reuse it.
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists on origin — OneClient $VERSION has already been released. Re-run with a bump, or bump the version in Cargo.toml."
exit 1
fi
RELEASE_EXISTS=0
IS_DRAFT=""
if IS_DRAFT="$(gh release view "$TAG" --json isDraft -q .isDraft 2>/dev/null)"; then
RELEASE_EXISTS=1
fi
if [ "$RELEASE_EXISTS" = "1" ] && [ "$IS_DRAFT" != "true" ]; then
echo "::error::Release $TAG already exists and is published — refusing to overwrite OneClient $VERSION. Re-run with a bump, or bump the version in Cargo.toml."
exit 1
fi
# Commit the bump only after the guards pass, so a rejected release
# does not leave a stray version bump on the branch.
if [ "$BUMP" != "none" ]; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -m "chore: bump to $VERSION"
git push origin "HEAD:${GITHUB_REF_NAME}"
fi
SHA="$(git rev-parse HEAD)"
if [ "$RELEASE_EXISTS" = "1" ]; then
echo "Reusing existing draft release $TAG"
gh release edit "$TAG" --target "$SHA"
else
gh release create "$TAG" \
--draft \
--target "$SHA" \
--title "OneClient $VERSION" \
--notes "Automated release for OneClient $VERSION."
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
# ---------------------------------------------------------------------------
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
macos_deployment_target: '10.14'
- 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:
# Pin to the commit `prepare` resolved — with `bump` that is the version
# commit it just pushed, which github.sha would not point at.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: ${{ needs.prepare.outputs.sha }}
- name: (macOS) Pin deployment target
if: matrix.macos_deployment_target != ''
shell: bash
run: echo "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_deployment_target }}" >> "$GITHUB_ENV"
- 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
uses: ./.github/actions/install-cargo-tool
with:
tool: cargo-packager
version: 0.11.8
- 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: (macOS) Verify deployment target
if: matrix.macos_deployment_target != ''
shell: bash
run: |
set -euo pipefail
bin="target/${{ matrix.target }}/release/oneclient_app"
minos="$(vtool -show-build "$bin" | awk '/minos/ {print $2; exit}')"
echo "linked minos=$minos, declared minimum=${{ matrix.macos_deployment_target }}"
if [ "$(printf '%s\n%s\n' "$minos" "${{ matrix.macos_deployment_target }}" | sort -V | tail -n1)" != "${{ matrix.macos_deployment_target }}" ]; then
echo "::error::Binary requires macOS $minos but the bundle advertises ${{ matrix.macos_deployment_target }}."
exit 1
fi
- name: (Windows) Upload unsigned app binary artifact
if: matrix.platform == 'windows-latest'
id: win_upload_unsigned_bin
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: oneclient-windows-binary-unsigned
path: target/release/oneclient_app.exe
- name: (Windows) Submit SignPath signing request for app binary
if: matrix.platform == 'windows-latest'
uses: signpath/github-action-submit-signing-request@ced31329c0317e779dad2eec2a7c3bb46ea1343e # v1.3
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-binary
github-artifact-id: ${{ steps.win_upload_unsigned_bin.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: signed-bin
- name: (Windows) Swap in signed app binary
if: matrix.platform == 'windows-latest'
shell: bash
run: |
set -euo pipefail
signed="signed-bin/oneclient_app.exe"
if [ ! -e "$signed" ]; then
echo "::error::SignPath did not return signed-bin/oneclient_app.exe"
ls -R signed-bin || true
exit 1
fi
cp "$signed" target/release/oneclient_app.exe
- name: (Windows) Verify app binary is signed
if: matrix.platform == 'windows-latest'
shell: pwsh
run: |
$sig = Get-AuthenticodeSignature 'target/release/oneclient_app.exe'
Write-Host "Authenticode status: $($sig.Status)"
Write-Host "Signer: $($sig.SignerCertificate.Subject)"
if ($sig.Status -eq 'NotSigned') {
Write-Error 'oneclient_app.exe carries no Authenticode signature'
exit 1
}
- name: (macOS) Compile app icon catalog
if: startsWith(matrix.platform, 'macos')
shell: bash
run: |
set -euo pipefail
out="packages/oneclient_app/icons/generated"
mkdir -p "$out"
xcrun actool packages/oneclient_app/icons/OneClient.icon \
--compile "$out" \
--app-icon OneClient \
--output-partial-info-plist "$out/partial.plist" \
--platform macosx \
--minimum-deployment-target 26.0 \
--errors --warnings --notices
rm -f "$out/OneClient.icns"
test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; }
- 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' }}
CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY }}
CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD }}
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) Install cargo-generate-rpm
if: matrix.platform == 'ubuntu-22.04'
uses: ./.github/actions/install-cargo-tool
with:
tool: cargo-generate-rpm
version: 0.21.0
- name: (Linux) Build RPM
if: matrix.platform == 'ubuntu-22.04'
shell: bash
run: |
set -euo pipefail
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: |
set -euo pipefail
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
export PATH="/usr/local/bin:$PATH"
SYMS="$(mktemp -d)"
shopt -s nullglob
for f in "$OUT_DIR"/oneclient_app "$OUT_DIR"/oneclient_app.exe \
"$OUT_DIR"/oneclient_app.pdb "$OUT_DIR"/*.dwp "$OUT_DIR"/*.dSYM; do
[ -e "$f" ] || continue
cp -R "$f" "$SYMS/"
done
sentry-cli debug-files upload --include-sources "$SYMS"
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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@ced31329c0317e779dad2eec2a7c3bb46ea1343e # v1.3
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
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 }}
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
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"