name: CI
on:
push:
tags: ['v*']
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
LZMA_API_STATIC: 1
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
label: linux-x86_64
lint: true
- os: ubuntu-24.04
target: aarch64-unknown-linux-gnu
label: linux-aarch64
cross: true
- os: macos-latest
target: aarch64-apple-darwin
label: macos-aarch64
- os: macos-latest
target: x86_64-apple-darwin
label: macos-x86_64
- os: windows-latest
target: x86_64-pc-windows-msvc
label: windows-x86_64
runs-on: ${{ matrix.os }}
name: ${{ matrix.label }}
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 with:
toolchain: stable
targets: ${{ matrix.target }}
components: ${{ matrix.lint && 'rustfmt, clippy' || '' }}
- name: Check tag matches Cargo.toml version
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
tag="${tag%%-*}"
py=$(command -v python3 || command -v python)
crate=$(cargo metadata --no-deps --format-version 1 \
| "$py" -c 'import json,sys; pkgs=json.load(sys.stdin)["packages"]; print(next(p["version"] for p in pkgs if p["name"]=="pinready"))')
if [ "$tag" != "$crate" ]; then
echo "::error::Tag $GITHUB_REF_NAME (version $tag) != version Cargo.toml ($crate) — aligne Cargo.toml ou retague."
exit 1
fi
echo "Tag $GITHUB_REF_NAME <-> Cargo.toml $crate : OK"
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 with:
key: ${{ matrix.os }}-${{ matrix.target }}
- name: Install dependencies (Linux native)
if: runner.os == 'Linux' && !matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libxkbcommon-dev libssl-dev \
libasound2-dev libpulse-dev \
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev \
libxtst-dev libxss-dev libwayland-dev wayland-protocols \
libdbus-1-dev libdrm-dev libgbm-dev \
libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev \
libudev-dev
- name: Install cross
if: matrix.cross
run: cargo install cross --locked --version 0.2.5
- name: Check formatting
if: matrix.lint
run: cargo fmt --check
- name: Clippy
if: matrix.lint
run: cargo clippy --release --all-targets --all-features --target ${{ matrix.target }} -- -D warnings
- name: Set macOS deployment target
if: runner.os == 'macOS'
run: |
echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
echo "CFLAGS=-mmacosx-version-min=11.0" >> $GITHUB_ENV
echo "CXXFLAGS=-mmacosx-version-min=11.0" >> $GITHUB_ENV
CLANG_RT_DIR=$(clang --print-search-dirs | grep libraries | sed 's/libraries: =//;s/:.*//')
CLANG_RT_LIB=$(find "$CLANG_RT_DIR" -name 'libclang_rt.osx.a' 2>/dev/null | head -1)
if [ -n "$CLANG_RT_LIB" ]; then
echo "RUSTFLAGS=-C link-arg=$CLANG_RT_LIB" >> $GITHUB_ENV
echo "Found clang_rt: $CLANG_RT_LIB"
else
echo "Warning: libclang_rt.osx.a not found"
fi
- name: Build (native)
if: '!matrix.cross'
run: cargo build --release --target ${{ matrix.target }} ${{ runner.os == 'Windows' && '--features win-icon' || '' }}
- name: Build (cross)
if: matrix.cross
run: cross build --release --target ${{ matrix.target }}
- name: Run tests
if: matrix.target != 'x86_64-apple-darwin' && !matrix.cross
run: cargo test --release --target ${{ matrix.target }}
- name: Package (Unix)
if: runner.os != 'Windows'
run: tar czf pinready-${{ matrix.label }}.tar.gz -C target/${{ matrix.target }}/release pinready
- name: Package (Windows)
if: runner.os == 'Windows'
run: Compress-Archive -Path target/${{ matrix.target }}/release/pinready.exe -DestinationPath pinready-${{ matrix.label }}.zip
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: pinready-${{ matrix.label }}
path: pinready-${{ matrix.label }}.*
sign-apple:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
environment: signing
steps:
- name: Download the macOS artifacts
uses: actions/download-artifact@v8
with:
pattern: pinready-macos-*
path: dl
- name: Import Apple Developer ID certificate
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$APPLE_CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-k "$KEYCHAIN_PATH" \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Sign & notarize the macOS binaries
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
# Submit and poll with timeout + retry (3 attempts, 5/15/45 min backoff)
notarize_once() {
local zip="$1" label="$2" timeout="$3"
local output
output=$(xcrun notarytool submit "$zip" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--verbose 2>&1)
echo "$output"
local sub_id
sub_id=$(echo "$output" | awk '/id:/{print $2; exit}')
# The submit POST itself can time out on Apple's side (seen in
# v0.14.0 CI). Treat "no submission ID" as retryable (rc=2) so
# the backoff/retry loop above catches transient outages
# instead of forcing a manual full-workflow re-run.
[ -n "$sub_id" ] || { echo "$label: failed to get submission ID (transient, will retry)"; return 2; }
echo "$label: submitted (id: $sub_id)"
local deadline=$((SECONDS + timeout))
while [ $SECONDS -lt $deadline ]; do
sleep 30
local info
info=$(xcrun notarytool info "$sub_id" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1) || { echo "$label: network error, retrying poll..."; continue; }
local status
status=$(echo "$info" | awk '/status:/{print $2; exit}')
echo "$label: $status"
case "$status" in
Accepted) return 0 ;;
Invalid|Rejected)
xcrun notarytool log "$sub_id" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1 || true
return 1 ;;
esac
done
echo "$label: timed out after $((timeout / 60)) minutes"
return 2 # timeout, retryable
}
notarize() {
local zip="$1" label="$2"
local backoffs=(300 900 2700) # 5 / 15 / 45 minutes
for i in 0 1 2; do
echo "$label: attempt $((i + 1))/3"
notarize_once "$zip" "$label" "${backoffs[$i]}" && return 0
local rc=$?
[ $rc -eq 2 ] || return $rc # only retry on timeout (rc=2)
[ $i -lt 2 ] && echo "$label: retrying after timeout..."
done
return 1
}
for label in macos-aarch64 macos-x86_64; do
echo "::group::$label"
work="$RUNNER_TEMP/$label"
mkdir -p "$work"
tar xzf "dl/pinready-$label/pinready-$label.tar.gz" -C "$work"
codesign --force --options runtime \
--sign "Developer ID Application: Sylvain Gargasson ($APPLE_TEAM_ID)" \
--timestamp "$work/pinready"
codesign --verify --strict --verbose=2 "$work/pinready"
ditto -c -k --keepParent "$work/pinready" "$RUNNER_TEMP/$label.zip"
notarize "$RUNNER_TEMP/$label.zip" "$label" || exit 1
tar czf "pinready-$label.tar.gz" -C "$work" pinready
echo "::endgroup::"
done
- name: Re-upload the signed macOS aarch64 artifact
uses: actions/upload-artifact@v7
with:
name: pinready-macos-aarch64
path: pinready-macos-aarch64.tar.gz
overwrite: true
- name: Re-upload the signed macOS x86_64 artifact
uses: actions/upload-artifact@v7
with:
name: pinready-macos-x86_64
path: pinready-macos-x86_64.tar.gz
overwrite: true
sign-windows:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: signing
steps:
- name: Install ssign (prebuilt binary from the latest release)
run: |
curl -fsSL https://github.com/Le-Syl21/ssign/releases/latest/download/ssign-linux-x86_64.tar.gz | tar xz
sudo install -m755 ssign /usr/local/bin/ssign
- name: Download the Windows artifact
uses: actions/download-artifact@v8
with:
name: pinready-windows-x86_64
path: dl
- name: Sign pinready.exe with ssign (via Certum)
env:
CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }}
CERTUM_OTP: ${{ secrets.CERTUM_OTP }}
run: |
unzip -o dl/pinready-windows-x86_64.zip -d dl
ssign --verbose \
-n "PinReady" -u "https://github.com/${{ github.repository }}" \
dl/pinready.exe
sudo apt-get update -qq && sudo apt-get install -y osslsigncode
# Vérifie les faits, pas la présence d'une phrase. « Calculated message
# digest » apparaît AUSSI quand le digest ne correspond pas, et ne dit
# rien de l'horodatage : ce contrôle laissait donc passer une signature
# que Windows aurait rejetée, et des binaires non horodatés — donc
# invalides dès l'expiration du certificat.
verify_signature() {
local out
out=$(osslsigncode verify "$1" 2>&1) || true
echo "$out"
if echo "$out" | grep -q "MISMATCH"; then
echo "::error::$1 : digest Authenticode invalide (Windows le verrait comme falsifié)"
return 1
fi
if echo "$out" | grep -q "Timestamp is not available"; then
echo "::error::$1 : signature non horodatée (invalide dès l'expiration du certificat)"
return 1
fi
if ! echo "$out" | grep -q "Signature verification: ok"; then
echo "::error::$1 : vérification de signature échouée"
return 1
fi
}
verify_signature dl/pinready.exe
(cd dl && rm -f pinready-windows-x86_64.zip && zip -j pinready-windows-x86_64.zip pinready.exe)
- name: Re-upload the signed Windows artifact
uses: actions/upload-artifact@v7
with:
name: pinready-windows-x86_64
path: dl/pinready-windows-x86_64.zip
overwrite: true
release:
needs: [build, sign-windows, sign-apple]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
- name: Create release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 with:
generate_release_notes: true
files: artifacts/**/*
publish-crate:
needs: [release]
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
runs-on: ubuntu-24.04
environment: crates-io
permissions:
contents: read
id-token: write steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 - name: Build dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libxkbcommon-dev libssl-dev libasound2-dev libpulse-dev \
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev \
libxtst-dev libxss-dev libwayland-dev wayland-protocols \
libdbus-1-dev libdrm-dev libgbm-dev \
libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libudev-dev
- name: Get a temporary crates.io token
id: auth
uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 - name: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish