Skip to content

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.

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.

The Jenkinsfile expects the following to already exist.

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.

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.

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.

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.

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.

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.

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.