Repository navigation
Add repo imagery: README logo and social preview card #62
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Continuous integration: every pull request and every push to main. | |
| # | |
| # This workflow never cuts a release. That is Release (release.yml), triggered | |
| # by a v* tag or run by hand, and it is deliberately a separate file: a build | |
| # that runs constantly and a build that produces something permanent want | |
| # different triggers, different concurrency and different blast radius. Both | |
| # build through .github/actions/build-apk, so what CI tests is what a release | |
| # ships. | |
| # | |
| # - JDK 17 rather than the Android starter template's 11, which AGP 8.x | |
| # refuses to run on. | |
| # - The debug APK uploaded as an artifact, so a build can be sideloaded onto a | |
| # phone without a local Android SDK. | |
| # - Pull request builds published to a rolling prerelease and linked from the | |
| # pull request, because an artifact is a login-walled zip and a release asset | |
| # is a direct .apk URL you can tap on the phone. | |
| # - Reports uploaded on failure, so a red run can be read without re-running it. | |
| name: Android CI | |
| on: | |
| push: | |
| branches: [ "main" ] | |
| pull_request: | |
| branches: [ "main" ] | |
| # Builds an APK from any branch on demand, as an artifact. Deliberately does | |
| # not publish: an arbitrary branch has no release worth naming. | |
| workflow_dispatch: | |
| # Pushes in quick succession overlap, and a slower older run finishing last | |
| # leaves the release asset and the pull request comment pointing at an older | |
| # build than the branch actually has. Only the newest run for a ref is worth | |
| # finishing. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # publish the APK to the rolling prerelease | |
| pull-requests: write # link it from the pull request | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build the APK | |
| id: build | |
| uses: ./.github/actions/build-apk | |
| with: | |
| pr_number: ${{ github.event.pull_request.number }} | |
| pr_sha: ${{ github.event.pull_request.head.sha }} | |
| keystore_base64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} | |
| - name: Upload debug APK | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: oh-tello-debug-apk | |
| path: ${{ steps.build.outputs.apk }} | |
| if-no-files-found: error | |
| # Pull requests only. Pushes to main build and test but publish nothing, so | |
| # a release is never quietly replaced by the next merge. Fork pull requests | |
| # are skipped too: their tokens are read-only, so publishing would fail the | |
| # build rather than just not happening. | |
| - name: Publish the APK to the pull request's prerelease | |
| id: publish | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| # The pull request head, not the ephemeral merge commit: the tag has to | |
| # point at something that exists on the remote. | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| APK: ${{ steps.build.outputs.apk }} | |
| run: | | |
| set -euo pipefail | |
| tag="pr-$PR_NUMBER" | |
| gh release view "$tag" --repo "$REPO" >/dev/null 2>&1 || \ | |
| gh release create "$tag" --repo "$REPO" --target "$HEAD_SHA" --prerelease \ | |
| --title "PR #$PR_NUMBER debug build" \ | |
| --notes 'Test build for this pull request, replaced on every push. Debug-signed, | |
| so it installs without a keystore and is not suitable for distribution.' | |
| # The filename carries a timestamp and commit, so each build is a new | |
| # asset rather than a replacement. Drop the previous ones: the prerelease | |
| # should hold the current build and nothing else, so there is no way to | |
| # install a stale APK by picking the wrong row. | |
| stale=$(gh release view "$tag" --repo "$REPO" --json assets --jq '.assets[].name' \ | |
| | grep '\.apk$' | grep -v "^$APK$" || true) | |
| while IFS= read -r asset; do | |
| [ -n "$asset" ] || continue | |
| # Tidying, not the job: the API 502s now and then, and the upload below | |
| # publishes the new build regardless. Worth a warning, not a failed build. | |
| gh release delete-asset "$tag" "$asset" --repo "$REPO" --yes \ | |
| || echo "::warning::could not delete stale asset $asset" | |
| done <<< "$stale" | |
| gh release upload "$tag" "$APK" --repo "$REPO" --clobber | |
| echo "url=https://github.com/$REPO/releases/download/$tag/$APK" >> "$GITHUB_OUTPUT" | |
| # One comment, edited in place on later pushes, rather than a new one per build. | |
| - name: Link the APK from the pull request | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| APK_URL: ${{ steps.publish.outputs.url }} | |
| APK: ${{ steps.build.outputs.apk }} | |
| BUILT_AT: ${{ steps.build.outputs.built_at }} | |
| SHARED_KEY: ${{ steps.build.outputs.signing_shared }} | |
| run: | | |
| set -euo pipefail | |
| marker='<!-- oh-tello-apk -->' | |
| if [ "$SHARED_KEY" = "true" ]; then | |
| signing_note='Builds share a debug signing key, so this replaces any previous Oh-Tello | |
| build. If Android refuses to install it, the copy on the device predates | |
| that shared key — uninstall it once and later builds will replace each | |
| other cleanly.' | |
| else | |
| signing_note='**This build carries a throwaway signing key**, because the | |
| DEBUG_KEYSTORE_BASE64 secret is not set. It will not install over an existing | |
| Oh-Tello — uninstall that one first.' | |
| fi | |
| body=$(cat <<EOF | |
| $marker | |
| ### 📲 [$APK]($APK_URL) | |
| Built from \`${HEAD_SHA:0:7}\` at $BUILT_AT. Tap the link on the phone and | |
| Android will offer to install it — no zip to unpack, no sign-in. | |
| The same string is stamped into the app version, so once installed you can | |
| still tell which build it is: it shows under the title on the first screen, | |
| and in Settings > Apps > Oh-Tello. This comment and the release asset are | |
| both replaced as new commits are pushed. | |
| $signing_note | |
| --- | |
| _Generated by [Claude Code](https://claude.ai/code)_ | |
| EOF | |
| ) | |
| id=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ | |
| --jq "map(select(.body | contains(\"$marker\"))) | .[0].id // empty" | head -n1) | |
| if [ -n "$id" ]; then | |
| gh api -X PATCH "repos/$REPO/issues/comments/$id" -f body="$body" >/dev/null | |
| else | |
| gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null | |
| fi | |
| - name: Upload reports | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: reports | |
| path: | | |
| app/build/reports/ | |
| app/build/test-results/ | |
| if-no-files-found: ignore |