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. 🕳️

1. Script Overview

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.

1.1 Core Functionalities

1.2 Key Benefits

1.3 Practical Applications

1.4 Prerequisites

1.5 Example Usage

./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.

2. Script Breakdown

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.

2.1 Core Functions

_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 () {
  # 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.

2.2 Input Parameters

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.

2.3 Package Validation & Creation

PKG_NAME=${PKG_NAME// /_}
if [ $(curl ... | grep "$PKG_NAME.zip" | wc -l) -eq 1 ]; then
  _log "Package exists—skipping creation."
else
  curl -X POST ... # Creates the package
fi

2.4 Dynamic Filter Configuration

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.

2.5 Build & Download Workflow

curl -X POST … -F "cmd=build"

Note: The script waits for the build to complete before proceeding.

BK_FILE="$PKG_NAME-$(date +%Y%m%d-%H%M%S).zip"
curl -o "$BK_FOLDER/$BK_FILE" ...

3. Error Handling, Security Notes, & Logging

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.

3.1 Logging Mechanism

_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).

3.2 Error Validation Workflow

Pre-Flight Checks:

if [ ! -d "$BK_FOLDER" ]; then  
  _log "Backup folder '$BK_FOLDER' does not exist!" && exit 1  
fi  

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

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  

3.4 Common Failure Scenarios

3.5 Security Considerations

3.6 Debugging Tips

./create-remote-aem-pkg.sh ... >> /var/log/aem_backup.log 2>&1

4. Tailoring the Tool to Your Workflow

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.

4.1 Adjusting the Backup Filename Format

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).

4.2 Expanding or Modifying Filters

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/*\"}]}"  

4.3 Enhancing Security

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" ...

4.4 Adding Post-Build Actions

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 {} \;  

4.5 Adding Notification Alerts

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  

5. Conclusion

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.

Key Takeaways

  1. Automation Wins: By eliminating repetitive GUI interactions, the script reduces human error and frees teams to focus on higher-value tasks.

  2. Flexibility Matters: Whether backing up critical content, syncing environments, or preparing for updates, the script adapts to diverse use cases with minimal tweaking.

  3. 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! 🚀

Appendix

Complete Code

#!/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

References

[¹] Skipping SSL verification with curl -k is handy for testing, but you’ll want something sturdier in production (for example --cacert)!

[²] AEM Package Manager Official Documentation