Skip to content

Commit 17390ba

Browse files
authored
Fix orphaned CI droplets on cancelled runs (#28)
1 parent 8a914c8 commit 17390ba

2 files changed

Lines changed: 59 additions & 8 deletions

File tree

.github/workflows/integration.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ jobs:
7979
run: |
8080
bash bin/ci/droplet.sh destroy \
8181
"${{ steps.droplet.outputs.DROPLET_ID }}" \
82-
"${{ steps.droplet.outputs.SSH_KEY_ID }}"
82+
"${{ steps.droplet.outputs.SSH_KEY_ID }}" \
83+
"ci-${{ matrix.distro }}-${{ github.run_id }}"

bin/ci/droplet.sh

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
#
44
# Usage:
55
# bin/ci/droplet.sh create <name> <image> <ssh_pub_key_file>
6-
# bin/ci/droplet.sh destroy <droplet_id> [ssh_key_id]
6+
# bin/ci/droplet.sh destroy <droplet_id> [ssh_key_id] [droplet_name]
77
# bin/ci/droplet.sh wait-ssh <ip> <ssh_private_key_file>
88
# bin/ci/droplet.sh run <ip> <ssh_private_key_file> <script>
9+
# bin/ci/droplet.sh list
910
#
1011
# Requires: DO_API_TOKEN env var
1112
#
12-
# create: Registers SSH key with DO, creates droplet, polls until active.
13-
# Outputs: DROPLET_ID=xxx DROPLET_IP=xxx SSH_KEY_ID=xxx
14-
# destroy: Deletes droplet and (optionally) SSH key from DO.
13+
# create: Registers SSH key with DO, creates droplet (tagged baudbot-ci),
14+
# polls until active. Outputs: DROPLET_ID=xxx DROPLET_IP=xxx SSH_KEY_ID=xxx
15+
# destroy: Deletes droplet and (optionally) SSH key from DO. If droplet_id is
16+
# empty but droplet_name is given, looks up the droplet by name.
17+
# This handles cancelled CI runs where the ID was never captured.
1518
# wait-ssh: Polls until SSH is reachable (up to 120s).
1619
# run: Executes a script on the droplet via SSH.
20+
# list: Lists all droplets tagged baudbot-ci.
1721

1822
set -euo pipefail
1923

@@ -71,7 +75,8 @@ cmd_create() {
7175
\"image\": \"$image\",
7276
\"ssh_keys\": [$ssh_key_id],
7377
\"backups\": false,
74-
\"monitoring\": false
78+
\"monitoring\": false,
79+
\"tags\": [\"baudbot-ci\"]
7580
}")
7681

7782
local droplet_id
@@ -111,11 +116,33 @@ print(v4[0]['ip_address'] if v4 else 'none')
111116
echo "SSH_KEY_ID=$ssh_key_id"
112117
}
113118

114-
# ── destroy <droplet_id> [ssh_key_id] ────────────────────────────────────────
119+
# ── destroy <droplet_id> [ssh_key_id] [droplet_name] ─────────────────────────
120+
# If droplet_id is empty but droplet_name is provided, looks up the droplet by
121+
# name. This handles the case where a CI run was cancelled before the create
122+
# step wrote the droplet ID to GITHUB_OUTPUT.
115123
cmd_destroy() {
116124
require_token
117125
local droplet_id="${1:-}"
118126
local ssh_key_id="${2:-}"
127+
local droplet_name="${3:-}"
128+
129+
# If no ID but we have a name, look it up
130+
if [ -z "$droplet_id" ] && [ -n "$droplet_name" ]; then
131+
echo " No droplet ID, looking up by name: $droplet_name" >&2
132+
local data
133+
data=$(do_api GET "droplets?per_page=200&tag_name=baudbot-ci")
134+
droplet_id=$(python3 -c "
135+
import json, sys
136+
for d in json.load(sys.stdin).get('droplets', []):
137+
if d['name'] == '$droplet_name':
138+
print(d['id'])
139+
break
140+
" <<< "$data" 2>/dev/null || true)
141+
142+
if [ -z "$droplet_id" ]; then
143+
echo " No droplet found with name $droplet_name" >&2
144+
fi
145+
fi
119146

120147
if [ -n "$droplet_id" ]; then
121148
local http_code
@@ -165,11 +192,34 @@ cmd_run() {
165192
-i "$key_file" "root@$ip" bash -s < "$script"
166193
}
167194

195+
# ── list ──────────────────────────────────────────────────────────────────────
196+
cmd_list() {
197+
require_token
198+
local data
199+
data=$(do_api GET "droplets?per_page=200&tag_name=baudbot-ci")
200+
201+
python3 -c "
202+
import json, sys
203+
droplets = json.load(sys.stdin).get('droplets', [])
204+
if not droplets:
205+
print(' No CI droplets found', file=sys.stderr)
206+
sys.exit(0)
207+
for d in droplets:
208+
ip = 'no-ip'
209+
for n in d.get('networks', {}).get('v4', []):
210+
if n['type'] == 'public':
211+
ip = n['ip_address']
212+
break
213+
print(f'{d[\"id\"]} {d[\"name\"]} {d[\"created_at\"]} {ip}')
214+
" <<< "$data"
215+
}
216+
168217
# ── Dispatch ──────────────────────────────────────────────────────────────────
169218
case "${1:-}" in
170219
create) shift; cmd_create "$@" ;;
171220
destroy) shift; cmd_destroy "$@" ;;
172221
wait-ssh) shift; cmd_wait_ssh "$@" ;;
173222
run) shift; cmd_run "$@" ;;
174-
*) die "Usage: droplet.sh {create|destroy|wait-ssh|run} ..." ;;
223+
list) shift; cmd_list "$@" ;;
224+
*) die "Usage: droplet.sh {create|destroy|wait-ssh|run|list} ..." ;;
175225
esac

0 commit comments

Comments
 (0)