name: Build # Packages are published to the `builds` branch instead of to a GitHub # Release, so everything the installers and the in-app updater fetch comes # straight from the repository and always matches what is on main. on: push: branches: - main # Nothing to rebuild when only documentation changed. paths-ignore: - "README.md" - "LICENSE" - ".gitignore" workflow_dispatch: permissions: contents: write jobs: linux: name: Linux AppImage runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential pkg-config curl \ libgtk-4-dev librsvg2-dev \ desktop-file-utils \ appstream - name: Build AppImage env: COMMIT: ${{ github.sha }} run: | chmod +x scripts/bundle-linux.sh ./scripts/bundle-linux.sh - name: Upload artifact uses: actions/upload-artifact@v4 with: name: maturita-linux path: dist/maturita-linux-*.AppImage windows: name: Windows zip runs-on: windows-latest defaults: run: shell: msys2 {0} steps: - uses: actions/checkout@v4 - uses: msys2/setup-msys2@v2 with: msystem: UCRT64 update: true install: >- make zip mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-pkg-config mingw-w64-ucrt-x86_64-make - name: Build and bundle env: COMMIT: ${{ github.sha }} run: | make bundle cd dist zip -qry ../maturita-windows-x64.zip . cd .. mkdir -p dist-out mv maturita-windows-x64.zip dist-out/ - name: Upload artifact uses: actions/upload-artifact@v4 with: name: maturita-windows path: dist-out/maturita-windows-x64.zip macos: name: macOS app runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Install dependencies run: | brew install gtk4 pkg-config # Developer ID lives in repo secrets. Without it the bundle is only # ad-hoc signed and Gatekeeper still blocks downloaded copies. # Do not use `secrets` in `if:` — GitHub rejects the whole workflow. - name: Import Developer ID certificate env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} run: | set -euo pipefail if [ -z "${MACOS_CERTIFICATE:-}" ]; then echo "No Developer ID certificate; the bundle will be ad-hoc signed." exit 0 fi cert="$RUNNER_TEMP/certificate.p12" keychain="$RUNNER_TEMP/signing.keychain-db" pass="$(openssl rand -base64 32)" echo "$MACOS_CERTIFICATE" | base64 --decode > "$cert" security create-keychain -p "$pass" "$keychain" security set-keychain-settings -lut 21600 "$keychain" security unlock-keychain -p "$pass" "$keychain" security import "$cert" -k "$keychain" \ -P "$MACOS_CERTIFICATE_PASSWORD" \ -T /usr/bin/codesign -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple: -s \ -k "$pass" "$keychain" security list-keychains -d user -s "$keychain" \ $(security list-keychains -d user | tr -d '"') rm -f "$cert" - name: Build .app zip env: COMMIT: ${{ github.sha }} MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: | chmod +x scripts/bundle-macos.sh ./scripts/bundle-macos.sh - name: Upload artifact uses: actions/upload-artifact@v4 with: name: maturita-macos path: dist/maturita-macos-*.zip android: name: Android APK runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 # v3 defaults to the retired `tools` package, which sdkmanager no longer # serves. Request only packages that still exist. - uses: android-actions/setup-android@v3 with: packages: platform-tools - name: Install Android SDK platform run: | set -euo pipefail sdkmanager --install "build-tools;36.0.0" if ! sdkmanager --install "platforms;android-37.0"; then sdkmanager --install "platforms;android-37" fi if [ -d "$ANDROID_HOME/platforms/android-37.0" ] && [ ! -e "$ANDROID_HOME/platforms/android-37" ]; then ln -sfn android-37.0 "$ANDROID_HOME/platforms/android-37" fi ls "$ANDROID_HOME/platforms" - uses: gradle/actions/setup-gradle@v4 with: build-root-directory: android - name: Extract content and build APK env: COMMIT: ${{ github.sha }} run: | python3 scripts/extract-android-content.py chmod +x android/gradlew cd android ./gradlew :app:assembleRelease --no-daemon mkdir -p ../dist-android cp app/build/outputs/apk/release/app-release.apk \ ../dist-android/maturita-android.apk - name: Upload artifact uses: actions/upload-artifact@v4 with: name: maturita-android path: dist-android/maturita-android.apk ios: name: iOS IPA # The app targets iOS 26; this image ships Xcode 26 as the default. runs-on: macos-26 steps: - uses: actions/checkout@v4 - name: Extract content and build IPA env: COMMIT: ${{ github.sha }} DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer run: | chmod +x scripts/bundle-ios.sh ./scripts/bundle-ios.sh - name: Upload artifact uses: actions/upload-artifact@v4 with: name: maturita-ios path: dist-ios/maturita-ios.ipa publish: name: Publish to builds branch needs: [linux, windows, macos, android, ios] # One broken platform must not freeze the others: publish whatever was # built and keep the previous package for anything that failed. if: ${{ !cancelled() && contains(needs.*.result, 'success') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Start from the previous builds branch run: | mkdir -p staged if git fetch --depth 1 origin builds 2>/dev/null; then git archive FETCH_HEAD | tar -x -C staged rm -f staged/VERSION fi - name: Download artifacts uses: actions/download-artifact@v4 with: path: fresh merge-multiple: true - name: Collect installers run: | set -euo pipefail cp -f fresh/* staged/ 2>/dev/null || true chmod +x installers/pack-installer-zips.sh ./installers/pack-installer-zips.sh cp installers/maturita-installer-linux.sh staged/ cp installers/maturita-installer-windows.cmd staged/ cp installers/maturita-installer-*.zip staged/ # The updaters download this to see whether they are behind. echo "${{ github.sha }}" > staged/VERSION # Per-platform markers name the commit whose package is really # here, so a platform that failed this run is not offered as new. [ -f fresh/maturita-android.apk ] && echo "${{ github.sha }}" > staged/VERSION-android [ -f fresh/maturita-ios.ipa ] && echo "${{ github.sha }}" > staged/VERSION-ios ls fresh staged # The branch is rewritten from scratch every time rather than added to, # so the repository keeps one copy of the packages instead of a new # ~70 MB set per commit. The same commit is then mirrored to Forgejo # when FORGEJO_TOKEN is set (git.dvorakmortonxd.dev). - name: Push to builds branch env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_USER: ${{ secrets.FORGEJO_USER }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" rm -rf .git/index.lock git checkout --orphan builds git rm -rf --cached . >/dev/null find . -maxdepth 1 ! -name . ! -name .git ! -name staged \ -exec rm -rf {} + mv staged/* . rmdir staged ls -la git add -A git commit -q -m "Build from ${{ github.sha }}" git push -f origin builds if [ -z "${FORGEJO_TOKEN:-}" ]; then echo "No FORGEJO_TOKEN; skipping Forgejo builds publish." exit 0 fi user="${FORGEJO_USER:-oauth2}" git push -f \ "https://${user}:${FORGEJO_TOKEN}@git.dvorakmortonxd.dev/edu/maturita.c.git" \ HEAD:refs/heads/builds forgejo-main: name: Mirror main to Forgejo runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Same rule as the macOS cert step: never put `secrets` in `if:`. - name: Push main env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_USER: ${{ secrets.FORGEJO_USER }} run: | set -euo pipefail if [ -z "${FORGEJO_TOKEN:-}" ]; then echo "No FORGEJO_TOKEN; skipping Forgejo main mirror." exit 0 fi user="${FORGEJO_USER:-oauth2}" git push \ "https://${user}:${FORGEJO_TOKEN}@git.dvorakmortonxd.dev/edu/maturita.c.git" \ HEAD:refs/heads/main