name: Build on: push: tags: ['v*'] permissions: contents: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.26' cache: true - name: Cache Go build uses: actions/cache@v4 with: path: ~/.cache/go-build key: gobuild-test-${{ runner.os }}-${{ hashFiles('**/go.sum') }} restore-keys: gobuild-test-${{ runner.os }}- - name: Test + vet run: | go vet ./... & go test -race -count=1 ./... wait build: needs: test runs-on: ubuntu-latest strategy: matrix: include: - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: darwin goarch: amd64 - goos: darwin goarch: arm64 - goos: freebsd goarch: amd64 - goos: freebsd goarch: arm64 - goos: windows goarch: amd64 - goos: android goarch: arm64 - goos: android goarch: arm goarm: '7' steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.26' cache: true - name: Cache Go build uses: actions/cache@v4 with: path: ~/.cache/go-build key: gobuild-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }} restore-keys: gobuild-${{ matrix.goos }}-${{ matrix.goarch }}- - name: Install UPX if: matrix.goos == 'linux' || matrix.goos == 'windows' run: sudo apt-get install -y upx-ucl - name: Build Server if: matrix.goos != 'android' env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} CGO_ENABLED: '0' run: | VERSION=${GITHUB_REF_NAME:-dev} COMMIT=$(git rev-parse --short HEAD) DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS="-s -w -X github.com/sartoopjj/thefeed/internal/version.Version=${VERSION} -X github.com/sartoopjj/thefeed/internal/version.Commit=${COMMIT} -X github.com/sartoopjj/thefeed/internal/version.Date=${DATE}" ext="" if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi go build -trimpath -ldflags="${LDFLAGS}" -o build/thefeed-server-${{ matrix.goos }}-${{ matrix.goarch }}${ext} ./cmd/server - name: Build Client env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm || '' }} CGO_ENABLED: '0' run: | VERSION=${GITHUB_REF_NAME:-dev} COMMIT=$(git rev-parse --short HEAD) DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) ext="" BUILD_MODE="" if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi # AssetTemplate matches the published filename in the # thefeed-files repo so the in-app update prompt links straight # to the right binary. {V} is replaced at runtime with the # version string read from the public VERSION file. if [ "${{ matrix.goos }}" = "android" ]; then ASSET_TEMPLATE="thefeed-client-android-${{ matrix.goarch }}" else ASSET_TEMPLATE="thefeed-client-{V}-${{ matrix.goos }}-${{ matrix.goarch }}${ext}" fi LDFLAGS="-s -w -X github.com/sartoopjj/thefeed/internal/version.Version=${VERSION} -X github.com/sartoopjj/thefeed/internal/version.Commit=${COMMIT} -X github.com/sartoopjj/thefeed/internal/version.Date=${DATE} -X github.com/sartoopjj/thefeed/internal/version.AssetTemplate=${ASSET_TEMPLATE}" # Modern Android requires PIE for executables launched via exec(), # and several heuristic AV engines (Kaspersky Boogr.gsh, # several VT vendors) flag non-PIE bundled binaries as suspicious. # Force PIE for the binaries that ship inside the APK. if [ "${{ matrix.goos }}" = "android" ]; then BUILD_MODE="-buildmode=pie" fi # Android: build with cgo so DNS resolution goes through bionic # libc / netd instead of Go's pure-Go resolver, which on Android # finds /etc/resolv.conf empty and dies with "[::1]:53 connection # refused". Wire the right NDK clang per architecture. if [ "${{ matrix.goos }}" = "android" ]; then export CGO_ENABLED=1 case "${{ matrix.goarch }}" in arm) export CC="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang" ;; arm64) export CC="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" ;; esac test -x "$CC" || { echo "NDK clang not found at $CC"; exit 1; } fi if [ "${{ matrix.goos }}" = "android" ] && [ "${{ matrix.goarch }}" = "arm64" ]; then out="build/thefeed-client-android-arm64" elif [ "${{ matrix.goos }}" = "android" ] && [ "${{ matrix.goarch }}" = "arm" ]; then out="build/thefeed-client-android-arm" else out="build/thefeed-client-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}${ext}" fi go build -trimpath -buildvcs=false $BUILD_MODE -ldflags="${LDFLAGS}" -o "$out" ./cmd/client - name: Compress with UPX if: matrix.goos == 'linux' || matrix.goos == 'windows' run: | # Keep best/lzma for small binaries; xargs -P does them # in parallel across CPUs so wall time stays low. find build -maxdepth 1 -type f -print0 \ | xargs -0 -n1 -P "$(nproc)" -I{} sh -c 'upx --best --lzma "$1" || true' _ {} - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} path: build/ android-apk: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts merge-multiple: true - name: Stage Android client binary as JNI library run: | mkdir -p android/app/src/main/jniLibs/arm64-v8a mkdir -p android/app/src/main/jniLibs/armeabi-v7a test -f artifacts/thefeed-client-android-arm64 test -f artifacts/thefeed-client-android-arm cp artifacts/thefeed-client-android-arm64 android/app/src/main/jniLibs/arm64-v8a/libthefeed.so cp artifacts/thefeed-client-android-arm android/app/src/main/jniLibs/armeabi-v7a/libthefeed.so - name: Decode signing keystore env: KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} run: | if [ -n "$KEYSTORE_BASE64" ]; then echo "$KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks echo "Keystore decoded successfully" else echo "No KEYSTORE_BASE64 secret set — will use debug signing" fi - name: Set up Java uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Build Android APK working-directory: android env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | VERSION=${GITHUB_REF_NAME:-dev} if [ ! -x ./gradlew ]; then gradle wrapper --gradle-version 8.10.2; fi if [ -f app/keystore.jks ]; then BT=release; TASK=assembleRelease; else BT=debug; TASK=assembleDebug; fi # No `clean` — fresh checkout is already clean and the # Gradle build cache can reuse work between runs. ./gradlew --no-daemon --build-cache $TASK APK_DIR=app/build/outputs/apk/$BT cp "$APK_DIR"/app-arm64-v8a-${BT}.apk ../artifacts/thefeed-android-${VERSION}-arm64-v8a.apk cp "$APK_DIR"/app-armeabi-v7a-${BT}.apk ../artifacts/thefeed-android-${VERSION}-armeabi-v7a.apk - name: Upload Android APK artifacts uses: actions/upload-artifact@v4 with: name: thefeed-android-apk path: | artifacts/thefeed-android-*-arm64-v8a.apk artifacts/thefeed-android-*-armeabi-v7a.apk ios-ipa: needs: test runs-on: macos-14 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.26' cache: true - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode.app - name: Install gomobile + gobind run: | go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gobind@latest gomobile init go get golang.org/x/mobile/bind golang.org/x/mobile/bind/objc go mod tidy - name: Build Mobile.xcframework run: | VERSION=${GITHUB_REF_NAME:-dev} COMMIT=$(git rev-parse --short HEAD) DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS="-X github.com/sartoopjj/thefeed/internal/version.Version=${VERSION} -X github.com/sartoopjj/thefeed/internal/version.Commit=${COMMIT} -X github.com/sartoopjj/thefeed/internal/version.Date=${DATE}" gomobile bind -iosversion=14.0 -target=ios,iossimulator -ldflags="${LDFLAGS}" -o ios/Mobile.xcframework ./mobile - name: Archive (unsigned) run: | VERSION=${GITHUB_REF_NAME:-dev} MARKETING_VERSION=${VERSION#v} BUILD_NUMBER=$(git rev-list --count HEAD) xcodebuild \ -project ios/Thefeed.xcodeproj \ -scheme Thefeed \ -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath build/Thefeed.xcarchive \ archive \ MARKETING_VERSION="${MARKETING_VERSION}" \ CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO \ DEVELOPMENT_TEAM="" - name: Pack unsigned IPA run: | VERSION=${GITHUB_REF_NAME:-dev} mkdir -p build/Payload cp -r build/Thefeed.xcarchive/Products/Applications/Thefeed.app build/Payload/ (cd build && zip -qry "thefeed-ios-${VERSION}-unsigned.ipa" Payload) ls -lh build/*.ipa - name: Upload iOS IPA artifact uses: actions/upload-artifact@v4 with: name: thefeed-ios-ipa path: build/thefeed-ios-*-unsigned.ipa release: needs: [build, android-apk, ios-ipa] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts merge-multiple: true - name: Create Release uses: softprops/action-gh-release@v2 with: files: artifacts/* generate_release_notes: true prerelease: ${{ contains(github.ref_name, '-') }} append_body: true body: | ## Downloads | Platform | Architecture | Download | |----------|-------------|----------| | Linux | amd64 | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-linux-amd64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-linux-amd64) | | Linux | arm64 | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-linux-arm64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-linux-arm64) | | macOS | amd64 (Intel) | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-darwin-amd64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-darwin-amd64) | | macOS | arm64 (Apple Silicon) | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-darwin-arm64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-darwin-arm64) | | FreeBSD | amd64 | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-freebsd-amd64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-freebsd-amd64) | | FreeBSD | arm64 | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-freebsd-arm64) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-freebsd-arm64) | | Windows | amd64 | [server-سرور](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-server-windows-amd64.exe) / [client-کلاینت](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-client-${{ github.ref_name }}-windows-amd64.exe) | | Android | arm64-v8a (most modern phones) | [thefeed-android-${{ github.ref_name }}-arm64-v8a.apk - اندروید (گوشی‌های جدید)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-android-${{ github.ref_name }}-arm64-v8a.apk) | | Android | armeabi-v7a (older 32-bit phones) | [thefeed-android-${{ github.ref_name }}-armeabi-v7a.apk - اندروید (دستگاه‌های قدیمی‌تر)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-android-${{ github.ref_name }}-armeabi-v7a.apk) | | iOS (unsigned) | universal | [thefeed-ios-${{ github.ref_name }}-unsigned.ipa](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/thefeed-ios-${{ github.ref_name }}-unsigned.ipa) | **iOS / iPadOS (preview, iOS 14+):** the `.ipa` is unsigned. Re-sign it with your own Apple ID and provisioning profile (AltStore, Sideloadly, or `xcrun altool` upload to your own TestFlight). It will not install directly from a download.