From 708535469e74cd62cf8714f2099d7be6657ef925 Mon Sep 17 00:00:00 2001 From: Sedat Qaja Date: Fri, 16 Oct 2026 16:45:41 +0200 Subject: [PATCH] ci: run the fork's pipelines on the self-hosted runners Squash of ten CI commits from 2026-08-04 (all part of HealthChat release 26.08.0): - ci(release): move releases to the persistent self-hosted runners - ci: grant contents:read so checkout works on this private fork - ci: disable the Label Community PRs job on this fork - ci: run the heavy Gradle jobs on the self-hosted runners - ci: rely on the runner image for the Android SDK - ci: size the Gradle heap for the self-hosted containers - ci: report runner capacity on the self-hosted jobs - ci: record why the Gradle heap is reduced - ci: cap Gradle workers so test JVMs fit the container - ci: skip the codecov upload when no token is configured The danger.yml hunk from the contents:read commit is omitted: upstream replaced Danger with a github-script workflow in v26.09. --- .github/workflows/build.yml | 23 +++++++- .github/workflows/build_enterprise.yml | 20 ++++++- .../healthchat-release-playstore.yml | 37 ++++++------ .github/workflows/maestro-local.yml | 20 ++++++- .github/workflows/pull_request.yml | 59 ++++++++++--------- .github/workflows/quality.yml | 43 +++++++++++--- .github/workflows/recordScreenshots.yml | 20 ++++++- .github/workflows/sonar.yml | 20 ++++++- .github/workflows/tests.yml | 49 +++++++++++++-- .github/workflows/validate-lfs.yml | 6 +- 10 files changed, 224 insertions(+), 73 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1ba9dd010..aacae9fe92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,18 +7,32 @@ on: push: branches: [ develop ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache jobs: build: name: Build APKs - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] permissions: + # A job-level block replaces the workflow-level one, so contents:read has to be + # repeated here or checkout fails on this private fork. + contents: read # For NejcZdovc/comment-pr pull-requests: write strategy: @@ -31,6 +45,9 @@ jobs: cancel-in-progress: true steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 589f0a329d..fadf018a84 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -7,17 +7,28 @@ on: push: branches: [ develop ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache jobs: build: name: Build Enterprise APKs - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Skip in forks if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} strategy: @@ -30,6 +41,9 @@ jobs: cancel-in-progress: true steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB diff --git a/.github/workflows/healthchat-release-playstore.yml b/.github/workflows/healthchat-release-playstore.yml index d646ba9794..734ff52ba9 100644 --- a/.github/workflows/healthchat-release-playstore.yml +++ b/.github/workflows/healthchat-release-playstore.yml @@ -46,17 +46,24 @@ concurrency: env: # Upstream's workflows hardcode -Xmx8g / Kotlin daemon -Xmx4g, sized for the - # 4-core/16GB ubuntu-latest that public repos get. On this container the - # daemon keeps getting SIGKILLed mid-build (DaemonDisappearedException, no - # OutOfMemoryError, no heap dump), so stay well under upstream's ceiling and - # leave room for the Kotlin daemon and the parallel lint workers. + # 4-core/16GB ubuntu-latest that public repos get. On the previous incus + # container the daemon kept getting SIGKILLed mid-build + # (DaemonDisappearedException, no OutOfMemoryError, no heap dump), so these + # values stay well under upstream's ceiling and leave room for the Kotlin + # daemon and the parallel lint workers. Re-check them against the "Report + # runner capacity" output now that jobs land on the persistent org runners, + # whose specs may differ. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache jobs: release: name: Build and upload AAB to Play Store - runs-on: novaloop-incus-large + # Persistent org runners (github-runner-01, gha-runner-1..3). The previous + # novaloop-incus-large label pointed at the GARM ephemeral pool, whose freshly + # spawned runners were reaped mid-job — jobs died ~10 minutes in with + # "The operation was canceled" and no other diagnostic. + runs-on: [self-hosted, Linux, X64] steps: - uses: actions/checkout@v4 with: @@ -176,18 +183,14 @@ jobs: distribution: 'temurin' java-version: '21' - # The self-hosted runner image has no Android SDK (unlike GitHub-hosted - # ubuntu-latest), so install it here. - # - platform: follows COMPILE_SDK in plugins/src/main/kotlin/Versions.kt. - # Platform packages are minor-versioned, so compileSdk 37 is android-37.0. - # - build-tools: nothing sets buildToolsVersion, so AGP picks its own - # default (SdkConstants.CURRENT_BUILD_TOOLS_VERSION). Bump it when AGP is - # upgraded, not when Versions.kt changes. - - name: Set up Android SDK - uses: android-actions/setup-android@v3 - with: - packages: 'platforms;android-37.0 build-tools;36.0.0' - + # The Android SDK now comes from the self-hosted runner image, so there is no + # setup-android step here. The image must provide it AND export ANDROID_HOME to + # the job environment — Gradle fails with "SDK location not found" otherwise. + # The image also has to carry the platform matching COMPILE_SDK in + # plugins/src/main/kotlin/Versions.kt (platform packages are minor-versioned, so + # compileSdk 37 means android-37.0) and the build-tools version AGP defaults to + # (SdkConstants.CURRENT_BUILD_TOOLS_VERSION), since nothing sets buildToolsVersion + # here. Both need refreshing in the image when AGP or COMPILE_SDK moves. - name: Configure gradle uses: gradle/actions/setup-gradle@v4 diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index 25aa0f6eac..ae08f64cc1 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -5,11 +5,22 @@ on: workflow_dispatch: pull_request: -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache ARCH: x86_64 DEVICE: pixel_7_pro @@ -19,7 +30,7 @@ env: jobs: build-apk: name: Build APK - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Secrets are not available to pull_request runs from forks, so the Maestro job below # cannot run for them; skip the build too, as its only consumer is that job. if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository @@ -28,6 +39,9 @@ jobs: cancel-in-progress: true steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e24847280e..66c747a70c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,33 +23,38 @@ jobs: script: | core.setFailed("PR has been labeled with X-Blocked; it cannot be merged."); - community-prs: - name: Label Community PRs - runs-on: ubuntu-latest - if: github.event.action == 'opened' - permissions: - pull-requests: write - steps: - - name: Check membership - if: github.event.pull_request.user.login != 'renovate[bot]' - uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2 - id: teams - with: - username: ${{ github.event.pull_request.user.login }} - organization: element-hq - team: Vector Core - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} - - name: Add label - if: steps.teams.outputs.isTeamMember == 'false' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['Z-Community-PR'] - }); + # Disabled on the HealthChat fork. This job asks whether the PR author belongs to the + # element-hq "Vector Core" team, using secrets.ELEMENT_BOT_TOKEN_READ_ORG — a secret + # that only exists upstream. Here it fails on every PR that is opened, and there is no + # community to label anyway. Kept commented rather than deleted so upstream changes to + # it still conflict visibly on merge. + # community-prs: + # name: Label Community PRs + # runs-on: ubuntu-latest + # if: github.event.action == 'opened' + # permissions: + # pull-requests: write + # steps: + # - name: Check membership + # if: github.event.pull_request.user.login != 'renovate[bot]' + # uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2 + # id: teams + # with: + # username: ${{ github.event.pull_request.user.login }} + # organization: element-hq + # team: Vector Core + # GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} + # - name: Add label + # if: steps.teams.outputs.isTeamMember == 'false' + # uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + # with: + # script: | + # github.rest.issues.addLabels({ + # issue_number: context.issue.number, + # owner: context.repo.owner, + # repo: context.repo.repo, + # labels: ['Z-Community-PR'] + # }); close-if-fork-develop: name: Forbid develop branch fork contributions diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1fa443c0f5..038ce4fe63 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -7,11 +7,22 @@ on: push: branches: [ main, develop ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache jobs: @@ -20,6 +31,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB @@ -63,7 +77,7 @@ jobs: checkDependencies: name: Search for invalid dependencies - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -87,7 +101,7 @@ jobs: # Code checks konsist: name: Konsist tests - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }} @@ -128,12 +142,21 @@ jobs: compose: name: Compose tests - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }} cancel-in-progress: true steps: + # Records the container's real limits so heap settings can be read against them + # instead of guessed at. Mirrors healthchat-release-playstore.yml. + - name: Report runner capacity + if: runner.environment == 'self-hosted' + run: | + free -h; nproc; df -h . + cat /sys/fs/cgroup/memory.max 2>/dev/null \ + || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null \ + || echo "no cgroup memory limit visible" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Ensure we are building the branch and not the branch after being merged on develop @@ -162,7 +185,7 @@ jobs: lint: name: Android lint check - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }} @@ -207,7 +230,7 @@ jobs: detekt: name: Detekt checks - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }} @@ -248,7 +271,7 @@ jobs: ktlint: name: Ktlint checks - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }} @@ -331,6 +354,10 @@ jobs: name: Run zizmor runs-on: ubuntu-latest permissions: + # A job-level block replaces the workflow-level one, so contents:read has to be + # repeated here or checkout fails on this private fork. + contents: read + actions: read # Required for zizmor to read workflow files. security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index e6580d2f7b..f9f1e5137a 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -5,11 +5,22 @@ on: pull_request: types: [ labeled ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g -Dsonar.gradle.skipCompile=true CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache jobs: @@ -19,11 +30,14 @@ jobs: pull-requests: write contents: write name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }} - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots' steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index d2b57a771e..4575aa96a7 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -7,24 +7,38 @@ on: push: branches: [ main, develop ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache GROUP: ${{ format('sonar-{0}', github.ref) }} jobs: sonar: name: Sonar Quality Checks - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ format('sonar-{0}', github.ref) }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} steps: - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6222ac024c..d2628ffb9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,24 +7,53 @@ on: push: branches: [ main, develop ] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache + # Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. + # Each incus container reports 48 GiB and 8 cores, so a single build has ample room — + # but the containers are oversubscribed against their host, and two concurrent heavy + # Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no + # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps + # make two builds likelier to coexist; the real fix is sizing the containers to the + # host. See the "Report runner capacity" step. + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC + # --max-workers caps how many test tasks run at once. build.gradle.kts sets + # maxParallelForks = cores/2 and screenshot tests fork at maxHeapSize=2g, so on these + # 8-core containers the default (workers = cores = 8) allows 8 x 4 x 2g = 64 GB of test + # JVMs against a 48 GiB cgroup limit, with no swap — the daemon gets OOM-killed and + # Gradle reports "the daemon has disappeared". GitHub's 4-core hosted runners reach only + # 2 x 2 x 2g = 16 GB and additionally have the 8 GB swapfile added above. + CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4 jobs: tests: name: Runs unit tests - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} cancel-in-progress: true steps: + # Records the container's real limits so heap settings can be read against them + # instead of guessed at. Mirrors healthchat-release-playstore.yml. + - name: Report runner capacity + if: runner.environment == 'self-hosted' + run: | + free -h; nproc; df -h . + cat /sys/fs/cgroup/memory.max 2>/dev/null \ + || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null \ + || echo "no cgroup memory limit visible" - name: Free Disk Space (Ubuntu) + # Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this + # would delete dotnet/haskell/docker images from the host on every run. + if: runner.environment == 'github-hosted' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be with: # This might remove tools that are actually needed, if set to "true" but frees about 6 GB @@ -41,6 +70,9 @@ jobs: # Increase swapfile size to prevent screenshot tests getting terminated # https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749 - name: 💽 Increase swapfile size + # /mnt/swapfile is a GitHub runner-image path and this needs passwordless sudo, + # so it only applies to the hosted VMs. + if: runner.environment == 'github-hosted' run: | sudo swapoff -a sudo fallocate -l 8G /mnt/swapfile @@ -112,8 +144,15 @@ jobs: echo "---" >> $GITHUB_STEP_SUMMARY # https://github.com/codecov/codecov-action + # This fork has no CODECOV_TOKEN, and Codecov rejects tokenless uploads from private + # repositories ("Token required - not valid tokenless upload"). With + # fail_ci_if_error: true that failed the whole job *after* the tests had passed, so + # skip the upload unless a token is configured. Adding the secret re-enables it. - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + if: env.CODECOV_TOKEN != '' + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml index 027c7d68e9..45c8fb8d29 100644 --- a/.github/workflows/validate-lfs.yml +++ b/.github/workflows/validate-lfs.yml @@ -2,7 +2,11 @@ name: Validate Git LFS on: [pull_request, merge_group] -permissions: {} +permissions: + # Private fork: actions/checkout needs contents:read to clone. Upstream is public, + # where an empty token can still read the repo, so an empty permissions block works there and + # fails here with `fatal: repository not found`. + contents: read jobs: build: