diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml new file mode 100644 index 0000000..77a3558 --- /dev/null +++ b/.github/workflows/apk.yml @@ -0,0 +1,139 @@ +name: Release APK +run-name: Release APK ${{ inputs.version }} + +on: + workflow_dispatch: + inputs: + version: + description: 'Release tag to create, e.g. v1.0.0' + required: true + type: string + +concurrency: release-apk + +permissions: {} + +jobs: + apk: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write # create the tag + release + id-token: write # sign the build attestation + attestations: write + env: + TAG: ${{ inputs.version }} + steps: + - name: Validate version + run: | + [[ "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || { echo "::error::version must look like v1.0.0"; exit 1; } + MA=${BASH_REMATCH[1]} MI=${BASH_REMATCH[2]} PA=${BASH_REMATCH[3]} + (( MI < 100 && PA < 100 )) || { echo "::error::minor and patch must be < 100"; exit 1; } + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + echo "CODE=$((MA*10000 + MI*100 + PA))" >> $GITHUB_ENV + echo "APK=sune-$TAG.apk" >> $GITHUB_ENV + echo "BT=$RUNNER_TEMP/sdk/build-tools/36.1.0" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Fail if the release already exists + if: github.ref == 'refs/heads/master' + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "::error::$TAG already exists"; exit 1 + fi + + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: 17 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + + - name: Install Android SDK + Bubblewrap + run: | + SDK=$RUNNER_TEMP/sdk + mkdir -p "$SDK/bin" # bubblewrap only checks that this exists + SM=$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager + yes | "$SM" --sdk_root="$SDK" --licenses >/dev/null || true # yes exits on SIGPIPE + "$SM" --sdk_root="$SDK" "build-tools;36.1.0" "platforms;android-36" >/dev/null + npm install --global --ignore-scripts @bubblewrap/cli@1.25.0 + mkdir -p ~/.bubblewrap + jq -n --arg jdk "$JAVA_HOME" --arg sdk "$SDK" '{jdkPath:$jdk,androidSdkPath:$sdk}' > ~/.bubblewrap/config.json + + - name: Build + sign + env: + ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + BUBBLEWRAP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} + BUBBLEWRAP_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} + run: | + B=$RUNNER_TEMP/build OUT=$RUNNER_TEMP/out + mkdir -p "$B" "$OUT" + echo "$ANDROID_KEYSTORE_B64" | base64 -d > "$B/sune.jks" + # pin the icon to this exact commit instead of whatever the site serves today + ICON="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$GITHUB_SHA/public/appstore_content/%E2%9C%BA.png" + jq --arg v "$VERSION" --argjson c "$CODE" --arg a "$ANDROID_KEY_ALIAS" --arg i "$ICON" \ + '.appVersion=$v | .appVersionCode=$c | .signingKey.alias=$a | .iconUrl=$i | .maskableIconUrl=$i' \ + android/twa-manifest.json > "$B/twa-manifest.json" + cd "$B" + bubblewrap update --manifest ./twa-manifest.json --skipVersionUpgrade + bubblewrap build --manifest ./twa-manifest.json --skipPwaValidation + mv app-release-signed.apk "$OUT/$APK" + rm -f sune.jks + echo "OUT=$OUT" >> $GITHUB_ENV + + - name: Verify the APK + run: | + cd "$OUT" + "$BT/apksigner" verify --verbose --print-certs "$APK" | tee verify.txt + grep -q "Number of signers: 1" verify.txt + GOT=$(sed -n 's/.*certificate SHA-256 digest: //p' verify.txt | sed -n 1p) + WANT=$(jq -r '.[].target | select(.package_name=="chat.sune") | .sha256_cert_fingerprints[]' "$GITHUB_WORKSPACE/public/.well-known/assetlinks.json" | tr -d ':' | tr A-F a-f) + [ "$GOT" = "$WANT" ] || { echo "::error::signing cert $GOT does not match assetlinks.json ($WANT)"; exit 1; } + "$BT/aapt2" dump badging "$APK" > full-badging.txt + grep -m1 '^package:' full-badging.txt | tee badging.txt + grep -q "name='chat.sune' versionCode='$CODE' versionName='$VERSION'" badging.txt + sha256sum "$APK" | tee "$APK.sha256" + echo "SIGNER=$GOT" >> $GITHUB_ENV + + - name: Attest build provenance + if: github.ref == 'refs/heads/master' + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: ${{ runner.temp }}/out/sune-${{ inputs.version }}.apk + + - name: Publish release + if: github.ref == 'refs/heads/master' + env: + GH_TOKEN: ${{ github.token }} + run: | + cd "$OUT" + SHA=$(cut -d' ' -f1 "$APK.sha256") + cat > notes.md <