From 0def991d6f009fe1d6a9d805aea075ad7a7288de Mon Sep 17 00:00:00 2001 From: Amedeo Palopoli Date: Wed, 29 Jul 2026 15:12:50 +0200 Subject: [PATCH 1/9] feat: migrate to acloud-cli v1.0.0 - Replace --client-secret flag with ACLOUD_CLIENT_SECRET env var - Rename *_uri inputs to *_id (vpc, subnet, security_group, keypair) - Replace --boot-disk-uri with --boot-disk-id; remove URI polling step - Replace --vpc-uri/--subnet-uri/--security-group-uri/--keypair-uri with --vpc-id/--subnet-id/--security-group-id/--keypair-id - Make keypair_id optional (keypair-id is optional in acloud-cli v1) - Switch status polling from grep/awk to --verbose + jq - Update integration test secrets ACLOUD_*_URI -> ACLOUD_*_ID - Replace --output json with --verbose in verify step - Update CHANGELOG, README, CLAUDE.md, examples --- .github/workflows/integration-test.yml | 15 ++++--- CHANGELOG.md | 9 ++++- CLAUDE.md | 14 +++---- README.md | 16 ++++---- action.sh | 56 +++++++++++--------------- action.yml | 24 +++++------ examples/basic.yml | 8 ++-- 7 files changed, 69 insertions(+), 73 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8e88045..09ba734 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -43,10 +43,10 @@ jobs: acloud_client_id: ${{ secrets.ACLOUD_CLIENT_ID }} acloud_client_secret: ${{ secrets.ACLOUD_CLIENT_SECRET }} acloud_project_id: ${{ secrets.ACLOUD_PROJECT_ID }} - vpc_uri: ${{ secrets.ACLOUD_VPC_URI }} - subnet_uri: ${{ secrets.ACLOUD_SUBNET_URI }} - security_group_uri: ${{ secrets.ACLOUD_SECURITY_GROUP_URI }} - keypair_uri: ${{ secrets.ACLOUD_KEYPAIR_URI }} + vpc_id: ${{ secrets.ACLOUD_VPC_ID }} + subnet_id: ${{ secrets.ACLOUD_SUBNET_ID }} + security_group_id: ${{ secrets.ACLOUD_SECURITY_GROUP_ID }} + keypair_id: ${{ secrets.ACLOUD_KEYPAIR_ID }} name: test-${{ github.run_id }}-${{ github.run_attempt }} flavor: ${{ inputs.flavor }} image: ${{ inputs.image }} @@ -123,13 +123,12 @@ jobs: ACLOUD_PROJECT_ID: ${{ needs.start-runner.outputs.project_id }} SERVER_ID: ${{ needs.start-runner.outputs.server_id }} run: | - acloud config set \ - --client-id "$ACLOUD_CLIENT_ID" \ - --client-secret "$ACLOUD_CLIENT_SECRET" + ACLOUD_CLIENT_SECRET="$ACLOUD_CLIENT_SECRET" \ + acloud config set --client-id "$ACLOUD_CLIENT_ID" acloud context set default --project-id "$ACLOUD_PROJECT_ID" STATUS=$(acloud compute cloudserver get "$SERVER_ID" \ - --output json 2>/dev/null \ + --verbose 2>/dev/null \ | jq -r '.status // empty' || echo "not-found") echo "Post-delete server status: '${STATUS}'" diff --git a/CHANGELOG.md b/CHANGELOG.md index c665de3..96af8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm --- -## [Unreleased] +## [1.0.0] - 2026-07-29 ### Added - `action.yml`: composite GitHub Action with full input/output definitions; auto-installs `acloud-cli` @@ -17,3 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `README.md`: quickstart workflow, input/output reference, flavor table, OS image table, regions, troubleshooting - CI: `shellcheck` linting workflow and Dependabot for Actions version updates - Integration test workflow (`workflow_dispatch`) + +### Breaking changes (requires acloud-cli v1.0.0+) +- Inputs `vpc_uri`, `subnet_uri`, `security_group_uri`, `keypair_uri` renamed to `vpc_id`, `subnet_id`, `security_group_id`, `keypair_id` to match the new acloud-cli flag names +- `keypair_id` is now optional (was required in the previous release) +- Authentication: `--client-secret` flag removed from `acloud config set`; pass the secret via the `ACLOUD_CLIENT_SECRET` environment variable instead (handled internally — no action input change required) +- `acloud-cli` config is now stored at `~/.config/acloud/config.yaml` (XDG Base Directory) +- GitHub Actions secrets for the integration test workflow renamed: `ACLOUD_VPC_URI→ACLOUD_VPC_ID`, `ACLOUD_SUBNET_URI→ACLOUD_SUBNET_ID`, `ACLOUD_SECURITY_GROUP_URI→ACLOUD_SECURITY_GROUP_ID`, `ACLOUD_KEYPAIR_URI→ACLOUD_KEYPAIR_ID` diff --git a/CLAUDE.md b/CLAUDE.md index 30baac6..0056721 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,10 +16,10 @@ On-demand self-hosted GitHub Actions runners on Aruba Cloud. Each workflow run p ### Authentication (CI/CD) ```sh -acloud config set --client-id "$ACLOUD_CLIENT_ID" --client-secret "$ACLOUD_CLIENT_SECRET" +ACLOUD_CLIENT_SECRET="$ACLOUD_CLIENT_SECRET" acloud config set --client-id "$ACLOUD_CLIENT_ID" acloud context set default --project-id "$ACLOUD_PROJECT_ID" ``` -Credentials are stored in `~/.acloud.yaml` (permissions `0600`). +Credentials are stored in `~/.config/acloud/config.yaml` (XDG Base Directory, permissions `0600`). ### Key server commands ```sh @@ -29,11 +29,11 @@ acloud compute cloudserver create \ --region "ITBG-Bergamo" \ --zone "ITBG-1" \ --flavor "CSO4A8" \ - --image "ubuntu-22.04" \ - --vpc-uri "" \ - --subnet-uri "" \ - --security-group-uri "" \ - --keypair-uri "" \ + --boot-disk-id "" \ + --vpc-id "" \ + --subnet-id "" \ + --security-group-id "" \ + --keypair-id "" \ --user-data-file cloud-init.yml acloud compute cloudserver list diff --git a/README.md b/README.md index 4448e1b..e7a835a 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,10 @@ jobs: acloud_client_id: ${{ secrets.ACLOUD_CLIENT_ID }} acloud_client_secret: ${{ secrets.ACLOUD_CLIENT_SECRET }} acloud_project_id: ${{ secrets.ACLOUD_PROJECT_ID }} - vpc_uri: ${{ secrets.ACLOUD_VPC_URI }} - subnet_uri: ${{ secrets.ACLOUD_SUBNET_URI }} - security_group_uri: ${{ secrets.ACLOUD_SECURITY_GROUP_URI }} - keypair_uri: ${{ secrets.ACLOUD_KEYPAIR_URI }} + vpc_id: ${{ secrets.ACLOUD_VPC_ID }} + subnet_id: ${{ secrets.ACLOUD_SUBNET_ID }} + security_group_id: ${{ secrets.ACLOUD_SECURITY_GROUP_ID }} + keypair_id: ${{ secrets.ACLOUD_KEYPAIR_ID }} flavor: CSO2A4 image: LU22-001 @@ -120,10 +120,10 @@ jobs: | `boot_disk_type` | no | `Performance` | Boot disk type (`Performance` or `Archive`) | | `boot_disk_wait` | no | `30` | Max polling attempts for boot disk `NotUsed` status (×10 s) | | `boot_disk_id` | yes (delete) | — | Boot disk ID returned by the create step | -| `vpc_uri` | yes (create) | — | VPC resource URI | -| `subnet_uri` | yes (create) | — | Subnet resource URI | -| `security_group_uri` | yes (create) | — | Security group resource URI | -| `keypair_uri` | yes (create) | — | SSH key pair resource URI | +| `vpc_id` | yes (create) | — | VPC ID | +| `subnet_id` | yes (create) | — | Subnet ID | +| `security_group_id` | yes (create) | — | Security group ID | +| `keypair_id` | no (create) | — | SSH key pair ID | | `runner_labels` | no | `self-hosted,linux,acloud` | Extra runner labels (comma-separated) | | `runner_version` | no | `latest` | GitHub Actions Runner version | | `runner_dir` | no | `/actions-runner` | Runner installation path on server | diff --git a/action.sh b/action.sh index d1e499a..e9d07e1 100644 --- a/action.sh +++ b/action.sh @@ -87,10 +87,10 @@ MY_IMAGE=${INPUT_IMAGE:-"LU22-001"} [[ "$MY_IMAGE" =~ ^[a-zA-Z0-9._-]{1,63}$ ]] || \ exit_with_failure "'$MY_IMAGE' is not a valid image name." -MY_VPC_URI=${INPUT_VPC_URI:-""} -MY_SUBNET_URI=${INPUT_SUBNET_URI:-""} -MY_SECURITY_GROUP_URI=${INPUT_SECURITY_GROUP_URI:-""} -MY_KEYPAIR_URI=${INPUT_KEYPAIR_URI:-""} +MY_VPC_ID=${INPUT_VPC_ID:-""} +MY_SUBNET_ID=${INPUT_SUBNET_ID:-""} +MY_SECURITY_GROUP_ID=${INPUT_SECURITY_GROUP_ID:-""} +MY_KEYPAIR_ID=${INPUT_KEYPAIR_ID:-""} MY_BOOT_DISK_SIZE=${INPUT_BOOT_DISK_SIZE:-20} [[ "$MY_BOOT_DISK_SIZE" =~ ^[0-9]+$ ]] || exit_with_failure "boot_disk_size must be an integer." @@ -122,9 +122,8 @@ MY_BOOT_DISK_ID=${INPUT_BOOT_DISK_ID:-""} # ─── acloud-cli authentication ──────────────────────────────────────────────── echo "Configuring acloud-cli..." -acloud config set \ - --client-id "$MY_ACLOUD_CLIENT_ID" \ - --client-secret "$MY_ACLOUD_CLIENT_SECRET" +ACLOUD_CLIENT_SECRET="$MY_ACLOUD_CLIENT_SECRET" \ + acloud config set --client-id "$MY_ACLOUD_CLIENT_ID" acloud context set default --project-id "$MY_ACLOUD_PROJECT_ID" # ─── DELETE ─────────────────────────────────────────────────────────────────── @@ -191,10 +190,9 @@ fi # ─── CREATE ─────────────────────────────────────────────────────────────────── -[[ -n "$MY_VPC_URI" ]] || exit_with_failure "vpc_uri is required for create mode." -[[ -n "$MY_SUBNET_URI" ]] || exit_with_failure "subnet_uri is required for create mode." -[[ -n "$MY_SECURITY_GROUP_URI" ]] || exit_with_failure "security_group_uri is required for create mode." -[[ -n "$MY_KEYPAIR_URI" ]] || exit_with_failure "keypair_uri is required for create mode." +[[ -n "$MY_VPC_ID" ]] || exit_with_failure "vpc_id is required for create mode." +[[ -n "$MY_SUBNET_ID" ]] || exit_with_failure "subnet_id is required for create mode." +[[ -n "$MY_SECURITY_GROUP_ID" ]] || exit_with_failure "security_group_id is required for create mode." # Get a GitHub runner registration token echo "Requesting GitHub runner registration token..." @@ -268,10 +266,9 @@ echo "Boot disk created (ID: $MY_BOOT_DISK_ID). Waiting for 'NotUsed' status..." MY_BOOT_DISK_STATUS="" RETRY_COUNT=0 while [[ $RETRY_COUNT -lt $MY_BOOT_DISK_WAIT ]]; do - acloud storage blockstorage get "$MY_BOOT_DISK_ID" --project-id "$MY_ACLOUD_PROJECT_ID" \ - > boot-disk-status.txt 2>/dev/null || true - - MY_BOOT_DISK_STATUS=$(grep -E '^Status:' boot-disk-status.txt | awk '{print $NF}' || true) + MY_BOOT_DISK_STATUS=$(acloud storage blockstorage get "$MY_BOOT_DISK_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" --verbose 2>/dev/null \ + | jq -r '.status // empty' || true) if [[ "$MY_BOOT_DISK_STATUS" == "NotUsed" ]]; then echo "Boot disk is ready (NotUsed)." @@ -286,11 +283,6 @@ done [[ "$MY_BOOT_DISK_STATUS" == "NotUsed" ]] || \ exit_with_failure "Boot disk did not reach 'NotUsed' in time. Check the Aruba Cloud console." -# Get the boot disk URI (required to attach it to the cloudserver) -MY_BOOT_DISK_URI=$(grep -E '^URI:' boot-disk-status.txt | awk '{print $NF}') -[[ -n "$MY_BOOT_DISK_URI" ]] || exit_with_failure "Could not parse boot disk URI from status response." -echo "Boot disk URI: $MY_BOOT_DISK_URI" - # ── Step 3: Create the cloudserver ─────────────────────────────────────────── SERVER_CREATE_MAX_ATTEMPTS=3 @@ -298,17 +290,19 @@ SERVER_CREATE_RETRY_WAIT=15 _run_cloudserver_create() { local extra_flags=("$@") + local keypair_flag=() + [[ -n "$MY_KEYPAIR_ID" ]] && keypair_flag=(--keypair-id "$MY_KEYPAIR_ID") acloud compute cloudserver create \ "${extra_flags[@]}" \ --name "$MY_NAME" \ --region "$MY_REGION" \ --zone "$MY_ZONE" \ --flavor "$MY_FLAVOR" \ - --boot-disk-uri "$MY_BOOT_DISK_URI" \ - --vpc-uri "$MY_VPC_URI" \ - --subnet-uri "$MY_SUBNET_URI" \ - --security-group-uri "$MY_SECURITY_GROUP_URI" \ - --keypair-uri "$MY_KEYPAIR_URI" \ + --boot-disk-id "$MY_BOOT_DISK_ID" \ + --vpc-id "$MY_VPC_ID" \ + --subnet-id "$MY_SUBNET_ID" \ + --security-group-id "$MY_SECURITY_GROUP_ID" \ + "${keypair_flag[@]}" \ --user-data-file cloud-init.yml \ --project-id "$MY_ACLOUD_PROJECT_ID" } @@ -325,9 +319,6 @@ while true; do _server_create_err=$(cat server-create.txt) echo >&2 "Attempt ${_server_create_attempt} failed: ${_server_create_err}" - # On any failure, print debug output to aid diagnosis - echo >&2 "--- Debug output for failed attempt ${_server_create_attempt} ---" - _run_cloudserver_create -d || true if echo "$_server_create_err" | grep -qE 'status 5[0-9]{2}'; then if [[ $_server_create_attempt -lt $SERVER_CREATE_MAX_ATTEMPTS ]]; then echo "Transient server error — retrying in ${SERVER_CREATE_RETRY_WAIT}s..." @@ -341,7 +332,7 @@ done cat server-create.txt -MY_ACLOUD_SERVER_ID=$(awk 'NR==2 {print $1}' server-create.txt) +MY_ACLOUD_SERVER_ID=$(grep -E '^ID:' server-create.txt | awk '{print $NF}') [[ -n "$MY_ACLOUD_SERVER_ID" ]] || exit_with_failure "Could not parse server ID from create response." _CREATED_SERVER_ID="$MY_ACLOUD_SERVER_ID" # arm cleanup trap @@ -363,10 +354,9 @@ echo "Waiting for server to become Active..." MY_SERVER_STATUS="" RETRY_COUNT=0 while [[ $RETRY_COUNT -lt $MY_SERVER_WAIT ]]; do - acloud compute cloudserver get "$MY_ACLOUD_SERVER_ID" --project-id "$MY_ACLOUD_PROJECT_ID" \ - > server-status.txt 2>/dev/null || true - - MY_SERVER_STATUS=$(grep -E '^Status:' server-status.txt | awk '{print $NF}' || true) + MY_SERVER_STATUS=$(acloud compute cloudserver get "$MY_ACLOUD_SERVER_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" --verbose 2>/dev/null \ + | jq -r '.status // empty' || true) if [[ "$MY_SERVER_STATUS" == "Active" ]]; then echo "Server is Active." diff --git a/action.yml b/action.yml index 3b1e6e3..fd1615a 100644 --- a/action.yml +++ b/action.yml @@ -85,20 +85,20 @@ inputs: was created by the create step. required: false - vpc_uri: - description: Resource URI of the VPC to attach the server to. + vpc_id: + description: ID of the VPC to attach the server to. required: false - subnet_uri: - description: Resource URI of the subnet inside the VPC. + subnet_id: + description: ID of the subnet inside the VPC. required: false - security_group_uri: - description: Resource URI of the security group to apply. + security_group_id: + description: ID of the security group to apply. required: false - keypair_uri: - description: Resource URI of the SSH key pair to inject into the server. + keypair_id: + description: ID of the SSH key pair to inject into the server. required: false runner_labels: @@ -210,10 +210,10 @@ runs: INPUT_ZONE: ${{ inputs.zone }} INPUT_FLAVOR: ${{ inputs.flavor }} INPUT_IMAGE: ${{ inputs.image }} - INPUT_VPC_URI: ${{ inputs.vpc_uri }} - INPUT_SUBNET_URI: ${{ inputs.subnet_uri }} - INPUT_SECURITY_GROUP_URI: ${{ inputs.security_group_uri }} - INPUT_KEYPAIR_URI: ${{ inputs.keypair_uri }} + INPUT_VPC_ID: ${{ inputs.vpc_id }} + INPUT_SUBNET_ID: ${{ inputs.subnet_id }} + INPUT_SECURITY_GROUP_ID: ${{ inputs.security_group_id }} + INPUT_KEYPAIR_ID: ${{ inputs.keypair_id }} INPUT_RUNNER_LABELS: ${{ inputs.runner_labels }} INPUT_RUNNER_VERSION: ${{ inputs.runner_version }} INPUT_RUNNER_DIR: ${{ inputs.runner_dir }} diff --git a/examples/basic.yml b/examples/basic.yml index 59f3161..cd4fbd2 100644 --- a/examples/basic.yml +++ b/examples/basic.yml @@ -23,10 +23,10 @@ jobs: acloud_client_id: ${{ secrets.ACLOUD_CLIENT_ID }} acloud_client_secret: ${{ secrets.ACLOUD_CLIENT_SECRET }} acloud_project_id: ${{ secrets.ACLOUD_PROJECT_ID }} - vpc_uri: ${{ secrets.ACLOUD_VPC_URI }} - subnet_uri: ${{ secrets.ACLOUD_SUBNET_URI }} - security_group_uri: ${{ secrets.ACLOUD_SECURITY_GROUP_URI }} - keypair_uri: ${{ secrets.ACLOUD_KEYPAIR_URI }} + vpc_id: ${{ secrets.ACLOUD_VPC_ID }} + subnet_id: ${{ secrets.ACLOUD_SUBNET_ID }} + security_group_id: ${{ secrets.ACLOUD_SECURITY_GROUP_ID }} + keypair_id: ${{ secrets.ACLOUD_KEYPAIR_ID }} flavor: CSO2A4 # 2 vCPU / 4 GB RAM image: LU22-001 # Ubuntu 22.04 LTS From f8541fa600e045cdbcf2451deb5b042221075c30 Mon Sep 17 00:00:00 2001 From: Amedeo Palopoli Date: Wed, 29 Jul 2026 15:23:55 +0200 Subject: [PATCH 2/9] feat: auto-provision VPC, subnet and security group when not provided When vpc_id, subnet_id or security_group_id are omitted the action now creates the missing resources automatically: - VPC: acloud network vpc create - Subnet: acloud network subnet create (attached to the VPC) - Security group: acloud network securitygroup create + egress allow-all rule New outputs auto_vpc_id, auto_subnet_id, auto_security_group_id are set only when the action created the resource. Pass them back to the delete step to have them cleaned up together with the server. The failure-path cleanup trap is extended to delete auto-created network resources in reverse order (server -> disk -> sg -> subnet -> vpc). --- action.sh | 211 +++++++++++++++++++++++++++++++++++++++++------------ action.yml | 59 +++++++++++++-- 2 files changed, 219 insertions(+), 51 deletions(-) diff --git a/action.sh b/action.sh index e9d07e1..fe9bf78 100644 --- a/action.sh +++ b/action.sh @@ -2,16 +2,20 @@ set -euo pipefail -# Track created resources for cleanup on failure. -_CREATED_BOOT_DISK_ID="" +# Track resources created in this run for failure-path cleanup. +# Populated as each resource is created; cleared on success. _CREATED_SERVER_ID="" +_CREATED_BOOT_DISK_ID="" +_CREATED_SECURITY_GROUP_ID="" +_CREATED_SUBNET_ID="" +_CREATED_VPC_ID="" function exit_with_failure() { echo >&2 "FAILURE: $1" exit 1 } -# On any non-zero exit, delete resources that were created in this run. +# On any non-zero exit during CREATE, delete resources in reverse creation order. function _cleanup_on_exit() { local code=$? [[ $code -eq 0 ]] && return @@ -25,7 +29,6 @@ function _cleanup_on_exit() { --yes 2>/dev/null \ && echo >&2 "Server '$_CREATED_SERVER_ID' deleted." \ || echo >&2 "Warning: could not auto-delete server '$_CREATED_SERVER_ID'. Remove it manually." - # Wait briefly for the server to release the boot disk before deleting it sleep 10 fi @@ -36,6 +39,36 @@ function _cleanup_on_exit() { --yes 2>/dev/null \ && echo >&2 "Boot disk '$_CREATED_BOOT_DISK_ID' deleted." \ || echo >&2 "Warning: could not auto-delete boot disk '$_CREATED_BOOT_DISK_ID'. Remove it manually." + sleep 5 + fi + + if [[ -n "$_CREATED_SECURITY_GROUP_ID" ]]; then + echo >&2 "Deleting orphan security group '$_CREATED_SECURITY_GROUP_ID'..." + acloud network securitygroup delete "$_CREATED_SECURITY_GROUP_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes 2>/dev/null \ + && echo >&2 "Security group deleted." \ + || echo >&2 "Warning: could not auto-delete security group '$_CREATED_SECURITY_GROUP_ID'. Remove it manually." + sleep 5 + fi + + if [[ -n "$_CREATED_SUBNET_ID" ]]; then + echo >&2 "Deleting orphan subnet '$_CREATED_SUBNET_ID'..." + acloud network subnet delete "$_CREATED_SUBNET_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes 2>/dev/null \ + && echo >&2 "Subnet deleted." \ + || echo >&2 "Warning: could not auto-delete subnet '$_CREATED_SUBNET_ID'. Remove it manually." + sleep 10 + fi + + if [[ -n "$_CREATED_VPC_ID" ]]; then + echo >&2 "Deleting orphan VPC '$_CREATED_VPC_ID'..." + acloud network vpc delete "$_CREATED_VPC_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes 2>/dev/null \ + && echo >&2 "VPC deleted." \ + || echo >&2 "Warning: could not auto-delete VPC '$_CREATED_VPC_ID'. Remove it manually." fi } trap _cleanup_on_exit EXIT @@ -87,6 +120,7 @@ MY_IMAGE=${INPUT_IMAGE:-"LU22-001"} [[ "$MY_IMAGE" =~ ^[a-zA-Z0-9._-]{1,63}$ ]] || \ exit_with_failure "'$MY_IMAGE' is not a valid image name." +# Optional — auto-created when empty in create mode. MY_VPC_ID=${INPUT_VPC_ID:-""} MY_SUBNET_ID=${INPUT_SUBNET_ID:-""} MY_SECURITY_GROUP_ID=${INPUT_SECURITY_GROUP_ID:-""} @@ -116,9 +150,16 @@ MY_RUNNER_WAIT=${INPUT_RUNNER_WAIT:-60} MY_SERVER_WAIT=${INPUT_SERVER_WAIT:-30} [[ "$MY_SERVER_WAIT" =~ ^[0-9]+$ ]] || exit_with_failure "server_wait must be an integer." +# Delete-mode inputs. MY_SERVER_ID=${INPUT_SERVER_ID:-""} MY_BOOT_DISK_ID=${INPUT_BOOT_DISK_ID:-""} +# IDs of resources that were auto-created by a previous create step. +# Only populated on delete mode; used to clean up managed resources. +MY_AUTO_VPC_ID=${INPUT_AUTO_VPC_ID:-""} +MY_AUTO_SUBNET_ID=${INPUT_AUTO_SUBNET_ID:-""} +MY_AUTO_SECURITY_GROUP_ID=${INPUT_AUTO_SECURITY_GROUP_ID:-""} + # ─── acloud-cli authentication ──────────────────────────────────────────────── echo "Configuring acloud-cli..." @@ -136,7 +177,7 @@ if [[ "$MY_MODE" == "delete" ]]; then --yes \ && echo "Server deleted." \ || echo "Warning: could not delete server '$MY_SERVER_ID' (may already be gone)." - # Wait for the server to release the boot disk before deleting it + # Wait for the server to release the boot disk before deleting it. sleep 10 else echo "No server_id provided — skipping server deletion." @@ -149,10 +190,40 @@ if [[ "$MY_MODE" == "delete" ]]; then --yes \ && echo "Boot disk deleted." \ || echo "Warning: could not delete boot disk '$MY_BOOT_DISK_ID' (may already be gone)." + sleep 5 + fi + + # Delete auto-provisioned network resources in reverse creation order. + if [[ -n "$MY_AUTO_SECURITY_GROUP_ID" ]]; then + echo "Deleting auto-created security group '$MY_AUTO_SECURITY_GROUP_ID'..." + acloud network securitygroup delete "$MY_AUTO_SECURITY_GROUP_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes \ + && echo "Security group deleted." \ + || echo "Warning: could not delete security group '$MY_AUTO_SECURITY_GROUP_ID'." + sleep 5 + fi + + if [[ -n "$MY_AUTO_SUBNET_ID" ]]; then + echo "Deleting auto-created subnet '$MY_AUTO_SUBNET_ID'..." + acloud network subnet delete "$MY_AUTO_SUBNET_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes \ + && echo "Subnet deleted." \ + || echo "Warning: could not delete subnet '$MY_AUTO_SUBNET_ID'." + sleep 10 fi - # Best-effort cleanup of the GitHub runner entry. Ephemeral runners - # self-deregister after a job, but may still appear if the job never ran. + if [[ -n "$MY_AUTO_VPC_ID" ]]; then + echo "Deleting auto-created VPC '$MY_AUTO_VPC_ID'..." + acloud network vpc delete "$MY_AUTO_VPC_ID" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + --yes \ + && echo "VPC deleted." \ + || echo "Warning: could not delete VPC '$MY_AUTO_VPC_ID'." + fi + + # Best-effort cleanup of the GitHub runner entry. echo "Looking up GitHub Actions Runner '$MY_NAME'..." if curl -fsSL \ -o github-runners.json \ @@ -180,21 +251,79 @@ if [[ "$MY_MODE" == "delete" ]]; then fi echo "Cleanup complete." - if [[ -n "$MY_SERVER_ID" || -n "$MY_BOOT_DISK_ID" ]]; then - echo "Aruba Cloud resources deleted successfully. 🗑️" >> "$GITHUB_STEP_SUMMARY" - else - echo "No Aruba Cloud resources to delete." >> "$GITHUB_STEP_SUMMARY" - fi + echo "Aruba Cloud resources deleted. 🗑️" >> "$GITHUB_STEP_SUMMARY" exit 0 fi # ─── CREATE ─────────────────────────────────────────────────────────────────── -[[ -n "$MY_VPC_ID" ]] || exit_with_failure "vpc_id is required for create mode." -[[ -n "$MY_SUBNET_ID" ]] || exit_with_failure "subnet_id is required for create mode." -[[ -n "$MY_SECURITY_GROUP_ID" ]] || exit_with_failure "security_group_id is required for create mode." +# ── Step 1: Auto-provision network resources if not provided ────────────────── + +if [[ -z "$MY_VPC_ID" ]]; then + echo "No vpc_id provided — creating VPC '${MY_NAME}-vpc'..." + acloud network vpc create \ + --name "${MY_NAME}-vpc" \ + --region "$MY_REGION" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + > vpc-create.txt \ + || exit_with_failure "Failed to create VPC." + cat vpc-create.txt + MY_VPC_ID=$(grep -E '^ID:' vpc-create.txt | awk '{print $NF}') + [[ -n "$MY_VPC_ID" ]] || exit_with_failure "Could not parse VPC ID from create response." + _CREATED_VPC_ID="$MY_VPC_ID" + echo "VPC created (ID: $MY_VPC_ID)." +fi + +if [[ -z "$MY_SUBNET_ID" ]]; then + echo "No subnet_id provided — creating subnet '${MY_NAME}-subnet'..." + acloud network subnet create "$MY_VPC_ID" \ + --name "${MY_NAME}-subnet" \ + --region "$MY_REGION" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + > subnet-create.txt \ + || exit_with_failure "Failed to create subnet." + cat subnet-create.txt + MY_SUBNET_ID=$(awk 'NR==2 {print $2}' subnet-create.txt) + [[ -n "$MY_SUBNET_ID" ]] || exit_with_failure "Could not parse subnet ID from create response." + _CREATED_SUBNET_ID="$MY_SUBNET_ID" + echo "Subnet created (ID: $MY_SUBNET_ID)." +fi + +if [[ -z "$MY_SECURITY_GROUP_ID" ]]; then + echo "No security_group_id provided — creating security group '${MY_NAME}-sg'..." + acloud network securitygroup create \ + --name "${MY_NAME}-sg" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + > sg-create.txt \ + || exit_with_failure "Failed to create security group." + cat sg-create.txt + MY_SECURITY_GROUP_ID=$(grep -E '^ID:' sg-create.txt | awk '{print $NF}') + [[ -n "$MY_SECURITY_GROUP_ID" ]] || exit_with_failure "Could not parse security group ID from create response." + _CREATED_SECURITY_GROUP_ID="$MY_SECURITY_GROUP_ID" + echo "Security group created (ID: $MY_SECURITY_GROUP_ID)." + + echo "Adding egress rule (allow all outbound)..." + acloud network securityrule create "$MY_VPC_ID" "$MY_SECURITY_GROUP_ID" \ + --name "allow-all-egress" \ + --region "$MY_REGION" \ + --direction Egress \ + --protocol ANY \ + --target-kind Ip \ + --target-value "0.0.0.0/0" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ + || exit_with_failure "Failed to add egress rule to security group." +fi + +# Emit auto-created IDs early so the delete step can clean up even if +# subsequent steps fail and the job is cancelled. +{ + echo "auto_vpc_id=${_CREATED_VPC_ID}" + echo "auto_subnet_id=${_CREATED_SUBNET_ID}" + echo "auto_security_group_id=${_CREATED_SECURITY_GROUP_ID}" +} >> "$GITHUB_OUTPUT" + +# ── Step 2: Get GitHub runner registration token ────────────────────────────── -# Get a GitHub runner registration token echo "Requesting GitHub runner registration token..." curl -fsSL \ -X POST \ @@ -207,7 +336,7 @@ curl -fsSL \ MY_GITHUB_RUNNER_REGISTRATION_TOKEN=$(jq -er '.token' < registration-token.json) -# Base64-encode embedded scripts for cloud-init +# Base64-encode embedded scripts for cloud-init. if [[ "$OSTYPE" == "darwin"* || "$OSTYPE" == "freebsd"* ]]; then MY_RUNNER_INSTALL_SH_BASE64=$(base64 < runner-install.sh) MY_PRE_RUNNER_SCRIPT_BASE64=$(printf '%s' "$MY_PRE_RUNNER_SCRIPT" | base64) @@ -216,7 +345,7 @@ else MY_PRE_RUNNER_SCRIPT_BASE64=$(printf '%s' "$MY_PRE_RUNNER_SCRIPT" | base64 --wrap=0) fi -# Render the cloud-init template +# Render the cloud-init template. export MY_GITHUB_REPOSITORY export MY_GITHUB_RUNNER_REGISTRATION_TOKEN export MY_RUNNER_INSTALL_SH_BASE64 @@ -228,21 +357,21 @@ export MY_RUNNER_LABELS envsubst < cloud-init.yml.tpl > cloud-init.yml -# ── Step 1: Create boot disk ────────────────────────────────────────────────── -# In Aruba Cloud, a cloudserver requires a dedicated block storage as its boot -# disk. The block storage must reach "NotUsed" status before it can be attached. +# ── Step 3: Create boot disk ────────────────────────────────────────────────── +# A cloudserver requires a dedicated bootable block-storage volume. +# The volume must reach "NotUsed" status before it can be attached. echo "Creating boot disk '${MY_NAME}-boot' from image '$MY_IMAGE'..." acloud storage blockstorage create \ - --name "${MY_NAME}-boot" \ - --region "$MY_REGION" \ - --zone "$MY_ZONE" \ + --name "${MY_NAME}-boot" \ + --region "$MY_REGION" \ + --zone "$MY_ZONE" \ --set-bootable \ --billing-period Hour \ - --size "$MY_BOOT_DISK_SIZE" \ - --type "$MY_BOOT_DISK_TYPE" \ - --image "$MY_IMAGE" \ - --project-id "$MY_ACLOUD_PROJECT_ID" \ + --size "$MY_BOOT_DISK_SIZE" \ + --type "$MY_BOOT_DISK_TYPE" \ + --image "$MY_IMAGE" \ + --project-id "$MY_ACLOUD_PROJECT_ID" \ > boot-disk-create.txt \ || exit_with_failure "Failed to create boot disk." @@ -250,10 +379,10 @@ cat boot-disk-create.txt MY_BOOT_DISK_ID=$(grep -E '^ID:' boot-disk-create.txt | awk '{print $NF}') [[ -n "$MY_BOOT_DISK_ID" ]] || exit_with_failure "Could not parse boot disk ID from create response." -_CREATED_BOOT_DISK_ID="$MY_BOOT_DISK_ID" # arm cleanup trap — must be set before any subsequent exit +_CREATED_BOOT_DISK_ID="$MY_BOOT_DISK_ID" -# Write boot_disk_id and project_id to GITHUB_OUTPUT immediately so stop-runner -# can delete the disk even if subsequent steps fail. +# Write IDs to GITHUB_OUTPUT immediately so the delete step can clean up +# even if subsequent steps fail. { echo "boot_disk_id=$MY_BOOT_DISK_ID" echo "project_id=$MY_ACLOUD_PROJECT_ID" @@ -261,7 +390,7 @@ _CREATED_BOOT_DISK_ID="$MY_BOOT_DISK_ID" # arm cleanup trap — must be set bef echo "Boot disk created (ID: $MY_BOOT_DISK_ID). Waiting for 'NotUsed' status..." -# ── Step 2: Poll boot disk until NotUsed ───────────────────────────────────── +# ── Step 4: Poll boot disk until NotUsed ───────────────────────────────────── MY_BOOT_DISK_STATUS="" RETRY_COUNT=0 @@ -283,7 +412,7 @@ done [[ "$MY_BOOT_DISK_STATUS" == "NotUsed" ]] || \ exit_with_failure "Boot disk did not reach 'NotUsed' in time. Check the Aruba Cloud console." -# ── Step 3: Create the cloudserver ─────────────────────────────────────────── +# ── Step 5: Create the cloudserver ─────────────────────────────────────────── SERVER_CREATE_MAX_ATTEMPTS=3 SERVER_CREATE_RETRY_WAIT=15 @@ -334,21 +463,13 @@ cat server-create.txt MY_ACLOUD_SERVER_ID=$(grep -E '^ID:' server-create.txt | awk '{print $NF}') [[ -n "$MY_ACLOUD_SERVER_ID" ]] || exit_with_failure "Could not parse server ID from create response." -_CREATED_SERVER_ID="$MY_ACLOUD_SERVER_ID" # arm cleanup trap +_CREATED_SERVER_ID="$MY_ACLOUD_SERVER_ID" -# Write server_id to GITHUB_OUTPUT immediately so stop-runner can delete it -# even if the runner polling timeout or any subsequent step fails. echo "server_id=$MY_ACLOUD_SERVER_ID" >> "$GITHUB_OUTPUT" - echo "Server created (ID: $MY_ACLOUD_SERVER_ID)." +echo "label=$MY_NAME" >> "$GITHUB_OUTPUT" -# Write remaining outputs (label already known from the start). -{ - echo "label=$MY_NAME" -} >> "$GITHUB_OUTPUT" - -# ── Step 4: Poll server until Active ───────────────────────────────────────── -# Aruba Cloud resources must be "Active" before they can be used. +# ── Step 6: Poll server until Active ───────────────────────────────────────── echo "Waiting for server to become Active..." MY_SERVER_STATUS="" @@ -371,7 +492,7 @@ done [[ "$MY_SERVER_STATUS" == "Active" ]] || \ exit_with_failure "Server did not reach 'Active' state in time. Check the Aruba Cloud console." -# ── Step 5: Poll GitHub until runner is registered ──────────────────────────── +# ── Step 7: Poll GitHub until runner is registered ──────────────────────────── echo "Waiting for GitHub Actions Runner to register..." MY_GITHUB_RUNNER_ID="" diff --git a/action.yml b/action.yml index fd1615a..863f746 100644 --- a/action.yml +++ b/action.yml @@ -86,21 +86,47 @@ inputs: required: false vpc_id: - description: ID of the VPC to attach the server to. + description: >- + ID of the VPC to attach the server to. + When omitted, a new VPC is created automatically and deleted by the stop step. required: false subnet_id: - description: ID of the subnet inside the VPC. + description: >- + ID of the subnet inside the VPC. + When omitted, a new subnet is created automatically and deleted by the stop step. required: false security_group_id: - description: ID of the security group to apply. + description: >- + ID of the security group to apply. + When omitted, a new security group with an egress allow-all rule is created + automatically and deleted by the stop step. required: false keypair_id: description: ID of the SSH key pair to inject into the server. required: false + auto_vpc_id: + description: >- + ID of the VPC that was auto-created by the start step. + Pass the 'auto_vpc_id' output from the start step here; the VPC will be + deleted together with the server. Leave empty if you provided your own vpc_id. + required: false + + auto_subnet_id: + description: >- + ID of the subnet that was auto-created by the start step. + Pass the 'auto_subnet_id' output here to have it deleted on stop. + required: false + + auto_security_group_id: + description: >- + ID of the security group that was auto-created by the start step. + Pass the 'auto_security_group_id' output here to have it deleted on stop. + required: false + runner_labels: description: >- Comma-separated list of additional runner labels. @@ -171,6 +197,24 @@ outputs: so the disk is removed together with the server. value: ${{ steps.acloud-github-runner.outputs.boot_disk_id }} + auto_vpc_id: + description: >- + ID of the VPC that was auto-created by this step (empty if you provided vpc_id). + Pass to the delete step as 'auto_vpc_id' to have it cleaned up automatically. + value: ${{ steps.acloud-github-runner.outputs.auto_vpc_id }} + + auto_subnet_id: + description: >- + ID of the subnet that was auto-created by this step (empty if you provided subnet_id). + Pass to the delete step as 'auto_subnet_id' to have it cleaned up automatically. + value: ${{ steps.acloud-github-runner.outputs.auto_subnet_id }} + + auto_security_group_id: + description: >- + ID of the security group that was auto-created by this step (empty if you provided + security_group_id). Pass to the delete step as 'auto_security_group_id'. + value: ${{ steps.acloud-github-runner.outputs.auto_security_group_id }} + runs: using: "composite" steps: @@ -222,6 +266,9 @@ runs: INPUT_BOOT_DISK_TYPE: ${{ inputs.boot_disk_type }} INPUT_BOOT_DISK_WAIT: ${{ inputs.boot_disk_wait }} INPUT_BOOT_DISK_ID: ${{ inputs.boot_disk_id }} - INPUT_RUNNER_WAIT: ${{ inputs.runner_wait }} - INPUT_SERVER_WAIT: ${{ inputs.server_wait }} - INPUT_SERVER_ID: ${{ inputs.server_id }} + INPUT_RUNNER_WAIT: ${{ inputs.runner_wait }} + INPUT_SERVER_WAIT: ${{ inputs.server_wait }} + INPUT_SERVER_ID: ${{ inputs.server_id }} + INPUT_AUTO_VPC_ID: ${{ inputs.auto_vpc_id }} + INPUT_AUTO_SUBNET_ID: ${{ inputs.auto_subnet_id }} + INPUT_AUTO_SECURITY_GROUP_ID: ${{ inputs.auto_security_group_id }} From c1db8ac75cd801faef2d97d8b94295bf48fbcd5a Mon Sep 17 00:00:00 2001 From: Amedeo Palopoli Date: Wed, 29 Jul 2026 15:29:51 +0200 Subject: [PATCH 3/9] fix: update acloud-cli install to fetch versioned tarball v1.0.0 changed the release asset format from a single binary (acloud-linux-amd64) to a versioned tarball (acloud_1.0.0_linux_amd64.tar.gz). Resolve the correct download URL via the GitHub releases API so the install step stays correct across future version bumps. --- action.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index 863f746..66a52ef 100644 --- a/action.yml +++ b/action.yml @@ -227,15 +227,20 @@ runs: aarch64|arm64) ACLOUD_ARCH="arm64" ;; *) ACLOUD_ARCH="amd64" ;; esac - URL="https://github.com/Arubacloud/acloud-cli/releases/latest/download/acloud-linux-${ACLOUD_ARCH}" + # Resolve the tarball URL for the latest release dynamically so the + # install step stays correct across version bumps. + ASSET_URL=$(curl -fsSL https://api.github.com/repos/Arubacloud/acloud-cli/releases/latest \ + | jq -r ".assets[] | select(.name | test(\"linux_${ACLOUD_ARCH}\\\\.tar\\\\.gz\")) | .browser_download_url") + [[ -n "$ASSET_URL" ]] || { echo "Could not resolve acloud-cli download URL."; exit 1; } for attempt in 1 2 3; do echo "Downloading acloud-cli (attempt ${attempt}/3)..." - curl -fsSL "$URL" -o /tmp/acloud && break + curl -fsSL "$ASSET_URL" -o /tmp/acloud.tar.gz && break [[ $attempt -lt 3 ]] && sleep 10 done - [[ -f /tmp/acloud ]] || { echo "Failed to download acloud-cli after 3 attempts."; exit 1; } - chmod +x /tmp/acloud + [[ -f /tmp/acloud.tar.gz ]] || { echo "Failed to download acloud-cli after 3 attempts."; exit 1; } + tar -xzf /tmp/acloud.tar.gz -C /tmp acloud sudo mv /tmp/acloud /usr/local/bin/acloud + rm -f /tmp/acloud.tar.gz fi - name: Aruba Cloud GitHub Runner From 7e12a0734cfb8a01019b8284672b37945be786a6 Mon Sep 17 00:00:00 2001 From: Amedeo Palopoli Date: Wed, 29 Jul 2026 15:45:37 +0200 Subject: [PATCH 4/9] fix: add readiness polling for auto-created network resources Based on acloud-cli e2e test patterns: - Add _wait_for_status helper (polls Status: line, exits on Failed/Error/Deleted) - Add _wait_for_removal helper (polls until resource no longer exists) VPC: poll Active|Ready (180s) before creating child resources Subnet: add --region, --cidr 10.0.0.0/24, --dhcp-enabled; poll Active|Ready (90s) Security group: vpc_id is now a positional arg; add --region; poll Active|Ready (120s) Security rule: poll security group Active|Ready (60s) after rule add (SG is InCreation during update) Delete mode: wait for subnet removal before deleting parent VPC Fix ID parsing for all auto-created network resources using grep for 24-char hex IDs. --- action.sh | 76 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/action.sh b/action.sh index fe9bf78..e52c3fb 100644 --- a/action.sh +++ b/action.sh @@ -15,6 +15,43 @@ function exit_with_failure() { exit 1 } +# Poll a resource (via text output) until its status matches or hits a +# terminal state. Uses 'Status:' line from plain-text acloud output. +# Usage: _wait_for_status