name: Cross-Platform Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
name: Build for ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
use_cross: false
- target: x86_64-unknown-linux-musl
runner: ubuntu-latest
use_cross: true
- target: aarch64-unknown-linux-gnu
runner: ubuntu-latest
use_cross: true
- target: aarch64-unknown-linux-musl
runner: ubuntu-latest
use_cross: true
- target: x86_64-apple-darwin
runner: macos-latest
use_cross: false
- target: aarch64-apple-darwin
runner: macos-latest
use_cross: false
- target: x86_64-pc-windows-msvc
runner: windows-latest
use_cross: false
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
shared-key: ${{ matrix.target }}
- name: Install cross
if: matrix.use_cross
run: cargo install cross --git https://github.com/cross-rs/cross --rev 8633ec65ab914015c2444c732568b414bd3c47cf
- name: Create .cargo/config.toml for Linux ARM
if: matrix.target == 'aarch64-unknown-linux-gnu'
shell: bash
run: |
mkdir -p .cargo
cat > .cargo/config.toml << 'EOF'
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
ar = "aarch64-linux-gnu-ar"
EOF
- name: Build
run: |
if [ "${{ matrix.use_cross }}" = "true" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
shell: bash
- name: Determine artifact name
id: artifact
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
echo "bin=treemd.exe" >> $GITHUB_OUTPUT
else
echo "bin=treemd" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Import Apple Code Signing Certificate
if: runner.os == 'macOS'
continue-on-error: true
uses: Apple-Actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Code sign and notarize macOS binary
if: runner.os == 'macOS'
run: |
CERT_IMPORTED=false
# Check if certificate was imported (if secrets exist, cert should be imported)
if security find-identity -v -p codesigning | grep -q "Developer ID Application"; then
CERT_IMPORTED=true
fi
if [ "$CERT_IMPORTED" = "true" ]; then
echo "Signing with Developer ID..."
codesign --deep --force --verify --verbose \
--timestamp --options runtime \
--sign "${{ secrets.APPLE_DEVELOPER_ID }}" \
target/${{ matrix.target }}/release/treemd
codesign -v target/${{ matrix.target }}/release/treemd
echo "Notarizing with Apple..."
ditto -c -k --sequesterRsrc target/${{ matrix.target }}/release/treemd treemd-notarize.zip
xcrun notarytool submit treemd-notarize.zip \
--apple-id "${{ secrets.APPLE_ID }}" \
--password "${{ secrets.APPLE_ID_PASSWORD }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--wait \
--timeout 30m
echo "Note: Stapling is not applicable to bare CLI binaries"
echo "Binary is notarized server-side - users will be verified online"
echo "Verifying notarization status..."
spctl -a -v target/${{ matrix.target }}/release/treemd || true
echo "✓ Binary signed and notarized"
else
echo "Developer ID certificate not configured. Using ad-hoc signing..."
codesign --remove-signature target/${{ matrix.target }}/release/treemd || true
codesign -s - target/${{ matrix.target }}/release/treemd
codesign -v target/${{ matrix.target }}/release/treemd
echo "⚠ Binary ad-hoc signed (users may see Gatekeeper warning)"
fi
- name: Prepare artifacts (Windows)
if: runner.os == 'Windows'
run: |
# Create artifacts directory
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
$binary = "treemd-${{ matrix.target }}.exe"
Copy-Item "target/${{ matrix.target }}/release/treemd.exe" $binary
# Create zip for Windows (tar.gz not common on Windows)
Compress-Archive -Path $binary -DestinationPath "artifacts/$binary.zip"
# Generate SHA256
$hash = (Get-FileHash $binary -Algorithm SHA256).Hash.ToLower()
"$hash $binary" | Out-File -FilePath "artifacts/$binary.sha256" -Encoding ascii -NoNewline
Write-Host "Binary info:"
Get-Item $binary | Format-List Length, LastWriteTime
Get-Content "artifacts/$binary.sha256"
shell: pwsh
- name: Prepare artifacts (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p artifacts
BINARY="treemd-${{ matrix.target }}"
cp "target/${{ matrix.target }}/release/treemd" "$BINARY"
chmod +x "$BINARY"
# Create tar to preserve permissions and prevent corruption
tar -czf "artifacts/$BINARY.tar.gz" "$BINARY"
sha256sum "$BINARY" > "artifacts/$BINARY.sha256"
ls -lh "$BINARY"
cat "artifacts/$BINARY.sha256"
shell: bash
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: artifacts/*
if-no-files-found: error
retention-days: 1
release:
name: Create Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
- name: Prepare release assets
run: |
mkdir -p final-release
# Copy archives directly (tar.gz and zip preserve permissions internally)
find release-artifacts -name "*.tar.gz" -type f -exec cp {} final-release/ \;
find release-artifacts -name "*.zip" -type f -exec cp {} final-release/ \;
# Copy individual SHA256 files
find release-artifacts -name "*.sha256" -type f -exec cp {} final-release/ \;
echo "Final release contents:"
ls -lah final-release/
echo "Verifying archive integrity:"
cd final-release
for archive in *.tar.gz; do
[ -f "$archive" ] || continue
echo "Testing $archive:"
tar -tzf "$archive" | head -5
done
for archive in *.zip; do
[ -f "$archive" ] || continue
echo "Testing $archive:"
unzip -l "$archive"
done
shell: bash
- name: Generate combined checksums
run: |
cd final-release
# Generate SHA256SUMS for archives (not individual binaries)
sha256sum *.tar.gz *.zip 2>/dev/null | grep -v "No such file" > SHA256SUMS || true
cat SHA256SUMS
shell: bash
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
final-release/*.tar.gz
final-release/*.zip
final-release/*.sha256
final-release/SHA256SUMS
generate_release_notes: true
draft: false
prerelease: false
fail_on_unmatched_files: true