642 lines
27 KiB
YAML
642 lines
27 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: (Windows) Stage app-local VC++ runtime
|
|
if: matrix.platform == 'windows-latest'
|
|
shell: bash
|
|
run: python3 distribution/windows/bundle-crt.py
|
|
|
|
- name: (Windows) Verify staged runtime is Microsoft-signed
|
|
if: matrix.platform == 'windows-latest'
|
|
shell: pwsh
|
|
run: |
|
|
$dlls = @(Get-ChildItem 'target/crt/*.dll')
|
|
if ($dlls.Count -eq 0) { Write-Error 'no CRT DLLs staged'; exit 1 }
|
|
foreach ($dll in $dlls) {
|
|
$sig = Get-AuthenticodeSignature $dll.FullName
|
|
Write-Host "$($dll.Name): $($sig.Status) $($sig.SignerCertificate.Subject)"
|
|
if ($sig.Status -ne 'Valid') {
|
|
Write-Error "$($dll.Name) signature is $($sig.Status), refusing to ship it"
|
|
exit 1
|
|
}
|
|
if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') {
|
|
Write-Error "$($dll.Name) is not signed by Microsoft"
|
|
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
|
|
cp target/release/.cargo-packager/deb/*/data/usr/share/applications/oneclient_app.desktop \
|
|
target/release/oneclient_app.desktop
|
|
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) Smoke-test installer layout
|
|
if: matrix.platform == 'windows-latest'
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$dest = 'C:\oneclient-smoke'
|
|
Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue
|
|
$exe = Resolve-Path '${{ steps.win_installer.outputs.path }}'
|
|
# NSIS: /S is silent, /D= sets the install dir and must be the last arg.
|
|
$p = Start-Process -FilePath $exe -ArgumentList '/S', "/D=$dest" -Wait -PassThru
|
|
if ($p.ExitCode -ne 0) { Write-Error "installer exited $($p.ExitCode)"; exit 1 }
|
|
|
|
Write-Host '--- installed layout ---'
|
|
Get-ChildItem -Recurse $dest | ForEach-Object { Write-Host $_.FullName }
|
|
|
|
if (-not (Test-Path "$dest\oneclient_app.exe")) {
|
|
Write-Error "oneclient_app.exe is not at the root of $dest"
|
|
exit 1
|
|
}
|
|
foreach ($dll in @(Get-ChildItem 'target/crt/*.dll')) {
|
|
if (-not (Test-Path (Join-Path $dest $dll.Name))) {
|
|
Write-Error "$($dll.Name) did not land beside oneclient_app.exe - cargo-packager put resources elsewhere, so app-local CRT loading will not work"
|
|
exit 1
|
|
}
|
|
}
|
|
Write-Host "OK: $($(Get-ChildItem 'target/crt/*.dll').Count) CRT DLLs sit beside oneclient_app.exe"
|
|
|
|
- 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"
|