Files
Axel Uhl 40cc60ca8f Fix Android SDK setup after runner image dropped legacy 'tools' package
The GitHub-hosted runner image rolled out on ~2026-09-14 ships
cmdline-tools 12.0, whose sdkmanager no longer resolves the obsolete
'tools' (legacy Android SDK Tools) package. The android-actions/setup-android
step inherits the action default packages 'tools platform-tools' and thus
failed with "Failed to find package 'tools'" (exit code 1) before the build
script even ran. The same request in buildAndUpdateProduct.sh would fail
the same way once reached.

Override the action's packages input to 'platform-tools' and drop the
trailing 'tools' argument from the sdkmanager call in
buildAndUpdateProduct.sh. build-tools, platform-tools and the platform
are still installed by the script. The cmdline-tools-version pin is left
unchanged.

Assisted-By: Claude (claude-4.8-opus)
2026-09-15 00:15:54 +02:00

431 lines
23 KiB
YAML

name: release
on:
push:
# NOTE: introducing a "tags:" filter makes GitHub treat any Git ref kind that
# is NOT filtered as "undefined", and the workflow then stops running for it.
# So branches MUST be listed explicitly here; without "branches: ['**']" the
# tag filter below would silently disable all branch-push builds. '**' (not
# '*') is required to match branch names containing slashes (e.g. releases/x).
branches:
- '**'
# Reviewed-fork-PR tags. A maintainer fetches a fork PR head, reviews the
# exact SHA, then pushes one of these tags at that SHA. Because a tag push is
# a push event in this (base) repo, the run executes in trusted context WITH
# secrets — the only way to run secret-bearing validation on fork-authored
# code. '*-reviewed-for-compile' runs a compile-only build with NO secrets
# (cheap sanity check); '*-reviewed-for-build' runs the full build with tests
# and secrets. Safety rests on the maintainer having read that exact SHA
# before pushing the tag. See the "Determine build mode" step below.
tags:
- '*-reviewed-for-compile'
- '*-reviewed-for-build'
# paths-ignore is not evaluated for tag pushes (GitHub rule), so it only
# scopes branch pushes, exactly as before.
paths-ignore:
- 'wiki/**'
- '.github/workflows/*'
- '**/*.md'
- 'README.md'
- 'docs/**'
# Auto-build submitted PRs. A same-repo PR (branch pushed by a committer) runs
# the full build with tests and secrets — same trust as a direct branch push.
# A fork PR runs compile-only with NO secrets (GitHub withholds secrets from
# fork pull_request runs at the event level); the secret-bearing full build of
# fork code stays gated behind a maintainer pushing a *-reviewed-for-build tag.
# No branches: filter here — for pull_request that would filter by BASE branch;
# omitting it targets PRs into any branch. Draft PRs are excluded by the build
# job's if:. See the "Determine build mode" step for the fork/same-repo split.
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths-ignore:
- 'wiki/**'
- '.github/workflows/*'
- '**/*.md'
- 'README.md'
- 'docs/**'
workflow_dispatch:
inputs:
skip_tests:
description: Skip tests (set to "true" to skip all tests)
default: 'false'
local_target_platform:
description: Build with a local target platform (set to "true" to use local target platform)
default: 'false'
runner_cpus:
description: Empty means ubuntu-latest, usually 4 CPUs with little disk/memory, but if you have runners named ubuntu-latest-Xcpu, it can be 4, 8, 16, and 64 as well, using, e.g., ubuntu-latest-16cpu; default is ubuntu-latest
default: ''
firefox_version:
description: Version to use for Selenium tests; default is "latest", but could, e.g., be "141.0" or a specific pin like "140.0"
default: 'latest'
jobs:
# The "changes" job will cause the "build" job to get skipped for trivial changes
changes:
permissions: {}
runs-on: ubuntu-latest
outputs:
# On workflow_dispatch there is no 'before' field in the event payload, so
# paths-filter cannot compute a meaningful diff (it would warn and fall back
# to the last commit only). A manual dispatch is an explicit request to build,
# so we skip the filter step entirely and force should_run=true. On push/PR the
# filter runs and its 'code' output decides. (The build job also short-circuits
# its own if-condition on workflow_dispatch, so this keeps both in agreement.)
should_run: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.code }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: filter
if: github.event_name != 'workflow_dispatch'
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
with:
# Under the default 'some' quantifier a negated rule still counts as a
# match (picomatch treats '!README.md' as "anything but README.md"),
# so a changed README.md would set code=true. 'some-with-excludes'
# (v4+) makes the '!' rules act as real exclusions: code is true only
# when a changed file matches the positive '**' rule AND none of the
# excludes.
predicate-quantifier: 'some-with-excludes'
filters: |
code:
- '**'
- '!wiki/**'
- '!.github/workflows/*'
- '!**/*.md'
- '!README.md'
- '!docs/**'
build:
needs: changes
if: >-
${{ (needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch')
&& (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
permissions:
contents: write
checks: write
actions: read
runs-on: ${{ github.event.inputs.runner_cpus != '' && format('ubuntu-latest-{0}cpu', github.event.inputs.runner_cpus) || 'ubuntu-latest' }}
outputs:
skip_tests: ${{ steps.mode.outputs.skip_tests }}
steps:
- name: Determine build mode
id: mode
shell: bash
# skip_tests=true means "compile only, no tests, no secrets". It is true
# for a workflow_dispatch with skip_tests=true, for a push of a
# *-reviewed-for-compile tag, OR for a fork-originated pull_request (fork
# runs get no secrets, so we compile without tests; the secret-bearing
# full build of fork code is gated behind a *-reviewed-for-build tag).
# Everything else (branches, same-repo PRs, *-reviewed-for-build tags)
# runs the full build with tests and secrets.
run: |
skip=false
if [[ "${{ github.event.inputs.skip_tests }}" == "true" ]]; then skip=true; fi
if [[ "${{ github.ref }}" == refs/tags/*-reviewed-for-compile ]]; then skip=true; fi
if [[ "${{ github.event_name }}" == "pull_request" \
&& "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
skip=true
fi
echo "skip_tests=$skip" >> "$GITHUB_OUTPUT"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@v1.3.1
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Allocate swap space
shell: bash
run: |
sudo dd if=/dev/zero of=/mnt/large-swapfile bs=1G count=30
sudo chmod 600 /mnt/large-swapfile
sudo mkswap /mnt/large-swapfile
sudo chown root:root /mnt/large-swapfile
sudo swapon /mnt/large-swapfile
- name: Create TMP space under /mnt/tmp
shell: bash
run: |
sudo mkdir /mnt/tmp
sudo chmod 777 /mnt/tmp
- name: Collect Workflow Telemetry
uses: ycfreeman/workflow-telemetry-action@72ec425db8a31670fdc4419966940a6d980a0b49 # v3.0.2
with:
comment_on_pr: false
proc_trace_min_duration: 5000 # only chart processes running >= 5s
proc_trace_chart_max_count: 40 # cap Gantt bars to keep mermaid source under 50k
metric_frequency: 30 # ~3h+ builds: coarser sampling so the per-sample CPU/memory xychart source stays well under mermaid's 50k maxTextSize (~390 points/3h15m)
- name: Setup firefox
id: setup-firefox
uses: browser-actions/setup-firefox@0bc507ddf224827e3b1af68e014d5e42ab93e795 # v1.7.2
with:
firefox-version: ${{ github.event.inputs.firefox_version == '' && 'latest' || github.event.inputs.firefox_version }}
- run: |
echo Installed firefox versions: ${{ steps.setup-firefox.outputs.firefox-version }}
${{ steps.setup-firefox.outputs.firefox-path }} --version
- name: Check out the repository to the runner
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: '500'
- name: Install JDK 8
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '8'
mvn-toolchain-id: 'JavaSE-1.8'
- name: Install JDK 17
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
mvn-toolchain-id: 'JavaSE-17'
- name: Install JDK 25
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '25'
mvn-toolchain-id: 'JavaSE-25'
- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
with:
log-accepted-android-sdk-licenses: 'false'
cmdline-tools-version: 9123335 # This corresponds to human-friendly version number 8.0
# The action's default 'packages' is "tools platform-tools". The legacy "tools"
# package was removed from Google's SDK repository; as of the ~2026-09-14 runner
# image (cmdline-tools 12.0 preinstalled) sdkmanager no longer resolves it and the
# step fails with "Failed to find package 'tools'". We only need the action to
# bootstrap cmdline-tools here; buildAndUpdateProduct.sh installs build-tools,
# platform-tools and the platform itself. So request just platform-tools.
packages: 'platform-tools'
- name: Start MongoDB
uses: supercharge/mongodb-github-action@315db7fe45ac2880b7758f1933e6e5d59afd5e94 #v1.12.1
with:
mongodb-version: 7.0
- name: Start RabbitMQ
uses: Namoshek/rabbitmq-github-action@80a182e44c7f60bc3417a8689d6a24fe9d08b27b #v1.2.0
with:
version: '3.8.9'
ports: '5672:5672'
- shell: bash
env: # Or as an environment variable
TMP: /mnt/tmp
# These five are consumed only at TEST runtime. For a compile-only run
# (skip_tests) they resolve to '' so the real secret values are never
# placed in the process environment where reviewed-but-fork-authored
# code could read them. The full build (skip_tests false) gets them all.
AWS_S3_TEST_S3ACCESSID: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.AWS_S3_TEST_S3ACCESSID }}
AWS_S3_TEST_S3ACCESSKEY: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.AWS_S3_TEST_S3ACCESSKEY }}
GEONAMES_ORG_USERNAMES: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.GEONAMES_ORG_USERNAMES }}
GOOGLE_MAPS_AUTHENTICATION_PARAMS: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.GOOGLE_MAPS_AUTHENTICATION_PARAMS }}
POLAR_DATA_BASE_URL: ${{ vars.POLAR_DATA_BASE_URL }}
POLAR_DATA_BEARER_TOKEN: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.POLAR_DATA_BEARER_TOKEN }}
APP_PARAMETERS: "-Daws.region=eu-west-1"
JAVA8_HOME: ${{env.JAVA_HOME_8_X64}}
JAVA17_HOME: ${{env.JAVA_HOME_17_X64}}
run: |
./configuration/buildAndUpdateProduct.sh -x ${{ github.event.inputs.runner_cpus == '' && '4' || github.event.inputs.runner_cpus }} ${{ steps.mode.outputs.skip_tests == 'true' && '-t' || '' }} ${{ github.event.inputs.local_target_platform == 'true' && '-v' || '' }} build 2>&1
- name: show disk stats
if: always()
run: df -h
- name: Upload build log
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: build.log
path: build.log
retention-days: 90
- name: Collect Test Reports
uses: dorny/test-reporter@a6ddd83ac95ff4586f5d3aceeb314d9a1841db95 # v3.0.0
# Skip for compile-only runs: no tests ran, so there are no TEST-*.xml
# and fail-on-error would otherwise error on zero matches.
if: ${{ always() && steps.mode.outputs.skip_tests != 'true' }}
with:
name: Maven Tests
path: '**/TEST-*.xml'
max-annotations: '1'
list-suites: failed
list-tests: failed
reporter: java-junit
fail-on-error: true
- name: Build Release
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
shell: bash
run: |
./configuration/buildAndUpdateProduct.sh -u -L ${{ github.event.inputs.skip_tests == 'true' && '-n untested' || '' }} release
- name: Determine release name
run: |
echo "SIMPLE_VERSION_INFO=$( basename dist/* )" >>${GITHUB_ENV}
- name: Upload test results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: test-results
path: |
**/surefire-reports/**
retention-days: 90
- name: Upload distribution artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
with:
name: ${{ env.SIMPLE_VERSION_INFO }}
path: dist/**/*
retention-days: 90
- name: Create Release
id: create_release
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
tag_name: ${{ env.SIMPLE_VERSION_INFO }}
target_commitish: ${{ github.ref_name }}
name: ${{ env.SIMPLE_VERSION_INFO }}
draft: false
prerelease: false
- name: Upload Release Asset tar.gz
id: upload-release-asset-tar-gz
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Do NOT fold this into action-gh-release's files: input: that action derives the
# content type via the mime-types library, which maps .tar.gz to application/gzip.
# Production installers (refreshInstance.sh, github-download-release-assets.sh) select
# the tarball with select(.content_type=="application/x-tar"), so the type must be set
# explicitly here.
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./dist/${{ env.SIMPLE_VERSION_INFO }}/${{ env.SIMPLE_VERSION_INFO }}.tar.gz
asset_name: ${{ env.SIMPLE_VERSION_INFO }}.tar.gz
asset_content_type: application/x-tar
- name: Upload Release Asset release_notes.txt
id: upload-release-asset-release-notes-txt
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./dist/${{ env.SIMPLE_VERSION_INFO }}/release-notes.txt
asset_name: release-notes.txt
asset_content_type: text/plain
- name: Trigger CI job
env:
CI_JOB_USERNAME: ${{ vars.CI_JOB_USERNAME }}
CI_JOB_PASSWORD: ${{ secrets.CI_JOB_PASSWORD }}
CI_JOB_TOKEN: ${{ secrets.CI_JOB_TOKEN }}
if: ${{ always() && env.CI_JOB_USERNAME != '' && env.CI_JOB_PASSWORD != '' && env.CI_JOB_TOKEN != '' }}
shell: bash
run: |
if [[ "${{ github.ref }}" =~ refs/heads/releases/.* ]]; then
JOB=$( echo "${{ github.ref }}" | sed -e 's/^refs\/heads\/releases\///' )
else
JOB=$( echo "${{ github.ref }}" | sed -e 's/^refs\/heads\///' )
fi
echo "Identified CI job: ${JOB}"
curl -u "${{ vars.CI_JOB_USERNAME }}:${{ secrets.CI_JOB_PASSWORD}}" ${{ vars.CI_BASE_URL }}/job/${JOB}/build?token=${{ secrets.CI_JOB_TOKEN }}
# build-gate is the REQUIRED status check in the branch ruleset (context
# "build-gate"). It must be SATISFIED only for a genuine full build (tests
# actually ran and passed) or a legitimate trivial-change skip; every other
# case — most importantly a compile-only fork PR — must leave the PR
# un-mergeable WITHOUT a red X.
#
# WHY THIS IS A POSTED COMMIT STATUS, NOT A JOB NAMED build-gate:
# GitHub Actions auto-creates a check-run for every JOB, and fills its
# conclusion from the job's fate — including "skipped" when the job's if: is
# false. A rule set / branch protection treats a "skipped" required check as
# SATISFIED (mergeable). So a job named build-gate can NEVER represent "absent"
# — a skipped job still materializes a skipped=passed check-run. That is the
# exact bug that let compile-only fork PRs #53/#54 merge without tests.
#
# A COMMIT STATUS (legacy Statuses API) is different: it exists only if we
# explicitly POST it. A skipped STEP posts nothing (steps are not check-runs).
# The ruleset matches the required context "build-gate" by NAME across both
# APIs, so a posted commit status with context=build-gate satisfies it exactly
# as the old check-run did — no ruleset change needed.
#
# Hence: this job is named post-build-gate-status (NOT build-gate) so its own
# auto check-run carries a context the ruleset ignores. Its single step POSTs a
# commit status context=build-gate ONLY when: the change was trivial (build
# skipped, no relevant changes) OR a full build ran WITH tests and succeeded
# -> state=success; OR a full build ran WITH tests and FAILED
# (build.result==failure, skip_tests==false) -> state=failure (red X, so a
# committer's own broken build is loud). In every OTHER case it posts NOTHING,
# so no build-gate context exists for the head SHA and the PR stays blocked at
# "Expected" with no red X. That "nothing" covers: any compile-only run,
# succeeded OR failed (fork PR, *-reviewed-for-compile, workflow_dispatch
# skip_tests) — no tests ran, so there is nothing meaningful to fail on; AND a
# CANCELLED trusted build — cancellation is indeterminate (tests might have
# passed had it finished), so "failure" would be misleading; re-run to get a
# real verdict. Merge qualification for fork code comes only from a maintainer
# pushing a *-reviewed-for-build tag (full trusted build with tests), which
# posts success/failure. compile-gate (advisory) still shows compile pass/fail.
#
# The status is posted against the PR HEAD sha on pull_request events
# (github.sha there is the ephemeral refs/pull/N/merge commit, which the
# ruleset does not evaluate) and github.sha otherwise. needs:[changes, build]
# + if: always() so the trivial-change path (build skipped) is still evaluated.
post-build-gate-status:
permissions:
statuses: write
needs: [changes, build]
if: always()
runs-on: ubuntu-latest
steps:
- name: Post build-gate commit status
env:
GH_TOKEN: ${{ github.token }}
BUILD_RESULT: ${{ needs.build.result }}
SKIP_TESTS: ${{ needs.build.outputs.skip_tests }}
SHOULD_RUN: ${{ needs.changes.outputs.should_run }}
GATE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
STATE=""
if [[ "$BUILD_RESULT" == "success" && "$SKIP_TESTS" != "true" ]]; then
STATE=success; REASON="Full build with tests passed."
elif [[ "$BUILD_RESULT" == "skipped" && "$SHOULD_RUN" == "false" ]]; then
STATE=success; REASON="No relevant changes — nothing to build."
elif [[ "$BUILD_RESULT" == "failure" && "$SKIP_TESTS" != "true" ]]; then
STATE=failure; REASON="Full build with tests failed."
fi
if [[ -n "$STATE" ]]; then
echo "Posting build-gate=$STATE on $GATE_SHA: $REASON"
gh api "repos/${GITHUB_REPOSITORY}/statuses/${GATE_SHA}" \
-f state="$STATE" \
-f context=build-gate \
-f description="$REASON" \
-f target_url="$RUN_URL"
else
echo "Not posting build-gate: build.result=$BUILD_RESULT skip_tests=$SKIP_TESTS should_run=$SHOULD_RUN"
echo "The build-gate context stays absent, so the PR remains un-mergeable"
echo "at 'Expected' without a red X — this covers a successful OR failed"
echo "compile-only run (e.g. a fork PR, where no tests ran so there is"
echo "nothing to fail on). A *-reviewed-for-build tag runs the full build"
echo "with tests and posts success (or failure) to the build-gate context."
fi
# compile-gate is an ADVISORY (non-required) check. It reflects only whether
# the build job compiled successfully, regardless of whether tests ran — a full
# build implies compilation succeeded. It gives *-reviewed-for-compile runs a
# distinct green/red on the PR without touching the required build-gate check.
compile-gate:
permissions: {}
needs: [changes, build]
if: always()
runs-on: ubuntu-latest
steps:
- run: |
if [[ "${{ needs.build.result }}" == "success" ]]; then
echo "Compilation succeeded."
elif [[ "${{ needs.build.result }}" == "skipped" && "${{ needs.changes.outputs.should_run }}" == "false" ]]; then
echo "No relevant changes — nothing to compile."
else
echo "Compilation failed or was cancelled."
exit 1
fi