diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..31ac99b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,156 @@ +name: Documentation + +on: + push: + branches: [main] + paths: + - 'docs/website/**' + - 'CHANGELOG.md' + - '.github/workflows/docs.yml' + pull_request: + branches: [main] + paths: + - 'docs/website/**' + - 'CHANGELOG.md' + - '.github/workflows/docs.yml' + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + name: Build Documentation + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'docs/version-')) + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs/website/package-lock.json + + - name: Install dependencies + working-directory: docs/website + run: npm ci + + - name: Sync versions.json with existing docs + working-directory: docs/website + run: | + echo "=== Syncing versions.json with existing versioned docs ===" + echo "[]" > versions.json + + if [ -d "versioned_docs" ]; then + echo "Checking for version directories..." + VERSIONS=$(find versioned_docs -maxdepth 1 -type d -name "version-*" 2>/dev/null | sed 's|.*/version-||' | sort -V -r) + + if [ -n "$VERSIONS" ]; then + echo "$VERSIONS" | jq -R . | jq -s . > versions.json + VERSION_COUNT=$(echo "$VERSIONS" | wc -l | tr -d ' ') + echo "✓ Updated versions.json with $VERSION_COUNT version(s)" + else + echo "✓ No version directories found, keeping empty array" + fi + else + echo "✓ No versioned_docs directory, keeping empty array" + fi + + echo "Final versions.json:" + cat versions.json + + if ! jq empty versions.json 2>/dev/null; then + echo "ERROR: versions.json is invalid JSON!" + echo "[]" > versions.json + exit 1 + fi + + rm -rf .docusaurus + echo "✓ Cleared Docusaurus cache" + + - name: Build documentation + working-directory: docs/website + run: npm run build + env: + NODE_ENV: production + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/website/build + + deploy: + name: Deploy Documentation + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + test: + name: Test Documentation Build + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs/website/package-lock.json + + - name: Install dependencies + working-directory: docs/website + run: npm ci + + - name: Remove versioned docs for PR check + working-directory: docs/website + run: | + echo "Cleaning up versioned docs for PR validation..." + rm -rf versioned_docs versioned_sidebars + if [ -f "versions.json" ]; then + echo "[]" > versions.json + fi + rm -rf .docusaurus + echo "✓ Versioned docs cleaned up" + + - name: Build and validate documentation + working-directory: docs/website + run: | + echo "Building documentation (versioning disabled for PR check)..." + npm run build -- --no-minify + echo "✓ Documentation build successful!" + env: + NODE_ENV: production + DISABLE_VERSIONING: 'true' 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/.gitignore b/.gitignore index 41ff21e..beba368 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,9 @@ others/ # Local test config (may contain credentials) test +.tokensave + +# Docusaurus +docs/website/node_modules/ +docs/website/build/ +docs/website/.docusaurus/ 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..165eca1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Thank you for your interest in contributing to `acloud-github-runner`! + +See the full [Contributing Guide](docs/website/docs/contributing.md) for detailed instructions. + +## Quick summary + +1. Open an [issue](https://github.com/Arubacloud/acloud-github-runner/issues) to discuss the change you want to make. +2. Fork the repository and create a branch: `feat/`. +3. Make your changes and ensure `shellcheck action.sh runner-install.sh` passes. +4. Update `CHANGELOG.md` under `[Unreleased]`. +5. Open a pull request with a clear description of what changed and why. + +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). Please be respectful and inclusive. diff --git a/README.md b/README.md index 4448e1b..0090efd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Self-Hosted GitHub Actions Runner on Aruba Cloud -On-demand self-hosted GitHub Actions runners on [Aruba Cloud](https://www.arubacloud.com). +On-demand self-hosted GitHub Actions runners on [Aruba Cloud](https://www.arubacloud.com). Each workflow run gets a fresh, ephemeral cloud server — created at job start, deleted at job end.

ArubaCloud Logo

