diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 984e4b4..e7f9d67 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -1,24 +1,73 @@ -name: Flutter CI +name: Build and Release Artifacts on: pull_request: paths: - ".github/workflows/flutter-ci.yml" + - "README.md" - "flutter_app/**" push: branches: - main tags: - "v*" + paths: + - ".github/workflows/flutter-ci.yml" + - "README.md" + - "flutter_app/**" workflow_dispatch: + inputs: + release_tag: + description: "Optional tag to create/update a GitHub Release for this build, for example v1.0.0." + required: false + type: string + prerelease: + description: "Mark a manually created GitHub Release as a pre-release." + required: false + default: true + type: boolean permissions: contents: write +env: + FLUTTER_CHANNEL: stable + FLUTTER_APP_DIR: flutter_app + jobs: - flutter: - name: Test and build Flutter app + validate: + name: Validate Flutter app + runs-on: ubuntu-latest + defaults: + run: + working-directory: flutter_app + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true + + - name: Resolve Flutter dependencies + run: flutter pub get + + - name: Check formatting + run: dart format --output=none --set-exit-if-changed lib test + + - name: Analyze Flutter app + run: flutter analyze + + - name: Run Flutter tests + run: flutter test + + build-android: + name: Build Android APK runs-on: ubuntu-latest + needs: validate defaults: run: working-directory: flutter_app @@ -36,14 +85,11 @@ jobs: distribution: temurin java-version: "21" - - name: Install Flutter stable - run: | - FLUTTER_ROOT="$RUNNER_TOOL_CACHE/flutter-stable" - rm -rf "$FLUTTER_ROOT" - git clone --depth 1 --branch stable https://github.com/flutter/flutter.git "$FLUTTER_ROOT" - echo "FLUTTER_ROOT=$FLUTTER_ROOT" >> "$GITHUB_ENV" - echo "$FLUTTER_ROOT/bin" >> "$GITHUB_PATH" - "$FLUTTER_ROOT/bin/flutter" --version + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true - name: Install Android SDK packages run: | @@ -55,43 +101,272 @@ jobs: - name: Resolve Flutter dependencies run: flutter pub get - - name: Check formatting - run: dart format --output=none --set-exit-if-changed lib test - - - name: Analyze Flutter app - run: flutter analyze - - - name: Run Flutter tests - run: flutter test - - - name: Build Android debug APK - run: flutter build apk --debug - - name: Build Android release APK run: flutter build apk --release - - name: Stage Android release APK + - name: Stage Android artifact run: | + set -euo pipefail + version="$(awk '/^version:/ { print $2 }' pubspec.yaml | tr '+' '-')" mkdir -p "$RUNNER_TEMP/studyos-agent" cp build/app/outputs/flutter-apk/app-release.apk \ - "$RUNNER_TEMP/studyos-agent/studyos-agent-android-release.apk" + "$RUNNER_TEMP/studyos-agent/studyos-agent-${version}-android.apk" + (cd "$RUNNER_TEMP/studyos-agent" && sha256sum "studyos-agent-${version}-android.apk" > "studyos-agent-${version}-android.apk.sha256") + + - name: Upload Android APK + uses: actions/upload-artifact@v7 + with: + name: studyos-agent-android-apk + path: ${{ runner.temp }}/studyos-agent/* + if-no-files-found: error + + build-web: + name: Build Web bundle + runs-on: ubuntu-latest + needs: validate + defaults: + run: + working-directory: flutter_app + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true + + - name: Resolve Flutter dependencies + run: flutter pub get + + - name: Build web app + run: flutter build web --release + + - name: Stage web artifact + run: | + set -euo pipefail + version="$(awk '/^version:/ { print $2 }' pubspec.yaml | tr '+' '-')" + artifact="$RUNNER_TEMP/studyos-agent-web-${version}.zip" + (cd build/web && zip -r "$artifact" .) + sha256sum "$artifact" > "$artifact.sha256" + + - name: Upload web bundle + uses: actions/upload-artifact@v7 + with: + name: studyos-agent-web + path: | + ${{ runner.temp }}/studyos-agent-web-*.zip + ${{ runner.temp }}/studyos-agent-web-*.zip.sha256 + if-no-files-found: error + + build-linux: + name: Build Linux x64 bundle + runs-on: ubuntu-latest + needs: validate + defaults: + run: + working-directory: flutter_app + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true + + - name: Enable Linux desktop + run: flutter config --enable-linux-desktop + + - name: Resolve Flutter dependencies + run: flutter pub get + + - name: Build Linux app + run: flutter build linux --release + + - name: Stage Linux artifact + run: | + set -euo pipefail + version="$(awk '/^version:/ { print $2 }' pubspec.yaml | tr '+' '-')" + artifact="$RUNNER_TEMP/studyos-agent-${version}-linux-x64.tar.gz" + tar -C build/linux/x64/release -czf "$artifact" bundle + sha256sum "$artifact" > "$artifact.sha256" + + - name: Upload Linux bundle + uses: actions/upload-artifact@v7 + with: + name: studyos-agent-linux-x64 + path: | + ${{ runner.temp }}/studyos-agent-*-linux-x64.tar.gz + ${{ runner.temp }}/studyos-agent-*-linux-x64.tar.gz.sha256 + if-no-files-found: error + + build-macos: + name: Build macOS app + runs-on: macos-latest + needs: validate + defaults: + run: + working-directory: flutter_app + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true - - name: Upload Android APK artifact + - name: Enable macOS desktop + run: flutter config --enable-macos-desktop + + - name: Resolve Flutter dependencies + run: flutter pub get + + - name: Build macOS app + run: flutter build macos --release + + - name: Stage macOS artifact + run: | + set -euo pipefail + version="$(awk '/^version:/ { print $2 }' pubspec.yaml | tr '+' '-')" + artifact="$RUNNER_TEMP/studyos-agent-${version}-macos.zip" + ditto -c -k --sequesterRsrc --keepParent \ + build/macos/Build/Products/Release/studyos_agent.app \ + "$artifact" + shasum -a 256 "$artifact" > "$artifact.sha256" + + - name: Upload macOS app uses: actions/upload-artifact@v7 with: - name: studyos-agent-android-release-apk - path: ${{ runner.temp }}/studyos-agent/studyos-agent-android-release.apk + name: studyos-agent-macos + path: | + ${{ runner.temp }}/studyos-agent-*-macos.zip + ${{ runner.temp }}/studyos-agent-*-macos.zip.sha256 if-no-files-found: error - - name: Attach APK to GitHub Release - if: startsWith(github.ref, 'refs/tags/v') + build-windows: + name: Build Windows x64 app + runs-on: windows-latest + needs: validate + defaults: + run: + working-directory: flutter_app + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + cache: true + + - name: Enable Windows desktop + run: flutter config --enable-windows-desktop + + - name: Resolve Flutter dependencies + run: flutter pub get + + - name: Build Windows app + run: flutter build windows --release + + - name: Stage Windows artifact + shell: pwsh + run: | + $version = (Select-String -Path pubspec.yaml -Pattern '^version:\s*(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }).Replace('+', '-') + $artifact = "$env:RUNNER_TEMP\studyos-agent-$version-windows-x64.zip" + Compress-Archive -Path build\windows\x64\runner\Release\* -DestinationPath $artifact -Force + Get-FileHash -Algorithm SHA256 $artifact | ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $artifact -Leaf)" } | Set-Content "$artifact.sha256" + + - name: Upload Windows app + uses: actions/upload-artifact@v7 + with: + name: studyos-agent-windows-x64 + path: | + ${{ runner.temp }}/studyos-agent-*-windows-x64.zip + ${{ runner.temp }}/studyos-agent-*-windows-x64.zip.sha256 + if-no-files-found: error + + release: + name: Publish GitHub Release + runs-on: ubuntu-latest + needs: + - build-android + - build-web + - build-linux + - build-macos + - build-windows + if: github.event_name != 'pull_request' + + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Decide whether to publish + id: release + env: + INPUT_RELEASE_TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + release_tag="$GITHUB_REF_NAME" + elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && -n "${INPUT_RELEASE_TAG:-}" ]]; then + release_tag="$INPUT_RELEASE_TAG" + elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]]; then + current_version="$(awk '/^version:/ { print $2 }' flutter_app/pubspec.yaml)" + previous_version="$(git show HEAD^:flutter_app/pubspec.yaml 2>/dev/null | awk '/^version:/ { print $2 }' || true)" + if [[ -z "$current_version" || "$current_version" == "$previous_version" ]]; then + echo "should_publish=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + release_tag="v$current_version" + else + echo "should_publish=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "should_publish=true" >> "$GITHUB_OUTPUT" + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + + - name: Download build artifacts + if: steps.release.outputs.should_publish == 'true' + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/studyos-release + merge-multiple: true + + - name: Publish release artifacts + if: steps.release.outputs.should_publish == 'true' env: GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.release.outputs.release_tag }} + INPUT_PRERELEASE: ${{ inputs.prerelease }} run: | - gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || \ - gh release create "$GITHUB_REF_NAME" \ - --title "$GITHUB_REF_NAME" \ - --notes "StudyOS Agent Android release for $GITHUB_REF_NAME." - gh release upload "$GITHUB_REF_NAME" \ - "$RUNNER_TEMP/studyos-agent/studyos-agent-android-release.apk" \ + set -euo pipefail + + release_args=(--title "$RELEASE_TAG" --notes "StudyOS Agent builds for $RELEASE_TAG." --target "$GITHUB_SHA") + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "${INPUT_PRERELEASE:-false}" == "true" ]]; then + release_args+=(--prerelease) + fi + + gh release view "$RELEASE_TAG" >/dev/null 2>&1 || \ + gh release create "$RELEASE_TAG" "${release_args[@]}" + gh release upload "$RELEASE_TAG" \ + "$RUNNER_TEMP"/studyos-release/* \ --clobber diff --git a/README.md b/README.md index 1c6b1c0..8e4fece 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,23 @@ flutter run The first migration PR is intentionally a bridge-first implementation. It does not rewrite every Android service in Dart because many current features are Android-specific and should stay behind native adapters. + +## Installing builds + +Installable app bundles are built by the `Build and Release Artifacts` GitHub +Actions workflow for Android, web, Linux, macOS, and Windows. iOS is skipped +because distribution needs to go through Apple's signing and App Store/TestFlight +flow. + +- For branch and pull request builds, open the workflow run and download the + platform artifact you need, such as `studyos-agent-android-apk`, + `studyos-agent-web`, `studyos-agent-linux-x64`, `studyos-agent-macos`, or + `studyos-agent-windows-x64`. +- For tagged releases such as `v1.0.0`, or a `flutter_app/pubspec.yaml` + version change on `main`, download the platform artifact from the matching + GitHub Release. + +Android may ask you to allow installing APKs from your browser or file manager +before opening the downloaded build. The APK is currently signed with the +debug signing configuration from the Flutter Android runner, so it is suitable +for course testing but not yet for app-store distribution.