mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
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:
parent
dadb2387e2
commit
7bebc6558f
2 changed files with 84 additions and 11 deletions
52
.github/workflows/packer-snapshots.yml
vendored
52
.github/workflows/packer-snapshots.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue