Skip to content

fix showing text alignment #90

fix showing text alignment

fix showing text alignment #90

name: Build and Release
on:
push:
branches:
- master
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 8.0.1)'
required: true
type: string
auto_release:
description: 'Automatically create GitHub release'
required: true
type: boolean
default: false
permissions:
contents: write
env:
JAVA_VERSION: '17'
jobs:
# Prepare and cache common build artifacts
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Determine version
id: version
run: |
# Use version from latest git tag, fallback to build.gradle.kts if no tags exist
git fetch --tags
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then
# Remove 'v' or 'V' prefix if present
VERSION=${LATEST_TAG#v}
VERSION=${VERSION#V}
echo "Using version from git tag: $VERSION"
else
# Fallback to build.gradle.kts
VERSION=$(grep '^version = ' build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/')
echo "No git tags found, using version from build.gradle.kts: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Build Jubler distribution
run: |
gradle clean assembleDistribution
- name: Upload build artifacts for platform packaging
uses: actions/upload-artifact@v4
with:
name: jubler-base-build-${{ steps.version.outputs.version }}
path: build/jubler/
retention-days: 1
# Parallel build matrix for all platforms
build-platform:
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- name: generic
artifact: "Jubler-*.tar.gz"
display: "Generic (tar.gz)"
- name: linux
artifact: "Jubler-*-x86_64.AppImage"
display: "Linux AppImage"
- name: macos
artifact: "Jubler-*.dmg"
display: "macOS DMG"
- name: windows
artifact: "Jubler-*-x64.exe"
display: "Windows Installer"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download base build
uses: actions/download-artifact@v4
with:
name: jubler-base-build-${{ needs.prepare.outputs.version }}
path: build/jubler/
- name: Set up Docker Buildx
if: matrix.target.name == 'windows'
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Set up tools and HFS+ support
run: |
echo "===== Initial system info ====="
uname -r
echo ""
echo "===== Installing packages ====="
sudo apt-get update
sudo apt-get install -y curl wget hfsplus hfsprogs hfsutils
echo ""
echo "===== Installing linux-modules-extra (contains HFS+ module) ====="
KERNEL_VERSION=$(uname -r)
echo "Current kernel: $KERNEL_VERSION"
# Install modules-extra FIRST before trying to load
sudo apt-get install -y "linux-modules-extra-${KERNEL_VERSION}" || {
echo "❌ Failed to install linux-modules-extra-${KERNEL_VERSION}"
echo "Available linux-modules-extra packages:"
apt-cache search linux-modules-extra | grep -E "^linux-modules-extra-.*generic" | head -5
exit 1
}
echo ""
echo "===== Checking if HFS+ module file exists ====="
if find /lib/modules/${KERNEL_VERSION} -name "hfsplus.ko*" 2>/dev/null | grep -q .; then
echo "✅ HFS+ module file found:"
find /lib/modules/${KERNEL_VERSION} -name "hfsplus.ko*"
else
echo "❌ HFS+ module file NOT found"
exit 1
fi
echo ""
echo "===== Loading HFS+ module ====="
if sudo modprobe hfsplus; then
echo "✅ HFS+ module loaded successfully"
else
echo "❌ Failed to load HFS+ module"
dmesg | tail -20
exit 1
fi
echo ""
echo "===== Verification ====="
echo "Loaded modules:"
lsmod | grep hfsplus || { echo "❌ Module not in lsmod"; exit 1; }
echo ""
echo "Available filesystems:"
cat /proc/filesystems | grep hfs || { echo "❌ HFS not in /proc/filesystems"; exit 1; }
echo ""
echo "✅ HFS+ support confirmed and ready"
- name: Install kpartx (for GPT partition support)
run: |
echo "===== Installing kpartx for GPT partition handling ====="
sudo apt-get install -y kpartx
echo "✅ kpartx installed"
- name: Download and setup KPacker
run: |
mkdir -p $HOME/Works/System/bin/arch/linux-x86_64
# Download KPacker binary
wget -O $HOME/Works/System/bin/arch/linux-x86_64/kpacker https://github.com/teras/KPacker/releases/download/v0.1/kpacker
chmod +x $HOME/Works/System/bin/arch/linux-x86_64/kpacker
# Test KPacker
echo "Testing KPacker..."
$HOME/Works/System/bin/arch/linux-x86_64/kpacker --help
- name: Build ${{ matrix.target.display }}
id: build
run: |
# Set environment for Docker/KPacker
export DOCKER_BUILDKIT=1
export DOCKER_CLI_EXPERIMENTAL=enabled
export BUILDX_NO_DEFAULT_ATTESTATIONS=1
# Build the platform target
echo "Building ${{ matrix.target.name }} artifact..."
export JUBLER_VERSION=${{ needs.prepare.outputs.version }}
./make.sh build ${{ matrix.target.name }}
# List created artifacts
echo "📦 Created artifacts:"
ls -lah dist/
# Find the created artifact
ARTIFACT_PATH=$(ls dist/${{ matrix.target.artifact }} 2>/dev/null | head -1)
if [ -z "$ARTIFACT_PATH" ]; then
echo "❌ Error: Artifact not found matching pattern: ${{ matrix.target.artifact }}"
exit 1
fi
echo "artifact_path=$ARTIFACT_PATH" >> $GITHUB_OUTPUT
echo "✅ Successfully created: $ARTIFACT_PATH"
- name: Upload ${{ matrix.target.display }} artifact
id: upload
uses: actions/upload-artifact@v4
with:
name: jubler-${{ matrix.target.name }}-${{ needs.prepare.outputs.version }}
path: ${{ steps.build.outputs.artifact_path }}
retention-days: 30
sign-macos:
needs: [prepare, build-platform]
runs-on: macos-latest
if: github.repository_owner == 'teras' # Replace with your username
environment: test
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download macOS build
uses: actions/download-artifact@v4
with:
name: jubler-macos-${{ needs.prepare.outputs.version }}
path: dist/
- name: Sign and compress macOS DMG
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
APPLE_NOTARY_JSON: ${{ secrets.APPLE_NOTARY_JSON }}
run: |
# Find the uncompressed DMG
DMG_FILE=$(ls dist/Jubler-*.dmg | head -1)
if [ -z "$DMG_FILE" ]; then
echo "❌ Error: DMG file not found"
exit 1
fi
echo "📦 Found DMG: $DMG_FILE"
# Setup signing certificate (required)
if [ -z "$MACOS_CERTIFICATE" ]; then
echo "❌ Error: MACOS_CERTIFICATE secret is not configured"
exit 1
fi
echo "🔐 Setting up code signing certificate..."
# Generate random keychain password (temporary, only for this job)
KEYCHAIN_PWD=$(openssl rand -base64 32)
# Create keychain
security create-keychain -p "$KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PWD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import certificate
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PWD" build.keychain
# Find certificate identity
IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | awk '{print $2}')
echo "✅ Certificate identity: $IDENTITY"
# Mount DMG without allowing Finder to interfere (nobrowse prevents Finder windows)
echo "📀 Mounting DMG..."
hdiutil attach "$DMG_FILE" -mountpoint /Volumes/Jubler -nobrowse
# Check for resource forks and extended attributes
echo "🔍 Checking for resource forks and extended attributes..."
echo "Files with extended attributes:"
find /Volumes/Jubler/Jubler.app -type f -exec ls -la@ {} \; | grep -v "^[^@]*$" | head -20
echo ""
echo "Common extended attributes found:"
xattr -r /Volumes/Jubler/Jubler.app | head -20
echo ""
echo "Checking for quarantine attributes:"
xattr -d com.apple.quarantine /Volumes/Jubler/Jubler.app 2>/dev/null || echo "No quarantine attributes found"
# Clean extended attributes and resource forks
echo "🧹 Cleaning extended attributes and resource forks..."
xattr -cr /Volumes/Jubler/Jubler.app
# Verify cleanup
echo "✅ Verifying cleanup..."
if xattr -r /Volumes/Jubler/Jubler.app | grep -q .; then
echo "⚠️ Some extended attributes remain:"
xattr -r /Volumes/Jubler/Jubler.app
else
echo "✅ All extended attributes removed"
fi
# Sign all binaries in the .app bundle with entitlements
echo "✍️ Signing app bundle contents..."
find /Volumes/Jubler/Jubler.app -type f \( -name "*.dylib" -o -name "*.jnilib" -o -perm +111 \) -exec codesign --force --sign "$IDENTITY" --timestamp --options runtime --entitlements resources/macos-entitlements.plist {} \; || true
# Sign native libraries inside JAR files
echo "✍️ Signing native libraries inside JAR files..."
# Create temporary directory for JAR extraction
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Find and sign libraries in JAR files
find /Volumes/Jubler/Jubler.app -name "*.jar" -type f | while read -r jar_file; do
echo "Processing JAR: $jar_file"
# Check if JAR contains native libraries
if unzip -l "$jar_file" | grep -q "\.dylib\|\.jnilib\|\.so$"; then
echo "Found native libraries in: $jar_file"
# Extract entire JAR to temp directory
JAR_TEMP="$TEMP_DIR/$(basename "$jar_file" .jar)"
mkdir -p "$JAR_TEMP"
unzip -q "$jar_file" -d "$JAR_TEMP"
# Sign any native libraries found with entitlements
find "$JAR_TEMP" -type f \( -name "*.dylib" -o -name "*.jnilib" -o -name "*.so" \) | while read -r lib_file; do
echo "Signing library: $lib_file"
codesign --force --sign "$IDENTITY" --timestamp --options runtime --entitlements resources/macos-entitlements.plist "$lib_file" || echo "Failed to sign $lib_file"
done
# Recreate JAR with signed libraries
echo "Recreating JAR with signed libraries: $jar_file"
(cd "$JAR_TEMP" && jar -cf "$jar_file" .) || echo "Failed to recreate JAR: $jar_file"
# Clean up
rm -rf "$JAR_TEMP"
fi
done
# Clean up temp directory
rm -rf "$TEMP_DIR"
# Sign the app bundle with entitlements
echo "✍️ Signing app bundle..."
codesign --force --deep --sign "$IDENTITY" --timestamp --options runtime --entitlements resources/macos-entitlements.plist /Volumes/Jubler/Jubler.app
# Verify signature
codesign --verify --deep --strict --verbose=2 /Volumes/Jubler/Jubler.app
# Unmount
echo "📤 Unmounting DMG..."
hdiutil detach /Volumes/Jubler -force
# Sign the DMG itself
echo "✍️ Signing DMG..."
codesign --force --sign "$IDENTITY" --timestamp "$DMG_FILE"
# Always compress DMG (convert to read-only compressed format)
echo "🗜️ Compressing DMG..."
COMPRESSED_DMG="${DMG_FILE%.dmg}-compressed.dmg"
hdiutil convert "$DMG_FILE" -format UDZO -o "$COMPRESSED_DMG"
rm "$DMG_FILE"
mv "$COMPRESSED_DMG" "$DMG_FILE"
echo "✅ DMG compressed successfully"
- name: Upload signed macOS DMG
uses: actions/upload-artifact@v4
with:
name: jubler-signed-macos-${{ needs.prepare.outputs.version }}
path: dist/*.dmg
retention-days: 30
- name: Notarize macOS DMG
env:
APPLE_NOTARY_JSON: ${{ secrets.APPLE_NOTARY_JSON }}
run: |
# Find the DMG file
DMG_FILE=$(ls dist/Jubler-*.dmg | head -1)
if [ -z "$DMG_FILE" ]; then
echo "❌ Error: DMG file not found"
exit 1
fi
# Notarize (required)
if [ -z "$APPLE_NOTARY_JSON" ]; then
echo "❌ Error: APPLE_NOTARY_JSON secret is not configured"
exit 1
fi
echo "📮 Notarizing with Apple (API Key)..."
# Parse JSON and extract values
APPLE_API_ISSUER=$(echo "$APPLE_NOTARY_JSON" | grep -o '"issuer_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"issuer_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
APPLE_API_KEY_ID=$(echo "$APPLE_NOTARY_JSON" | grep -o '"key_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"key_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
APPLE_API_KEY=$(echo "$APPLE_NOTARY_JSON" | grep -o '"private_key"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"private_key"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
echo " Issuer ID: $APPLE_API_ISSUER"
echo " Key ID: $APPLE_API_KEY_ID"
# Create private key file
mkdir -p ~/private_keys
echo "-----BEGIN PRIVATE KEY-----" > ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
echo "$APPLE_API_KEY" >> ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
echo "-----END PRIVATE KEY-----" >> ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
chmod 600 ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
# Submit for notarization
NOTARY_OUTPUT=$(xcrun notarytool submit "$DMG_FILE" \
--key ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER" \
--wait)
echo "$NOTARY_OUTPUT"
# Extract submission ID for log retrieval
SUBMISSION_ID=$(echo "$NOTARY_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
echo "Submission ID: $SUBMISSION_ID"
# Get detailed notarization logs if submission failed
if echo "$NOTARY_OUTPUT" | grep -q "Invalid"; then
echo "❌ Notarization failed. Fetching detailed logs..."
xcrun notarytool log "$SUBMISSION_ID" \
--key ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER"
fi
# Clean up API key
rm -rf ~/private_keys
# Staple notarization ticket
echo "📎 Stapling notarization ticket..."
xcrun stapler staple "$DMG_FILE"
echo "✅ Notarization complete"
- name: Re-upload notarized macOS DMG
if: success()
uses: actions/upload-artifact@v4
with:
name: jubler-signed-macos-${{ needs.prepare.outputs.version }}
path: dist/*.dmg
retention-days: 30
overwrite: 'true'
sign-windows:
needs: [prepare, build-platform]
runs-on: windows-latest
if: github.repository_owner == 'teras' # Replace with your username
environment: test
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Windows build
uses: actions/download-artifact@v4
with:
name: jubler-windows-${{ needs.prepare.outputs.version }}
path: dist/
- name: Prepare signed artifact directory
run: New-Item -ItemType Directory -Force -Path "dist-signed"
- name: Get Windows artifact ID
id: get-artifact
uses: actions/github-script@v7
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
const windowsArtifact = artifacts.data.artifacts.find(a =>
a.name === `jubler-windows-${{ needs.prepare.outputs.version }}`
);
if (!windowsArtifact) {
core.setFailed('Windows artifact not found');
return;
}
core.setOutput('artifact-id', windowsArtifact.id);
- name: Submit SignPath signing request
id: signpath
uses: SignPath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: Jubler
signing-policy-slug: test-signing
artifact-configuration-slug: jubler-exe-in-zip
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
output-artifact-directory: dist-signed
- name: List signed artifact
run: |
Get-ChildItem -Path "dist-signed"
- name: Upload signed Windows installer
uses: actions/upload-artifact@v4
with:
name: jubler-signed-windows-${{ needs.prepare.outputs.version }}
path: dist-signed/*
retention-days: 30
release:
needs: [prepare, build-platform, sign-macos, sign-windows]
runs-on: ubuntu-latest
if: always() && needs.prepare.result == 'success' && needs.build-platform.result == 'success' && (github.event.inputs.auto_release == 'true' || github.event_name == 'push')
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all platform artifacts
uses: actions/download-artifact@v4
with:
pattern: jubler-*-${{ needs.prepare.outputs.version }}
path: artifacts/
merge-multiple: false
- name: Prepare release artifacts
run: |
mkdir -p release-artifacts
echo "📦 Downloaded artifacts:"
ls -laR artifacts/
# Copy all platform builds
echo "Copying platform artifacts..."
cp artifacts/jubler-generic-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No generic artifacts"
cp artifacts/jubler-linux-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No Linux artifacts"
# Use signed macOS artifact if available, otherwise use unsigned
if [ -d "artifacts/jubler-signed-macos-${{ needs.prepare.outputs.version }}" ] && [ "$(ls -A artifacts/jubler-signed-macos-${{ needs.prepare.outputs.version }})" ]; then
echo "✅ Using signed macOS artifact"
cp artifacts/jubler-signed-macos-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No signed macOS artifacts"
else
echo "⚠️ Using unsigned macOS artifact (signing may have failed or been skipped)"
cp artifacts/jubler-macos-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No macOS artifacts"
fi
# Use signed Windows artifact if available, otherwise use unsigned
if [ -d "artifacts/jubler-signed-windows-${{ needs.prepare.outputs.version }}" ] && [ "$(ls -A artifacts/jubler-signed-windows-${{ needs.prepare.outputs.version }})" ]; then
echo "✅ Using signed Windows artifact"
cp artifacts/jubler-signed-windows-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No signed Windows artifacts"
else
echo "⚠️ Using unsigned Windows artifact (signing may have failed or been skipped)"
cp artifacts/jubler-windows-*/* release-artifacts/ 2>/dev/null || echo "⚠️ No Windows artifacts"
fi
echo "📋 Final release artifacts:"
ls -lah release-artifacts/
- name: Set prerelease flag
id: prerelease
run: |
# Always mark as prerelease (alpha)
echo "prerelease=true" >> $GITHUB_OUTPUT
echo "Marking as prerelease (alpha): ${{ needs.prepare.outputs.version }}"
- name: Publish release
uses: ncipollo/release-action@v1
with:
tag: "v${{ needs.prepare.outputs.version }}"
name: "Jubler v${{ needs.prepare.outputs.version }}"
draft: false
prerelease: ${{ steps.prerelease.outputs.prerelease }}
allowUpdates: true
removeArtifacts: true
makeLatest: false
artifacts: release-artifacts/*
artifactErrorsFailBuild: false
bodyFile: .github/release-template.md
token: ${{ secrets.GITHUB_TOKEN }}
cleanup:
needs: [prepare, build-platform, sign-macos, sign-windows, release]
runs-on: ubuntu-latest
if: always() && (needs.release.result == 'success' || needs.release.result == 'skipped')
steps:
- name: Delete temporary build artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: |
jubler-base-build-${{ needs.prepare.outputs.version }}
jubler-windows-${{ needs.prepare.outputs.version }}
jubler-macos-${{ needs.prepare.outputs.version }}
failOnError: false