diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b25313f9..bf7c7a1e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -369,6 +369,106 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Signs the UPM package with Unity's UPM CLI using service-account credentials, + # then attaches the signed .tgz to the just-created GitHub Release. + # + # OpenUPM picks up the signed tarball when its listing has `trackingMode: githubRelease` + # (see docs/openupm-signing.md for the cross-repo OpenUPM listing change). Until then, + # OpenUPM continues to pack from git and ships unsigned packages — the asset attached + # here is still uploaded for verification. + # + # Required repo secrets (configure via `gh secret set --repo IvanMurzak/Unity-MCP `): + # - UPM_SERVICE_ACCOUNT_KEY_ID + # - UPM_SERVICE_ACCOUNT_KEY_SECRET + # - UPM_ORG_ID + # If any are missing the job soft-fails with `continue-on-error: true` so an absent + # signing key never blocks the release pipeline (the upstream release flow still ships). + # See https://openupm.com/docs/signing-upm-packages.html for the procedure. + sign-and-publish-upm: + runs-on: ubuntu-latest + needs: release-unity-plugin + if: needs.release-unity-plugin.outputs.success == 'true' + continue-on-error: true + env: + UPM_SERVICE_ACCOUNT_KEY_ID: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_ID }} + UPM_SERVICE_ACCOUNT_KEY_SECRET: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_SECRET }} + UPM_ORG_ID: ${{ secrets.UPM_ORG_ID }} + PACKAGE_DIR: Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp + DIST_DIR: /tmp/signed-upm-dist + steps: + - name: Verify signing secrets are configured + run: | + missing=() + [ -z "$UPM_SERVICE_ACCOUNT_KEY_ID" ] && missing+=("UPM_SERVICE_ACCOUNT_KEY_ID") + [ -z "$UPM_SERVICE_ACCOUNT_KEY_SECRET" ] && missing+=("UPM_SERVICE_ACCOUNT_KEY_SECRET") + [ -z "$UPM_ORG_ID" ] && missing+=("UPM_ORG_ID") + if [ "${#missing[@]}" -ne 0 ]; then + printf '::warning::UPM signing secrets are not configured (%s); skipping signing. See docs/openupm-signing.md.\n' "${missing[*]}" + # Exit non-zero so subsequent steps short-circuit; continue-on-error at the + # job level keeps the overall release pipeline green. + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log package metadata + run: | + package_name="$(jq -r '.name' "$PACKAGE_DIR/package.json")" + package_version="$(jq -r '.version' "$PACKAGE_DIR/package.json")" + + printf 'Package name: %s\n' "$package_name" + printf 'Package version: %s\n' "$package_version" + + - name: Install Unity UPM CLI + run: | + curl -fsSL https://cdn.packages.unity.com/upm-cli/install.sh -o install.sh + bash install.sh + echo "$HOME/.upm/bin" >> "$GITHUB_PATH" + + - name: Verify Unity UPM CLI + run: upm --help + + - name: Sign package + run: | + mkdir -p "$DIST_DIR" + upm pack "./$PACKAGE_DIR" --organization-id "$UPM_ORG_ID" --destination "$DIST_DIR" + + - name: Verify signed package contains attestation + run: | + shopt -s nullglob + archives=("$DIST_DIR"/*.tgz "$DIST_DIR"/*.tar.gz) + if [ "${#archives[@]}" -ne 1 ]; then + printf 'Expected exactly one signed package archive, found %s: %s\n' "${#archives[@]}" "${archives[*]:-}" >&2 + exit 1 + fi + + archive="${archives[0]}" + archive_basename="$(basename "$archive")" + # OpenUPM consumes the asset via the `githubReleaseAssetName: 'com.ivanmurzak.unity.mcp-'` + # prefix documented in docs/openupm-signing.md. Enforce the contract here so a future + # `upm pack` naming change fails CI loudly instead of silently breaking OpenUPM pickup. + if [[ "$archive_basename" != com.ivanmurzak.unity.mcp-* ]]; then + printf 'Signed archive basename %q does not begin with the OpenUPM-expected prefix com.ivanmurzak.unity.mcp- (see docs/openupm-signing.md)\n' "$archive_basename" >&2 + exit 1 + fi + + archive_entries="$(tar -tzf "$archive")" + grep -qx 'package/package.json' <<<"$archive_entries" + grep -qx 'package/.attestation.p7m' <<<"$archive_entries" + + echo "PACKAGE_ARCHIVE=$archive" >> "$GITHUB_ENV" + printf 'Signed archive: %s\n' "$archive_basename" + tar -xOzf "$archive" package/package.json | jq '{name, version}' + + - name: Attach signed package to release + uses: softprops/action-gh-release@v2 + with: + files: ${{ env.PACKAGE_ARCHIVE }} + tag_name: ${{ needs.release-unity-plugin.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish_discord: runs-on: ubuntu-latest needs: diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 3eae84329..f087469fc 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,6 +16,7 @@ - **Development** - [Development Guide](/dev/Development.md) - [Version Management](/version-management.md) + - [OpenUPM Package Signing](/openupm-signing.md) - [Architecture](/#how-unity-mcp-architecture-works) - **Languages** diff --git a/docs/openupm-signing.md b/docs/openupm-signing.md new file mode 100644 index 000000000..907a3791b --- /dev/null +++ b/docs/openupm-signing.md @@ -0,0 +1,136 @@ +# OpenUPM Package Signing + +Unity 6.3 introduced a package-signature check that surfaces a trust warning for +unsigned UPM packages installed from third-party registries (including OpenUPM). +This document describes how `IvanMurzak/Unity-MCP` signs its +`com.ivanmurzak.unity.mcp` package so the warning no longer appears in Unity 6.3+. + +Tracks issue [#414](https://github.com/IvanMurzak/Unity-MCP/issues/414). + +## How signing works + +OpenUPM does **not** sign packages on behalf of authors — each package author runs +the signing flow in their own CI using a Unity organization's service account. The +signed `.tgz` is uploaded as a GitHub Release asset, and OpenUPM picks it up when +the package's listing has `trackingMode: githubRelease`. + +References: +- +- +- Reference workflow / repo layout: + +## What this repo ships + +The signing step is implemented as the `sign-and-publish-upm` job in +[`.github/workflows/release.yml`](../.github/workflows/release.yml). It runs on +every successful version-bump release (the same trigger as `publish-mcp-server` +and `publish-unity-installer`), packs the package at +`Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/` with Unity's UPM CLI, and +attaches the resulting signed `.tgz` to the release tag. + +The job is declared `continue-on-error: true` and exits early with a warning if +the required Unity-org secrets are not configured — so until the secrets are in +place the existing release flow continues to ship unchanged. + +## One-time setup (repository owner) + +These steps land outside this PR — they are operational, not code changes. + +### 1. Create a Unity organization service account + +A Unity organization is required to obtain UPM signing credentials (the +individual / personal Unity license cannot sign packages). + +1. Go to the [Unity Cloud Dashboard](https://cloud.unity.com/) and either create + an organization or use an existing one you own. +2. Inside the organization settings, create a service account dedicated to + package signing. +3. Grant the service account the **package signing** permission for the + organization. +4. Generate a service-account key — record the `Key ID`, the `Key Secret`, and + the organization's `Org ID`. The secret is shown only once. + +### 2. Add the three GitHub repository secrets + +In this repo's Settings → Secrets and variables → Actions, add: + +| Secret name | Value | +| --------------------------------- | ------------------------------------ | +| `UPM_SERVICE_ACCOUNT_KEY_ID` | Service account key ID | +| `UPM_SERVICE_ACCOUNT_KEY_SECRET` | Service account key secret | +| `UPM_ORG_ID` | Unity organization ID | + +CLI equivalent: + +```bash +gh secret set UPM_SERVICE_ACCOUNT_KEY_ID --repo IvanMurzak/Unity-MCP +gh secret set UPM_SERVICE_ACCOUNT_KEY_SECRET --repo IvanMurzak/Unity-MCP +gh secret set UPM_ORG_ID --repo IvanMurzak/Unity-MCP +``` + +### 3. File the OpenUPM listing change + +OpenUPM's package listing for `com.ivanmurzak.unity.mcp` currently has +`trackingMode: git`, which makes OpenUPM pack and serve unsigned tarballs from +the repository's git tags. To make OpenUPM serve the signed tarball that the +workflow now uploads, the listing must be flipped to `trackingMode: githubRelease`. + +The listing lives in the [openupm/openupm](https://github.com/openupm/openupm) +repository at `data/packages/com.ivanmurzak.unity.mcp.yml`. Open a PR there +changing: + +```yaml +trackingMode: git +``` + +to: + +```yaml +trackingMode: githubRelease +``` + +Per the OpenUPM blog, switch `trackingMode` to `githubRelease` **before** the +first signed release ships, so OpenUPM does not race-publish the unsigned git +tag in parallel. + +Also set `githubReleaseAssetName` so OpenUPM picks the signed tarball by +filename prefix rather than guessing from the asset list. The release already +ships `.unitypackage` and multiple server `.zip` assets, and may add more +`.tgz` assets later (e.g. signing a second package), so the prefix guard +prevents a future-breaking failure mode: + +```yaml +githubReleaseAssetName: 'com.ivanmurzak.unity.mcp-' +``` + +## Verifying signing worked + +After the next release that runs with the secrets in place: + +1. Go to the [release page](https://github.com/IvanMurzak/Unity-MCP/releases) + for the new version and confirm a `com.ivanmurzak.unity.mcp-.tgz` + asset is attached alongside the existing `.unitypackage` and server `.zip`s. +2. Inspect the tarball locally to confirm it contains the signing attestation: + + ```bash + curl -fsSL -o package.tgz \ + https://github.com/IvanMurzak/Unity-MCP/releases/download//com.ivanmurzak.unity.mcp-.tgz + tar -tzf package.tgz | grep '\.attestation\.p7m$' + # expected: package/.attestation.p7m + ``` + +3. Once the OpenUPM listing change merges, install the package in Unity 6.3+ + from OpenUPM and confirm the unsigned-package warning no longer appears. + +## Troubleshooting + +- **Job is skipped with warning "UPM signing secrets are not configured"** — + expected when the three secrets are not yet set. Complete the + "One-time setup" steps above. +- **`upm pack` fails with an authentication error** — the service account key + is invalid or lacks the package-signing permission. Regenerate the key in the + Unity org dashboard and re-set the GitHub secrets. +- **The release contains the `.tgz` but Unity 6.3 still shows the warning** — + the OpenUPM listing is still on `trackingMode: git` (OpenUPM is serving the + unsigned git-packed version, not the release asset). File the + `openupm/openupm` PR described above.