name: Build and Release # Asset naming standard (see nightly.yml header for the full scheme): # grablytic-v[]-.apk (release: suffix empty — # names are byte-identical to v0.0.1/v0.0.2 releases, Obtainium-safe) # Linux tar/pkg and Windows zip take the same optional suffix; .deb/.rpm # keep native conventions untouched. on: workflow_dispatch: inputs: publish_to_release: description: 'Run the Publish Release Assets job (version-tagged GitHub release).' required: false default: true type: boolean workflow_call: inputs: asset_suffix: description: 'Inserted after the version in every asset name (e.g. -nightly-20260916-abc1234). Empty = release names.' required: false default: '' type: string publish_to_release: description: 'Run the Publish Release Assets job (version-tagged GitHub release).' required: false default: true type: boolean # Least privilege, stated rather than inherited (#120). The publish job below # widens itself to contents: write because it creates the release; every other # job only reads the checkout. permissions: contents: read jobs: # TEARDOWN-6: manual dispatches used to build+publish without ever running # verify (analyze + flutter test + pytest + coverage gate). Reuse the # verify workflow as a gate: no build job starts unless it is green. verify: name: Verify (gate) uses: ./.github/workflows/verify.yml android: name: Build Android (${{ matrix.variant }}) needs: [verify] runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: # Modern 64-bit ARM (ARM64-v8a): - variant: arm64-deno target_arg: "-PtargetAbi=arm64-v8a" jsruntime_arg: "-PjsRuntime=deno" - variant: arm64-node target_arg: "-PtargetAbi=arm64-v8a" jsruntime_arg: "-PjsRuntime=node" # 32-bit ARM (ARMeabi-v7a) — low-end / budget devices (e.g. Galaxy A04e, older phones): - variant: armv7-node target_arg: "-PtargetAbi=armeabi-v7a" jsruntime_arg: "-PjsRuntime=node" # 64-bit x86 (x86_64) — emulators, Chromebooks, Intel-based Android: - variant: x86_64-deno target_arg: "-PtargetAbi=x86_64" jsruntime_arg: "-PjsRuntime=deno" - variant: x86_64-node target_arg: "-PtargetAbi=x86_64" jsruntime_arg: "-PjsRuntime=node" # Universal multi-ABI bundle (all 3 ABIs + all runtimes): - variant: universal target_arg: "" jsruntime_arg: "" env: KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Java 17 uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 with: distribution: 'zulu' java-version: '17' - name: Install Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Ensure python3.11 on PATH run: | which python3.11 || sudo ln -sf "$(which python3)" /usr/local/bin/python3.11 python3.11 --version - name: Install CMake 3.31.4 run: | ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}" yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" "cmake;3.31.4" - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable # The values come from `env:`, so the shell sees data (#121). `printf '%s'` # keeps a trailing newline out of the base64 payload and decodes a paste # that was wrapped mid-line; `test -s` turns a corrupt keystore into a # named failure here instead of a signing error twenty minutes later. - name: Decode keystore if: ${{ env.KEYSTORE_BASE64 != '' }} run: | set -euo pipefail printf '%s' "$KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks test -s android/app/upload-keystore.jks - name: Create key.properties if: ${{ env.KEYSTORE_BASE64 != '' }} run: | set -euo pipefail { printf 'storePassword=%s\n' "$KEYSTORE_PASSWORD" printf 'keyPassword=%s\n' "$KEY_PASSWORD" printf 'keyAlias=%s\n' "$KEY_ALIAS" printf 'storeFile=upload-keystore.jks\n' } > android/key.properties - run: flutter pub get - name: Stamp Flutter version run: echo "GRABLYTIC_FLUTTER_VERSION=$(flutter --version | head -n 1)" >> $GITHUB_ENV - name: Stamp git commit run: echo "GRABLYTIC_GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Stamp release version run: | V=$(grep '^version:' pubspec.yaml | awk '{print $2}') echo "GRABLYTIC_RELEASE_VERSION=${V%%+*}" >> $GITHUB_ENV # The full value, build number included: AppVersion reads this, so # the About screen, the report composer and every crash body name # the build the user actually installed (#105). echo "GRABLYTIC_APP_VERSION=${V}" >> $GITHUB_ENV - name: Build Android APK (${{ matrix.variant }}) run: | flutter build apk --release \ --dart-define=GRABLYTIC_FLUTTER_VERSION="${GRABLYTIC_FLUTTER_VERSION}" \ --dart-define=GRABLYTIC_GIT_SHA="${GRABLYTIC_GIT_SHA}" \ --dart-define=GRABLYTIC_APP_VERSION="${GRABLYTIC_APP_VERSION}" \ ${{ matrix.target_arg }} \ ${{ matrix.jsruntime_arg }} - name: Prepare APK output run: | mkdir -p dist/android cp build/app/outputs/flutter-apk/app-release.apk "dist/android/grablytic-v${GRABLYTIC_RELEASE_VERSION}${{ inputs.asset_suffix }}-${{ matrix.variant }}.apk" - name: Upload Android Artifact (${{ matrix.variant }}) uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: grablytic-${{ matrix.variant }}${{ inputs.asset_suffix }} path: dist/android/grablytic-v${{ env.GRABLYTIC_RELEASE_VERSION }}${{ inputs.asset_suffix }}-${{ matrix.variant }}.apk linux: name: Build Linux (${{ matrix.arch }} - deb, rpm, arch, tar.gz) needs: [verify] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - arch: x64 deb_arch: amd64 rpm_arch: x86_64 arch_arch: x86_64 os: ubuntu-latest - arch: arm64 deb_arch: arm64 rpm_arch: aarch64 arch_arch: aarch64 os: ubuntu-24.04-arm steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Java 17 uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 with: distribution: 'zulu' java-version: '17' - name: Install Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" # `libsecret-1-dev` + `libjsoncpp-dev` are not optional: the # flutter_secure_storage Linux plugin's CMakeLists does # `pkg_check_modules(SECRET REQUIRED ... libsecret-1>=0.18.4)` and links # jsoncpp, so `flutter build linux` dies in CMake configure without them # ("The following required packages were not found: libsecret-1>=0.18.4") # — which is exactly how the 2026-10-21 nightly lost its Linux artifacts. - name: Install Linux build and packaging dependencies run: | sudo apt-get update -y sudo apt-get install -y ninja-build libgtk-3-dev liblzma-dev \ libsecret-1-dev libjsoncpp-dev pkg-config alien rpm zstd - name: Set up Flutter (Linux ARM64) if: matrix.arch == 'arm64' run: | git clone --depth 1 -b stable https://github.com/flutter/flutter.git $HOME/flutter git config --global --add safe.directory "$HOME/flutter" echo "$HOME/flutter/bin" >> $GITHUB_PATH $HOME/flutter/bin/flutter precache --linux - name: Set up Flutter (Linux x64) if: matrix.arch != 'arm64' uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable - run: flutter pub get - name: Stamp Flutter version run: echo "GRABLYTIC_FLUTTER_VERSION=$(flutter --version | head -n 1)" >> $GITHUB_ENV - name: Stamp release version run: | V=$(grep '^version:' pubspec.yaml | awk '{print $2}') echo "GRABLYTIC_RELEASE_VERSION=${V%%+*}" >> $GITHUB_ENV echo "GRABLYTIC_APP_VERSION=${V}" >> $GITHUB_ENV - name: Stamp git commit run: echo "GRABLYTIC_GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build Linux Release Bundle run: | flutter build linux --release \ --dart-define="GRABLYTIC_FLUTTER_VERSION=${GRABLYTIC_FLUTTER_VERSION}" \ --dart-define="GRABLYTIC_GIT_SHA=${GRABLYTIC_GIT_SHA}" \ --dart-define="GRABLYTIC_APP_VERSION=${GRABLYTIC_APP_VERSION}" - name: Bundle Python Engine run: | BUNDLE_DIR=$(find build/linux -name "bundle" -type d | head -n 1) echo "Found bundle directory at: $BUNDLE_DIR" echo "BUNDLE_DIR=$BUNDLE_DIR" >> $GITHUB_ENV cp -r engine/grablytic_engine "$BUNDLE_DIR/" - name: Package Tarball (.tar.gz) run: | mkdir -p dist/linux tar -czvf "dist/linux/grablytic-${GRABLYTIC_RELEASE_VERSION}${{ inputs.asset_suffix }}-linux-${{ matrix.arch }}.tar.gz" -C "$BUNDLE_DIR" . - name: Package Debian (.deb) run: | mkdir -p pkg_deb/DEBIAN mkdir -p pkg_deb/opt/grablytic mkdir -p pkg_deb/usr/bin mkdir -p pkg_deb/usr/share/applications mkdir -p pkg_deb/usr/share/icons/hicolor/512x512/apps # dpkg control files accept no `#` comments and backticks would # execute under an unquoted heredoc — keep this body literal-only. # (Replaces/Conflicts takes over a manually installed pre-rename # v0.0.1 `truestream` package instead of orphaning it alongside.) # Unquoted delimiter: the body interpolates ${GRABLYTIC_RELEASE_VERSION}. # Nothing else in it contains a `$`, so there is nothing to escape. cat << EOF > pkg_deb/DEBIAN/control Package: grablytic Version: ${GRABLYTIC_RELEASE_VERSION} Section: video Priority: optional Architecture: ${{ matrix.deb_arch }} Maintainer: OnlyXianzo <184820152+OnlyXianzo@users.noreply.github.com> Depends: libgtk-3-0, libsecret-1-0 | libsecret-1-0t64, python3, ffmpeg, aria2 Replaces: truestream Conflicts: truestream Description: Fast, beautiful, privacy-first media downloader Download from 1,000+ platforms with Flutter UI and yt-dlp Python engine. EOF chmod 644 pkg_deb/DEBIAN/control chmod 755 pkg_deb/DEBIAN cat << 'EOF' > pkg_deb/usr/bin/grablytic #!/bin/sh exec /opt/grablytic/grablytic "$@" EOF chmod 755 pkg_deb/usr/bin/grablytic cat << 'EOF' > pkg_deb/usr/share/applications/grablytic.desktop [Desktop Entry] Name=Grablytic Comment=Privacy-first media downloader Exec=/usr/bin/grablytic Icon=grablytic Terminal=false Type=Application Categories=AudioVideo;Video;Network; EOF chmod 644 pkg_deb/usr/share/applications/grablytic.desktop cp -r "$BUNDLE_DIR"/* pkg_deb/opt/grablytic/ cp assets/brand/grablytic_logo_circle.png pkg_deb/usr/share/icons/hicolor/512x512/apps/grablytic.png chmod -R 755 pkg_deb/opt/grablytic dpkg-deb --build pkg_deb "dist/linux/grablytic_${GRABLYTIC_RELEASE_VERSION}_${{ matrix.deb_arch }}.deb" - name: Package RPM (.rpm) run: | sudo alien --to-rpm --scripts --target=${{ matrix.rpm_arch }} "dist/linux/grablytic_${GRABLYTIC_RELEASE_VERSION}_${{ matrix.deb_arch }}.deb" RPM_FILE=$(ls grablytic*.rpm 2>/dev/null | head -n 1) if [ -n "$RPM_FILE" ]; then mv "$RPM_FILE" "dist/linux/grablytic-${GRABLYTIC_RELEASE_VERSION}-1.${{ matrix.rpm_arch }}.rpm" fi - name: Package Arch Linux (.pkg.tar.zst) run: | mkdir -p pkg_arch/opt/grablytic mkdir -p pkg_arch/usr/bin mkdir -p pkg_arch/usr/share/applications mkdir -p pkg_arch/usr/share/icons/hicolor/512x512/apps cp -r "$BUNDLE_DIR"/* pkg_arch/opt/grablytic/ cp pkg_deb/usr/bin/grablytic pkg_arch/usr/bin/grablytic cp pkg_deb/usr/share/applications/grablytic.desktop pkg_arch/usr/share/applications/grablytic.desktop cp assets/brand/grablytic_logo_circle.png pkg_arch/usr/share/icons/hicolor/512x512/apps/grablytic.png chmod -R 755 pkg_arch/opt/grablytic chmod 755 pkg_arch/usr/bin/grablytic INST_SIZE=$(du -sb pkg_arch | awk '{print $1}') cat << EOF > pkg_arch/.PKGINFO pkgname = grablytic pkgver = ${GRABLYTIC_RELEASE_VERSION}-1 pkgdesc = Fast, beautiful, privacy-first media downloader url = https://github.com/OnlyXianzo/Grablytic builddate = $(date +%s) packager = Grablytic CI size = $INST_SIZE arch = ${{ matrix.arch_arch }} license = custom depend = gtk3 depend = libsecret depend = python depend = ffmpeg depend = aria2 EOF tar --zstd -cf "dist/linux/grablytic-${GRABLYTIC_RELEASE_VERSION}${{ inputs.asset_suffix }}-1-${{ matrix.arch_arch }}.pkg.tar.zst" -C pkg_arch .PKGINFO opt usr - name: Upload Linux Packages (${{ matrix.arch }}) uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: grablytic-linux-${{ matrix.arch }}-packages path: dist/linux/* windows: name: Build Windows App (x64) needs: [verify] runs-on: windows-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Java 17 uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 with: distribution: 'zulu' java-version: '17' - name: Install Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable - run: flutter pub get - name: Stamp Flutter version run: echo "GRABLYTIC_FLUTTER_VERSION=$(flutter --version | head -n 1)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Stamp git commit run: echo "GRABLYTIC_GIT_SHA=$(git rev-parse --short HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Stamp app version run: | $V = (Select-String -Path pubspec.yaml -Pattern '^version:\s*(\S+)').Matches.Groups[1].Value echo "GRABLYTIC_APP_VERSION=$V" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build Windows App (64-bit) run: flutter build windows --release --dart-define="GRABLYTIC_FLUTTER_VERSION=$env:GRABLYTIC_FLUTTER_VERSION" --dart-define="GRABLYTIC_GIT_SHA=$env:GRABLYTIC_GIT_SHA" --dart-define="GRABLYTIC_APP_VERSION=$env:GRABLYTIC_APP_VERSION" - name: Copy Python Engine to Bundle run: xcopy /E /I engine\grablytic_engine build\windows\x64\runner\Release\grablytic_engine - name: Package Windows ZIP run: | mkdir dist\windows Compress-Archive -Path build\windows\x64\runner\Release\* -DestinationPath "dist\windows\grablytic-windows-x64${{ inputs.asset_suffix }}.zip" - name: Upload Windows Portable Zip uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: grablytic-windows-x64-zip${{ inputs.asset_suffix }} path: dist\windows\grablytic-windows-x64${{ inputs.asset_suffix }}.zip - name: Upload Windows Release Bundle uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: grablytic-windows-x64-bundle path: build\windows\x64\runner\Release\ publish-release: name: Publish Release Assets # Skipped when reused by nightly (which publishes to the rolling tag # itself). `!= false` keeps manual dispatch behavior unchanged, since # workflow_dispatch runs carry no workflow_call inputs. if: ${{ inputs.publish_to_release != false }} needs: [android, linux, windows] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Download all build artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: build-artifacts - name: Stage and upload release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | V=$(grep '^version:' pubspec.yaml | awk '{print $2}') TAG="v${V%%+*}" echo "Target tag: $TAG" mkdir -p release_assets find build-artifacts -type f \( -name "*.apk" -o -name "*.deb" -o -name "*.rpm" -o -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.zip" \) | while read -r file; do fname=$(basename "$file") echo "Staging asset: $fname from $file" cp "$file" "release_assets/$fname" done echo "=== Release Assets ===" ls -lh release_assets/ if ! gh release view "$TAG" >/dev/null 2>&1; then gh release create "$TAG" release_assets/* --title "Grablytic $TAG" --generate-notes --latest else gh release upload "$TAG" release_assets/* --clobber gh release edit "$TAG" --latest --prerelease=false fi