Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9973072
Initialize guard and bypass fixes, gating logic, test isolation, and …
May 22, 2026
63f1ee3
fix: preserve service layer state on SDK initialization failure
May 30, 2026
651278c
chore: bump version to 3.5.6, update CHANGELOG and README
May 30, 2026
f70c37c
Revise SECURITY.md for clarity on versions and reporting
naynovi Jun 1, 2026
0166a0c
Merge branch 'pr-13' into feature/3.6.0
charlesoj6205 Jun 2, 2026
6bf6a40
merge: fold feature/3.6.0 (SECURITY.md) into initialize-guard branch
ivolz Jun 21, 2026
c7d7ea1
fix(security): correct supported-versions threshold to < 3.5 and fix …
ivolz Jun 21, 2026
9027b9a
docs: add GitHub badge row and initialize() try/catch example to README
ivolz Jun 21, 2026
24791c3
fix(signing): fail open on account/base64/ASN.1 errors (core #564)
ivolz Jun 21, 2026
0e44f1e
build: shade BouncyCastle into io.approov.internal.httpsurlconn + add…
ivolz Jun 21, 2026
2b70de1
feat(signing): add addApproov(connection, byte[]) body-digest overloa…
ivolz Jun 21, 2026
ac087f6
build(test): wire mini-sdk integration projects (approov-sdk, test-su…
ivolz Jun 21, 2026
8cb5cc3
feat!: remove automated query param substitution (Issue #14); release…
ivolz Jun 21, 2026
36eb0e6
test+fix: mini-sdk integration tests; NO_APPROOV_SERVICE emits empty …
ivolz Jun 21, 2026
a2552d8
docs: document addApproov(connection, byte[]) body-digest overload; d…
ivolz Jun 21, 2026
2df4f75
ci: fail publish if CHANGELOG top entry does not match the release tag
ivolz Jun 21, 2026
9950bf4
fix: spec-review findings — ignore empty-config-after-valid, null pre…
ivolz Jun 21, 2026
ad1355e
chore: strengthen byte-seq test, document initialize() throws, remove…
ivolz Jun 21, 2026
fe6cbd5
fix: message signing serialization/base-build now fail open (#564)
ivolz Jun 22, 2026
716165f
fix: report service-layer version in setUserProperty on release
ivolz Jun 23, 2026
7f92729
ci: auto-tag releases from CHANGELOG on main
ivolz Jun 23, 2026
3441ef4
ci: full build_and_test with mini-SDK; gate tagging on tests
ivolz Jun 23, 2026
cc7eac5
Apply suggestions from code review
charlesoj6205 Jun 23, 2026
b3697e4
Fix stale Javadoc: remove references to removed query substitution
Copilot Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4

# Fail fast if the release tag and the CHANGELOG have drifted, before any
# build or publish work. The top "## [x.y.z]" entry must match the tag.
- name: Verify CHANGELOG matches tag
run: |
TAG="${{ github.ref_name }}"
CHANGELOG_VERSION=$(grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') || true
if [ -z "$CHANGELOG_VERSION" ]; then
echo "::error::Could not find a version header (## [x.y.z]) in CHANGELOG.md"
exit 1
fi
if [ "$CHANGELOG_VERSION" != "$TAG" ]; then
echo "::error::CHANGELOG.md top entry ($CHANGELOG_VERSION) does not match release tag ($TAG). Update CHANGELOG.md before tagging."
exit 1
fi
echo "CHANGELOG.md top entry matches tag: $TAG"

- name: Set Up Java
uses: actions/setup-java@v4
with:
Expand Down Expand Up @@ -70,8 +86,10 @@ jobs:
KEY_ID=$(gpg --list-keys --with-colons | grep pub | cut -d: -f5)
echo -e "trust\n5\ny\nquit" | gpg --batch --yes --command-fd 0 --edit-key $KEY_ID

# Inject the release version (the git tag) so the published AAR bakes the correct
# BuildConfig.APPROOV_SERVICE_VERSION (reported via Approov.setUserProperty) instead of "dev".
- name: Build AAR
run: ./gradlew assembleRelease
run: ./gradlew assembleRelease -PapproovServiceVersion=${{ github.ref_name }}

- name: Create Package
run: cd .maven && ./build-and-sign.sh
Expand Down
345 changes: 345 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
name: Build and Test

# push covers direct work on main while pull_request covers all PR branches,
# avoiding duplicate runs for branches that have an open PR
on:
push:
branches:
- main
pull_request:

jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
WORKSPACE: "${{ github.workspace }}"
GIT_BRANCH: "${{ github.ref }}"
CURRENT_TAG: "${{ github.ref_name }}"
# Worker endpoints: consumed from GitHub organisation variables.
# The verification step below fails fast if these variables are not
# defined, as no hardcoded fallback is provided for security.
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 }}
CORE_SERVICE_LAYERS_TESTING_PAT: ${{ secrets.CORE_SERVICE_LAYERS_TESTING_PAT }}

steps:
# -----------------------------------------------------------------------
# 1. Checkout this repository
# -----------------------------------------------------------------------
- name: Set up Git
run: git config --global --add safe.directory '*'

- name: Checkout approov-service-httpsurlconn
uses: actions/checkout@v6
with:
path: approov-service-httpsurlconn

# -----------------------------------------------------------------------
# 2. Clone core-service-layers-testing as a sibling directory
# (provides the mini-SDK: approov-sdk + test-support).
# -----------------------------------------------------------------------
- name: Checkout core-service-layers-testing
if: ${{ env.CORE_SERVICE_LAYERS_TESTING_PAT != '' }}
uses: actions/checkout@v6
with:
repository: approov/core-service-layers-testing
token: ${{ secrets.CORE_SERVICE_LAYERS_TESTING_PAT }}
path: core-service-layers-testing

# -----------------------------------------------------------------------
# 2b. Point Gradle at the checked-out mini-SDK.
# settings.gradle resolves miniSdkAndroidRoot relative to the repo root
# (default '../../core-service-layers-testing/...' for the nested local
# layout). In CI the testing repo is a sibling of the service repo, so
# override with an absolute path via local.properties.
# -----------------------------------------------------------------------
- name: Wire mini-SDK path for CI layout
if: ${{ env.CORE_SERVICE_LAYERS_TESTING_PAT != '' }}
run: |
echo "miniSdkAndroidRoot=${{ github.workspace }}/core-service-layers-testing/mini-sdk/android" \
>> approov-service-httpsurlconn/local.properties

# -----------------------------------------------------------------------
# 3. Verify worker endpoints (with auto-redeploy)
# -----------------------------------------------------------------------
- name: Verify worker endpoints
if: ${{ env.CORE_SERVICE_LAYERS_TESTING_PAT != '' }}
run: |
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."
echo " See CONTRIBUTING.md for setup instructions."
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_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..."
./core-service-layers-testing/cloudflare-workers/redeploy-workers.sh

echo "==> Verifying after redeploy..."
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
# -----------------------------------------------------------------------
- name: Build AAR
working-directory: approov-service-httpsurlconn
run: ./gradlew assembleRelease

- name: Run unit tests
working-directory: approov-service-httpsurlconn
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
# -----------------------------------------------------------------------
- name: Print test summary
if: always()
working-directory: approov-service-httpsurlconn
run: |
python3 - <<'EOF'
import os, sys, glob, xml.etree.ElementTree as ET
from collections import defaultdict

PASS = "✅"
FAIL = "❌"
SKIP = "⏭️"
ERROR = "⚠️"

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)

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)

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()

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")
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-httpsurlconn/approov-service/build/reports/tests/
retention-days: 14

# ---------------------------------------------------------------------------
# Automatic release tagging (main only). Runs only after build-and-test
# passes, so a release is never tagged on a failing build. The CHANGELOG top
# entry drives a matching git tag, which triggers the Maven Publish workflow
# (build_and_publish.yml). The published AAR bakes the tag version into
# BuildConfig.APPROOV_SERVICE_VERSION (-PapproovServiceVersion), reported at
# runtime via Approov.setUserProperty. Skipped if the tag already exists.
# ---------------------------------------------------------------------------
tag-release:
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Extract Version from CHANGELOG
id: get-version
run: |
VERSION=$(grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') || true
if [ -z "$VERSION" ]; then
echo "::error::Could not find a version header (## [x.y.z]) in CHANGELOG.md"
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Version extracted from CHANGELOG.md is: $VERSION"

- name: Create and Push Tag
run: |
VERSION="${{ steps.get-version.outputs.VERSION }}"

if git fetch origin "refs/tags/$VERSION" --quiet 2>/dev/null; then
echo "Tag $VERSION already exists. Skipping tagging."
exit 0
fi

echo "Tag $VERSION does not exist. Verifying dev placeholder..."
FILE="approov-service/build.gradle"
if ! grep -q "'approovServiceVersion') ?: 'dev'" "$FILE"; then
echo "::error::Could not find default 'dev' version in $FILE"
exit 1
fi

echo "Configuring git..."
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

echo "Creating and pushing tag $VERSION..."
git tag "$VERSION"
git push origin "$VERSION"
Loading