diff --git a/.github/workflows/packer-snapshots.yml b/.github/workflows/packer-snapshots.yml index 21bf76e4..d96a8da9 100644 --- a/.github/workflows/packer-snapshots.yml +++ b/.github/workflows/packer-snapshots.yml @@ -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 }} diff --git a/packer/digitalocean.pkr.hcl b/packer/digitalocean.pkr.hcl index acfc2bcd..210f93b4 100644 --- a/packer/digitalocean.pkr.hcl +++ b/packer/digitalocean.pkr.hcl @@ -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 + } }