In the modern DevOps lifecycle, “Manual Upload” is a dirty word. If your mobile developers are building an APK/IPA, downloading it to their desktop, logging into the MDM console, and clicking “Upload App,” your pipeline is broken. This manual gap introduces human error, slows down release velocity, and frustrates high-performing engineering teams. For Enterprises managing custom internal apps, the goal is Headless App Delivery with Hexnode CI/CD integration.
It is a zero-touch pipeline where code is committed, built, and deployed to 50,000 devices without requiring a human to log into the Hexnode dashboard. This guide provides the architecture and code to automate app delivery directly from your CI/CD pipeline (such as GitHub Actions, Jenkins, or GitLab) to your device management portal.
The Architecture of Headless MDM
Before writing code, we must define the architecture. “Headless MDM” means removing the Administrative Console from the critical path of software delivery.
The “Human Air-Gap” vs. The Automated Pipeline
In a traditional workflow, the MDM admin is a bottleneck. Developers wait for admins to deploy builds. Admins wait for developers to fix build errors. In a Headless Architecture, the MDM acts as a silent infrastructure layer—a “dumb pipe” that accepts binaries from the build server and delivers them to the edge.
The Data Flow
- Commit: A developer pushes code to the main branch of the GitHub repository.
- Build: GitHub Actions spins up a runner to compile the binary (Gradle for Android, xcodebuild for iOS).
- Authentication: The runner authenticates with the Hexnode REST API using secure secrets.
- Ingestion: The binary is uploaded directly to the Hexnode App Repository via a POST request.
- Orchestration: The API triggers a policy update, assigning the new app version to a specific “Deployment Ring” (Device Group).
- Delivery: Hexnode pushes the app to 50,000 devices via WebSocket command
The Benefit: Zero human touches between “Git Commit” and “Device Install”.
Prerequisites & Security Setup
Automation requires rigorous security. Since we are creating a pipeline that can push software to your entire corporate fleet, we must secure the credentials.
Generating the Hexnode API Key
You need a Hexnode API key with specific scopes. Do not use a “Super Admin” key if possible; adhere to the Principle of Least Privilege.
- Log in to your Hexnode Portal.
- Navigate to Admin API.
- Click New API Key.
- Scopes Required:
a. Apps (Read/Write)
b. Policies (Read/Write) – If automating assignment.
c. Device Groups (Read) - Copy the key immediately.
Configuring GitHub Secrets
Never hardcode API keys or URLs in your main.yml file. Use GitHub Secrets to inject them as environment variables at runtime.
- Go to your GitHub Repository.
- Navigate to Settings > Secrets and variables > Actions.
- Create the following Repository Secrets:
a. HEXNODE_API_KEY: The alphanumeric key generated above.
b. HEXNODE_PORTAL_URL: Your instance URL (e.g., https://enterprise-a.hexnodemdm.com)
The Integration Logic (Python Script)
While you can use curl commands directly in your YAML workflow, this is fragile. A Python script allows for robust error handling, response parsing, and logic branching (e.g., “If upload fails, retry twice”).
Create a file named deploy_to_hexnode.py in the root of your repository.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
import requests import os import sys import json # 1. Load Environment Variables # These are injected by GitHub Actions at runtime API_KEY = os.getenv('HEXNODE_API_KEY') PORTAL_URL = os.getenv('HEXNODE_PORTAL_URL') # 2. Validate Inputs if not API_KEY or not PORTAL_URL: print("Error: Missing HEXNODE_API_KEY or HEXNODE_PORTAL_URL") sys.exit(1) if len(sys.argv) < 2: print("Error: Path to app binary (APK/IPA) not provided") sys.exit(1) APP_PATH = sys.argv[1] # 3. Configure Headers headers = { "Authorization": f"{API_KEY}" # Note: Content-Type for file uploads is handled automatically by requests } def upload_app(file_path): """ Uploads the binary to the Hexnode App Repository. """ if not os.path.exists(file_path): print(f"Error: File not found at {file_path}") sys.exit(1) print(f"Starting upload for: {file_path}") print(f"Target: {PORTAL_URL}/api/v1/applications/") upload_url = f"{PORTAL_URL}/api/v1/applications/" try: with open(file_path, 'rb') as app_file: files = {'file': app_file} # 'app_type' ensures Hexnode treats this as a private Enterprise app data = { 'app_type': 'enterprise', 'platform': 'android', # Change to 'ios' dynamically if needed 'notify_users': 'false' # Silent update preferred for Kiosks } response = requests.post(upload_url, headers=headers, files=files, data=data) # 4. Handle Response if response.status_code in [200, 201]: resp_json = response.json() app_id = resp_json.get('id') version = resp_json.get('version') print(f"Upload Success!") print(f"App ID: {app_id}") print(f"Version: {version}") return app_id else: print(f"API Error {response.status_code}: {response.text}") sys.exit(1) except Exception as e: print(f"Critical Exception: {str(e)}") sys.exit(1) if __name__ == "__main__": uploaded_app_id = upload_app(APP_PATH) |
Step 3: The CI/CD Pipeline (GitHub Actions YAML)
Now we configure the “Runner” that will execute the build and run our Python script. Create a file at .github/workflows/hexnode-android-deploy.yml.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
YAML name: Hexnode Enterprise Deploy # Trigger Logic: Run only on pushes to specific branches on: push: branches: - "main" - "staging" workflow_dispatch: # Allows manual triggering from GitHub UI jobs: build-and-deploy: name: Build APK and Push to MDM runs-on: ubuntu-latest steps: # --- Phase 1: Setup --- - name: Checkout Source Code uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' # --- Phase 2: Build --- - name: Build with Gradle run: ./gradlew assembleRelease # This generates the APK in app/build/outputs/apk/release/ # --- Phase 3: Prepare Environment --- - name: Install Python Dependencies run: pip install requests # --- Phase 4: Deploy to Hexnode --- - name: Upload to Hexnode API env: # Inject Secrets into the Environment for the Python script HEXNODE_API_KEY: ${{ secrets.HEXNODE_API_KEY }} HEXNODE_PORTAL_URL: ${{ secrets.HEXNODE_PORTAL_URL }} run: | # Find the APK file dynamically (handling version changes in filenames) APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1) echo "Found artifact at: $APK_PATH" # Execute the deployment script python deploy_to_hexnode.py "$APK_PATH" |
Advanced Strategy – Deployment Rings
Pushing code to production immediately is risky. A mature enterprise pipeline uses Deployment Rings. You can orchestrate this entirely via the Hexnode API by mapping Git Branches to Device Groups.
The Logic
- Alpha Ring (Dev Branch): Code pushed to dev is uploaded and assigned to the “IT QA Devices” Group in Hexnode.
- Beta Ring (Staging Branch): Code pushed to staging is assigned to the “Store Managers” Group.
- Production Ring (Main Branch): Code pushed to main is assigned to the “All Kiosks” Group.
Implementing Logic in Python
You can enhance the Python script to read the GITHUB_REF (branch name) and make a second API call to assign the app.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# ... (inside deploy_to_hexnode.py) def assign_to_group(app_id, branch_name): # Map Branches to Hexnode Group IDs (Found in URL of Group View) RING_MAP = { 'refs/heads/dev': 101, # IT QA Group 'refs/heads/staging': 102, # Beta Testers 'refs/heads/main': 103 # Production } target_group_id = RING_MAP.get(branch_name) if target_group_id: print(f"Branch is '{branch_name}'. Assigning to Group ID {target_group_id}...") # Call Hexnode Policy API to add 'app_id' to 'target_group_id' # (Refer to Hexnode API docs for '/api/v1/policies/' endpoint) else: print("Branch not mapped to a ring. Upload only.") |
Troubleshooting & Best Practices
Handling API Rate Limits
Hexnode’s standard API limits are generous, but CI/CD pipelines can trigger burst limits if you run 50 builds an hour.
- Best Practice: Implement a “Debounce” or “Cache” logic. If the binary hash hasn’t changed, skip the upload step.
- Enterprise Tier: For high-frequency pipelines (e.g., 500+ deploys/day), contact Hexnode support to enable “High-Throughput API” on your tenant.
Version Control
Hexnode uses the versionCode and versionName inside the APK’s AndroidManifest.xml to determine if an app is an update.
- Critical: If you upload an APK with the same versionCode as an existing app, the API may reject it or treat it as a duplicate.
- Fix: Ensure your Gradle build script auto-increments the versionCode (e.g., using the Git Commit Count or a Timestamp) before building.
Conclusion
By integrating Hexnode with GitHub Actions, you transform your MDM from a static administrative tool into a dynamic infrastructure platform. You eliminate the “human middleware,” reduce deployment time from hours to minutes, and ensure that your Kiosks and Corporate devices are always running the latest, most secure code. This is the difference between “Managing Devices” and “Engineering a Fleet.”
Streamline Hexnode deployments via GitHub.
Start your 14-day free trial for Hexnode and GitHub integration.
SIGN UP NOWFAQs
1. Can I automate app uploads to Hexnode?
Yes. Hexnode supports Headless App Delivery via its REST API. Developers can integrate Hexnode directly into CI/CD pipelines (like GitHub Actions, Jenkins, or Azure DevOps) to automatically build, upload, and distribute enterprise apps (.apk or .ipa) without logging into the management console.
2. How do I generate a Hexnode API Key for automation?
To generate an API key, log in to your Hexnode portal and navigate to Admin > API > New API Key. Allow your CI/CD scripts to upload and assign applications programmatically.