Adobe Experience Manager (AEM) packages are the unsung heroes of content management — powerful containers that bundle everything from code and configurations to critical content. But let’s face it: manually creating, configuring, and downloading these packages can feel like a tedious dance of clicks.
What if you could automate this process with a few keystrokes, ensuring consistency, speed, reliability, and less heavy lifting?
I will show you a Bash script that flips the script (pun intended!) on how AEM developers and admins work with the Package Manager API. Think about crafting packages in seconds, tailoring filters on the fly, and snagging backups with surgical precision — all before your coffee cools to that perfect sipping temperature. ☕
Before we dive in, a quick note: This article is a deep dive, meticulously detailed and unapologetically technical. We’ll dissect the script’s logic, explore AEM API intricacies, and troubleshoot edge cases. For developers eager to jump straight into the code, you can jump to the bottom of the article. But if you’re here to understand the how and why behind the automation, strap in — we’re going all the way down the rabbit hole. 🕳️
The create-remote-aem-pkg.sh
script automates interactions with AEM’s Package Manager API, offering a structured approach to package creation, configuration, and distribution. Designed for developers and administrators, it replaces manual workflows with a command-line-driven process that emphasizes consistency and reliability.
/content/dam
, /apps
) to include in the package.curl
./etc
or /apps
before applying system updates../create-remote-aem-pkg.sh admin securepass123 localhost 4502 backup-group "Content Backup" /backups /content/dam /etc/clientlibs
This command creates a package named “Content Backup” under the group backup-group
, including /content/dam
and /etc/clientlibs
, and saves the output to the /backups
directory.
Let’s dissect the create-remote-aem-pkg.sh
script (you can find it at the bottom of the article) to understand how it orchestrates AEM package management. We’ll focus on its structure, key functions, and workflow logic—ideal for developers looking to customize or debug the tool.
_log()
: A utility function that prefixes messages with timestamps for clear audit trails._log () {
echo "[$(date +%Y.%m.%d-%H:%M:%S)] $1"
}
Why it matters: Ensures every action (e.g., “Package built”) is logged with context, simplifying troubleshooting.
check_last_exec()
: Validates the success of prior commands by checking exit codes and API responses.check_last_exec () {
# Checks $? (exit status) and $CURL_OUTPUT for errors
if [ "$status" -ne 0 ] || [[ $output =~ .*success\":false* ]]; then
_log "Error detected!";
exit 1;
fi
}
Why it matters: Prevents silent failures by halting execution on critical errors like authentication issues or invalid paths.
The script accepts seven positional arguments followed by dynamic filters:
USR="$1" # AEM username
PWD="$2" # AEM password
SVR="$3" # Server host (e.g., localhost)
PORT="$4" # Port (e.g., 4502)
PKG_GROUP="$5" # Package group (e.g., "backups")
PKG_NAME="$6" # Package name (e.g., "dam-backup")
BK_FOLDER="$7" # Backup directory (e.g., "/backups")
shift 7 # Remaining arguments become filters (e.g., "/content/dam")
Positional arguments ensure simplicity, while shift
handles variable filter paths flexibly.
PKG_NAME
with underscores to avoid URL issues.PKG_NAME=${PKG_NAME// /_}
curl
to list packages via AEM’s API, avoiding redundant creations.if [ $(curl ... | grep "$PKG_NAME.zip" | wc -l) -eq 1 ]; then
_log "Package exists—skipping creation."
else
curl -X POST ... # Creates the package
fi
Constructs a JSON array of filters from input paths:
FILTERS_PARAM=""
for i in "${!FILTERS[@]}"; do
FILTERS_PARAM+="{\"root\": \"${FILTERS[$i]}\", \"rules\": []}"
# Adds commas between entries, but not after the last
done
Example output:
[{"root": "/content/dam"}, {"root": "/apps"}]
This JSON is injected into the package definition via AEM’s /crx/packmgr/update.jsp
endpoint.
build
command:curl -X POST … -F "cmd=build"
Note: The script waits for the build to complete before proceeding.
curl
to fetch the .zip
and save it with a timestamped filename:BK_FILE="$PKG_NAME-$(date +%Y%m%d-%H%M%S).zip"
curl -o "$BK_FOLDER/$BK_FILE" ...
Robust error handling and logging are critical for unattended scripts like create-remote-aem-pkg.sh
, ensuring failures are caught early and logged clearly. Here’s how the script safeguards against unexpected issues and provides actionable insights.
_log
function prefixes every message with a [YYYY.MM.DD-HH:MM:SS]
timestamp, creating an audit trail for debugging:_log "Starting backup process..." # Output: [2023.10.25-14:30:45] Starting backup process...
Why it matters: Timestamps help correlate script activity with AEM server logs or external events (e.g., cron job schedules).
Pre-Flight Checks:
BK_FOLDER
) before proceeding:if [ ! -d "$BK_FOLDER" ]; then
_log "Backup folder '$BK_FOLDER' does not exist!" && exit 1
fi
PKG_NAME
to avoid URL issues (e.g., spaces replaced with underscores).
API Response Validation:
The check_last_exec
function examines both shell exit codes ($?
) and AEM API responses:
check_last_exec "Error message" "$CURL_OUTPUT" $CURL_STATUS
curl
network failures) trigger immediate exits.
success\":false
JSON responses or "HTTP ERROR" strings in AEM output.
3.3 HTTP Status Verification: When downloading the package, the script checks for a 200
status code:
if [ "$(curl -w "%{http_code}" ...)" -eq "200" ]; then
# Proceed if download succeeds
else
_log "Error downloading the package!" && exit 1
fi
check_last_exec
catches 401 Unauthorized
responses and exits with a clear error message.success:false
, the script logs "Error adding filters" and terminates.BK_FILE
, checks file size with -s
flag and alerts before exiting.curl
exits with a non-zero code, the script logs "Error building the package".curl -k
for simplicity, which skips SSL verification. Recommendation for Production: Replace with --cacert
to specify a CA bundle.
$AEM_PASSWORD
).set -x
at the script’s start to print executed commands.curl
commands outside the script./create-remote-aem-pkg.sh ... >> /var/log/aem_backup.log 2>&1
The create-remote-aem-pkg.sh
script is designed to be a starting point—a foundation you can modify to align with your team’s needs. Below are common customizations, along with implementation guidance, to extend its functionality or adapt it to specific use cases.
The default filename uses a timestamp ($PKG_NAME-$(date +%Y%m%d-%H%M%S).zip
). Modify this to include environment names, project IDs, or semantic versioning:
# Example: Include environment (e.g., "dev", "prod")
BK_FILE="${PKG_NAME}-${ENV}-$(date +%Y%m%d).zip"
# Example: Add Git commit SHA for traceability
COMMIT_SHA=$(git rev-parse --short HEAD)
BK_FILE="${PKG_NAME}-${COMMIT_SHA}.zip"
Tip: Ensure date/time formats avoid characters forbidden in filenames (e.g., colons :
on Windows).
The script accepts dynamic paths as filters but you can also hardcode frequently used paths or add exclusions:
# Hardcode essential paths (e.g., "/var/audit")
DEFAULT_FILTERS=("/content/dam" "/apps" "/var/audit")
FILTERS=("${DEFAULT_FILTERS[@]}" "${@}") # Merge with command-line inputs
# Add exclusion rules (requires AEM API support)
FILTERS_PARAM+="{\"root\": \"${FILTERS[$i]}\", \"rules\": [{\"modifier\": \"exclude\", \"pattern\": \".*/test/*\"}]}"
Avoid Plaintext Passwords:
Use environment variables or a secrets manager to inject credentials:
# Fetch password from environment variable
PWD="$AEM_PASSWORD"
# Use AWS Secrets Manager (example)
PWD=$(aws secretsmanager get-secret-value --secret-id aem/prod/password --query SecretString --output text)
Enforce SSL Validation:
Replacecurl -k
(insecure) with a trusted CA certificate:
curl --cacert /path/to/ca-bundle.crt -u "$USR":"$PWD" ...
Extend the script to trigger downstream processes after a successful download:
# Example: Upload to cloud storage
aws s3 cp "$BK_FOLDER/$BK_FILE" s3://my-backup-bucket/
# Example: Validate package integrity
CHECKSUM=$(sha256sum "$BK_FOLDER/$BK_FILE" | cut -d ' ' -f 1)
_log "SHA-256 checksum: $CHECKSUM"
# Example: Clean up old backups (retain last 7 days)
find "$BK_FOLDER" -name "*.zip" -mtime +7 -exec rm {} \;
Notify teams of success/failure via Slack, email, or monitoring tools:
# Post to Slack on failure
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"🚨 AEM backup failed: $(hostname)\"}" \
https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# Send email via sendmail
if [ $? -ne 0 ]; then
echo "Subject: Backup Failed" | sendmail [email protected]
fi
Managing AEM packages doesn’t have to be a manual, error-prone chore. With the create-remote-aem-pkg.sh
script, you can transform package creation, filtering, and distribution into a streamlined, repeatable process. This tool isn’t just about saving time, it’s about enabling consistency, reliability, and scalability in your AEM operations.
Automation Wins: By eliminating repetitive GUI interactions, the script reduces human error and frees teams to focus on higher-value tasks.
Flexibility Matters: Whether backing up critical content, syncing environments, or preparing for updates, the script adapts to diverse use cases with minimal tweaking.
Resilience is Key: Built-in logging, error checks, and security considerations ensure the script behaves predictably, even when things go sideways.
Great tools are born from real-world challenges. This script is a starting point; think of it as a foundation to build upon as your team’s needs grow. Whether you’re a solo developer or part of a large DevOps team, automation like this exemplifies how small investments in code can yield outsized returns in productivity and peace of mind.
Ready to take the next step?
Thank you for following along — now go forth and automate! 🚀
#!/bin/bash
set -eo pipefail
# The script will create a package thought the package manager api:
# - The package is created, if not already present
# - Package filters are populated accordingly to specified paths
# - Package is builded
# - Package is download to the specified folder
_log () {
echo "[$(date +%Y.%m.%d-%H:%M:%S)] $1"
}
check_last_exec () {
local message="$1"
local output="$2"
local status=$3
if [ "$status" -ne 0 ]; then
echo && echo "$message" && echo
exit 1
fi
if [[ $output =~ .*success\":false* ]] || [[ $output =~ .*"HTTP ERROR"* ]]; then
_log "$message"
exit 1
fi
}
USR="$1"
PWD="$2"
SVR="$3"
PORT="$4"
PKG_GROUP="$5"
PKG_NAME="$6"
BK_FOLDER="$7"
shift 7
# The following paths will be included in the package
FILTERS=($@)
BK_FILE=$PKG_NAME"-"$(date +%Y%m%d-%H%M%S).zip
_log "Starting backup process..."
echo "AEM instance: '$SVR':'$PORT'
AEM User: '$USR'
Package group: $PKG_GROUP
Package name: '$PKG_NAME'
Destination folder: $BK_FOLDER
Destination file: '$BK_FILE'
Filter paths: "
printf '\t%s\n\n' "${FILTERS[@]}"
if [ ! -d "$BK_FOLDER" ]; then
_log "Backup folder '$BK_FOLDER' does not exist!" && echo
exit 1
fi
PKG_NAME=${PKG_NAME// /_}
check_last_exec "Error replacing white space chars from package name!" "" $? || exit 1
_log "Removed whitespaces from package name: '$PKG_NAME'"
BK_FILE=$PKG_NAME.zip
_log "Backup file: '$BK_FILE'"
_log "Creating the package..."
if [ $(curl -k -u "$USR":"$PWD" "$SVR:$PORT/crx/packmgr/service.jsp?cmd=ls" 2>/dev/null | grep "$PKG_NAME.zip" | wc -l) -eq 1 ]; then
_log " Package '$PKG_GROUP/$PKG_NAME' is already present: skipping creation."
else
curl -k --silent -u "$USR":"$PWD" -X POST \
"$SVR:$PORT/crx/packmgr/service/.json/etc/packages/$PKG_GROUP/$PKG_NAME?cmd=create" \
-d packageName="$PKG_NAME" -d groupName="$PKG_GROUP"
check_last_exec " Error creating the package!" "" $?
_log " Package created"
fi
# create filters variable
FILTERS_PARAM=""
ARR_LEN="${#FILTERS[@]}"
for i in "${!FILTERS[@]}"; do
FILTERS_PARAM=$FILTERS_PARAM"{\"root\": \"${FILTERS[$i]}\", \"rules\": []}"
T=$((i+1))
if [ $T -ne $ARR_LEN ]; then
FILTERS_PARAM=$FILTERS_PARAM", "
fi
done
# add filters
_log "Adding filters to the package..."
CURL_OUTPUT=$(curl -k --silent -u "$USR":"$PWD" -X POST "$SVR:$PORT/crx/packmgr/update.jsp" \
-F path=/etc/packages/"$PKG_GROUP"/"$PKG_NAME".zip -F packageName="$PKG_NAME" \
-F groupName="$PKG_GROUP" \
-F filter="[$FILTERS_PARAM]" \
-F "_charset_=UTF-8")
CURL_STATUS=$?
# Pass the status to the check_last_exec function
check_last_exec "Error adding filters to the package!" "$CURL_OUTPUT" $CURL_STATUS
_log " Package filters updated successfully."
# build package
_log "Building the package..."
CURL_OUTPUT=$(curl -k -u "$USR":"$PWD" -X POST \
"$SVR:$PORT/crx/packmgr/service/script.html/etc/packages/$PKG_GROUP/$PKG_NAME.zip" \
-F "cmd=build")
check_last_exec " Error building the package!" "$CURL_OUTPUT" $?
_log " Package built."
# download package
_log "Downloading the package..."
if [ "$(curl -w "%{http_code}" -o "$BK_FOLDER/$BK_FILE" -k --silent -u "$USR":"$PWD" "$SVR:$PORT/etc/packages/$PKG_GROUP/$PKG_NAME.zip")" -eq "200" ]; then
if [ -f "$BK_FOLDER/$BK_FILE" ] && [ -s "$BK_FOLDER/$BK_FILE" ]; then
_log " Package $BK_FILE downloaded in $BK_FOLDER."
exit 0
fi
fi
_log " Error downloading the package!"
exit 1
[¹] Skipping SSL verification with curl -k
is handy for testing, but you’ll want something sturdier in production (for example --cacert
)!