Skip to content

Feature/zero config r8 #68

Feature/zero config r8

Feature/zero config r8 #68

name: Build and Test
on:
push:
pull_request:
# Allow other workflows (e.g. Maven Publish) to run this same test suite as a
# gating job, so the test definition stays single-sourced.
workflow_call:
jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
WORKSPACE: "${{ github.workspace }}"
GIT_BRANCH: "${{ github.ref }}"
CURRENT_TAG: "${{ github.ref_name }}"
# Worker endpoints: consumed from GitHub organisation or repository variables.
# Variables MUST be set — no hardcoded fallback is provided to avoid
# exposing worker endpoints in the public repository.
# Worker management (create / redeploy) logic is centralized in the
# core-service-layers-testing script, but invoked here when needed.
TESTING_REPLY_URL: ${{ vars.TESTING_REPLY_URL }}
TESTING_REPLY_URL_UNPROTECTED: ${{ vars.TESTING_REPLY_URL_UNPROTECTED }}
# Required for the redeploy script invocation on failure
CLOUDFLARE_API_TOKEN_WORKERS_DEV: ${{ secrets.CLOUDFLARE_API_TOKEN_WORKERS_DEV }}
CLOUDFLARE_ACCOUNT_ID_WORKERS_DEV: ${{ secrets.CLOUDFLARE_ACCOUNT_ID_WORKERS_DEV }}
steps:
# -----------------------------------------------------------------------
# 1. Checkout this repository
# -----------------------------------------------------------------------
- name: Set up Git
run: git config --global --add safe.directory '*'
- name: Checkout approov-service-retrofit
uses: actions/checkout@v5
with:
path: approov-service-retrofit
# -----------------------------------------------------------------------
# 2. Clone core-service-layers-testing as a sibling directory
# settings.gradle expects: ../core-service-layers-testing/...
# so both repos must sit under the same parent ($GITHUB_WORKSPACE).
# -----------------------------------------------------------------------
- name: Checkout core-service-layers-testing
uses: actions/checkout@v5
with:
repository: approov/core-service-layers-testing
token: ${{ secrets.CORE_SERVICE_LAYERS_TESTING_PAT }}
path: core-service-layers-testing
# -----------------------------------------------------------------------
# 3. Verify worker endpoints (with auto-redeploy)
#
# URLs MUST be set as GitHub organisation or repository variables.
# The step hard-fails if they are missing — no hardcoded fallback.
# If the workers are unavailable, this step invokes the management
# script in core-service-layers-testing to redeploy them.
# -----------------------------------------------------------------------
- name: Verify worker endpoints
run: |
# ── URL resolution ──────────────────────────────────────────────────
# URLs MUST be set as GitHub organisation or repository variables.
# No hardcoded fallback is provided to avoid exposing endpoints
# in the public repository.
if [[ -z "$TESTING_REPLY_URL" || -z "$TESTING_REPLY_URL_UNPROTECTED" ]]; then
echo "ERROR: TESTING_REPLY_URL and TESTING_REPLY_URL_UNPROTECTED must be"
echo " set as GitHub organisation or repository variables."
exit 1
fi
PROTECTED_URL="$TESTING_REPLY_URL"
UNPROTECTED_URL="$TESTING_REPLY_URL_UNPROTECTED"
echo "TESTING_REPLY_URL=$PROTECTED_URL" >> "$GITHUB_ENV"
echo "TESTING_REPLY_URL_UNPROTECTED=$UNPROTECTED_URL" >> "$GITHUB_ENV"
# ── Probe function ──────────────────────────────────────────────────
probe_worker() {
local url="$1"
curl --silent --show-error --fail --max-time 10 \
-X POST "$url" \
-H "Content-Type: application/json" \
-d '{"check":"probe"}' | grep -q '"body"'
}
echo "==> Probing workers..."
if probe_worker "$PROTECTED_URL" && probe_worker "$UNPROTECTED_URL"; then
echo " All workers are healthy."
exit 0
fi
echo " WARNING: One or more workers are down. Attempting redeploy..."
# ── Invoke redeploy script ──────────────────────────────────────────
# The script is in the sibling directory cloned in Step 2.
# It uses the CLOUDFLARE_* env variables defined at the job level.
./core-service-layers-testing/cloudflare-workers/redeploy-workers.sh
echo "==> Verifying after redeploy..."
# Try up to 3 times with a 5s delay between attempts
for i in {1..3}; do
if probe_worker "$PROTECTED_URL" && probe_worker "$UNPROTECTED_URL"; then
echo " Workers successfully restored."
exit 0
fi
echo " Waiting for propagation (attempt $i/3)..."
sleep 5
done
echo "ERROR: Workers are still unreachable after redeployment effort."
exit 1
# -----------------------------------------------------------------------
# 4. Java / Android toolchain setup
# -----------------------------------------------------------------------
- name: Set Up Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Install Android SDK command-line tools
run: |
sudo apt-get update -q
sudo apt-get install -y -q unzip curl
mkdir -p "$ANDROID_HOME/cmdline-tools"
curl -o android-sdk.zip \
https://dl.google.com/android/repository/commandlinetools-linux-9123335_latest.zip
unzip -q android-sdk.zip -d "$ANDROID_HOME/cmdline-tools"
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
rm android-sdk.zip
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
echo "$ANDROID_HOME/cmdline-tools/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator" >> "$GITHUB_PATH"
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses || true
- name: Install required Android SDK packages
run: |
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0"
# -----------------------------------------------------------------------
# 5. Build and run tests
# Gradle is invoked from inside the checked-out service directory.
# The mini-sdk is already available via the sibling path wired into
# settings.gradle — no extra configuration needed here.
# -----------------------------------------------------------------------
- name: Build AAR
working-directory: approov-service-retrofit
run: ./gradlew assembleRelease
- name: Run unit tests
working-directory: approov-service-retrofit
env:
TESTING_REPLY_URL: ${{ env.TESTING_REPLY_URL }}
TESTING_REPLY_URL_UNPROTECTED: ${{ env.TESTING_REPLY_URL_UNPROTECTED }}
run: ./gradlew test
# -----------------------------------------------------------------------
# 6. Print test summary to the console and to the Actions Job Summary
# Parses every JUnit XML produced by Gradle and emits:
# • a formatted table in the step log
# • a markdown table in the Job Summary (Actions UI → Summary tab)
# -----------------------------------------------------------------------
- name: Print test summary
if: always()
working-directory: approov-service-retrofit
run: |
python3 - <<'EOF'
import os, sys, glob, xml.etree.ElementTree as ET
from collections import defaultdict
PASS = "\u2705"
FAIL = "\u274c"
SKIP = "\u23ed\ufe0f"
ERROR = "\u26a0\ufe0f"
results_root = "approov-service/build/test-results"
xml_files = sorted(glob.glob(f"{results_root}/**/*.xml", recursive=True))
if not xml_files:
print("No test-result XML files found.")
sys.exit(0)
# Group suites by variant directory name (testDebugUnitTest, etc.)
by_variant = defaultdict(list)
for path in xml_files:
variant = os.path.basename(os.path.dirname(path))
try:
root = ET.parse(path).getroot()
except ET.ParseError:
continue
suite = {
"name": root.get("name", os.path.basename(path)),
"tests": int(root.get("tests", 0)),
"failures": int(root.get("failures", 0)),
"errors": int(root.get("errors", 0)),
"skipped": int(root.get("skipped", 0)),
"time": float(root.get("time", 0)),
"cases": [],
}
for tc in root.findall("testcase"):
status = PASS
detail = ""
if tc.find("failure") is not None:
status = FAIL
detail = (tc.find("failure").get("message") or "").split("\n")[0][:120]
elif tc.find("error") is not None:
status = ERROR
detail = (tc.find("error").get("message") or "").split("\n")[0][:120]
elif tc.find("skipped") is not None:
status = SKIP
suite["cases"].append({
"name": tc.get("name", "?"),
"time": float(tc.get("time", 0)),
"status": status,
"detail": detail,
})
by_variant[variant].append(suite)
# ── Console output ──────────────────────────────────────────────────
overall_ok = True
for variant, suites in sorted(by_variant.items()):
total_t = sum(s["tests"] for s in suites)
total_f = sum(s["failures"] + s["errors"] for s in suites)
total_s = sum(s["skipped"] for s in suites)
total_p = total_t - total_f - total_s
icon = PASS if total_f == 0 else FAIL
if total_f > 0:
overall_ok = False
print()
print(f"{'='*70}")
print(f" {icon} {variant} | {total_p}/{total_t} passed "
f"| {total_f} failed | {total_s} skipped")
print(f"{'='*70}")
for suite in suites:
short = suite["name"].split(".")[-1]
p = suite["tests"] - suite["failures"] - suite["errors"] - suite["skipped"]
f = suite["failures"] + suite["errors"]
print(f" {PASS if f==0 else FAIL} {short:<55} {p}/{suite['tests']} ({suite['time']:.2f}s)")
for case in suite["cases"]:
if case["status"] != PASS:
print(f" {case['status']} {case['name']}")
if case["detail"]:
print(f" {case['detail']}")
print()
# ── GitHub Job Summary (markdown) ───────────────────────────────────
summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "")
if not summary_path:
sys.exit(0 if overall_ok else 1)
with open(summary_path, "a") as md:
md.write("## \U0001f9ea Unit Test Results\n\n")
for variant, suites in sorted(by_variant.items()):
total_t = sum(s["tests"] for s in suites)
total_f = sum(s["failures"] + s["errors"] for s in suites)
total_s = sum(s["skipped"] for s in suites)
total_p = total_t - total_f - total_s
badge = "\U0001f7e2 PASSED" if total_f == 0 else "\U0001f534 FAILED"
md.write(f"### {variant} &nbsp; {badge}\n\n")
md.write(f"> **{total_p} passed** &nbsp;|&nbsp; "
f"**{total_f} failed** &nbsp;|&nbsp; "
f"**{total_s} skipped** &nbsp;|&nbsp; "
f"**{total_t} total**\n\n")
md.write("| Status | Test suite | Tests | Failed | Skipped | Time |\n")
md.write("|--------|------------|------:|-------:|--------:|-----:|\n")
for suite in suites:
short = suite["name"].split(".")[-1]
f = suite["failures"] + suite["errors"]
icon = "\U0001f7e2" if f == 0 else "\U0001f534"
md.write(f"| {icon} | `{short}` | {suite['tests']} | {f} | {suite['skipped']} | {suite['time']:.2f}s |\n")
# Expand failures inline
failures = [c for s in suites for c in s["cases"] if c["status"] in (FAIL, ERROR)]
if failures:
md.write("\n<details><summary>Failed tests</summary>\n\n")
for c in failures:
md.write(f"- {c['status']} `{c['name']}`")
if c["detail"]:
md.write(f" \n > {c['detail']}")
md.write("\n")
md.write("\n</details>\n")
md.write("\n")
sys.exit(0 if overall_ok else 1)
EOF
# -----------------------------------------------------------------------
# 7. Upload HTML reports as a downloadable artifact
# -----------------------------------------------------------------------
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ github.run_number }}
path: approov-service-retrofit/approov-service/build/reports/tests/
retention-days: 14