name: release # Builds every release artifact on GitHub's runners and publishes them with checksums as a GitHub release. # scripts/release.ps1 dispatches this workflow. Start it from Actions › release › Run workflow after bumping the version. # # The APK must be signed with the same key as the app already on the phones, or Android refuses the update. Add the # key as repository secrets: PHONEBOX_KEYSTORE_BASE64 (base64 of the .jks / .keystore file), PHONEBOX_STORE_PASSWORD, # PHONEBOX_KEY_ALIAS and PHONEBOX_KEY_PASSWORD. Without them the workflow stops before publishing anything. on: workflow_dispatch: inputs: draft: description: Create the release as a draft type: boolean default: true permissions: contents: read concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: checks: uses: ./.github/workflows/build.yml version: needs: checks runs-on: ubuntu-latest outputs: version: ${{ steps.v.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - id: v name: Read and check the version run: | v=$(sed -n 's:.*\(.*\).*:\1:p' agent/PhoneBox.Agent/PhoneBox.Agent.csproj | head -n1) grep -q "versionName = \"$v\"" android/app/build.gradle.kts || { echo "::error::android versionName does not match $v"; exit 1; } if git ls-remote --exit-code --tags origin "refs/tags/v$v" >/dev/null; then echo "::error::v$v is already released; bump the version first"; exit 1; fi echo "version=$v" >> "$GITHUB_OUTPUT" - name: Check release metadata run: python3 tools/check-release.py apk: needs: version runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: distribution: temurin java-version: 17 cache: gradle - name: Signing key env: KEYSTORE: ${{ secrets.PHONEBOX_KEYSTORE_BASE64 }} run: | if [ -z "$KEYSTORE" ]; then echo "::error::add the PHONEBOX_KEYSTORE_BASE64 secret (see the top of release.yml)"; exit 1; fi echo "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/release.keystore" - name: Build the release APK working-directory: android env: PHONEBOX_STORE_FILE: ${{ runner.temp }}/release.keystore PHONEBOX_STORE_PASSWORD: ${{ secrets.PHONEBOX_STORE_PASSWORD }} PHONEBOX_KEY_ALIAS: ${{ secrets.PHONEBOX_KEY_ALIAS }} PHONEBOX_KEY_PASSWORD: ${{ secrets.PHONEBOX_KEY_PASSWORD }} run: | chmod +x gradlew ./gradlew assembleRelease --console=plain mkdir -p ../dist cp app/build/outputs/apk/release/app-release.apk "../dist/PhoneBox-${{ needs.version.outputs.version }}.apk" - name: Remove the signing key if: always() run: rm -f "$RUNNER_TEMP/release.keystore" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-apk path: dist/ agent: needs: version strategy: matrix: include: - { os: windows-latest, rid: win-x64, tfm: net10.0-windows10.0.19041.0 } - { os: ubuntu-latest, rid: linux-x64, tfm: net10.0 } - { os: ubuntu-24.04-arm, rid: linux-arm64, tfm: net10.0 } - { os: macos-latest, rid: osx-arm64, tfm: net10.0 } - { os: macos-15-intel, rid: osx-x64, tfm: net10.0 } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: 10.0.x - name: Unit tests run: dotnet test agent/PhoneBox.Agent.Tests --nologo # Self-contained single file: no .NET runtime to install. Not compressed (the zip is barely smaller and a # compressed bundle costs about 90 MB more memory in a process that runs all day); native libraries stay # loose next to the exe instead of being extracted to the temp folder at run time. - name: Publish (self-contained, single file) run: dotnet publish agent/PhoneBox.Agent -c Release -f ${{ matrix.tfm }} -r ${{ matrix.rid }} --self-contained true -p:PublishSingleFile=true -o out --nologo - name: Sign macOS apphost ad hoc if: startsWith(matrix.rid, 'osx-') run: codesign --force --sign - out/PhoneBoxAgent - name: Smoke-test the published agent on its native architecture shell: pwsh run: | $exe = if ($IsWindows) { 'out/PhoneBoxAgent.exe' } else { 'out/PhoneBoxAgent' } python tools/smoke-agent.py $exe --version '${{ needs.version.outputs.version }}' if ($LASTEXITCODE) { exit $LASTEXITCODE } - name: Zip shell: pwsh run: | New-Item -ItemType Directory -Force dist | Out-Null Compress-Archive -Path out/* -DestinationPath "dist/PhoneBoxAgent-${{ needs.version.outputs.version }}-${{ matrix.rid }}.zip" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-agent-${{ matrix.rid }} path: dist/ publish: needs: [version, apk, agent] runs-on: ubuntu-latest permissions: contents: write id-token: write attestations: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: path: dist pattern: release-* merge-multiple: true - name: Attest release artifacts id: provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | dist/*.apk dist/*.zip - name: Save the provenance bundle env: BUNDLE: ${{ steps.provenance.outputs.bundle-path }} run: cp "$BUNDLE" "dist/PhoneBox-${{ needs.version.outputs.version }}.intoto.jsonl" - name: Checksums working-directory: dist run: | test "$(find . -maxdepth 1 -type f | wc -l)" -eq 7 test "$(find . -maxdepth 1 -type f -name '*.zip' | wc -l)" -eq 5 test "$(find . -maxdepth 1 -type f -name '*.apk' | wc -l)" -eq 1 sha256sum * > "SHA256SUMS-${{ needs.version.outputs.version }}.txt" cat "SHA256SUMS-${{ needs.version.outputs.version }}.txt" - name: Release notes from CHANGELOG.md run: | v="${{ needs.version.outputs.version }}" # the section for this version, else the "Unreleased" one section() { awk -v v="$1" '/^## /{ if (on) exit; on = ($2 == v); next } on' CHANGELOG.md; } section "$v" > notes.md [ -s notes.md ] || section Unreleased > notes.md [ -s notes.md ] || echo "PhoneBox $v" > notes.md cat >> notes.md <<'EOF' --- [Code signing policy](https://github.com/ersinkoc/phonebox#code-signing-policy) · [Privacy policy](https://github.com/ersinkoc/phonebox/blob/main/PRIVACY.md) EOF - name: Create the release env: GH_TOKEN: ${{ github.token }} run: | v="${{ needs.version.outputs.version }}" gh release create "v$v" dist/* --title "PhoneBox $v" --notes-file notes.md --target "${{ github.sha }}" ${{ inputs.draft && '--draft' || '' }} # a draft's downloads are not public yet, so the package manifests follow only published releases - name: Update the Scoop bucket and the winget manifests if: ${{ !inputs.draft }} run: | v="${{ needs.version.outputs.version }}" sha=$(sha256sum "dist/PhoneBoxAgent-$v-win-x64.zip" | cut -d' ' -f1) python3 - "$v" "$sha" <<'PY' import json, sys v, sha = sys.argv[1], sys.argv[2] p = 'bucket/phonebox.json' m = json.load(open(p, encoding='utf-8')) m['version'] = v m['architecture']['64bit']['url'] = f'https://github.com/ersinkoc/phonebox/releases/download/v{v}/PhoneBoxAgent-{v}-win-x64.zip' m['architecture']['64bit']['hash'] = sha # the agent is self-contained since 0.9.7: no .NET runtime to suggest m.get('suggest', {}).pop('.NET Desktop Runtime 10', None) open(p, 'w', encoding='utf-8').write(json.dumps(m, indent=4, ensure_ascii=False) + '\n') PY python3 packaging/winget/make-manifests.py "$v" "$sha" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add bucket/phonebox.json packaging/winget/manifests git commit -m "Scoop and winget manifests for $v" git push origin HEAD:main