---
title: "GitHub Actions"
description: "Verify a workflow and exchange GitHub identity for short-lived, project-scoped Metalhost API access."
url: "https://metalhost.net/docs/developers/guides/github-actions"
---

# GitHub Actions

Let an approved workflow call Metalhost without storing a long-lived Metalhost API key in GitHub.

> **Not a managed runner**
>
> This authenticates a workflow that already runs on GitHub-hosted runners or runners you manage. It does not create runners or VMs. CLI v1.1.2 includes `metalhost auth github`.

## What it does

A job obtains signed identity from GitHub. Metalhost checks it against an approved connection and returns a token valid for 15 minutes. The token can act only in the approved project with permissions allowed by both the connection and its service account.

The job still runs on GitHub-hosted runners or runners you manage. GitHub sign-in to the dashboard is a separate feature; signing in does not authorize a workflow.

## Connect a workflow

1. Open the target project's **Developers → Service accounts**. Create or choose an active account. For the example below, give it `monitoring.read`.
2. Open **Developers → GitHub Actions** and start verification for that account.
3. Copy the temporary verification step into the exact workflow/job you intend to authorize. Save the supplied pairing code as the temporary `METALHOST_SETUP_CODE` GitHub Actions secret. Use the displayed API origin, setup ID and audience.
4. Run that workflow from the intended branch, event and environment. Verification expires after 30 minutes. It proves identity but does not grant project access yet.
5. Return to Metalhost and review the observed repository, owner, workflow, branch, event and environment. Approve only the required permissions.
6. Remove the temporary verification step and pairing secret. Replace the step with the exchange example below. Copy the approved connection's trust name and audience into GitHub Actions variables.

Keep the same workflow file, event and branch used during verification. If an environment or reusable workflow was included, keep those too. Renaming the workflow, changing the triggering event, or moving branches can require a newly verified connection. Pull-request events are not supported; accepted events are `push`, `workflow_dispatch` and `schedule`, subject to the connection's exact event match.

## Example: read your VM inventory

This complete workflow requires `bash`, `curl` and `jq`, provided by the selected GitHub-hosted Ubuntu image. Verify it using `workflow_dispatch` from the intended branch. It lists at most 50 VMs without querying metrics; `nextPageToken` signals more results. Repeat with `pageToken` to paginate.

```
name: Read Metalhost monitoring
on:
  workflow_dispatch:
permissions: {}
jobs:
  monitoring:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      id-token: write
    env:
      # Copy these non-secret values from the approved Metalhost connection.
      METALHOST_ENDPOINT: ${{ vars.METALHOST_ENDPOINT }}
      METALHOST_TRUST: ${{ vars.METALHOST_TRUST }}
      METALHOST_AUDIENCE: ${{ vars.METALHOST_AUDIENCE }}
    steps:
      - name: Exchange GitHub identity and read VM inventory
        shell: bash
        run: |
          set -euo pipefail
          set +x
          : "${METALHOST_ENDPOINT:?Set the API origin for your enabled environment}"
          : "${METALHOST_TRUST:?Set the approved trust name}"
          : "${METALHOST_AUDIENCE:?Set the connection audience}"
          case "$METALHOST_ENDPOINT" in
            https://*) ;;
            *) echo 'An HTTPS API origin is required' >&2; exit 1 ;;
          esac
          audience=$(jq -rn --arg value "$METALHOST_AUDIENCE" '$value|@uri')
          assertion=$(curl --fail --silent --show-error --max-time 30 \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$audience" | jq -er '.value')
          echo "::add-mask::$assertion"
          response=$(jq -nc --arg trust "$METALHOST_TRUST" --arg assertion "$assertion" \
            '{trustName:$trust,assertion:$assertion}' |
            curl --fail --silent --show-error --max-time 30 \
              -H 'Content-Type: application/json' -H 'Connect-Protocol-Version: 1' \
              --data-binary @- "$METALHOST_ENDPOINT/aes.iam.v1.AutomationService/ExchangeGitHubToken")
          token=$(jq -er '.accessToken' <<<"$response")
          echo "::add-mask::$token"
          project=$(jq -er '.projectName' <<<"$response")
          unset assertion response
          # Requires monitoring.read. This request does not create or change VMs.
          jq -nc --arg project "$project" \
            '{projectName:$project,pageSize:50,inventoryOnly:true}' |
            curl --fail --silent --show-error --max-time 30 \
              -H "Authorization: Bearer $token" \
              -H 'Content-Type: application/json' -H 'Connect-Protocol-Version: 1' \
              --data-binary @- "$METALHOST_ENDPOINT/aes.monitoring.v1.MonitoringService/ListVMMonitoring" |
            jq '{vms:[.vms[]? | {name,displayName,state}],nextPageToken}'
          unset token
```

[Download workflow](https://metalhost.net/examples/github-monitoring.yml). Set `METALHOST_ENDPOINT` to the API origin, not the portal URL, without a trailing slash. Set `METALHOST_TRUST` to the full approved trust name and `METALHOST_AUDIENCE` exactly as shown by Metalhost. These identifiers are not secrets.

This uses the HTTP API directly, so no CLI helper is required. The September CLI branch also implements `metalhost auth github` and `metalhost monitoring`; these are absent from older CLI releases. Use a matching September build and check its `--help` before using the CLI alternative.

## Other uses

- Read monitoring during a deployment and evaluate your own readiness criteria.
- Manage alert rules using `monitoring.write`.
- Provision or remove test VMs using the compute API and explicitly granted compute permissions. This is a separate, potentially billable operation; the authentication step does not do it for you.

Scoped automation does not provide guest SSH access, interactive consoles, identity administration or arbitrary administrative RPCs. A workflow that deploys software inside a VM still needs a separate, deliberate guest-access mechanism.

## Secrets and failures

- Never enable shell tracing, print the exchange response, or put tokens into artifacts or `GITHUB_ENV`. The example masks tokens and keeps them inside one step.
- A job needing access after 15 minutes must request fresh GitHub proof and exchange again. There is no refresh token.
- A denied exchange usually means the observed identity, audience, event or service-account state does not match. Inspect the connection, not the token contents in logs.
- Protect the approved repository and branch. An attacker able to edit an authorized workflow can exercise its granted permissions.
- Disable or delete a connection to stop its access. Removing a temporary pairing secret alone does not revoke an approved connection.

The job's `id-token: write` permission allows it to request GitHub identity; Metalhost permissions still come from the approved connection. See [GitHub's OIDC reference](https://docs.github.com/en/actions/reference/security/oidc).