-Each workflow gets a **fresh, ephemeral cloud server**. The server is created at the start of the job and deleted at the end — no idle costs, no shared state between runs. +**[Documentation](https://arubacloud.github.io/acloud-github-runner/)** · [Getting Started](https://arubacloud.github.io/acloud-github-runner/getting-started) · [Inputs & Outputs](https://arubacloud.github.io/acloud-github-runner/reference) · [Changelog](CHANGELOG.md) --- @@ -22,27 +22,17 @@ Each workflow gets a **fresh, ephemeral cloud server**. The server is created at │ Workflow │ │ │ │ start-runner ──► [this action: create] │ -│ │ • provisions Aruba Cloud server │ -│ │ • waits for runner registration │ +│ │ • auto-provisions VPC/subnet/SG │ +│ │ • creates server + waits for runner │ │ ▼ │ │ your-job ──────► runs-on: ephemeral runner │ │ │ │ │ ▼ │ │ stop-runner ──► [this action: delete] (if: always()) │ -│ • deletes the server │ +│ • deletes server + network resources │ └─────────────────────────────────────────────────────────┘ ``` ---- - -## Prerequisites - -- An Aruba Cloud account with API credentials (`client-id` and `client-secret`). -- A project with a pre-created **VPC**, **subnet**, **security group**, and **SSH key pair**. Resource URIs for each are required inputs. -- A GitHub PAT with **Read and write → Administration** permission on the target repository (or `admin:org` for organisation runners). - ---- - ## Quickstart ```yaml @@ -55,10 +45,13 @@ jobs: name: Start ephemeral runner runs-on: ubuntu-latest outputs: - label: ${{ steps.runner.outputs.label }} - server_id: ${{ steps.runner.outputs.server_id }} - project_id: ${{ steps.runner.outputs.project_id }} - boot_disk_id: ${{ steps.runner.outputs.boot_disk_id }} + label: ${{ steps.runner.outputs.label }} + server_id: ${{ steps.runner.outputs.server_id }} + project_id: ${{ steps.runner.outputs.project_id }} + boot_disk_id: ${{ steps.runner.outputs.boot_disk_id }} + auto_vpc_id: ${{ steps.runner.outputs.auto_vpc_id }} + auto_subnet_id: ${{ steps.runner.outputs.auto_subnet_id }} + auto_security_group_id: ${{ steps.runner.outputs.auto_security_group_id }} steps: - uses: Arubacloud/acloud-github-runner@v1 id: runner @@ -68,12 +61,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 }} - flavor: CSO2A4 - image: LU22-001 + # vpc_id, subnet_id, security_group_id are optional — + # the action auto-creates and manages them when omitted. + flavor: CSO2A4 # 2 vCPU / 4 GB RAM + image: LU22-001 # Ubuntu 22.04 LTS build: name: Build @@ -91,134 +82,24 @@ jobs: steps: - uses: Arubacloud/acloud-github-runner@v1 with: - mode: delete - github_token: ${{ secrets.GH_PAT }} - acloud_client_id: ${{ secrets.ACLOUD_CLIENT_ID }} - acloud_client_secret: ${{ secrets.ACLOUD_CLIENT_SECRET }} - acloud_project_id: ${{ needs.start-runner.outputs.project_id }} - server_id: ${{ needs.start-runner.outputs.server_id }} - boot_disk_id: ${{ needs.start-runner.outputs.boot_disk_id }} + mode: delete + github_token: ${{ secrets.GH_PAT }} + acloud_client_id: ${{ secrets.ACLOUD_CLIENT_ID }} + acloud_client_secret: ${{ secrets.ACLOUD_CLIENT_SECRET }} + acloud_project_id: ${{ needs.start-runner.outputs.project_id }} + server_id: ${{ needs.start-runner.outputs.server_id }} + boot_disk_id: ${{ needs.start-runner.outputs.boot_disk_id }} + name: ${{ needs.start-runner.outputs.label }} + auto_vpc_id: ${{ needs.start-runner.outputs.auto_vpc_id }} + auto_subnet_id: ${{ needs.start-runner.outputs.auto_subnet_id }} + auto_security_group_id: ${{ needs.start-runner.outputs.auto_security_group_id }} ``` ---- - -## Inputs - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `mode` | yes | — | `create` or `delete` | -| `github_token` | yes | — | PAT with Administration read/write | -| `acloud_client_id` | yes | — | Aruba Cloud API client ID | -| `acloud_client_secret` | yes | — | Aruba Cloud API client secret | -| `acloud_project_id` | yes | — | Aruba Cloud project ID | -| `name` | no | `acloud-runner--` | Server name and runner label | -| `region` | no | `ITBG-Bergamo` | Aruba Cloud region | -| `zone` | no | `ITBG-1` | Availability zone | -| `flavor` | no | `CSO2A4` | Server size (see [Flavors](#flavors)) | -| `image` | no | `LU22-001` | Boot image used to create the boot disk (see [Images](#images)) | -| `boot_disk_size` | no | `20` | Boot disk size in GB | -| `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 | -| `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 | -| `pre_runner_script` | no | `""` | Bash commands to run before the runner starts | -| `runner_wait` | no | `60` | Max polling attempts for runner registration (×10 s) | -| `server_wait` | no | `30` | Max polling attempts for server active status (×10 s) | -| `server_id` | yes (delete) | — | Server ID returned by the create step | - -## Outputs - -| Output | Description | -|--------|-------------| -| `label` | Runner label — use as the `runs-on` value in your job | -| `server_id` | Aruba Cloud server ID — pass to the delete step together with `project_id` | -| `project_id` | Aruba Cloud project ID — a server is uniquely identified by `server_id` + `project_id`; pass both to the delete step | -| `boot_disk_id` | ID of the boot disk created for the server — pass to the delete step so it is removed together with the server | - ---- - -## Flavors - -> Source: [Aruba Cloud API metadata](http://api.arubacloud.com/docs/metadata/) +## Documentation -### Linux flavors - -| Flavor | vCPU | RAM | -|--------|-----:|----:| -| CSO1A2 | 1 | 2 GB | -| CSO1A4 | 1 | 4 GB | -| CSO2A4 | 2 | 4 GB | -| CSO2A8 | 2 | 8 GB | -| CSO4A8 | 4 | 8 GB | -| CSO4A16 | 4 | 16 GB | -| CSO8A16 | 8 | 16 GB | -| CSO8A32 | 8 | 32 GB | -| CSO16A32 | 16 | 32 GB | -| CSO16A64 | 16 | 64 GB | -| CSO32A64 | 32 | 64 GB | - -### Windows flavors - -Windows flavors start from `CSO1A4`. All Linux flavors ≥ `CSO1A4` are also available for Windows images. - ---- +Full documentation, usage examples, and input/output reference are available at: -## Images - -> Source: [Aruba Cloud API metadata](http://api.arubacloud.com/docs/metadata/) - -| Image code | Operating system | -|------------|-----------------| -| `LU20-001` | Ubuntu 20.04 LTS (64-bit) | -| `LU22-001` | Ubuntu 22.04 LTS (64-bit) | -| `LU24-001` | Ubuntu 24.04 LTS (64-bit) | -| `DE11-001` | Debian 11 (64-bit) | -| `DE12-001` | Debian 12 (64-bit) | -| `alma8` | AlmaLinux 8 (64-bit) | -| `alma9` | AlmaLinux 9 (64-bit) | -| `osuse15_2_x64_1_0` | openSUSE 15 (64-bit) | -| `WS19-001_W2K19_1_0` | Windows Server 2019 | -| `WS22-001_W2K22_1_0` | Windows Server 2022 | - ---- - -## Regions and zones - -| Region | Zones | -|--------|-------| -| `ITBG-Bergamo` | `ITBG-1`, `ITBG-2`, `ITBG-3` | - -> Once a resource is created in a location it cannot be moved. - ---- - -## Required GitHub PAT scopes - -| Use case | Scope | -|----------|-------| -| Repository runner | `repo` | -| Organisation runner | `admin:org` | - ---- - -## Troubleshooting - -**Runner never registers** -Cloud-init takes 2–4 minutes to install packages and download the runner binary. Increase `runner_wait` (each unit = 10 s) if your image or network is slow. Check cloud-init logs on the server: `/var/log/cloud-init-output.log`. - -**Server not deleted after failure** -The delete step uses `if: always()` so it runs even when earlier jobs fail. Verify the `server_id` output is correctly passed via `needs..outputs.server_id`. You can also delete servers manually from the [Aruba Cloud portal](https://portal.arubacloud.com). - -**`acloud-cli` authentication fails** -Ensure `ACLOUD_CLIENT_ID` and `ACLOUD_CLIENT_SECRET` are stored as repository secrets and mapped to the action inputs. The credentials are stored in `~/.acloud.yaml` with `0600` permissions during the run. - ---- +**[https://arubacloud.github.io/acloud-github-runner/](https://arubacloud.github.io/acloud-github-runner/)** ## License diff --git a/action.sh b/action.sh index d1e499a..363fb83 100644 --- a/action.sh +++ b/action.sh @@ -2,16 +2,57 @@ 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. +# 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