| 1 | #!/usr/bin/env bash |
| 2 | # SPDX-License-Identifier: AGPL-3.0-or-later |
| 3 | # |
| 4 | # Generate an Ansible inventory for DigitalOcean shithub Actions runners. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | |
| 8 | POOL_NAME="${POOL_NAME:-shared-linux}" |
| 9 | RESOURCE_TAG="${RESOURCE_TAG:-shithub-actions-runner}" |
| 10 | NAME_PREFIX="${NAME_PREFIX:-}" |
| 11 | OUTPUT="${OUTPUT:-}" |
| 12 | ANSIBLE_USER="${ANSIBLE_USER:-root}" |
| 13 | SERVER_URL="${SHITHUB_RUNNER_SERVER_URL:-https://shithub.sh}" |
| 14 | LABELS="${SHITHUB_RUNNER_LABELS:-self-hosted,linux,ubuntu-latest,x64}" |
| 15 | CAPACITY="${SHITHUB_RUNNER_CAPACITY:-1}" |
| 16 | DEFAULT_IMAGE="${SHITHUB_RUNNER_DEFAULT_IMAGE:-ghcr.io/tenseleyflow/shithub/runner-nix:1.0}" |
| 17 | TOKEN_PLACEHOLDER="${SHITHUB_RUNNER_TOKEN_PLACEHOLDER:-REPLACE_WITH_RUNNER_TOKEN}" |
| 18 | |
| 19 | usage() { |
| 20 | cat <<'USAGE' |
| 21 | Usage: |
| 22 | deploy/doctl/generate-actions-runner-inventory.sh [flags] |
| 23 | |
| 24 | Flags: |
| 25 | --pool-name NAME Pool slug used in droplet names (default: shared-linux) |
| 26 | --resource-tag TAG DigitalOcean tag to read (default: shithub-actions-runner) |
| 27 | --name-prefix PREFIX Droplet name prefix (default: shithub-runner-<pool-name>-) |
| 28 | --output PATH Write inventory to PATH instead of stdout |
| 29 | --ansible-user USER SSH user for Ansible (default: root) |
| 30 | --server-url URL shithub server URL (default: https://shithub.sh) |
| 31 | --labels LIST Runner labels (default: self-hosted,linux,ubuntu-latest,x64) |
| 32 | --capacity N Runner capacity per host (default: 1) |
| 33 | --default-image IMAGE Runner default image |
| 34 | --token-placeholder S Placeholder text for per-host runner tokens |
| 35 | -h, --help Show this help |
| 36 | USAGE |
| 37 | } |
| 38 | |
| 39 | fatal() { |
| 40 | echo "fatal: $*" >&2 |
| 41 | exit 2 |
| 42 | } |
| 43 | |
| 44 | while [[ $# -gt 0 ]]; do |
| 45 | case "$1" in |
| 46 | --pool-name) |
| 47 | POOL_NAME="${2:?missing value for --pool-name}" |
| 48 | shift 2 |
| 49 | ;; |
| 50 | --resource-tag) |
| 51 | RESOURCE_TAG="${2:?missing value for --resource-tag}" |
| 52 | shift 2 |
| 53 | ;; |
| 54 | --name-prefix) |
| 55 | NAME_PREFIX="${2:?missing value for --name-prefix}" |
| 56 | shift 2 |
| 57 | ;; |
| 58 | --output) |
| 59 | OUTPUT="${2:?missing value for --output}" |
| 60 | shift 2 |
| 61 | ;; |
| 62 | --ansible-user) |
| 63 | ANSIBLE_USER="${2:?missing value for --ansible-user}" |
| 64 | shift 2 |
| 65 | ;; |
| 66 | --server-url) |
| 67 | SERVER_URL="${2:?missing value for --server-url}" |
| 68 | shift 2 |
| 69 | ;; |
| 70 | --labels) |
| 71 | LABELS="${2:?missing value for --labels}" |
| 72 | shift 2 |
| 73 | ;; |
| 74 | --capacity) |
| 75 | CAPACITY="${2:?missing value for --capacity}" |
| 76 | shift 2 |
| 77 | ;; |
| 78 | --default-image) |
| 79 | DEFAULT_IMAGE="${2:?missing value for --default-image}" |
| 80 | shift 2 |
| 81 | ;; |
| 82 | --token-placeholder) |
| 83 | TOKEN_PLACEHOLDER="${2:?missing value for --token-placeholder}" |
| 84 | shift 2 |
| 85 | ;; |
| 86 | -h | --help) |
| 87 | usage |
| 88 | exit 0 |
| 89 | ;; |
| 90 | *) |
| 91 | fatal "unknown flag: $1" |
| 92 | ;; |
| 93 | esac |
| 94 | done |
| 95 | |
| 96 | if [[ -z "$NAME_PREFIX" ]]; then |
| 97 | NAME_PREFIX="shithub-runner-$POOL_NAME-" |
| 98 | fi |
| 99 | |
| 100 | command -v doctl >/dev/null 2>&1 || fatal "doctl not on PATH" |
| 101 | command -v jq >/dev/null 2>&1 || fatal "jq not on PATH" |
| 102 | |
| 103 | if ! doctl account get >/dev/null 2>&1; then |
| 104 | fatal "doctl is not authenticated; run 'doctl auth init'" |
| 105 | fi |
| 106 | |
| 107 | droplets_json="$(doctl compute droplet list --tag-name "$RESOURCE_TAG" --output json | |
| 108 | jq --arg prefix "$NAME_PREFIX" '[.[] | select(.name | startswith($prefix)) | { |
| 109 | id: (.id | tostring), |
| 110 | name: .name, |
| 111 | status: .status, |
| 112 | public_ipv4: ((.networks.v4 // []) | map(select(.type == "public")) | first | .ip_address // ""), |
| 113 | private_ipv4: ((.networks.v4 // []) | map(select(.type == "private")) | first | .ip_address // "") |
| 114 | }] | sort_by(.name)')" |
| 115 | |
| 116 | count="$(jq 'length' <<<"$droplets_json")" |
| 117 | (( count > 0 )) || fatal "no droplets tagged $RESOURCE_TAG with prefix $NAME_PREFIX" |
| 118 | public_count="$(jq '[.[] | select(.public_ipv4 != "")] | length' <<<"$droplets_json")" |
| 119 | (( public_count > 0 )) || fatal "no matching droplets have public IPv4 addresses for direct Ansible SSH" |
| 120 | |
| 121 | render_inventory() { |
| 122 | cat <<HEADER |
| 123 | # Generated by deploy/doctl/generate-actions-runner-inventory.sh. |
| 124 | # Store the real shithub_runner_token values in ansible-vault or host_vars. |
| 125 | |
| 126 | [actions_runners] |
| 127 | HEADER |
| 128 | jq -r \ |
| 129 | --arg user "$ANSIBLE_USER" \ |
| 130 | --arg token "$TOKEN_PLACEHOLDER" \ |
| 131 | '.[] | select(.public_ipv4 != "") | |
| 132 | "\(.name) ansible_host=\(.public_ipv4) ansible_user=\($user) do_droplet_id=\(.id) do_private_ipv4=\(.private_ipv4) shithub_runner_token=\($token)_\(.name | ascii_upcase | gsub("[^A-Z0-9]"; "_"))"' \ |
| 133 | <<<"$droplets_json" |
| 134 | |
| 135 | cat <<VARS |
| 136 | |
| 137 | [actions_runners:vars] |
| 138 | shithub_runner_enabled=true |
| 139 | shithub_runner_server_url=$SERVER_URL |
| 140 | shithub_runner_engine=docker |
| 141 | shithub_runner_labels=$LABELS |
| 142 | shithub_runner_capacity=$CAPACITY |
| 143 | shithub_runner_default_image=$DEFAULT_IMAGE |
| 144 | VARS |
| 145 | } |
| 146 | |
| 147 | if [[ -n "$OUTPUT" ]]; then |
| 148 | mkdir -p "$(dirname "$OUTPUT")" |
| 149 | tmp="$OUTPUT.tmp" |
| 150 | render_inventory >"$tmp" |
| 151 | mv "$tmp" "$OUTPUT" |
| 152 | echo "wrote $OUTPUT" >&2 |
| 153 | else |
| 154 | render_inventory |
| 155 | fi |