From 17ef33ee31121d9af8a74a9fd1e85ab7cb871850 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Thu, 1 Oct 2026 21:19:55 -0400 Subject: [PATCH 1/3] fix: find the stable app release and accept fork builds when signing PR test builds --- .github/workflows/build-gamescope.yml | 2 +- .github/workflows/build-wlroots.yml | 2 +- .github/workflows/pr-release-publish.yml | 23 +++++++++++++++++------ tools/release/update_catalog.py | 18 ++++++++++++------ tools/tests/test_update_catalog.py | 14 ++++++++++++++ 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-gamescope.yml b/.github/workflows/build-gamescope.yml index 2b2ddbe77..73b97f61a 100644 --- a/.github/workflows/build-gamescope.yml +++ b/.github/workflows/build-gamescope.yml @@ -42,4 +42,4 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber + gh release create "${{ inputs.tag }}" --latest=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber diff --git a/.github/workflows/build-wlroots.yml b/.github/workflows/build-wlroots.yml index 2bee0ac7b..e36d5d1a9 100644 --- a/.github/workflows/build-wlroots.yml +++ b/.github/workflows/build-wlroots.yml @@ -43,4 +43,4 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber + gh release create "${{ inputs.tag }}" --latest=false --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber diff --git a/.github/workflows/pr-release-publish.yml b/.github/workflows/pr-release-publish.yml index f2b6b9941..f27a5a4af 100644 --- a/.github/workflows/pr-release-publish.yml +++ b/.github/workflows/pr-release-publish.yml @@ -88,7 +88,8 @@ jobs: esac fi pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER") - jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null + jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null || + { echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; } head=$(jq -r '.head.sha' <<<"$pr") base=$(jq -r '.base.sha' <<<"$pr") @@ -114,15 +115,24 @@ jobs: sleep 30 done [[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; } + # GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is + # tied to this PR by its head repository, branch and commit instead. run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id") - jq -e --arg head "$head" --arg base "$base" --argjson number "$PR_NUMBER" ' + jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" ' .event == "pull_request" and .head_sha == $head and .conclusion == "success" and - any(.pull_requests[]?; .number == $number and .head.sha == $head and .base.sha == $base) - ' <<<"$run" >/dev/null + .head_repository.full_name == $repo and .head_branch == $ref + ' <<<"$run" >/dev/null || + { echo "::error::Build run $run_id is not a green build of PR #$PR_NUMBER at ${head:0:7}"; exit 1; } sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}} tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1) - stable_tag=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name) + # Not releases/latest: gamescope and wlroots releases can hold that mark. The stable app + # release is the newest published one that ships DroidDeck-.apk. + stable_tag=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" --jq ' + [.[] | select((.draft | not) and (.prerelease | not)) | .tag_name as $t | + select(any(.assets[]; .name == "DroidDeck-\($t).apk"))] | + sort_by(.published_at) | last | .tag_name // empty') + [[ -n "$stable_tag" ]] || { echo "::error::No published DroidDeck release to compare against"; exit 1; } mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr" gh release download "$stable_tag" -R "$GITHUB_REPOSITORY" \ -p "DroidDeck-$stable_tag.apk" -D "$RUNNER_TEMP/stable" @@ -143,7 +153,8 @@ jobs: jq -e --arg head "$head" --arg base "$base" ' .state == "open" and .draft == false and .base.ref == "main" and .head.sha == $head and .base.sha == $base - ' <<<"$current" >/dev/null + ' <<<"$current" >/dev/null || + { echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; } { echo "pr_number=$PR_NUMBER" echo "run_id=$run_id" diff --git a/tools/release/update_catalog.py b/tools/release/update_catalog.py index ee0799273..67b800ba2 100755 --- a/tools/release/update_catalog.py +++ b/tools/release/update_catalog.py @@ -243,13 +243,19 @@ def main_commit(release: dict) -> str | None: return match.group(1) if match else None +def stable_app_release(releases: list[dict]) -> dict | None: + """The newest published app release. The repo also publishes gamescope and wlroots + releases that GitHub may mark latest, so an app release is one shipping DroidDeck-.apk.""" + apps = [ + r for r in releases + if not r.get("draft") and not r.get("prerelease") + and any(a.get("name") == f"DroidDeck-{r.get('tag_name')}.apk" for a in r.get("assets") or []) + ] + return max(apps, key=lambda r: r.get("published_at") or r.get("created_at") or "", default=None) + + def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) -> dict: - try: - stable_release = source.get(f"/repos/{source_repo}/releases/latest") - except ApiError as e: - if e.status != 404: - raise - stable_release = None + stable_release = stable_app_release(source.get(f"/repos/{source_repo}/releases?per_page=100")) stable = None if stable_release: diff --git a/tools/tests/test_update_catalog.py b/tools/tests/test_update_catalog.py index f0b77cb7f..2e8e125bc 100644 --- a/tools/tests/test_update_catalog.py +++ b/tools/tests/test_update_catalog.py @@ -7,6 +7,7 @@ from tools.release.update_catalog import ( parse_variants, published_millis, release_body_value, + stable_app_release, ) @@ -86,6 +87,19 @@ pubg com.tencent.ig -pubg published_millis(release, prefer_updated=True), ) + def test_stable_is_the_newest_app_release_not_github_latest(self): + def release(tag, at, *assets, **flags): + return {"tag_name": tag, "published_at": at, "assets": [{"name": a} for a in assets], **flags} + releases = [ + release("gamescope-3.16.29-p5", "2026-10-01T18:53:08Z", "gamescope.tzst"), + release("0.3.0", "2026-09-30T00:00:00Z", "DroidDeck-0.3.0.apk", prerelease=True), + release("0.2.1", "2026-09-29T00:00:00Z", "DroidDeck-0.2.1.apk", draft=True), + release("0.2.0", "2026-09-28T03:20:01Z", "DroidDeck-0.2.0-pubg.apk", "DroidDeck-0.2.0.apk"), + release("0.1.6", "2026-09-01T00:00:00Z", "DroidDeck-0.1.6.apk"), + ] + self.assertEqual("0.2.0", stable_app_release(releases)["tag_name"]) + self.assertIsNone(stable_app_release(releases[:1])) + if __name__ == "__main__": unittest.main() From a491dec5c40d84a62836e41b4e6d21ee68ec1ba7 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Thu, 1 Oct 2026 21:29:01 -0400 Subject: [PATCH 2/3] fix: check PR test builds against main's versionCode instead of a release --- .github/workflows/pr-release-publish.yml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-release-publish.yml b/.github/workflows/pr-release-publish.yml index f27a5a4af..6679d9482 100644 --- a/.github/workflows/pr-release-publish.yml +++ b/.github/workflows/pr-release-publish.yml @@ -126,27 +126,20 @@ jobs: sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}} tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1) - # Not releases/latest: gamescope and wlroots releases can hold that mark. The stable app - # release is the newest published one that ships DroidDeck-.apk. - stable_tag=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" --jq ' - [.[] | select((.draft | not) and (.prerelease | not)) | .tag_name as $t | - select(any(.assets[]; .name == "DroidDeck-\($t).apk"))] | - sort_by(.published_at) | last | .tag_name // empty') - [[ -n "$stable_tag" ]] || { echo "::error::No published DroidDeck release to compare against"; exit 1; } - mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr" - gh release download "$stable_tag" -R "$GITHUB_REPOSITORY" \ - -p "DroidDeck-$stable_tag.apk" -D "$RUNNER_TEMP/stable" + # A PR build is made from the PR merged onto main, so it must install over main's own + # build. main is what this job checked out; no release has to exist or be marked latest. + main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle) + [[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; } + mkdir -p "$RUNNER_TEMP/pr" gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr" mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk') [[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; } package_line() { "$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p" } - stable=$(package_line "$RUNNER_TEMP/stable/DroidDeck-$stable_tag.apk") candidate=$(package_line "${apks[0]}") - [[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && - ${candidate##* } -ge ${stable##* } ]] || { - echo "::error::PR package/versionCode '$candidate' cannot install over stable '$stable'"; exit 1; + [[ "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && ${candidate##* } -ge $main_code ]] || { + echo "::error::PR package/versionCode '$candidate' cannot install over main (versionCode $main_code)"; exit 1; } current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER") From 9928bfe7809a39dc97f14f0d6dc482c4d9f5502a Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Thu, 1 Oct 2026 21:34:06 -0400 Subject: [PATCH 3/3] ci: keep component releases off latest and pre-release --- .github/workflows/build-gamescope.yml | 5 ++++- .github/workflows/build-wlroots.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-gamescope.yml b/.github/workflows/build-gamescope.yml index 73b97f61a..649c01e9f 100644 --- a/.github/workflows/build-gamescope.yml +++ b/.github/workflows/build-gamescope.yml @@ -42,4 +42,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh release create "${{ inputs.tag }}" --latest=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber + gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber + # A component the apk fetches by tag, never something users install: it must not + # become the repo's Latest release or appear as a pre-release, also when re-uploaded. + gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false diff --git a/.github/workflows/build-wlroots.yml b/.github/workflows/build-wlroots.yml index e36d5d1a9..92780a2a4 100644 --- a/.github/workflows/build-wlroots.yml +++ b/.github/workflows/build-wlroots.yml @@ -43,4 +43,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh release create "${{ inputs.tag }}" --latest=false --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber + gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber + # A component the apk fetches by tag, never something users install: it must not + # become the repo's Latest release or appear as a pre-release, also when re-uploaded. + gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false