feat: full marketplace compliance + automated Vendor API submission (#2295)

Packer template:
- Match official 90-cleanup.sh: remove SSH host keys, create
  revoked_keys, remove cloud-init instances, zero-fill free space,
  use --force-confold for upgrades, autoremove/autoclean
- Add Packer manifest post-processor for snapshot ID extraction
- Remove PACKER_LOG=1 (debug logging not needed in production)

Workflow:
- Add "Submit to DO Marketplace" step after successful build
- Reads agent→app_id mapping from MARKETPLACE_APP_IDS secret (JSON)
- Extracts snapshot ID from Packer manifest, PATCHes Vendor API
- Gracefully handles 400 (app already pending review)
- Skips silently if no MARKETPLACE_APP_IDS secret is configured

Setup: add MARKETPLACE_APP_IDS secret as JSON, e.g.:
  {"claude":"60089fc6...", "codex":"60089fc7..."}
App IDs come from the DO Vendor Portal after initial approval.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-03-07 13:40:04 -08:00 committed by GitHub
parent dadb2387e2
commit 7bebc6558f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 84 additions and 11 deletions

View file

@ -84,8 +84,6 @@ jobs:
- name: Build snapshot
run: packer build -var-file=packer/auto.pkrvars.json packer/digitalocean.pkr.hcl
env:
PACKER_LOG: "1"
- name: Cleanup old snapshots
if: success()
@ -105,3 +103,53 @@ jobs:
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
AGENT_NAME: ${{ matrix.agent }}
- name: Submit to DO Marketplace
if: success()
run: |
# Skip if no marketplace app IDs configured
if [ -z "$MARKETPLACE_APP_IDS" ]; then
echo "No MARKETPLACE_APP_IDS secret — skipping marketplace submission"
exit 0
fi
# Look up this agent's app ID from the JSON map
APP_ID=$(echo "$MARKETPLACE_APP_IDS" | jq -r --arg a "$AGENT_NAME" '.[$a] // empty')
if [ -z "$APP_ID" ]; then
echo "No marketplace app ID for agent ${AGENT_NAME} — skipping"
exit 0
fi
# Extract snapshot ID from Packer manifest
# artifact_id format is "region:snapshot_id" (e.g. "sfo3:12345678")
IMG_ID=$(jq '.builds[-1].artifact_id | split(":")[1] | tonumber' packer/manifest.json)
if [ -z "$IMG_ID" ] || [ "$IMG_ID" = "null" ]; then
echo "Failed to extract snapshot ID from manifest"
exit 1
fi
echo "Submitting snapshot ${IMG_ID} for ${AGENT_NAME} (app: ${APP_ID})"
# PATCH the Vendor API — updates go to "pending" review.
# 400 = app already pending/in-review (expected for nightly runs), not an error.
HTTP_CODE=$(curl -s -o /tmp/mp-response.json -w "%{http_code}" \
-X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
-d "$(jq -n \
--arg reason "Nightly rebuild — $(date -u '+%Y-%m-%d')" \
--argjson imageId "$IMG_ID" \
'{reasonForUpdate: $reason, imageId: $imageId}')" \
"https://api.digitalocean.com/api/v1/vendor-portal/apps/${APP_ID}")
case "$HTTP_CODE" in
200) echo "Marketplace submission accepted (pending review)" ;;
400) echo "App already pending review — skipping (expected for nightly runs)" ;;
*) echo "Marketplace API returned ${HTTP_CODE}:"
cat /tmp/mp-response.json
exit 1 ;;
esac
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
AGENT_NAME: ${{ matrix.agent }}
MARKETPLACE_APP_IDS: ${{ secrets.MARKETPLACE_APP_IDS }}

View file

@ -107,10 +107,13 @@ build {
}
# DO Marketplace: install all security updates and remove DO droplet agent
# Uses --force-confold to keep existing config files during upgrades
provisioner "shell" {
inline = [
"apt-get update -y",
"apt-get dist-upgrade -y",
"apt-get -o Dpkg::Options::='--force-confold' dist-upgrade -y",
"apt-get -y autoremove",
"apt-get -y autoclean",
"apt-get purge -y droplet-agent || true",
"rm -rf /opt/digitalocean",
]
@ -119,22 +122,31 @@ build {
]
}
# DO Marketplace cleanup runs last before snapshot.
# Based on https://github.com/digitalocean/marketplace-partners/blob/master/scripts/cleanup.sh
# Clears secrets, history, logs, and machine-id so each launched droplet
# DO Marketplace cleanup matches digitalocean/marketplace-partners/scripts/90-cleanup.sh
# Clears secrets, keys, history, logs, and machine-id so each launched droplet
# gets a fresh identity. cloud-init re-runs on first boot to re-inject keys.
provisioner "shell" {
inline = [
# Remove SSH authorized keys (cloud-init re-injects them on first boot)
# Ensure /tmp exists with correct permissions
"mkdir -p /tmp",
"chmod 1777 /tmp",
# Remove SSH authorized keys (cloud-init re-injects on first boot)
"rm -f /root/.ssh/authorized_keys",
"find /home -name authorized_keys -delete",
# Clear bash history (history -c is bash-only; Packer runs /bin/sh)
# Remove SSH host keys (regenerated on first boot)
"rm -f /etc/ssh/ssh_host_*",
"touch /etc/ssh/revoked_keys",
"chmod 600 /etc/ssh/revoked_keys",
# Clear bash history
"rm -f /root/.bash_history",
"find /home -name .bash_history -delete",
# Purge log files
"find /var/log -type f -exec truncate --size 0 {} \\;",
# Truncate recent log files and remove archived logs
"find /var/log -mtime -1 -type f -exec truncate -s 0 {} \\;",
"rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????",
# Clear apt cache
"apt-get clean",
@ -143,14 +155,21 @@ build {
# Clear tmp
"rm -rf /tmp/* /var/tmp/*",
# Remove cloud-init instance data so it re-runs on first boot
"rm -rf /var/lib/cloud/instances/*",
# Remove machine-id so each launched droplet gets a unique one
"truncate -s 0 /etc/machine-id",
"rm -f /var/lib/dbus/machine-id",
"ln -sf /etc/machine-id /var/lib/dbus/machine-id",
# Reset cloud-init so it runs again on first boot (re-injects SSH keys, hostname, etc.)
# Reset cloud-init so it runs again on first boot
"cloud-init clean --logs",
# Zero-fill free disk space to reduce snapshot size
"dd if=/dev/zero of=/zerofile bs=4096 || true",
"rm -f /zerofile",
"sync",
]
}
@ -165,4 +184,10 @@ build {
"rm -f /tmp/img_check.sh",
]
}
# Write Packer manifest for automated Marketplace submission
post-processor "manifest" {
output = "packer/manifest.json"
strip_path = true
}
}