Jenkins
This guide releases to Aether from Jenkins. It takes more setup than the others, because the Jenkinsfile relies on several pieces of Jenkins configuration. Read the setup sections before you run it.
Before you start, create a deploy-scoped API key as described in Prerequisites.
Use a Multibranch Pipeline
Section titled “Use a Multibranch Pipeline”Create the job as a Multibranch Pipeline, not a plain Pipeline. This is the part that catches people out.
The Jenkinsfile gates its deploy stages with when { branch 'main' }, which reads the BRANCH_NAME environment variable. A Multibranch Pipeline sets BRANCH_NAME for each branch it discovers. A plain Pipeline job never sets it, so when { branch 'main' } is always false, both deploy stages are skipped, and the build still reports success. You get a green build that deployed nothing.
To set it up: New Item → Multibranch Pipeline, add your repository as the branch source, and point the build configuration at the Jenkinsfile.
Configure credentials and global settings
Section titled “Configure credentials and global settings”The Jenkinsfile expects the following to already exist.
API key: a Secret text credential
Section titled “API key: a Secret text credential”Add a Secret text credential with the ID aether-api-key and the value of an API key created in the dashboard (Manage Jenkins → Credentials). The Jenkinsfile reads it with credentials('aether-api-key'), which sets AETHER_API_KEY for the build.
Server URL: a global environment property
Section titled “Server URL: a global environment property”The Jenkinsfile does not hardcode the Aether server URL. It reads AETHER_API_URL and passes --serverUrl only when that variable is set. To point at a non-production server, add AETHER_API_URL under Manage Jenkins → System → Global properties → Environment variables. Leave it unset to use Aether’s production server.
Private repository: a Git credential
Section titled “Private repository: a Git credential”If your repository is private, Jenkins needs a Git credential to clone it. Add a Username with password credential holding a personal access token with repo scope, and select it as the branch source credential on the Multibranch Pipeline.
Docker on the controller
Section titled “Docker on the controller”The pipeline runs on a Docker agent (agent { docker { image 'node:22' } }), so the node that runs it needs Docker installed and available to Jenkins.
The Jenkinsfile
Section titled “The Jenkinsfile”Commit this as Jenkinsfile at the repository root. It pins @aetherpush/cli@0.6.0 and sets CI = 'true' so the CLI runs non-interactively. Jenkins does not export CI on its own, unlike most hosted CI systems, so the Jenkinsfile sets it.
def aetherRelease() { sh ''' set -eu npm ci npm install -g "@aetherpush/cli@${AETHER_CLI_VERSION}" INSTALLED=$(aether --version | tr -d ' \\r\\n') if [ "$INSTALLED" != "$AETHER_CLI_VERSION" ]; then echo "Installed Aether CLI '$INSTALLED' does not match pinned '$AETHER_CLI_VERSION'." exit 1 fi if [ -z "${AETHER_API_KEY:-}" ]; then echo "AETHER_API_KEY is not set. Add it as a Secret Text credential." exit 1 fi if [ -n "${AETHER_API_URL:-}" ]; then aether login --accessKey "$AETHER_API_KEY" --serverUrl "$AETHER_API_URL" else aether login --accessKey "$AETHER_API_KEY" fi aether release-react "$AETHER_APP_NAME" "$AETHER_PLATFORM" \\ --deploymentName "$AETHER_DEPLOYMENT" \\ --rollout "$AETHER_ROLLOUT" \\ --json > release.json node --input-type=commonjs -e ' const fs = require("fs"); const raw = fs.readFileSync("release.json", "utf8").trim(); const lines = raw.split(String.fromCharCode(10)); const r = JSON.parse(lines[lines.length - 1]); const out = { label: r.label, packageHash: r.packageHash, size: r.size, appVersion: r.appVersion, releaseMethod: r.releaseMethod, rollout: r.rollout, isMandatory: r.isMandatory, isDisabled: r.isDisabled }; Object.keys(out).forEach(function (k) { if (out[k] === undefined || out[k] === null) delete out[k]; }); fs.writeFileSync("release.json.tmp", JSON.stringify(out)); fs.renameSync("release.json.tmp", "release.json"); ' ''' archiveArtifacts artifacts: 'release.json', fingerprint: true}
pipeline { agent { docker { image 'node:22' } } environment { CI = 'true' AETHER_CLI_VERSION = '0.6.0' AETHER_APP_NAME = 'my-react-native-app' AETHER_PLATFORM = 'android' AETHER_API_KEY = credentials('aether-api-key') } stages { stage('Deploy to Staging') { when { branch 'main' } environment { AETHER_DEPLOYMENT = 'Staging' AETHER_ROLLOUT = '100%' } steps { script { aetherRelease() } } } stage('Deploy to Production') { when { branch 'main' } input { message 'Deploy to Production?' ok 'Deploy' } environment { AETHER_DEPLOYMENT = 'Production' AETHER_ROLLOUT = '25%' } steps { script { aetherRelease() } } } }}The Deploy to Staging stage runs on main at 100% rollout. Deploy to Production uses an input step, so the build pauses and waits for someone to click Deploy before releasing at 25%.
Change AETHER_APP_NAME and AETHER_PLATFORM in the environment block to your app’s values.
The release artifact
Section titled “The release artifact”After --json writes release.json, the same sh step keeps eight fields: label, packageHash, size, appVersion, releaseMethod, rollout, isMandatory, and isDisabled. archiveArtifacts then archives that file.
--json still prints the full server response, including signed download URLs. Those URLs are not in the artifact. Recopying this Jenkinsfile over an older one drops blobUrl, manifestBlobUrl, description, releasedBy, and uploadTime from the archived file. Pipelines that still have the older Jenkinsfile keep uploading the full response until they recopy.
Keep this guide in sync
Section titled “Keep this guide in sync”This template is maintained at aether-cli/examples/ci/Jenkinsfile. When it changes, update this guide to match. The template is the source of truth.