Add Compliance Checks to GitHub Actions Before Issues Reach Review
16 Mar 2026 · 6 min read · Erik van Kammen
You've built your app. The tests pass. The build artifact is sitting in your CI workspace. But between "build succeeded" and "upload to store," there's a gap where compliance issues hide. Privacy manifests, over-declared permissions, missing entitlements, SDK licensing, target API levels. You either catch them now or discover them during review. For the broader picture of compliance checks in CI/CD, the GitHub Action is the fastest path to get started.
The AppCompliance GitHub Action plugs into that gap. It takes your compiled IPA, APK, or AAB, uploads it for scanning, waits for results, and returns a pass/warn/fail decision your pipeline can act on. Setup takes about 5 minutes.
The action runs the AppCompliance CLI's ci command inside your GitHub Actions workflow. It:
appcompliance-report.jsonThe exit code is what your pipeline uses for gating. A non-zero exit fails the workflow step, blocking the PR or deployment.
Before adding the action, you'll need an API token from your AppCompliance workspace.
Store the token and your API URL as GitHub repository secrets:
APPCOMPLIANCE_TOKEN with your API tokenAPPCOMPLIANCE_API_URL with your API base URL (e.g. https://app.appcompliance.io)Here's a minimal workflow that scans your iOS build:
# GitHub Actions: basic compliance scan on push to main
name: Compliance Scan
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: app-build
path: build/
- name: Run compliance scan
id: compliance
uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
Replace build/MyApp.ipa with the actual path to your binary. The action works with IPA, APK, and AAB files.
If your build step runs in a separate job, use actions/upload-artifact in the build job and actions/download-artifact in the scan job to pass the binary between them.
The action supports two modes via the mode input:
Default mode (no mode specified): the action evaluates all active rules against your binary. Findings with critical or high severity cause a fail decision.
Regression mode (mode: regression): the action compares findings against the previous scan for the same app. Only new findings cause a fail. Existing findings that haven't been resolved are reported but don't block.
# GitHub Actions: regression mode only flags new findings
- name: Run compliance scan (regression mode)
uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
mode: regression
Regression mode is useful for teams that are onboarding an existing app with known findings. It's especially valuable when SDK updates shift your compliance posture between releases. You don't want the first scan to block your pipeline because of pre-existing issues. You want it to block only when new issues are introduced.
The action downloads a SARIF file that you can upload to GitHub Code Scanning. This shows compliance findings as annotations in your PR's "Security" tab.
# GitHub Actions: scan with SARIF upload for Code Scanning integration
- name: Run compliance scan
id: compliance
uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: appcompliance.sarif
The if: always() ensures the SARIF upload runs even when the scan step fails. This way, findings appear in your PR regardless of the gating decision.
SARIF integration requires GitHub Code Security (formerly GitHub Advanced Security) or a public repository. Private repos on GitHub Free won't see the Code Scanning tab.
The action exposes four outputs you can use in subsequent steps:
| Output | Values | Use for |
|---|---|---|
decision |
pass, warn, fail |
Custom notifications |
exit_code |
0, 1, 2 |
Conditional steps |
report_url |
URL | PR comments, Slack |
sarif_path |
File path | SARIF upload |
Example: posting the scan result to a PR comment:
# GitHub Actions: post scan result as a PR comment
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const decision = '${{ steps.compliance.outputs.decision }}';
const url = '${{ steps.compliance.outputs.report_url }}';
const icon = decision === 'pass' ? '[PASS]' : decision === 'warn' ? '[WARN]' : '[FAIL]';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `${icon} Compliance scan: **${decision}**\n\n[View full report](${url})`
});
Not every team wants a hard gate from day one. Here are two common approaches:
The scan step fails on blocking findings. The PR can't merge. The release can't ship. This is the strictest approach and works best for apps that haven't accumulated compliance debt.
# GitHub Actions: hard gate blocks PR on blocking findings
- name: Run compliance scan
uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
# Step fails on exit code 1, blocking the workflow
Run the scan with continue-on-error: true so findings are reported but don't block the pipeline. Use this while your team triages existing findings and establishes accepted risks.
# GitHub Actions: notify-only mode for onboarding existing apps
- name: Run compliance scan
uses: appcompliance/scan@v1
continue-on-error: true
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
Once your baseline is clean, switch to hard gate mode.
If you ship both iOS and Android, run separate scan steps for each binary:
# GitHub Actions: matrix build scans iOS and Android in parallel
jobs:
scan:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: ios
binary: build/MyApp.ipa
- platform: android
binary: build/app-release.aab
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ matrix.platform }}-build
path: build/
- name: Scan ${{ matrix.platform }}
uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: ${{ matrix.binary }}
Matrix builds run in parallel, so both platforms are scanned simultaneously. Each gets its own gating decision.
The default scan timeout is 900 seconds (15 minutes). Most scans complete in under 5 minutes, but large binaries can take longer. Adjust with the timeout input:
# GitHub Actions: custom timeout for large binaries
- uses: appcompliance/scan@v1
with:
api_url: ${{ secrets.APPCOMPLIANCE_API_URL }}
token: ${{ secrets.APPCOMPLIANCE_TOKEN }}
binary_path: build/MyApp.ipa
timeout: 1200 # 20 minutes
Common issues:
binary_path points to where your build step placed the artifact.CI/CD scanning works on every AppCompliance tier, including Free. The scan itself and the gating decision are available regardless of plan. SARIF export and API-driven workflows are part of the standard offering.
Adding compliance scanning to your pipeline takes 5 minutes. Removing a rejection from your release timeline takes days.
Everything technical in this post is checkable from the build itself, and that is exactly what AppCompliance does: upload an iOS or Android build, and minutes later you have every issue that would get it rejected, with the exact fix next to it.
One email when we open the doors. No spam, just the launch.