From 0aa886c5b2f48a6fdbf817d08f1d3f6e79eee999 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 13:02:57 +0000 Subject: [PATCH 1/2] Migrate to Unity Build Automation API v2 Unity deprecated Build Automation API v1 and removes it on 2026-12-21, so default to v2 at https://build-automation.services.api.unity.com/v2. v2 authenticates with HTTP Basic using a service account key ID and secret and rejects the legacy Build Automation API key, so add `unity_service_account_key_id` / `unity_service_account_secret_key` inputs that are base64-encoded into the Authorization header. `authorization_header` stays supported for callers that can only supply a pre-built header, but the two are mutually exclusive: supplying both, or half of the key pair, now fails before any request is sent rather than silently using stale credentials. Adapt response handling to v2: - 202 Accepted is the success status (the existing `^2` check covers it). - Errors are RFC 7807, so read `detail`/`title` alongside v1's `error`/`message`, and surface the `requestId` Unity support asks for. - 401/403 responses explain that v2 needs service account credentials with a Build Automation role, the most likely migration failure. - A 2xx carrying a per-target `error` and no build ID (e.g. a build is already running) now reports that message instead of the raw body. - Expose the `buildStatus` field v2 returns as a new `build_status` output. Pointing `api_url` back at v1 still works and logs a deprecation warning. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016Mxrx7ufpE3maxhLfSYwPA --- README.md | 94 +++++++++++++++++++++++++++---------- action.yml | 135 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 177 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 4fbc331..04bea49 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,45 @@ A reusable GitHub Action that triggers a [Unity Build Automation](https://docs.u The action does not check out or read your repository — it only calls the Unity Build Automation REST API — so it is safe to run before, after, or without `actions/checkout`. +It targets **API v2** by default. Unity removes API v1 on **2026-12-21**; see [Migrating from API v1](#migrating-from-api-v1) if you are upgrading from an earlier release of this action. + +## Authentication + +API v2 authenticates with HTTP Basic using a Unity **service account** key ID and secret. The legacy Build Automation API key does not work on v2. + +1. In the Unity Cloud Dashboard, go to **Administration → Service Accounts** and create a service account. +2. Give it a role that allows Build Automation builds in the target project. +3. Create an API key on that service account and store the **key ID** and **secret key** as GitHub secrets. + +Pass them as `unity_service_account_key_id` and `unity_service_account_secret_key`; the action base64-encodes `keyId:secretKey` and sends the `Authorization: Basic …` header for you. Both values are masked in the logs. + ## Inputs -| Input | Required | Description | Default | -| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| `unity_org_id` | Yes | The Unity Organization ID (used in the endpoint `orgs/{unity_org_id}`). | — | -| `unity_project_id` | Yes | The Unity Build Automation Project ID. | — | -| `build_target_id` | Yes | The Build Target ID within the Unity Build Automation project. | — | -| `authorization_header` | Yes | Authorization header for the API, e.g. `Authorization: Basic `. A bare token is also accepted and gets the `Authorization: ` prefix added. | — | -| `branch` | No | Git branch to build. When empty, the branch configured on the build target is used. | `''` | -| `clean` | No | Whether to perform a clean build. Accepts `true`/`false` (also `1`/`0`, `yes`/`no`, any casing). | `false` | -| `platform` | No | Build platform override (e.g. `standalonewindows64`). Sent only when set. | `''` | -| `machine_type_label` | No | Machine type label (e.g. `win_premium_v1`). Sent as `machineTypeLabel`, only when set. | `''` | -| `api_url` | No | Base URL of the API, including the version segment. Override to target a newer API version. | `https://build-api.cloud.unity3d.com/api/v1` | +| Input | Required | Description | Default | +| ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| `unity_org_id` | Yes | The Unity Organization ID (used in the endpoint `orgs/{unity_org_id}`). | — | +| `unity_project_id` | Yes | The Unity Build Automation Project ID. | — | +| `build_target_id` | Yes | The Build Target ID within the Unity Build Automation project. | — | +| `unity_service_account_key_id` | Yes\* | Key ID of a Unity service account with a Build Automation role. | `''` | +| `unity_service_account_secret_key` | Yes\* | Secret key of that service account. Pass as a secret. | `''` | +| `authorization_header` | No\* | Pre-built header, e.g. `Authorization: Basic `. A bare token also works and gets the `Authorization: ` prefix added. | `''` | +| `branch` | No | Git branch to build. When empty, the branch configured on the build target is used. | `''` | +| `clean` | No | Whether to perform a clean build. Accepts `true`/`false` (also `1`/`0`, `yes`/`no`, any casing). | `false` | +| `platform` | No | Build platform override (e.g. `standalonewindows64`). Sent only when set. | `''` | +| `machine_type_label` | No | Machine type label (e.g. `win_premium_v1`). Sent as `machineTypeLabel`, only when set. | `''` | +| `api_url` | No | Base URL of the API, including the version segment. | `https://build-automation.services.api.unity.com/v2` | + +\* Supply **either** the service account pair **or** `authorization_header` — not both. Providing both, or only one half of the pair, fails the step before any request is sent. `platform` and `machine_type_label` are omitted from the request when left empty, so the build target's own configuration applies. Set them only if you want to override the target. ## Outputs -| Output | Description | -| ---------- | -------------------------------------------------------- | -| `build_id` | The ID (build number) of the triggered Unity build. | -| `platform` | The platform of the triggered build, as reported by the API. | +| Output | Description | +| -------------- | ------------------------------------------------------------------------ | +| `build_id` | The ID (build number) of the triggered Unity build. | +| `platform` | The platform of the triggered build, as reported by the API. | +| `build_status` | The status the build was created with (e.g. `queued`). Empty if the API did not report one. | ## Usage @@ -57,7 +74,8 @@ jobs: unity_org_id: ${{ secrets.UNITY_ORG_ID }} unity_project_id: ${{ secrets.UNITY_PROJECT_ID }} build_target_id: ${{ secrets.UNITY_BUILD_TARGET_ID }} - authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} + unity_service_account_key_id: ${{ secrets.UNITY_SA_KEY_ID }} + unity_service_account_secret_key: ${{ secrets.UNITY_SA_SECRET_KEY }} branch: main # Optional: override the branch to build ``` @@ -92,7 +110,8 @@ jobs: unity_org_id: ${{ secrets.UNITY_ORG_ID }} unity_project_id: ${{ secrets.UNITY_PROJECT_ID }} build_target_id: ${{ secrets.UNITY_BUILD_TARGET_ID }} - authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} + unity_service_account_key_id: ${{ secrets.UNITY_SA_KEY_ID }} + unity_service_account_secret_key: ${{ secrets.UNITY_SA_SECRET_KEY }} branch: ${{ inputs.branch }} clean: ${{ inputs.clean }} @@ -100,29 +119,54 @@ jobs: run: | echo "Build ${{ steps.unity.outputs.build_id }} started" echo "Platform: ${{ steps.unity.outputs.platform }}" + echo "Status: ${{ steps.unity.outputs.build_status }}" +``` + +## Migrating from API v1 + +Unity deprecated Build Automation API v1 and **removes it on 2026-12-21**. This action calls v2 by default. If you used an earlier release, two things change in your workflow: + +| | v1 (before) | v2 (now) | +| --- | --- | --- | +| Base URL | `https://build-api.cloud.unity3d.com/api/v1` | `https://build-automation.services.api.unity.com/v2` | +| Credentials | Build Automation API key | Service account key ID + secret | +| Success status | `200`/`201` | `202 Accepted` | +| Error body | `{ "error": … }` | [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) `{ "detail": …, "requestId": … }` | + +The endpoint path (`/orgs/{org}/projects/{project}/buildtargets/{target}/builds`) and the request fields this action sends (`clean`, `delay`, `branch`, `platform`, `machineTypeLabel`) are unchanged. + +To migrate, replace the `authorization_header` input with the service account pair: + +```diff +- authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} ++ unity_service_account_key_id: ${{ secrets.UNITY_SA_KEY_ID }} ++ unity_service_account_secret_key: ${{ secrets.UNITY_SA_SECRET_KEY }} ``` -## API version +Reusing your old API key will fail with `HTTP 401`/`403` — v2 only accepts service account credentials. A `403` with valid credentials usually means the service account has no Build Automation role on the project. The action calls both cases out explicitly in its error message and includes the API's `requestId`, which Unity support asks for. -By default the action calls API **v1** at `https://build-api.cloud.unity3d.com/api/v1`, which is the version Unity's published Build Automation API reference documents. +### Staying on v1 temporarily -If you are moving to a newer API version, point `api_url` at it — no change to the action is needed as long as the request/response shapes stay compatible: +Point `api_url` back at v1 until you have a service account in place. The action logs a deprecation warning when you do: ```yaml with: - api_url: https://build-api.cloud.unity3d.com/api/v2 + api_url: https://build-api.cloud.unity3d.com/api/v1 + authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} # ... ``` -The response parser accepts both a single-element array (`[{ "build": 123, ... }]`, the v1 shape) and a bare object (`{ "build": 123, ... }`). If a newer version renames the `build` field, the action fails with the raw response body rather than silently reporting an empty build ID. +Both versions are parsed by the same code: the response parser accepts a single-element array (`[{ "build": 123, … }]`) or a bare object, and reads error text from `detail`, `error`, `message`, or `title`, whichever the response carries. If a response has no build ID, the action fails with the API's own explanation (e.g. a build is already running for that target) rather than silently reporting an empty one. + +Reference: [Build Automation API v2](https://docs.unity.com/en-us/oas-build-automation-client/2.0.0) · [Unity's v1 → v2 migration guide](https://support.unity.com/hc/en-us/articles/51178448254356-How-do-I-migrate-my-Unity-Build-Automation-UBA-REST-API-calls-from-V1-to-V2) ## Behaviour notes - **Non-idempotent by design.** Triggering a build is not idempotent, so a request that reached Unity is never retried. Only DNS and connection failures — where nothing reached the server — are retried (up to 3 attempts). This avoids double-charging build minutes. -- **Secrets stay in one step.** The authorization header is passed to a single step's environment and masked in the logs. It is not written to `$GITHUB_ENV`, so it does not leak into other steps of the calling job. +- **Secrets stay in one step.** Credentials are passed to a single step's environment and masked in the logs — the secret key, the base64 credential derived from it, and the assembled header are all masked. Nothing is written to `$GITHUB_ENV`, so they do not leak into other steps of the calling job. - **The response body is not logged** by default. Re-run the workflow with debug logging enabled to see it. -- **Required inputs are validated before the request** — an empty org/project/target ID or an invalid `clean` value fails immediately instead of producing a confusing API error. -- **`curl` and `jq`** are used, and are preinstalled on GitHub-hosted runners. On runners that lack them, the action installs them via `apt-get` or `brew`, and fails with a clear message if neither is available. +- **Required inputs are validated before the request** — an empty org/project/target ID, a missing or ambiguous credential, or an invalid `clean` value fails immediately instead of producing a confusing API error. +- **`curl` and `jq`** are used, and are preinstalled on GitHub-hosted runners. On runners that lack them, the action installs them via `apt-get` or `brew`, and fails with a clear message if neither is available. `base64` (from coreutils) is also required and is present on any runner that has a shell. ## Development diff --git a/action.yml b/action.yml index 591ae4f..2cca950 100644 --- a/action.yml +++ b/action.yml @@ -14,12 +14,29 @@ inputs: build_target_id: description: 'Your Build Target ID' required: true + unity_service_account_key_id: + description: >- + Key ID of a Unity service account with a Build Automation role. Combined + with `unity_service_account_secret_key` into the HTTP Basic credential + required by API v2. Required unless `authorization_header` is supplied. + required: false + default: '' + unity_service_account_secret_key: + description: >- + Secret key of the Unity service account. Pass this as a secret; it is + masked in the logs. + required: false + default: '' authorization_header: description: >- - Authorization header for the Unity Cloud Build API, e.g. - "Authorization: Basic ". A bare token is also accepted and gets the - "Authorization: " prefix added automatically. - required: true + Pre-built Authorization header, e.g. "Authorization: Basic ". A + bare token is also accepted and gets the "Authorization: " prefix added + automatically. Use this only if you cannot pass the service account key + and secret separately; it is mutually exclusive with them. Note that API + v2 rejects the legacy Build Automation API key — the token must be + base64(":") of a service account. + required: false + default: '' branch: description: >- Git branch to build. When empty, the branch configured on the build target @@ -46,10 +63,11 @@ inputs: api_url: description: >- Base URL of the Unity Build Automation API, including the version segment. - Override this to move to a newer API version (e.g. ".../api/v2") without - changing the action. + Defaults to API v2. The v1 base URL + (https://build-api.cloud.unity3d.com/api/v1) still works until Unity + removes it on 2026-12-21, but the action warns when it is used. required: false - default: 'https://build-api.cloud.unity3d.com/api/v1' + default: 'https://build-automation.services.api.unity.com/v2' outputs: build_id: description: 'The ID (build number) of the triggered build.' @@ -57,6 +75,11 @@ outputs: platform: description: 'The platform of the build, as reported by the API.' value: '${{ steps.trigger-build.outputs.platform }}' + build_status: + description: >- + The status the build was created with, as reported by the API (e.g. + queued). Empty if the API did not report one. + value: '${{ steps.trigger-build.outputs.build_status }}' runs: using: 'composite' steps: @@ -112,6 +135,8 @@ runs: # executed as code. env: INPUT_AUTHORIZATION_HEADER: ${{ inputs.authorization_header }} + INPUT_KEY_ID: ${{ inputs.unity_service_account_key_id }} + INPUT_SECRET_KEY: ${{ inputs.unity_service_account_secret_key }} INPUT_UNITY_ORG_ID: ${{ inputs.unity_org_id }} INPUT_UNITY_PROJECT_ID: ${{ inputs.unity_project_id }} INPUT_BUILD_TARGET_ID: ${{ inputs.build_target_id }} @@ -128,7 +153,9 @@ runs: printf '%s' "${1:-}" | tr -d '\r\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } - AUTH_HEADER="$(trim "${INPUT_AUTHORIZATION_HEADER:-}")" + RAW_AUTH_HEADER="$(trim "${INPUT_AUTHORIZATION_HEADER:-}")" + KEY_ID="$(trim "${INPUT_KEY_ID:-}")" + SECRET_KEY="$(trim "${INPUT_SECRET_KEY:-}")" ORG_ID="$(trim "${INPUT_UNITY_ORG_ID:-}")" PROJ_ID="$(trim "${INPUT_UNITY_PROJECT_ID:-}")" TARGET_ID="$(trim "${INPUT_BUILD_TARGET_ID:-}")" @@ -138,17 +165,18 @@ runs: API_URL="$(trim "${INPUT_API_URL:-}")" API_URL="${API_URL%/}" - # Keep the credential out of the logs even if it was not passed as a secret. - if [ -n "$AUTH_HEADER" ]; then - echo "::add-mask::${AUTH_HEADER}" - fi + # Keep credentials out of the logs even if they were not passed as secrets. + for secret in "$RAW_AUTH_HEADER" "$SECRET_KEY"; do + if [ -n "$secret" ]; then + echo "::add-mask::${secret}" + fi + done # Fail fast on missing required inputs instead of sending a doomed request. for pair in \ "unity_org_id:${ORG_ID}" \ "unity_project_id:${PROJ_ID}" \ "build_target_id:${TARGET_ID}" \ - "authorization_header:${AUTH_HEADER}" \ "api_url:${API_URL}" do name="${pair%%:*}" @@ -159,13 +187,44 @@ runs: fi done - # Accept a bare credential as well as a full header line. - case "$AUTH_HEADER" in - *:*) ;; - *) - echo "::warning ::'authorization_header' has no header name; assuming 'Authorization: '." - AUTH_HEADER="Authorization: ${AUTH_HEADER}" - echo "::add-mask::${AUTH_HEADER}" + # API v2 authenticates with HTTP Basic using a service account key ID and + # secret. `authorization_header` remains supported for callers that can + # only supply a pre-built header, but the two are mutually exclusive so a + # half-finished migration fails loudly instead of using stale credentials. + if [ -n "$KEY_ID" ] || [ -n "$SECRET_KEY" ]; then + if [ -n "$RAW_AUTH_HEADER" ]; then + echo "::error ::Provide either 'unity_service_account_key_id'/'unity_service_account_secret_key' or 'authorization_header', not both." + exit 1 + fi + if [ -z "$KEY_ID" ] || [ -z "$SECRET_KEY" ]; then + echo "::error ::'unity_service_account_key_id' and 'unity_service_account_secret_key' must be set together." + exit 1 + fi + # `base64` wraps long lines on GNU coreutils, so strip the newlines. + BASIC_CREDENTIAL="$(printf '%s:%s' "$KEY_ID" "$SECRET_KEY" | base64 | tr -d '\r\n')" + echo "::add-mask::${BASIC_CREDENTIAL}" + AUTH_HEADER="Authorization: Basic ${BASIC_CREDENTIAL}" + elif [ -n "$RAW_AUTH_HEADER" ]; then + AUTH_HEADER="$RAW_AUTH_HEADER" + # Accept a bare credential as well as a full header line. + case "$AUTH_HEADER" in + *:*) ;; + *) + echo "::warning ::'authorization_header' has no header name; assuming 'Authorization: '." + AUTH_HEADER="Authorization: ${AUTH_HEADER}" + ;; + esac + else + echo "::error ::No credentials supplied. Set 'unity_service_account_key_id' and 'unity_service_account_secret_key' (recommended), or 'authorization_header'." + exit 1 + fi + echo "::add-mask::${AUTH_HEADER}" + + # Unity removes API v1 on 2026-12-21; warn rather than fail so existing + # workflows keep running until then. + case "$API_URL" in + *build-api.cloud.unity3d.com*) + echo "::warning ::'api_url' points at Unity Build Automation API v1, which Unity removes on 2026-12-21. Drop the 'api_url' override to use v2 (https://build-automation.services.api.unity.com/v2) and switch to service account credentials." ;; esac @@ -222,13 +281,13 @@ runs: break fi if [ "$attempt" -lt 3 ]; then - echo "::warning ::Could not connect to the Unity Cloud Build API (curl exit ${CURL_RC}); retrying (attempt ${attempt}/3)." + echo "::warning ::Could not connect to the Unity Build Automation API (curl exit ${CURL_RC}); retrying (attempt ${attempt}/3)." sleep $((attempt * 2)) fi done if [ "$CURL_RC" -ne 0 ]; then - echo "::error ::Request to the Unity Cloud Build API failed (curl exit ${CURL_RC}). No build was triggered." + echo "::error ::Request to the Unity Build Automation API failed (curl exit ${CURL_RC}). No build was triggered." exit 1 fi @@ -236,34 +295,55 @@ runs: echo "::debug ::HTTP ${HTTP_CODE} $(tr -d '\r\n' < "$RESPONSE_FILE")" fi - # The API returns a single-element array; tolerate a bare object too. + # A successful trigger returns a single-element array (HTTP 202 on v2); + # tolerate a bare object too. Errors are bare objects on both versions. FIRST_ENTRY='if type == "array" then (.[0] // {}) else . end' if [[ ! "$HTTP_CODE" =~ ^2 ]]; then - ERROR_MSG="$(jq -r "${FIRST_ENTRY} | (.error // .message // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" + # v2 reports errors in the RFC 7807 shape (detail/title/requestId); v1 + # used error/message. Read whichever the response carries. + ERROR_MSG="$(jq -r "${FIRST_ENTRY} | (.detail // .error // .message // .title // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" + REQUEST_ID="$(jq -r "${FIRST_ENTRY} | (.requestId // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" if [ -z "$ERROR_MSG" ]; then # Not JSON (or no message field) — fall back to a truncated raw body. ERROR_MSG="$(head -c 500 "$RESPONSE_FILE" | tr -d '\r\n')" fi - echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}" + if [ -n "$REQUEST_ID" ]; then + # Unity support asks for this when investigating a failed request. + ERROR_MSG="${ERROR_MSG} (requestId: ${REQUEST_ID})" + fi + if [ "$HTTP_CODE" = '401' ] || [ "$HTTP_CODE" = '403' ]; then + echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}. API v2 requires a Unity service account key and secret with a Build Automation role; the legacy Build Automation API key is not accepted." + else + echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}" + fi exit 1 fi BUILD_ID="$(jq -r "${FIRST_ENTRY} | (.build // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" PLATFORM_OUT="$(jq -r "${FIRST_ENTRY} | (.platform // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" + BUILD_STATUS="$(jq -r "${FIRST_ENTRY} | (.buildStatus // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" if [ -z "$BUILD_ID" ]; then - echo "::error ::Unity Cloud Build returned HTTP ${HTTP_CODE} but no build ID: $(head -c 500 "$RESPONSE_FILE" | tr -d '\r\n')" + # A 2xx can still carry a per-target 'error' — e.g. a build for this + # target is already running — in which case no build was started. + ENTRY_ERROR="$(jq -r "${FIRST_ENTRY} | (.error // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" + if [ -n "$ENTRY_ERROR" ]; then + echo "::error ::Unity Build Automation did not start a build: ${ENTRY_ERROR}" + else + echo "::error ::Unity Build Automation returned HTTP ${HTTP_CODE} but no build ID: $(head -c 500 "$RESPONSE_FILE" | tr -d '\r\n')" + fi exit 1 fi { echo "build_id=${BUILD_ID}" echo "platform=${PLATFORM_OUT}" + echo "build_status=${BUILD_STATUS}" } >> "$GITHUB_OUTPUT" { - echo "### Unity Cloud Build triggered" + echo "### Unity Build Automation build triggered" echo "" echo "| Field | Value |" echo "| --- | --- |" @@ -271,6 +351,7 @@ runs: echo "| Build target | \`${TARGET_ID}\` |" echo "| Branch | \`${BRANCH:-}\` |" echo "| Platform | \`${PLATFORM_OUT:-unknown}\` |" + echo "| Status | \`${BUILD_STATUS:-unknown}\` |" echo "| Clean | \`${CLEAN_FLAG}\` |" } >> "${GITHUB_STEP_SUMMARY:-/dev/null}" From b61c8e126a5d7f7799b0ec021f1fe022842a6ce1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 13:08:46 +0000 Subject: [PATCH 2/2] Drop API v1 compatibility now that the action ships a new major version Since this releases under a new version tag, the v1 fallbacks are dead weight. Remove them so there is exactly one supported configuration: - Delete the `authorization_header` input. API v2 only accepts service account credentials, so the pre-built-header escape hatch could only carry a value the caller had to hand-encode anyway. The key ID and secret inputs are now plain required inputs, validated alongside the org/project/target IDs, which drops the mutually-exclusive/paired-input checks entirely. - Reject a v1 base URL instead of warning. v1 does not accept service account credentials, so pointing `api_url` at it can only produce a confusing 401; the error names the older release to pin instead. - Parse errors as RFC 7807 only (`detail`/`title`), dropping v1's `error`/`message`. The 2xx-without-a-build-ID path still reads `error`, which v2's GetBuildResponse defines for an already-running target. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016Mxrx7ufpE3maxhLfSYwPA --- README.md | 35 ++++++++------------- action.yml | 91 +++++++++++++++--------------------------------------- 2 files changed, 38 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 04bea49..f11e92d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A reusable GitHub Action that triggers a [Unity Build Automation](https://docs.u The action does not check out or read your repository — it only calls the Unity Build Automation REST API — so it is safe to run before, after, or without `actions/checkout`. -It targets **API v2** by default. Unity removes API v1 on **2026-12-21**; see [Migrating from API v1](#migrating-from-api-v1) if you are upgrading from an earlier release of this action. +It speaks **API v2**. Unity removes API v1 on **2026-12-21**; if you are upgrading from an earlier release of this action, see [Migrating from API v1](#migrating-from-api-v1) — the credentials change. ## Authentication @@ -25,16 +25,13 @@ Pass them as `unity_service_account_key_id` and `unity_service_account_secret_ke | `unity_org_id` | Yes | The Unity Organization ID (used in the endpoint `orgs/{unity_org_id}`). | — | | `unity_project_id` | Yes | The Unity Build Automation Project ID. | — | | `build_target_id` | Yes | The Build Target ID within the Unity Build Automation project. | — | -| `unity_service_account_key_id` | Yes\* | Key ID of a Unity service account with a Build Automation role. | `''` | -| `unity_service_account_secret_key` | Yes\* | Secret key of that service account. Pass as a secret. | `''` | -| `authorization_header` | No\* | Pre-built header, e.g. `Authorization: Basic `. A bare token also works and gets the `Authorization: ` prefix added. | `''` | +| `unity_service_account_key_id` | Yes | Key ID of a Unity service account with a Build Automation role. | — | +| `unity_service_account_secret_key` | Yes | Secret key of that service account. Pass as a secret. | — | | `branch` | No | Git branch to build. When empty, the branch configured on the build target is used. | `''` | | `clean` | No | Whether to perform a clean build. Accepts `true`/`false` (also `1`/`0`, `yes`/`no`, any casing). | `false` | | `platform` | No | Build platform override (e.g. `standalonewindows64`). Sent only when set. | `''` | | `machine_type_label` | No | Machine type label (e.g. `win_premium_v1`). Sent as `machineTypeLabel`, only when set. | `''` | -| `api_url` | No | Base URL of the API, including the version segment. | `https://build-automation.services.api.unity.com/v2` | - -\* Supply **either** the service account pair **or** `authorization_header` — not both. Providing both, or only one half of the pair, fails the step before any request is sent. +| `api_url` | No | Base URL of the API, including the version segment. v2 only — a v1 base URL is rejected. | `https://build-automation.services.api.unity.com/v2` | `platform` and `machine_type_label` are omitted from the request when left empty, so the build target's own configuration applies. Set them only if you want to override the target. @@ -124,7 +121,7 @@ jobs: ## Migrating from API v1 -Unity deprecated Build Automation API v1 and **removes it on 2026-12-21**. This action calls v2 by default. If you used an earlier release, two things change in your workflow: +Unity deprecated Build Automation API v1 and **removes it on 2026-12-21**. This action calls v2 and no longer supports v1, so upgrading from an earlier release is a breaking change. | | v1 (before) | v2 (now) | | --- | --- | --- | @@ -135,7 +132,9 @@ Unity deprecated Build Automation API v1 and **removes it on 2026-12-21**. This The endpoint path (`/orgs/{org}/projects/{project}/buildtargets/{target}/builds`) and the request fields this action sends (`clean`, `delay`, `branch`, `platform`, `machineTypeLabel`) are unchanged. -To migrate, replace the `authorization_header` input with the service account pair: +### What you need to change + +The `authorization_header` input is **removed**. Create a [service account](#authentication) and replace it with the key pair: ```diff - authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} @@ -143,20 +142,11 @@ To migrate, replace the `authorization_header` input with the service account pa + unity_service_account_secret_key: ${{ secrets.UNITY_SA_SECRET_KEY }} ``` -Reusing your old API key will fail with `HTTP 401`/`403` — v2 only accepts service account credentials. A `403` with valid credentials usually means the service account has no Build Automation role on the project. The action calls both cases out explicitly in its error message and includes the API's `requestId`, which Unity support asks for. - -### Staying on v1 temporarily - -Point `api_url` back at v1 until you have a service account in place. The action logs a deprecation warning when you do: +Your old API key cannot be reused in any form — v2 only accepts service account credentials. A `403` with otherwise valid credentials usually means the service account has no Build Automation role on the project. The action calls both cases out in its error message and includes the API's `requestId`, which Unity support asks for. -```yaml - with: - api_url: https://build-api.cloud.unity3d.com/api/v1 - authorization_header: ${{ secrets.UNITY_AUTH_HEADER }} - # ... -``` +Pointing `api_url` at a v1 URL fails immediately with a message telling you to pin an older release of this action, rather than sending a request that could only return `401`. -Both versions are parsed by the same code: the response parser accepts a single-element array (`[{ "build": 123, … }]`) or a bare object, and reads error text from `detail`, `error`, `message`, or `title`, whichever the response carries. If a response has no build ID, the action fails with the API's own explanation (e.g. a build is already running for that target) rather than silently reporting an empty one. +If you are not ready to create a service account, stay on the previous release of this action until you are; v1 keeps working until Unity removes it. Reference: [Build Automation API v2](https://docs.unity.com/en-us/oas-build-automation-client/2.0.0) · [Unity's v1 → v2 migration guide](https://support.unity.com/hc/en-us/articles/51178448254356-How-do-I-migrate-my-Unity-Build-Automation-UBA-REST-API-calls-from-V1-to-V2) @@ -165,7 +155,8 @@ Reference: [Build Automation API v2](https://docs.unity.com/en-us/oas-build-auto - **Non-idempotent by design.** Triggering a build is not idempotent, so a request that reached Unity is never retried. Only DNS and connection failures — where nothing reached the server — are retried (up to 3 attempts). This avoids double-charging build minutes. - **Secrets stay in one step.** Credentials are passed to a single step's environment and masked in the logs — the secret key, the base64 credential derived from it, and the assembled header are all masked. Nothing is written to `$GITHUB_ENV`, so they do not leak into other steps of the calling job. - **The response body is not logged** by default. Re-run the workflow with debug logging enabled to see it. -- **Required inputs are validated before the request** — an empty org/project/target ID, a missing or ambiguous credential, or an invalid `clean` value fails immediately instead of producing a confusing API error. +- **Required inputs are validated before the request** — an empty org/project/target ID, a missing credential, or an invalid `clean` value fails immediately instead of producing a confusing API error. +- **A 2xx without a build ID is still a failure.** v2 can return `202` carrying only an `error` — typically because a build is already running for that target. The action reports the API's own explanation instead of an empty `build_id`. The parser accepts both the documented single-element array and a bare object. - **`curl` and `jq`** are used, and are preinstalled on GitHub-hosted runners. On runners that lack them, the action installs them via `apt-get` or `brew`, and fails with a clear message if neither is available. `base64` (from coreutils) is also required and is present on any runner that has a shell. ## Development diff --git a/action.yml b/action.yml index 2cca950..a87d0d9 100644 --- a/action.yml +++ b/action.yml @@ -18,25 +18,13 @@ inputs: description: >- Key ID of a Unity service account with a Build Automation role. Combined with `unity_service_account_secret_key` into the HTTP Basic credential - required by API v2. Required unless `authorization_header` is supplied. - required: false - default: '' + required by API v2. The legacy Build Automation API key is not accepted. + required: true unity_service_account_secret_key: description: >- Secret key of the Unity service account. Pass this as a secret; it is masked in the logs. - required: false - default: '' - authorization_header: - description: >- - Pre-built Authorization header, e.g. "Authorization: Basic ". A - bare token is also accepted and gets the "Authorization: " prefix added - automatically. Use this only if you cannot pass the service account key - and secret separately; it is mutually exclusive with them. Note that API - v2 rejects the legacy Build Automation API key — the token must be - base64(":") of a service account. - required: false - default: '' + required: true branch: description: >- Git branch to build. When empty, the branch configured on the build target @@ -63,9 +51,7 @@ inputs: api_url: description: >- Base URL of the Unity Build Automation API, including the version segment. - Defaults to API v2. The v1 base URL - (https://build-api.cloud.unity3d.com/api/v1) still works until Unity - removes it on 2026-12-21, but the action warns when it is used. + This action speaks API v2 only; the v1 base URL is rejected. required: false default: 'https://build-automation.services.api.unity.com/v2' outputs: @@ -134,7 +120,6 @@ runs: # the script, so that values containing shell metacharacters cannot be # executed as code. env: - INPUT_AUTHORIZATION_HEADER: ${{ inputs.authorization_header }} INPUT_KEY_ID: ${{ inputs.unity_service_account_key_id }} INPUT_SECRET_KEY: ${{ inputs.unity_service_account_secret_key }} INPUT_UNITY_ORG_ID: ${{ inputs.unity_org_id }} @@ -153,7 +138,6 @@ runs: printf '%s' "${1:-}" | tr -d '\r\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } - RAW_AUTH_HEADER="$(trim "${INPUT_AUTHORIZATION_HEADER:-}")" KEY_ID="$(trim "${INPUT_KEY_ID:-}")" SECRET_KEY="$(trim "${INPUT_SECRET_KEY:-}")" ORG_ID="$(trim "${INPUT_UNITY_ORG_ID:-}")" @@ -165,18 +149,18 @@ runs: API_URL="$(trim "${INPUT_API_URL:-}")" API_URL="${API_URL%/}" - # Keep credentials out of the logs even if they were not passed as secrets. - for secret in "$RAW_AUTH_HEADER" "$SECRET_KEY"; do - if [ -n "$secret" ]; then - echo "::add-mask::${secret}" - fi - done + # Keep the credential out of the logs even if it was not passed as a secret. + if [ -n "$SECRET_KEY" ]; then + echo "::add-mask::${SECRET_KEY}" + fi # Fail fast on missing required inputs instead of sending a doomed request. for pair in \ "unity_org_id:${ORG_ID}" \ "unity_project_id:${PROJ_ID}" \ "build_target_id:${TARGET_ID}" \ + "unity_service_account_key_id:${KEY_ID}" \ + "unity_service_account_secret_key:${SECRET_KEY}" \ "api_url:${API_URL}" do name="${pair%%:*}" @@ -187,44 +171,19 @@ runs: fi done - # API v2 authenticates with HTTP Basic using a service account key ID and - # secret. `authorization_header` remains supported for callers that can - # only supply a pre-built header, but the two are mutually exclusive so a - # half-finished migration fails loudly instead of using stale credentials. - if [ -n "$KEY_ID" ] || [ -n "$SECRET_KEY" ]; then - if [ -n "$RAW_AUTH_HEADER" ]; then - echo "::error ::Provide either 'unity_service_account_key_id'/'unity_service_account_secret_key' or 'authorization_header', not both." - exit 1 - fi - if [ -z "$KEY_ID" ] || [ -z "$SECRET_KEY" ]; then - echo "::error ::'unity_service_account_key_id' and 'unity_service_account_secret_key' must be set together." - exit 1 - fi - # `base64` wraps long lines on GNU coreutils, so strip the newlines. - BASIC_CREDENTIAL="$(printf '%s:%s' "$KEY_ID" "$SECRET_KEY" | base64 | tr -d '\r\n')" - echo "::add-mask::${BASIC_CREDENTIAL}" - AUTH_HEADER="Authorization: Basic ${BASIC_CREDENTIAL}" - elif [ -n "$RAW_AUTH_HEADER" ]; then - AUTH_HEADER="$RAW_AUTH_HEADER" - # Accept a bare credential as well as a full header line. - case "$AUTH_HEADER" in - *:*) ;; - *) - echo "::warning ::'authorization_header' has no header name; assuming 'Authorization: '." - AUTH_HEADER="Authorization: ${AUTH_HEADER}" - ;; - esac - else - echo "::error ::No credentials supplied. Set 'unity_service_account_key_id' and 'unity_service_account_secret_key' (recommended), or 'authorization_header'." - exit 1 - fi + # API v2 authenticates with HTTP Basic over base64(":"). + # `base64` wraps long lines on GNU coreutils, so strip the newlines. + BASIC_CREDENTIAL="$(printf '%s:%s' "$KEY_ID" "$SECRET_KEY" | base64 | tr -d '\r\n')" + echo "::add-mask::${BASIC_CREDENTIAL}" + AUTH_HEADER="Authorization: Basic ${BASIC_CREDENTIAL}" echo "::add-mask::${AUTH_HEADER}" - # Unity removes API v1 on 2026-12-21; warn rather than fail so existing - # workflows keep running until then. + # This action speaks v2 only, and v1 does not accept service account + # credentials, so a v1 base URL can only ever produce a confusing 401. case "$API_URL" in *build-api.cloud.unity3d.com*) - echo "::warning ::'api_url' points at Unity Build Automation API v1, which Unity removes on 2026-12-21. Drop the 'api_url' override to use v2 (https://build-automation.services.api.unity.com/v2) and switch to service account credentials." + echo "::error ::'api_url' points at Unity Build Automation API v1, which this action no longer supports (Unity removes it on 2026-12-21). Drop the 'api_url' override to use v2, or pin an older release of this action." + exit 1 ;; esac @@ -295,14 +254,14 @@ runs: echo "::debug ::HTTP ${HTTP_CODE} $(tr -d '\r\n' < "$RESPONSE_FILE")" fi - # A successful trigger returns a single-element array (HTTP 202 on v2); - # tolerate a bare object too. Errors are bare objects on both versions. + # A successful trigger returns HTTP 202 with a single-element array; + # tolerate a bare object too. Errors are always bare objects. FIRST_ENTRY='if type == "array" then (.[0] // {}) else . end' if [[ ! "$HTTP_CODE" =~ ^2 ]]; then - # v2 reports errors in the RFC 7807 shape (detail/title/requestId); v1 - # used error/message. Read whichever the response carries. - ERROR_MSG="$(jq -r "${FIRST_ENTRY} | (.detail // .error // .message // .title // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" + # v2 reports errors in the RFC 7807 shape: 'detail' carries the + # explanation, 'title' the status text, 'requestId' the trace ID. + ERROR_MSG="$(jq -r "${FIRST_ENTRY} | (.detail // .title // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" REQUEST_ID="$(jq -r "${FIRST_ENTRY} | (.requestId // empty)" "$RESPONSE_FILE" 2>/dev/null || true)" if [ -z "$ERROR_MSG" ]; then # Not JSON (or no message field) — fall back to a truncated raw body. @@ -313,7 +272,7 @@ runs: ERROR_MSG="${ERROR_MSG} (requestId: ${REQUEST_ID})" fi if [ "$HTTP_CODE" = '401' ] || [ "$HTTP_CODE" = '403' ]; then - echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}. API v2 requires a Unity service account key and secret with a Build Automation role; the legacy Build Automation API key is not accepted." + echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}. Check that the service account key and secret are correct and that the account has a Build Automation role on this project; the legacy Build Automation API key is not accepted by API v2." else echo "::error ::Failed to trigger build: HTTP ${HTTP_CODE} – ${ERROR_MSG:-}" fi