CI already skips the Android build for docs-only pull requests, and that part works — a README-only PR reports green in ~11s. The skip-list was just too narrow to catch a realistic docs PR. PR #96 is the example: a contributing guide, docs corrections and issue templates. It ran the full lint + test + assemble four times, over exactly two files Gradle never reads — `.forgejo/ISSUE_TEMPLATE/config.yml` and `.gitignore`. Added to the list: issue templates, `.planning/`, `licenses/`, `renovate.json5`, `.gitignore`, `.gitattributes`, `.editorconfig`. It stays a skip-list rather than a build-list, so an unfamiliar path still builds by default; the workflows themselves, `.gitmodules` and `scripts/` are deliberately not skippable. The step now also prints which files forced the build, so the next "why did my docs PR build?" is answerable from the log. One latent bug alongside it: `github.base_ref` is normalised against a `refs/heads/` prefix. Arriving in full-ref form would fail the merge-base lookup and quietly degrade the guard into "always build" — the failure mode this PR is fixing, but permanently and invisibly. Verified by replaying the new pattern over real history: PR #96 and #97 now skip; the forge migration (workflows), the search fix (app code) and a Weblate translation merge still build. No issue — reported directly. Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/98
170 lines
7.1 KiB
YAML
170 lines
7.1 KiB
YAML
name: CI
|
|
|
|
# One gate per pull request. Branch pushes no longer trigger CI on their own,
|
|
# so a change is built once on its PR (covering feature -> release/* and
|
|
# release/* -> main) instead of once per push and again on the merge to main.
|
|
# The merge itself is handled by release.yaml, which only does heavy work when
|
|
# the merge actually cuts a release.
|
|
on:
|
|
pull_request:
|
|
|
|
# Cancel superseded runs for the same PR.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# Single job named `ci` so the required "CI" status check is always reported,
|
|
# even for docs-only PRs: those just skip the Android build and the job still
|
|
# succeeds (fast green check) instead of being filtered out and leaving the
|
|
# required check pending forever.
|
|
ci:
|
|
runs-on: docker
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# Full history so the base..HEAD diff below has a merge-base.
|
|
fetch-depth: 0
|
|
submodules: recursive
|
|
|
|
# Cheap, always-on guard: the release build must stay reproducible for the
|
|
# official F-Droid repo (no AGP VCS-info embedding). Runs regardless of
|
|
# change scope so a regression can't slip through on a "docs-only" PR.
|
|
- name: Reproducible-release invariant
|
|
run: bash scripts/check_reproducible_release.sh
|
|
|
|
# Decide whether anything that affects the app build changed. Docs, store
|
|
# metadata, licence texts and forge housekeeping don't, so those PRs skip
|
|
# the SDK + Gradle work below but still report a green `ci`.
|
|
- name: Classify change scope
|
|
id: scope
|
|
env:
|
|
# Deliberately a skip-list, not a build-list: a path nobody thought
|
|
# about defaults to building. Only paths the Gradle build provably
|
|
# never reads belong here — note that the workflows themselves, the
|
|
# `.gitmodules` submodule pointer and `scripts/` are *not* in it.
|
|
SKIP_RE: '(\.md$|^docs/|^fastlane/|^fdroid-metadata/|^licenses/|^\.planning/|^\.(forgejo|gitea)/ISSUE_TEMPLATE/|^\.editorconfig$|^\.gitattributes$|^\.gitignore$|^renovate\.json5$|^LICENSE$)'
|
|
run: |
|
|
set -e
|
|
BASE="${{ github.base_ref }}"
|
|
# Normally the bare branch name; tolerate a full ref, which would
|
|
# otherwise make the merge-base lookup fail and quietly degrade this
|
|
# guard into "always build".
|
|
BASE="${BASE#refs/heads/}"
|
|
if [ -z "$BASE" ]; then
|
|
echo "No base branch on this event — running the full build to be safe."
|
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# Full (not --depth=1) base fetch so the merge-base is present even when
|
|
# the PR branch forked several commits back; a shallow tip has no merge
|
|
# base with a divergent branch and `git diff base...HEAD` aborts.
|
|
git fetch --no-tags origin "$BASE"
|
|
MB=$(git merge-base "origin/$BASE" HEAD 2>/dev/null || true)
|
|
if [ -z "$MB" ]; then
|
|
# No common ancestor available — don't risk skipping the build.
|
|
echo "No merge base with origin/$BASE — running the full build to be safe."
|
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
CHANGED=$(git diff --name-only "$MB" HEAD)
|
|
echo "Changed files:"; echo "$CHANGED"
|
|
RELEVANT=$(echo "$CHANGED" | grep -vE "$SKIP_RE" || true)
|
|
if [ -n "$RELEVANT" ]; then
|
|
# Naming them makes "why did my docs PR build for four minutes?"
|
|
# answerable from the log alone.
|
|
echo "Build-relevant changes:"; echo "$RELEVANT"
|
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Docs/metadata-only change — skipping the Android build."
|
|
echo "code=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Setup Java
|
|
if: steps.scope.outputs.code == 'true'
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: '17'
|
|
|
|
# Fully qualified on purpose. Codeberg resolves bare `uses:` refs against
|
|
# data.forgejo.org, Forgejo's own action mirror — actions/checkout,
|
|
# setup-java and cache all exist there, but android-actions/setup-android
|
|
# does not, and the job dies with "repository not found". Gitea's instance
|
|
# defaults to GitHub, which is why this never surfaced before the split.
|
|
- name: Setup Android SDK
|
|
if: steps.scope.outputs.code == 'true'
|
|
uses: https://github.com/android-actions/setup-android@v3
|
|
with:
|
|
# Default ("tools platform-tools") drags in the Android Emulator
|
|
# (~300 MB) which the build never uses.
|
|
packages: ''
|
|
|
|
- name: Setup Android SDK cache
|
|
if: steps.scope.outputs.code == 'true'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: /opt/android-sdk
|
|
key: ${{ runner.os }}-android-sdk-37-36.0.0
|
|
|
|
- name: Install Android SDK packages
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: |
|
|
yes | sdkmanager --licenses >/dev/null || true
|
|
sdkmanager \
|
|
"platform-tools" \
|
|
"platforms;android-37.0" \
|
|
"build-tools;36.0.0"
|
|
|
|
- name: Setup Gradle cache
|
|
if: steps.scope.outputs.code == 'true'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gradle-
|
|
|
|
- name: Grant execute permission for gradlew
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: chmod +x ./gradlew
|
|
|
|
# No --no-daemon: the daemon lives only as long as this job container
|
|
# and lets the following steps skip JVM startup + reconfiguration.
|
|
- name: Lint (debug variant only)
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: ./gradlew lintDebug
|
|
|
|
- name: Unit tests
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: ./gradlew testDebugUnitTest
|
|
|
|
- name: Assemble debug APK
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: ./gradlew assembleDebug
|
|
|
|
- name: Trivy filesystem scan
|
|
if: steps.scope.outputs.code == 'true'
|
|
run: |
|
|
set -e
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
SUDO="sudo"
|
|
fi
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y wget apt-transport-https gnupg lsb-release
|
|
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null
|
|
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y trivy
|
|
fi
|
|
trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
|
|
continue-on-error: true
|